@cosmonapse/sdk 0.1.2 → 0.1.4

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/dist/index.js CHANGED
@@ -55,7 +55,13 @@ var SignalType = {
55
55
  IMPRINT: "IMPRINT",
56
56
  IMPRINTED: "IMPRINTED",
57
57
  // Discovery [C]
58
- DISCOVER: "DISCOVER"
58
+ DISCOVER: "DISCOVER",
59
+ // Workflow control [C] - cooperative cancellation of a whole trace.
60
+ // STOP is broadcast on the trace; every Dendrite filters by trace_id,
61
+ // cancels its in-flight work + engram I/O, optionally rolls back Engram
62
+ // writes via the saga journal, then acks with STOPPED.
63
+ STOP: "STOP",
64
+ STOPPED: "STOPPED"
59
65
  };
60
66
  var AXON_TYPES = /* @__PURE__ */ new Set([
61
67
  SignalType.AGENT_OUTPUT,
@@ -92,7 +98,11 @@ var SYNAPSE_TYPES = /* @__PURE__ */ new Set([
92
98
  SignalType.RECALL,
93
99
  SignalType.RECALLED,
94
100
  SignalType.IMPRINT,
95
- SignalType.IMPRINTED
101
+ SignalType.IMPRINTED,
102
+ // Workflow control - STOP is orchestrator-gated (see Dendrite role gate);
103
+ // STOPPED is the per-Dendrite ack.
104
+ SignalType.STOP,
105
+ SignalType.STOPPED
96
106
  ]);
97
107
  function normalizeDirected(d) {
98
108
  if (d === null || d === void 0) return null;
@@ -125,6 +135,12 @@ function createSignal(input) {
125
135
  return signal;
126
136
  }
127
137
  function validateSignal(signal) {
138
+ const major = signal.v.split(".", 1)[0];
139
+ if (major !== "1") {
140
+ throw new Error(
141
+ `unsupported protocol version '${signal.v}': this SDK speaks major version 1 (accepts '1' or '1.x')`
142
+ );
143
+ }
128
144
  if (!signal.id.startsWith("evt_")) {
129
145
  throw new Error(`Signal id must start with 'evt_', got: ${signal.id}`);
130
146
  }
@@ -162,6 +178,7 @@ function taskSignal(args) {
162
178
  const payload = { input: args.input };
163
179
  if (args.contextRef) payload["context_ref"] = args.contextRef;
164
180
  if (args.capabilities) payload["capabilities"] = args.capabilities;
181
+ if (args.finalize) payload["finalize"] = true;
165
182
  return createSignal({
166
183
  type: SignalType.TASK,
167
184
  trace_id: args.traceId ?? newTraceId(),
@@ -258,9 +275,10 @@ function errorSignal(args) {
258
275
  }
259
276
  function registerSignal(args) {
260
277
  const caps = args.capabilities ?? args.directed?.capabilities ?? [];
261
- const payload = { capabilities: caps };
278
+ const role = args.role ?? (args.engram ? "engram" : "neuron");
279
+ const payload = { role, capabilities: caps };
262
280
  if (args.version) payload["version"] = args.version;
263
- if (args.engram) payload["engram"] = true;
281
+ if (args.engram || role === "engram") payload["engram"] = true;
264
282
  return createSignal({
265
283
  type: SignalType.REGISTER,
266
284
  trace_id: newTraceId(),
@@ -325,6 +343,32 @@ function bidSignal(args) {
325
343
  meta: args.meta ?? {}
326
344
  });
327
345
  }
346
+ function taskAwardedSignal(args) {
347
+ const payload = { input: args.input };
348
+ if (args.winningBid !== void 0) payload["winning_bid"] = args.winningBid;
349
+ if (args.contextRef !== void 0) payload["context_ref"] = args.contextRef;
350
+ if (args.finalize) payload["finalize"] = true;
351
+ return createSignal({
352
+ type: SignalType.TASK_AWARDED,
353
+ trace_id: args.traceId,
354
+ parent_id: args.parentId,
355
+ directed: args.directed ?? null,
356
+ payload,
357
+ meta: args.meta ?? {}
358
+ });
359
+ }
360
+ function taskDeclinedSignal(args) {
361
+ const payload = {};
362
+ if (args.reason !== void 0) payload["reason"] = args.reason;
363
+ return createSignal({
364
+ type: SignalType.TASK_DECLINED,
365
+ trace_id: args.traceId,
366
+ parent_id: args.parentId,
367
+ directed: args.directed ?? null,
368
+ payload,
369
+ meta: args.meta ?? {}
370
+ });
371
+ }
328
372
  function critiqueSignal(args) {
329
373
  return createSignal({
330
374
  type: SignalType.CRITIQUE,
@@ -520,6 +564,237 @@ function imprintedSignal(args) {
520
564
  meta: args.meta ?? {}
521
565
  });
522
566
  }
567
+ function stopSignal(args) {
568
+ const payload = { rollback: Boolean(args.rollback) };
569
+ if (args.reason !== void 0) payload["reason"] = args.reason;
570
+ return createSignal({
571
+ type: SignalType.STOP,
572
+ trace_id: args.traceId,
573
+ parent_id: args.parentId ?? null,
574
+ directed: args.directed ?? null,
575
+ payload,
576
+ meta: args.meta ?? {}
577
+ });
578
+ }
579
+ function stoppedSignal(args) {
580
+ const payload = {
581
+ rolled_back: Boolean(args.rolledBack),
582
+ cancelled: args.cancelled ?? 0,
583
+ compensated: args.compensated ?? 0
584
+ };
585
+ if (args.node !== void 0) payload["node"] = args.node;
586
+ return createSignal({
587
+ type: SignalType.STOPPED,
588
+ trace_id: args.traceId,
589
+ parent_id: args.parentId ?? null,
590
+ directed: args.directed ?? null,
591
+ payload,
592
+ meta: args.meta ?? {}
593
+ });
594
+ }
595
+
596
+ // src/pathway.ts
597
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set([
598
+ SignalType.FINAL,
599
+ SignalType.ERROR
600
+ ]);
601
+ var WAIT_TYPES = /* @__PURE__ */ new Set([
602
+ SignalType.AGENT_OUTPUT,
603
+ SignalType.CLARIFICATION,
604
+ SignalType.PERMISSION,
605
+ SignalType.ERROR,
606
+ SignalType.FINAL
607
+ ]);
608
+ var SCOPE_TERMINAL_TYPES = /* @__PURE__ */ new Set([
609
+ SignalType.FINAL,
610
+ SignalType.ERROR,
611
+ SignalType.CLARIFICATION,
612
+ SignalType.PERMISSION
613
+ ]);
614
+ var PATHWAY_TYPES = new Set(
615
+ Object.values(SignalType).filter(
616
+ (t) => t !== SignalType.TASK && t !== SignalType.REGISTER && t !== SignalType.DEREGISTER && t !== SignalType.HEARTBEAT && t !== SignalType.DISCOVER
617
+ )
618
+ );
619
+ var PathwayClosedError = class extends Error {
620
+ constructor(message) {
621
+ super(message);
622
+ this.name = "PathwayClosedError";
623
+ }
624
+ };
625
+ var Pathway = class {
626
+ traceId;
627
+ parentId;
628
+ role;
629
+ scope;
630
+ scopeFilter;
631
+ onCloseHook;
632
+ handlers = /* @__PURE__ */ new Map();
633
+ waiters = [];
634
+ buffered = [];
635
+ closed_ = false;
636
+ // Async iteration: a pull queue of pending `next()` resolvers and a push
637
+ // queue of undelivered values. `null` is the close sentinel.
638
+ iterPush = [];
639
+ iterPull = [];
640
+ constructor(opts) {
641
+ const scope = opts.scope ?? "all";
642
+ if (scope !== "all" && scope !== "terminal") {
643
+ throw new Error(`scope must be 'all' or 'terminal', got '${scope}'`);
644
+ }
645
+ this.traceId = opts.traceId;
646
+ this.parentId = opts.parentId ?? null;
647
+ this.role = opts.role ?? "originator";
648
+ this.scope = scope;
649
+ this.scopeFilter = scope === "terminal" ? SCOPE_TERMINAL_TYPES : null;
650
+ this.onCloseHook = opts.onClose;
651
+ }
652
+ get closed() {
653
+ return this.closed_;
654
+ }
655
+ // -- consumer shape #1: wait ---------------------------------------
656
+ /** Resolve on the next AGENT_OUTPUT, CLARIFICATION, PERMISSION, ERROR or
657
+ * FINAL. Rejects with PathwayClosedError if the Pathway closes first, and
658
+ * with a TimeoutError-named Error if `timeoutMs` elapses. */
659
+ async wait(timeoutMs) {
660
+ return this.waitForTypes(WAIT_TYPES, timeoutMs);
661
+ }
662
+ /** Resolve on the next Signal of the given type. */
663
+ async waitFor(type, timeoutMs) {
664
+ return this.waitForTypes(/* @__PURE__ */ new Set([type]), timeoutMs);
665
+ }
666
+ async waitForTypes(types, timeoutMs) {
667
+ for (let i = 0; i < this.buffered.length; i++) {
668
+ const sig = this.buffered[i];
669
+ if (types.has(sig.type)) {
670
+ this.buffered.splice(i, 1);
671
+ return sig;
672
+ }
673
+ }
674
+ if (this.closed_) {
675
+ throw new PathwayClosedError(`Pathway for trace '${this.traceId}' is closed`);
676
+ }
677
+ return new Promise((resolve, reject) => {
678
+ const waiter = { types, resolve, reject, settled: false };
679
+ let timer = null;
680
+ const settle = (fn) => (a) => {
681
+ if (waiter.settled) return;
682
+ waiter.settled = true;
683
+ if (timer !== null) clearTimeout(timer);
684
+ this.waiters = this.waiters.filter((w) => w !== waiter);
685
+ fn(a);
686
+ };
687
+ waiter.resolve = settle(resolve);
688
+ waiter.reject = settle(reject);
689
+ if (timeoutMs !== void 0) {
690
+ timer = setTimeout(() => {
691
+ const err = new Error(
692
+ `Pathway.wait timed out after ${timeoutMs}ms on trace '${this.traceId}'`
693
+ );
694
+ err.name = "TimeoutError";
695
+ waiter.reject(err);
696
+ }, timeoutMs);
697
+ }
698
+ this.waiters.push(waiter);
699
+ });
700
+ }
701
+ // -- consumer shape #2: callbacks ----------------------------------
702
+ /** Register a callback fired for each Signal of the given type. */
703
+ on(type, fn) {
704
+ const list = this.handlers.get(type) ?? [];
705
+ list.push(fn);
706
+ this.handlers.set(type, list);
707
+ return fn;
708
+ }
709
+ // -- consumer shape #3: async iteration ----------------------------
710
+ [Symbol.asyncIterator]() {
711
+ return {
712
+ next: () => {
713
+ if (this.iterPush.length > 0) {
714
+ const v = this.iterPush.shift();
715
+ return Promise.resolve(
716
+ v === null ? { value: void 0, done: true } : { value: v, done: false }
717
+ );
718
+ }
719
+ if (this.closed_) {
720
+ return Promise.resolve({ value: void 0, done: true });
721
+ }
722
+ return new Promise((resolve) => this.iterPull.push(resolve));
723
+ }
724
+ };
725
+ }
726
+ iterEmit(v) {
727
+ const pull = this.iterPull.shift();
728
+ if (pull) {
729
+ pull(v === null ? { value: void 0, done: true } : { value: v, done: false });
730
+ } else {
731
+ this.iterPush.push(v);
732
+ }
733
+ }
734
+ // -- lifecycle ------------------------------------------------------
735
+ /** Close the Pathway. Idempotent. Pending waits reject with
736
+ * PathwayClosedError; iteration completes; the onClose hook fires once. */
737
+ async close() {
738
+ if (this.closed_) return;
739
+ this.closed_ = true;
740
+ for (const w of [...this.waiters]) {
741
+ w.reject(
742
+ new PathwayClosedError(
743
+ `Pathway for trace '${this.traceId}' closed before a matching Signal arrived`
744
+ )
745
+ );
746
+ }
747
+ this.waiters = [];
748
+ this.iterEmit(null);
749
+ if (this.onCloseHook) {
750
+ try {
751
+ await this.onCloseHook(this);
752
+ } catch {
753
+ }
754
+ }
755
+ }
756
+ /** `await using pathway = ...` support. */
757
+ async [Symbol.asyncDispose]() {
758
+ await this.close();
759
+ }
760
+ // -- internal: signal delivery (called by the owning Dendrite) ------
761
+ /** @internal */
762
+ async _deliver(signal) {
763
+ if (this.closed_) return;
764
+ if (this.scopeFilter !== null && !this.scopeFilter.has(signal.type)) {
765
+ await this.fireHandlers(signal);
766
+ if (TERMINAL_TYPES.has(signal.type)) await this.close();
767
+ return;
768
+ }
769
+ let consumed = false;
770
+ for (const w of [...this.waiters]) {
771
+ if (w.types.has(signal.type)) {
772
+ w.resolve(signal);
773
+ consumed = true;
774
+ }
775
+ }
776
+ if (!consumed) this.buffered.push(signal);
777
+ await this.fireHandlers(signal);
778
+ this.iterEmit(signal);
779
+ if (TERMINAL_TYPES.has(signal.type)) await this.close();
780
+ }
781
+ async fireHandlers(signal) {
782
+ for (const h of this.handlers.get(signal.type) ?? []) {
783
+ try {
784
+ await h(signal);
785
+ } catch {
786
+ }
787
+ }
788
+ }
789
+ };
790
+
791
+ // src/retry.ts
792
+ function defaultRetryOn(outcome) {
793
+ if (outcome instanceof PathwayClosedError) return true;
794
+ if (outcome instanceof Error) return outcome.name === "TimeoutError";
795
+ const sig = outcome;
796
+ return sig.type === SignalType.ERROR && Boolean(sig.payload?.["recoverable"]);
797
+ }
523
798
 
524
799
  // src/synapse.ts
525
800
  var MemorySubscription = class {
@@ -1796,6 +2071,16 @@ var LifecycleHooks = class {
1796
2071
  }
1797
2072
  };
1798
2073
 
2074
+ // src/trace-context.ts
2075
+ import { AsyncLocalStorage } from "async_hooks";
2076
+ var storage = new AsyncLocalStorage();
2077
+ function ambientTrace() {
2078
+ return storage.getStore() ?? null;
2079
+ }
2080
+ function runWithTraceContext(traceId, parentId, fn) {
2081
+ return storage.run([traceId, parentId], fn);
2082
+ }
2083
+
1799
2084
  // src/neuron.ts
1800
2085
  function clarify(question, context) {
1801
2086
  return context === void 0 ? { __clarification__: true, question } : { __clarification__: true, question, context };
@@ -1827,122 +2112,492 @@ function isErrorOutput(output) {
1827
2112
  return typeof output === "object" && output !== null && output["__error__"] === true;
1828
2113
  }
1829
2114
 
1830
- // src/neuron-mcp.ts
1831
- var standardMcpServers = {
1832
- filesystem: {
1833
- command: "npx",
1834
- args: ["-y", "@modelcontextprotocol/server-filesystem"],
1835
- note: "Append one or more allowed directories, e.g. args=['/data']."
1836
- },
1837
- memory: {
1838
- command: "npx",
1839
- args: ["-y", "@modelcontextprotocol/server-memory"],
1840
- note: "Knowledge-graph memory store."
1841
- },
1842
- everything: {
1843
- command: "npx",
1844
- args: ["-y", "@modelcontextprotocol/server-everything"],
1845
- note: "Reference server exercising every MCP feature; handy for tests."
1846
- },
1847
- sequentialthinking: {
1848
- command: "npx",
1849
- args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
1850
- note: "Structured step-by-step reasoning tool."
1851
- },
1852
- fetch: {
1853
- command: "uvx",
1854
- args: ["mcp-server-fetch"],
1855
- note: "Fetch a URL and return its content as markdown/text."
1856
- },
1857
- git: {
1858
- command: "uvx",
1859
- args: ["mcp-server-git"],
1860
- note: "Read/inspect a git repo. Append --repository <path>."
1861
- },
1862
- time: {
1863
- command: "uvx",
1864
- args: ["mcp-server-time"],
1865
- note: "Current time and timezone conversions."
1866
- }
1867
- };
1868
- var CONTROL_KEYS = /* @__PURE__ */ new Set(["tool", "arguments", "args", "__list_tools__"]);
1869
- function resolveLaunch(opts) {
1870
- const extra = opts.args ?? [];
1871
- if (opts.server != null) {
1872
- const preset = standardMcpServers[opts.server];
1873
- if (!preset) {
1874
- const available = Object.keys(standardMcpServers).sort().join(", ");
2115
+ // src/engram.ts
2116
+ var EngramBinding = class {
2117
+ name;
2118
+ directedId;
2119
+ directedType;
2120
+ defaultDeadlineMs;
2121
+ defaultRecallMode;
2122
+ constructor(init) {
2123
+ this.name = init.name;
2124
+ this.directedId = init.directedId ?? null;
2125
+ this.directedType = init.directedType ?? null;
2126
+ this.defaultDeadlineMs = init.defaultDeadlineMs ?? null;
2127
+ this.defaultRecallMode = init.defaultRecallMode ?? "first";
2128
+ if (!this.directedId && !this.directedType) {
1875
2129
  throw new Error(
1876
- `Unknown MCP server preset '${opts.server}'. Available: ${available}. (Or pass command/args to wrap any other stdio MCP server.)`
2130
+ `EngramBinding '${this.name}' requires directedId (engram_id) or directedType (engram_kind), or both`
1877
2131
  );
1878
2132
  }
1879
- return { command: opts.command ?? preset.command, args: [...preset.args, ...extra] };
1880
2133
  }
1881
- if (opts.command == null) {
1882
- throw new Error("mcpNeuron(...) needs either `command` (+optional `args`) or a `server` preset name.");
2134
+ /** Build the `Directed` addressing this Engram. */
2135
+ toDirected() {
2136
+ return { id: this.directedId, type: this.directedType, capabilities: [] };
1883
2137
  }
1884
- return { command: opts.command, args: extra };
1885
- }
1886
- function mcpNeuron(opts) {
1887
- const { command, args } = resolveLaunch(opts);
1888
- let client = null;
1889
- let connecting = null;
1890
- async function ensure() {
1891
- if (client) return client;
1892
- if (connecting) return connecting;
1893
- connecting = (async () => {
1894
- const clientSpec = "@modelcontextprotocol/sdk/client/index.js";
1895
- const stdioSpec = "@modelcontextprotocol/sdk/client/stdio.js";
1896
- const clientMod = await import(clientSpec);
1897
- const stdioMod = await import(stdioSpec);
1898
- const Client = clientMod.Client;
1899
- const StdioClientTransport = stdioMod.StdioClientTransport;
1900
- const transport = new StdioClientTransport({
1901
- command,
1902
- args,
1903
- ...opts.env ? { env: opts.env } : {},
1904
- ...opts.cwd ? { cwd: opts.cwd } : {}
1905
- });
1906
- const c = new Client(
1907
- { name: opts.clientName ?? "cosmonapse", version: opts.clientVersion ?? "0.2.0" },
1908
- { capabilities: {} }
1909
- );
1910
- await c.connect(transport);
1911
- client = c;
1912
- return c;
1913
- })();
1914
- return connecting;
2138
+ };
2139
+ var EngramError = class extends Error {
2140
+ constructor(message) {
2141
+ super(message);
2142
+ this.name = new.target.name;
1915
2143
  }
1916
- const fn = (async (input, _context) => {
1917
- const c = await ensure();
1918
- const inp = input ?? {};
1919
- if (inp.__list_tools__) {
1920
- const res2 = await c.listTools();
1921
- return {
1922
- tools: (res2.tools ?? []).map((t) => ({
1923
- name: t.name,
1924
- description: t.description ?? null,
1925
- input_schema: t.inputSchema ?? null
1926
- }))
1927
- };
2144
+ };
2145
+ var EngramTimeout = class extends EngramError {
2146
+ };
2147
+ var EngramCancelled = class extends EngramError {
2148
+ };
2149
+ var EngramNotBound = class extends EngramError {
2150
+ };
2151
+ var EngramOverloaded = class extends EngramError {
2152
+ };
2153
+ var Engram = class {
2154
+ version = null;
2155
+ // ----------------------------------------------------------------------
2156
+ // Saga / compensating-log rollback
2157
+ // ----------------------------------------------------------------------
2158
+ // A backend opts in by calling `sagaRecord` from inside `imprint` with the
2159
+ // inverse op needed to undo the write it is about to apply. `compensate`
2160
+ // then replays those inverses in reverse (LIFO) through the public
2161
+ // `imprint` path with no traceId/imprintId (so they neither re-journal nor
2162
+ // consume idempotency keys). Every inverse is itself a valid
2163
+ // add/upsert/delete, so this is fully backend-agnostic.
2164
+ sagaJournal = /* @__PURE__ */ new Map();
2165
+ sagaRecord(traceId, op, entry, mergeKey) {
2166
+ if (!traceId) return;
2167
+ let j = this.sagaJournal.get(traceId);
2168
+ if (!j) {
2169
+ j = [];
2170
+ this.sagaJournal.set(traceId, j);
1928
2171
  }
1929
- let tool = inp.tool ?? opts.tool;
1930
- let toolArgs = inp.arguments ?? inp.args;
1931
- if (toolArgs == null) {
1932
- toolArgs = {};
1933
- for (const [k, v] of Object.entries(inp)) {
1934
- if (!CONTROL_KEYS.has(k)) toolArgs[k] = v;
2172
+ j.push({ op, entry, mergeKey });
2173
+ }
2174
+ /** Reverse every journaled write for `traceId` (LIFO) and discard the
2175
+ * journal. Returns the number of inverse ops applied. Best-effort. Only
2176
+ * Engram state is reversed - external side effects are out of scope. */
2177
+ async compensate(traceId) {
2178
+ const inverses = this.sagaJournal.get(traceId);
2179
+ if (!inverses) return 0;
2180
+ this.sagaJournal.delete(traceId);
2181
+ let applied = 0;
2182
+ for (let i = inverses.length - 1; i >= 0; i--) {
2183
+ const inv = inverses[i];
2184
+ try {
2185
+ const opts = inv.mergeKey !== void 0 ? { mergeKey: inv.mergeKey } : {};
2186
+ await this.imprint(inv.op, inv.entry, opts);
2187
+ applied++;
2188
+ } catch {
1935
2189
  }
1936
2190
  }
1937
- if (!tool) {
1938
- const res2 = await c.listTools();
1939
- const names = (res2.tools ?? []).map((t) => t.name);
1940
- if (names.length === 1) {
1941
- tool = names[0];
1942
- } else {
1943
- throw new Error(
1944
- `MCP Neuron could not determine which tool to call. Pass tool=... (server exposes: ${JSON.stringify(names)}).`
1945
- );
2191
+ return applied;
2192
+ }
2193
+ /** Discard the trace's saga journal without reversing anything. Called at
2194
+ * the workflow commit point (FINAL/ERROR on the trace). */
2195
+ async commit(traceId) {
2196
+ this.sagaJournal.delete(traceId);
2197
+ }
2198
+ /** Return false if this Engram cannot satisfy the query. Default: serve all. */
2199
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2200
+ async canServe(_query) {
2201
+ return true;
2202
+ }
2203
+ };
2204
+ function receipt(engramId, op, fields = {}) {
2205
+ const error = fields.error ?? null;
2206
+ return {
2207
+ engramId,
2208
+ op,
2209
+ id: fields.id ?? null,
2210
+ version: fields.version ?? null,
2211
+ tookMs: fields.tookMs ?? null,
2212
+ error,
2213
+ ok: error === null
2214
+ };
2215
+ }
2216
+ function entryToDict(e) {
2217
+ const out = {
2218
+ id: e.id,
2219
+ content: e.content,
2220
+ tags: [...e.tags],
2221
+ version: e.version,
2222
+ created_at: e.createdAt,
2223
+ updated_at: e.updatedAt
2224
+ };
2225
+ if (e.mergeKey !== null) out["merge_key"] = e.mergeKey;
2226
+ if (Object.keys(e.extra).length > 0) out["meta"] = { ...e.extra };
2227
+ return out;
2228
+ }
2229
+ function asStringArray(v) {
2230
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
2231
+ }
2232
+ function asObject(v) {
2233
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
2234
+ }
2235
+ var InMemoryEngram = class extends Engram {
2236
+ engramId;
2237
+ engramKind;
2238
+ capabilities;
2239
+ entries = /* @__PURE__ */ new Map();
2240
+ byMergeKey = /* @__PURE__ */ new Map();
2241
+ imprintSeen = /* @__PURE__ */ new Map();
2242
+ constructor(init = {}) {
2243
+ super();
2244
+ this.engramId = init.engramId ?? "engram-memory";
2245
+ this.engramKind = init.engramKind ?? "keyvalue";
2246
+ this.capabilities = init.capabilities ?? ["substring", "tags", "merge_key"];
2247
+ this.version = init.version ?? "0.0.1";
2248
+ }
2249
+ async connect() {
2250
+ return;
2251
+ }
2252
+ async close() {
2253
+ this.entries.clear();
2254
+ this.byMergeKey.clear();
2255
+ this.imprintSeen.clear();
2256
+ }
2257
+ async recall(query, opts = {}) {
2258
+ const q = query ?? {};
2259
+ const text = typeof q["text"] === "string" ? q["text"].toLowerCase() : "";
2260
+ const tagQ = typeof q["tag"] === "string" ? q["tag"] : null;
2261
+ const mergeKey = typeof q["merge_key"] === "string" ? q["merge_key"] : null;
2262
+ const topK = typeof q["top_k"] === "number" ? q["top_k"] : 50;
2263
+ const filters = opts.filters ?? {};
2264
+ const requireTags = asStringArray(filters["tags"]);
2265
+ const since = typeof filters["since"] === "string" ? Date.parse(filters["since"]) : NaN;
2266
+ const until = typeof filters["until"] === "string" ? Date.parse(filters["until"]) : NaN;
2267
+ let candidates;
2268
+ if (mergeKey !== null) {
2269
+ const ids = this.byMergeKey.get(mergeKey) ?? [];
2270
+ candidates = ids.map((i) => this.entries.get(i)).filter((e) => e !== void 0);
2271
+ } else {
2272
+ candidates = [...this.entries.values()];
2273
+ }
2274
+ const hits = [];
2275
+ for (const ent of candidates) {
2276
+ if (requireTags.length > 0 && !requireTags.every((t) => ent.tags.includes(t))) continue;
2277
+ const updated = Date.parse(ent.updatedAt);
2278
+ if (!Number.isNaN(since) && updated < since) continue;
2279
+ if (!Number.isNaN(until) && updated > until) continue;
2280
+ if (tagQ !== null && !ent.tags.includes(tagQ)) continue;
2281
+ let score = 1;
2282
+ if (text) {
2283
+ const hay = String(ent.content).toLowerCase();
2284
+ if (!hay.includes(text)) continue;
2285
+ score = Math.min(1, text.length / Math.max(1, hay.length));
2286
+ }
2287
+ if (opts.minConfidence !== void 0 && score < opts.minConfidence) continue;
2288
+ hits.push({ id: ent.id, entry: entryToDict(ent), score });
2289
+ }
2290
+ hits.sort((a, b) => b.score - a.score);
2291
+ return hits.slice(0, topK);
2292
+ }
2293
+ async imprint(op, entry, opts = {}) {
2294
+ const t0 = Date.now();
2295
+ const mergeKey = opts.mergeKey ?? null;
2296
+ const traceId = opts.traceId;
2297
+ const tookMs = () => Date.now() - t0;
2298
+ if (opts.imprintId !== void 0) {
2299
+ const seen = this.imprintSeen.get(opts.imprintId);
2300
+ if (seen !== void 0) {
2301
+ const existing = this.entries.get(seen);
2302
+ return receipt(this.engramId, op, {
2303
+ id: seen,
2304
+ version: existing ? existing.version : null,
2305
+ tookMs: tookMs()
2306
+ });
2307
+ }
2308
+ }
2309
+ let resultingId = null;
2310
+ let version = null;
2311
+ if (op === "add") {
2312
+ const ent = this.makeEntry(entry, mergeKey);
2313
+ if (this.entries.has(ent.id)) {
2314
+ return receipt(this.engramId, op, { error: `entry id '${ent.id}' already exists`, tookMs: tookMs() });
2315
+ }
2316
+ this.store(ent);
2317
+ resultingId = ent.id;
2318
+ version = ent.version;
2319
+ this.sagaRecord(traceId, "delete", { id: ent.id });
2320
+ } else if (op === "append") {
2321
+ let ent = this.makeEntry(entry, mergeKey);
2322
+ while (this.entries.has(ent.id)) {
2323
+ ent = this.makeEntry({ ...entry, id: newEngramId() }, mergeKey);
2324
+ }
2325
+ this.store(ent);
2326
+ resultingId = ent.id;
2327
+ version = ent.version;
2328
+ this.sagaRecord(traceId, "delete", { id: ent.id });
2329
+ } else if (op === "upsert") {
2330
+ const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
2331
+ const targetId = existingIds[existingIds.length - 1];
2332
+ const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
2333
+ if (old !== void 0) {
2334
+ this.sagaRecord(
2335
+ traceId,
2336
+ "upsert",
2337
+ { id: old.id, content: structuredClone(old.content), tags: [...old.tags], meta: structuredClone(old.extra) },
2338
+ old.mergeKey ?? void 0
2339
+ );
2340
+ const next = this.makeEntry({ ...entry, id: old.id }, mergeKey);
2341
+ next.createdAt = old.createdAt;
2342
+ next.version = old.version + 1;
2343
+ this.store(next, true);
2344
+ resultingId = next.id;
2345
+ version = next.version;
2346
+ } else {
2347
+ const ent = this.makeEntry(entry, mergeKey);
2348
+ this.store(ent);
2349
+ resultingId = ent.id;
2350
+ version = ent.version;
2351
+ this.sagaRecord(traceId, "delete", { id: ent.id });
2352
+ }
2353
+ } else if (op === "merge") {
2354
+ const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
2355
+ const targetId = existingIds[existingIds.length - 1];
2356
+ const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
2357
+ if (old === void 0) {
2358
+ return receipt(this.engramId, op, { error: `no entry for merge_key='${mergeKey}'`, tookMs: tookMs() });
2359
+ }
2360
+ this.sagaRecord(
2361
+ traceId,
2362
+ "upsert",
2363
+ { id: old.id, content: structuredClone(old.content), tags: [...old.tags], meta: structuredClone(old.extra) },
2364
+ old.mergeKey ?? void 0
2365
+ );
2366
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2367
+ const next = {
2368
+ id: old.id,
2369
+ content: deepMerge(old.content, entry["content"]),
2370
+ tags: [.../* @__PURE__ */ new Set([...old.tags, ...asStringArray(entry["tags"])])],
2371
+ mergeKey: old.mergeKey,
2372
+ version: old.version + 1,
2373
+ createdAt: old.createdAt,
2374
+ updatedAt: now,
2375
+ extra: asObject(deepMerge(old.extra, entry["meta"]))
2376
+ };
2377
+ this.store(next, true);
2378
+ resultingId = next.id;
2379
+ version = next.version;
2380
+ } else if (op === "delete") {
2381
+ let targetId = null;
2382
+ const entId = entry["id"];
2383
+ if (typeof entId === "string") {
2384
+ targetId = entId;
2385
+ } else if (mergeKey !== null) {
2386
+ const ids = this.byMergeKey.get(mergeKey) ?? [];
2387
+ targetId = ids[ids.length - 1] ?? null;
2388
+ }
2389
+ if (targetId === null || !this.entries.has(targetId)) {
2390
+ return receipt(this.engramId, op, { tookMs: tookMs() });
2391
+ }
2392
+ const old = this.entries.get(targetId);
2393
+ this.sagaRecord(
2394
+ traceId,
2395
+ "add",
2396
+ { id: old.id, content: structuredClone(old.content), tags: [...old.tags], meta: structuredClone(old.extra) },
2397
+ old.mergeKey ?? void 0
2398
+ );
2399
+ this.evict(targetId);
2400
+ resultingId = targetId;
2401
+ version = null;
2402
+ } else {
2403
+ return receipt(this.engramId, op, { error: `unknown op '${op}'`, tookMs: tookMs() });
2404
+ }
2405
+ if (opts.imprintId !== void 0 && resultingId !== null) {
2406
+ this.imprintSeen.set(opts.imprintId, resultingId);
2407
+ }
2408
+ return receipt(this.engramId, op, { id: resultingId, version, tookMs: tookMs() });
2409
+ }
2410
+ /** Test/debug helper - NOT part of the Engram contract. */
2411
+ snapshot() {
2412
+ return [...this.entries.values()].map(entryToDict);
2413
+ }
2414
+ makeEntry(entry, mergeKey) {
2415
+ const id = typeof entry["id"] === "string" ? entry["id"] : newEngramId();
2416
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2417
+ return {
2418
+ id,
2419
+ content: entry["content"],
2420
+ tags: asStringArray(entry["tags"]),
2421
+ mergeKey,
2422
+ version: 1,
2423
+ createdAt: now,
2424
+ updatedAt: now,
2425
+ extra: asObject(entry["meta"])
2426
+ };
2427
+ }
2428
+ store(ent, replace = false) {
2429
+ if (replace) {
2430
+ const old = this.entries.get(ent.id);
2431
+ if (old !== void 0 && old.mergeKey) {
2432
+ const bucket = this.byMergeKey.get(old.mergeKey);
2433
+ if (bucket) {
2434
+ const idx = bucket.indexOf(ent.id);
2435
+ if (idx >= 0) bucket.splice(idx, 1);
2436
+ if (bucket.length === 0) this.byMergeKey.delete(old.mergeKey);
2437
+ }
2438
+ }
2439
+ }
2440
+ this.entries.set(ent.id, ent);
2441
+ if (ent.mergeKey) {
2442
+ const bucket = this.byMergeKey.get(ent.mergeKey);
2443
+ if (bucket) bucket.push(ent.id);
2444
+ else this.byMergeKey.set(ent.mergeKey, [ent.id]);
2445
+ }
2446
+ }
2447
+ evict(entryId) {
2448
+ const ent = this.entries.get(entryId);
2449
+ this.entries.delete(entryId);
2450
+ if (ent === void 0) return;
2451
+ if (ent.mergeKey) {
2452
+ const bucket = this.byMergeKey.get(ent.mergeKey);
2453
+ if (bucket) {
2454
+ const idx = bucket.indexOf(entryId);
2455
+ if (idx >= 0) bucket.splice(idx, 1);
2456
+ if (bucket.length === 0) this.byMergeKey.delete(ent.mergeKey);
2457
+ }
2458
+ }
2459
+ }
2460
+ };
2461
+ function deepMerge(base, incoming) {
2462
+ if (incoming === void 0 || incoming === null) return base;
2463
+ const bothObjects = base !== null && typeof base === "object" && !Array.isArray(base) && typeof incoming === "object" && !Array.isArray(incoming);
2464
+ if (bothObjects) {
2465
+ const out = { ...base };
2466
+ for (const [k, v] of Object.entries(incoming)) {
2467
+ out[k] = k in out ? deepMerge(out[k], v) : v;
2468
+ }
2469
+ return out;
2470
+ }
2471
+ if (Array.isArray(base) && Array.isArray(incoming)) {
2472
+ const seen = /* @__PURE__ */ new Set();
2473
+ const out = [];
2474
+ for (const item of [...base, ...incoming]) {
2475
+ const key = JSON.stringify(item);
2476
+ if (seen.has(key)) continue;
2477
+ seen.add(key);
2478
+ out.push(item);
2479
+ }
2480
+ return out;
2481
+ }
2482
+ return incoming;
2483
+ }
2484
+
2485
+ // src/neuron-mcp.ts
2486
+ var standardMcpServers = {
2487
+ filesystem: {
2488
+ command: "npx",
2489
+ args: ["-y", "@modelcontextprotocol/server-filesystem"],
2490
+ note: "Append one or more allowed directories, e.g. args=['/data']."
2491
+ },
2492
+ memory: {
2493
+ command: "npx",
2494
+ args: ["-y", "@modelcontextprotocol/server-memory"],
2495
+ note: "Knowledge-graph memory store."
2496
+ },
2497
+ everything: {
2498
+ command: "npx",
2499
+ args: ["-y", "@modelcontextprotocol/server-everything"],
2500
+ note: "Reference server exercising every MCP feature; handy for tests."
2501
+ },
2502
+ sequentialthinking: {
2503
+ command: "npx",
2504
+ args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
2505
+ note: "Structured step-by-step reasoning tool."
2506
+ },
2507
+ fetch: {
2508
+ command: "uvx",
2509
+ args: ["mcp-server-fetch"],
2510
+ note: "Fetch a URL and return its content as markdown/text."
2511
+ },
2512
+ git: {
2513
+ command: "uvx",
2514
+ args: ["mcp-server-git"],
2515
+ note: "Read/inspect a git repo. Append --repository <path>."
2516
+ },
2517
+ time: {
2518
+ command: "uvx",
2519
+ args: ["mcp-server-time"],
2520
+ note: "Current time and timezone conversions."
2521
+ }
2522
+ };
2523
+ var CONTROL_KEYS = /* @__PURE__ */ new Set(["tool", "arguments", "args", "__list_tools__"]);
2524
+ function resolveLaunch(opts) {
2525
+ const extra = opts.args ?? [];
2526
+ if (opts.server != null) {
2527
+ const preset = standardMcpServers[opts.server];
2528
+ if (!preset) {
2529
+ const available = Object.keys(standardMcpServers).sort().join(", ");
2530
+ throw new Error(
2531
+ `Unknown MCP server preset '${opts.server}'. Available: ${available}. (Or pass command/args to wrap any other stdio MCP server.)`
2532
+ );
2533
+ }
2534
+ return { command: opts.command ?? preset.command, args: [...preset.args, ...extra] };
2535
+ }
2536
+ if (opts.command == null) {
2537
+ throw new Error("mcpNeuron(...) needs either `command` (+optional `args`) or a `server` preset name.");
2538
+ }
2539
+ return { command: opts.command, args: extra };
2540
+ }
2541
+ function mcpNeuron(opts) {
2542
+ const { command, args } = resolveLaunch(opts);
2543
+ let client = null;
2544
+ let connecting = null;
2545
+ async function ensure() {
2546
+ if (client) return client;
2547
+ if (connecting) return connecting;
2548
+ connecting = (async () => {
2549
+ const clientSpec = "@modelcontextprotocol/sdk/client/index.js";
2550
+ const stdioSpec = "@modelcontextprotocol/sdk/client/stdio.js";
2551
+ const clientMod = await import(clientSpec);
2552
+ const stdioMod = await import(stdioSpec);
2553
+ const Client = clientMod.Client;
2554
+ const StdioClientTransport = stdioMod.StdioClientTransport;
2555
+ const transport = new StdioClientTransport({
2556
+ command,
2557
+ args,
2558
+ ...opts.env ? { env: opts.env } : {},
2559
+ ...opts.cwd ? { cwd: opts.cwd } : {}
2560
+ });
2561
+ const c = new Client(
2562
+ { name: opts.clientName ?? "cosmonapse", version: opts.clientVersion ?? "0.2.0" },
2563
+ { capabilities: {} }
2564
+ );
2565
+ await c.connect(transport);
2566
+ client = c;
2567
+ return c;
2568
+ })();
2569
+ return connecting;
2570
+ }
2571
+ const fn = (async (input, _context) => {
2572
+ const c = await ensure();
2573
+ const inp = input ?? {};
2574
+ if (inp.__list_tools__) {
2575
+ const res2 = await c.listTools();
2576
+ return {
2577
+ tools: (res2.tools ?? []).map((t) => ({
2578
+ name: t.name,
2579
+ description: t.description ?? null,
2580
+ input_schema: t.inputSchema ?? null
2581
+ }))
2582
+ };
2583
+ }
2584
+ let tool = inp.tool ?? opts.tool;
2585
+ let toolArgs = inp.arguments ?? inp.args;
2586
+ if (toolArgs == null) {
2587
+ toolArgs = {};
2588
+ for (const [k, v] of Object.entries(inp)) {
2589
+ if (!CONTROL_KEYS.has(k)) toolArgs[k] = v;
2590
+ }
2591
+ }
2592
+ if (!tool) {
2593
+ const res2 = await c.listTools();
2594
+ const names = (res2.tools ?? []).map((t) => t.name);
2595
+ if (names.length === 1) {
2596
+ tool = names[0];
2597
+ } else {
2598
+ throw new Error(
2599
+ `MCP Neuron could not determine which tool to call. Pass tool=... (server exposes: ${JSON.stringify(names)}).`
2600
+ );
1946
2601
  }
1947
2602
  }
1948
2603
  const res = await c.callTool({ name: tool, arguments: toolArgs });
@@ -1979,8 +2634,46 @@ function readMessages(input) {
1979
2634
  const m = input["messages"];
1980
2635
  return Array.isArray(m) ? m : null;
1981
2636
  }
2637
+ function followupPrompt(input) {
2638
+ const c = input["clarification"];
2639
+ if (c !== null && typeof c === "object" && !Array.isArray(c)) {
2640
+ const cd = c;
2641
+ const lines = ["You previously asked a clarifying question while working on a task."];
2642
+ if (cd["question"] !== void 0 && cd["question"] !== null) {
2643
+ lines.push(`Your question: ${String(cd["question"])}`);
2644
+ }
2645
+ if ("answer" in cd) lines.push(`The answer: ${JSON.stringify(cd["answer"])}`);
2646
+ const extra = Object.fromEntries(
2647
+ Object.entries(cd).filter(([k]) => k !== "question" && k !== "answer")
2648
+ );
2649
+ if (Object.keys(extra).length) lines.push(`Additional context: ${JSON.stringify(extra)}`);
2650
+ lines.push("Continue the original task using this answer.");
2651
+ return lines.join("\n");
2652
+ }
2653
+ const p = input["permission"];
2654
+ if (p !== null && typeof p === "object" && !Array.isArray(p)) {
2655
+ const pd = p;
2656
+ const granted = Boolean(pd["granted"]);
2657
+ const lines = ["You previously requested permission while working on a task."];
2658
+ if (pd["action"] !== void 0 && pd["action"] !== null) {
2659
+ lines.push(`Requested action: ${String(pd["action"])}`);
2660
+ }
2661
+ lines.push(`The decision: ${granted ? "GRANTED" : "DENIED"}.`);
2662
+ if (pd["reason"] !== void 0 && pd["reason"] !== null) {
2663
+ lines.push(`Reason: ${String(pd["reason"])}`);
2664
+ }
2665
+ if (pd["ttl_ms"] !== void 0 && pd["ttl_ms"] !== null) {
2666
+ lines.push(`The grant is valid for ${String(pd["ttl_ms"])} ms.`);
2667
+ }
2668
+ lines.push(
2669
+ granted ? "Proceed with the action and continue the original task." : "Do not perform the action. Continue the task another way, or explain why you cannot."
2670
+ );
2671
+ return lines.join("\n");
2672
+ }
2673
+ return null;
2674
+ }
1982
2675
  function requireInput(input, provider) {
1983
- const prompt = readPrompt(input);
2676
+ const prompt = readPrompt(input) ?? followupPrompt(input);
1984
2677
  const messages = readMessages(input);
1985
2678
  if (!prompt && !messages) {
1986
2679
  throw new Error(
@@ -2191,9 +2884,11 @@ var Axon = class _Axon {
2191
2884
  neuronId;
2192
2885
  capabilities;
2193
2886
  version;
2887
+ neuronKind;
2194
2888
  fn;
2195
2889
  contextFetcher;
2196
2890
  outputParser;
2891
+ engramBindings = /* @__PURE__ */ new Map();
2197
2892
  dendrite = null;
2198
2893
  /**
2199
2894
  * Decorator-registered recognisers, one bucket per capability (the asking
@@ -2202,48 +2897,139 @@ var Axon = class _Axon {
2202
2897
  * output by {@link applyRecognisers}.
2203
2898
  */
2204
2899
  recognisers = { error: [], clarification: [], permission: [], output: [] };
2900
+ /** Pre-task hooks (beforeTask): transform/validate/reject the TASK input
2901
+ * before the Neuron runs. */
2902
+ beforeTaskHooks = [];
2205
2903
  /** @internal - lifecycle hooks, driven by the hosting Dendrite. */
2206
2904
  hooks = new LifecycleHooks(this);
2207
2905
  constructor(opts) {
2208
2906
  this.neuronId = opts.neuronId;
2209
2907
  this.capabilities = opts.capabilities ?? [];
2210
2908
  this.version = opts.version;
2909
+ this.neuronKind = opts.neuronKind ?? "neuron";
2211
2910
  this.fn = opts.neuronFn;
2212
2911
  this.contextFetcher = opts.contextFetcher ?? noopContextFetcher;
2213
2912
  this.outputParser = opts.outputParser;
2913
+ for (const b of opts.engrams ?? []) {
2914
+ if (this.engramBindings.has(b.name)) {
2915
+ throw new Error(`Axon '${opts.neuronId}': duplicate EngramBinding name '${b.name}'`);
2916
+ }
2917
+ this.engramBindings.set(b.name, b);
2918
+ }
2919
+ }
2920
+ /** Declared Engram bindings, keyed by name. */
2921
+ get engrams() {
2922
+ return new Map(this.engramBindings);
2923
+ }
2924
+ resolveBinding(name) {
2925
+ const binding = this.engramBindings.get(name);
2926
+ if (!binding) {
2927
+ throw new EngramNotBound(
2928
+ `Axon '${this.neuronId}': no Engram binding named '${name}'; available: ${[...this.engramBindings.keys()].sort().join(", ")}`
2929
+ );
2930
+ }
2931
+ return binding;
2932
+ }
2933
+ /** Build the per-task helpers object handed to the Neuron as its third
2934
+ * argument. Helpers throw EngramNotBound for undeclared names and
2935
+ * require a hosting Dendrite (the only thing the Axon pulls from it). */
2936
+ buildHelpers(traceId, parentId) {
2937
+ const requireClient = () => {
2938
+ if (this.dendrite === null) {
2939
+ throw new Error(
2940
+ `Axon '${this.neuronId}': not attached to a Dendrite; engram helpers require a hosting Dendrite`
2941
+ );
2942
+ }
2943
+ return this.dendrite.engramClient;
2944
+ };
2945
+ return {
2946
+ recall: async (name, args) => {
2947
+ const binding = this.resolveBinding(name);
2948
+ return requireClient().recall({
2949
+ binding,
2950
+ query: args.query,
2951
+ traceId,
2952
+ parentId,
2953
+ ...args.filters !== void 0 ? { filters: args.filters } : {},
2954
+ ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
2955
+ ...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
2956
+ ...args.recallMode !== void 0 ? { recallMode: args.recallMode } : {},
2957
+ ...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
2958
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
2959
+ });
2960
+ },
2961
+ imprint: async (name, args) => {
2962
+ const binding = this.resolveBinding(name);
2963
+ return requireClient().imprint({
2964
+ binding,
2965
+ op: args.op,
2966
+ entry: args.entry,
2967
+ traceId,
2968
+ parentId,
2969
+ ...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
2970
+ ...args.awaitAck !== void 0 ? { awaitAck: args.awaitAck } : {},
2971
+ ...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
2972
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
2973
+ });
2974
+ }
2975
+ };
2214
2976
  }
2215
2977
  // -- source-paired factories --------------------------------------
2216
2978
  // Build an Axon already paired with one of the `neuron(source, ...)`
2217
2979
  // providers AND wired with the matching recogniser. No new class: the
2218
2980
  // result is a plain Axon.
2981
+ /** Resolve the teach-intents decision and return (possibly augmented) source opts. */
2982
+ static applyTeachIntents(source, opts, extra) {
2983
+ const recognize = extra.recognize ?? true;
2984
+ const teach = extra.teachIntents ?? (recognize && SYSTEM_CAPABLE_SOURCES.has(source.toLowerCase()));
2985
+ if (!teach) return opts;
2986
+ if (!SYSTEM_CAPABLE_SOURCES.has(source.toLowerCase())) {
2987
+ throw new Error(
2988
+ `teachIntents: true is not supported for source '${source}': its Neuron wrapper accepts no system option. Embed the convention in the prompt yourself (COSMO_INTENT_SYSTEM_PROMPT).`
2989
+ );
2990
+ }
2991
+ const existing = opts?.system;
2992
+ return {
2993
+ ...opts ?? {},
2994
+ system: existing ? `${existing}
2995
+
2996
+ ${COSMO_INTENT_SYSTEM_PROMPT}` : COSMO_INTENT_SYSTEM_PROMPT
2997
+ };
2998
+ }
2219
2999
  static build(neuronId, neuronFn, source, extra) {
2220
3000
  const recognize = extra.recognize ?? true;
2221
3001
  const o = { neuronId, neuronFn };
2222
3002
  if (extra.capabilities) o.capabilities = extra.capabilities;
2223
3003
  if (extra.version !== void 0) o.version = extra.version;
3004
+ if (extra.neuronKind !== void 0) o.neuronKind = extra.neuronKind;
2224
3005
  if (extra.contextFetcher) o.contextFetcher = extra.contextFetcher;
2225
3006
  if (recognize) o.outputParser = source === "mcp" ? parseMcpIntents : parseLlmIntents;
2226
3007
  return new _Axon(o);
2227
3008
  }
2228
3009
  /** Axon paired with any registered Neuron source + its recogniser. */
2229
3010
  static fromSource(source, neuronId, opts, extra = {}) {
2230
- return _Axon.build(neuronId, neuron(source, opts), source, extra);
3011
+ const o = _Axon.applyTeachIntents(source, opts, extra);
3012
+ return _Axon.build(neuronId, neuron(source, o), source, extra);
2231
3013
  }
2232
3014
  /** Axon paired with the OpenAI Chat Completions API. */
2233
3015
  static openai(neuronId, opts, extra = {}) {
2234
- return _Axon.build(neuronId, neuron("openai", opts), "openai", extra);
3016
+ const o = _Axon.applyTeachIntents("openai", opts, extra);
3017
+ return _Axon.build(neuronId, neuron("openai", o), "openai", extra);
2235
3018
  }
2236
3019
  /** Axon paired with the Anthropic Messages API. */
2237
3020
  static anthropic(neuronId, opts, extra = {}) {
2238
- return _Axon.build(neuronId, neuron("anthropic", opts), "anthropic", extra);
3021
+ const o = _Axon.applyTeachIntents("anthropic", opts, extra);
3022
+ return _Axon.build(neuronId, neuron("anthropic", o), "anthropic", extra);
2239
3023
  }
2240
3024
  /** Axon paired with a local Ollama daemon. */
2241
3025
  static ollama(neuronId, opts, extra = {}) {
2242
- return _Axon.build(neuronId, neuron("ollama", opts), "ollama", extra);
3026
+ const o = _Axon.applyTeachIntents("ollama", opts, extra);
3027
+ return _Axon.build(neuronId, neuron("ollama", o), "ollama", extra);
2243
3028
  }
2244
3029
  /** Axon paired with a HuggingFace TGI / OpenAI-compatible endpoint. */
2245
3030
  static huggingface(neuronId, opts, extra = {}) {
2246
- return _Axon.build(neuronId, neuron("huggingface", opts), "huggingface", extra);
3031
+ const o = _Axon.applyTeachIntents("huggingface", opts, extra);
3032
+ return _Axon.build(neuronId, neuron("huggingface", o), "huggingface", extra);
2247
3033
  }
2248
3034
  /** Axon paired with a stdio MCP server. */
2249
3035
  static mcp(neuronId, opts, extra = {}) {
@@ -2255,6 +3041,26 @@ var Axon = class _Axon {
2255
3041
  // Signals). Return the intent's fields to match, or null/undefined to fall
2256
3042
  // through. Sync or async; multiple per capability tried in order. These run
2257
3043
  // after `outputParser` and before the literal `__marker__` checks.
3044
+ /**
3045
+ * Register a pre-task hook over the TASK's `input`. Runs before the Neuron.
3046
+ * Sync or async; multiple hooks run in registration order, each receiving
3047
+ * the previous one's result. Return a (new) object to replace the input,
3048
+ * return null/undefined to pass through unchanged, or throw to reject the
3049
+ * TASK (surfaces as an ERROR Signal, code NEURON_EXCEPTION). The natural
3050
+ * place for input normalisation or per-Axon policy checks.
3051
+ */
3052
+ beforeTask(fn) {
3053
+ this.beforeTaskHooks.push(fn);
3054
+ return fn;
3055
+ }
3056
+ async applyBeforeTask(input) {
3057
+ let current = input;
3058
+ for (const fn of this.beforeTaskHooks) {
3059
+ const r = await fn(current);
3060
+ if (r !== null && r !== void 0) current = r;
3061
+ }
3062
+ return current;
3063
+ }
2258
3064
  /** Detector returning the AGENT_OUTPUT payload, or null to wrap verbatim. */
2259
3065
  detectsOutput(fn) {
2260
3066
  this.recognisers.output.push(fn);
@@ -2325,8 +3131,21 @@ var Axon = class _Axon {
2325
3131
  [DETACH]() {
2326
3132
  this.dendrite = null;
2327
3133
  }
2328
- /** Run the Neuron and return AGENT_OUTPUT / CLARIFICATION / ERROR. */
3134
+ /** Run the Neuron and return AGENT_OUTPUT / CLARIFICATION / ERROR.
3135
+ *
3136
+ * Binds the TASK's (traceId, parentId=task.id) as the ambient trace
3137
+ * context for the whole handling pass - neuronFn, detectors, and hooks
3138
+ * included - so engram calls made without explicit trace plumbing (e.g.
3139
+ * `dendrite.imprint` from a `detectsOutput` hook) are attributed to this
3140
+ * task's trace. */
2329
3141
  async handleTask(task) {
3142
+ return runWithTraceContext(
3143
+ task.trace_id,
3144
+ task.id,
3145
+ () => this.handleTaskInner(task)
3146
+ );
3147
+ }
3148
+ async handleTaskInner(task) {
2330
3149
  const traceId = task.trace_id;
2331
3150
  const parentId = task.id;
2332
3151
  const input = task.payload["input"] ?? {};
@@ -2341,7 +3160,9 @@ var Axon = class _Axon {
2341
3160
  }
2342
3161
  let rawOutput;
2343
3162
  try {
2344
- rawOutput = await this.fn(input, context);
3163
+ const effectiveInput = this.beforeTaskHooks.length ? await this.applyBeforeTask(input) : input;
3164
+ const helpers = this.engramBindings.size ? this.buildHelpers(traceId, parentId) : void 0;
3165
+ rawOutput = await this.fn(effectiveInput, context, helpers);
2345
3166
  if (this.outputParser) rawOutput = this.outputParser(rawOutput);
2346
3167
  rawOutput = await this.applyRecognisers(rawOutput);
2347
3168
  } catch (err) {
@@ -2388,6 +3209,16 @@ var Axon = class _Axon {
2388
3209
  return agentOutputSignal({ traceId, parentId, directed: { id: this.neuronId }, output });
2389
3210
  }
2390
3211
  };
3212
+ var COSMO_INTENT_SYSTEM_PROMPT = 'You can control the surrounding agent protocol by replying with a single JSON object carrying a "cosmo" key (either as your whole reply or inside a ```json fenced block):\n{"cosmo": "clarification", "question": "<what you need to know>"} - ask the orchestrator a question when the task is ambiguous.\n{"cosmo": "permission", "action": "<action>", "scope": {...}, "reason": "<why>"} - request approval before a sensitive action.\n{"cosmo": "error", "code": "<CODE>", "message": "<details>"} - report a structured failure.\n{"cosmo": "output", "output": {...}} - return a structured result.\nFor a normal answer, just reply with plain text - do not wrap ordinary answers in a cosmo object.';
3213
+ var SYSTEM_CAPABLE_SOURCES = /* @__PURE__ */ new Set([
3214
+ "ollama",
3215
+ "openai",
3216
+ "anthropic",
3217
+ "groq",
3218
+ "openrouter",
3219
+ "together",
3220
+ "mistral"
3221
+ ]);
2391
3222
  var INTENT_KEY = "cosmo";
2392
3223
  var FENCED_JSON = /```(?:json)?\s*(\{[\s\S]*?\})\s*```/g;
2393
3224
  function extractCosmoIntent(text) {
@@ -2460,15 +3291,241 @@ function parseMcpIntents(raw) {
2460
3291
  const msg = r["response"] ?? r["content"] ?? "MCP tool returned is_error";
2461
3292
  return { __error__: true, code: "MCP_TOOL_ERROR", message: String(msg) };
2462
3293
  }
2463
- const text = r["response"];
2464
- if (typeof text === "string") {
2465
- const intent = extractCosmoIntent(text);
2466
- if (intent) {
2467
- const marker = intentToMarker(intent);
2468
- if (marker) return marker;
2469
- }
3294
+ const text = r["response"];
3295
+ if (typeof text === "string") {
3296
+ const intent = extractCosmoIntent(text);
3297
+ if (intent) {
3298
+ const marker = intentToMarker(intent);
3299
+ if (marker) return marker;
3300
+ }
3301
+ }
3302
+ return raw;
3303
+ }
3304
+
3305
+ // src/engram-client.ts
3306
+ function deferred() {
3307
+ let resolve;
3308
+ let reject;
3309
+ const promise = new Promise((res, rej) => {
3310
+ resolve = res;
3311
+ reject = rej;
3312
+ });
3313
+ return { promise, resolve, reject };
3314
+ }
3315
+ var EngramClient = class {
3316
+ constructor(publisher) {
3317
+ this.publisher = publisher;
3318
+ }
3319
+ publisher;
3320
+ pendingRecalls = /* @__PURE__ */ new Map();
3321
+ pendingImprints = /* @__PURE__ */ new Map();
3322
+ byTrace = /* @__PURE__ */ new Map();
3323
+ async recall(args) {
3324
+ let engramId = args.engramId;
3325
+ let engramKind = args.engramKind;
3326
+ let deadlineMs = args.deadlineMs;
3327
+ let recallMode = args.recallMode;
3328
+ if (args.binding) {
3329
+ engramId = engramId ?? args.binding.directedId ?? void 0;
3330
+ engramKind = engramKind ?? args.binding.directedType ?? void 0;
3331
+ if (deadlineMs === void 0) deadlineMs = args.binding.defaultDeadlineMs ?? void 0;
3332
+ if (recallMode === void 0) recallMode = args.binding.defaultRecallMode;
3333
+ }
3334
+ const mode = recallMode ?? "first";
3335
+ const sig = recallSignal({
3336
+ traceId: args.traceId,
3337
+ parentId: args.parentId,
3338
+ directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
3339
+ query: args.query,
3340
+ ...args.filters !== void 0 ? { filters: args.filters } : {},
3341
+ ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
3342
+ ...deadlineMs !== void 0 ? { deadlineMs } : {},
3343
+ ...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
3344
+ recallMode: mode,
3345
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
3346
+ });
3347
+ const d = deferred();
3348
+ const pending = { deferred: d, mode, timer: null, done: false, hitsSoFar: [], engrams: [] };
3349
+ this.pendingRecalls.set(sig.id, pending);
3350
+ this.track(args.traceId, sig.id);
3351
+ if (deadlineMs !== void 0 && deadlineMs > 0) {
3352
+ pending.timer = setTimeout(() => this.onRecallDeadline(sig.id), deadlineMs);
3353
+ }
3354
+ try {
3355
+ await this.publisher.publish(sig);
3356
+ } catch (err) {
3357
+ this.cleanupRecall(args.traceId, sig.id);
3358
+ throw err;
3359
+ }
3360
+ try {
3361
+ return await d.promise;
3362
+ } finally {
3363
+ this.cleanupRecall(args.traceId, sig.id);
3364
+ }
3365
+ }
3366
+ async imprint(args) {
3367
+ let engramId = args.engramId;
3368
+ let engramKind = args.engramKind;
3369
+ if (args.binding) {
3370
+ engramId = engramId ?? args.binding.directedId ?? void 0;
3371
+ engramKind = engramKind ?? args.binding.directedType ?? void 0;
3372
+ }
3373
+ const sig = imprintSignal({
3374
+ traceId: args.traceId,
3375
+ parentId: args.parentId,
3376
+ directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
3377
+ op: args.op,
3378
+ entry: args.entry,
3379
+ ...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
3380
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
3381
+ });
3382
+ if (!args.awaitAck) {
3383
+ await this.publisher.publish(sig);
3384
+ return null;
3385
+ }
3386
+ const d = deferred();
3387
+ const pending = { deferred: d, timer: null, done: false };
3388
+ this.pendingImprints.set(sig.id, pending);
3389
+ this.track(args.traceId, sig.id);
3390
+ if (args.deadlineMs !== void 0 && args.deadlineMs > 0) {
3391
+ pending.timer = setTimeout(() => this.onImprintDeadline(sig.id), args.deadlineMs);
3392
+ }
3393
+ try {
3394
+ await this.publisher.publish(sig);
3395
+ } catch (err) {
3396
+ this.cleanupImprint(args.traceId, sig.id);
3397
+ throw err;
3398
+ }
3399
+ try {
3400
+ return await d.promise;
3401
+ } finally {
3402
+ this.cleanupImprint(args.traceId, sig.id);
3403
+ }
3404
+ }
3405
+ /** Match RECALLED / IMPRINTED by parent_id and resolve pendings. */
3406
+ deliver(sig) {
3407
+ const pid = sig.parent_id;
3408
+ if (pid === null) return;
3409
+ if (sig.type === SignalType.RECALLED) {
3410
+ const pending = this.pendingRecalls.get(pid);
3411
+ if (pending === void 0) return;
3412
+ const hits = hitsFromPayload(sig.payload["hits"]);
3413
+ const engramId = typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "";
3414
+ const tookMs = typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null;
3415
+ const truncated = sig.payload["truncated"] === true;
3416
+ if (pending.mode === "first") {
3417
+ if (!pending.done) {
3418
+ pending.done = true;
3419
+ pending.deferred.resolve({
3420
+ hits,
3421
+ engramIds: engramId ? [engramId] : [],
3422
+ truncated,
3423
+ tookMs
3424
+ });
3425
+ }
3426
+ } else {
3427
+ pending.hitsSoFar.push(...hits);
3428
+ if (engramId) pending.engrams.push(engramId);
3429
+ }
3430
+ } else if (sig.type === SignalType.IMPRINTED) {
3431
+ const pending = this.pendingImprints.get(pid);
3432
+ if (pending === void 0 || pending.done) return;
3433
+ pending.done = true;
3434
+ pending.deferred.resolve({
3435
+ engramId: typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "",
3436
+ op: typeof sig.payload["op"] === "string" ? sig.payload["op"] : "",
3437
+ id: typeof sig.payload["id"] === "string" ? sig.payload["id"] : null,
3438
+ version: typeof sig.payload["version"] === "number" ? sig.payload["version"] : null,
3439
+ tookMs: typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null,
3440
+ error: typeof sig.payload["error"] === "string" ? sig.payload["error"] : null,
3441
+ ok: !(typeof sig.payload["error"] === "string")
3442
+ });
3443
+ }
3444
+ }
3445
+ /** Cancel every in-flight recall/imprint on a trace (FINAL/ERROR or shutdown). */
3446
+ cancelTrace(traceId) {
3447
+ const ids = this.byTrace.get(traceId);
3448
+ this.byTrace.delete(traceId);
3449
+ if (ids === void 0) return;
3450
+ for (const id of ids) {
3451
+ const pr = this.pendingRecalls.get(id);
3452
+ if (pr !== void 0 && !pr.done) {
3453
+ pr.done = true;
3454
+ if (pr.timer !== null) clearTimeout(pr.timer);
3455
+ pr.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while recall ${id} in flight`));
3456
+ this.pendingRecalls.delete(id);
3457
+ }
3458
+ const pi = this.pendingImprints.get(id);
3459
+ if (pi !== void 0 && !pi.done) {
3460
+ pi.done = true;
3461
+ if (pi.timer !== null) clearTimeout(pi.timer);
3462
+ pi.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while imprint ${id} in flight`));
3463
+ this.pendingImprints.delete(id);
3464
+ }
3465
+ }
3466
+ }
3467
+ cancelAll() {
3468
+ for (const traceId of [...this.byTrace.keys()]) this.cancelTrace(traceId);
3469
+ }
3470
+ onRecallDeadline(id) {
3471
+ const pending = this.pendingRecalls.get(id);
3472
+ if (pending === void 0 || pending.done) return;
3473
+ pending.done = true;
3474
+ if (pending.mode === "first") {
3475
+ pending.deferred.reject(new EngramTimeout(`RECALL ${id} elapsed deadline without any responder`));
3476
+ } else {
3477
+ pending.deferred.resolve({
3478
+ hits: [...pending.hitsSoFar].sort((a, b) => b.score - a.score),
3479
+ engramIds: [...pending.engrams],
3480
+ truncated: false,
3481
+ tookMs: null
3482
+ });
3483
+ }
3484
+ }
3485
+ onImprintDeadline(id) {
3486
+ const pending = this.pendingImprints.get(id);
3487
+ if (pending === void 0 || pending.done) return;
3488
+ pending.done = true;
3489
+ pending.deferred.reject(new EngramTimeout(`IMPRINT ${id} elapsed deadline without IMPRINTED`));
3490
+ }
3491
+ track(traceId, id) {
3492
+ const bucket = this.byTrace.get(traceId);
3493
+ if (bucket) bucket.add(id);
3494
+ else this.byTrace.set(traceId, /* @__PURE__ */ new Set([id]));
3495
+ }
3496
+ cleanupRecall(traceId, id) {
3497
+ const p = this.pendingRecalls.get(id);
3498
+ if (p?.timer != null) clearTimeout(p.timer);
3499
+ this.pendingRecalls.delete(id);
3500
+ this.discardTrace(traceId, id);
3501
+ }
3502
+ cleanupImprint(traceId, id) {
3503
+ const p = this.pendingImprints.get(id);
3504
+ if (p?.timer != null) clearTimeout(p.timer);
3505
+ this.pendingImprints.delete(id);
3506
+ this.discardTrace(traceId, id);
3507
+ }
3508
+ discardTrace(traceId, id) {
3509
+ const bucket = this.byTrace.get(traceId);
3510
+ if (bucket === void 0) return;
3511
+ bucket.delete(id);
3512
+ if (bucket.size === 0) this.byTrace.delete(traceId);
2470
3513
  }
2471
- return raw;
3514
+ };
3515
+ function hitsFromPayload(raw) {
3516
+ if (!Array.isArray(raw)) return [];
3517
+ const out = [];
3518
+ for (const h of raw) {
3519
+ if (h === null || typeof h !== "object") continue;
3520
+ const obj = h;
3521
+ const entryVal = obj["entry"];
3522
+ out.push({
3523
+ id: typeof obj["id"] === "string" ? obj["id"] : "",
3524
+ entry: entryVal !== null && typeof entryVal === "object" && !Array.isArray(entryVal) ? entryVal : { value: entryVal },
3525
+ score: typeof obj["score"] === "number" ? obj["score"] : 1
3526
+ });
3527
+ }
3528
+ return out;
2472
3529
  }
2473
3530
 
2474
3531
  // src/dendrite.ts
@@ -2479,33 +3536,72 @@ var DendriteProtocolError = class extends Error {
2479
3536
  this.name = "DendriteProtocolError";
2480
3537
  }
2481
3538
  };
2482
- var Dendrite = class {
3539
+ var Dendrite = class _Dendrite {
2483
3540
  synapse;
2484
3541
  registryStore;
2485
3542
  namespace;
2486
3543
  dendriteId;
3544
+ role;
2487
3545
  heartbeatMs;
2488
3546
  reregisterOnHeartbeat;
3547
+ autoBid;
3548
+ staleAfterMs;
2489
3549
  _axons = /* @__PURE__ */ new Map();
2490
3550
  handlers = /* @__PURE__ */ new Map();
2491
3551
  taskSub = null;
3552
+ routedTaskSub = null;
2492
3553
  inboundSubs = /* @__PURE__ */ new Map();
2493
- // Self-scheduling setTimeout handle (not setInterval - see startHeartbeatLoop).
3554
+ inflightSubs = /* @__PURE__ */ new Map();
3555
+ pendingSubs = /* @__PURE__ */ new Set();
3556
+ /** Recently seen CLARIFICATION_ANSWER / PERMISSION_DECISION signals keyed
3557
+ * by parent_id, so {@link awaitDecision} can serve an answer that arrived
3558
+ * before it was called (an in-process synapse can deliver the whole
3559
+ * request->answer chain within the original publish). Bounded FIFO. */
3560
+ recentDecisions = /* @__PURE__ */ new Map();
3561
+ /** Hosted Engrams keyed by engramId, plus a kind index so RECALL/IMPRINT
3562
+ * addressed by engramKind reach every matching host. */
3563
+ _engrams = /* @__PURE__ */ new Map();
3564
+ // In-flight neuron work keyed by trace_id so a STOP can abandon exactly
3565
+ // one workflow. JS can't force-kill a running async body, so abort means
3566
+ // 'stop awaiting + suppress the reply'; the neuron should also check the
3567
+ // AbortSignal cooperatively where it can.
3568
+ traceAborts = /* @__PURE__ */ new Map();
3569
+ engramKindIndex = /* @__PURE__ */ new Map();
3570
+ /** Engrams learned from peer REGISTER signals (possibly out-of-process). */
3571
+ _engramRegistrations = /* @__PURE__ */ new Map();
3572
+ engramRegKindIndex = /* @__PURE__ */ new Map();
3573
+ /** Caller-side correlation table for RECALL/IMPRINT awaiting
3574
+ * RECALLED/IMPRINTED. The Dendrite owns the subscriptions and feeds it. */
3575
+ engramClient = new EngramClient(this);
2494
3576
  heartbeatTimer = null;
2495
- // Set true by stop() so an in-flight tick won't re-arm the loop.
2496
3577
  heartbeatStopped = true;
2497
3578
  running = false;
3579
+ /** Open Pathways keyed by trace_id (dispatch / observePathway). */
3580
+ pathways = /* @__PURE__ */ new Map();
3581
+ /** Per-operation Pathways keyed by the issuing request's id (matched
3582
+ * against inbound parent_id) - the generic request/reply primitive
3583
+ * behind awaitDecision (and a future EngramClient wiring). */
3584
+ opPathways = /* @__PURE__ */ new Map();
2498
3585
  /** @internal - lifecycle hooks for this Dendrite. */
2499
3586
  hooks = new LifecycleHooks(this);
2500
3587
  constructor(opts) {
2501
3588
  if (!opts.synapse) throw new TypeError("Dendrite requires a synapse");
3589
+ const role = opts.role ?? "orchestrator";
3590
+ if (role !== "orchestrator" && role !== "worker") {
3591
+ throw new Error(`role must be 'orchestrator' or 'worker', got '${role}'`);
3592
+ }
2502
3593
  this.synapse = opts.synapse;
2503
3594
  this.registryStore = opts.registryStore ?? null;
2504
3595
  this.namespace = opts.namespace ?? "default";
2505
3596
  this.dendriteId = opts.dendriteId ?? "dendrite";
2506
3597
  this.heartbeatMs = opts.heartbeatMs ?? 3e4;
2507
3598
  this.reregisterOnHeartbeat = opts.reregisterOnHeartbeat ?? true;
2508
- for (const t of AXON_TYPES) this.handlers.set(t, []);
3599
+ this.role = role;
3600
+ this.autoBid = opts.autoBid ?? true;
3601
+ this.staleAfterMs = opts.staleAfterMs ?? (this.heartbeatMs > 0 ? this.heartbeatMs * 3 : 0);
3602
+ for (const t of Object.values(SignalType)) {
3603
+ this.handlers.set(t, []);
3604
+ }
2509
3605
  }
2510
3606
  // -- properties ----------------------------------------------------
2511
3607
  get axons() {
@@ -2514,70 +3610,393 @@ var Dendrite = class {
2514
3610
  axon(neuronId) {
2515
3611
  return this._axons.get(neuronId);
2516
3612
  }
3613
+ /** Aggregate of every attached Axon's capabilities, deduplicated + sorted. */
3614
+ get capabilities() {
3615
+ const caps = /* @__PURE__ */ new Set();
3616
+ for (const ax of this._axons.values()) for (const c of ax.capabilities) caps.add(c);
3617
+ return [...caps].sort();
3618
+ }
3619
+ /** Canonical queue-group name for this Dendrite's aggregate caps, or null
3620
+ * when no Axons are attached. Identical Dendrites share a group. */
3621
+ capQueueGroup() {
3622
+ const caps = this.capabilities;
3623
+ return caps.length ? `caps:${caps.join(",")}` : null;
3624
+ }
3625
+ requireOrchestrator(op) {
3626
+ if (this.role !== "orchestrator") {
3627
+ throw new DendriteProtocolError(
3628
+ `Dendrite role='${this.role}' cannot perform '${op}': only role='orchestrator' Dendrites may dispatch TASK signals. Workers host Axons and emit replies / cognition signals freely.`
3629
+ );
3630
+ }
3631
+ }
2517
3632
  // -- attachment ----------------------------------------------------
3633
+ /**
3634
+ * Attach an Axon to a *stopped* Dendrite. Throws if the Dendrite is
3635
+ * running - a running Dendrite needs the async activation path
3636
+ * (subscriptions, queue-group refresh, REGISTER): use
3637
+ * `await dendrite.addAxon(axon)` instead, which works in both states.
3638
+ */
2518
3639
  attachAxon(axon) {
3640
+ if (this.running) {
3641
+ throw new Error(
3642
+ "attachAxon on a running Dendrite would never receive TASKs (no subscription / REGISTER is set up after start). Use `await dendrite.addAxon(axon)` instead."
3643
+ );
3644
+ }
3645
+ this.attachAxonRecord(axon);
3646
+ }
3647
+ attachAxonRecord(axon) {
2519
3648
  if (this._axons.has(axon.neuronId)) {
2520
3649
  throw new Error(`Dendrite already has an Axon for neuronId='${axon.neuronId}'`);
2521
3650
  }
2522
3651
  this._axons.set(axon.neuronId, axon);
2523
3652
  axon[ATTACH](this);
2524
3653
  }
3654
+ /**
3655
+ * Attach an Axon; if the Dendrite is running, activate it live: ensure the
3656
+ * addressed + routed TASK subscriptions exist (re-keying the routed queue
3657
+ * group for the new aggregate cap profile), subscribe TASK_AWARDED /
3658
+ * DISCOVER (and TASK_OFFER when autoBid), mirror to the registry store,
3659
+ * emit REGISTER, and fire the Axon's onConnect hooks.
3660
+ */
3661
+ async addAxon(axon) {
3662
+ this.attachAxonRecord(axon);
3663
+ if (!this.running) return;
3664
+ if (this.taskSub === null) {
3665
+ this.taskSub = await this.synapse.subscribe(
3666
+ this.subject(SignalType.TASK),
3667
+ (s) => this.onTask(s)
3668
+ );
3669
+ }
3670
+ await this.refreshRoutedSub();
3671
+ await this.ensureInboundSub(SignalType.TASK_AWARDED);
3672
+ await this.ensureInboundSub(SignalType.DISCOVER);
3673
+ if (this.autoBid) await this.ensureInboundSub(SignalType.TASK_OFFER);
3674
+ await this.mirrorToStore(axon, "registered");
3675
+ await this.emitRegister(axon);
3676
+ await axon.hooks._fireConnect();
3677
+ axon.hooks._launchSchedule();
3678
+ }
3679
+ /** Detach an Axon. If running: deregister, tear down its hooks, and re-key
3680
+ * (or drop) the TASK subscriptions for the changed cap profile. */
3681
+ async detachAxon(neuronId, opts = {}) {
3682
+ const axon = this._axons.get(neuronId);
3683
+ if (!axon) {
3684
+ throw new Error(`Dendrite has no Axon for neuronId='${neuronId}'`);
3685
+ }
3686
+ if (this.running) {
3687
+ axon.hooks._stopHooks();
3688
+ if (this.registryStore !== null) {
3689
+ try {
3690
+ await this.registryStore.markDeregistered(neuronId);
3691
+ } catch {
3692
+ }
3693
+ }
3694
+ await this.emitDeregister(axon, opts.reason);
3695
+ }
3696
+ this._axons.delete(neuronId);
3697
+ axon[DETACH]();
3698
+ if (this.running && this._axons.size === 0) {
3699
+ if (this.taskSub !== null) {
3700
+ try {
3701
+ await this.taskSub.unsubscribe();
3702
+ } catch {
3703
+ }
3704
+ this.taskSub = null;
3705
+ }
3706
+ if (this.routedTaskSub !== null) {
3707
+ try {
3708
+ await this.routedTaskSub.unsubscribe();
3709
+ } catch {
3710
+ }
3711
+ this.routedTaskSub = null;
3712
+ }
3713
+ } else if (this.running) {
3714
+ await this.refreshRoutedSub();
3715
+ }
3716
+ }
3717
+ /**
3718
+ * Mount an Engram on this Dendrite. After attachment (and start), the
3719
+ * Dendrite subscribes to RECALL/IMPRINT, routes Signals addressed to
3720
+ * `engram.engramId` or matching `engram.engramKind` to the instance, and
3721
+ * announces it on the Synapse with an engram REGISTER. The Engram still
3722
+ * owns its backend lifecycle: `connect()` on start(), `close()` on stop().
3723
+ * When the Dendrite is already running, the backend is connected and the
3724
+ * subscriptions/REGISTER are established immediately.
3725
+ */
3726
+ async attachEngram(engram) {
3727
+ if (this._engrams.has(engram.engramId)) {
3728
+ throw new Error(`Dendrite already hosts an Engram with engramId='${engram.engramId}'`);
3729
+ }
3730
+ this._engrams.set(engram.engramId, engram);
3731
+ const bucket = this.engramKindIndex.get(engram.engramKind) ?? [];
3732
+ bucket.push(engram.engramId);
3733
+ this.engramKindIndex.set(engram.engramKind, bucket);
3734
+ if (this.running) {
3735
+ await engram.connect();
3736
+ await this.ensureInboundSub(SignalType.RECALL);
3737
+ await this.ensureInboundSub(SignalType.IMPRINT);
3738
+ await this.ensureInboundSub(SignalType.REGISTER);
3739
+ await this.emitEngramRegister(engram);
3740
+ }
3741
+ }
3742
+ /** Remove a hosted Engram. Closes its backend if the Dendrite is running. */
3743
+ async detachEngram(engramId) {
3744
+ const engram = this._engrams.get(engramId);
3745
+ if (!engram) {
3746
+ throw new Error(`Dendrite has no Engram with engramId='${engramId}'`);
3747
+ }
3748
+ if (this.running) {
3749
+ try {
3750
+ await engram.close();
3751
+ } catch {
3752
+ }
3753
+ }
3754
+ const bucket = this.engramKindIndex.get(engram.engramKind) ?? [];
3755
+ const kept = bucket.filter((id) => id !== engramId);
3756
+ if (kept.length) this.engramKindIndex.set(engram.engramKind, kept);
3757
+ else this.engramKindIndex.delete(engram.engramKind);
3758
+ this._engrams.delete(engramId);
3759
+ }
3760
+ get engrams() {
3761
+ return new Map(this._engrams);
3762
+ }
3763
+ /** Engrams learned via REGISTER, keyed by directed.id (or directed.type
3764
+ * when no id), including in-process ones. */
3765
+ get engramRegistrations() {
3766
+ return new Map(this._engramRegistrations);
3767
+ }
3768
+ /** True when an Engram with this id/kind is reachable - hosted
3769
+ * in-process or learned from a peer's REGISTER. */
3770
+ isEngramKnown(opts) {
3771
+ if (opts.engramId) {
3772
+ if (this._engrams.has(opts.engramId) || this._engramRegistrations.has(opts.engramId)) {
3773
+ return true;
3774
+ }
3775
+ }
3776
+ if (opts.engramKind) {
3777
+ if (this.engramKindIndex.has(opts.engramKind) || this.engramRegKindIndex.has(opts.engramKind)) {
3778
+ return true;
3779
+ }
3780
+ }
3781
+ return false;
3782
+ }
3783
+ /** (Re)subscribe the capability-routed TASK subscription so its queue
3784
+ * group matches the *current* aggregate cap profile. */
3785
+ async refreshRoutedSub() {
3786
+ const qgroup = this.capQueueGroup();
3787
+ if (this.routedTaskSub !== null) {
3788
+ try {
3789
+ await this.routedTaskSub.unsubscribe();
3790
+ } catch {
3791
+ }
3792
+ this.routedTaskSub = null;
3793
+ }
3794
+ if (qgroup !== null) {
3795
+ this.routedTaskSub = await this.synapse.subscribe(
3796
+ this.routedSubject(),
3797
+ (s) => this.onTask(s),
3798
+ { queueGroup: qgroup }
3799
+ );
3800
+ }
3801
+ }
2525
3802
  // -- inbound handler registration ----------------------------------
2526
- on(type, fn) {
2527
- const list = this.handlers.get(type);
2528
- if (!list) {
2529
- throw new DendriteProtocolError(`Cannot handle non-Axon type '${type}'`);
3803
+ wrapWithFilter(fn, filter) {
3804
+ if (!filter || filter.neuron === void 0 && filter.capability === void 0 && filter.traceId === void 0) {
3805
+ return fn;
2530
3806
  }
2531
- list.push(fn);
3807
+ return async (sig) => {
3808
+ const sigNeuron = sig.directed?.id ?? null;
3809
+ if (filter.neuron !== void 0 && sigNeuron !== filter.neuron) return;
3810
+ if (filter.traceId !== void 0 && sig.trace_id !== filter.traceId) return;
3811
+ if (filter.capability !== void 0) {
3812
+ if (!await this.neuronHasCapability(sigNeuron, filter.capability)) return;
3813
+ }
3814
+ await fn(sig);
3815
+ };
3816
+ }
3817
+ async neuronHasCapability(neuronId, capability) {
3818
+ if (!neuronId) return false;
3819
+ const axon = this._axons.get(neuronId);
3820
+ if (axon) return axon.capabilities.includes(capability);
3821
+ if (this.registryStore !== null) {
3822
+ try {
3823
+ const recs = await this.registryStore.list({ includeDeregistered: true });
3824
+ const rec = recs.find((r) => r.neuron_id === neuronId);
3825
+ return rec ? rec.capabilities.includes(capability) : false;
3826
+ } catch {
3827
+ return false;
3828
+ }
3829
+ }
3830
+ return false;
3831
+ }
3832
+ on(type, fn, filter) {
3833
+ const list = this.handlers.get(type);
3834
+ list.push(this.wrapWithFilter(fn, filter));
2532
3835
  if (this.running && !this.inboundSubs.has(type)) {
2533
- void this.ensureInboundSub(type);
3836
+ const p = this.ensureInboundSub(type).finally(() => this.pendingSubs.delete(p));
3837
+ this.pendingSubs.add(p);
3838
+ p.catch(() => {
3839
+ });
2534
3840
  }
2535
3841
  return fn;
2536
3842
  }
2537
- onAgentOutput(fn) {
2538
- return this.on(SignalType.AGENT_OUTPUT, fn);
3843
+ /**
3844
+ * Generic handler registration for *any* SignalType - the escape hatch
3845
+ * behind every named `on*` helper. New protocol types are observable the
3846
+ * day they exist. Supports the same filters as the named helpers.
3847
+ */
3848
+ onSignal(type, fn, filter) {
3849
+ return this.on(type, fn, filter);
3850
+ }
3851
+ /** Await until inbound subscriptions exist for `types` - removes the
3852
+ * late-registration race deterministically. Idempotent. */
3853
+ async ensureSubscribed(...types) {
3854
+ for (const t of types) await this.ensureInboundSub(t);
3855
+ }
3856
+ // -- lifecycle / reply handlers --
3857
+ onAgentOutput(fn, filter) {
3858
+ return this.on(SignalType.AGENT_OUTPUT, fn, filter);
2539
3859
  }
2540
- onClarification(fn) {
2541
- return this.on(SignalType.CLARIFICATION, fn);
3860
+ onClarification(fn, filter) {
3861
+ return this.on(SignalType.CLARIFICATION, fn, filter);
2542
3862
  }
2543
3863
  /**
2544
3864
  * Register a handler fired on inbound PERMISSION requests - the *answering*
2545
- * side. A central Cortex or a peer Dendrite evaluates the request (often
2546
- * consulting an Engram of standing grants, keyed per-neuron) and replies via
2547
- * {@link respondToPermission} (re-dispatch a TASK with the verdict) or
2548
- * {@link grantPermission} / {@link denyPermission} (emit a discrete
2549
- * PERMISSION_DECISION). It may also imprint the decision into an Engram so
2550
- * future recalls hit.
3865
+ * side. Reply via {@link respondToPermission} (re-dispatch a TASK with the
3866
+ * verdict) or {@link grantPermission} / {@link denyPermission} (emit a
3867
+ * discrete PERMISSION_DECISION).
2551
3868
  */
2552
- onPermission(fn) {
2553
- return this.on(SignalType.PERMISSION, fn);
3869
+ onPermission(fn, filter) {
3870
+ return this.on(SignalType.PERMISSION, fn, filter);
2554
3871
  }
2555
- onErrorSignal(fn) {
2556
- return this.on(SignalType.ERROR, fn);
3872
+ onErrorSignal(fn, filter) {
3873
+ return this.on(SignalType.ERROR, fn, filter);
2557
3874
  }
2558
- onRegister(fn) {
2559
- return this.on(SignalType.REGISTER, fn);
3875
+ /** Register a handler fired on FINAL - workflow conclusion. */
3876
+ onFinal(fn, filter) {
3877
+ return this.on(SignalType.FINAL, fn, filter);
2560
3878
  }
2561
- onDeregister(fn) {
2562
- return this.on(SignalType.DEREGISTER, fn);
2563
- }
2564
- onHeartbeat(fn) {
2565
- return this.on(SignalType.HEARTBEAT, fn);
3879
+ /** Observe inbound TASKs (audit/logging). Observation only - Axon routing
3880
+ * happens on its own subscription and is unaffected. */
3881
+ onTaskSignal(fn, filter) {
3882
+ return this.on(SignalType.TASK, fn, filter);
3883
+ }
3884
+ onRegister(fn, filter) {
3885
+ return this.on(SignalType.REGISTER, fn, filter);
3886
+ }
3887
+ onDeregister(fn, filter) {
3888
+ return this.on(SignalType.DEREGISTER, fn, filter);
3889
+ }
3890
+ onHeartbeat(fn, filter) {
3891
+ return this.on(SignalType.HEARTBEAT, fn, filter);
3892
+ }
3893
+ onDiscover(fn, filter) {
3894
+ return this.on(SignalType.DISCOVER, fn, filter);
3895
+ }
3896
+ // -- cognition handlers --
3897
+ onPlan(fn, filter) {
3898
+ return this.on(SignalType.PLAN, fn, filter);
3899
+ }
3900
+ onThoughtDelta(fn, filter) {
3901
+ return this.on(SignalType.THOUGHT_DELTA, fn, filter);
3902
+ }
3903
+ onToolCall(fn, filter) {
3904
+ return this.on(SignalType.TOOL_CALL, fn, filter);
3905
+ }
3906
+ onToolResult(fn, filter) {
3907
+ return this.on(SignalType.TOOL_RESULT, fn, filter);
3908
+ }
3909
+ onMemoryAppend(fn, filter) {
3910
+ return this.on(SignalType.MEMORY_APPEND, fn, filter);
3911
+ }
3912
+ onCritique(fn, filter) {
3913
+ return this.on(SignalType.CRITIQUE, fn, filter);
3914
+ }
3915
+ onEscalation(fn, filter) {
3916
+ return this.on(SignalType.ESCALATION, fn, filter);
3917
+ }
3918
+ onConsensus(fn, filter) {
3919
+ return this.on(SignalType.CONSENSUS, fn, filter);
3920
+ }
3921
+ onContextSync(fn, filter) {
3922
+ return this.on(SignalType.CONTEXT_SYNC, fn, filter);
3923
+ }
3924
+ // -- routing / market handlers --
3925
+ /** Workers use this to evaluate offers and call {@link bid} to compete.
3926
+ * Registering it suppresses the default auto-bidder entirely. */
3927
+ onTaskOffer(fn, filter) {
3928
+ return this.on(SignalType.TASK_OFFER, fn, filter);
3929
+ }
3930
+ /** Observe BIDs (market observability). dispatchOffer collects its own. */
3931
+ onBid(fn, filter) {
3932
+ return this.on(SignalType.BID, fn, filter);
3933
+ }
3934
+ /** Observe TASK_AWARDED. The hosting Dendrite's award-to-TASK synthesis is
3935
+ * unaffected by handlers here. */
3936
+ onTaskAwarded(fn, filter) {
3937
+ return this.on(SignalType.TASK_AWARDED, fn, filter);
3938
+ }
3939
+ /** e.g. release a reservation made while bidding. */
3940
+ onTaskDeclined(fn, filter) {
3941
+ return this.on(SignalType.TASK_DECLINED, fn, filter);
3942
+ }
3943
+ // -- discrete answer-path consumers --
3944
+ /** Fired on CLARIFICATION_ANSWER - correlate by `sig.parent_id === the
3945
+ * CLARIFICATION's id`, or use {@link awaitDecision}. */
3946
+ onClarificationAnswer(fn, filter) {
3947
+ return this.on(SignalType.CLARIFICATION_ANSWER, fn, filter);
3948
+ }
3949
+ /** Fired on PERMISSION_DECISION - correlate by parent_id, or use
3950
+ * {@link awaitDecision}. */
3951
+ onPermissionDecision(fn, filter) {
3952
+ return this.on(SignalType.PERMISSION_DECISION, fn, filter);
3953
+ }
3954
+ // -- memory-traffic observers --
3955
+ onRecalled(fn, filter) {
3956
+ return this.on(SignalType.RECALLED, fn, filter);
3957
+ }
3958
+ onImprinted(fn, filter) {
3959
+ return this.on(SignalType.IMPRINTED, fn, filter);
3960
+ }
3961
+ onRecallSignal(fn, filter) {
3962
+ return this.on(SignalType.RECALL, fn, filter);
3963
+ }
3964
+ onImprintSignal(fn, filter) {
3965
+ return this.on(SignalType.IMPRINT, fn, filter);
3966
+ }
3967
+ // -- trace-scoped helper --
3968
+ static TRACE_DEFAULT_TYPES = [
3969
+ SignalType.AGENT_OUTPUT,
3970
+ SignalType.FINAL,
3971
+ SignalType.ERROR,
3972
+ SignalType.PLAN,
3973
+ SignalType.THOUGHT_DELTA,
3974
+ SignalType.TOOL_CALL,
3975
+ SignalType.TOOL_RESULT,
3976
+ SignalType.MEMORY_APPEND,
3977
+ SignalType.CRITIQUE,
3978
+ SignalType.ESCALATION,
3979
+ SignalType.CONSENSUS,
3980
+ SignalType.CONTEXT_SYNC,
3981
+ SignalType.CLARIFICATION
3982
+ ];
3983
+ /** Register one handler for multiple types narrowed to a single workflow. */
3984
+ onTrace(traceId, fn, types) {
3985
+ for (const t of types ?? _Dendrite.TRACE_DEFAULT_TYPES) {
3986
+ this.on(t, fn, { traceId });
3987
+ }
3988
+ return fn;
2566
3989
  }
2567
3990
  // -- lifecycle hooks ----------------------------------------------
2568
- /** Register a fire-once handler called after start() completes. */
2569
3991
  onConnect(fn) {
2570
3992
  return this.hooks.onConnect(fn);
2571
3993
  }
2572
- /** Register a handler called whenever this Dendrite's state refreshes. */
2573
3994
  onRefresh(fn) {
2574
3995
  return this.hooks.onRefresh(fn);
2575
3996
  }
2576
- /** Register a periodic handler that runs every `everyMs` until stop(). */
2577
3997
  onSchedule(everyMs, fn) {
2578
3998
  return this.hooks.onSchedule(everyMs, fn);
2579
3999
  }
2580
- /** Manually fire a refresh event (reason defaults to "manual"). */
2581
4000
  async refresh(opts = {}) {
2582
4001
  await this.hooks.refresh(opts);
2583
4002
  }
@@ -2590,11 +4009,44 @@ var Dendrite = class {
2590
4009
  this.subject(SignalType.TASK),
2591
4010
  (s) => this.onTask(s)
2592
4011
  );
4012
+ const qgroup = this.capQueueGroup();
4013
+ if (qgroup !== null) {
4014
+ this.routedTaskSub = await this.synapse.subscribe(
4015
+ this.routedSubject(),
4016
+ (s) => this.onTask(s),
4017
+ { queueGroup: qgroup }
4018
+ );
4019
+ }
4020
+ await this.ensureInboundSub(SignalType.TASK_AWARDED);
4021
+ await this.ensureInboundSub(SignalType.DISCOVER);
4022
+ await this.ensureInboundSub(SignalType.STOP);
4023
+ if (this.autoBid) await this.ensureInboundSub(SignalType.TASK_OFFER);
2593
4024
  for (const axon of this._axons.values()) {
2594
4025
  await this.mirrorToStore(axon, "registered");
2595
4026
  await this.emitRegister(axon);
2596
4027
  }
2597
4028
  }
4029
+ if (this._engrams.size > 0) {
4030
+ for (const engram of this._engrams.values()) {
4031
+ try {
4032
+ await engram.connect();
4033
+ } catch {
4034
+ }
4035
+ }
4036
+ await this.ensureInboundSub(SignalType.RECALL);
4037
+ await this.ensureInboundSub(SignalType.IMPRINT);
4038
+ await this.ensureInboundSub(SignalType.FINAL);
4039
+ await this.ensureInboundSub(SignalType.ERROR);
4040
+ await this.ensureInboundSub(SignalType.REGISTER);
4041
+ for (const engram of this._engrams.values()) {
4042
+ try {
4043
+ await this.emitEngramRegister(engram);
4044
+ } catch {
4045
+ }
4046
+ }
4047
+ }
4048
+ await this.ensureInboundSub(SignalType.RECALLED);
4049
+ await this.ensureInboundSub(SignalType.IMPRINTED);
2598
4050
  for (const [type, hs] of this.handlers) {
2599
4051
  if (hs.length) await this.ensureInboundSub(type);
2600
4052
  }
@@ -2603,6 +4055,7 @@ var Dendrite = class {
2603
4055
  await this.ensureInboundSub(t);
2604
4056
  }
2605
4057
  }
4058
+ await this.ensureInboundSub(SignalType.STOP);
2606
4059
  this.running = true;
2607
4060
  if (this._axons.size > 0 && this.heartbeatMs > 0) {
2608
4061
  this.startHeartbeatLoop();
@@ -2614,17 +4067,6 @@ var Dendrite = class {
2614
4067
  axon.hooks._launchSchedule();
2615
4068
  }
2616
4069
  }
2617
- /**
2618
- * Heartbeat as a self-scheduling async loop rather than `setInterval`.
2619
- *
2620
- * Why not setInterval: it fires on a fixed wall-clock cadence regardless of
2621
- * whether the previous tick finished, so under load ticks overlap and the
2622
- * effective interval drifts; and because the callback is sync, any rejection
2623
- * from the async work inside is an unhandled rejection that setInterval
2624
- * silently drops. Here each tick is fully awaited, its errors are caught, and
2625
- * only then is the next tick scheduled - matching the Python SDK's
2626
- * asyncio.Task semantics (structured error handling + clean cancellation).
2627
- */
2628
4070
  startHeartbeatLoop() {
2629
4071
  this.heartbeatStopped = false;
2630
4072
  const schedule = () => {
@@ -2644,8 +4086,29 @@ var Dendrite = class {
2644
4086
  schedule();
2645
4087
  }
2646
4088
  async stop(reason) {
4089
+ for (const pw of [...this.pathways.values()]) {
4090
+ try {
4091
+ await pw.close();
4092
+ } catch {
4093
+ }
4094
+ }
4095
+ this.pathways.clear();
4096
+ for (const pw of [...this.opPathways.values()]) {
4097
+ try {
4098
+ await pw.close();
4099
+ } catch {
4100
+ }
4101
+ }
4102
+ this.opPathways.clear();
4103
+ this.engramClient.cancelAll();
2647
4104
  if (!this.running) return;
2648
4105
  this.running = false;
4106
+ for (const engram of this._engrams.values()) {
4107
+ try {
4108
+ await engram.close();
4109
+ } catch {
4110
+ }
4111
+ }
2649
4112
  this.hooks._stopHooks();
2650
4113
  for (const axon of this._axons.values()) axon.hooks._stopHooks();
2651
4114
  this.heartbeatStopped = true;
@@ -2657,6 +4120,10 @@ var Dendrite = class {
2657
4120
  await this.taskSub.unsubscribe();
2658
4121
  this.taskSub = null;
2659
4122
  }
4123
+ if (this.routedTaskSub !== null) {
4124
+ await this.routedTaskSub.unsubscribe();
4125
+ this.routedTaskSub = null;
4126
+ }
2660
4127
  for (const sub of this.inboundSubs.values()) {
2661
4128
  try {
2662
4129
  await sub.unsubscribe();
@@ -2674,20 +4141,6 @@ var Dendrite = class {
2674
4141
  await this.emitDeregister(axon, reason);
2675
4142
  }
2676
4143
  }
2677
- /**
2678
- * Explicit-resource-management hook so a Dendrite can be used with
2679
- * `await using` - the TS equivalent of Python's `async with dendrite:`.
2680
- *
2681
- * ```ts
2682
- * await using dendrite = new Dendrite({ synapse });
2683
- * dendrite.attachAxon(axon);
2684
- * await dendrite.start();
2685
- * // ... stop() runs automatically when this scope exits, even on throw.
2686
- * ```
2687
- *
2688
- * Idempotent: stop() is a no-op if the Dendrite was never started or already
2689
- * stopped. As with stop(), the caller still owns the Synapse/registry store.
2690
- */
2691
4144
  async [Symbol.asyncDispose]() {
2692
4145
  await this.stop();
2693
4146
  }
@@ -2702,29 +4155,390 @@ var Dendrite = class {
2702
4155
  }
2703
4156
  /** All known records, optionally filtered (live records only by default). */
2704
4157
  async registrySnapshot(opts = {}) {
2705
- return this.requireStore().list(opts);
4158
+ const { maxAgeMs, ...listOpts } = opts;
4159
+ const records = await this.requireStore().list(listOpts);
4160
+ return maxAgeMs !== void 0 ? _Dendrite.filterFresh(records, maxAgeMs) : records;
2706
4161
  }
2707
- /** Live (non-deregistered) records, optionally filtered by capability. */
4162
+ /** Live (non-deregistered) records, optionally filtered by capability.
4163
+ * `maxAgeMs` additionally drops records whose last heartbeat is older -
4164
+ * a read-side freshness guard when the background sweep can't be relied on. */
2708
4165
  async findNeurons(opts = {}) {
2709
- return this.requireStore().list({
4166
+ const records = await this.requireStore().list({
2710
4167
  ...opts.capability !== void 0 ? { capability: opts.capability } : {},
2711
4168
  includeDeregistered: false
2712
4169
  });
4170
+ return opts.maxAgeMs !== void 0 ? _Dendrite.filterFresh(records, opts.maxAgeMs) : records;
4171
+ }
4172
+ static filterFresh(records, maxAgeMs) {
4173
+ const now = Date.now();
4174
+ return records.filter((r) => {
4175
+ const seen = r.last_heartbeat ?? r.registered_at;
4176
+ if (!seen) return false;
4177
+ return now - Date.parse(seen) <= maxAgeMs;
4178
+ });
2713
4179
  }
2714
4180
  // -- outbound primitives ------------------------------------------
4181
+ /**
4182
+ * Emit a TASK. Addressed (`neuron`) or capability-routed (`capabilities`)
4183
+ * - at least one must be set. `finalize: true` tags the TASK so the
4184
+ * handling worker Dendrite promotes a successful AGENT_OUTPUT to FINAL
4185
+ * (terminal-handler finalize - see {@link dispatch}). Only
4186
+ * orchestrator-role Dendrites may dispatch.
4187
+ */
2715
4188
  async dispatchTask(args) {
4189
+ this.requireOrchestrator("dispatchTask");
4190
+ if (!args.neuron && !args.capabilities?.length) {
4191
+ throw new Error(
4192
+ "dispatchTask requires either neuron (addressed) or capabilities (capability-routed)"
4193
+ );
4194
+ }
2716
4195
  const sig = taskSignal({
2717
- directed: { id: args.neuron },
2718
4196
  input: args.input,
4197
+ ...args.neuron ? { directed: { id: args.neuron } } : {},
2719
4198
  ...args.traceId !== void 0 ? { traceId: args.traceId } : {},
2720
4199
  ...args.parentId !== void 0 ? { parentId: args.parentId } : {},
2721
4200
  ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
2722
4201
  ...args.capabilities !== void 0 ? { capabilities: args.capabilities } : {},
4202
+ ...args.finalize !== void 0 ? { finalize: args.finalize } : {},
4203
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4204
+ });
4205
+ await this.publishTask(sig);
4206
+ return sig;
4207
+ }
4208
+ /** Publish a TASK to the right subject for its routing mode. Addressed ->
4209
+ * broadcast subject; capability-routed -> queue-grouped routed subject. */
4210
+ async publishTask(sig) {
4211
+ const subject = sig.directed?.id ? this.subject(SignalType.TASK) : sig.payload["capabilities"]?.length ? this.routedSubject() : this.subject(SignalType.TASK);
4212
+ await this.synapse.publish(subject, sig);
4213
+ }
4214
+ // -- Pathway-based dispatch ----------------------------------------
4215
+ /**
4216
+ * Dispatch a TASK and return a {@link Pathway} scoped to its trace -
4217
+ * await it, attach callbacks, or iterate:
4218
+ *
4219
+ * ```ts
4220
+ * // 1) sequential / request-reply
4221
+ * const pw = await orch.dispatch({ neuron: "summarize", input });
4222
+ * const out = await pw.wait();
4223
+ *
4224
+ * // 2) reactive
4225
+ * pw.on(SignalType.PLAN, (sig) => { ... });
4226
+ *
4227
+ * // 3) streaming
4228
+ * for await (const sig of pw) { ... }
4229
+ * ```
4230
+ *
4231
+ * `capabilities` instead of `neuron` gives event-driven dispatch. Delivery
4232
+ * is exactly-once within a queue group (identical cap profiles) but
4233
+ * **at-least-once across heterogeneous groups** - use
4234
+ * {@link dispatchOffer} when overlapping profiles need an atomic claim.
4235
+ *
4236
+ * `scope: "terminal"` filters delivery to FINAL / ERROR / CLARIFICATION /
4237
+ * PERMISSION. `finalize` (default: true exactly when scope is "terminal")
4238
+ * tags the TASK for terminal-handler finalize: the worker Dendrite promotes
4239
+ * a successful AGENT_OUTPUT by also emitting FINAL - a default Axon never
4240
+ * emits FINAL itself, so a terminal-scoped Pathway would otherwise never
4241
+ * resolve against stock workers.
4242
+ */
4243
+ async dispatch(args) {
4244
+ this.requireOrchestrator("dispatch");
4245
+ if (!args.neuron && !args.capabilities?.length) {
4246
+ throw new Error(
4247
+ "dispatch requires either neuron (addressed) or capabilities (capability-routed)"
4248
+ );
4249
+ }
4250
+ const scope = args.scope ?? "all";
4251
+ const finalize = args.finalize ?? scope === "terminal";
4252
+ const tid = args.traceId ?? newTraceId();
4253
+ await this.ensurePathwaySubs();
4254
+ const pathway = new Pathway({
4255
+ traceId: tid,
4256
+ role: "originator",
4257
+ scope,
4258
+ onClose: (pw) => {
4259
+ this.pathways.delete(pw.traceId);
4260
+ }
4261
+ });
4262
+ this.pathways.set(tid, pathway);
4263
+ const sig = taskSignal({
4264
+ input: args.input,
4265
+ traceId: tid,
4266
+ ...args.neuron ? { directed: { id: args.neuron } } : {},
4267
+ ...args.parentId !== void 0 ? { parentId: args.parentId } : {},
4268
+ ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
4269
+ ...args.capabilities !== void 0 ? { capabilities: args.capabilities } : {},
4270
+ finalize,
4271
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4272
+ });
4273
+ try {
4274
+ await this.publishTask(sig);
4275
+ } catch (err) {
4276
+ this.pathways.delete(tid);
4277
+ await pathway.close();
4278
+ throw err;
4279
+ }
4280
+ return pathway;
4281
+ }
4282
+ /** Sync-shape sugar: dispatch, await the first matching Signal, close the
4283
+ * Pathway, return the Signal. Use `scope: "terminal"` to wait only for
4284
+ * FINAL / ERROR / CLARIFICATION / PERMISSION. */
4285
+ async dispatchAndWait(args) {
4286
+ const { timeoutMs, retry, ...rest } = args;
4287
+ if (retry) {
4288
+ return this.runWithRetry({ ...rest, retry, ...timeoutMs !== void 0 ? { timeoutMs } : {} });
4289
+ }
4290
+ const pathway = await this.dispatch(rest);
4291
+ try {
4292
+ return await pathway.wait(timeoutMs ?? 3e4);
4293
+ } finally {
4294
+ await pathway.close();
4295
+ }
4296
+ }
4297
+ /** Async-shape sugar: dispatch, return the live Pathway immediately. The
4298
+ * caller attaches `pw.on(...)` callbacks or iterates. */
4299
+ async dispatchAndSubscribe(args) {
4300
+ return this.dispatch(args);
4301
+ }
4302
+ /** Open a Pathway in *observer* role for a trace another peer started. */
4303
+ async observePathway(traceId) {
4304
+ if (this.pathways.has(traceId)) {
4305
+ throw new Error(`Dendrite already has a Pathway open for trace '${traceId}'`);
4306
+ }
4307
+ await this.ensurePathwaySubs();
4308
+ const pathway = new Pathway({
4309
+ traceId,
4310
+ role: "observer",
4311
+ onClose: (pw) => {
4312
+ this.pathways.delete(pw.traceId);
4313
+ }
4314
+ });
4315
+ this.pathways.set(traceId, pathway);
4316
+ return pathway;
4317
+ }
4318
+ async ensurePathwaySubs() {
4319
+ for (const t of PATHWAY_TYPES) await this.ensureInboundSub(t);
4320
+ }
4321
+ // -- per-operation (request/reply) Pathways -------------------------
4322
+ openOpPathway(opId, traceId) {
4323
+ const pathway = new Pathway({
4324
+ traceId,
4325
+ parentId: opId,
4326
+ role: "originator",
4327
+ onClose: (pw) => {
4328
+ if (pw.parentId !== null) this.opPathways.delete(pw.parentId);
4329
+ }
4330
+ });
4331
+ this.opPathways.set(opId, pathway);
4332
+ return pathway;
4333
+ }
4334
+ async cancelOpPathways(traceId) {
4335
+ for (const pw of [...this.opPathways.values()].filter((p) => p.traceId === traceId)) {
4336
+ try {
4337
+ await pw.close();
4338
+ } catch {
4339
+ }
4340
+ }
4341
+ }
4342
+ /**
4343
+ * Await the discrete answer to a CLARIFICATION or PERMISSION request.
4344
+ *
4345
+ * Opens a per-operation Pathway keyed on `request.id` and resolves on the
4346
+ * first CLARIFICATION_ANSWER / PERMISSION_DECISION whose parent_id matches.
4347
+ * The awaitable counterpart to {@link onClarificationAnswer} /
4348
+ * {@link onPermissionDecision}.
4349
+ */
4350
+ async awaitDecision(request, opts = {}) {
4351
+ let expected;
4352
+ if (request.type === SignalType.CLARIFICATION) {
4353
+ expected = SignalType.CLARIFICATION_ANSWER;
4354
+ } else if (request.type === SignalType.PERMISSION) {
4355
+ expected = SignalType.PERMISSION_DECISION;
4356
+ } else {
4357
+ throw new DendriteProtocolError(
4358
+ `awaitDecision expects a CLARIFICATION or PERMISSION signal, got '${request.type}'`
4359
+ );
4360
+ }
4361
+ await this.ensureInboundSub(expected);
4362
+ const cached = this.recentDecisions.get(request.id);
4363
+ if (cached && cached.type === expected) {
4364
+ this.recentDecisions.delete(request.id);
4365
+ return cached;
4366
+ }
4367
+ const pathway = this.openOpPathway(request.id, request.trace_id);
4368
+ try {
4369
+ return await pathway.waitFor(expected, opts.timeoutMs ?? 3e4);
4370
+ } finally {
4371
+ await pathway.close();
4372
+ }
4373
+ }
4374
+ // -- competitive bidding: TASK_OFFER / BID / TASK_AWARDED ------------
4375
+ /**
4376
+ * Broadcast a TASK_OFFER, collect BIDs for `deadlineMs`, award the winner
4377
+ * per `select` ("first_bid" | "lowest_cost" | "highest_confidence"), and
4378
+ * return a Pathway scoped to the resulting workflow. Losers get
4379
+ * TASK_DECLINED. Throws a TimeoutError-named Error when no BID arrives.
4380
+ * `finalize` follows the same rule as {@link dispatch}.
4381
+ */
4382
+ async dispatchOffer(args) {
4383
+ this.requireOrchestrator("dispatchOffer");
4384
+ const select = args.select ?? "first_bid";
4385
+ if (!["first_bid", "lowest_cost", "highest_confidence"].includes(select)) {
4386
+ throw new Error(
4387
+ `select must be 'first_bid' / 'lowest_cost' / 'highest_confidence', got '${select}'`
4388
+ );
4389
+ }
4390
+ const deadlineMs = args.deadlineMs ?? 250;
4391
+ const scope = args.scope ?? "all";
4392
+ const finalize = args.finalize ?? scope === "terminal";
4393
+ const tid = args.traceId ?? newTraceId();
4394
+ await this.ensurePathwaySubs();
4395
+ await this.ensureInboundSub(SignalType.BID);
4396
+ const pathway = new Pathway({
4397
+ traceId: tid,
4398
+ role: "originator",
4399
+ scope,
4400
+ onClose: (pw) => {
4401
+ this.pathways.delete(pw.traceId);
4402
+ }
4403
+ });
4404
+ this.pathways.set(tid, pathway);
4405
+ const offer = taskOfferSignal({
4406
+ traceId: tid,
4407
+ input: args.input,
4408
+ deadlineMs,
4409
+ ...args.parentId !== void 0 ? { parentId: args.parentId } : {},
4410
+ ...args.capabilities !== void 0 ? { capabilities: args.capabilities } : {},
4411
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4412
+ });
4413
+ const bids = [];
4414
+ let firstBid = null;
4415
+ const firstBidArrived = new Promise((resolve) => {
4416
+ firstBid = resolve;
4417
+ });
4418
+ pathway.on(SignalType.BID, (sig) => {
4419
+ bids.push(sig);
4420
+ if (select === "first_bid") firstBid?.(sig);
4421
+ });
4422
+ try {
4423
+ await this.emit(offer);
4424
+ } catch (err) {
4425
+ this.pathways.delete(tid);
4426
+ await pathway.close();
4427
+ throw err;
4428
+ }
4429
+ const sleep = (ms) => new Promise((r) => setTimeout(() => r(null), ms));
4430
+ if (select === "first_bid") {
4431
+ const winnerOrNull = await Promise.race([firstBidArrived, sleep(deadlineMs)]);
4432
+ if (winnerOrNull === null && bids.length === 0) {
4433
+ await pathway.close();
4434
+ const err = new Error(`dispatchOffer: no BID arrived within ${deadlineMs}ms`);
4435
+ err.name = "TimeoutError";
4436
+ throw err;
4437
+ }
4438
+ } else {
4439
+ await sleep(deadlineMs);
4440
+ if (bids.length === 0) {
4441
+ await pathway.close();
4442
+ const err = new Error(`dispatchOffer: no BID arrived within ${deadlineMs}ms`);
4443
+ err.name = "TimeoutError";
4444
+ throw err;
4445
+ }
4446
+ }
4447
+ let winner;
4448
+ if (select === "first_bid") {
4449
+ winner = bids[0];
4450
+ } else if (select === "lowest_cost") {
4451
+ winner = bids.reduce(
4452
+ (a, b) => (b.payload["cost"] ?? Infinity) < (a.payload["cost"] ?? Infinity) ? b : a
4453
+ );
4454
+ } else {
4455
+ winner = bids.reduce(
4456
+ (a, b) => (b.payload["confidence"] ?? -Infinity) > (a.payload["confidence"] ?? -Infinity) ? b : a
4457
+ );
4458
+ }
4459
+ for (const b of bids) {
4460
+ if (b.id === winner.id) continue;
4461
+ const bNeuron = b.directed?.id ?? null;
4462
+ try {
4463
+ await this.emit(
4464
+ taskDeclinedSignal({
4465
+ traceId: tid,
4466
+ parentId: b.id,
4467
+ reason: "not selected",
4468
+ ...bNeuron ? { directed: { id: bNeuron } } : {}
4469
+ })
4470
+ );
4471
+ } catch {
4472
+ }
4473
+ }
4474
+ const winnerNeuron = winner.directed?.id ?? null;
4475
+ const winningBid = {};
4476
+ for (const k of ["cost", "eta_ms", "confidence"]) {
4477
+ if (k in winner.payload) winningBid[k] = winner.payload[k];
4478
+ }
4479
+ const awarded = taskAwardedSignal({
4480
+ traceId: tid,
4481
+ parentId: winner.id,
4482
+ input: args.input,
4483
+ winningBid,
4484
+ finalize,
4485
+ ...winnerNeuron ? { directed: { id: winnerNeuron } } : {},
4486
+ ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {}
4487
+ });
4488
+ try {
4489
+ await this.emit(awarded);
4490
+ } catch (err) {
4491
+ await pathway.close();
4492
+ throw err;
4493
+ }
4494
+ return pathway;
4495
+ }
4496
+ /**
4497
+ * Emit a BID in response to a TASK_OFFER, on behalf of the local Axon named
4498
+ * by `neuron`. Bypasses the role guard - a worker bidding announces
4499
+ * capability, not orchestration.
4500
+ */
4501
+ async bid(offer, args) {
4502
+ if (offer.type !== SignalType.TASK_OFFER) {
4503
+ throw new DendriteProtocolError(
4504
+ `bid() expects a TASK_OFFER signal, got '${offer.type}'`
4505
+ );
4506
+ }
4507
+ const sig = bidSignal({
4508
+ traceId: offer.trace_id,
4509
+ parentId: offer.id,
4510
+ directed: { id: args.neuron },
4511
+ cost: args.cost,
4512
+ ...args.etaMs !== void 0 ? { etaMs: args.etaMs } : {},
4513
+ ...args.confidence !== void 0 ? { confidence: args.confidence } : {},
2723
4514
  ...args.meta !== void 0 ? { meta: args.meta } : {}
2724
4515
  });
2725
- await this.emit(sig);
4516
+ await this.publish(sig);
2726
4517
  return sig;
2727
4518
  }
4519
+ /** Default bidder: first hosted Axon whose caps cover the offer answers
4520
+ * (cost 0, confidence 1). No-op when nothing matches. */
4521
+ async maybeAutoBid(offer) {
4522
+ const requested = new Set(
4523
+ offer.payload["capabilities"] ?? []
4524
+ );
4525
+ for (const axon of this._axons.values()) {
4526
+ if (requested.size && ![...requested].every((c) => axon.capabilities.includes(c))) {
4527
+ continue;
4528
+ }
4529
+ try {
4530
+ await this.bid(offer, {
4531
+ neuron: axon.neuronId,
4532
+ cost: 0,
4533
+ confidence: 1,
4534
+ meta: { auto_bid: true }
4535
+ });
4536
+ } catch {
4537
+ }
4538
+ return;
4539
+ }
4540
+ }
4541
+ // -- reply / cognition emit helpers ---------------------------------
2728
4542
  async emitFinal(args) {
2729
4543
  const sig = finalSignal({
2730
4544
  traceId: args.traceId,
@@ -2749,16 +4563,187 @@ var Dendrite = class {
2749
4563
  await this.emit(sig);
2750
4564
  return sig;
2751
4565
  }
2752
- /** Emit a synapse-side Signal. Refuses Axon-owned types. */
4566
+ async emitPlan(args) {
4567
+ const sig = planSignal({
4568
+ traceId: args.traceId,
4569
+ parentId: args.parentId,
4570
+ steps: args.steps,
4571
+ directed: { id: args.neuron ?? this.dendriteId },
4572
+ ...args.rationale !== void 0 ? { rationale: args.rationale } : {},
4573
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4574
+ });
4575
+ await this.emit(sig);
4576
+ return sig;
4577
+ }
4578
+ async emitThoughtDelta(args) {
4579
+ const sig = thoughtDeltaSignal({
4580
+ traceId: args.traceId,
4581
+ parentId: args.parentId,
4582
+ delta: args.delta,
4583
+ directed: { id: args.neuron ?? this.dendriteId },
4584
+ ...args.seq !== void 0 ? { seq: args.seq } : {},
4585
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4586
+ });
4587
+ await this.emit(sig);
4588
+ return sig;
4589
+ }
4590
+ async emitToolCall(args) {
4591
+ const sig = toolCallSignal({
4592
+ traceId: args.traceId,
4593
+ parentId: args.parentId,
4594
+ tool: args.tool,
4595
+ args: args.args_,
4596
+ directed: { id: args.neuron ?? this.dendriteId },
4597
+ ...args.callId !== void 0 ? { callId: args.callId } : {},
4598
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4599
+ });
4600
+ await this.emit(sig);
4601
+ return sig;
4602
+ }
4603
+ async emitToolResult(args) {
4604
+ const sig = toolResultSignal({
4605
+ traceId: args.traceId,
4606
+ parentId: args.parentId,
4607
+ tool: args.tool,
4608
+ directed: { id: args.neuron ?? this.dendriteId },
4609
+ ...args.result !== void 0 ? { result: args.result } : {},
4610
+ ...args.error !== void 0 ? { error: args.error } : {},
4611
+ ...args.callId !== void 0 ? { callId: args.callId } : {},
4612
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4613
+ });
4614
+ await this.emit(sig);
4615
+ return sig;
4616
+ }
4617
+ async emitMemoryAppend(args) {
4618
+ const sig = memoryAppendSignal({
4619
+ traceId: args.traceId,
4620
+ parentId: args.parentId,
4621
+ key: args.key,
4622
+ value: args.value,
4623
+ directed: { id: args.neuron ?? this.dendriteId },
4624
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4625
+ });
4626
+ await this.emit(sig);
4627
+ return sig;
4628
+ }
4629
+ async emitCritique(args) {
4630
+ const sig = critiqueSignal({
4631
+ traceId: args.traceId,
4632
+ parentId: args.parentId,
4633
+ targetEventId: args.targetEventId,
4634
+ issues: args.issues,
4635
+ verdict: args.verdict,
4636
+ directed: { id: args.neuron ?? this.dendriteId },
4637
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4638
+ });
4639
+ await this.emit(sig);
4640
+ return sig;
4641
+ }
4642
+ async emitEscalation(args) {
4643
+ const sig = escalationSignal({
4644
+ traceId: args.traceId,
4645
+ parentId: args.parentId,
4646
+ reason: args.reason,
4647
+ directed: { id: args.neuron ?? this.dendriteId },
4648
+ ...args.target !== void 0 ? { target: args.target } : {},
4649
+ ...args.context !== void 0 ? { context: args.context } : {},
4650
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4651
+ });
4652
+ await this.emit(sig);
4653
+ return sig;
4654
+ }
4655
+ async emitConsensus(args) {
4656
+ const sig = consensusSignal({
4657
+ traceId: args.traceId,
4658
+ parentId: args.parentId,
4659
+ members: args.members,
4660
+ verdict: args.verdict,
4661
+ directed: { id: args.neuron ?? this.dendriteId },
4662
+ ...args.votes !== void 0 ? { votes: args.votes } : {},
4663
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4664
+ });
4665
+ await this.emit(sig);
4666
+ return sig;
4667
+ }
4668
+ async emitContextSync(args) {
4669
+ const sig = contextSyncSignal({
4670
+ traceId: args.traceId,
4671
+ parentId: args.parentId,
4672
+ snapshot: args.snapshot,
4673
+ directed: { id: args.neuron ?? this.dendriteId },
4674
+ ...args.version !== void 0 ? { version: args.version } : {},
4675
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4676
+ });
4677
+ await this.emit(sig);
4678
+ return sig;
4679
+ }
4680
+ // -- close-the-loop helpers ------------------------------------------
4681
+ /**
4682
+ * Reply to a CLARIFICATION by re-dispatching a TASK with the answer. The
4683
+ * new TASK is addressed by default to the asking Neuron, with parentId =
4684
+ * the clarification's id and the original traceId carried over. Input
4685
+ * shape: `{ clarification: { question, answer, ...extra } }`.
4686
+ */
4687
+ async respondToClarification(request, opts) {
4688
+ if (request.type !== SignalType.CLARIFICATION) {
4689
+ throw new DendriteProtocolError(
4690
+ `respondToClarification expects a CLARIFICATION signal, got '${request.type}'`
4691
+ );
4692
+ }
4693
+ const target = opts.neuron ?? request.directed?.id ?? null;
4694
+ if (!target) {
4695
+ throw new DendriteProtocolError(
4696
+ "respondToClarification: signal has no neuron and no neuron override - nowhere to dispatch the follow-up TASK"
4697
+ );
4698
+ }
4699
+ const clarification = {
4700
+ question: request.payload["question"] ?? null,
4701
+ answer: opts.answer
4702
+ };
4703
+ if (opts.extra !== void 0) Object.assign(clarification, opts.extra);
4704
+ return this.dispatchTask({
4705
+ neuron: target,
4706
+ input: { clarification },
4707
+ traceId: request.trace_id,
4708
+ parentId: request.id,
4709
+ ...opts.meta !== void 0 ? { meta: opts.meta } : {}
4710
+ });
4711
+ }
4712
+ /**
4713
+ * Reply to an ESCALATION by dispatching a TASK to the escalation target
4714
+ * (default: `payload.target`). Default input:
4715
+ * `{ escalation: { reason, context, from } }`.
4716
+ */
4717
+ async respondToEscalation(request, opts = {}) {
4718
+ if (request.type !== SignalType.ESCALATION) {
4719
+ throw new DendriteProtocolError(
4720
+ `respondToEscalation expects an ESCALATION signal, got '${request.type}'`
4721
+ );
4722
+ }
4723
+ const target = opts.neuron ?? request.payload["target"] ?? null;
4724
+ if (!target) {
4725
+ throw new DendriteProtocolError(
4726
+ "respondToEscalation: signal has no payload.target and no neuron override - nowhere to dispatch the follow-up TASK"
4727
+ );
4728
+ }
4729
+ const input = opts.input ?? {
4730
+ escalation: {
4731
+ reason: request.payload["reason"] ?? null,
4732
+ context: request.payload["context"] ?? null,
4733
+ from: request.directed?.id ?? null
4734
+ }
4735
+ };
4736
+ return this.dispatchTask({
4737
+ neuron: target,
4738
+ input,
4739
+ traceId: request.trace_id,
4740
+ parentId: request.id,
4741
+ ...opts.meta !== void 0 ? { meta: opts.meta } : {}
4742
+ });
4743
+ }
2753
4744
  /**
2754
4745
  * Reply to a PERMISSION by re-dispatching a TASK carrying the verdict.
2755
- *
2756
- * The "send it back to the axon" path: the follow-up TASK is addressed by
2757
- * default to the Neuron that asked (`signal.neuron`), with `parentId` = the
2758
- * PERMISSION's id and the original `traceId` carried over, so the Neuron
2759
- * resumes on the same thread and can imprint the decision into an Engram (or
2760
- * recall it next time). New TASK input: `{ permission: { action, granted,
2761
- * reason?, ttlMs?, ...extra } }`.
4746
+ * Input shape: `{ permission: { action, granted, reason?, ttl_ms?, ...extra } }`.
2762
4747
  */
2763
4748
  async respondToPermission(request, opts) {
2764
4749
  if (request.type !== SignalType.PERMISSION) {
@@ -2788,14 +4773,7 @@ var Dendrite = class {
2788
4773
  });
2789
4774
  }
2790
4775
  // -- cognition decision signals (discrete, decentralised option) -----
2791
- // Thin, stateless emit helpers for the new response signal types - no
2792
- // correlation client. Use these when you want the decision to travel as a
2793
- // discrete PERMISSION_DECISION / CLARIFICATION_ANSWER signal (e.g. for a
2794
- // peer/observer to imprint into an Engram) rather than as a re-dispatched
2795
- // TASK. Published via `publish` so any Dendrite - including a peer - can
2796
- // answer; correlation, if needed, is the developer's choice.
2797
- /** Approve a PERMISSION request. `ttlMs` optionally advertises how long the
2798
- * grant is valid so the requester can cache it (e.g. in an Engram). */
4776
+ /** Approve a PERMISSION request. */
2799
4777
  async grantPermission(request, opts = {}) {
2800
4778
  return this.decidePermission(request, true, opts);
2801
4779
  }
@@ -2809,707 +4787,755 @@ var Dendrite = class {
2809
4787
  `grant/denyPermission expects a PERMISSION signal, got '${request.type}'`
2810
4788
  );
2811
4789
  }
2812
- const sig = permissionDecisionSignal({
2813
- traceId: request.trace_id,
2814
- parentId: request.id,
2815
- granted,
2816
- directed: { id: this.dendriteId },
2817
- ...opts.reason !== void 0 ? { reason: opts.reason } : {},
2818
- ...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
2819
- ...opts.meta !== void 0 ? { meta: opts.meta } : {}
2820
- });
2821
- await this.publish(sig);
2822
- return sig;
2823
- }
2824
- /** Answer a *blocking* CLARIFICATION (the Neuron called ask(...) and is
2825
- * awaiting). Distinct from the legacy return-marker flow. */
2826
- async answerClarification(request, answer, opts = {}) {
2827
- if (request.type !== SignalType.CLARIFICATION) {
2828
- throw new DendriteProtocolError(
2829
- `answerClarification expects a CLARIFICATION signal, got '${request.type}'`
2830
- );
2831
- }
2832
- const sig = clarificationAnswerSignal({
2833
- traceId: request.trace_id,
2834
- parentId: request.id,
2835
- answer,
2836
- directed: { id: this.dendriteId },
2837
- ...opts.meta !== void 0 ? { meta: opts.meta } : {}
2838
- });
2839
- await this.publish(sig);
2840
- return sig;
2841
- }
2842
- async emit(signal) {
2843
- if (!SYNAPSE_TYPES.has(signal.type)) {
2844
- throw new DendriteProtocolError(
2845
- `Dendrite refuses to emit '${signal.type}': only synapse-side types may be emitted this way. '${signal.type}' is an Axon-owned type.`
2846
- );
2847
- }
2848
- await this.publish(signal);
2849
- }
2850
- async publish(signal) {
2851
- await this.synapse.publish(this.subject(signal.type), signal);
2852
- }
2853
- async subscribe(type, handler, opts) {
2854
- return this.synapse.subscribe(this.subject(type), handler, opts);
2855
- }
2856
- // -- internal ------------------------------------------------------
2857
- subject(type) {
2858
- return `cosmonapse.${this.namespace}.${type}`;
2859
- }
2860
- async ensureInboundSub(type) {
2861
- if (this.inboundSubs.has(type)) return;
2862
- const sub = await this.subscribe(type, (s) => this.dispatchInbound(s));
2863
- this.inboundSubs.set(type, sub);
2864
- }
2865
- async onTask(task) {
2866
- const target = task.directed?.id ?? null;
2867
- if (!target) return;
2868
- const axon = this._axons.get(target);
2869
- if (!axon) return;
2870
- let reply2;
2871
- try {
2872
- reply2 = await axon.handleTask(task);
2873
- } catch (err) {
2874
- reply2 = errorSignal({
2875
- traceId: task.trace_id,
2876
- parentId: task.id,
2877
- directed: { id: target },
2878
- code: "AXON_EXCEPTION",
2879
- message: err instanceof Error ? err.message : String(err),
2880
- recoverable: false
2881
- });
2882
- }
2883
- await this.publish(reply2);
2884
- }
2885
- async emitRegister(axon) {
2886
- await this.publish(
2887
- registerSignal({
2888
- directed: { id: axon.neuronId, capabilities: [...axon.capabilities] },
2889
- capabilities: axon.capabilities,
2890
- ...axon.version !== void 0 ? { version: axon.version } : {}
2891
- })
2892
- );
2893
- }
2894
- async emitDeregister(axon, reason) {
2895
- await this.publish(
2896
- deregisterSignal({
2897
- directed: { id: axon.neuronId },
2898
- ...reason !== void 0 ? { reason } : {}
2899
- })
2900
- );
2901
- }
2902
- async heartbeatTick() {
2903
- if (!this.running) return;
2904
- const now = (/* @__PURE__ */ new Date()).toISOString();
2905
- for (const axon of this._axons.values()) {
2906
- try {
2907
- if (this.reregisterOnHeartbeat) await this.emitRegister(axon);
2908
- await this.synapse.publish(
2909
- this.subject(SignalType.HEARTBEAT),
2910
- heartbeatSignal({ directed: { id: axon.neuronId } })
2911
- );
2912
- } catch {
2913
- }
2914
- if (this.registryStore !== null) {
2915
- try {
2916
- await this.registryStore.touchHeartbeat(axon.neuronId, now);
2917
- } catch {
2918
- }
2919
- }
2920
- await this.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
2921
- await axon.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
2922
- }
2923
- }
2924
- async mirrorToStore(axon, status) {
2925
- if (this.registryStore === null) return;
2926
- try {
2927
- await this.registryStore.upsert(
2928
- neuronRecord({
2929
- neuron_id: axon.neuronId,
2930
- capabilities: [...axon.capabilities],
2931
- version: axon.version ?? null,
2932
- status,
2933
- last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
2934
- })
2935
- );
2936
- } catch {
2937
- }
2938
- }
2939
- async dispatchInbound(signal) {
2940
- if (!AXON_TYPES.has(signal.type)) return;
2941
- if (this.registryStore !== null) {
2942
- try {
2943
- await this.updateRegistry(signal);
2944
- } catch {
2945
- }
2946
- }
2947
- const handlers = this.handlers.get(signal.type) ?? [];
2948
- if (!handlers.length) return;
2949
- await Promise.allSettled(handlers.map((h) => h(signal)));
2950
- }
2951
- async updateRegistry(signal) {
2952
- if (this.registryStore === null) return;
2953
- if (signal.payload["engram"]) return;
2954
- const neuronId = signal.directed?.id ?? null;
2955
- if (!neuronId) return;
2956
- let reason = null;
2957
- if (signal.type === SignalType.REGISTER) {
2958
- await this.registryStore.upsert(
2959
- neuronRecord({
2960
- neuron_id: neuronId,
2961
- capabilities: signal.payload["capabilities"] ?? [],
2962
- version: signal.payload["version"] ?? null,
2963
- status: "registered",
2964
- last_heartbeat: signal.ts
2965
- })
2966
- );
2967
- reason = "register";
2968
- } else if (signal.type === SignalType.DEREGISTER) {
2969
- await this.registryStore.markDeregistered(neuronId);
2970
- reason = "deregister";
2971
- } else if (signal.type === SignalType.HEARTBEAT) {
2972
- const status = signal.payload["status"];
2973
- if (status) await this.registryStore.touchHeartbeat(neuronId, signal.ts, status);
2974
- else await this.registryStore.touchHeartbeat(neuronId, signal.ts);
2975
- reason = "heartbeat";
2976
- }
2977
- if (reason !== null) {
2978
- await this.hooks._fireRefresh({ reason, neuronId, extra: {} });
2979
- }
4790
+ const sig = permissionDecisionSignal({
4791
+ traceId: request.trace_id,
4792
+ parentId: request.id,
4793
+ granted,
4794
+ directed: { id: this.dendriteId },
4795
+ ...opts.reason !== void 0 ? { reason: opts.reason } : {},
4796
+ ...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
4797
+ ...opts.meta !== void 0 ? { meta: opts.meta } : {}
4798
+ });
4799
+ await this.publish(sig);
4800
+ return sig;
2980
4801
  }
2981
- };
2982
- var Cortex = Dendrite;
2983
-
2984
- // src/engram.ts
2985
- var EngramBinding = class {
2986
- name;
2987
- directedId;
2988
- directedType;
2989
- defaultDeadlineMs;
2990
- defaultRecallMode;
2991
- constructor(init) {
2992
- this.name = init.name;
2993
- this.directedId = init.directedId ?? null;
2994
- this.directedType = init.directedType ?? null;
2995
- this.defaultDeadlineMs = init.defaultDeadlineMs ?? null;
2996
- this.defaultRecallMode = init.defaultRecallMode ?? "first";
2997
- if (!this.directedId && !this.directedType) {
2998
- throw new Error(
2999
- `EngramBinding '${this.name}' requires directedId (engram_id) or directedType (engram_kind), or both`
4802
+ /** Answer a CLARIFICATION with a discrete CLARIFICATION_ANSWER signal
4803
+ * (parent_id = the request's id). Consumers pick it up via
4804
+ * {@link onClarificationAnswer} or {@link awaitDecision}. Distinct from
4805
+ * {@link respondToClarification}, which re-dispatches a TASK. */
4806
+ async answerClarification(request, answer, opts = {}) {
4807
+ if (request.type !== SignalType.CLARIFICATION) {
4808
+ throw new DendriteProtocolError(
4809
+ `answerClarification expects a CLARIFICATION signal, got '${request.type}'`
3000
4810
  );
3001
4811
  }
4812
+ const sig = clarificationAnswerSignal({
4813
+ traceId: request.trace_id,
4814
+ parentId: request.id,
4815
+ answer,
4816
+ directed: { id: this.dendriteId },
4817
+ ...opts.meta !== void 0 ? { meta: opts.meta } : {}
4818
+ });
4819
+ await this.publish(sig);
4820
+ return sig;
3002
4821
  }
3003
- /** Build the `Directed` addressing this Engram. */
3004
- toDirected() {
3005
- return { id: this.directedId, type: this.directedType, capabilities: [] };
3006
- }
3007
- };
3008
- var EngramError = class extends Error {
3009
- constructor(message) {
3010
- super(message);
3011
- this.name = new.target.name;
4822
+ /** Emit a synapse-side Signal. Refuses Axon-owned types; TASK initiation
4823
+ * additionally requires orchestrator role. */
4824
+ async emit(signal) {
4825
+ if (signal.type === SignalType.TASK) {
4826
+ this.requireOrchestrator(`emit(${signal.type})`);
4827
+ }
4828
+ if (!SYNAPSE_TYPES.has(signal.type)) {
4829
+ throw new DendriteProtocolError(
4830
+ `Dendrite refuses to emit '${signal.type}': only synapse-side types may be emitted this way. '${signal.type}' is an Axon-owned type.`
4831
+ );
4832
+ }
4833
+ await this.publish(signal);
3012
4834
  }
3013
- };
3014
- var EngramTimeout = class extends EngramError {
3015
- };
3016
- var EngramCancelled = class extends EngramError {
3017
- };
3018
- var EngramNotBound = class extends EngramError {
3019
- };
3020
- var EngramOverloaded = class extends EngramError {
3021
- };
3022
- var Engram = class {
3023
- version = null;
3024
- /** Return false if this Engram cannot satisfy the query. Default: serve all. */
3025
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
3026
- async canServe(_query) {
3027
- return true;
4835
+ async publish(signal) {
4836
+ await this.synapse.publish(this.subject(signal.type), signal);
3028
4837
  }
3029
- };
3030
- function receipt(engramId, op, fields = {}) {
3031
- const error = fields.error ?? null;
3032
- return {
3033
- engramId,
3034
- op,
3035
- id: fields.id ?? null,
3036
- version: fields.version ?? null,
3037
- tookMs: fields.tookMs ?? null,
3038
- error,
3039
- ok: error === null
3040
- };
3041
- }
3042
- function entryToDict(e) {
3043
- const out = {
3044
- id: e.id,
3045
- content: e.content,
3046
- tags: [...e.tags],
3047
- version: e.version,
3048
- created_at: e.createdAt,
3049
- updated_at: e.updatedAt
3050
- };
3051
- if (e.mergeKey !== null) out["merge_key"] = e.mergeKey;
3052
- if (Object.keys(e.extra).length > 0) out["meta"] = { ...e.extra };
3053
- return out;
3054
- }
3055
- function asStringArray(v) {
3056
- return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
3057
- }
3058
- function asObject(v) {
3059
- return v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
3060
- }
3061
- var InMemoryEngram = class extends Engram {
3062
- engramId;
3063
- engramKind;
3064
- capabilities;
3065
- entries = /* @__PURE__ */ new Map();
3066
- byMergeKey = /* @__PURE__ */ new Map();
3067
- imprintSeen = /* @__PURE__ */ new Map();
3068
- constructor(init = {}) {
3069
- super();
3070
- this.engramId = init.engramId ?? "engram-memory";
3071
- this.engramKind = init.engramKind ?? "keyvalue";
3072
- this.capabilities = init.capabilities ?? ["substring", "tags", "merge_key"];
3073
- this.version = init.version ?? "0.0.1";
4838
+ async subscribe(type, handler, opts) {
4839
+ return this.synapse.subscribe(this.subject(type), handler, opts);
3074
4840
  }
3075
- async connect() {
3076
- return;
4841
+ // -- internal ------------------------------------------------------
4842
+ subject(type) {
4843
+ return `cosmonapse.${this.namespace}.${type}`;
3077
4844
  }
3078
- async close() {
3079
- this.entries.clear();
3080
- this.byMergeKey.clear();
3081
- this.imprintSeen.clear();
4845
+ /** Subject for capability-routed TASKs (queue-grouped consumers). */
4846
+ routedSubject() {
4847
+ return `cosmonapse.${this.namespace}.${SignalType.TASK}.routed`;
4848
+ }
4849
+ ensureInboundSub(type) {
4850
+ if (this.inboundSubs.has(type)) return Promise.resolve();
4851
+ let p = this.inflightSubs.get(type);
4852
+ if (!p) {
4853
+ p = (async () => {
4854
+ const sub = await this.subscribe(type, (s) => this.dispatchInbound(s));
4855
+ this.inboundSubs.set(type, sub);
4856
+ })().finally(() => this.inflightSubs.delete(type));
4857
+ this.inflightSubs.set(type, p);
4858
+ }
4859
+ return p;
3082
4860
  }
3083
- async recall(query, opts = {}) {
3084
- const q = query ?? {};
3085
- const text = typeof q["text"] === "string" ? q["text"].toLowerCase() : "";
3086
- const tagQ = typeof q["tag"] === "string" ? q["tag"] : null;
3087
- const mergeKey = typeof q["merge_key"] === "string" ? q["merge_key"] : null;
3088
- const topK = typeof q["top_k"] === "number" ? q["top_k"] : 50;
3089
- const filters = opts.filters ?? {};
3090
- const requireTags = asStringArray(filters["tags"]);
3091
- const since = typeof filters["since"] === "string" ? Date.parse(filters["since"]) : NaN;
3092
- const until = typeof filters["until"] === "string" ? Date.parse(filters["until"]) : NaN;
3093
- let candidates;
3094
- if (mergeKey !== null) {
3095
- const ids = this.byMergeKey.get(mergeKey) ?? [];
3096
- candidates = ids.map((i) => this.entries.get(i)).filter((e) => e !== void 0);
4861
+ /**
4862
+ * Route an inbound TASK to a local Axon. Addressed: by neuron_id (drop if
4863
+ * not hosted here). Capability-routed: first local Axon whose caps superset
4864
+ * the request. After publishing the reply, apply terminal-handler finalize:
4865
+ * a TASK tagged `payload.finalize` promotes a successful AGENT_OUTPUT by
4866
+ * also emitting FINAL (parented to the AGENT_OUTPUT, attributed to the
4867
+ * producing Neuron). Only AGENT_OUTPUT is promoted - CLARIFICATION /
4868
+ * PERMISSION pause the workflow and ERROR is already terminal.
4869
+ */
4870
+ async onTask(task) {
4871
+ let target = task.directed?.id ?? null;
4872
+ let axon;
4873
+ if (target) {
4874
+ axon = this._axons.get(target);
4875
+ if (!axon) return;
3097
4876
  } else {
3098
- candidates = [...this.entries.values()];
4877
+ const requested = task.payload["capabilities"] ?? [];
4878
+ if (!requested.length) return;
4879
+ for (const candidate of this._axons.values()) {
4880
+ if (requested.every((c) => candidate.capabilities.includes(c))) {
4881
+ axon = candidate;
4882
+ break;
4883
+ }
4884
+ }
4885
+ if (!axon) return;
4886
+ target = axon.neuronId;
3099
4887
  }
3100
- const hits = [];
3101
- for (const ent of candidates) {
3102
- if (requireTags.length > 0 && !requireTags.every((t) => ent.tags.includes(t))) continue;
3103
- const updated = Date.parse(ent.updatedAt);
3104
- if (!Number.isNaN(since) && updated < since) continue;
3105
- if (!Number.isNaN(until) && updated > until) continue;
3106
- if (tagQ !== null && !ent.tags.includes(tagQ)) continue;
3107
- let score = 1;
3108
- if (text) {
3109
- const hay = String(ent.content).toLowerCase();
3110
- if (!hay.includes(text)) continue;
3111
- score = Math.min(1, text.length / Math.max(1, hay.length));
4888
+ const ac = new AbortController();
4889
+ this.registerTraceAbort(task.trace_id, ac);
4890
+ let reply2;
4891
+ try {
4892
+ reply2 = await this.raceAbort(axon.handleTask(task), ac.signal);
4893
+ } catch (err) {
4894
+ reply2 = errorSignal({
4895
+ traceId: task.trace_id,
4896
+ parentId: task.id,
4897
+ directed: { id: target },
4898
+ code: "AXON_EXCEPTION",
4899
+ message: err instanceof Error ? err.message : String(err),
4900
+ recoverable: false
4901
+ });
4902
+ } finally {
4903
+ this.unregisterTraceAbort(task.trace_id, ac);
4904
+ }
4905
+ if (reply2 === null) {
4906
+ return;
4907
+ }
4908
+ await this.publish(reply2);
4909
+ if (reply2.type === SignalType.AGENT_OUTPUT && task.payload["finalize"]) {
4910
+ try {
4911
+ await this.publish(
4912
+ finalSignal({
4913
+ traceId: reply2.trace_id,
4914
+ parentId: reply2.id,
4915
+ directed: { id: target },
4916
+ result: reply2.payload["output"] ?? {}
4917
+ })
4918
+ );
4919
+ } catch {
3112
4920
  }
3113
- if (opts.minConfidence !== void 0 && score < opts.minConfidence) continue;
3114
- hits.push({ id: ent.id, entry: entryToDict(ent), score });
3115
4921
  }
3116
- hits.sort((a, b) => b.score - a.score);
3117
- return hits.slice(0, topK);
3118
4922
  }
3119
- async imprint(op, entry, opts = {}) {
3120
- const t0 = Date.now();
3121
- const mergeKey = opts.mergeKey ?? null;
3122
- const tookMs = () => Date.now() - t0;
3123
- if (opts.imprintId !== void 0) {
3124
- const seen = this.imprintSeen.get(opts.imprintId);
3125
- if (seen !== void 0) {
3126
- const existing = this.entries.get(seen);
3127
- return receipt(this.engramId, op, {
3128
- id: seen,
3129
- version: existing ? existing.version : null,
3130
- tookMs: tookMs()
3131
- });
3132
- }
4923
+ // -- workflow control: STOP / STOPPED -------------------------------
4924
+ registerTraceAbort(traceId, ac) {
4925
+ let set = this.traceAborts.get(traceId);
4926
+ if (!set) {
4927
+ set = /* @__PURE__ */ new Set();
4928
+ this.traceAborts.set(traceId, set);
3133
4929
  }
3134
- let resultingId = null;
3135
- let version = null;
3136
- if (op === "add") {
3137
- const ent = this.makeEntry(entry, mergeKey);
3138
- if (this.entries.has(ent.id)) {
3139
- return receipt(this.engramId, op, { error: `entry id '${ent.id}' already exists`, tookMs: tookMs() });
3140
- }
3141
- this.store(ent);
3142
- resultingId = ent.id;
3143
- version = ent.version;
3144
- } else if (op === "append") {
3145
- let ent = this.makeEntry(entry, mergeKey);
3146
- while (this.entries.has(ent.id)) {
3147
- ent = this.makeEntry({ ...entry, id: newEngramId() }, mergeKey);
3148
- }
3149
- this.store(ent);
3150
- resultingId = ent.id;
3151
- version = ent.version;
3152
- } else if (op === "upsert") {
3153
- const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
3154
- const targetId = existingIds[existingIds.length - 1];
3155
- const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
3156
- if (old !== void 0) {
3157
- const next = this.makeEntry({ ...entry, id: old.id }, mergeKey);
3158
- next.createdAt = old.createdAt;
3159
- next.version = old.version + 1;
3160
- this.store(next, true);
3161
- resultingId = next.id;
3162
- version = next.version;
3163
- } else {
3164
- const ent = this.makeEntry(entry, mergeKey);
3165
- this.store(ent);
3166
- resultingId = ent.id;
3167
- version = ent.version;
3168
- }
3169
- } else if (op === "merge") {
3170
- const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
3171
- const targetId = existingIds[existingIds.length - 1];
3172
- const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
3173
- if (old === void 0) {
3174
- return receipt(this.engramId, op, { error: `no entry for merge_key='${mergeKey}'`, tookMs: tookMs() });
4930
+ set.add(ac);
4931
+ }
4932
+ unregisterTraceAbort(traceId, ac) {
4933
+ const set = this.traceAborts.get(traceId);
4934
+ if (set) {
4935
+ set.delete(ac);
4936
+ if (set.size === 0) this.traceAborts.delete(traceId);
4937
+ }
4938
+ }
4939
+ /** Resolve to the promise's value, or to null if the signal aborts first. */
4940
+ raceAbort(p, signal) {
4941
+ if (signal.aborted) return Promise.resolve(null);
4942
+ return new Promise((resolve, reject) => {
4943
+ const onAbort = () => resolve(null);
4944
+ signal.addEventListener("abort", onAbort, { once: true });
4945
+ p.then(
4946
+ (v) => {
4947
+ signal.removeEventListener("abort", onAbort);
4948
+ resolve(v);
4949
+ },
4950
+ (e) => {
4951
+ signal.removeEventListener("abort", onAbort);
4952
+ reject(e);
4953
+ }
4954
+ );
4955
+ });
4956
+ }
4957
+ async onStop(signal) {
4958
+ const traceId = signal.trace_id;
4959
+ if (!traceId) return;
4960
+ const rollback = Boolean(signal.payload["rollback"]);
4961
+ let cancelled = 0;
4962
+ let compensated = 0;
4963
+ let didWork = false;
4964
+ const acs = this.traceAborts.get(traceId);
4965
+ if (acs) {
4966
+ for (const ac of acs) {
4967
+ if (!ac.signal.aborted) {
4968
+ ac.abort();
4969
+ cancelled++;
4970
+ }
3175
4971
  }
3176
- const now = (/* @__PURE__ */ new Date()).toISOString();
3177
- const next = {
3178
- id: old.id,
3179
- content: deepMerge(old.content, entry["content"]),
3180
- tags: [.../* @__PURE__ */ new Set([...old.tags, ...asStringArray(entry["tags"])])],
3181
- mergeKey: old.mergeKey,
3182
- version: old.version + 1,
3183
- createdAt: old.createdAt,
3184
- updatedAt: now,
3185
- extra: asObject(deepMerge(old.extra, entry["meta"]))
3186
- };
3187
- this.store(next, true);
3188
- resultingId = next.id;
3189
- version = next.version;
3190
- } else if (op === "delete") {
3191
- let targetId = null;
3192
- const entId = entry["id"];
3193
- if (typeof entId === "string") {
3194
- targetId = entId;
3195
- } else if (mergeKey !== null) {
3196
- const ids = this.byMergeKey.get(mergeKey) ?? [];
3197
- targetId = ids[ids.length - 1] ?? null;
4972
+ this.traceAborts.delete(traceId);
4973
+ didWork = true;
4974
+ }
4975
+ try {
4976
+ await this.cancelOpPathways(traceId);
4977
+ this.engramClient.cancelTrace(traceId);
4978
+ } catch {
4979
+ }
4980
+ for (const engram of this._engrams.values()) {
4981
+ try {
4982
+ if (rollback) {
4983
+ const n = await engram.compensate(traceId);
4984
+ if (n > 0) {
4985
+ compensated += n;
4986
+ didWork = true;
4987
+ }
4988
+ } else {
4989
+ await engram.commit(traceId);
4990
+ }
4991
+ } catch {
3198
4992
  }
3199
- if (targetId === null || !this.entries.has(targetId)) {
3200
- return receipt(this.engramId, op, { tookMs: tookMs() });
4993
+ }
4994
+ const pw = this.pathways.get(traceId);
4995
+ if (pw && !pw.closed) {
4996
+ didWork = true;
4997
+ try {
4998
+ await pw.close();
4999
+ } catch {
3201
5000
  }
3202
- this.evict(targetId);
3203
- resultingId = targetId;
3204
- version = null;
3205
- } else {
3206
- return receipt(this.engramId, op, { error: `unknown op '${op}'`, tookMs: tookMs() });
3207
5001
  }
3208
- if (opts.imprintId !== void 0 && resultingId !== null) {
3209
- this.imprintSeen.set(opts.imprintId, resultingId);
5002
+ if (didWork) {
5003
+ try {
5004
+ await this.publish(
5005
+ stoppedSignal({
5006
+ traceId,
5007
+ parentId: signal.id,
5008
+ node: this.namespace,
5009
+ rolledBack: rollback,
5010
+ cancelled,
5011
+ compensated
5012
+ })
5013
+ );
5014
+ } catch {
5015
+ }
3210
5016
  }
3211
- return receipt(this.engramId, op, { id: resultingId, version, tookMs: tookMs() });
3212
5017
  }
3213
- /** Test/debug helper - NOT part of the Engram contract. */
3214
- snapshot() {
3215
- return [...this.entries.values()].map(entryToDict);
5018
+ /** Broadcast a STOP for `traceId` (orchestrator-gated). Best-effort and
5019
+ * idempotent. */
5020
+ async emitStop(args) {
5021
+ this.requireOrchestrator("emitStop");
5022
+ await this.ensureInboundSub(SignalType.STOP);
5023
+ const sig = stopSignal({
5024
+ traceId: args.traceId,
5025
+ ...args.rollback !== void 0 ? { rollback: args.rollback } : {},
5026
+ ...args.reason !== void 0 ? { reason: args.reason } : {}
5027
+ });
5028
+ await this.publish(sig);
5029
+ return sig;
3216
5030
  }
3217
- makeEntry(entry, mergeKey) {
3218
- const id = typeof entry["id"] === "string" ? entry["id"] : newEngramId();
3219
- const now = (/* @__PURE__ */ new Date()).toISOString();
3220
- return {
3221
- id,
3222
- content: entry["content"],
3223
- tags: asStringArray(entry["tags"]),
3224
- mergeKey,
3225
- version: 1,
3226
- createdAt: now,
3227
- updatedAt: now,
3228
- extra: asObject(entry["meta"])
5031
+ /** Stop a whole workflow. With `collectAcks` returns the STOPPED acks seen
5032
+ * within `timeoutMs` (best effort). */
5033
+ async stopTrace(traceId, opts = {}) {
5034
+ if (!opts.collectAcks) {
5035
+ await this.emitStop({
5036
+ traceId,
5037
+ ...opts.rollback !== void 0 ? { rollback: opts.rollback } : {},
5038
+ ...opts.reason !== void 0 ? { reason: opts.reason } : {}
5039
+ });
5040
+ return [];
5041
+ }
5042
+ const acks = [];
5043
+ const collect = async (sig) => {
5044
+ if (sig.trace_id === traceId) acks.push(sig);
3229
5045
  };
5046
+ const list = this.handlers.get(SignalType.STOPPED) ?? [];
5047
+ list.push(collect);
5048
+ this.handlers.set(SignalType.STOPPED, list);
5049
+ await this.ensureInboundSub(SignalType.STOPPED);
5050
+ try {
5051
+ await this.emitStop({
5052
+ traceId,
5053
+ ...opts.rollback !== void 0 ? { rollback: opts.rollback } : {},
5054
+ ...opts.reason !== void 0 ? { reason: opts.reason } : {}
5055
+ });
5056
+ await new Promise((r) => setTimeout(r, opts.timeoutMs ?? 1e3));
5057
+ } finally {
5058
+ const idx = (this.handlers.get(SignalType.STOPPED) ?? []).indexOf(collect);
5059
+ if (idx >= 0) (this.handlers.get(SignalType.STOPPED) ?? []).splice(idx, 1);
5060
+ }
5061
+ return acks;
3230
5062
  }
3231
- store(ent, replace = false) {
3232
- if (replace) {
3233
- const old = this.entries.get(ent.id);
3234
- if (old !== void 0 && old.mergeKey) {
3235
- const bucket = this.byMergeKey.get(old.mergeKey);
3236
- if (bucket) {
3237
- const idx = bucket.indexOf(ent.id);
3238
- if (idx >= 0) bucket.splice(idx, 1);
3239
- if (bucket.length === 0) this.byMergeKey.delete(old.mergeKey);
5063
+ // -- retry ----------------------------------------------------------
5064
+ async safeStop(traceId, retry) {
5065
+ try {
5066
+ await this.emitStop({
5067
+ traceId,
5068
+ rollback: Boolean(retry.rollbackOnRetry),
5069
+ reason: retry.reason ?? "retry"
5070
+ });
5071
+ } catch {
5072
+ }
5073
+ }
5074
+ /** Dispatch and wait, retrying per `retry` until a non-retryable outcome or
5075
+ * attempts are exhausted. Returns the resolved Signal; re-throws the last
5076
+ * error when every attempt failed with an exception. */
5077
+ async runWithRetry(args) {
5078
+ const { retry, timeoutMs, traceId: callerTrace, ...rest } = args;
5079
+ const maxAttempts = retry.maxAttempts ?? 3;
5080
+ const retryOn = retry.retryOn ?? defaultRetryOn;
5081
+ const newTrace = retry.newTrace ?? true;
5082
+ const perTimeout = retry.timeoutMs ?? timeoutMs ?? 3e4;
5083
+ let outcome = null;
5084
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
5085
+ const tid = callerTrace && !newTrace ? callerTrace : newTraceId();
5086
+ const meta = { ...rest.meta ?? {}, attempt };
5087
+ try {
5088
+ const pathway = await this.dispatch({ ...rest, traceId: tid, meta });
5089
+ try {
5090
+ outcome = await pathway.wait(perTimeout);
5091
+ } finally {
5092
+ await pathway.close();
3240
5093
  }
5094
+ } catch (err) {
5095
+ outcome = err instanceof Error ? err : new Error(String(err));
3241
5096
  }
5097
+ if (!retryOn(outcome)) {
5098
+ if (outcome instanceof Error) throw outcome;
5099
+ return outcome;
5100
+ }
5101
+ if (newTrace) await this.safeStop(tid, retry);
5102
+ if (attempt + 1 >= maxAttempts) {
5103
+ if (outcome instanceof Error) throw outcome;
5104
+ return outcome;
5105
+ }
5106
+ if (retry.onRetry) {
5107
+ try {
5108
+ retry.onRetry(attempt, outcome);
5109
+ } catch {
5110
+ }
5111
+ }
5112
+ const delay = retry.backoffMs ? retry.backoffMs(attempt) : 0;
5113
+ if (delay > 0) await new Promise((r) => setTimeout(r, delay));
3242
5114
  }
3243
- this.entries.set(ent.id, ent);
3244
- if (ent.mergeKey) {
3245
- const bucket = this.byMergeKey.get(ent.mergeKey);
3246
- if (bucket) bucket.push(ent.id);
3247
- else this.byMergeKey.set(ent.mergeKey, [ent.id]);
3248
- }
5115
+ throw new Error("runWithRetry: exhausted attempts unexpectedly");
3249
5116
  }
3250
- evict(entryId) {
3251
- const ent = this.entries.get(entryId);
3252
- this.entries.delete(entryId);
3253
- if (ent === void 0) return;
3254
- if (ent.mergeKey) {
3255
- const bucket = this.byMergeKey.get(ent.mergeKey);
3256
- if (bucket) {
3257
- const idx = bucket.indexOf(entryId);
3258
- if (idx >= 0) bucket.splice(idx, 1);
3259
- if (bucket.length === 0) this.byMergeKey.delete(ent.mergeKey);
5117
+ async emitRegister(axon) {
5118
+ await this.publish(
5119
+ registerSignal({
5120
+ directed: {
5121
+ id: axon.neuronId,
5122
+ type: axon.neuronKind ?? "neuron",
5123
+ capabilities: [...axon.capabilities]
5124
+ },
5125
+ capabilities: axon.capabilities,
5126
+ role: "neuron",
5127
+ ...axon.version !== void 0 ? { version: axon.version } : {}
5128
+ })
5129
+ );
5130
+ }
5131
+ async emitDeregister(axon, reason) {
5132
+ await this.publish(
5133
+ deregisterSignal({
5134
+ directed: { id: axon.neuronId },
5135
+ ...reason !== void 0 ? { reason } : {}
5136
+ })
5137
+ );
5138
+ }
5139
+ async heartbeatTick() {
5140
+ if (!this.running) return;
5141
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5142
+ for (const axon of this._axons.values()) {
5143
+ try {
5144
+ if (this.reregisterOnHeartbeat) await this.emitRegister(axon);
5145
+ await this.synapse.publish(
5146
+ this.subject(SignalType.HEARTBEAT),
5147
+ heartbeatSignal({ directed: { id: axon.neuronId } })
5148
+ );
5149
+ } catch {
5150
+ }
5151
+ if (this.registryStore !== null) {
5152
+ try {
5153
+ await this.registryStore.touchHeartbeat(axon.neuronId, now);
5154
+ } catch {
5155
+ }
3260
5156
  }
5157
+ await this.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
5158
+ await axon.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
3261
5159
  }
3262
- }
3263
- };
3264
- function deepMerge(base, incoming) {
3265
- if (incoming === void 0 || incoming === null) return base;
3266
- const bothObjects = base !== null && typeof base === "object" && !Array.isArray(base) && typeof incoming === "object" && !Array.isArray(incoming);
3267
- if (bothObjects) {
3268
- const out = { ...base };
3269
- for (const [k, v] of Object.entries(incoming)) {
3270
- out[k] = k in out ? deepMerge(out[k], v) : v;
5160
+ if (this.registryStore !== null && this.staleAfterMs > 0) {
5161
+ try {
5162
+ await this.sweepStaleNeurons(Date.now());
5163
+ } catch {
5164
+ }
3271
5165
  }
3272
- return out;
3273
5166
  }
3274
- if (Array.isArray(base) && Array.isArray(incoming)) {
3275
- const seen = /* @__PURE__ */ new Set();
3276
- const out = [];
3277
- for (const item of [...base, ...incoming]) {
3278
- const key = JSON.stringify(item);
3279
- if (seen.has(key)) continue;
3280
- seen.add(key);
3281
- out.push(item);
5167
+ /** Mark Neurons deregistered when their last heartbeat is older than
5168
+ * `staleAfterMs`. Own hosted Axons were touched immediately before the
5169
+ * sweep, so they never qualify. */
5170
+ async sweepStaleNeurons(nowMs) {
5171
+ const store = this.registryStore;
5172
+ if (store === null) return;
5173
+ const records = await store.list({ includeDeregistered: false });
5174
+ for (const rec of records) {
5175
+ const seen = rec.last_heartbeat ?? rec.registered_at;
5176
+ if (!seen) continue;
5177
+ if (nowMs - Date.parse(seen) > this.staleAfterMs) {
5178
+ try {
5179
+ await store.markDeregistered(rec.neuron_id);
5180
+ await this.hooks._fireRefresh({
5181
+ reason: "stale",
5182
+ neuronId: rec.neuron_id,
5183
+ extra: {}
5184
+ });
5185
+ } catch {
5186
+ }
5187
+ }
3282
5188
  }
3283
- return out;
3284
- }
3285
- return incoming;
3286
- }
3287
-
3288
- // src/engram-client.ts
3289
- function deferred() {
3290
- let resolve;
3291
- let reject;
3292
- const promise = new Promise((res, rej) => {
3293
- resolve = res;
3294
- reject = rej;
3295
- });
3296
- return { promise, resolve, reject };
3297
- }
3298
- var EngramClient = class {
3299
- constructor(publisher) {
3300
- this.publisher = publisher;
3301
5189
  }
3302
- publisher;
3303
- pendingRecalls = /* @__PURE__ */ new Map();
3304
- pendingImprints = /* @__PURE__ */ new Map();
3305
- byTrace = /* @__PURE__ */ new Map();
3306
- async recall(args) {
3307
- let engramId = args.engramId;
3308
- let engramKind = args.engramKind;
3309
- let deadlineMs = args.deadlineMs;
3310
- let recallMode = args.recallMode;
3311
- if (args.binding) {
3312
- engramId = engramId ?? args.binding.directedId ?? void 0;
3313
- engramKind = engramKind ?? args.binding.directedType ?? void 0;
3314
- if (deadlineMs === void 0) deadlineMs = args.binding.defaultDeadlineMs ?? void 0;
3315
- if (recallMode === void 0) recallMode = args.binding.defaultRecallMode;
5190
+ async mirrorToStore(axon, status) {
5191
+ if (this.registryStore === null) return;
5192
+ try {
5193
+ await this.registryStore.upsert(
5194
+ neuronRecord({
5195
+ neuron_id: axon.neuronId,
5196
+ capabilities: [...axon.capabilities],
5197
+ version: axon.version ?? null,
5198
+ status,
5199
+ last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
5200
+ })
5201
+ );
5202
+ } catch {
3316
5203
  }
3317
- const mode = recallMode ?? "first";
3318
- const sig = recallSignal({
3319
- traceId: args.traceId,
3320
- parentId: args.parentId,
3321
- directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
3322
- query: args.query,
3323
- ...args.filters !== void 0 ? { filters: args.filters } : {},
3324
- ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
3325
- ...deadlineMs !== void 0 ? { deadlineMs } : {},
3326
- ...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
3327
- recallMode: mode,
3328
- ...args.meta !== void 0 ? { meta: args.meta } : {}
3329
- });
3330
- const d = deferred();
3331
- const pending = { deferred: d, mode, timer: null, done: false, hitsSoFar: [], engrams: [] };
3332
- this.pendingRecalls.set(sig.id, pending);
3333
- this.track(args.traceId, sig.id);
3334
- if (deadlineMs !== void 0 && deadlineMs > 0) {
3335
- pending.timer = setTimeout(() => this.onRecallDeadline(sig.id), deadlineMs);
5204
+ }
5205
+ /** Respond to a DISCOVER by re-emitting REGISTER for matching Axons. */
5206
+ async respondToDiscover(signal) {
5207
+ if (this._axons.size === 0) return;
5208
+ const target = signal.payload["neuron"];
5209
+ const capsFilter = signal.payload["capabilities"];
5210
+ for (const axon of this._axons.values()) {
5211
+ if (target && axon.neuronId !== target) continue;
5212
+ if (capsFilter?.length && !capsFilter.every((c) => axon.capabilities.includes(c))) {
5213
+ continue;
5214
+ }
5215
+ try {
5216
+ await this.emitRegister(axon);
5217
+ } catch {
5218
+ }
3336
5219
  }
3337
- try {
3338
- await this.publisher.publish(sig);
3339
- } catch (err) {
3340
- this.cleanupRecall(args.traceId, sig.id);
3341
- throw err;
5220
+ }
5221
+ async dispatchInbound(signal) {
5222
+ if (signal.type === SignalType.DISCOVER) {
5223
+ const hs = this.handlers.get(SignalType.DISCOVER) ?? [];
5224
+ if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
5225
+ else await this.respondToDiscover(signal);
5226
+ return;
3342
5227
  }
3343
- try {
3344
- return await d.promise;
3345
- } finally {
3346
- this.cleanupRecall(args.traceId, sig.id);
5228
+ if (signal.type === SignalType.RECALL) {
5229
+ await this.onRecall(signal);
5230
+ const hs = this.handlers.get(SignalType.RECALL) ?? [];
5231
+ if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
5232
+ return;
3347
5233
  }
3348
- }
3349
- async imprint(args) {
3350
- let engramId = args.engramId;
3351
- let engramKind = args.engramKind;
3352
- if (args.binding) {
3353
- engramId = engramId ?? args.binding.directedId ?? void 0;
3354
- engramKind = engramKind ?? args.binding.directedType ?? void 0;
5234
+ if (signal.type === SignalType.IMPRINT) {
5235
+ await this.onImprint(signal);
5236
+ const hs = this.handlers.get(SignalType.IMPRINT) ?? [];
5237
+ if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
5238
+ return;
3355
5239
  }
3356
- const sig = imprintSignal({
3357
- traceId: args.traceId,
3358
- parentId: args.parentId,
3359
- directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
3360
- op: args.op,
3361
- entry: args.entry,
3362
- ...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
3363
- ...args.meta !== void 0 ? { meta: args.meta } : {}
3364
- });
3365
- if (!args.awaitAck) {
3366
- await this.publisher.publish(sig);
3367
- return null;
5240
+ if (signal.type === SignalType.STOP) {
5241
+ if (signal.trace_id) {
5242
+ const pw = this.pathways.get(signal.trace_id);
5243
+ if (pw) {
5244
+ try {
5245
+ await pw._deliver(signal);
5246
+ } catch {
5247
+ }
5248
+ }
5249
+ }
5250
+ await this.onStop(signal);
5251
+ const hs = this.handlers.get(SignalType.STOP) ?? [];
5252
+ if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
5253
+ return;
3368
5254
  }
3369
- const d = deferred();
3370
- const pending = { deferred: d, timer: null, done: false };
3371
- this.pendingImprints.set(sig.id, pending);
3372
- this.track(args.traceId, sig.id);
3373
- if (args.deadlineMs !== void 0 && args.deadlineMs > 0) {
3374
- pending.timer = setTimeout(() => this.onImprintDeadline(sig.id), args.deadlineMs);
5255
+ if (signal.type === SignalType.RECALLED || signal.type === SignalType.IMPRINTED) {
5256
+ this.engramClient.deliver(signal);
3375
5257
  }
3376
- try {
3377
- await this.publisher.publish(sig);
3378
- } catch (err) {
3379
- this.cleanupImprint(args.traceId, sig.id);
3380
- throw err;
5258
+ if (signal.type === SignalType.REGISTER && this.isEngramRegister(signal)) {
5259
+ this.recordEngramRegistration(signal);
5260
+ return;
3381
5261
  }
3382
- try {
3383
- return await d.promise;
3384
- } finally {
3385
- this.cleanupImprint(args.traceId, sig.id);
5262
+ if (signal.parent_id) {
5263
+ const opPw = this.opPathways.get(signal.parent_id);
5264
+ if (opPw) {
5265
+ try {
5266
+ await opPw._deliver(signal);
5267
+ } catch {
5268
+ }
5269
+ }
3386
5270
  }
3387
- }
3388
- /** Match RECALLED / IMPRINTED by parent_id and resolve pendings. */
3389
- deliver(sig) {
3390
- const pid = sig.parent_id;
3391
- if (pid === null) return;
3392
- if (sig.type === SignalType.RECALLED) {
3393
- const pending = this.pendingRecalls.get(pid);
3394
- if (pending === void 0) return;
3395
- const hits = hitsFromPayload(sig.payload["hits"]);
3396
- const engramId = typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "";
3397
- const tookMs = typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null;
3398
- const truncated = sig.payload["truncated"] === true;
3399
- if (pending.mode === "first") {
3400
- if (!pending.done) {
3401
- pending.done = true;
3402
- pending.deferred.resolve({
3403
- hits,
3404
- engramIds: engramId ? [engramId] : [],
3405
- truncated,
3406
- tookMs
3407
- });
5271
+ if ((signal.type === SignalType.CLARIFICATION_ANSWER || signal.type === SignalType.PERMISSION_DECISION) && signal.parent_id) {
5272
+ this.recentDecisions.set(signal.parent_id, signal);
5273
+ while (this.recentDecisions.size > 256) {
5274
+ const oldest = this.recentDecisions.keys().next().value;
5275
+ this.recentDecisions.delete(oldest);
5276
+ }
5277
+ }
5278
+ if ((signal.type === SignalType.FINAL || signal.type === SignalType.ERROR) && signal.trace_id) {
5279
+ await this.cancelOpPathways(signal.trace_id);
5280
+ this.engramClient.cancelTrace(signal.trace_id);
5281
+ for (const engram of this._engrams.values()) {
5282
+ try {
5283
+ await engram.commit(signal.trace_id);
5284
+ } catch {
3408
5285
  }
3409
- } else {
3410
- pending.hitsSoFar.push(...hits);
3411
- if (engramId) pending.engrams.push(engramId);
3412
5286
  }
3413
- } else if (sig.type === SignalType.IMPRINTED) {
3414
- const pending = this.pendingImprints.get(pid);
3415
- if (pending === void 0 || pending.done) return;
3416
- pending.done = true;
3417
- pending.deferred.resolve({
3418
- engramId: typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "",
3419
- op: typeof sig.payload["op"] === "string" ? sig.payload["op"] : "",
3420
- id: typeof sig.payload["id"] === "string" ? sig.payload["id"] : null,
3421
- version: typeof sig.payload["version"] === "number" ? sig.payload["version"] : null,
3422
- tookMs: typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null,
3423
- error: typeof sig.payload["error"] === "string" ? sig.payload["error"] : null,
3424
- ok: !(typeof sig.payload["error"] === "string")
3425
- });
5287
+ this.traceAborts.delete(signal.trace_id);
5288
+ }
5289
+ if (signal.type === SignalType.TASK_AWARDED) {
5290
+ const target = signal.directed?.id ?? null;
5291
+ if (target && this._axons.has(target)) {
5292
+ const synthetic = taskSignal({
5293
+ traceId: signal.trace_id,
5294
+ parentId: signal.id,
5295
+ directed: { id: target },
5296
+ input: signal.payload["input"] ?? {},
5297
+ finalize: Boolean(signal.payload["finalize"]),
5298
+ ...signal.payload["context_ref"] !== void 0 ? { contextRef: signal.payload["context_ref"] } : {},
5299
+ meta: signal.meta
5300
+ });
5301
+ await this.onTask(synthetic);
5302
+ }
5303
+ }
5304
+ if (signal.trace_id && PATHWAY_TYPES.has(signal.type)) {
5305
+ const pathway = this.pathways.get(signal.trace_id);
5306
+ if (pathway) {
5307
+ try {
5308
+ await pathway._deliver(signal);
5309
+ } catch {
5310
+ }
5311
+ }
5312
+ }
5313
+ if (AXON_TYPES.has(signal.type) && this.registryStore !== null) {
5314
+ try {
5315
+ await this.updateRegistry(signal);
5316
+ } catch {
5317
+ }
5318
+ }
5319
+ if (signal.type === SignalType.TASK_OFFER && this.autoBid && this._axons.size > 0 && (this.handlers.get(SignalType.TASK_OFFER) ?? []).length === 0) {
5320
+ await this.maybeAutoBid(signal);
5321
+ }
5322
+ const handlers = this.handlers.get(signal.type) ?? [];
5323
+ if (handlers.length) await Promise.allSettled(handlers.map((h) => h(signal)));
5324
+ }
5325
+ // -- Engram: hosted-side handlers -----------------------------------
5326
+ /** Pick the hosted Engrams that should respond to a RECALL/IMPRINT.
5327
+ * directed.id (engramId) wins over directed.type (engramKind). */
5328
+ resolveEngramTargets(signal) {
5329
+ const eid = signal.directed?.id ?? null;
5330
+ if (eid) {
5331
+ const ent = this._engrams.get(eid);
5332
+ return ent ? [ent] : [];
5333
+ }
5334
+ const ekind = signal.directed?.type ?? null;
5335
+ if (ekind) {
5336
+ return (this.engramKindIndex.get(ekind) ?? []).map((id) => this._engrams.get(id)).filter((e) => e !== void 0);
5337
+ }
5338
+ return [];
5339
+ }
5340
+ async onRecall(signal) {
5341
+ const targets = this.resolveEngramTargets(signal);
5342
+ if (!targets.length) return;
5343
+ const query = signal.payload["query"] ?? {};
5344
+ const filters = signal.payload["filters"];
5345
+ const contextRef = signal.payload["context_ref"];
5346
+ const deadlineMs = signal.payload["deadline_ms"];
5347
+ const minConfidence = signal.payload["min_confidence"];
5348
+ for (const engram of targets) {
5349
+ let hits;
5350
+ try {
5351
+ if (!await engram.canServe(query)) continue;
5352
+ hits = await engram.recall(query, {
5353
+ ...filters !== void 0 ? { filters } : {},
5354
+ ...contextRef !== void 0 ? { contextRef } : {},
5355
+ ...deadlineMs !== void 0 ? { deadlineMs } : {},
5356
+ ...minConfidence !== void 0 ? { minConfidence } : {}
5357
+ });
5358
+ } catch {
5359
+ continue;
5360
+ }
5361
+ try {
5362
+ await this.publish(
5363
+ recalledSignal({
5364
+ traceId: signal.trace_id,
5365
+ parentId: signal.id,
5366
+ engramId: engram.engramId,
5367
+ hits: hits.map((h) => ({ id: h.id, entry: h.entry, score: h.score })),
5368
+ // Attribute the reply to the Engram that answered, not the host
5369
+ // Dendrite, so observers classify it by the Engram's REGISTER.
5370
+ directed: { id: engram.engramId, type: engram.engramKind }
5371
+ })
5372
+ );
5373
+ } catch {
5374
+ }
3426
5375
  }
3427
5376
  }
3428
- /** Cancel every in-flight recall/imprint on a trace (FINAL/ERROR or shutdown). */
3429
- cancelTrace(traceId) {
3430
- const ids = this.byTrace.get(traceId);
3431
- this.byTrace.delete(traceId);
3432
- if (ids === void 0) return;
3433
- for (const id of ids) {
3434
- const pr = this.pendingRecalls.get(id);
3435
- if (pr !== void 0 && !pr.done) {
3436
- pr.done = true;
3437
- if (pr.timer !== null) clearTimeout(pr.timer);
3438
- pr.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while recall ${id} in flight`));
3439
- this.pendingRecalls.delete(id);
5377
+ async onImprint(signal) {
5378
+ const targets = this.resolveEngramTargets(signal);
5379
+ if (!targets.length) return;
5380
+ const op = signal.payload["op"] ?? "add";
5381
+ const entry = signal.payload["entry"] ?? {};
5382
+ const mergeKey = signal.payload["merge_key"];
5383
+ for (const engram of targets) {
5384
+ let reply2;
5385
+ try {
5386
+ const receipt2 = await engram.imprint(op, entry, {
5387
+ imprintId: signal.id,
5388
+ traceId: signal.trace_id,
5389
+ ...mergeKey !== void 0 ? { mergeKey } : {}
5390
+ });
5391
+ reply2 = imprintedSignal({
5392
+ traceId: signal.trace_id,
5393
+ parentId: signal.id,
5394
+ engramId: receipt2.engramId || engram.engramId,
5395
+ op: receipt2.op,
5396
+ ...receipt2.id !== null ? { id: receipt2.id } : {},
5397
+ ...receipt2.version !== null ? { version: receipt2.version } : {},
5398
+ ...receipt2.tookMs !== null ? { tookMs: receipt2.tookMs } : {},
5399
+ ...receipt2.error !== null ? { error: receipt2.error } : {},
5400
+ directed: { id: engram.engramId, type: engram.engramKind }
5401
+ });
5402
+ } catch (err) {
5403
+ reply2 = imprintedSignal({
5404
+ traceId: signal.trace_id,
5405
+ parentId: signal.id,
5406
+ engramId: engram.engramId,
5407
+ op,
5408
+ error: `engram_exception: ${err instanceof Error ? err.message : String(err)}`,
5409
+ directed: { id: engram.engramId, type: engram.engramKind }
5410
+ });
3440
5411
  }
3441
- const pi = this.pendingImprints.get(id);
3442
- if (pi !== void 0 && !pi.done) {
3443
- pi.done = true;
3444
- if (pi.timer !== null) clearTimeout(pi.timer);
3445
- pi.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while imprint ${id} in flight`));
3446
- this.pendingImprints.delete(id);
5412
+ try {
5413
+ await this.publish(reply2);
5414
+ } catch {
3447
5415
  }
3448
5416
  }
3449
5417
  }
3450
- cancelAll() {
3451
- for (const traceId of [...this.byTrace.keys()]) this.cancelTrace(traceId);
5418
+ // -- Engram: registration (announce + learn) -------------------------
5419
+ async emitEngramRegister(engram) {
5420
+ await this.publish(
5421
+ registerSignal({
5422
+ directed: {
5423
+ id: engram.engramId,
5424
+ type: engram.engramKind,
5425
+ capabilities: [...engram.capabilities]
5426
+ },
5427
+ capabilities: engram.capabilities,
5428
+ role: "engram",
5429
+ ...engram.version !== null ? { version: engram.version } : {}
5430
+ })
5431
+ );
3452
5432
  }
3453
- onRecallDeadline(id) {
3454
- const pending = this.pendingRecalls.get(id);
3455
- if (pending === void 0 || pending.done) return;
3456
- pending.done = true;
3457
- if (pending.mode === "first") {
3458
- pending.deferred.reject(new EngramTimeout(`RECALL ${id} elapsed deadline without any responder`));
3459
- } else {
3460
- pending.deferred.resolve({
3461
- hits: [...pending.hitsSoFar].sort((a, b) => b.score - a.score),
3462
- engramIds: [...pending.engrams],
3463
- truncated: false,
3464
- tookMs: null
3465
- });
5433
+ isEngramRegister(signal) {
5434
+ if (signal.payload["role"] === "engram" || signal.payload["engram"]) return true;
5435
+ const dtype = signal.directed?.type ?? null;
5436
+ if (dtype && (this.engramKindIndex.has(dtype) || this.engramRegKindIndex.has(dtype))) {
5437
+ return true;
5438
+ }
5439
+ return false;
5440
+ }
5441
+ recordEngramRegistration(signal) {
5442
+ const d = signal.directed;
5443
+ if (!d || !d.id && !d.type) return;
5444
+ let caps = [...d.capabilities];
5445
+ if (!caps.length) {
5446
+ caps = [...signal.payload["capabilities"] ?? []];
5447
+ }
5448
+ const key = d.id ?? d.type;
5449
+ this._engramRegistrations.set(key, { id: d.id, type: d.type, capabilities: caps });
5450
+ if (d.type) {
5451
+ const bucket = this.engramRegKindIndex.get(d.type) ?? /* @__PURE__ */ new Set();
5452
+ bucket.add(key);
5453
+ this.engramRegKindIndex.set(d.type, bucket);
3466
5454
  }
3467
5455
  }
3468
- onImprintDeadline(id) {
3469
- const pending = this.pendingImprints.get(id);
3470
- if (pending === void 0 || pending.done) return;
3471
- pending.done = true;
3472
- pending.deferred.reject(new EngramTimeout(`IMPRINT ${id} elapsed deadline without IMPRINTED`));
3473
- }
3474
- track(traceId, id) {
3475
- const bucket = this.byTrace.get(traceId);
3476
- if (bucket) bucket.add(id);
3477
- else this.byTrace.set(traceId, /* @__PURE__ */ new Set([id]));
5456
+ // -- Engram: caller-side helpers -------------------------------------
5457
+ /** Resolve (traceId, parentId) for a caller-side engram op: explicit ids
5458
+ * win, then the ambient task context (bound by Axon.handleTask), then a
5459
+ * freshly minted trace (the pre-task-hydration shape). */
5460
+ static resolveTrace(traceId, parentId) {
5461
+ let tid = traceId;
5462
+ let pid = parentId;
5463
+ if (tid === void 0) {
5464
+ const amb = ambientTrace();
5465
+ if (amb !== null) {
5466
+ tid = amb[0];
5467
+ if (pid === void 0) pid = amb[1];
5468
+ }
5469
+ }
5470
+ return [tid ?? newTraceId(), pid ?? newEventId()];
3478
5471
  }
3479
- cleanupRecall(traceId, id) {
3480
- const p = this.pendingRecalls.get(id);
3481
- if (p?.timer != null) clearTimeout(p.timer);
3482
- this.pendingRecalls.delete(id);
3483
- this.discardTrace(traceId, id);
5472
+ /** Emit RECALL and await RECALLED. Trace attribution: explicit ids win,
5473
+ * then the ambient task context, then a fresh trace. */
5474
+ async recall(args) {
5475
+ const [tid, pid] = _Dendrite.resolveTrace(args.traceId, args.parentId);
5476
+ return this.engramClient.recall({
5477
+ query: args.query,
5478
+ traceId: tid,
5479
+ parentId: pid,
5480
+ ...args.engramId !== void 0 ? { engramId: args.engramId } : {},
5481
+ ...args.engramKind !== void 0 ? { engramKind: args.engramKind } : {},
5482
+ ...args.filters !== void 0 ? { filters: args.filters } : {},
5483
+ ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
5484
+ ...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
5485
+ ...args.recallMode !== void 0 ? { recallMode: args.recallMode } : {},
5486
+ ...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
5487
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
5488
+ });
3484
5489
  }
3485
- cleanupImprint(traceId, id) {
3486
- const p = this.pendingImprints.get(id);
3487
- if (p?.timer != null) clearTimeout(p.timer);
3488
- this.pendingImprints.delete(id);
3489
- this.discardTrace(traceId, id);
5490
+ /** Emit IMPRINT. Resolves null unless `awaitAck: true`. Trace attribution
5491
+ * as {@link recall}. */
5492
+ async imprint(args) {
5493
+ const [tid, pid] = _Dendrite.resolveTrace(args.traceId, args.parentId);
5494
+ return this.engramClient.imprint({
5495
+ op: args.op,
5496
+ entry: args.entry,
5497
+ traceId: tid,
5498
+ parentId: pid,
5499
+ ...args.engramId !== void 0 ? { engramId: args.engramId } : {},
5500
+ ...args.engramKind !== void 0 ? { engramKind: args.engramKind } : {},
5501
+ ...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
5502
+ ...args.awaitAck !== void 0 ? { awaitAck: args.awaitAck } : {},
5503
+ ...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
5504
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
5505
+ });
3490
5506
  }
3491
- discardTrace(traceId, id) {
3492
- const bucket = this.byTrace.get(traceId);
3493
- if (bucket === void 0) return;
3494
- bucket.delete(id);
3495
- if (bucket.size === 0) this.byTrace.delete(traceId);
5507
+ async updateRegistry(signal) {
5508
+ if (this.registryStore === null) return;
5509
+ if (signal.payload["role"] === "engram" || signal.payload["engram"]) return;
5510
+ const neuronId = signal.directed?.id ?? null;
5511
+ if (!neuronId) return;
5512
+ let reason = null;
5513
+ if (signal.type === SignalType.REGISTER) {
5514
+ await this.registryStore.upsert(
5515
+ neuronRecord({
5516
+ neuron_id: neuronId,
5517
+ capabilities: signal.payload["capabilities"] ?? [],
5518
+ version: signal.payload["version"] ?? null,
5519
+ status: "registered",
5520
+ last_heartbeat: signal.ts
5521
+ })
5522
+ );
5523
+ reason = "register";
5524
+ } else if (signal.type === SignalType.DEREGISTER) {
5525
+ await this.registryStore.markDeregistered(neuronId);
5526
+ reason = "deregister";
5527
+ } else if (signal.type === SignalType.HEARTBEAT) {
5528
+ const status = signal.payload["status"];
5529
+ if (status) await this.registryStore.touchHeartbeat(neuronId, signal.ts, status);
5530
+ else await this.registryStore.touchHeartbeat(neuronId, signal.ts);
5531
+ reason = "heartbeat";
5532
+ }
5533
+ if (reason !== null) {
5534
+ await this.hooks._fireRefresh({ reason, neuronId, extra: {} });
5535
+ }
3496
5536
  }
3497
5537
  };
3498
- function hitsFromPayload(raw) {
3499
- if (!Array.isArray(raw)) return [];
3500
- const out = [];
3501
- for (const h of raw) {
3502
- if (h === null || typeof h !== "object") continue;
3503
- const obj = h;
3504
- const entryVal = obj["entry"];
3505
- out.push({
3506
- id: typeof obj["id"] === "string" ? obj["id"] : "",
3507
- entry: entryVal !== null && typeof entryVal === "object" && !Array.isArray(entryVal) ? entryVal : { value: entryVal },
3508
- score: typeof obj["score"] === "number" ? obj["score"] : 1
3509
- });
3510
- }
3511
- return out;
3512
- }
5538
+ var Cortex = Dendrite;
3513
5539
 
3514
5540
  // src/engram-sqlite.ts
3515
5541
  var SCHEMA3 = `
@@ -4047,10 +6073,11 @@ var PostgresEngram = class extends Engram {
4047
6073
  };
4048
6074
 
4049
6075
  // src/index.ts
4050
- var VERSION = true ? "0.1.2" : "0.0.0-dev";
6076
+ var VERSION = true ? "0.1.4" : "0.0.0-dev";
4051
6077
  export {
4052
6078
  AXON_TYPES,
4053
6079
  Axon,
6080
+ COSMO_INTENT_SYSTEM_PROMPT,
4054
6081
  Cortex,
4055
6082
  DendriteProtocolError as CortexProtocolError,
4056
6083
  Dendrite,
@@ -4071,14 +6098,19 @@ export {
4071
6098
  MemoryRegistryStore,
4072
6099
  MemorySynapse,
4073
6100
  NatsSynapse,
6101
+ PATHWAY_TYPES,
6102
+ Pathway,
6103
+ PathwayClosedError,
4074
6104
  PostgresEngram,
4075
6105
  PostgresRegistryStore,
4076
6106
  SYNAPSE_TYPES,
4077
6107
  SignalType,
4078
6108
  SqliteEngram,
4079
6109
  SqliteRegistryStore,
6110
+ TERMINAL_TYPES,
4080
6111
  VERSION,
4081
6112
  agentOutputSignal,
6113
+ ambientTrace,
4082
6114
  anthropicNeuron,
4083
6115
  bidSignal,
4084
6116
  clarificationAnswerSignal,
@@ -4091,6 +6123,7 @@ export {
4091
6123
  critiqueSignal,
4092
6124
  decode,
4093
6125
  deepMerge,
6126
+ defaultRetryOn,
4094
6127
  deregisterSignal,
4095
6128
  directedTo,
4096
6129
  discoverSignal,
@@ -4099,6 +6132,7 @@ export {
4099
6132
  errorSignal,
4100
6133
  escalationSignal,
4101
6134
  finalSignal,
6135
+ followupPrompt,
4102
6136
  heartbeatSignal,
4103
6137
  huggingFaceNeuron,
4104
6138
  imprintSignal,
@@ -4126,8 +6160,13 @@ export {
4126
6160
  recalledSignal,
4127
6161
  registerSignal,
4128
6162
  reply,
6163
+ runWithTraceContext,
4129
6164
  standardMcpServers,
6165
+ stopSignal,
6166
+ stoppedSignal,
4130
6167
  synapseFromUrl,
6168
+ taskAwardedSignal,
6169
+ taskDeclinedSignal,
4131
6170
  taskOfferSignal,
4132
6171
  taskSignal,
4133
6172
  thoughtDeltaSignal,