@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.cjs CHANGED
@@ -32,6 +32,7 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AXON_TYPES: () => AXON_TYPES,
34
34
  Axon: () => Axon,
35
+ COSMO_INTENT_SYSTEM_PROMPT: () => COSMO_INTENT_SYSTEM_PROMPT,
35
36
  Cortex: () => Cortex,
36
37
  CortexProtocolError: () => DendriteProtocolError,
37
38
  Dendrite: () => Dendrite,
@@ -52,14 +53,19 @@ __export(index_exports, {
52
53
  MemoryRegistryStore: () => MemoryRegistryStore,
53
54
  MemorySynapse: () => MemorySynapse,
54
55
  NatsSynapse: () => NatsSynapse,
56
+ PATHWAY_TYPES: () => PATHWAY_TYPES,
57
+ Pathway: () => Pathway,
58
+ PathwayClosedError: () => PathwayClosedError,
55
59
  PostgresEngram: () => PostgresEngram,
56
60
  PostgresRegistryStore: () => PostgresRegistryStore,
57
61
  SYNAPSE_TYPES: () => SYNAPSE_TYPES,
58
62
  SignalType: () => SignalType,
59
63
  SqliteEngram: () => SqliteEngram,
60
64
  SqliteRegistryStore: () => SqliteRegistryStore,
65
+ TERMINAL_TYPES: () => TERMINAL_TYPES,
61
66
  VERSION: () => VERSION,
62
67
  agentOutputSignal: () => agentOutputSignal,
68
+ ambientTrace: () => ambientTrace,
63
69
  anthropicNeuron: () => anthropicNeuron,
64
70
  bidSignal: () => bidSignal,
65
71
  clarificationAnswerSignal: () => clarificationAnswerSignal,
@@ -72,6 +78,7 @@ __export(index_exports, {
72
78
  critiqueSignal: () => critiqueSignal,
73
79
  decode: () => decode,
74
80
  deepMerge: () => deepMerge,
81
+ defaultRetryOn: () => defaultRetryOn,
75
82
  deregisterSignal: () => deregisterSignal,
76
83
  directedTo: () => directedTo,
77
84
  discoverSignal: () => discoverSignal,
@@ -80,6 +87,7 @@ __export(index_exports, {
80
87
  errorSignal: () => errorSignal,
81
88
  escalationSignal: () => escalationSignal,
82
89
  finalSignal: () => finalSignal,
90
+ followupPrompt: () => followupPrompt,
83
91
  heartbeatSignal: () => heartbeatSignal,
84
92
  huggingFaceNeuron: () => huggingFaceNeuron,
85
93
  imprintSignal: () => imprintSignal,
@@ -107,8 +115,13 @@ __export(index_exports, {
107
115
  recalledSignal: () => recalledSignal,
108
116
  registerSignal: () => registerSignal,
109
117
  reply: () => reply,
118
+ runWithTraceContext: () => runWithTraceContext,
110
119
  standardMcpServers: () => standardMcpServers,
120
+ stopSignal: () => stopSignal,
121
+ stoppedSignal: () => stoppedSignal,
111
122
  synapseFromUrl: () => synapseFromUrl,
123
+ taskAwardedSignal: () => taskAwardedSignal,
124
+ taskDeclinedSignal: () => taskDeclinedSignal,
112
125
  taskOfferSignal: () => taskOfferSignal,
113
126
  taskSignal: () => taskSignal,
114
127
  thoughtDeltaSignal: () => thoughtDeltaSignal,
@@ -175,7 +188,13 @@ var SignalType = {
175
188
  IMPRINT: "IMPRINT",
176
189
  IMPRINTED: "IMPRINTED",
177
190
  // Discovery [C]
178
- DISCOVER: "DISCOVER"
191
+ DISCOVER: "DISCOVER",
192
+ // Workflow control [C] - cooperative cancellation of a whole trace.
193
+ // STOP is broadcast on the trace; every Dendrite filters by trace_id,
194
+ // cancels its in-flight work + engram I/O, optionally rolls back Engram
195
+ // writes via the saga journal, then acks with STOPPED.
196
+ STOP: "STOP",
197
+ STOPPED: "STOPPED"
179
198
  };
180
199
  var AXON_TYPES = /* @__PURE__ */ new Set([
181
200
  SignalType.AGENT_OUTPUT,
@@ -212,7 +231,11 @@ var SYNAPSE_TYPES = /* @__PURE__ */ new Set([
212
231
  SignalType.RECALL,
213
232
  SignalType.RECALLED,
214
233
  SignalType.IMPRINT,
215
- SignalType.IMPRINTED
234
+ SignalType.IMPRINTED,
235
+ // Workflow control - STOP is orchestrator-gated (see Dendrite role gate);
236
+ // STOPPED is the per-Dendrite ack.
237
+ SignalType.STOP,
238
+ SignalType.STOPPED
216
239
  ]);
217
240
  function normalizeDirected(d) {
218
241
  if (d === null || d === void 0) return null;
@@ -245,6 +268,12 @@ function createSignal(input) {
245
268
  return signal;
246
269
  }
247
270
  function validateSignal(signal) {
271
+ const major = signal.v.split(".", 1)[0];
272
+ if (major !== "1") {
273
+ throw new Error(
274
+ `unsupported protocol version '${signal.v}': this SDK speaks major version 1 (accepts '1' or '1.x')`
275
+ );
276
+ }
248
277
  if (!signal.id.startsWith("evt_")) {
249
278
  throw new Error(`Signal id must start with 'evt_', got: ${signal.id}`);
250
279
  }
@@ -282,6 +311,7 @@ function taskSignal(args) {
282
311
  const payload = { input: args.input };
283
312
  if (args.contextRef) payload["context_ref"] = args.contextRef;
284
313
  if (args.capabilities) payload["capabilities"] = args.capabilities;
314
+ if (args.finalize) payload["finalize"] = true;
285
315
  return createSignal({
286
316
  type: SignalType.TASK,
287
317
  trace_id: args.traceId ?? newTraceId(),
@@ -378,9 +408,10 @@ function errorSignal(args) {
378
408
  }
379
409
  function registerSignal(args) {
380
410
  const caps = args.capabilities ?? args.directed?.capabilities ?? [];
381
- const payload = { capabilities: caps };
411
+ const role = args.role ?? (args.engram ? "engram" : "neuron");
412
+ const payload = { role, capabilities: caps };
382
413
  if (args.version) payload["version"] = args.version;
383
- if (args.engram) payload["engram"] = true;
414
+ if (args.engram || role === "engram") payload["engram"] = true;
384
415
  return createSignal({
385
416
  type: SignalType.REGISTER,
386
417
  trace_id: newTraceId(),
@@ -445,6 +476,32 @@ function bidSignal(args) {
445
476
  meta: args.meta ?? {}
446
477
  });
447
478
  }
479
+ function taskAwardedSignal(args) {
480
+ const payload = { input: args.input };
481
+ if (args.winningBid !== void 0) payload["winning_bid"] = args.winningBid;
482
+ if (args.contextRef !== void 0) payload["context_ref"] = args.contextRef;
483
+ if (args.finalize) payload["finalize"] = true;
484
+ return createSignal({
485
+ type: SignalType.TASK_AWARDED,
486
+ trace_id: args.traceId,
487
+ parent_id: args.parentId,
488
+ directed: args.directed ?? null,
489
+ payload,
490
+ meta: args.meta ?? {}
491
+ });
492
+ }
493
+ function taskDeclinedSignal(args) {
494
+ const payload = {};
495
+ if (args.reason !== void 0) payload["reason"] = args.reason;
496
+ return createSignal({
497
+ type: SignalType.TASK_DECLINED,
498
+ trace_id: args.traceId,
499
+ parent_id: args.parentId,
500
+ directed: args.directed ?? null,
501
+ payload,
502
+ meta: args.meta ?? {}
503
+ });
504
+ }
448
505
  function critiqueSignal(args) {
449
506
  return createSignal({
450
507
  type: SignalType.CRITIQUE,
@@ -640,6 +697,237 @@ function imprintedSignal(args) {
640
697
  meta: args.meta ?? {}
641
698
  });
642
699
  }
700
+ function stopSignal(args) {
701
+ const payload = { rollback: Boolean(args.rollback) };
702
+ if (args.reason !== void 0) payload["reason"] = args.reason;
703
+ return createSignal({
704
+ type: SignalType.STOP,
705
+ trace_id: args.traceId,
706
+ parent_id: args.parentId ?? null,
707
+ directed: args.directed ?? null,
708
+ payload,
709
+ meta: args.meta ?? {}
710
+ });
711
+ }
712
+ function stoppedSignal(args) {
713
+ const payload = {
714
+ rolled_back: Boolean(args.rolledBack),
715
+ cancelled: args.cancelled ?? 0,
716
+ compensated: args.compensated ?? 0
717
+ };
718
+ if (args.node !== void 0) payload["node"] = args.node;
719
+ return createSignal({
720
+ type: SignalType.STOPPED,
721
+ trace_id: args.traceId,
722
+ parent_id: args.parentId ?? null,
723
+ directed: args.directed ?? null,
724
+ payload,
725
+ meta: args.meta ?? {}
726
+ });
727
+ }
728
+
729
+ // src/pathway.ts
730
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set([
731
+ SignalType.FINAL,
732
+ SignalType.ERROR
733
+ ]);
734
+ var WAIT_TYPES = /* @__PURE__ */ new Set([
735
+ SignalType.AGENT_OUTPUT,
736
+ SignalType.CLARIFICATION,
737
+ SignalType.PERMISSION,
738
+ SignalType.ERROR,
739
+ SignalType.FINAL
740
+ ]);
741
+ var SCOPE_TERMINAL_TYPES = /* @__PURE__ */ new Set([
742
+ SignalType.FINAL,
743
+ SignalType.ERROR,
744
+ SignalType.CLARIFICATION,
745
+ SignalType.PERMISSION
746
+ ]);
747
+ var PATHWAY_TYPES = new Set(
748
+ Object.values(SignalType).filter(
749
+ (t) => t !== SignalType.TASK && t !== SignalType.REGISTER && t !== SignalType.DEREGISTER && t !== SignalType.HEARTBEAT && t !== SignalType.DISCOVER
750
+ )
751
+ );
752
+ var PathwayClosedError = class extends Error {
753
+ constructor(message) {
754
+ super(message);
755
+ this.name = "PathwayClosedError";
756
+ }
757
+ };
758
+ var Pathway = class {
759
+ traceId;
760
+ parentId;
761
+ role;
762
+ scope;
763
+ scopeFilter;
764
+ onCloseHook;
765
+ handlers = /* @__PURE__ */ new Map();
766
+ waiters = [];
767
+ buffered = [];
768
+ closed_ = false;
769
+ // Async iteration: a pull queue of pending `next()` resolvers and a push
770
+ // queue of undelivered values. `null` is the close sentinel.
771
+ iterPush = [];
772
+ iterPull = [];
773
+ constructor(opts) {
774
+ const scope = opts.scope ?? "all";
775
+ if (scope !== "all" && scope !== "terminal") {
776
+ throw new Error(`scope must be 'all' or 'terminal', got '${scope}'`);
777
+ }
778
+ this.traceId = opts.traceId;
779
+ this.parentId = opts.parentId ?? null;
780
+ this.role = opts.role ?? "originator";
781
+ this.scope = scope;
782
+ this.scopeFilter = scope === "terminal" ? SCOPE_TERMINAL_TYPES : null;
783
+ this.onCloseHook = opts.onClose;
784
+ }
785
+ get closed() {
786
+ return this.closed_;
787
+ }
788
+ // -- consumer shape #1: wait ---------------------------------------
789
+ /** Resolve on the next AGENT_OUTPUT, CLARIFICATION, PERMISSION, ERROR or
790
+ * FINAL. Rejects with PathwayClosedError if the Pathway closes first, and
791
+ * with a TimeoutError-named Error if `timeoutMs` elapses. */
792
+ async wait(timeoutMs) {
793
+ return this.waitForTypes(WAIT_TYPES, timeoutMs);
794
+ }
795
+ /** Resolve on the next Signal of the given type. */
796
+ async waitFor(type, timeoutMs) {
797
+ return this.waitForTypes(/* @__PURE__ */ new Set([type]), timeoutMs);
798
+ }
799
+ async waitForTypes(types, timeoutMs) {
800
+ for (let i = 0; i < this.buffered.length; i++) {
801
+ const sig = this.buffered[i];
802
+ if (types.has(sig.type)) {
803
+ this.buffered.splice(i, 1);
804
+ return sig;
805
+ }
806
+ }
807
+ if (this.closed_) {
808
+ throw new PathwayClosedError(`Pathway for trace '${this.traceId}' is closed`);
809
+ }
810
+ return new Promise((resolve, reject) => {
811
+ const waiter = { types, resolve, reject, settled: false };
812
+ let timer = null;
813
+ const settle = (fn) => (a) => {
814
+ if (waiter.settled) return;
815
+ waiter.settled = true;
816
+ if (timer !== null) clearTimeout(timer);
817
+ this.waiters = this.waiters.filter((w) => w !== waiter);
818
+ fn(a);
819
+ };
820
+ waiter.resolve = settle(resolve);
821
+ waiter.reject = settle(reject);
822
+ if (timeoutMs !== void 0) {
823
+ timer = setTimeout(() => {
824
+ const err = new Error(
825
+ `Pathway.wait timed out after ${timeoutMs}ms on trace '${this.traceId}'`
826
+ );
827
+ err.name = "TimeoutError";
828
+ waiter.reject(err);
829
+ }, timeoutMs);
830
+ }
831
+ this.waiters.push(waiter);
832
+ });
833
+ }
834
+ // -- consumer shape #2: callbacks ----------------------------------
835
+ /** Register a callback fired for each Signal of the given type. */
836
+ on(type, fn) {
837
+ const list = this.handlers.get(type) ?? [];
838
+ list.push(fn);
839
+ this.handlers.set(type, list);
840
+ return fn;
841
+ }
842
+ // -- consumer shape #3: async iteration ----------------------------
843
+ [Symbol.asyncIterator]() {
844
+ return {
845
+ next: () => {
846
+ if (this.iterPush.length > 0) {
847
+ const v = this.iterPush.shift();
848
+ return Promise.resolve(
849
+ v === null ? { value: void 0, done: true } : { value: v, done: false }
850
+ );
851
+ }
852
+ if (this.closed_) {
853
+ return Promise.resolve({ value: void 0, done: true });
854
+ }
855
+ return new Promise((resolve) => this.iterPull.push(resolve));
856
+ }
857
+ };
858
+ }
859
+ iterEmit(v) {
860
+ const pull = this.iterPull.shift();
861
+ if (pull) {
862
+ pull(v === null ? { value: void 0, done: true } : { value: v, done: false });
863
+ } else {
864
+ this.iterPush.push(v);
865
+ }
866
+ }
867
+ // -- lifecycle ------------------------------------------------------
868
+ /** Close the Pathway. Idempotent. Pending waits reject with
869
+ * PathwayClosedError; iteration completes; the onClose hook fires once. */
870
+ async close() {
871
+ if (this.closed_) return;
872
+ this.closed_ = true;
873
+ for (const w of [...this.waiters]) {
874
+ w.reject(
875
+ new PathwayClosedError(
876
+ `Pathway for trace '${this.traceId}' closed before a matching Signal arrived`
877
+ )
878
+ );
879
+ }
880
+ this.waiters = [];
881
+ this.iterEmit(null);
882
+ if (this.onCloseHook) {
883
+ try {
884
+ await this.onCloseHook(this);
885
+ } catch {
886
+ }
887
+ }
888
+ }
889
+ /** `await using pathway = ...` support. */
890
+ async [Symbol.asyncDispose]() {
891
+ await this.close();
892
+ }
893
+ // -- internal: signal delivery (called by the owning Dendrite) ------
894
+ /** @internal */
895
+ async _deliver(signal) {
896
+ if (this.closed_) return;
897
+ if (this.scopeFilter !== null && !this.scopeFilter.has(signal.type)) {
898
+ await this.fireHandlers(signal);
899
+ if (TERMINAL_TYPES.has(signal.type)) await this.close();
900
+ return;
901
+ }
902
+ let consumed = false;
903
+ for (const w of [...this.waiters]) {
904
+ if (w.types.has(signal.type)) {
905
+ w.resolve(signal);
906
+ consumed = true;
907
+ }
908
+ }
909
+ if (!consumed) this.buffered.push(signal);
910
+ await this.fireHandlers(signal);
911
+ this.iterEmit(signal);
912
+ if (TERMINAL_TYPES.has(signal.type)) await this.close();
913
+ }
914
+ async fireHandlers(signal) {
915
+ for (const h of this.handlers.get(signal.type) ?? []) {
916
+ try {
917
+ await h(signal);
918
+ } catch {
919
+ }
920
+ }
921
+ }
922
+ };
923
+
924
+ // src/retry.ts
925
+ function defaultRetryOn(outcome) {
926
+ if (outcome instanceof PathwayClosedError) return true;
927
+ if (outcome instanceof Error) return outcome.name === "TimeoutError";
928
+ const sig = outcome;
929
+ return sig.type === SignalType.ERROR && Boolean(sig.payload?.["recoverable"]);
930
+ }
643
931
 
644
932
  // src/synapse.ts
645
933
  var MemorySubscription = class {
@@ -1916,6 +2204,16 @@ var LifecycleHooks = class {
1916
2204
  }
1917
2205
  };
1918
2206
 
2207
+ // src/trace-context.ts
2208
+ var import_node_async_hooks = require("async_hooks");
2209
+ var storage = new import_node_async_hooks.AsyncLocalStorage();
2210
+ function ambientTrace() {
2211
+ return storage.getStore() ?? null;
2212
+ }
2213
+ function runWithTraceContext(traceId, parentId, fn) {
2214
+ return storage.run([traceId, parentId], fn);
2215
+ }
2216
+
1919
2217
  // src/neuron.ts
1920
2218
  function clarify(question, context) {
1921
2219
  return context === void 0 ? { __clarification__: true, question } : { __clarification__: true, question, context };
@@ -1947,122 +2245,492 @@ function isErrorOutput(output) {
1947
2245
  return typeof output === "object" && output !== null && output["__error__"] === true;
1948
2246
  }
1949
2247
 
1950
- // src/neuron-mcp.ts
1951
- var standardMcpServers = {
1952
- filesystem: {
1953
- command: "npx",
1954
- args: ["-y", "@modelcontextprotocol/server-filesystem"],
1955
- note: "Append one or more allowed directories, e.g. args=['/data']."
1956
- },
1957
- memory: {
1958
- command: "npx",
1959
- args: ["-y", "@modelcontextprotocol/server-memory"],
1960
- note: "Knowledge-graph memory store."
1961
- },
1962
- everything: {
1963
- command: "npx",
1964
- args: ["-y", "@modelcontextprotocol/server-everything"],
1965
- note: "Reference server exercising every MCP feature; handy for tests."
1966
- },
1967
- sequentialthinking: {
1968
- command: "npx",
1969
- args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
1970
- note: "Structured step-by-step reasoning tool."
1971
- },
1972
- fetch: {
1973
- command: "uvx",
1974
- args: ["mcp-server-fetch"],
1975
- note: "Fetch a URL and return its content as markdown/text."
1976
- },
1977
- git: {
1978
- command: "uvx",
1979
- args: ["mcp-server-git"],
1980
- note: "Read/inspect a git repo. Append --repository <path>."
1981
- },
1982
- time: {
1983
- command: "uvx",
1984
- args: ["mcp-server-time"],
1985
- note: "Current time and timezone conversions."
1986
- }
1987
- };
1988
- var CONTROL_KEYS = /* @__PURE__ */ new Set(["tool", "arguments", "args", "__list_tools__"]);
1989
- function resolveLaunch(opts) {
1990
- const extra = opts.args ?? [];
1991
- if (opts.server != null) {
1992
- const preset = standardMcpServers[opts.server];
1993
- if (!preset) {
1994
- const available = Object.keys(standardMcpServers).sort().join(", ");
2248
+ // src/engram.ts
2249
+ var EngramBinding = class {
2250
+ name;
2251
+ directedId;
2252
+ directedType;
2253
+ defaultDeadlineMs;
2254
+ defaultRecallMode;
2255
+ constructor(init) {
2256
+ this.name = init.name;
2257
+ this.directedId = init.directedId ?? null;
2258
+ this.directedType = init.directedType ?? null;
2259
+ this.defaultDeadlineMs = init.defaultDeadlineMs ?? null;
2260
+ this.defaultRecallMode = init.defaultRecallMode ?? "first";
2261
+ if (!this.directedId && !this.directedType) {
1995
2262
  throw new Error(
1996
- `Unknown MCP server preset '${opts.server}'. Available: ${available}. (Or pass command/args to wrap any other stdio MCP server.)`
2263
+ `EngramBinding '${this.name}' requires directedId (engram_id) or directedType (engram_kind), or both`
1997
2264
  );
1998
2265
  }
1999
- return { command: opts.command ?? preset.command, args: [...preset.args, ...extra] };
2000
2266
  }
2001
- if (opts.command == null) {
2002
- throw new Error("mcpNeuron(...) needs either `command` (+optional `args`) or a `server` preset name.");
2267
+ /** Build the `Directed` addressing this Engram. */
2268
+ toDirected() {
2269
+ return { id: this.directedId, type: this.directedType, capabilities: [] };
2003
2270
  }
2004
- return { command: opts.command, args: extra };
2005
- }
2006
- function mcpNeuron(opts) {
2007
- const { command, args } = resolveLaunch(opts);
2008
- let client = null;
2009
- let connecting = null;
2010
- async function ensure() {
2011
- if (client) return client;
2012
- if (connecting) return connecting;
2013
- connecting = (async () => {
2014
- const clientSpec = "@modelcontextprotocol/sdk/client/index.js";
2015
- const stdioSpec = "@modelcontextprotocol/sdk/client/stdio.js";
2016
- const clientMod = await import(clientSpec);
2017
- const stdioMod = await import(stdioSpec);
2018
- const Client = clientMod.Client;
2019
- const StdioClientTransport = stdioMod.StdioClientTransport;
2020
- const transport = new StdioClientTransport({
2021
- command,
2022
- args,
2023
- ...opts.env ? { env: opts.env } : {},
2024
- ...opts.cwd ? { cwd: opts.cwd } : {}
2025
- });
2026
- const c = new Client(
2027
- { name: opts.clientName ?? "cosmonapse", version: opts.clientVersion ?? "0.2.0" },
2028
- { capabilities: {} }
2029
- );
2030
- await c.connect(transport);
2031
- client = c;
2032
- return c;
2033
- })();
2034
- return connecting;
2271
+ };
2272
+ var EngramError = class extends Error {
2273
+ constructor(message) {
2274
+ super(message);
2275
+ this.name = new.target.name;
2035
2276
  }
2036
- const fn = (async (input, _context) => {
2037
- const c = await ensure();
2038
- const inp = input ?? {};
2039
- if (inp.__list_tools__) {
2040
- const res2 = await c.listTools();
2041
- return {
2042
- tools: (res2.tools ?? []).map((t) => ({
2043
- name: t.name,
2044
- description: t.description ?? null,
2045
- input_schema: t.inputSchema ?? null
2046
- }))
2047
- };
2277
+ };
2278
+ var EngramTimeout = class extends EngramError {
2279
+ };
2280
+ var EngramCancelled = class extends EngramError {
2281
+ };
2282
+ var EngramNotBound = class extends EngramError {
2283
+ };
2284
+ var EngramOverloaded = class extends EngramError {
2285
+ };
2286
+ var Engram = class {
2287
+ version = null;
2288
+ // ----------------------------------------------------------------------
2289
+ // Saga / compensating-log rollback
2290
+ // ----------------------------------------------------------------------
2291
+ // A backend opts in by calling `sagaRecord` from inside `imprint` with the
2292
+ // inverse op needed to undo the write it is about to apply. `compensate`
2293
+ // then replays those inverses in reverse (LIFO) through the public
2294
+ // `imprint` path with no traceId/imprintId (so they neither re-journal nor
2295
+ // consume idempotency keys). Every inverse is itself a valid
2296
+ // add/upsert/delete, so this is fully backend-agnostic.
2297
+ sagaJournal = /* @__PURE__ */ new Map();
2298
+ sagaRecord(traceId, op, entry, mergeKey) {
2299
+ if (!traceId) return;
2300
+ let j = this.sagaJournal.get(traceId);
2301
+ if (!j) {
2302
+ j = [];
2303
+ this.sagaJournal.set(traceId, j);
2048
2304
  }
2049
- let tool = inp.tool ?? opts.tool;
2050
- let toolArgs = inp.arguments ?? inp.args;
2051
- if (toolArgs == null) {
2052
- toolArgs = {};
2053
- for (const [k, v] of Object.entries(inp)) {
2054
- if (!CONTROL_KEYS.has(k)) toolArgs[k] = v;
2305
+ j.push({ op, entry, mergeKey });
2306
+ }
2307
+ /** Reverse every journaled write for `traceId` (LIFO) and discard the
2308
+ * journal. Returns the number of inverse ops applied. Best-effort. Only
2309
+ * Engram state is reversed - external side effects are out of scope. */
2310
+ async compensate(traceId) {
2311
+ const inverses = this.sagaJournal.get(traceId);
2312
+ if (!inverses) return 0;
2313
+ this.sagaJournal.delete(traceId);
2314
+ let applied = 0;
2315
+ for (let i = inverses.length - 1; i >= 0; i--) {
2316
+ const inv = inverses[i];
2317
+ try {
2318
+ const opts = inv.mergeKey !== void 0 ? { mergeKey: inv.mergeKey } : {};
2319
+ await this.imprint(inv.op, inv.entry, opts);
2320
+ applied++;
2321
+ } catch {
2055
2322
  }
2056
2323
  }
2057
- if (!tool) {
2058
- const res2 = await c.listTools();
2059
- const names = (res2.tools ?? []).map((t) => t.name);
2060
- if (names.length === 1) {
2061
- tool = names[0];
2062
- } else {
2063
- throw new Error(
2064
- `MCP Neuron could not determine which tool to call. Pass tool=... (server exposes: ${JSON.stringify(names)}).`
2065
- );
2324
+ return applied;
2325
+ }
2326
+ /** Discard the trace's saga journal without reversing anything. Called at
2327
+ * the workflow commit point (FINAL/ERROR on the trace). */
2328
+ async commit(traceId) {
2329
+ this.sagaJournal.delete(traceId);
2330
+ }
2331
+ /** Return false if this Engram cannot satisfy the query. Default: serve all. */
2332
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2333
+ async canServe(_query) {
2334
+ return true;
2335
+ }
2336
+ };
2337
+ function receipt(engramId, op, fields = {}) {
2338
+ const error = fields.error ?? null;
2339
+ return {
2340
+ engramId,
2341
+ op,
2342
+ id: fields.id ?? null,
2343
+ version: fields.version ?? null,
2344
+ tookMs: fields.tookMs ?? null,
2345
+ error,
2346
+ ok: error === null
2347
+ };
2348
+ }
2349
+ function entryToDict(e) {
2350
+ const out = {
2351
+ id: e.id,
2352
+ content: e.content,
2353
+ tags: [...e.tags],
2354
+ version: e.version,
2355
+ created_at: e.createdAt,
2356
+ updated_at: e.updatedAt
2357
+ };
2358
+ if (e.mergeKey !== null) out["merge_key"] = e.mergeKey;
2359
+ if (Object.keys(e.extra).length > 0) out["meta"] = { ...e.extra };
2360
+ return out;
2361
+ }
2362
+ function asStringArray(v) {
2363
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
2364
+ }
2365
+ function asObject(v) {
2366
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
2367
+ }
2368
+ var InMemoryEngram = class extends Engram {
2369
+ engramId;
2370
+ engramKind;
2371
+ capabilities;
2372
+ entries = /* @__PURE__ */ new Map();
2373
+ byMergeKey = /* @__PURE__ */ new Map();
2374
+ imprintSeen = /* @__PURE__ */ new Map();
2375
+ constructor(init = {}) {
2376
+ super();
2377
+ this.engramId = init.engramId ?? "engram-memory";
2378
+ this.engramKind = init.engramKind ?? "keyvalue";
2379
+ this.capabilities = init.capabilities ?? ["substring", "tags", "merge_key"];
2380
+ this.version = init.version ?? "0.0.1";
2381
+ }
2382
+ async connect() {
2383
+ return;
2384
+ }
2385
+ async close() {
2386
+ this.entries.clear();
2387
+ this.byMergeKey.clear();
2388
+ this.imprintSeen.clear();
2389
+ }
2390
+ async recall(query, opts = {}) {
2391
+ const q = query ?? {};
2392
+ const text = typeof q["text"] === "string" ? q["text"].toLowerCase() : "";
2393
+ const tagQ = typeof q["tag"] === "string" ? q["tag"] : null;
2394
+ const mergeKey = typeof q["merge_key"] === "string" ? q["merge_key"] : null;
2395
+ const topK = typeof q["top_k"] === "number" ? q["top_k"] : 50;
2396
+ const filters = opts.filters ?? {};
2397
+ const requireTags = asStringArray(filters["tags"]);
2398
+ const since = typeof filters["since"] === "string" ? Date.parse(filters["since"]) : NaN;
2399
+ const until = typeof filters["until"] === "string" ? Date.parse(filters["until"]) : NaN;
2400
+ let candidates;
2401
+ if (mergeKey !== null) {
2402
+ const ids = this.byMergeKey.get(mergeKey) ?? [];
2403
+ candidates = ids.map((i) => this.entries.get(i)).filter((e) => e !== void 0);
2404
+ } else {
2405
+ candidates = [...this.entries.values()];
2406
+ }
2407
+ const hits = [];
2408
+ for (const ent of candidates) {
2409
+ if (requireTags.length > 0 && !requireTags.every((t) => ent.tags.includes(t))) continue;
2410
+ const updated = Date.parse(ent.updatedAt);
2411
+ if (!Number.isNaN(since) && updated < since) continue;
2412
+ if (!Number.isNaN(until) && updated > until) continue;
2413
+ if (tagQ !== null && !ent.tags.includes(tagQ)) continue;
2414
+ let score = 1;
2415
+ if (text) {
2416
+ const hay = String(ent.content).toLowerCase();
2417
+ if (!hay.includes(text)) continue;
2418
+ score = Math.min(1, text.length / Math.max(1, hay.length));
2419
+ }
2420
+ if (opts.minConfidence !== void 0 && score < opts.minConfidence) continue;
2421
+ hits.push({ id: ent.id, entry: entryToDict(ent), score });
2422
+ }
2423
+ hits.sort((a, b) => b.score - a.score);
2424
+ return hits.slice(0, topK);
2425
+ }
2426
+ async imprint(op, entry, opts = {}) {
2427
+ const t0 = Date.now();
2428
+ const mergeKey = opts.mergeKey ?? null;
2429
+ const traceId = opts.traceId;
2430
+ const tookMs = () => Date.now() - t0;
2431
+ if (opts.imprintId !== void 0) {
2432
+ const seen = this.imprintSeen.get(opts.imprintId);
2433
+ if (seen !== void 0) {
2434
+ const existing = this.entries.get(seen);
2435
+ return receipt(this.engramId, op, {
2436
+ id: seen,
2437
+ version: existing ? existing.version : null,
2438
+ tookMs: tookMs()
2439
+ });
2440
+ }
2441
+ }
2442
+ let resultingId = null;
2443
+ let version = null;
2444
+ if (op === "add") {
2445
+ const ent = this.makeEntry(entry, mergeKey);
2446
+ if (this.entries.has(ent.id)) {
2447
+ return receipt(this.engramId, op, { error: `entry id '${ent.id}' already exists`, tookMs: tookMs() });
2448
+ }
2449
+ this.store(ent);
2450
+ resultingId = ent.id;
2451
+ version = ent.version;
2452
+ this.sagaRecord(traceId, "delete", { id: ent.id });
2453
+ } else if (op === "append") {
2454
+ let ent = this.makeEntry(entry, mergeKey);
2455
+ while (this.entries.has(ent.id)) {
2456
+ ent = this.makeEntry({ ...entry, id: newEngramId() }, mergeKey);
2457
+ }
2458
+ this.store(ent);
2459
+ resultingId = ent.id;
2460
+ version = ent.version;
2461
+ this.sagaRecord(traceId, "delete", { id: ent.id });
2462
+ } else if (op === "upsert") {
2463
+ const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
2464
+ const targetId = existingIds[existingIds.length - 1];
2465
+ const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
2466
+ if (old !== void 0) {
2467
+ this.sagaRecord(
2468
+ traceId,
2469
+ "upsert",
2470
+ { id: old.id, content: structuredClone(old.content), tags: [...old.tags], meta: structuredClone(old.extra) },
2471
+ old.mergeKey ?? void 0
2472
+ );
2473
+ const next = this.makeEntry({ ...entry, id: old.id }, mergeKey);
2474
+ next.createdAt = old.createdAt;
2475
+ next.version = old.version + 1;
2476
+ this.store(next, true);
2477
+ resultingId = next.id;
2478
+ version = next.version;
2479
+ } else {
2480
+ const ent = this.makeEntry(entry, mergeKey);
2481
+ this.store(ent);
2482
+ resultingId = ent.id;
2483
+ version = ent.version;
2484
+ this.sagaRecord(traceId, "delete", { id: ent.id });
2485
+ }
2486
+ } else if (op === "merge") {
2487
+ const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
2488
+ const targetId = existingIds[existingIds.length - 1];
2489
+ const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
2490
+ if (old === void 0) {
2491
+ return receipt(this.engramId, op, { error: `no entry for merge_key='${mergeKey}'`, tookMs: tookMs() });
2492
+ }
2493
+ this.sagaRecord(
2494
+ traceId,
2495
+ "upsert",
2496
+ { id: old.id, content: structuredClone(old.content), tags: [...old.tags], meta: structuredClone(old.extra) },
2497
+ old.mergeKey ?? void 0
2498
+ );
2499
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2500
+ const next = {
2501
+ id: old.id,
2502
+ content: deepMerge(old.content, entry["content"]),
2503
+ tags: [.../* @__PURE__ */ new Set([...old.tags, ...asStringArray(entry["tags"])])],
2504
+ mergeKey: old.mergeKey,
2505
+ version: old.version + 1,
2506
+ createdAt: old.createdAt,
2507
+ updatedAt: now,
2508
+ extra: asObject(deepMerge(old.extra, entry["meta"]))
2509
+ };
2510
+ this.store(next, true);
2511
+ resultingId = next.id;
2512
+ version = next.version;
2513
+ } else if (op === "delete") {
2514
+ let targetId = null;
2515
+ const entId = entry["id"];
2516
+ if (typeof entId === "string") {
2517
+ targetId = entId;
2518
+ } else if (mergeKey !== null) {
2519
+ const ids = this.byMergeKey.get(mergeKey) ?? [];
2520
+ targetId = ids[ids.length - 1] ?? null;
2521
+ }
2522
+ if (targetId === null || !this.entries.has(targetId)) {
2523
+ return receipt(this.engramId, op, { tookMs: tookMs() });
2524
+ }
2525
+ const old = this.entries.get(targetId);
2526
+ this.sagaRecord(
2527
+ traceId,
2528
+ "add",
2529
+ { id: old.id, content: structuredClone(old.content), tags: [...old.tags], meta: structuredClone(old.extra) },
2530
+ old.mergeKey ?? void 0
2531
+ );
2532
+ this.evict(targetId);
2533
+ resultingId = targetId;
2534
+ version = null;
2535
+ } else {
2536
+ return receipt(this.engramId, op, { error: `unknown op '${op}'`, tookMs: tookMs() });
2537
+ }
2538
+ if (opts.imprintId !== void 0 && resultingId !== null) {
2539
+ this.imprintSeen.set(opts.imprintId, resultingId);
2540
+ }
2541
+ return receipt(this.engramId, op, { id: resultingId, version, tookMs: tookMs() });
2542
+ }
2543
+ /** Test/debug helper - NOT part of the Engram contract. */
2544
+ snapshot() {
2545
+ return [...this.entries.values()].map(entryToDict);
2546
+ }
2547
+ makeEntry(entry, mergeKey) {
2548
+ const id = typeof entry["id"] === "string" ? entry["id"] : newEngramId();
2549
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2550
+ return {
2551
+ id,
2552
+ content: entry["content"],
2553
+ tags: asStringArray(entry["tags"]),
2554
+ mergeKey,
2555
+ version: 1,
2556
+ createdAt: now,
2557
+ updatedAt: now,
2558
+ extra: asObject(entry["meta"])
2559
+ };
2560
+ }
2561
+ store(ent, replace = false) {
2562
+ if (replace) {
2563
+ const old = this.entries.get(ent.id);
2564
+ if (old !== void 0 && old.mergeKey) {
2565
+ const bucket = this.byMergeKey.get(old.mergeKey);
2566
+ if (bucket) {
2567
+ const idx = bucket.indexOf(ent.id);
2568
+ if (idx >= 0) bucket.splice(idx, 1);
2569
+ if (bucket.length === 0) this.byMergeKey.delete(old.mergeKey);
2570
+ }
2571
+ }
2572
+ }
2573
+ this.entries.set(ent.id, ent);
2574
+ if (ent.mergeKey) {
2575
+ const bucket = this.byMergeKey.get(ent.mergeKey);
2576
+ if (bucket) bucket.push(ent.id);
2577
+ else this.byMergeKey.set(ent.mergeKey, [ent.id]);
2578
+ }
2579
+ }
2580
+ evict(entryId) {
2581
+ const ent = this.entries.get(entryId);
2582
+ this.entries.delete(entryId);
2583
+ if (ent === void 0) return;
2584
+ if (ent.mergeKey) {
2585
+ const bucket = this.byMergeKey.get(ent.mergeKey);
2586
+ if (bucket) {
2587
+ const idx = bucket.indexOf(entryId);
2588
+ if (idx >= 0) bucket.splice(idx, 1);
2589
+ if (bucket.length === 0) this.byMergeKey.delete(ent.mergeKey);
2590
+ }
2591
+ }
2592
+ }
2593
+ };
2594
+ function deepMerge(base, incoming) {
2595
+ if (incoming === void 0 || incoming === null) return base;
2596
+ const bothObjects = base !== null && typeof base === "object" && !Array.isArray(base) && typeof incoming === "object" && !Array.isArray(incoming);
2597
+ if (bothObjects) {
2598
+ const out = { ...base };
2599
+ for (const [k, v] of Object.entries(incoming)) {
2600
+ out[k] = k in out ? deepMerge(out[k], v) : v;
2601
+ }
2602
+ return out;
2603
+ }
2604
+ if (Array.isArray(base) && Array.isArray(incoming)) {
2605
+ const seen = /* @__PURE__ */ new Set();
2606
+ const out = [];
2607
+ for (const item of [...base, ...incoming]) {
2608
+ const key = JSON.stringify(item);
2609
+ if (seen.has(key)) continue;
2610
+ seen.add(key);
2611
+ out.push(item);
2612
+ }
2613
+ return out;
2614
+ }
2615
+ return incoming;
2616
+ }
2617
+
2618
+ // src/neuron-mcp.ts
2619
+ var standardMcpServers = {
2620
+ filesystem: {
2621
+ command: "npx",
2622
+ args: ["-y", "@modelcontextprotocol/server-filesystem"],
2623
+ note: "Append one or more allowed directories, e.g. args=['/data']."
2624
+ },
2625
+ memory: {
2626
+ command: "npx",
2627
+ args: ["-y", "@modelcontextprotocol/server-memory"],
2628
+ note: "Knowledge-graph memory store."
2629
+ },
2630
+ everything: {
2631
+ command: "npx",
2632
+ args: ["-y", "@modelcontextprotocol/server-everything"],
2633
+ note: "Reference server exercising every MCP feature; handy for tests."
2634
+ },
2635
+ sequentialthinking: {
2636
+ command: "npx",
2637
+ args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
2638
+ note: "Structured step-by-step reasoning tool."
2639
+ },
2640
+ fetch: {
2641
+ command: "uvx",
2642
+ args: ["mcp-server-fetch"],
2643
+ note: "Fetch a URL and return its content as markdown/text."
2644
+ },
2645
+ git: {
2646
+ command: "uvx",
2647
+ args: ["mcp-server-git"],
2648
+ note: "Read/inspect a git repo. Append --repository <path>."
2649
+ },
2650
+ time: {
2651
+ command: "uvx",
2652
+ args: ["mcp-server-time"],
2653
+ note: "Current time and timezone conversions."
2654
+ }
2655
+ };
2656
+ var CONTROL_KEYS = /* @__PURE__ */ new Set(["tool", "arguments", "args", "__list_tools__"]);
2657
+ function resolveLaunch(opts) {
2658
+ const extra = opts.args ?? [];
2659
+ if (opts.server != null) {
2660
+ const preset = standardMcpServers[opts.server];
2661
+ if (!preset) {
2662
+ const available = Object.keys(standardMcpServers).sort().join(", ");
2663
+ throw new Error(
2664
+ `Unknown MCP server preset '${opts.server}'. Available: ${available}. (Or pass command/args to wrap any other stdio MCP server.)`
2665
+ );
2666
+ }
2667
+ return { command: opts.command ?? preset.command, args: [...preset.args, ...extra] };
2668
+ }
2669
+ if (opts.command == null) {
2670
+ throw new Error("mcpNeuron(...) needs either `command` (+optional `args`) or a `server` preset name.");
2671
+ }
2672
+ return { command: opts.command, args: extra };
2673
+ }
2674
+ function mcpNeuron(opts) {
2675
+ const { command, args } = resolveLaunch(opts);
2676
+ let client = null;
2677
+ let connecting = null;
2678
+ async function ensure() {
2679
+ if (client) return client;
2680
+ if (connecting) return connecting;
2681
+ connecting = (async () => {
2682
+ const clientSpec = "@modelcontextprotocol/sdk/client/index.js";
2683
+ const stdioSpec = "@modelcontextprotocol/sdk/client/stdio.js";
2684
+ const clientMod = await import(clientSpec);
2685
+ const stdioMod = await import(stdioSpec);
2686
+ const Client = clientMod.Client;
2687
+ const StdioClientTransport = stdioMod.StdioClientTransport;
2688
+ const transport = new StdioClientTransport({
2689
+ command,
2690
+ args,
2691
+ ...opts.env ? { env: opts.env } : {},
2692
+ ...opts.cwd ? { cwd: opts.cwd } : {}
2693
+ });
2694
+ const c = new Client(
2695
+ { name: opts.clientName ?? "cosmonapse", version: opts.clientVersion ?? "0.2.0" },
2696
+ { capabilities: {} }
2697
+ );
2698
+ await c.connect(transport);
2699
+ client = c;
2700
+ return c;
2701
+ })();
2702
+ return connecting;
2703
+ }
2704
+ const fn = (async (input, _context) => {
2705
+ const c = await ensure();
2706
+ const inp = input ?? {};
2707
+ if (inp.__list_tools__) {
2708
+ const res2 = await c.listTools();
2709
+ return {
2710
+ tools: (res2.tools ?? []).map((t) => ({
2711
+ name: t.name,
2712
+ description: t.description ?? null,
2713
+ input_schema: t.inputSchema ?? null
2714
+ }))
2715
+ };
2716
+ }
2717
+ let tool = inp.tool ?? opts.tool;
2718
+ let toolArgs = inp.arguments ?? inp.args;
2719
+ if (toolArgs == null) {
2720
+ toolArgs = {};
2721
+ for (const [k, v] of Object.entries(inp)) {
2722
+ if (!CONTROL_KEYS.has(k)) toolArgs[k] = v;
2723
+ }
2724
+ }
2725
+ if (!tool) {
2726
+ const res2 = await c.listTools();
2727
+ const names = (res2.tools ?? []).map((t) => t.name);
2728
+ if (names.length === 1) {
2729
+ tool = names[0];
2730
+ } else {
2731
+ throw new Error(
2732
+ `MCP Neuron could not determine which tool to call. Pass tool=... (server exposes: ${JSON.stringify(names)}).`
2733
+ );
2066
2734
  }
2067
2735
  }
2068
2736
  const res = await c.callTool({ name: tool, arguments: toolArgs });
@@ -2099,8 +2767,46 @@ function readMessages(input) {
2099
2767
  const m = input["messages"];
2100
2768
  return Array.isArray(m) ? m : null;
2101
2769
  }
2770
+ function followupPrompt(input) {
2771
+ const c = input["clarification"];
2772
+ if (c !== null && typeof c === "object" && !Array.isArray(c)) {
2773
+ const cd = c;
2774
+ const lines = ["You previously asked a clarifying question while working on a task."];
2775
+ if (cd["question"] !== void 0 && cd["question"] !== null) {
2776
+ lines.push(`Your question: ${String(cd["question"])}`);
2777
+ }
2778
+ if ("answer" in cd) lines.push(`The answer: ${JSON.stringify(cd["answer"])}`);
2779
+ const extra = Object.fromEntries(
2780
+ Object.entries(cd).filter(([k]) => k !== "question" && k !== "answer")
2781
+ );
2782
+ if (Object.keys(extra).length) lines.push(`Additional context: ${JSON.stringify(extra)}`);
2783
+ lines.push("Continue the original task using this answer.");
2784
+ return lines.join("\n");
2785
+ }
2786
+ const p = input["permission"];
2787
+ if (p !== null && typeof p === "object" && !Array.isArray(p)) {
2788
+ const pd = p;
2789
+ const granted = Boolean(pd["granted"]);
2790
+ const lines = ["You previously requested permission while working on a task."];
2791
+ if (pd["action"] !== void 0 && pd["action"] !== null) {
2792
+ lines.push(`Requested action: ${String(pd["action"])}`);
2793
+ }
2794
+ lines.push(`The decision: ${granted ? "GRANTED" : "DENIED"}.`);
2795
+ if (pd["reason"] !== void 0 && pd["reason"] !== null) {
2796
+ lines.push(`Reason: ${String(pd["reason"])}`);
2797
+ }
2798
+ if (pd["ttl_ms"] !== void 0 && pd["ttl_ms"] !== null) {
2799
+ lines.push(`The grant is valid for ${String(pd["ttl_ms"])} ms.`);
2800
+ }
2801
+ lines.push(
2802
+ 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."
2803
+ );
2804
+ return lines.join("\n");
2805
+ }
2806
+ return null;
2807
+ }
2102
2808
  function requireInput(input, provider) {
2103
- const prompt = readPrompt(input);
2809
+ const prompt = readPrompt(input) ?? followupPrompt(input);
2104
2810
  const messages = readMessages(input);
2105
2811
  if (!prompt && !messages) {
2106
2812
  throw new Error(
@@ -2311,9 +3017,11 @@ var Axon = class _Axon {
2311
3017
  neuronId;
2312
3018
  capabilities;
2313
3019
  version;
3020
+ neuronKind;
2314
3021
  fn;
2315
3022
  contextFetcher;
2316
3023
  outputParser;
3024
+ engramBindings = /* @__PURE__ */ new Map();
2317
3025
  dendrite = null;
2318
3026
  /**
2319
3027
  * Decorator-registered recognisers, one bucket per capability (the asking
@@ -2322,48 +3030,139 @@ var Axon = class _Axon {
2322
3030
  * output by {@link applyRecognisers}.
2323
3031
  */
2324
3032
  recognisers = { error: [], clarification: [], permission: [], output: [] };
3033
+ /** Pre-task hooks (beforeTask): transform/validate/reject the TASK input
3034
+ * before the Neuron runs. */
3035
+ beforeTaskHooks = [];
2325
3036
  /** @internal - lifecycle hooks, driven by the hosting Dendrite. */
2326
3037
  hooks = new LifecycleHooks(this);
2327
3038
  constructor(opts) {
2328
3039
  this.neuronId = opts.neuronId;
2329
3040
  this.capabilities = opts.capabilities ?? [];
2330
3041
  this.version = opts.version;
3042
+ this.neuronKind = opts.neuronKind ?? "neuron";
2331
3043
  this.fn = opts.neuronFn;
2332
3044
  this.contextFetcher = opts.contextFetcher ?? noopContextFetcher;
2333
3045
  this.outputParser = opts.outputParser;
3046
+ for (const b of opts.engrams ?? []) {
3047
+ if (this.engramBindings.has(b.name)) {
3048
+ throw new Error(`Axon '${opts.neuronId}': duplicate EngramBinding name '${b.name}'`);
3049
+ }
3050
+ this.engramBindings.set(b.name, b);
3051
+ }
3052
+ }
3053
+ /** Declared Engram bindings, keyed by name. */
3054
+ get engrams() {
3055
+ return new Map(this.engramBindings);
3056
+ }
3057
+ resolveBinding(name) {
3058
+ const binding = this.engramBindings.get(name);
3059
+ if (!binding) {
3060
+ throw new EngramNotBound(
3061
+ `Axon '${this.neuronId}': no Engram binding named '${name}'; available: ${[...this.engramBindings.keys()].sort().join(", ")}`
3062
+ );
3063
+ }
3064
+ return binding;
3065
+ }
3066
+ /** Build the per-task helpers object handed to the Neuron as its third
3067
+ * argument. Helpers throw EngramNotBound for undeclared names and
3068
+ * require a hosting Dendrite (the only thing the Axon pulls from it). */
3069
+ buildHelpers(traceId, parentId) {
3070
+ const requireClient = () => {
3071
+ if (this.dendrite === null) {
3072
+ throw new Error(
3073
+ `Axon '${this.neuronId}': not attached to a Dendrite; engram helpers require a hosting Dendrite`
3074
+ );
3075
+ }
3076
+ return this.dendrite.engramClient;
3077
+ };
3078
+ return {
3079
+ recall: async (name, args) => {
3080
+ const binding = this.resolveBinding(name);
3081
+ return requireClient().recall({
3082
+ binding,
3083
+ query: args.query,
3084
+ traceId,
3085
+ parentId,
3086
+ ...args.filters !== void 0 ? { filters: args.filters } : {},
3087
+ ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
3088
+ ...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
3089
+ ...args.recallMode !== void 0 ? { recallMode: args.recallMode } : {},
3090
+ ...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
3091
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
3092
+ });
3093
+ },
3094
+ imprint: async (name, args) => {
3095
+ const binding = this.resolveBinding(name);
3096
+ return requireClient().imprint({
3097
+ binding,
3098
+ op: args.op,
3099
+ entry: args.entry,
3100
+ traceId,
3101
+ parentId,
3102
+ ...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
3103
+ ...args.awaitAck !== void 0 ? { awaitAck: args.awaitAck } : {},
3104
+ ...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
3105
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
3106
+ });
3107
+ }
3108
+ };
2334
3109
  }
2335
3110
  // -- source-paired factories --------------------------------------
2336
3111
  // Build an Axon already paired with one of the `neuron(source, ...)`
2337
3112
  // providers AND wired with the matching recogniser. No new class: the
2338
3113
  // result is a plain Axon.
3114
+ /** Resolve the teach-intents decision and return (possibly augmented) source opts. */
3115
+ static applyTeachIntents(source, opts, extra) {
3116
+ const recognize = extra.recognize ?? true;
3117
+ const teach = extra.teachIntents ?? (recognize && SYSTEM_CAPABLE_SOURCES.has(source.toLowerCase()));
3118
+ if (!teach) return opts;
3119
+ if (!SYSTEM_CAPABLE_SOURCES.has(source.toLowerCase())) {
3120
+ throw new Error(
3121
+ `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).`
3122
+ );
3123
+ }
3124
+ const existing = opts?.system;
3125
+ return {
3126
+ ...opts ?? {},
3127
+ system: existing ? `${existing}
3128
+
3129
+ ${COSMO_INTENT_SYSTEM_PROMPT}` : COSMO_INTENT_SYSTEM_PROMPT
3130
+ };
3131
+ }
2339
3132
  static build(neuronId, neuronFn, source, extra) {
2340
3133
  const recognize = extra.recognize ?? true;
2341
3134
  const o = { neuronId, neuronFn };
2342
3135
  if (extra.capabilities) o.capabilities = extra.capabilities;
2343
3136
  if (extra.version !== void 0) o.version = extra.version;
3137
+ if (extra.neuronKind !== void 0) o.neuronKind = extra.neuronKind;
2344
3138
  if (extra.contextFetcher) o.contextFetcher = extra.contextFetcher;
2345
3139
  if (recognize) o.outputParser = source === "mcp" ? parseMcpIntents : parseLlmIntents;
2346
3140
  return new _Axon(o);
2347
3141
  }
2348
3142
  /** Axon paired with any registered Neuron source + its recogniser. */
2349
3143
  static fromSource(source, neuronId, opts, extra = {}) {
2350
- return _Axon.build(neuronId, neuron(source, opts), source, extra);
3144
+ const o = _Axon.applyTeachIntents(source, opts, extra);
3145
+ return _Axon.build(neuronId, neuron(source, o), source, extra);
2351
3146
  }
2352
3147
  /** Axon paired with the OpenAI Chat Completions API. */
2353
3148
  static openai(neuronId, opts, extra = {}) {
2354
- return _Axon.build(neuronId, neuron("openai", opts), "openai", extra);
3149
+ const o = _Axon.applyTeachIntents("openai", opts, extra);
3150
+ return _Axon.build(neuronId, neuron("openai", o), "openai", extra);
2355
3151
  }
2356
3152
  /** Axon paired with the Anthropic Messages API. */
2357
3153
  static anthropic(neuronId, opts, extra = {}) {
2358
- return _Axon.build(neuronId, neuron("anthropic", opts), "anthropic", extra);
3154
+ const o = _Axon.applyTeachIntents("anthropic", opts, extra);
3155
+ return _Axon.build(neuronId, neuron("anthropic", o), "anthropic", extra);
2359
3156
  }
2360
3157
  /** Axon paired with a local Ollama daemon. */
2361
3158
  static ollama(neuronId, opts, extra = {}) {
2362
- return _Axon.build(neuronId, neuron("ollama", opts), "ollama", extra);
3159
+ const o = _Axon.applyTeachIntents("ollama", opts, extra);
3160
+ return _Axon.build(neuronId, neuron("ollama", o), "ollama", extra);
2363
3161
  }
2364
3162
  /** Axon paired with a HuggingFace TGI / OpenAI-compatible endpoint. */
2365
3163
  static huggingface(neuronId, opts, extra = {}) {
2366
- return _Axon.build(neuronId, neuron("huggingface", opts), "huggingface", extra);
3164
+ const o = _Axon.applyTeachIntents("huggingface", opts, extra);
3165
+ return _Axon.build(neuronId, neuron("huggingface", o), "huggingface", extra);
2367
3166
  }
2368
3167
  /** Axon paired with a stdio MCP server. */
2369
3168
  static mcp(neuronId, opts, extra = {}) {
@@ -2375,6 +3174,26 @@ var Axon = class _Axon {
2375
3174
  // Signals). Return the intent's fields to match, or null/undefined to fall
2376
3175
  // through. Sync or async; multiple per capability tried in order. These run
2377
3176
  // after `outputParser` and before the literal `__marker__` checks.
3177
+ /**
3178
+ * Register a pre-task hook over the TASK's `input`. Runs before the Neuron.
3179
+ * Sync or async; multiple hooks run in registration order, each receiving
3180
+ * the previous one's result. Return a (new) object to replace the input,
3181
+ * return null/undefined to pass through unchanged, or throw to reject the
3182
+ * TASK (surfaces as an ERROR Signal, code NEURON_EXCEPTION). The natural
3183
+ * place for input normalisation or per-Axon policy checks.
3184
+ */
3185
+ beforeTask(fn) {
3186
+ this.beforeTaskHooks.push(fn);
3187
+ return fn;
3188
+ }
3189
+ async applyBeforeTask(input) {
3190
+ let current = input;
3191
+ for (const fn of this.beforeTaskHooks) {
3192
+ const r = await fn(current);
3193
+ if (r !== null && r !== void 0) current = r;
3194
+ }
3195
+ return current;
3196
+ }
2378
3197
  /** Detector returning the AGENT_OUTPUT payload, or null to wrap verbatim. */
2379
3198
  detectsOutput(fn) {
2380
3199
  this.recognisers.output.push(fn);
@@ -2445,8 +3264,21 @@ var Axon = class _Axon {
2445
3264
  [DETACH]() {
2446
3265
  this.dendrite = null;
2447
3266
  }
2448
- /** Run the Neuron and return AGENT_OUTPUT / CLARIFICATION / ERROR. */
3267
+ /** Run the Neuron and return AGENT_OUTPUT / CLARIFICATION / ERROR.
3268
+ *
3269
+ * Binds the TASK's (traceId, parentId=task.id) as the ambient trace
3270
+ * context for the whole handling pass - neuronFn, detectors, and hooks
3271
+ * included - so engram calls made without explicit trace plumbing (e.g.
3272
+ * `dendrite.imprint` from a `detectsOutput` hook) are attributed to this
3273
+ * task's trace. */
2449
3274
  async handleTask(task) {
3275
+ return runWithTraceContext(
3276
+ task.trace_id,
3277
+ task.id,
3278
+ () => this.handleTaskInner(task)
3279
+ );
3280
+ }
3281
+ async handleTaskInner(task) {
2450
3282
  const traceId = task.trace_id;
2451
3283
  const parentId = task.id;
2452
3284
  const input = task.payload["input"] ?? {};
@@ -2461,7 +3293,9 @@ var Axon = class _Axon {
2461
3293
  }
2462
3294
  let rawOutput;
2463
3295
  try {
2464
- rawOutput = await this.fn(input, context);
3296
+ const effectiveInput = this.beforeTaskHooks.length ? await this.applyBeforeTask(input) : input;
3297
+ const helpers = this.engramBindings.size ? this.buildHelpers(traceId, parentId) : void 0;
3298
+ rawOutput = await this.fn(effectiveInput, context, helpers);
2465
3299
  if (this.outputParser) rawOutput = this.outputParser(rawOutput);
2466
3300
  rawOutput = await this.applyRecognisers(rawOutput);
2467
3301
  } catch (err) {
@@ -2508,6 +3342,16 @@ var Axon = class _Axon {
2508
3342
  return agentOutputSignal({ traceId, parentId, directed: { id: this.neuronId }, output });
2509
3343
  }
2510
3344
  };
3345
+ 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.';
3346
+ var SYSTEM_CAPABLE_SOURCES = /* @__PURE__ */ new Set([
3347
+ "ollama",
3348
+ "openai",
3349
+ "anthropic",
3350
+ "groq",
3351
+ "openrouter",
3352
+ "together",
3353
+ "mistral"
3354
+ ]);
2511
3355
  var INTENT_KEY = "cosmo";
2512
3356
  var FENCED_JSON = /```(?:json)?\s*(\{[\s\S]*?\})\s*```/g;
2513
3357
  function extractCosmoIntent(text) {
@@ -2580,15 +3424,241 @@ function parseMcpIntents(raw) {
2580
3424
  const msg = r["response"] ?? r["content"] ?? "MCP tool returned is_error";
2581
3425
  return { __error__: true, code: "MCP_TOOL_ERROR", message: String(msg) };
2582
3426
  }
2583
- const text = r["response"];
2584
- if (typeof text === "string") {
2585
- const intent = extractCosmoIntent(text);
2586
- if (intent) {
2587
- const marker = intentToMarker(intent);
2588
- if (marker) return marker;
2589
- }
3427
+ const text = r["response"];
3428
+ if (typeof text === "string") {
3429
+ const intent = extractCosmoIntent(text);
3430
+ if (intent) {
3431
+ const marker = intentToMarker(intent);
3432
+ if (marker) return marker;
3433
+ }
3434
+ }
3435
+ return raw;
3436
+ }
3437
+
3438
+ // src/engram-client.ts
3439
+ function deferred() {
3440
+ let resolve;
3441
+ let reject;
3442
+ const promise = new Promise((res, rej) => {
3443
+ resolve = res;
3444
+ reject = rej;
3445
+ });
3446
+ return { promise, resolve, reject };
3447
+ }
3448
+ var EngramClient = class {
3449
+ constructor(publisher) {
3450
+ this.publisher = publisher;
3451
+ }
3452
+ publisher;
3453
+ pendingRecalls = /* @__PURE__ */ new Map();
3454
+ pendingImprints = /* @__PURE__ */ new Map();
3455
+ byTrace = /* @__PURE__ */ new Map();
3456
+ async recall(args) {
3457
+ let engramId = args.engramId;
3458
+ let engramKind = args.engramKind;
3459
+ let deadlineMs = args.deadlineMs;
3460
+ let recallMode = args.recallMode;
3461
+ if (args.binding) {
3462
+ engramId = engramId ?? args.binding.directedId ?? void 0;
3463
+ engramKind = engramKind ?? args.binding.directedType ?? void 0;
3464
+ if (deadlineMs === void 0) deadlineMs = args.binding.defaultDeadlineMs ?? void 0;
3465
+ if (recallMode === void 0) recallMode = args.binding.defaultRecallMode;
3466
+ }
3467
+ const mode = recallMode ?? "first";
3468
+ const sig = recallSignal({
3469
+ traceId: args.traceId,
3470
+ parentId: args.parentId,
3471
+ directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
3472
+ query: args.query,
3473
+ ...args.filters !== void 0 ? { filters: args.filters } : {},
3474
+ ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
3475
+ ...deadlineMs !== void 0 ? { deadlineMs } : {},
3476
+ ...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
3477
+ recallMode: mode,
3478
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
3479
+ });
3480
+ const d = deferred();
3481
+ const pending = { deferred: d, mode, timer: null, done: false, hitsSoFar: [], engrams: [] };
3482
+ this.pendingRecalls.set(sig.id, pending);
3483
+ this.track(args.traceId, sig.id);
3484
+ if (deadlineMs !== void 0 && deadlineMs > 0) {
3485
+ pending.timer = setTimeout(() => this.onRecallDeadline(sig.id), deadlineMs);
3486
+ }
3487
+ try {
3488
+ await this.publisher.publish(sig);
3489
+ } catch (err) {
3490
+ this.cleanupRecall(args.traceId, sig.id);
3491
+ throw err;
3492
+ }
3493
+ try {
3494
+ return await d.promise;
3495
+ } finally {
3496
+ this.cleanupRecall(args.traceId, sig.id);
3497
+ }
3498
+ }
3499
+ async imprint(args) {
3500
+ let engramId = args.engramId;
3501
+ let engramKind = args.engramKind;
3502
+ if (args.binding) {
3503
+ engramId = engramId ?? args.binding.directedId ?? void 0;
3504
+ engramKind = engramKind ?? args.binding.directedType ?? void 0;
3505
+ }
3506
+ const sig = imprintSignal({
3507
+ traceId: args.traceId,
3508
+ parentId: args.parentId,
3509
+ directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
3510
+ op: args.op,
3511
+ entry: args.entry,
3512
+ ...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
3513
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
3514
+ });
3515
+ if (!args.awaitAck) {
3516
+ await this.publisher.publish(sig);
3517
+ return null;
3518
+ }
3519
+ const d = deferred();
3520
+ const pending = { deferred: d, timer: null, done: false };
3521
+ this.pendingImprints.set(sig.id, pending);
3522
+ this.track(args.traceId, sig.id);
3523
+ if (args.deadlineMs !== void 0 && args.deadlineMs > 0) {
3524
+ pending.timer = setTimeout(() => this.onImprintDeadline(sig.id), args.deadlineMs);
3525
+ }
3526
+ try {
3527
+ await this.publisher.publish(sig);
3528
+ } catch (err) {
3529
+ this.cleanupImprint(args.traceId, sig.id);
3530
+ throw err;
3531
+ }
3532
+ try {
3533
+ return await d.promise;
3534
+ } finally {
3535
+ this.cleanupImprint(args.traceId, sig.id);
3536
+ }
3537
+ }
3538
+ /** Match RECALLED / IMPRINTED by parent_id and resolve pendings. */
3539
+ deliver(sig) {
3540
+ const pid = sig.parent_id;
3541
+ if (pid === null) return;
3542
+ if (sig.type === SignalType.RECALLED) {
3543
+ const pending = this.pendingRecalls.get(pid);
3544
+ if (pending === void 0) return;
3545
+ const hits = hitsFromPayload(sig.payload["hits"]);
3546
+ const engramId = typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "";
3547
+ const tookMs = typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null;
3548
+ const truncated = sig.payload["truncated"] === true;
3549
+ if (pending.mode === "first") {
3550
+ if (!pending.done) {
3551
+ pending.done = true;
3552
+ pending.deferred.resolve({
3553
+ hits,
3554
+ engramIds: engramId ? [engramId] : [],
3555
+ truncated,
3556
+ tookMs
3557
+ });
3558
+ }
3559
+ } else {
3560
+ pending.hitsSoFar.push(...hits);
3561
+ if (engramId) pending.engrams.push(engramId);
3562
+ }
3563
+ } else if (sig.type === SignalType.IMPRINTED) {
3564
+ const pending = this.pendingImprints.get(pid);
3565
+ if (pending === void 0 || pending.done) return;
3566
+ pending.done = true;
3567
+ pending.deferred.resolve({
3568
+ engramId: typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "",
3569
+ op: typeof sig.payload["op"] === "string" ? sig.payload["op"] : "",
3570
+ id: typeof sig.payload["id"] === "string" ? sig.payload["id"] : null,
3571
+ version: typeof sig.payload["version"] === "number" ? sig.payload["version"] : null,
3572
+ tookMs: typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null,
3573
+ error: typeof sig.payload["error"] === "string" ? sig.payload["error"] : null,
3574
+ ok: !(typeof sig.payload["error"] === "string")
3575
+ });
3576
+ }
3577
+ }
3578
+ /** Cancel every in-flight recall/imprint on a trace (FINAL/ERROR or shutdown). */
3579
+ cancelTrace(traceId) {
3580
+ const ids = this.byTrace.get(traceId);
3581
+ this.byTrace.delete(traceId);
3582
+ if (ids === void 0) return;
3583
+ for (const id of ids) {
3584
+ const pr = this.pendingRecalls.get(id);
3585
+ if (pr !== void 0 && !pr.done) {
3586
+ pr.done = true;
3587
+ if (pr.timer !== null) clearTimeout(pr.timer);
3588
+ pr.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while recall ${id} in flight`));
3589
+ this.pendingRecalls.delete(id);
3590
+ }
3591
+ const pi = this.pendingImprints.get(id);
3592
+ if (pi !== void 0 && !pi.done) {
3593
+ pi.done = true;
3594
+ if (pi.timer !== null) clearTimeout(pi.timer);
3595
+ pi.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while imprint ${id} in flight`));
3596
+ this.pendingImprints.delete(id);
3597
+ }
3598
+ }
3599
+ }
3600
+ cancelAll() {
3601
+ for (const traceId of [...this.byTrace.keys()]) this.cancelTrace(traceId);
3602
+ }
3603
+ onRecallDeadline(id) {
3604
+ const pending = this.pendingRecalls.get(id);
3605
+ if (pending === void 0 || pending.done) return;
3606
+ pending.done = true;
3607
+ if (pending.mode === "first") {
3608
+ pending.deferred.reject(new EngramTimeout(`RECALL ${id} elapsed deadline without any responder`));
3609
+ } else {
3610
+ pending.deferred.resolve({
3611
+ hits: [...pending.hitsSoFar].sort((a, b) => b.score - a.score),
3612
+ engramIds: [...pending.engrams],
3613
+ truncated: false,
3614
+ tookMs: null
3615
+ });
3616
+ }
3617
+ }
3618
+ onImprintDeadline(id) {
3619
+ const pending = this.pendingImprints.get(id);
3620
+ if (pending === void 0 || pending.done) return;
3621
+ pending.done = true;
3622
+ pending.deferred.reject(new EngramTimeout(`IMPRINT ${id} elapsed deadline without IMPRINTED`));
3623
+ }
3624
+ track(traceId, id) {
3625
+ const bucket = this.byTrace.get(traceId);
3626
+ if (bucket) bucket.add(id);
3627
+ else this.byTrace.set(traceId, /* @__PURE__ */ new Set([id]));
3628
+ }
3629
+ cleanupRecall(traceId, id) {
3630
+ const p = this.pendingRecalls.get(id);
3631
+ if (p?.timer != null) clearTimeout(p.timer);
3632
+ this.pendingRecalls.delete(id);
3633
+ this.discardTrace(traceId, id);
3634
+ }
3635
+ cleanupImprint(traceId, id) {
3636
+ const p = this.pendingImprints.get(id);
3637
+ if (p?.timer != null) clearTimeout(p.timer);
3638
+ this.pendingImprints.delete(id);
3639
+ this.discardTrace(traceId, id);
3640
+ }
3641
+ discardTrace(traceId, id) {
3642
+ const bucket = this.byTrace.get(traceId);
3643
+ if (bucket === void 0) return;
3644
+ bucket.delete(id);
3645
+ if (bucket.size === 0) this.byTrace.delete(traceId);
2590
3646
  }
2591
- return raw;
3647
+ };
3648
+ function hitsFromPayload(raw) {
3649
+ if (!Array.isArray(raw)) return [];
3650
+ const out = [];
3651
+ for (const h of raw) {
3652
+ if (h === null || typeof h !== "object") continue;
3653
+ const obj = h;
3654
+ const entryVal = obj["entry"];
3655
+ out.push({
3656
+ id: typeof obj["id"] === "string" ? obj["id"] : "",
3657
+ entry: entryVal !== null && typeof entryVal === "object" && !Array.isArray(entryVal) ? entryVal : { value: entryVal },
3658
+ score: typeof obj["score"] === "number" ? obj["score"] : 1
3659
+ });
3660
+ }
3661
+ return out;
2592
3662
  }
2593
3663
 
2594
3664
  // src/dendrite.ts
@@ -2599,33 +3669,72 @@ var DendriteProtocolError = class extends Error {
2599
3669
  this.name = "DendriteProtocolError";
2600
3670
  }
2601
3671
  };
2602
- var Dendrite = class {
3672
+ var Dendrite = class _Dendrite {
2603
3673
  synapse;
2604
3674
  registryStore;
2605
3675
  namespace;
2606
3676
  dendriteId;
3677
+ role;
2607
3678
  heartbeatMs;
2608
3679
  reregisterOnHeartbeat;
3680
+ autoBid;
3681
+ staleAfterMs;
2609
3682
  _axons = /* @__PURE__ */ new Map();
2610
3683
  handlers = /* @__PURE__ */ new Map();
2611
3684
  taskSub = null;
3685
+ routedTaskSub = null;
2612
3686
  inboundSubs = /* @__PURE__ */ new Map();
2613
- // Self-scheduling setTimeout handle (not setInterval - see startHeartbeatLoop).
3687
+ inflightSubs = /* @__PURE__ */ new Map();
3688
+ pendingSubs = /* @__PURE__ */ new Set();
3689
+ /** Recently seen CLARIFICATION_ANSWER / PERMISSION_DECISION signals keyed
3690
+ * by parent_id, so {@link awaitDecision} can serve an answer that arrived
3691
+ * before it was called (an in-process synapse can deliver the whole
3692
+ * request->answer chain within the original publish). Bounded FIFO. */
3693
+ recentDecisions = /* @__PURE__ */ new Map();
3694
+ /** Hosted Engrams keyed by engramId, plus a kind index so RECALL/IMPRINT
3695
+ * addressed by engramKind reach every matching host. */
3696
+ _engrams = /* @__PURE__ */ new Map();
3697
+ // In-flight neuron work keyed by trace_id so a STOP can abandon exactly
3698
+ // one workflow. JS can't force-kill a running async body, so abort means
3699
+ // 'stop awaiting + suppress the reply'; the neuron should also check the
3700
+ // AbortSignal cooperatively where it can.
3701
+ traceAborts = /* @__PURE__ */ new Map();
3702
+ engramKindIndex = /* @__PURE__ */ new Map();
3703
+ /** Engrams learned from peer REGISTER signals (possibly out-of-process). */
3704
+ _engramRegistrations = /* @__PURE__ */ new Map();
3705
+ engramRegKindIndex = /* @__PURE__ */ new Map();
3706
+ /** Caller-side correlation table for RECALL/IMPRINT awaiting
3707
+ * RECALLED/IMPRINTED. The Dendrite owns the subscriptions and feeds it. */
3708
+ engramClient = new EngramClient(this);
2614
3709
  heartbeatTimer = null;
2615
- // Set true by stop() so an in-flight tick won't re-arm the loop.
2616
3710
  heartbeatStopped = true;
2617
3711
  running = false;
3712
+ /** Open Pathways keyed by trace_id (dispatch / observePathway). */
3713
+ pathways = /* @__PURE__ */ new Map();
3714
+ /** Per-operation Pathways keyed by the issuing request's id (matched
3715
+ * against inbound parent_id) - the generic request/reply primitive
3716
+ * behind awaitDecision (and a future EngramClient wiring). */
3717
+ opPathways = /* @__PURE__ */ new Map();
2618
3718
  /** @internal - lifecycle hooks for this Dendrite. */
2619
3719
  hooks = new LifecycleHooks(this);
2620
3720
  constructor(opts) {
2621
3721
  if (!opts.synapse) throw new TypeError("Dendrite requires a synapse");
3722
+ const role = opts.role ?? "orchestrator";
3723
+ if (role !== "orchestrator" && role !== "worker") {
3724
+ throw new Error(`role must be 'orchestrator' or 'worker', got '${role}'`);
3725
+ }
2622
3726
  this.synapse = opts.synapse;
2623
3727
  this.registryStore = opts.registryStore ?? null;
2624
3728
  this.namespace = opts.namespace ?? "default";
2625
3729
  this.dendriteId = opts.dendriteId ?? "dendrite";
2626
3730
  this.heartbeatMs = opts.heartbeatMs ?? 3e4;
2627
3731
  this.reregisterOnHeartbeat = opts.reregisterOnHeartbeat ?? true;
2628
- for (const t of AXON_TYPES) this.handlers.set(t, []);
3732
+ this.role = role;
3733
+ this.autoBid = opts.autoBid ?? true;
3734
+ this.staleAfterMs = opts.staleAfterMs ?? (this.heartbeatMs > 0 ? this.heartbeatMs * 3 : 0);
3735
+ for (const t of Object.values(SignalType)) {
3736
+ this.handlers.set(t, []);
3737
+ }
2629
3738
  }
2630
3739
  // -- properties ----------------------------------------------------
2631
3740
  get axons() {
@@ -2634,70 +3743,393 @@ var Dendrite = class {
2634
3743
  axon(neuronId) {
2635
3744
  return this._axons.get(neuronId);
2636
3745
  }
3746
+ /** Aggregate of every attached Axon's capabilities, deduplicated + sorted. */
3747
+ get capabilities() {
3748
+ const caps = /* @__PURE__ */ new Set();
3749
+ for (const ax of this._axons.values()) for (const c of ax.capabilities) caps.add(c);
3750
+ return [...caps].sort();
3751
+ }
3752
+ /** Canonical queue-group name for this Dendrite's aggregate caps, or null
3753
+ * when no Axons are attached. Identical Dendrites share a group. */
3754
+ capQueueGroup() {
3755
+ const caps = this.capabilities;
3756
+ return caps.length ? `caps:${caps.join(",")}` : null;
3757
+ }
3758
+ requireOrchestrator(op) {
3759
+ if (this.role !== "orchestrator") {
3760
+ throw new DendriteProtocolError(
3761
+ `Dendrite role='${this.role}' cannot perform '${op}': only role='orchestrator' Dendrites may dispatch TASK signals. Workers host Axons and emit replies / cognition signals freely.`
3762
+ );
3763
+ }
3764
+ }
2637
3765
  // -- attachment ----------------------------------------------------
3766
+ /**
3767
+ * Attach an Axon to a *stopped* Dendrite. Throws if the Dendrite is
3768
+ * running - a running Dendrite needs the async activation path
3769
+ * (subscriptions, queue-group refresh, REGISTER): use
3770
+ * `await dendrite.addAxon(axon)` instead, which works in both states.
3771
+ */
2638
3772
  attachAxon(axon) {
3773
+ if (this.running) {
3774
+ throw new Error(
3775
+ "attachAxon on a running Dendrite would never receive TASKs (no subscription / REGISTER is set up after start). Use `await dendrite.addAxon(axon)` instead."
3776
+ );
3777
+ }
3778
+ this.attachAxonRecord(axon);
3779
+ }
3780
+ attachAxonRecord(axon) {
2639
3781
  if (this._axons.has(axon.neuronId)) {
2640
3782
  throw new Error(`Dendrite already has an Axon for neuronId='${axon.neuronId}'`);
2641
3783
  }
2642
3784
  this._axons.set(axon.neuronId, axon);
2643
3785
  axon[ATTACH](this);
2644
3786
  }
3787
+ /**
3788
+ * Attach an Axon; if the Dendrite is running, activate it live: ensure the
3789
+ * addressed + routed TASK subscriptions exist (re-keying the routed queue
3790
+ * group for the new aggregate cap profile), subscribe TASK_AWARDED /
3791
+ * DISCOVER (and TASK_OFFER when autoBid), mirror to the registry store,
3792
+ * emit REGISTER, and fire the Axon's onConnect hooks.
3793
+ */
3794
+ async addAxon(axon) {
3795
+ this.attachAxonRecord(axon);
3796
+ if (!this.running) return;
3797
+ if (this.taskSub === null) {
3798
+ this.taskSub = await this.synapse.subscribe(
3799
+ this.subject(SignalType.TASK),
3800
+ (s) => this.onTask(s)
3801
+ );
3802
+ }
3803
+ await this.refreshRoutedSub();
3804
+ await this.ensureInboundSub(SignalType.TASK_AWARDED);
3805
+ await this.ensureInboundSub(SignalType.DISCOVER);
3806
+ if (this.autoBid) await this.ensureInboundSub(SignalType.TASK_OFFER);
3807
+ await this.mirrorToStore(axon, "registered");
3808
+ await this.emitRegister(axon);
3809
+ await axon.hooks._fireConnect();
3810
+ axon.hooks._launchSchedule();
3811
+ }
3812
+ /** Detach an Axon. If running: deregister, tear down its hooks, and re-key
3813
+ * (or drop) the TASK subscriptions for the changed cap profile. */
3814
+ async detachAxon(neuronId, opts = {}) {
3815
+ const axon = this._axons.get(neuronId);
3816
+ if (!axon) {
3817
+ throw new Error(`Dendrite has no Axon for neuronId='${neuronId}'`);
3818
+ }
3819
+ if (this.running) {
3820
+ axon.hooks._stopHooks();
3821
+ if (this.registryStore !== null) {
3822
+ try {
3823
+ await this.registryStore.markDeregistered(neuronId);
3824
+ } catch {
3825
+ }
3826
+ }
3827
+ await this.emitDeregister(axon, opts.reason);
3828
+ }
3829
+ this._axons.delete(neuronId);
3830
+ axon[DETACH]();
3831
+ if (this.running && this._axons.size === 0) {
3832
+ if (this.taskSub !== null) {
3833
+ try {
3834
+ await this.taskSub.unsubscribe();
3835
+ } catch {
3836
+ }
3837
+ this.taskSub = null;
3838
+ }
3839
+ if (this.routedTaskSub !== null) {
3840
+ try {
3841
+ await this.routedTaskSub.unsubscribe();
3842
+ } catch {
3843
+ }
3844
+ this.routedTaskSub = null;
3845
+ }
3846
+ } else if (this.running) {
3847
+ await this.refreshRoutedSub();
3848
+ }
3849
+ }
3850
+ /**
3851
+ * Mount an Engram on this Dendrite. After attachment (and start), the
3852
+ * Dendrite subscribes to RECALL/IMPRINT, routes Signals addressed to
3853
+ * `engram.engramId` or matching `engram.engramKind` to the instance, and
3854
+ * announces it on the Synapse with an engram REGISTER. The Engram still
3855
+ * owns its backend lifecycle: `connect()` on start(), `close()` on stop().
3856
+ * When the Dendrite is already running, the backend is connected and the
3857
+ * subscriptions/REGISTER are established immediately.
3858
+ */
3859
+ async attachEngram(engram) {
3860
+ if (this._engrams.has(engram.engramId)) {
3861
+ throw new Error(`Dendrite already hosts an Engram with engramId='${engram.engramId}'`);
3862
+ }
3863
+ this._engrams.set(engram.engramId, engram);
3864
+ const bucket = this.engramKindIndex.get(engram.engramKind) ?? [];
3865
+ bucket.push(engram.engramId);
3866
+ this.engramKindIndex.set(engram.engramKind, bucket);
3867
+ if (this.running) {
3868
+ await engram.connect();
3869
+ await this.ensureInboundSub(SignalType.RECALL);
3870
+ await this.ensureInboundSub(SignalType.IMPRINT);
3871
+ await this.ensureInboundSub(SignalType.REGISTER);
3872
+ await this.emitEngramRegister(engram);
3873
+ }
3874
+ }
3875
+ /** Remove a hosted Engram. Closes its backend if the Dendrite is running. */
3876
+ async detachEngram(engramId) {
3877
+ const engram = this._engrams.get(engramId);
3878
+ if (!engram) {
3879
+ throw new Error(`Dendrite has no Engram with engramId='${engramId}'`);
3880
+ }
3881
+ if (this.running) {
3882
+ try {
3883
+ await engram.close();
3884
+ } catch {
3885
+ }
3886
+ }
3887
+ const bucket = this.engramKindIndex.get(engram.engramKind) ?? [];
3888
+ const kept = bucket.filter((id) => id !== engramId);
3889
+ if (kept.length) this.engramKindIndex.set(engram.engramKind, kept);
3890
+ else this.engramKindIndex.delete(engram.engramKind);
3891
+ this._engrams.delete(engramId);
3892
+ }
3893
+ get engrams() {
3894
+ return new Map(this._engrams);
3895
+ }
3896
+ /** Engrams learned via REGISTER, keyed by directed.id (or directed.type
3897
+ * when no id), including in-process ones. */
3898
+ get engramRegistrations() {
3899
+ return new Map(this._engramRegistrations);
3900
+ }
3901
+ /** True when an Engram with this id/kind is reachable - hosted
3902
+ * in-process or learned from a peer's REGISTER. */
3903
+ isEngramKnown(opts) {
3904
+ if (opts.engramId) {
3905
+ if (this._engrams.has(opts.engramId) || this._engramRegistrations.has(opts.engramId)) {
3906
+ return true;
3907
+ }
3908
+ }
3909
+ if (opts.engramKind) {
3910
+ if (this.engramKindIndex.has(opts.engramKind) || this.engramRegKindIndex.has(opts.engramKind)) {
3911
+ return true;
3912
+ }
3913
+ }
3914
+ return false;
3915
+ }
3916
+ /** (Re)subscribe the capability-routed TASK subscription so its queue
3917
+ * group matches the *current* aggregate cap profile. */
3918
+ async refreshRoutedSub() {
3919
+ const qgroup = this.capQueueGroup();
3920
+ if (this.routedTaskSub !== null) {
3921
+ try {
3922
+ await this.routedTaskSub.unsubscribe();
3923
+ } catch {
3924
+ }
3925
+ this.routedTaskSub = null;
3926
+ }
3927
+ if (qgroup !== null) {
3928
+ this.routedTaskSub = await this.synapse.subscribe(
3929
+ this.routedSubject(),
3930
+ (s) => this.onTask(s),
3931
+ { queueGroup: qgroup }
3932
+ );
3933
+ }
3934
+ }
2645
3935
  // -- inbound handler registration ----------------------------------
2646
- on(type, fn) {
2647
- const list = this.handlers.get(type);
2648
- if (!list) {
2649
- throw new DendriteProtocolError(`Cannot handle non-Axon type '${type}'`);
3936
+ wrapWithFilter(fn, filter) {
3937
+ if (!filter || filter.neuron === void 0 && filter.capability === void 0 && filter.traceId === void 0) {
3938
+ return fn;
2650
3939
  }
2651
- list.push(fn);
3940
+ return async (sig) => {
3941
+ const sigNeuron = sig.directed?.id ?? null;
3942
+ if (filter.neuron !== void 0 && sigNeuron !== filter.neuron) return;
3943
+ if (filter.traceId !== void 0 && sig.trace_id !== filter.traceId) return;
3944
+ if (filter.capability !== void 0) {
3945
+ if (!await this.neuronHasCapability(sigNeuron, filter.capability)) return;
3946
+ }
3947
+ await fn(sig);
3948
+ };
3949
+ }
3950
+ async neuronHasCapability(neuronId, capability) {
3951
+ if (!neuronId) return false;
3952
+ const axon = this._axons.get(neuronId);
3953
+ if (axon) return axon.capabilities.includes(capability);
3954
+ if (this.registryStore !== null) {
3955
+ try {
3956
+ const recs = await this.registryStore.list({ includeDeregistered: true });
3957
+ const rec = recs.find((r) => r.neuron_id === neuronId);
3958
+ return rec ? rec.capabilities.includes(capability) : false;
3959
+ } catch {
3960
+ return false;
3961
+ }
3962
+ }
3963
+ return false;
3964
+ }
3965
+ on(type, fn, filter) {
3966
+ const list = this.handlers.get(type);
3967
+ list.push(this.wrapWithFilter(fn, filter));
2652
3968
  if (this.running && !this.inboundSubs.has(type)) {
2653
- void this.ensureInboundSub(type);
3969
+ const p = this.ensureInboundSub(type).finally(() => this.pendingSubs.delete(p));
3970
+ this.pendingSubs.add(p);
3971
+ p.catch(() => {
3972
+ });
2654
3973
  }
2655
3974
  return fn;
2656
3975
  }
2657
- onAgentOutput(fn) {
2658
- return this.on(SignalType.AGENT_OUTPUT, fn);
3976
+ /**
3977
+ * Generic handler registration for *any* SignalType - the escape hatch
3978
+ * behind every named `on*` helper. New protocol types are observable the
3979
+ * day they exist. Supports the same filters as the named helpers.
3980
+ */
3981
+ onSignal(type, fn, filter) {
3982
+ return this.on(type, fn, filter);
3983
+ }
3984
+ /** Await until inbound subscriptions exist for `types` - removes the
3985
+ * late-registration race deterministically. Idempotent. */
3986
+ async ensureSubscribed(...types) {
3987
+ for (const t of types) await this.ensureInboundSub(t);
3988
+ }
3989
+ // -- lifecycle / reply handlers --
3990
+ onAgentOutput(fn, filter) {
3991
+ return this.on(SignalType.AGENT_OUTPUT, fn, filter);
2659
3992
  }
2660
- onClarification(fn) {
2661
- return this.on(SignalType.CLARIFICATION, fn);
3993
+ onClarification(fn, filter) {
3994
+ return this.on(SignalType.CLARIFICATION, fn, filter);
2662
3995
  }
2663
3996
  /**
2664
3997
  * Register a handler fired on inbound PERMISSION requests - the *answering*
2665
- * side. A central Cortex or a peer Dendrite evaluates the request (often
2666
- * consulting an Engram of standing grants, keyed per-neuron) and replies via
2667
- * {@link respondToPermission} (re-dispatch a TASK with the verdict) or
2668
- * {@link grantPermission} / {@link denyPermission} (emit a discrete
2669
- * PERMISSION_DECISION). It may also imprint the decision into an Engram so
2670
- * future recalls hit.
3998
+ * side. Reply via {@link respondToPermission} (re-dispatch a TASK with the
3999
+ * verdict) or {@link grantPermission} / {@link denyPermission} (emit a
4000
+ * discrete PERMISSION_DECISION).
2671
4001
  */
2672
- onPermission(fn) {
2673
- return this.on(SignalType.PERMISSION, fn);
4002
+ onPermission(fn, filter) {
4003
+ return this.on(SignalType.PERMISSION, fn, filter);
2674
4004
  }
2675
- onErrorSignal(fn) {
2676
- return this.on(SignalType.ERROR, fn);
4005
+ onErrorSignal(fn, filter) {
4006
+ return this.on(SignalType.ERROR, fn, filter);
2677
4007
  }
2678
- onRegister(fn) {
2679
- return this.on(SignalType.REGISTER, fn);
4008
+ /** Register a handler fired on FINAL - workflow conclusion. */
4009
+ onFinal(fn, filter) {
4010
+ return this.on(SignalType.FINAL, fn, filter);
2680
4011
  }
2681
- onDeregister(fn) {
2682
- return this.on(SignalType.DEREGISTER, fn);
2683
- }
2684
- onHeartbeat(fn) {
2685
- return this.on(SignalType.HEARTBEAT, fn);
4012
+ /** Observe inbound TASKs (audit/logging). Observation only - Axon routing
4013
+ * happens on its own subscription and is unaffected. */
4014
+ onTaskSignal(fn, filter) {
4015
+ return this.on(SignalType.TASK, fn, filter);
4016
+ }
4017
+ onRegister(fn, filter) {
4018
+ return this.on(SignalType.REGISTER, fn, filter);
4019
+ }
4020
+ onDeregister(fn, filter) {
4021
+ return this.on(SignalType.DEREGISTER, fn, filter);
4022
+ }
4023
+ onHeartbeat(fn, filter) {
4024
+ return this.on(SignalType.HEARTBEAT, fn, filter);
4025
+ }
4026
+ onDiscover(fn, filter) {
4027
+ return this.on(SignalType.DISCOVER, fn, filter);
4028
+ }
4029
+ // -- cognition handlers --
4030
+ onPlan(fn, filter) {
4031
+ return this.on(SignalType.PLAN, fn, filter);
4032
+ }
4033
+ onThoughtDelta(fn, filter) {
4034
+ return this.on(SignalType.THOUGHT_DELTA, fn, filter);
4035
+ }
4036
+ onToolCall(fn, filter) {
4037
+ return this.on(SignalType.TOOL_CALL, fn, filter);
4038
+ }
4039
+ onToolResult(fn, filter) {
4040
+ return this.on(SignalType.TOOL_RESULT, fn, filter);
4041
+ }
4042
+ onMemoryAppend(fn, filter) {
4043
+ return this.on(SignalType.MEMORY_APPEND, fn, filter);
4044
+ }
4045
+ onCritique(fn, filter) {
4046
+ return this.on(SignalType.CRITIQUE, fn, filter);
4047
+ }
4048
+ onEscalation(fn, filter) {
4049
+ return this.on(SignalType.ESCALATION, fn, filter);
4050
+ }
4051
+ onConsensus(fn, filter) {
4052
+ return this.on(SignalType.CONSENSUS, fn, filter);
4053
+ }
4054
+ onContextSync(fn, filter) {
4055
+ return this.on(SignalType.CONTEXT_SYNC, fn, filter);
4056
+ }
4057
+ // -- routing / market handlers --
4058
+ /** Workers use this to evaluate offers and call {@link bid} to compete.
4059
+ * Registering it suppresses the default auto-bidder entirely. */
4060
+ onTaskOffer(fn, filter) {
4061
+ return this.on(SignalType.TASK_OFFER, fn, filter);
4062
+ }
4063
+ /** Observe BIDs (market observability). dispatchOffer collects its own. */
4064
+ onBid(fn, filter) {
4065
+ return this.on(SignalType.BID, fn, filter);
4066
+ }
4067
+ /** Observe TASK_AWARDED. The hosting Dendrite's award-to-TASK synthesis is
4068
+ * unaffected by handlers here. */
4069
+ onTaskAwarded(fn, filter) {
4070
+ return this.on(SignalType.TASK_AWARDED, fn, filter);
4071
+ }
4072
+ /** e.g. release a reservation made while bidding. */
4073
+ onTaskDeclined(fn, filter) {
4074
+ return this.on(SignalType.TASK_DECLINED, fn, filter);
4075
+ }
4076
+ // -- discrete answer-path consumers --
4077
+ /** Fired on CLARIFICATION_ANSWER - correlate by `sig.parent_id === the
4078
+ * CLARIFICATION's id`, or use {@link awaitDecision}. */
4079
+ onClarificationAnswer(fn, filter) {
4080
+ return this.on(SignalType.CLARIFICATION_ANSWER, fn, filter);
4081
+ }
4082
+ /** Fired on PERMISSION_DECISION - correlate by parent_id, or use
4083
+ * {@link awaitDecision}. */
4084
+ onPermissionDecision(fn, filter) {
4085
+ return this.on(SignalType.PERMISSION_DECISION, fn, filter);
4086
+ }
4087
+ // -- memory-traffic observers --
4088
+ onRecalled(fn, filter) {
4089
+ return this.on(SignalType.RECALLED, fn, filter);
4090
+ }
4091
+ onImprinted(fn, filter) {
4092
+ return this.on(SignalType.IMPRINTED, fn, filter);
4093
+ }
4094
+ onRecallSignal(fn, filter) {
4095
+ return this.on(SignalType.RECALL, fn, filter);
4096
+ }
4097
+ onImprintSignal(fn, filter) {
4098
+ return this.on(SignalType.IMPRINT, fn, filter);
4099
+ }
4100
+ // -- trace-scoped helper --
4101
+ static TRACE_DEFAULT_TYPES = [
4102
+ SignalType.AGENT_OUTPUT,
4103
+ SignalType.FINAL,
4104
+ SignalType.ERROR,
4105
+ SignalType.PLAN,
4106
+ SignalType.THOUGHT_DELTA,
4107
+ SignalType.TOOL_CALL,
4108
+ SignalType.TOOL_RESULT,
4109
+ SignalType.MEMORY_APPEND,
4110
+ SignalType.CRITIQUE,
4111
+ SignalType.ESCALATION,
4112
+ SignalType.CONSENSUS,
4113
+ SignalType.CONTEXT_SYNC,
4114
+ SignalType.CLARIFICATION
4115
+ ];
4116
+ /** Register one handler for multiple types narrowed to a single workflow. */
4117
+ onTrace(traceId, fn, types) {
4118
+ for (const t of types ?? _Dendrite.TRACE_DEFAULT_TYPES) {
4119
+ this.on(t, fn, { traceId });
4120
+ }
4121
+ return fn;
2686
4122
  }
2687
4123
  // -- lifecycle hooks ----------------------------------------------
2688
- /** Register a fire-once handler called after start() completes. */
2689
4124
  onConnect(fn) {
2690
4125
  return this.hooks.onConnect(fn);
2691
4126
  }
2692
- /** Register a handler called whenever this Dendrite's state refreshes. */
2693
4127
  onRefresh(fn) {
2694
4128
  return this.hooks.onRefresh(fn);
2695
4129
  }
2696
- /** Register a periodic handler that runs every `everyMs` until stop(). */
2697
4130
  onSchedule(everyMs, fn) {
2698
4131
  return this.hooks.onSchedule(everyMs, fn);
2699
4132
  }
2700
- /** Manually fire a refresh event (reason defaults to "manual"). */
2701
4133
  async refresh(opts = {}) {
2702
4134
  await this.hooks.refresh(opts);
2703
4135
  }
@@ -2710,11 +4142,44 @@ var Dendrite = class {
2710
4142
  this.subject(SignalType.TASK),
2711
4143
  (s) => this.onTask(s)
2712
4144
  );
4145
+ const qgroup = this.capQueueGroup();
4146
+ if (qgroup !== null) {
4147
+ this.routedTaskSub = await this.synapse.subscribe(
4148
+ this.routedSubject(),
4149
+ (s) => this.onTask(s),
4150
+ { queueGroup: qgroup }
4151
+ );
4152
+ }
4153
+ await this.ensureInboundSub(SignalType.TASK_AWARDED);
4154
+ await this.ensureInboundSub(SignalType.DISCOVER);
4155
+ await this.ensureInboundSub(SignalType.STOP);
4156
+ if (this.autoBid) await this.ensureInboundSub(SignalType.TASK_OFFER);
2713
4157
  for (const axon of this._axons.values()) {
2714
4158
  await this.mirrorToStore(axon, "registered");
2715
4159
  await this.emitRegister(axon);
2716
4160
  }
2717
4161
  }
4162
+ if (this._engrams.size > 0) {
4163
+ for (const engram of this._engrams.values()) {
4164
+ try {
4165
+ await engram.connect();
4166
+ } catch {
4167
+ }
4168
+ }
4169
+ await this.ensureInboundSub(SignalType.RECALL);
4170
+ await this.ensureInboundSub(SignalType.IMPRINT);
4171
+ await this.ensureInboundSub(SignalType.FINAL);
4172
+ await this.ensureInboundSub(SignalType.ERROR);
4173
+ await this.ensureInboundSub(SignalType.REGISTER);
4174
+ for (const engram of this._engrams.values()) {
4175
+ try {
4176
+ await this.emitEngramRegister(engram);
4177
+ } catch {
4178
+ }
4179
+ }
4180
+ }
4181
+ await this.ensureInboundSub(SignalType.RECALLED);
4182
+ await this.ensureInboundSub(SignalType.IMPRINTED);
2718
4183
  for (const [type, hs] of this.handlers) {
2719
4184
  if (hs.length) await this.ensureInboundSub(type);
2720
4185
  }
@@ -2723,6 +4188,7 @@ var Dendrite = class {
2723
4188
  await this.ensureInboundSub(t);
2724
4189
  }
2725
4190
  }
4191
+ await this.ensureInboundSub(SignalType.STOP);
2726
4192
  this.running = true;
2727
4193
  if (this._axons.size > 0 && this.heartbeatMs > 0) {
2728
4194
  this.startHeartbeatLoop();
@@ -2734,17 +4200,6 @@ var Dendrite = class {
2734
4200
  axon.hooks._launchSchedule();
2735
4201
  }
2736
4202
  }
2737
- /**
2738
- * Heartbeat as a self-scheduling async loop rather than `setInterval`.
2739
- *
2740
- * Why not setInterval: it fires on a fixed wall-clock cadence regardless of
2741
- * whether the previous tick finished, so under load ticks overlap and the
2742
- * effective interval drifts; and because the callback is sync, any rejection
2743
- * from the async work inside is an unhandled rejection that setInterval
2744
- * silently drops. Here each tick is fully awaited, its errors are caught, and
2745
- * only then is the next tick scheduled - matching the Python SDK's
2746
- * asyncio.Task semantics (structured error handling + clean cancellation).
2747
- */
2748
4203
  startHeartbeatLoop() {
2749
4204
  this.heartbeatStopped = false;
2750
4205
  const schedule = () => {
@@ -2764,8 +4219,29 @@ var Dendrite = class {
2764
4219
  schedule();
2765
4220
  }
2766
4221
  async stop(reason) {
4222
+ for (const pw of [...this.pathways.values()]) {
4223
+ try {
4224
+ await pw.close();
4225
+ } catch {
4226
+ }
4227
+ }
4228
+ this.pathways.clear();
4229
+ for (const pw of [...this.opPathways.values()]) {
4230
+ try {
4231
+ await pw.close();
4232
+ } catch {
4233
+ }
4234
+ }
4235
+ this.opPathways.clear();
4236
+ this.engramClient.cancelAll();
2767
4237
  if (!this.running) return;
2768
4238
  this.running = false;
4239
+ for (const engram of this._engrams.values()) {
4240
+ try {
4241
+ await engram.close();
4242
+ } catch {
4243
+ }
4244
+ }
2769
4245
  this.hooks._stopHooks();
2770
4246
  for (const axon of this._axons.values()) axon.hooks._stopHooks();
2771
4247
  this.heartbeatStopped = true;
@@ -2777,6 +4253,10 @@ var Dendrite = class {
2777
4253
  await this.taskSub.unsubscribe();
2778
4254
  this.taskSub = null;
2779
4255
  }
4256
+ if (this.routedTaskSub !== null) {
4257
+ await this.routedTaskSub.unsubscribe();
4258
+ this.routedTaskSub = null;
4259
+ }
2780
4260
  for (const sub of this.inboundSubs.values()) {
2781
4261
  try {
2782
4262
  await sub.unsubscribe();
@@ -2794,20 +4274,6 @@ var Dendrite = class {
2794
4274
  await this.emitDeregister(axon, reason);
2795
4275
  }
2796
4276
  }
2797
- /**
2798
- * Explicit-resource-management hook so a Dendrite can be used with
2799
- * `await using` - the TS equivalent of Python's `async with dendrite:`.
2800
- *
2801
- * ```ts
2802
- * await using dendrite = new Dendrite({ synapse });
2803
- * dendrite.attachAxon(axon);
2804
- * await dendrite.start();
2805
- * // ... stop() runs automatically when this scope exits, even on throw.
2806
- * ```
2807
- *
2808
- * Idempotent: stop() is a no-op if the Dendrite was never started or already
2809
- * stopped. As with stop(), the caller still owns the Synapse/registry store.
2810
- */
2811
4277
  async [Symbol.asyncDispose]() {
2812
4278
  await this.stop();
2813
4279
  }
@@ -2822,29 +4288,390 @@ var Dendrite = class {
2822
4288
  }
2823
4289
  /** All known records, optionally filtered (live records only by default). */
2824
4290
  async registrySnapshot(opts = {}) {
2825
- return this.requireStore().list(opts);
4291
+ const { maxAgeMs, ...listOpts } = opts;
4292
+ const records = await this.requireStore().list(listOpts);
4293
+ return maxAgeMs !== void 0 ? _Dendrite.filterFresh(records, maxAgeMs) : records;
2826
4294
  }
2827
- /** Live (non-deregistered) records, optionally filtered by capability. */
4295
+ /** Live (non-deregistered) records, optionally filtered by capability.
4296
+ * `maxAgeMs` additionally drops records whose last heartbeat is older -
4297
+ * a read-side freshness guard when the background sweep can't be relied on. */
2828
4298
  async findNeurons(opts = {}) {
2829
- return this.requireStore().list({
4299
+ const records = await this.requireStore().list({
2830
4300
  ...opts.capability !== void 0 ? { capability: opts.capability } : {},
2831
4301
  includeDeregistered: false
2832
4302
  });
4303
+ return opts.maxAgeMs !== void 0 ? _Dendrite.filterFresh(records, opts.maxAgeMs) : records;
4304
+ }
4305
+ static filterFresh(records, maxAgeMs) {
4306
+ const now = Date.now();
4307
+ return records.filter((r) => {
4308
+ const seen = r.last_heartbeat ?? r.registered_at;
4309
+ if (!seen) return false;
4310
+ return now - Date.parse(seen) <= maxAgeMs;
4311
+ });
2833
4312
  }
2834
4313
  // -- outbound primitives ------------------------------------------
4314
+ /**
4315
+ * Emit a TASK. Addressed (`neuron`) or capability-routed (`capabilities`)
4316
+ * - at least one must be set. `finalize: true` tags the TASK so the
4317
+ * handling worker Dendrite promotes a successful AGENT_OUTPUT to FINAL
4318
+ * (terminal-handler finalize - see {@link dispatch}). Only
4319
+ * orchestrator-role Dendrites may dispatch.
4320
+ */
2835
4321
  async dispatchTask(args) {
4322
+ this.requireOrchestrator("dispatchTask");
4323
+ if (!args.neuron && !args.capabilities?.length) {
4324
+ throw new Error(
4325
+ "dispatchTask requires either neuron (addressed) or capabilities (capability-routed)"
4326
+ );
4327
+ }
2836
4328
  const sig = taskSignal({
2837
- directed: { id: args.neuron },
2838
4329
  input: args.input,
4330
+ ...args.neuron ? { directed: { id: args.neuron } } : {},
2839
4331
  ...args.traceId !== void 0 ? { traceId: args.traceId } : {},
2840
4332
  ...args.parentId !== void 0 ? { parentId: args.parentId } : {},
2841
4333
  ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
2842
4334
  ...args.capabilities !== void 0 ? { capabilities: args.capabilities } : {},
4335
+ ...args.finalize !== void 0 ? { finalize: args.finalize } : {},
4336
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4337
+ });
4338
+ await this.publishTask(sig);
4339
+ return sig;
4340
+ }
4341
+ /** Publish a TASK to the right subject for its routing mode. Addressed ->
4342
+ * broadcast subject; capability-routed -> queue-grouped routed subject. */
4343
+ async publishTask(sig) {
4344
+ const subject = sig.directed?.id ? this.subject(SignalType.TASK) : sig.payload["capabilities"]?.length ? this.routedSubject() : this.subject(SignalType.TASK);
4345
+ await this.synapse.publish(subject, sig);
4346
+ }
4347
+ // -- Pathway-based dispatch ----------------------------------------
4348
+ /**
4349
+ * Dispatch a TASK and return a {@link Pathway} scoped to its trace -
4350
+ * await it, attach callbacks, or iterate:
4351
+ *
4352
+ * ```ts
4353
+ * // 1) sequential / request-reply
4354
+ * const pw = await orch.dispatch({ neuron: "summarize", input });
4355
+ * const out = await pw.wait();
4356
+ *
4357
+ * // 2) reactive
4358
+ * pw.on(SignalType.PLAN, (sig) => { ... });
4359
+ *
4360
+ * // 3) streaming
4361
+ * for await (const sig of pw) { ... }
4362
+ * ```
4363
+ *
4364
+ * `capabilities` instead of `neuron` gives event-driven dispatch. Delivery
4365
+ * is exactly-once within a queue group (identical cap profiles) but
4366
+ * **at-least-once across heterogeneous groups** - use
4367
+ * {@link dispatchOffer} when overlapping profiles need an atomic claim.
4368
+ *
4369
+ * `scope: "terminal"` filters delivery to FINAL / ERROR / CLARIFICATION /
4370
+ * PERMISSION. `finalize` (default: true exactly when scope is "terminal")
4371
+ * tags the TASK for terminal-handler finalize: the worker Dendrite promotes
4372
+ * a successful AGENT_OUTPUT by also emitting FINAL - a default Axon never
4373
+ * emits FINAL itself, so a terminal-scoped Pathway would otherwise never
4374
+ * resolve against stock workers.
4375
+ */
4376
+ async dispatch(args) {
4377
+ this.requireOrchestrator("dispatch");
4378
+ if (!args.neuron && !args.capabilities?.length) {
4379
+ throw new Error(
4380
+ "dispatch requires either neuron (addressed) or capabilities (capability-routed)"
4381
+ );
4382
+ }
4383
+ const scope = args.scope ?? "all";
4384
+ const finalize = args.finalize ?? scope === "terminal";
4385
+ const tid = args.traceId ?? newTraceId();
4386
+ await this.ensurePathwaySubs();
4387
+ const pathway = new Pathway({
4388
+ traceId: tid,
4389
+ role: "originator",
4390
+ scope,
4391
+ onClose: (pw) => {
4392
+ this.pathways.delete(pw.traceId);
4393
+ }
4394
+ });
4395
+ this.pathways.set(tid, pathway);
4396
+ const sig = taskSignal({
4397
+ input: args.input,
4398
+ traceId: tid,
4399
+ ...args.neuron ? { directed: { id: args.neuron } } : {},
4400
+ ...args.parentId !== void 0 ? { parentId: args.parentId } : {},
4401
+ ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
4402
+ ...args.capabilities !== void 0 ? { capabilities: args.capabilities } : {},
4403
+ finalize,
4404
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4405
+ });
4406
+ try {
4407
+ await this.publishTask(sig);
4408
+ } catch (err) {
4409
+ this.pathways.delete(tid);
4410
+ await pathway.close();
4411
+ throw err;
4412
+ }
4413
+ return pathway;
4414
+ }
4415
+ /** Sync-shape sugar: dispatch, await the first matching Signal, close the
4416
+ * Pathway, return the Signal. Use `scope: "terminal"` to wait only for
4417
+ * FINAL / ERROR / CLARIFICATION / PERMISSION. */
4418
+ async dispatchAndWait(args) {
4419
+ const { timeoutMs, retry, ...rest } = args;
4420
+ if (retry) {
4421
+ return this.runWithRetry({ ...rest, retry, ...timeoutMs !== void 0 ? { timeoutMs } : {} });
4422
+ }
4423
+ const pathway = await this.dispatch(rest);
4424
+ try {
4425
+ return await pathway.wait(timeoutMs ?? 3e4);
4426
+ } finally {
4427
+ await pathway.close();
4428
+ }
4429
+ }
4430
+ /** Async-shape sugar: dispatch, return the live Pathway immediately. The
4431
+ * caller attaches `pw.on(...)` callbacks or iterates. */
4432
+ async dispatchAndSubscribe(args) {
4433
+ return this.dispatch(args);
4434
+ }
4435
+ /** Open a Pathway in *observer* role for a trace another peer started. */
4436
+ async observePathway(traceId) {
4437
+ if (this.pathways.has(traceId)) {
4438
+ throw new Error(`Dendrite already has a Pathway open for trace '${traceId}'`);
4439
+ }
4440
+ await this.ensurePathwaySubs();
4441
+ const pathway = new Pathway({
4442
+ traceId,
4443
+ role: "observer",
4444
+ onClose: (pw) => {
4445
+ this.pathways.delete(pw.traceId);
4446
+ }
4447
+ });
4448
+ this.pathways.set(traceId, pathway);
4449
+ return pathway;
4450
+ }
4451
+ async ensurePathwaySubs() {
4452
+ for (const t of PATHWAY_TYPES) await this.ensureInboundSub(t);
4453
+ }
4454
+ // -- per-operation (request/reply) Pathways -------------------------
4455
+ openOpPathway(opId, traceId) {
4456
+ const pathway = new Pathway({
4457
+ traceId,
4458
+ parentId: opId,
4459
+ role: "originator",
4460
+ onClose: (pw) => {
4461
+ if (pw.parentId !== null) this.opPathways.delete(pw.parentId);
4462
+ }
4463
+ });
4464
+ this.opPathways.set(opId, pathway);
4465
+ return pathway;
4466
+ }
4467
+ async cancelOpPathways(traceId) {
4468
+ for (const pw of [...this.opPathways.values()].filter((p) => p.traceId === traceId)) {
4469
+ try {
4470
+ await pw.close();
4471
+ } catch {
4472
+ }
4473
+ }
4474
+ }
4475
+ /**
4476
+ * Await the discrete answer to a CLARIFICATION or PERMISSION request.
4477
+ *
4478
+ * Opens a per-operation Pathway keyed on `request.id` and resolves on the
4479
+ * first CLARIFICATION_ANSWER / PERMISSION_DECISION whose parent_id matches.
4480
+ * The awaitable counterpart to {@link onClarificationAnswer} /
4481
+ * {@link onPermissionDecision}.
4482
+ */
4483
+ async awaitDecision(request, opts = {}) {
4484
+ let expected;
4485
+ if (request.type === SignalType.CLARIFICATION) {
4486
+ expected = SignalType.CLARIFICATION_ANSWER;
4487
+ } else if (request.type === SignalType.PERMISSION) {
4488
+ expected = SignalType.PERMISSION_DECISION;
4489
+ } else {
4490
+ throw new DendriteProtocolError(
4491
+ `awaitDecision expects a CLARIFICATION or PERMISSION signal, got '${request.type}'`
4492
+ );
4493
+ }
4494
+ await this.ensureInboundSub(expected);
4495
+ const cached = this.recentDecisions.get(request.id);
4496
+ if (cached && cached.type === expected) {
4497
+ this.recentDecisions.delete(request.id);
4498
+ return cached;
4499
+ }
4500
+ const pathway = this.openOpPathway(request.id, request.trace_id);
4501
+ try {
4502
+ return await pathway.waitFor(expected, opts.timeoutMs ?? 3e4);
4503
+ } finally {
4504
+ await pathway.close();
4505
+ }
4506
+ }
4507
+ // -- competitive bidding: TASK_OFFER / BID / TASK_AWARDED ------------
4508
+ /**
4509
+ * Broadcast a TASK_OFFER, collect BIDs for `deadlineMs`, award the winner
4510
+ * per `select` ("first_bid" | "lowest_cost" | "highest_confidence"), and
4511
+ * return a Pathway scoped to the resulting workflow. Losers get
4512
+ * TASK_DECLINED. Throws a TimeoutError-named Error when no BID arrives.
4513
+ * `finalize` follows the same rule as {@link dispatch}.
4514
+ */
4515
+ async dispatchOffer(args) {
4516
+ this.requireOrchestrator("dispatchOffer");
4517
+ const select = args.select ?? "first_bid";
4518
+ if (!["first_bid", "lowest_cost", "highest_confidence"].includes(select)) {
4519
+ throw new Error(
4520
+ `select must be 'first_bid' / 'lowest_cost' / 'highest_confidence', got '${select}'`
4521
+ );
4522
+ }
4523
+ const deadlineMs = args.deadlineMs ?? 250;
4524
+ const scope = args.scope ?? "all";
4525
+ const finalize = args.finalize ?? scope === "terminal";
4526
+ const tid = args.traceId ?? newTraceId();
4527
+ await this.ensurePathwaySubs();
4528
+ await this.ensureInboundSub(SignalType.BID);
4529
+ const pathway = new Pathway({
4530
+ traceId: tid,
4531
+ role: "originator",
4532
+ scope,
4533
+ onClose: (pw) => {
4534
+ this.pathways.delete(pw.traceId);
4535
+ }
4536
+ });
4537
+ this.pathways.set(tid, pathway);
4538
+ const offer = taskOfferSignal({
4539
+ traceId: tid,
4540
+ input: args.input,
4541
+ deadlineMs,
4542
+ ...args.parentId !== void 0 ? { parentId: args.parentId } : {},
4543
+ ...args.capabilities !== void 0 ? { capabilities: args.capabilities } : {},
4544
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4545
+ });
4546
+ const bids = [];
4547
+ let firstBid = null;
4548
+ const firstBidArrived = new Promise((resolve) => {
4549
+ firstBid = resolve;
4550
+ });
4551
+ pathway.on(SignalType.BID, (sig) => {
4552
+ bids.push(sig);
4553
+ if (select === "first_bid") firstBid?.(sig);
4554
+ });
4555
+ try {
4556
+ await this.emit(offer);
4557
+ } catch (err) {
4558
+ this.pathways.delete(tid);
4559
+ await pathway.close();
4560
+ throw err;
4561
+ }
4562
+ const sleep = (ms) => new Promise((r) => setTimeout(() => r(null), ms));
4563
+ if (select === "first_bid") {
4564
+ const winnerOrNull = await Promise.race([firstBidArrived, sleep(deadlineMs)]);
4565
+ if (winnerOrNull === null && bids.length === 0) {
4566
+ await pathway.close();
4567
+ const err = new Error(`dispatchOffer: no BID arrived within ${deadlineMs}ms`);
4568
+ err.name = "TimeoutError";
4569
+ throw err;
4570
+ }
4571
+ } else {
4572
+ await sleep(deadlineMs);
4573
+ if (bids.length === 0) {
4574
+ await pathway.close();
4575
+ const err = new Error(`dispatchOffer: no BID arrived within ${deadlineMs}ms`);
4576
+ err.name = "TimeoutError";
4577
+ throw err;
4578
+ }
4579
+ }
4580
+ let winner;
4581
+ if (select === "first_bid") {
4582
+ winner = bids[0];
4583
+ } else if (select === "lowest_cost") {
4584
+ winner = bids.reduce(
4585
+ (a, b) => (b.payload["cost"] ?? Infinity) < (a.payload["cost"] ?? Infinity) ? b : a
4586
+ );
4587
+ } else {
4588
+ winner = bids.reduce(
4589
+ (a, b) => (b.payload["confidence"] ?? -Infinity) > (a.payload["confidence"] ?? -Infinity) ? b : a
4590
+ );
4591
+ }
4592
+ for (const b of bids) {
4593
+ if (b.id === winner.id) continue;
4594
+ const bNeuron = b.directed?.id ?? null;
4595
+ try {
4596
+ await this.emit(
4597
+ taskDeclinedSignal({
4598
+ traceId: tid,
4599
+ parentId: b.id,
4600
+ reason: "not selected",
4601
+ ...bNeuron ? { directed: { id: bNeuron } } : {}
4602
+ })
4603
+ );
4604
+ } catch {
4605
+ }
4606
+ }
4607
+ const winnerNeuron = winner.directed?.id ?? null;
4608
+ const winningBid = {};
4609
+ for (const k of ["cost", "eta_ms", "confidence"]) {
4610
+ if (k in winner.payload) winningBid[k] = winner.payload[k];
4611
+ }
4612
+ const awarded = taskAwardedSignal({
4613
+ traceId: tid,
4614
+ parentId: winner.id,
4615
+ input: args.input,
4616
+ winningBid,
4617
+ finalize,
4618
+ ...winnerNeuron ? { directed: { id: winnerNeuron } } : {},
4619
+ ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {}
4620
+ });
4621
+ try {
4622
+ await this.emit(awarded);
4623
+ } catch (err) {
4624
+ await pathway.close();
4625
+ throw err;
4626
+ }
4627
+ return pathway;
4628
+ }
4629
+ /**
4630
+ * Emit a BID in response to a TASK_OFFER, on behalf of the local Axon named
4631
+ * by `neuron`. Bypasses the role guard - a worker bidding announces
4632
+ * capability, not orchestration.
4633
+ */
4634
+ async bid(offer, args) {
4635
+ if (offer.type !== SignalType.TASK_OFFER) {
4636
+ throw new DendriteProtocolError(
4637
+ `bid() expects a TASK_OFFER signal, got '${offer.type}'`
4638
+ );
4639
+ }
4640
+ const sig = bidSignal({
4641
+ traceId: offer.trace_id,
4642
+ parentId: offer.id,
4643
+ directed: { id: args.neuron },
4644
+ cost: args.cost,
4645
+ ...args.etaMs !== void 0 ? { etaMs: args.etaMs } : {},
4646
+ ...args.confidence !== void 0 ? { confidence: args.confidence } : {},
2843
4647
  ...args.meta !== void 0 ? { meta: args.meta } : {}
2844
4648
  });
2845
- await this.emit(sig);
4649
+ await this.publish(sig);
2846
4650
  return sig;
2847
4651
  }
4652
+ /** Default bidder: first hosted Axon whose caps cover the offer answers
4653
+ * (cost 0, confidence 1). No-op when nothing matches. */
4654
+ async maybeAutoBid(offer) {
4655
+ const requested = new Set(
4656
+ offer.payload["capabilities"] ?? []
4657
+ );
4658
+ for (const axon of this._axons.values()) {
4659
+ if (requested.size && ![...requested].every((c) => axon.capabilities.includes(c))) {
4660
+ continue;
4661
+ }
4662
+ try {
4663
+ await this.bid(offer, {
4664
+ neuron: axon.neuronId,
4665
+ cost: 0,
4666
+ confidence: 1,
4667
+ meta: { auto_bid: true }
4668
+ });
4669
+ } catch {
4670
+ }
4671
+ return;
4672
+ }
4673
+ }
4674
+ // -- reply / cognition emit helpers ---------------------------------
2848
4675
  async emitFinal(args) {
2849
4676
  const sig = finalSignal({
2850
4677
  traceId: args.traceId,
@@ -2869,16 +4696,187 @@ var Dendrite = class {
2869
4696
  await this.emit(sig);
2870
4697
  return sig;
2871
4698
  }
2872
- /** Emit a synapse-side Signal. Refuses Axon-owned types. */
4699
+ async emitPlan(args) {
4700
+ const sig = planSignal({
4701
+ traceId: args.traceId,
4702
+ parentId: args.parentId,
4703
+ steps: args.steps,
4704
+ directed: { id: args.neuron ?? this.dendriteId },
4705
+ ...args.rationale !== void 0 ? { rationale: args.rationale } : {},
4706
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4707
+ });
4708
+ await this.emit(sig);
4709
+ return sig;
4710
+ }
4711
+ async emitThoughtDelta(args) {
4712
+ const sig = thoughtDeltaSignal({
4713
+ traceId: args.traceId,
4714
+ parentId: args.parentId,
4715
+ delta: args.delta,
4716
+ directed: { id: args.neuron ?? this.dendriteId },
4717
+ ...args.seq !== void 0 ? { seq: args.seq } : {},
4718
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4719
+ });
4720
+ await this.emit(sig);
4721
+ return sig;
4722
+ }
4723
+ async emitToolCall(args) {
4724
+ const sig = toolCallSignal({
4725
+ traceId: args.traceId,
4726
+ parentId: args.parentId,
4727
+ tool: args.tool,
4728
+ args: args.args_,
4729
+ directed: { id: args.neuron ?? this.dendriteId },
4730
+ ...args.callId !== void 0 ? { callId: args.callId } : {},
4731
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4732
+ });
4733
+ await this.emit(sig);
4734
+ return sig;
4735
+ }
4736
+ async emitToolResult(args) {
4737
+ const sig = toolResultSignal({
4738
+ traceId: args.traceId,
4739
+ parentId: args.parentId,
4740
+ tool: args.tool,
4741
+ directed: { id: args.neuron ?? this.dendriteId },
4742
+ ...args.result !== void 0 ? { result: args.result } : {},
4743
+ ...args.error !== void 0 ? { error: args.error } : {},
4744
+ ...args.callId !== void 0 ? { callId: args.callId } : {},
4745
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4746
+ });
4747
+ await this.emit(sig);
4748
+ return sig;
4749
+ }
4750
+ async emitMemoryAppend(args) {
4751
+ const sig = memoryAppendSignal({
4752
+ traceId: args.traceId,
4753
+ parentId: args.parentId,
4754
+ key: args.key,
4755
+ value: args.value,
4756
+ directed: { id: args.neuron ?? this.dendriteId },
4757
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4758
+ });
4759
+ await this.emit(sig);
4760
+ return sig;
4761
+ }
4762
+ async emitCritique(args) {
4763
+ const sig = critiqueSignal({
4764
+ traceId: args.traceId,
4765
+ parentId: args.parentId,
4766
+ targetEventId: args.targetEventId,
4767
+ issues: args.issues,
4768
+ verdict: args.verdict,
4769
+ directed: { id: args.neuron ?? this.dendriteId },
4770
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4771
+ });
4772
+ await this.emit(sig);
4773
+ return sig;
4774
+ }
4775
+ async emitEscalation(args) {
4776
+ const sig = escalationSignal({
4777
+ traceId: args.traceId,
4778
+ parentId: args.parentId,
4779
+ reason: args.reason,
4780
+ directed: { id: args.neuron ?? this.dendriteId },
4781
+ ...args.target !== void 0 ? { target: args.target } : {},
4782
+ ...args.context !== void 0 ? { context: args.context } : {},
4783
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4784
+ });
4785
+ await this.emit(sig);
4786
+ return sig;
4787
+ }
4788
+ async emitConsensus(args) {
4789
+ const sig = consensusSignal({
4790
+ traceId: args.traceId,
4791
+ parentId: args.parentId,
4792
+ members: args.members,
4793
+ verdict: args.verdict,
4794
+ directed: { id: args.neuron ?? this.dendriteId },
4795
+ ...args.votes !== void 0 ? { votes: args.votes } : {},
4796
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4797
+ });
4798
+ await this.emit(sig);
4799
+ return sig;
4800
+ }
4801
+ async emitContextSync(args) {
4802
+ const sig = contextSyncSignal({
4803
+ traceId: args.traceId,
4804
+ parentId: args.parentId,
4805
+ snapshot: args.snapshot,
4806
+ directed: { id: args.neuron ?? this.dendriteId },
4807
+ ...args.version !== void 0 ? { version: args.version } : {},
4808
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
4809
+ });
4810
+ await this.emit(sig);
4811
+ return sig;
4812
+ }
4813
+ // -- close-the-loop helpers ------------------------------------------
4814
+ /**
4815
+ * Reply to a CLARIFICATION by re-dispatching a TASK with the answer. The
4816
+ * new TASK is addressed by default to the asking Neuron, with parentId =
4817
+ * the clarification's id and the original traceId carried over. Input
4818
+ * shape: `{ clarification: { question, answer, ...extra } }`.
4819
+ */
4820
+ async respondToClarification(request, opts) {
4821
+ if (request.type !== SignalType.CLARIFICATION) {
4822
+ throw new DendriteProtocolError(
4823
+ `respondToClarification expects a CLARIFICATION signal, got '${request.type}'`
4824
+ );
4825
+ }
4826
+ const target = opts.neuron ?? request.directed?.id ?? null;
4827
+ if (!target) {
4828
+ throw new DendriteProtocolError(
4829
+ "respondToClarification: signal has no neuron and no neuron override - nowhere to dispatch the follow-up TASK"
4830
+ );
4831
+ }
4832
+ const clarification = {
4833
+ question: request.payload["question"] ?? null,
4834
+ answer: opts.answer
4835
+ };
4836
+ if (opts.extra !== void 0) Object.assign(clarification, opts.extra);
4837
+ return this.dispatchTask({
4838
+ neuron: target,
4839
+ input: { clarification },
4840
+ traceId: request.trace_id,
4841
+ parentId: request.id,
4842
+ ...opts.meta !== void 0 ? { meta: opts.meta } : {}
4843
+ });
4844
+ }
4845
+ /**
4846
+ * Reply to an ESCALATION by dispatching a TASK to the escalation target
4847
+ * (default: `payload.target`). Default input:
4848
+ * `{ escalation: { reason, context, from } }`.
4849
+ */
4850
+ async respondToEscalation(request, opts = {}) {
4851
+ if (request.type !== SignalType.ESCALATION) {
4852
+ throw new DendriteProtocolError(
4853
+ `respondToEscalation expects an ESCALATION signal, got '${request.type}'`
4854
+ );
4855
+ }
4856
+ const target = opts.neuron ?? request.payload["target"] ?? null;
4857
+ if (!target) {
4858
+ throw new DendriteProtocolError(
4859
+ "respondToEscalation: signal has no payload.target and no neuron override - nowhere to dispatch the follow-up TASK"
4860
+ );
4861
+ }
4862
+ const input = opts.input ?? {
4863
+ escalation: {
4864
+ reason: request.payload["reason"] ?? null,
4865
+ context: request.payload["context"] ?? null,
4866
+ from: request.directed?.id ?? null
4867
+ }
4868
+ };
4869
+ return this.dispatchTask({
4870
+ neuron: target,
4871
+ input,
4872
+ traceId: request.trace_id,
4873
+ parentId: request.id,
4874
+ ...opts.meta !== void 0 ? { meta: opts.meta } : {}
4875
+ });
4876
+ }
2873
4877
  /**
2874
4878
  * Reply to a PERMISSION by re-dispatching a TASK carrying the verdict.
2875
- *
2876
- * The "send it back to the axon" path: the follow-up TASK is addressed by
2877
- * default to the Neuron that asked (`signal.neuron`), with `parentId` = the
2878
- * PERMISSION's id and the original `traceId` carried over, so the Neuron
2879
- * resumes on the same thread and can imprint the decision into an Engram (or
2880
- * recall it next time). New TASK input: `{ permission: { action, granted,
2881
- * reason?, ttlMs?, ...extra } }`.
4879
+ * Input shape: `{ permission: { action, granted, reason?, ttl_ms?, ...extra } }`.
2882
4880
  */
2883
4881
  async respondToPermission(request, opts) {
2884
4882
  if (request.type !== SignalType.PERMISSION) {
@@ -2908,14 +4906,7 @@ var Dendrite = class {
2908
4906
  });
2909
4907
  }
2910
4908
  // -- cognition decision signals (discrete, decentralised option) -----
2911
- // Thin, stateless emit helpers for the new response signal types - no
2912
- // correlation client. Use these when you want the decision to travel as a
2913
- // discrete PERMISSION_DECISION / CLARIFICATION_ANSWER signal (e.g. for a
2914
- // peer/observer to imprint into an Engram) rather than as a re-dispatched
2915
- // TASK. Published via `publish` so any Dendrite - including a peer - can
2916
- // answer; correlation, if needed, is the developer's choice.
2917
- /** Approve a PERMISSION request. `ttlMs` optionally advertises how long the
2918
- * grant is valid so the requester can cache it (e.g. in an Engram). */
4909
+ /** Approve a PERMISSION request. */
2919
4910
  async grantPermission(request, opts = {}) {
2920
4911
  return this.decidePermission(request, true, opts);
2921
4912
  }
@@ -2929,707 +4920,755 @@ var Dendrite = class {
2929
4920
  `grant/denyPermission expects a PERMISSION signal, got '${request.type}'`
2930
4921
  );
2931
4922
  }
2932
- const sig = permissionDecisionSignal({
2933
- traceId: request.trace_id,
2934
- parentId: request.id,
2935
- granted,
2936
- directed: { id: this.dendriteId },
2937
- ...opts.reason !== void 0 ? { reason: opts.reason } : {},
2938
- ...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
2939
- ...opts.meta !== void 0 ? { meta: opts.meta } : {}
2940
- });
2941
- await this.publish(sig);
2942
- return sig;
2943
- }
2944
- /** Answer a *blocking* CLARIFICATION (the Neuron called ask(...) and is
2945
- * awaiting). Distinct from the legacy return-marker flow. */
2946
- async answerClarification(request, answer, opts = {}) {
2947
- if (request.type !== SignalType.CLARIFICATION) {
2948
- throw new DendriteProtocolError(
2949
- `answerClarification expects a CLARIFICATION signal, got '${request.type}'`
2950
- );
2951
- }
2952
- const sig = clarificationAnswerSignal({
2953
- traceId: request.trace_id,
2954
- parentId: request.id,
2955
- answer,
2956
- directed: { id: this.dendriteId },
2957
- ...opts.meta !== void 0 ? { meta: opts.meta } : {}
2958
- });
2959
- await this.publish(sig);
2960
- return sig;
2961
- }
2962
- async emit(signal) {
2963
- if (!SYNAPSE_TYPES.has(signal.type)) {
2964
- throw new DendriteProtocolError(
2965
- `Dendrite refuses to emit '${signal.type}': only synapse-side types may be emitted this way. '${signal.type}' is an Axon-owned type.`
2966
- );
2967
- }
2968
- await this.publish(signal);
2969
- }
2970
- async publish(signal) {
2971
- await this.synapse.publish(this.subject(signal.type), signal);
2972
- }
2973
- async subscribe(type, handler, opts) {
2974
- return this.synapse.subscribe(this.subject(type), handler, opts);
2975
- }
2976
- // -- internal ------------------------------------------------------
2977
- subject(type) {
2978
- return `cosmonapse.${this.namespace}.${type}`;
2979
- }
2980
- async ensureInboundSub(type) {
2981
- if (this.inboundSubs.has(type)) return;
2982
- const sub = await this.subscribe(type, (s) => this.dispatchInbound(s));
2983
- this.inboundSubs.set(type, sub);
2984
- }
2985
- async onTask(task) {
2986
- const target = task.directed?.id ?? null;
2987
- if (!target) return;
2988
- const axon = this._axons.get(target);
2989
- if (!axon) return;
2990
- let reply2;
2991
- try {
2992
- reply2 = await axon.handleTask(task);
2993
- } catch (err) {
2994
- reply2 = errorSignal({
2995
- traceId: task.trace_id,
2996
- parentId: task.id,
2997
- directed: { id: target },
2998
- code: "AXON_EXCEPTION",
2999
- message: err instanceof Error ? err.message : String(err),
3000
- recoverable: false
3001
- });
3002
- }
3003
- await this.publish(reply2);
3004
- }
3005
- async emitRegister(axon) {
3006
- await this.publish(
3007
- registerSignal({
3008
- directed: { id: axon.neuronId, capabilities: [...axon.capabilities] },
3009
- capabilities: axon.capabilities,
3010
- ...axon.version !== void 0 ? { version: axon.version } : {}
3011
- })
3012
- );
3013
- }
3014
- async emitDeregister(axon, reason) {
3015
- await this.publish(
3016
- deregisterSignal({
3017
- directed: { id: axon.neuronId },
3018
- ...reason !== void 0 ? { reason } : {}
3019
- })
3020
- );
3021
- }
3022
- async heartbeatTick() {
3023
- if (!this.running) return;
3024
- const now = (/* @__PURE__ */ new Date()).toISOString();
3025
- for (const axon of this._axons.values()) {
3026
- try {
3027
- if (this.reregisterOnHeartbeat) await this.emitRegister(axon);
3028
- await this.synapse.publish(
3029
- this.subject(SignalType.HEARTBEAT),
3030
- heartbeatSignal({ directed: { id: axon.neuronId } })
3031
- );
3032
- } catch {
3033
- }
3034
- if (this.registryStore !== null) {
3035
- try {
3036
- await this.registryStore.touchHeartbeat(axon.neuronId, now);
3037
- } catch {
3038
- }
3039
- }
3040
- await this.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
3041
- await axon.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
3042
- }
3043
- }
3044
- async mirrorToStore(axon, status) {
3045
- if (this.registryStore === null) return;
3046
- try {
3047
- await this.registryStore.upsert(
3048
- neuronRecord({
3049
- neuron_id: axon.neuronId,
3050
- capabilities: [...axon.capabilities],
3051
- version: axon.version ?? null,
3052
- status,
3053
- last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
3054
- })
3055
- );
3056
- } catch {
3057
- }
3058
- }
3059
- async dispatchInbound(signal) {
3060
- if (!AXON_TYPES.has(signal.type)) return;
3061
- if (this.registryStore !== null) {
3062
- try {
3063
- await this.updateRegistry(signal);
3064
- } catch {
3065
- }
3066
- }
3067
- const handlers = this.handlers.get(signal.type) ?? [];
3068
- if (!handlers.length) return;
3069
- await Promise.allSettled(handlers.map((h) => h(signal)));
3070
- }
3071
- async updateRegistry(signal) {
3072
- if (this.registryStore === null) return;
3073
- if (signal.payload["engram"]) return;
3074
- const neuronId = signal.directed?.id ?? null;
3075
- if (!neuronId) return;
3076
- let reason = null;
3077
- if (signal.type === SignalType.REGISTER) {
3078
- await this.registryStore.upsert(
3079
- neuronRecord({
3080
- neuron_id: neuronId,
3081
- capabilities: signal.payload["capabilities"] ?? [],
3082
- version: signal.payload["version"] ?? null,
3083
- status: "registered",
3084
- last_heartbeat: signal.ts
3085
- })
3086
- );
3087
- reason = "register";
3088
- } else if (signal.type === SignalType.DEREGISTER) {
3089
- await this.registryStore.markDeregistered(neuronId);
3090
- reason = "deregister";
3091
- } else if (signal.type === SignalType.HEARTBEAT) {
3092
- const status = signal.payload["status"];
3093
- if (status) await this.registryStore.touchHeartbeat(neuronId, signal.ts, status);
3094
- else await this.registryStore.touchHeartbeat(neuronId, signal.ts);
3095
- reason = "heartbeat";
3096
- }
3097
- if (reason !== null) {
3098
- await this.hooks._fireRefresh({ reason, neuronId, extra: {} });
3099
- }
4923
+ const sig = permissionDecisionSignal({
4924
+ traceId: request.trace_id,
4925
+ parentId: request.id,
4926
+ granted,
4927
+ directed: { id: this.dendriteId },
4928
+ ...opts.reason !== void 0 ? { reason: opts.reason } : {},
4929
+ ...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
4930
+ ...opts.meta !== void 0 ? { meta: opts.meta } : {}
4931
+ });
4932
+ await this.publish(sig);
4933
+ return sig;
3100
4934
  }
3101
- };
3102
- var Cortex = Dendrite;
3103
-
3104
- // src/engram.ts
3105
- var EngramBinding = class {
3106
- name;
3107
- directedId;
3108
- directedType;
3109
- defaultDeadlineMs;
3110
- defaultRecallMode;
3111
- constructor(init) {
3112
- this.name = init.name;
3113
- this.directedId = init.directedId ?? null;
3114
- this.directedType = init.directedType ?? null;
3115
- this.defaultDeadlineMs = init.defaultDeadlineMs ?? null;
3116
- this.defaultRecallMode = init.defaultRecallMode ?? "first";
3117
- if (!this.directedId && !this.directedType) {
3118
- throw new Error(
3119
- `EngramBinding '${this.name}' requires directedId (engram_id) or directedType (engram_kind), or both`
4935
+ /** Answer a CLARIFICATION with a discrete CLARIFICATION_ANSWER signal
4936
+ * (parent_id = the request's id). Consumers pick it up via
4937
+ * {@link onClarificationAnswer} or {@link awaitDecision}. Distinct from
4938
+ * {@link respondToClarification}, which re-dispatches a TASK. */
4939
+ async answerClarification(request, answer, opts = {}) {
4940
+ if (request.type !== SignalType.CLARIFICATION) {
4941
+ throw new DendriteProtocolError(
4942
+ `answerClarification expects a CLARIFICATION signal, got '${request.type}'`
3120
4943
  );
3121
4944
  }
4945
+ const sig = clarificationAnswerSignal({
4946
+ traceId: request.trace_id,
4947
+ parentId: request.id,
4948
+ answer,
4949
+ directed: { id: this.dendriteId },
4950
+ ...opts.meta !== void 0 ? { meta: opts.meta } : {}
4951
+ });
4952
+ await this.publish(sig);
4953
+ return sig;
3122
4954
  }
3123
- /** Build the `Directed` addressing this Engram. */
3124
- toDirected() {
3125
- return { id: this.directedId, type: this.directedType, capabilities: [] };
3126
- }
3127
- };
3128
- var EngramError = class extends Error {
3129
- constructor(message) {
3130
- super(message);
3131
- this.name = new.target.name;
4955
+ /** Emit a synapse-side Signal. Refuses Axon-owned types; TASK initiation
4956
+ * additionally requires orchestrator role. */
4957
+ async emit(signal) {
4958
+ if (signal.type === SignalType.TASK) {
4959
+ this.requireOrchestrator(`emit(${signal.type})`);
4960
+ }
4961
+ if (!SYNAPSE_TYPES.has(signal.type)) {
4962
+ throw new DendriteProtocolError(
4963
+ `Dendrite refuses to emit '${signal.type}': only synapse-side types may be emitted this way. '${signal.type}' is an Axon-owned type.`
4964
+ );
4965
+ }
4966
+ await this.publish(signal);
3132
4967
  }
3133
- };
3134
- var EngramTimeout = class extends EngramError {
3135
- };
3136
- var EngramCancelled = class extends EngramError {
3137
- };
3138
- var EngramNotBound = class extends EngramError {
3139
- };
3140
- var EngramOverloaded = class extends EngramError {
3141
- };
3142
- var Engram = class {
3143
- version = null;
3144
- /** Return false if this Engram cannot satisfy the query. Default: serve all. */
3145
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
3146
- async canServe(_query) {
3147
- return true;
4968
+ async publish(signal) {
4969
+ await this.synapse.publish(this.subject(signal.type), signal);
3148
4970
  }
3149
- };
3150
- function receipt(engramId, op, fields = {}) {
3151
- const error = fields.error ?? null;
3152
- return {
3153
- engramId,
3154
- op,
3155
- id: fields.id ?? null,
3156
- version: fields.version ?? null,
3157
- tookMs: fields.tookMs ?? null,
3158
- error,
3159
- ok: error === null
3160
- };
3161
- }
3162
- function entryToDict(e) {
3163
- const out = {
3164
- id: e.id,
3165
- content: e.content,
3166
- tags: [...e.tags],
3167
- version: e.version,
3168
- created_at: e.createdAt,
3169
- updated_at: e.updatedAt
3170
- };
3171
- if (e.mergeKey !== null) out["merge_key"] = e.mergeKey;
3172
- if (Object.keys(e.extra).length > 0) out["meta"] = { ...e.extra };
3173
- return out;
3174
- }
3175
- function asStringArray(v) {
3176
- return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
3177
- }
3178
- function asObject(v) {
3179
- return v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
3180
- }
3181
- var InMemoryEngram = class extends Engram {
3182
- engramId;
3183
- engramKind;
3184
- capabilities;
3185
- entries = /* @__PURE__ */ new Map();
3186
- byMergeKey = /* @__PURE__ */ new Map();
3187
- imprintSeen = /* @__PURE__ */ new Map();
3188
- constructor(init = {}) {
3189
- super();
3190
- this.engramId = init.engramId ?? "engram-memory";
3191
- this.engramKind = init.engramKind ?? "keyvalue";
3192
- this.capabilities = init.capabilities ?? ["substring", "tags", "merge_key"];
3193
- this.version = init.version ?? "0.0.1";
4971
+ async subscribe(type, handler, opts) {
4972
+ return this.synapse.subscribe(this.subject(type), handler, opts);
3194
4973
  }
3195
- async connect() {
3196
- return;
4974
+ // -- internal ------------------------------------------------------
4975
+ subject(type) {
4976
+ return `cosmonapse.${this.namespace}.${type}`;
3197
4977
  }
3198
- async close() {
3199
- this.entries.clear();
3200
- this.byMergeKey.clear();
3201
- this.imprintSeen.clear();
4978
+ /** Subject for capability-routed TASKs (queue-grouped consumers). */
4979
+ routedSubject() {
4980
+ return `cosmonapse.${this.namespace}.${SignalType.TASK}.routed`;
4981
+ }
4982
+ ensureInboundSub(type) {
4983
+ if (this.inboundSubs.has(type)) return Promise.resolve();
4984
+ let p = this.inflightSubs.get(type);
4985
+ if (!p) {
4986
+ p = (async () => {
4987
+ const sub = await this.subscribe(type, (s) => this.dispatchInbound(s));
4988
+ this.inboundSubs.set(type, sub);
4989
+ })().finally(() => this.inflightSubs.delete(type));
4990
+ this.inflightSubs.set(type, p);
4991
+ }
4992
+ return p;
3202
4993
  }
3203
- async recall(query, opts = {}) {
3204
- const q = query ?? {};
3205
- const text = typeof q["text"] === "string" ? q["text"].toLowerCase() : "";
3206
- const tagQ = typeof q["tag"] === "string" ? q["tag"] : null;
3207
- const mergeKey = typeof q["merge_key"] === "string" ? q["merge_key"] : null;
3208
- const topK = typeof q["top_k"] === "number" ? q["top_k"] : 50;
3209
- const filters = opts.filters ?? {};
3210
- const requireTags = asStringArray(filters["tags"]);
3211
- const since = typeof filters["since"] === "string" ? Date.parse(filters["since"]) : NaN;
3212
- const until = typeof filters["until"] === "string" ? Date.parse(filters["until"]) : NaN;
3213
- let candidates;
3214
- if (mergeKey !== null) {
3215
- const ids = this.byMergeKey.get(mergeKey) ?? [];
3216
- candidates = ids.map((i) => this.entries.get(i)).filter((e) => e !== void 0);
4994
+ /**
4995
+ * Route an inbound TASK to a local Axon. Addressed: by neuron_id (drop if
4996
+ * not hosted here). Capability-routed: first local Axon whose caps superset
4997
+ * the request. After publishing the reply, apply terminal-handler finalize:
4998
+ * a TASK tagged `payload.finalize` promotes a successful AGENT_OUTPUT by
4999
+ * also emitting FINAL (parented to the AGENT_OUTPUT, attributed to the
5000
+ * producing Neuron). Only AGENT_OUTPUT is promoted - CLARIFICATION /
5001
+ * PERMISSION pause the workflow and ERROR is already terminal.
5002
+ */
5003
+ async onTask(task) {
5004
+ let target = task.directed?.id ?? null;
5005
+ let axon;
5006
+ if (target) {
5007
+ axon = this._axons.get(target);
5008
+ if (!axon) return;
3217
5009
  } else {
3218
- candidates = [...this.entries.values()];
5010
+ const requested = task.payload["capabilities"] ?? [];
5011
+ if (!requested.length) return;
5012
+ for (const candidate of this._axons.values()) {
5013
+ if (requested.every((c) => candidate.capabilities.includes(c))) {
5014
+ axon = candidate;
5015
+ break;
5016
+ }
5017
+ }
5018
+ if (!axon) return;
5019
+ target = axon.neuronId;
3219
5020
  }
3220
- const hits = [];
3221
- for (const ent of candidates) {
3222
- if (requireTags.length > 0 && !requireTags.every((t) => ent.tags.includes(t))) continue;
3223
- const updated = Date.parse(ent.updatedAt);
3224
- if (!Number.isNaN(since) && updated < since) continue;
3225
- if (!Number.isNaN(until) && updated > until) continue;
3226
- if (tagQ !== null && !ent.tags.includes(tagQ)) continue;
3227
- let score = 1;
3228
- if (text) {
3229
- const hay = String(ent.content).toLowerCase();
3230
- if (!hay.includes(text)) continue;
3231
- score = Math.min(1, text.length / Math.max(1, hay.length));
5021
+ const ac = new AbortController();
5022
+ this.registerTraceAbort(task.trace_id, ac);
5023
+ let reply2;
5024
+ try {
5025
+ reply2 = await this.raceAbort(axon.handleTask(task), ac.signal);
5026
+ } catch (err) {
5027
+ reply2 = errorSignal({
5028
+ traceId: task.trace_id,
5029
+ parentId: task.id,
5030
+ directed: { id: target },
5031
+ code: "AXON_EXCEPTION",
5032
+ message: err instanceof Error ? err.message : String(err),
5033
+ recoverable: false
5034
+ });
5035
+ } finally {
5036
+ this.unregisterTraceAbort(task.trace_id, ac);
5037
+ }
5038
+ if (reply2 === null) {
5039
+ return;
5040
+ }
5041
+ await this.publish(reply2);
5042
+ if (reply2.type === SignalType.AGENT_OUTPUT && task.payload["finalize"]) {
5043
+ try {
5044
+ await this.publish(
5045
+ finalSignal({
5046
+ traceId: reply2.trace_id,
5047
+ parentId: reply2.id,
5048
+ directed: { id: target },
5049
+ result: reply2.payload["output"] ?? {}
5050
+ })
5051
+ );
5052
+ } catch {
3232
5053
  }
3233
- if (opts.minConfidence !== void 0 && score < opts.minConfidence) continue;
3234
- hits.push({ id: ent.id, entry: entryToDict(ent), score });
3235
5054
  }
3236
- hits.sort((a, b) => b.score - a.score);
3237
- return hits.slice(0, topK);
3238
5055
  }
3239
- async imprint(op, entry, opts = {}) {
3240
- const t0 = Date.now();
3241
- const mergeKey = opts.mergeKey ?? null;
3242
- const tookMs = () => Date.now() - t0;
3243
- if (opts.imprintId !== void 0) {
3244
- const seen = this.imprintSeen.get(opts.imprintId);
3245
- if (seen !== void 0) {
3246
- const existing = this.entries.get(seen);
3247
- return receipt(this.engramId, op, {
3248
- id: seen,
3249
- version: existing ? existing.version : null,
3250
- tookMs: tookMs()
3251
- });
3252
- }
5056
+ // -- workflow control: STOP / STOPPED -------------------------------
5057
+ registerTraceAbort(traceId, ac) {
5058
+ let set = this.traceAborts.get(traceId);
5059
+ if (!set) {
5060
+ set = /* @__PURE__ */ new Set();
5061
+ this.traceAborts.set(traceId, set);
3253
5062
  }
3254
- let resultingId = null;
3255
- let version = null;
3256
- if (op === "add") {
3257
- const ent = this.makeEntry(entry, mergeKey);
3258
- if (this.entries.has(ent.id)) {
3259
- return receipt(this.engramId, op, { error: `entry id '${ent.id}' already exists`, tookMs: tookMs() });
3260
- }
3261
- this.store(ent);
3262
- resultingId = ent.id;
3263
- version = ent.version;
3264
- } else if (op === "append") {
3265
- let ent = this.makeEntry(entry, mergeKey);
3266
- while (this.entries.has(ent.id)) {
3267
- ent = this.makeEntry({ ...entry, id: newEngramId() }, mergeKey);
3268
- }
3269
- this.store(ent);
3270
- resultingId = ent.id;
3271
- version = ent.version;
3272
- } else if (op === "upsert") {
3273
- const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
3274
- const targetId = existingIds[existingIds.length - 1];
3275
- const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
3276
- if (old !== void 0) {
3277
- const next = this.makeEntry({ ...entry, id: old.id }, mergeKey);
3278
- next.createdAt = old.createdAt;
3279
- next.version = old.version + 1;
3280
- this.store(next, true);
3281
- resultingId = next.id;
3282
- version = next.version;
3283
- } else {
3284
- const ent = this.makeEntry(entry, mergeKey);
3285
- this.store(ent);
3286
- resultingId = ent.id;
3287
- version = ent.version;
3288
- }
3289
- } else if (op === "merge") {
3290
- const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
3291
- const targetId = existingIds[existingIds.length - 1];
3292
- const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
3293
- if (old === void 0) {
3294
- return receipt(this.engramId, op, { error: `no entry for merge_key='${mergeKey}'`, tookMs: tookMs() });
5063
+ set.add(ac);
5064
+ }
5065
+ unregisterTraceAbort(traceId, ac) {
5066
+ const set = this.traceAborts.get(traceId);
5067
+ if (set) {
5068
+ set.delete(ac);
5069
+ if (set.size === 0) this.traceAborts.delete(traceId);
5070
+ }
5071
+ }
5072
+ /** Resolve to the promise's value, or to null if the signal aborts first. */
5073
+ raceAbort(p, signal) {
5074
+ if (signal.aborted) return Promise.resolve(null);
5075
+ return new Promise((resolve, reject) => {
5076
+ const onAbort = () => resolve(null);
5077
+ signal.addEventListener("abort", onAbort, { once: true });
5078
+ p.then(
5079
+ (v) => {
5080
+ signal.removeEventListener("abort", onAbort);
5081
+ resolve(v);
5082
+ },
5083
+ (e) => {
5084
+ signal.removeEventListener("abort", onAbort);
5085
+ reject(e);
5086
+ }
5087
+ );
5088
+ });
5089
+ }
5090
+ async onStop(signal) {
5091
+ const traceId = signal.trace_id;
5092
+ if (!traceId) return;
5093
+ const rollback = Boolean(signal.payload["rollback"]);
5094
+ let cancelled = 0;
5095
+ let compensated = 0;
5096
+ let didWork = false;
5097
+ const acs = this.traceAborts.get(traceId);
5098
+ if (acs) {
5099
+ for (const ac of acs) {
5100
+ if (!ac.signal.aborted) {
5101
+ ac.abort();
5102
+ cancelled++;
5103
+ }
3295
5104
  }
3296
- const now = (/* @__PURE__ */ new Date()).toISOString();
3297
- const next = {
3298
- id: old.id,
3299
- content: deepMerge(old.content, entry["content"]),
3300
- tags: [.../* @__PURE__ */ new Set([...old.tags, ...asStringArray(entry["tags"])])],
3301
- mergeKey: old.mergeKey,
3302
- version: old.version + 1,
3303
- createdAt: old.createdAt,
3304
- updatedAt: now,
3305
- extra: asObject(deepMerge(old.extra, entry["meta"]))
3306
- };
3307
- this.store(next, true);
3308
- resultingId = next.id;
3309
- version = next.version;
3310
- } else if (op === "delete") {
3311
- let targetId = null;
3312
- const entId = entry["id"];
3313
- if (typeof entId === "string") {
3314
- targetId = entId;
3315
- } else if (mergeKey !== null) {
3316
- const ids = this.byMergeKey.get(mergeKey) ?? [];
3317
- targetId = ids[ids.length - 1] ?? null;
5105
+ this.traceAborts.delete(traceId);
5106
+ didWork = true;
5107
+ }
5108
+ try {
5109
+ await this.cancelOpPathways(traceId);
5110
+ this.engramClient.cancelTrace(traceId);
5111
+ } catch {
5112
+ }
5113
+ for (const engram of this._engrams.values()) {
5114
+ try {
5115
+ if (rollback) {
5116
+ const n = await engram.compensate(traceId);
5117
+ if (n > 0) {
5118
+ compensated += n;
5119
+ didWork = true;
5120
+ }
5121
+ } else {
5122
+ await engram.commit(traceId);
5123
+ }
5124
+ } catch {
3318
5125
  }
3319
- if (targetId === null || !this.entries.has(targetId)) {
3320
- return receipt(this.engramId, op, { tookMs: tookMs() });
5126
+ }
5127
+ const pw = this.pathways.get(traceId);
5128
+ if (pw && !pw.closed) {
5129
+ didWork = true;
5130
+ try {
5131
+ await pw.close();
5132
+ } catch {
3321
5133
  }
3322
- this.evict(targetId);
3323
- resultingId = targetId;
3324
- version = null;
3325
- } else {
3326
- return receipt(this.engramId, op, { error: `unknown op '${op}'`, tookMs: tookMs() });
3327
5134
  }
3328
- if (opts.imprintId !== void 0 && resultingId !== null) {
3329
- this.imprintSeen.set(opts.imprintId, resultingId);
5135
+ if (didWork) {
5136
+ try {
5137
+ await this.publish(
5138
+ stoppedSignal({
5139
+ traceId,
5140
+ parentId: signal.id,
5141
+ node: this.namespace,
5142
+ rolledBack: rollback,
5143
+ cancelled,
5144
+ compensated
5145
+ })
5146
+ );
5147
+ } catch {
5148
+ }
3330
5149
  }
3331
- return receipt(this.engramId, op, { id: resultingId, version, tookMs: tookMs() });
3332
5150
  }
3333
- /** Test/debug helper - NOT part of the Engram contract. */
3334
- snapshot() {
3335
- return [...this.entries.values()].map(entryToDict);
5151
+ /** Broadcast a STOP for `traceId` (orchestrator-gated). Best-effort and
5152
+ * idempotent. */
5153
+ async emitStop(args) {
5154
+ this.requireOrchestrator("emitStop");
5155
+ await this.ensureInboundSub(SignalType.STOP);
5156
+ const sig = stopSignal({
5157
+ traceId: args.traceId,
5158
+ ...args.rollback !== void 0 ? { rollback: args.rollback } : {},
5159
+ ...args.reason !== void 0 ? { reason: args.reason } : {}
5160
+ });
5161
+ await this.publish(sig);
5162
+ return sig;
3336
5163
  }
3337
- makeEntry(entry, mergeKey) {
3338
- const id = typeof entry["id"] === "string" ? entry["id"] : newEngramId();
3339
- const now = (/* @__PURE__ */ new Date()).toISOString();
3340
- return {
3341
- id,
3342
- content: entry["content"],
3343
- tags: asStringArray(entry["tags"]),
3344
- mergeKey,
3345
- version: 1,
3346
- createdAt: now,
3347
- updatedAt: now,
3348
- extra: asObject(entry["meta"])
5164
+ /** Stop a whole workflow. With `collectAcks` returns the STOPPED acks seen
5165
+ * within `timeoutMs` (best effort). */
5166
+ async stopTrace(traceId, opts = {}) {
5167
+ if (!opts.collectAcks) {
5168
+ await this.emitStop({
5169
+ traceId,
5170
+ ...opts.rollback !== void 0 ? { rollback: opts.rollback } : {},
5171
+ ...opts.reason !== void 0 ? { reason: opts.reason } : {}
5172
+ });
5173
+ return [];
5174
+ }
5175
+ const acks = [];
5176
+ const collect = async (sig) => {
5177
+ if (sig.trace_id === traceId) acks.push(sig);
3349
5178
  };
5179
+ const list = this.handlers.get(SignalType.STOPPED) ?? [];
5180
+ list.push(collect);
5181
+ this.handlers.set(SignalType.STOPPED, list);
5182
+ await this.ensureInboundSub(SignalType.STOPPED);
5183
+ try {
5184
+ await this.emitStop({
5185
+ traceId,
5186
+ ...opts.rollback !== void 0 ? { rollback: opts.rollback } : {},
5187
+ ...opts.reason !== void 0 ? { reason: opts.reason } : {}
5188
+ });
5189
+ await new Promise((r) => setTimeout(r, opts.timeoutMs ?? 1e3));
5190
+ } finally {
5191
+ const idx = (this.handlers.get(SignalType.STOPPED) ?? []).indexOf(collect);
5192
+ if (idx >= 0) (this.handlers.get(SignalType.STOPPED) ?? []).splice(idx, 1);
5193
+ }
5194
+ return acks;
3350
5195
  }
3351
- store(ent, replace = false) {
3352
- if (replace) {
3353
- const old = this.entries.get(ent.id);
3354
- if (old !== void 0 && old.mergeKey) {
3355
- const bucket = this.byMergeKey.get(old.mergeKey);
3356
- if (bucket) {
3357
- const idx = bucket.indexOf(ent.id);
3358
- if (idx >= 0) bucket.splice(idx, 1);
3359
- if (bucket.length === 0) this.byMergeKey.delete(old.mergeKey);
5196
+ // -- retry ----------------------------------------------------------
5197
+ async safeStop(traceId, retry) {
5198
+ try {
5199
+ await this.emitStop({
5200
+ traceId,
5201
+ rollback: Boolean(retry.rollbackOnRetry),
5202
+ reason: retry.reason ?? "retry"
5203
+ });
5204
+ } catch {
5205
+ }
5206
+ }
5207
+ /** Dispatch and wait, retrying per `retry` until a non-retryable outcome or
5208
+ * attempts are exhausted. Returns the resolved Signal; re-throws the last
5209
+ * error when every attempt failed with an exception. */
5210
+ async runWithRetry(args) {
5211
+ const { retry, timeoutMs, traceId: callerTrace, ...rest } = args;
5212
+ const maxAttempts = retry.maxAttempts ?? 3;
5213
+ const retryOn = retry.retryOn ?? defaultRetryOn;
5214
+ const newTrace = retry.newTrace ?? true;
5215
+ const perTimeout = retry.timeoutMs ?? timeoutMs ?? 3e4;
5216
+ let outcome = null;
5217
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
5218
+ const tid = callerTrace && !newTrace ? callerTrace : newTraceId();
5219
+ const meta = { ...rest.meta ?? {}, attempt };
5220
+ try {
5221
+ const pathway = await this.dispatch({ ...rest, traceId: tid, meta });
5222
+ try {
5223
+ outcome = await pathway.wait(perTimeout);
5224
+ } finally {
5225
+ await pathway.close();
3360
5226
  }
5227
+ } catch (err) {
5228
+ outcome = err instanceof Error ? err : new Error(String(err));
3361
5229
  }
5230
+ if (!retryOn(outcome)) {
5231
+ if (outcome instanceof Error) throw outcome;
5232
+ return outcome;
5233
+ }
5234
+ if (newTrace) await this.safeStop(tid, retry);
5235
+ if (attempt + 1 >= maxAttempts) {
5236
+ if (outcome instanceof Error) throw outcome;
5237
+ return outcome;
5238
+ }
5239
+ if (retry.onRetry) {
5240
+ try {
5241
+ retry.onRetry(attempt, outcome);
5242
+ } catch {
5243
+ }
5244
+ }
5245
+ const delay = retry.backoffMs ? retry.backoffMs(attempt) : 0;
5246
+ if (delay > 0) await new Promise((r) => setTimeout(r, delay));
3362
5247
  }
3363
- this.entries.set(ent.id, ent);
3364
- if (ent.mergeKey) {
3365
- const bucket = this.byMergeKey.get(ent.mergeKey);
3366
- if (bucket) bucket.push(ent.id);
3367
- else this.byMergeKey.set(ent.mergeKey, [ent.id]);
3368
- }
5248
+ throw new Error("runWithRetry: exhausted attempts unexpectedly");
3369
5249
  }
3370
- evict(entryId) {
3371
- const ent = this.entries.get(entryId);
3372
- this.entries.delete(entryId);
3373
- if (ent === void 0) return;
3374
- if (ent.mergeKey) {
3375
- const bucket = this.byMergeKey.get(ent.mergeKey);
3376
- if (bucket) {
3377
- const idx = bucket.indexOf(entryId);
3378
- if (idx >= 0) bucket.splice(idx, 1);
3379
- if (bucket.length === 0) this.byMergeKey.delete(ent.mergeKey);
5250
+ async emitRegister(axon) {
5251
+ await this.publish(
5252
+ registerSignal({
5253
+ directed: {
5254
+ id: axon.neuronId,
5255
+ type: axon.neuronKind ?? "neuron",
5256
+ capabilities: [...axon.capabilities]
5257
+ },
5258
+ capabilities: axon.capabilities,
5259
+ role: "neuron",
5260
+ ...axon.version !== void 0 ? { version: axon.version } : {}
5261
+ })
5262
+ );
5263
+ }
5264
+ async emitDeregister(axon, reason) {
5265
+ await this.publish(
5266
+ deregisterSignal({
5267
+ directed: { id: axon.neuronId },
5268
+ ...reason !== void 0 ? { reason } : {}
5269
+ })
5270
+ );
5271
+ }
5272
+ async heartbeatTick() {
5273
+ if (!this.running) return;
5274
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5275
+ for (const axon of this._axons.values()) {
5276
+ try {
5277
+ if (this.reregisterOnHeartbeat) await this.emitRegister(axon);
5278
+ await this.synapse.publish(
5279
+ this.subject(SignalType.HEARTBEAT),
5280
+ heartbeatSignal({ directed: { id: axon.neuronId } })
5281
+ );
5282
+ } catch {
5283
+ }
5284
+ if (this.registryStore !== null) {
5285
+ try {
5286
+ await this.registryStore.touchHeartbeat(axon.neuronId, now);
5287
+ } catch {
5288
+ }
3380
5289
  }
5290
+ await this.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
5291
+ await axon.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
3381
5292
  }
3382
- }
3383
- };
3384
- function deepMerge(base, incoming) {
3385
- if (incoming === void 0 || incoming === null) return base;
3386
- const bothObjects = base !== null && typeof base === "object" && !Array.isArray(base) && typeof incoming === "object" && !Array.isArray(incoming);
3387
- if (bothObjects) {
3388
- const out = { ...base };
3389
- for (const [k, v] of Object.entries(incoming)) {
3390
- out[k] = k in out ? deepMerge(out[k], v) : v;
5293
+ if (this.registryStore !== null && this.staleAfterMs > 0) {
5294
+ try {
5295
+ await this.sweepStaleNeurons(Date.now());
5296
+ } catch {
5297
+ }
3391
5298
  }
3392
- return out;
3393
5299
  }
3394
- if (Array.isArray(base) && Array.isArray(incoming)) {
3395
- const seen = /* @__PURE__ */ new Set();
3396
- const out = [];
3397
- for (const item of [...base, ...incoming]) {
3398
- const key = JSON.stringify(item);
3399
- if (seen.has(key)) continue;
3400
- seen.add(key);
3401
- out.push(item);
5300
+ /** Mark Neurons deregistered when their last heartbeat is older than
5301
+ * `staleAfterMs`. Own hosted Axons were touched immediately before the
5302
+ * sweep, so they never qualify. */
5303
+ async sweepStaleNeurons(nowMs) {
5304
+ const store = this.registryStore;
5305
+ if (store === null) return;
5306
+ const records = await store.list({ includeDeregistered: false });
5307
+ for (const rec of records) {
5308
+ const seen = rec.last_heartbeat ?? rec.registered_at;
5309
+ if (!seen) continue;
5310
+ if (nowMs - Date.parse(seen) > this.staleAfterMs) {
5311
+ try {
5312
+ await store.markDeregistered(rec.neuron_id);
5313
+ await this.hooks._fireRefresh({
5314
+ reason: "stale",
5315
+ neuronId: rec.neuron_id,
5316
+ extra: {}
5317
+ });
5318
+ } catch {
5319
+ }
5320
+ }
3402
5321
  }
3403
- return out;
3404
- }
3405
- return incoming;
3406
- }
3407
-
3408
- // src/engram-client.ts
3409
- function deferred() {
3410
- let resolve;
3411
- let reject;
3412
- const promise = new Promise((res, rej) => {
3413
- resolve = res;
3414
- reject = rej;
3415
- });
3416
- return { promise, resolve, reject };
3417
- }
3418
- var EngramClient = class {
3419
- constructor(publisher) {
3420
- this.publisher = publisher;
3421
5322
  }
3422
- publisher;
3423
- pendingRecalls = /* @__PURE__ */ new Map();
3424
- pendingImprints = /* @__PURE__ */ new Map();
3425
- byTrace = /* @__PURE__ */ new Map();
3426
- async recall(args) {
3427
- let engramId = args.engramId;
3428
- let engramKind = args.engramKind;
3429
- let deadlineMs = args.deadlineMs;
3430
- let recallMode = args.recallMode;
3431
- if (args.binding) {
3432
- engramId = engramId ?? args.binding.directedId ?? void 0;
3433
- engramKind = engramKind ?? args.binding.directedType ?? void 0;
3434
- if (deadlineMs === void 0) deadlineMs = args.binding.defaultDeadlineMs ?? void 0;
3435
- if (recallMode === void 0) recallMode = args.binding.defaultRecallMode;
5323
+ async mirrorToStore(axon, status) {
5324
+ if (this.registryStore === null) return;
5325
+ try {
5326
+ await this.registryStore.upsert(
5327
+ neuronRecord({
5328
+ neuron_id: axon.neuronId,
5329
+ capabilities: [...axon.capabilities],
5330
+ version: axon.version ?? null,
5331
+ status,
5332
+ last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
5333
+ })
5334
+ );
5335
+ } catch {
3436
5336
  }
3437
- const mode = recallMode ?? "first";
3438
- const sig = recallSignal({
3439
- traceId: args.traceId,
3440
- parentId: args.parentId,
3441
- directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
3442
- query: args.query,
3443
- ...args.filters !== void 0 ? { filters: args.filters } : {},
3444
- ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
3445
- ...deadlineMs !== void 0 ? { deadlineMs } : {},
3446
- ...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
3447
- recallMode: mode,
3448
- ...args.meta !== void 0 ? { meta: args.meta } : {}
3449
- });
3450
- const d = deferred();
3451
- const pending = { deferred: d, mode, timer: null, done: false, hitsSoFar: [], engrams: [] };
3452
- this.pendingRecalls.set(sig.id, pending);
3453
- this.track(args.traceId, sig.id);
3454
- if (deadlineMs !== void 0 && deadlineMs > 0) {
3455
- pending.timer = setTimeout(() => this.onRecallDeadline(sig.id), deadlineMs);
5337
+ }
5338
+ /** Respond to a DISCOVER by re-emitting REGISTER for matching Axons. */
5339
+ async respondToDiscover(signal) {
5340
+ if (this._axons.size === 0) return;
5341
+ const target = signal.payload["neuron"];
5342
+ const capsFilter = signal.payload["capabilities"];
5343
+ for (const axon of this._axons.values()) {
5344
+ if (target && axon.neuronId !== target) continue;
5345
+ if (capsFilter?.length && !capsFilter.every((c) => axon.capabilities.includes(c))) {
5346
+ continue;
5347
+ }
5348
+ try {
5349
+ await this.emitRegister(axon);
5350
+ } catch {
5351
+ }
3456
5352
  }
3457
- try {
3458
- await this.publisher.publish(sig);
3459
- } catch (err) {
3460
- this.cleanupRecall(args.traceId, sig.id);
3461
- throw err;
5353
+ }
5354
+ async dispatchInbound(signal) {
5355
+ if (signal.type === SignalType.DISCOVER) {
5356
+ const hs = this.handlers.get(SignalType.DISCOVER) ?? [];
5357
+ if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
5358
+ else await this.respondToDiscover(signal);
5359
+ return;
3462
5360
  }
3463
- try {
3464
- return await d.promise;
3465
- } finally {
3466
- this.cleanupRecall(args.traceId, sig.id);
5361
+ if (signal.type === SignalType.RECALL) {
5362
+ await this.onRecall(signal);
5363
+ const hs = this.handlers.get(SignalType.RECALL) ?? [];
5364
+ if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
5365
+ return;
3467
5366
  }
3468
- }
3469
- async imprint(args) {
3470
- let engramId = args.engramId;
3471
- let engramKind = args.engramKind;
3472
- if (args.binding) {
3473
- engramId = engramId ?? args.binding.directedId ?? void 0;
3474
- engramKind = engramKind ?? args.binding.directedType ?? void 0;
5367
+ if (signal.type === SignalType.IMPRINT) {
5368
+ await this.onImprint(signal);
5369
+ const hs = this.handlers.get(SignalType.IMPRINT) ?? [];
5370
+ if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
5371
+ return;
3475
5372
  }
3476
- const sig = imprintSignal({
3477
- traceId: args.traceId,
3478
- parentId: args.parentId,
3479
- directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
3480
- op: args.op,
3481
- entry: args.entry,
3482
- ...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
3483
- ...args.meta !== void 0 ? { meta: args.meta } : {}
3484
- });
3485
- if (!args.awaitAck) {
3486
- await this.publisher.publish(sig);
3487
- return null;
5373
+ if (signal.type === SignalType.STOP) {
5374
+ if (signal.trace_id) {
5375
+ const pw = this.pathways.get(signal.trace_id);
5376
+ if (pw) {
5377
+ try {
5378
+ await pw._deliver(signal);
5379
+ } catch {
5380
+ }
5381
+ }
5382
+ }
5383
+ await this.onStop(signal);
5384
+ const hs = this.handlers.get(SignalType.STOP) ?? [];
5385
+ if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
5386
+ return;
3488
5387
  }
3489
- const d = deferred();
3490
- const pending = { deferred: d, timer: null, done: false };
3491
- this.pendingImprints.set(sig.id, pending);
3492
- this.track(args.traceId, sig.id);
3493
- if (args.deadlineMs !== void 0 && args.deadlineMs > 0) {
3494
- pending.timer = setTimeout(() => this.onImprintDeadline(sig.id), args.deadlineMs);
5388
+ if (signal.type === SignalType.RECALLED || signal.type === SignalType.IMPRINTED) {
5389
+ this.engramClient.deliver(signal);
3495
5390
  }
3496
- try {
3497
- await this.publisher.publish(sig);
3498
- } catch (err) {
3499
- this.cleanupImprint(args.traceId, sig.id);
3500
- throw err;
5391
+ if (signal.type === SignalType.REGISTER && this.isEngramRegister(signal)) {
5392
+ this.recordEngramRegistration(signal);
5393
+ return;
3501
5394
  }
3502
- try {
3503
- return await d.promise;
3504
- } finally {
3505
- this.cleanupImprint(args.traceId, sig.id);
5395
+ if (signal.parent_id) {
5396
+ const opPw = this.opPathways.get(signal.parent_id);
5397
+ if (opPw) {
5398
+ try {
5399
+ await opPw._deliver(signal);
5400
+ } catch {
5401
+ }
5402
+ }
3506
5403
  }
3507
- }
3508
- /** Match RECALLED / IMPRINTED by parent_id and resolve pendings. */
3509
- deliver(sig) {
3510
- const pid = sig.parent_id;
3511
- if (pid === null) return;
3512
- if (sig.type === SignalType.RECALLED) {
3513
- const pending = this.pendingRecalls.get(pid);
3514
- if (pending === void 0) return;
3515
- const hits = hitsFromPayload(sig.payload["hits"]);
3516
- const engramId = typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "";
3517
- const tookMs = typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null;
3518
- const truncated = sig.payload["truncated"] === true;
3519
- if (pending.mode === "first") {
3520
- if (!pending.done) {
3521
- pending.done = true;
3522
- pending.deferred.resolve({
3523
- hits,
3524
- engramIds: engramId ? [engramId] : [],
3525
- truncated,
3526
- tookMs
3527
- });
5404
+ if ((signal.type === SignalType.CLARIFICATION_ANSWER || signal.type === SignalType.PERMISSION_DECISION) && signal.parent_id) {
5405
+ this.recentDecisions.set(signal.parent_id, signal);
5406
+ while (this.recentDecisions.size > 256) {
5407
+ const oldest = this.recentDecisions.keys().next().value;
5408
+ this.recentDecisions.delete(oldest);
5409
+ }
5410
+ }
5411
+ if ((signal.type === SignalType.FINAL || signal.type === SignalType.ERROR) && signal.trace_id) {
5412
+ await this.cancelOpPathways(signal.trace_id);
5413
+ this.engramClient.cancelTrace(signal.trace_id);
5414
+ for (const engram of this._engrams.values()) {
5415
+ try {
5416
+ await engram.commit(signal.trace_id);
5417
+ } catch {
3528
5418
  }
3529
- } else {
3530
- pending.hitsSoFar.push(...hits);
3531
- if (engramId) pending.engrams.push(engramId);
3532
5419
  }
3533
- } else if (sig.type === SignalType.IMPRINTED) {
3534
- const pending = this.pendingImprints.get(pid);
3535
- if (pending === void 0 || pending.done) return;
3536
- pending.done = true;
3537
- pending.deferred.resolve({
3538
- engramId: typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "",
3539
- op: typeof sig.payload["op"] === "string" ? sig.payload["op"] : "",
3540
- id: typeof sig.payload["id"] === "string" ? sig.payload["id"] : null,
3541
- version: typeof sig.payload["version"] === "number" ? sig.payload["version"] : null,
3542
- tookMs: typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null,
3543
- error: typeof sig.payload["error"] === "string" ? sig.payload["error"] : null,
3544
- ok: !(typeof sig.payload["error"] === "string")
3545
- });
5420
+ this.traceAborts.delete(signal.trace_id);
5421
+ }
5422
+ if (signal.type === SignalType.TASK_AWARDED) {
5423
+ const target = signal.directed?.id ?? null;
5424
+ if (target && this._axons.has(target)) {
5425
+ const synthetic = taskSignal({
5426
+ traceId: signal.trace_id,
5427
+ parentId: signal.id,
5428
+ directed: { id: target },
5429
+ input: signal.payload["input"] ?? {},
5430
+ finalize: Boolean(signal.payload["finalize"]),
5431
+ ...signal.payload["context_ref"] !== void 0 ? { contextRef: signal.payload["context_ref"] } : {},
5432
+ meta: signal.meta
5433
+ });
5434
+ await this.onTask(synthetic);
5435
+ }
5436
+ }
5437
+ if (signal.trace_id && PATHWAY_TYPES.has(signal.type)) {
5438
+ const pathway = this.pathways.get(signal.trace_id);
5439
+ if (pathway) {
5440
+ try {
5441
+ await pathway._deliver(signal);
5442
+ } catch {
5443
+ }
5444
+ }
5445
+ }
5446
+ if (AXON_TYPES.has(signal.type) && this.registryStore !== null) {
5447
+ try {
5448
+ await this.updateRegistry(signal);
5449
+ } catch {
5450
+ }
5451
+ }
5452
+ if (signal.type === SignalType.TASK_OFFER && this.autoBid && this._axons.size > 0 && (this.handlers.get(SignalType.TASK_OFFER) ?? []).length === 0) {
5453
+ await this.maybeAutoBid(signal);
5454
+ }
5455
+ const handlers = this.handlers.get(signal.type) ?? [];
5456
+ if (handlers.length) await Promise.allSettled(handlers.map((h) => h(signal)));
5457
+ }
5458
+ // -- Engram: hosted-side handlers -----------------------------------
5459
+ /** Pick the hosted Engrams that should respond to a RECALL/IMPRINT.
5460
+ * directed.id (engramId) wins over directed.type (engramKind). */
5461
+ resolveEngramTargets(signal) {
5462
+ const eid = signal.directed?.id ?? null;
5463
+ if (eid) {
5464
+ const ent = this._engrams.get(eid);
5465
+ return ent ? [ent] : [];
5466
+ }
5467
+ const ekind = signal.directed?.type ?? null;
5468
+ if (ekind) {
5469
+ return (this.engramKindIndex.get(ekind) ?? []).map((id) => this._engrams.get(id)).filter((e) => e !== void 0);
5470
+ }
5471
+ return [];
5472
+ }
5473
+ async onRecall(signal) {
5474
+ const targets = this.resolveEngramTargets(signal);
5475
+ if (!targets.length) return;
5476
+ const query = signal.payload["query"] ?? {};
5477
+ const filters = signal.payload["filters"];
5478
+ const contextRef = signal.payload["context_ref"];
5479
+ const deadlineMs = signal.payload["deadline_ms"];
5480
+ const minConfidence = signal.payload["min_confidence"];
5481
+ for (const engram of targets) {
5482
+ let hits;
5483
+ try {
5484
+ if (!await engram.canServe(query)) continue;
5485
+ hits = await engram.recall(query, {
5486
+ ...filters !== void 0 ? { filters } : {},
5487
+ ...contextRef !== void 0 ? { contextRef } : {},
5488
+ ...deadlineMs !== void 0 ? { deadlineMs } : {},
5489
+ ...minConfidence !== void 0 ? { minConfidence } : {}
5490
+ });
5491
+ } catch {
5492
+ continue;
5493
+ }
5494
+ try {
5495
+ await this.publish(
5496
+ recalledSignal({
5497
+ traceId: signal.trace_id,
5498
+ parentId: signal.id,
5499
+ engramId: engram.engramId,
5500
+ hits: hits.map((h) => ({ id: h.id, entry: h.entry, score: h.score })),
5501
+ // Attribute the reply to the Engram that answered, not the host
5502
+ // Dendrite, so observers classify it by the Engram's REGISTER.
5503
+ directed: { id: engram.engramId, type: engram.engramKind }
5504
+ })
5505
+ );
5506
+ } catch {
5507
+ }
3546
5508
  }
3547
5509
  }
3548
- /** Cancel every in-flight recall/imprint on a trace (FINAL/ERROR or shutdown). */
3549
- cancelTrace(traceId) {
3550
- const ids = this.byTrace.get(traceId);
3551
- this.byTrace.delete(traceId);
3552
- if (ids === void 0) return;
3553
- for (const id of ids) {
3554
- const pr = this.pendingRecalls.get(id);
3555
- if (pr !== void 0 && !pr.done) {
3556
- pr.done = true;
3557
- if (pr.timer !== null) clearTimeout(pr.timer);
3558
- pr.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while recall ${id} in flight`));
3559
- this.pendingRecalls.delete(id);
5510
+ async onImprint(signal) {
5511
+ const targets = this.resolveEngramTargets(signal);
5512
+ if (!targets.length) return;
5513
+ const op = signal.payload["op"] ?? "add";
5514
+ const entry = signal.payload["entry"] ?? {};
5515
+ const mergeKey = signal.payload["merge_key"];
5516
+ for (const engram of targets) {
5517
+ let reply2;
5518
+ try {
5519
+ const receipt2 = await engram.imprint(op, entry, {
5520
+ imprintId: signal.id,
5521
+ traceId: signal.trace_id,
5522
+ ...mergeKey !== void 0 ? { mergeKey } : {}
5523
+ });
5524
+ reply2 = imprintedSignal({
5525
+ traceId: signal.trace_id,
5526
+ parentId: signal.id,
5527
+ engramId: receipt2.engramId || engram.engramId,
5528
+ op: receipt2.op,
5529
+ ...receipt2.id !== null ? { id: receipt2.id } : {},
5530
+ ...receipt2.version !== null ? { version: receipt2.version } : {},
5531
+ ...receipt2.tookMs !== null ? { tookMs: receipt2.tookMs } : {},
5532
+ ...receipt2.error !== null ? { error: receipt2.error } : {},
5533
+ directed: { id: engram.engramId, type: engram.engramKind }
5534
+ });
5535
+ } catch (err) {
5536
+ reply2 = imprintedSignal({
5537
+ traceId: signal.trace_id,
5538
+ parentId: signal.id,
5539
+ engramId: engram.engramId,
5540
+ op,
5541
+ error: `engram_exception: ${err instanceof Error ? err.message : String(err)}`,
5542
+ directed: { id: engram.engramId, type: engram.engramKind }
5543
+ });
3560
5544
  }
3561
- const pi = this.pendingImprints.get(id);
3562
- if (pi !== void 0 && !pi.done) {
3563
- pi.done = true;
3564
- if (pi.timer !== null) clearTimeout(pi.timer);
3565
- pi.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while imprint ${id} in flight`));
3566
- this.pendingImprints.delete(id);
5545
+ try {
5546
+ await this.publish(reply2);
5547
+ } catch {
3567
5548
  }
3568
5549
  }
3569
5550
  }
3570
- cancelAll() {
3571
- for (const traceId of [...this.byTrace.keys()]) this.cancelTrace(traceId);
5551
+ // -- Engram: registration (announce + learn) -------------------------
5552
+ async emitEngramRegister(engram) {
5553
+ await this.publish(
5554
+ registerSignal({
5555
+ directed: {
5556
+ id: engram.engramId,
5557
+ type: engram.engramKind,
5558
+ capabilities: [...engram.capabilities]
5559
+ },
5560
+ capabilities: engram.capabilities,
5561
+ role: "engram",
5562
+ ...engram.version !== null ? { version: engram.version } : {}
5563
+ })
5564
+ );
3572
5565
  }
3573
- onRecallDeadline(id) {
3574
- const pending = this.pendingRecalls.get(id);
3575
- if (pending === void 0 || pending.done) return;
3576
- pending.done = true;
3577
- if (pending.mode === "first") {
3578
- pending.deferred.reject(new EngramTimeout(`RECALL ${id} elapsed deadline without any responder`));
3579
- } else {
3580
- pending.deferred.resolve({
3581
- hits: [...pending.hitsSoFar].sort((a, b) => b.score - a.score),
3582
- engramIds: [...pending.engrams],
3583
- truncated: false,
3584
- tookMs: null
3585
- });
5566
+ isEngramRegister(signal) {
5567
+ if (signal.payload["role"] === "engram" || signal.payload["engram"]) return true;
5568
+ const dtype = signal.directed?.type ?? null;
5569
+ if (dtype && (this.engramKindIndex.has(dtype) || this.engramRegKindIndex.has(dtype))) {
5570
+ return true;
5571
+ }
5572
+ return false;
5573
+ }
5574
+ recordEngramRegistration(signal) {
5575
+ const d = signal.directed;
5576
+ if (!d || !d.id && !d.type) return;
5577
+ let caps = [...d.capabilities];
5578
+ if (!caps.length) {
5579
+ caps = [...signal.payload["capabilities"] ?? []];
5580
+ }
5581
+ const key = d.id ?? d.type;
5582
+ this._engramRegistrations.set(key, { id: d.id, type: d.type, capabilities: caps });
5583
+ if (d.type) {
5584
+ const bucket = this.engramRegKindIndex.get(d.type) ?? /* @__PURE__ */ new Set();
5585
+ bucket.add(key);
5586
+ this.engramRegKindIndex.set(d.type, bucket);
3586
5587
  }
3587
5588
  }
3588
- onImprintDeadline(id) {
3589
- const pending = this.pendingImprints.get(id);
3590
- if (pending === void 0 || pending.done) return;
3591
- pending.done = true;
3592
- pending.deferred.reject(new EngramTimeout(`IMPRINT ${id} elapsed deadline without IMPRINTED`));
3593
- }
3594
- track(traceId, id) {
3595
- const bucket = this.byTrace.get(traceId);
3596
- if (bucket) bucket.add(id);
3597
- else this.byTrace.set(traceId, /* @__PURE__ */ new Set([id]));
5589
+ // -- Engram: caller-side helpers -------------------------------------
5590
+ /** Resolve (traceId, parentId) for a caller-side engram op: explicit ids
5591
+ * win, then the ambient task context (bound by Axon.handleTask), then a
5592
+ * freshly minted trace (the pre-task-hydration shape). */
5593
+ static resolveTrace(traceId, parentId) {
5594
+ let tid = traceId;
5595
+ let pid = parentId;
5596
+ if (tid === void 0) {
5597
+ const amb = ambientTrace();
5598
+ if (amb !== null) {
5599
+ tid = amb[0];
5600
+ if (pid === void 0) pid = amb[1];
5601
+ }
5602
+ }
5603
+ return [tid ?? newTraceId(), pid ?? newEventId()];
3598
5604
  }
3599
- cleanupRecall(traceId, id) {
3600
- const p = this.pendingRecalls.get(id);
3601
- if (p?.timer != null) clearTimeout(p.timer);
3602
- this.pendingRecalls.delete(id);
3603
- this.discardTrace(traceId, id);
5605
+ /** Emit RECALL and await RECALLED. Trace attribution: explicit ids win,
5606
+ * then the ambient task context, then a fresh trace. */
5607
+ async recall(args) {
5608
+ const [tid, pid] = _Dendrite.resolveTrace(args.traceId, args.parentId);
5609
+ return this.engramClient.recall({
5610
+ query: args.query,
5611
+ traceId: tid,
5612
+ parentId: pid,
5613
+ ...args.engramId !== void 0 ? { engramId: args.engramId } : {},
5614
+ ...args.engramKind !== void 0 ? { engramKind: args.engramKind } : {},
5615
+ ...args.filters !== void 0 ? { filters: args.filters } : {},
5616
+ ...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
5617
+ ...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
5618
+ ...args.recallMode !== void 0 ? { recallMode: args.recallMode } : {},
5619
+ ...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
5620
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
5621
+ });
3604
5622
  }
3605
- cleanupImprint(traceId, id) {
3606
- const p = this.pendingImprints.get(id);
3607
- if (p?.timer != null) clearTimeout(p.timer);
3608
- this.pendingImprints.delete(id);
3609
- this.discardTrace(traceId, id);
5623
+ /** Emit IMPRINT. Resolves null unless `awaitAck: true`. Trace attribution
5624
+ * as {@link recall}. */
5625
+ async imprint(args) {
5626
+ const [tid, pid] = _Dendrite.resolveTrace(args.traceId, args.parentId);
5627
+ return this.engramClient.imprint({
5628
+ op: args.op,
5629
+ entry: args.entry,
5630
+ traceId: tid,
5631
+ parentId: pid,
5632
+ ...args.engramId !== void 0 ? { engramId: args.engramId } : {},
5633
+ ...args.engramKind !== void 0 ? { engramKind: args.engramKind } : {},
5634
+ ...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
5635
+ ...args.awaitAck !== void 0 ? { awaitAck: args.awaitAck } : {},
5636
+ ...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
5637
+ ...args.meta !== void 0 ? { meta: args.meta } : {}
5638
+ });
3610
5639
  }
3611
- discardTrace(traceId, id) {
3612
- const bucket = this.byTrace.get(traceId);
3613
- if (bucket === void 0) return;
3614
- bucket.delete(id);
3615
- if (bucket.size === 0) this.byTrace.delete(traceId);
5640
+ async updateRegistry(signal) {
5641
+ if (this.registryStore === null) return;
5642
+ if (signal.payload["role"] === "engram" || signal.payload["engram"]) return;
5643
+ const neuronId = signal.directed?.id ?? null;
5644
+ if (!neuronId) return;
5645
+ let reason = null;
5646
+ if (signal.type === SignalType.REGISTER) {
5647
+ await this.registryStore.upsert(
5648
+ neuronRecord({
5649
+ neuron_id: neuronId,
5650
+ capabilities: signal.payload["capabilities"] ?? [],
5651
+ version: signal.payload["version"] ?? null,
5652
+ status: "registered",
5653
+ last_heartbeat: signal.ts
5654
+ })
5655
+ );
5656
+ reason = "register";
5657
+ } else if (signal.type === SignalType.DEREGISTER) {
5658
+ await this.registryStore.markDeregistered(neuronId);
5659
+ reason = "deregister";
5660
+ } else if (signal.type === SignalType.HEARTBEAT) {
5661
+ const status = signal.payload["status"];
5662
+ if (status) await this.registryStore.touchHeartbeat(neuronId, signal.ts, status);
5663
+ else await this.registryStore.touchHeartbeat(neuronId, signal.ts);
5664
+ reason = "heartbeat";
5665
+ }
5666
+ if (reason !== null) {
5667
+ await this.hooks._fireRefresh({ reason, neuronId, extra: {} });
5668
+ }
3616
5669
  }
3617
5670
  };
3618
- function hitsFromPayload(raw) {
3619
- if (!Array.isArray(raw)) return [];
3620
- const out = [];
3621
- for (const h of raw) {
3622
- if (h === null || typeof h !== "object") continue;
3623
- const obj = h;
3624
- const entryVal = obj["entry"];
3625
- out.push({
3626
- id: typeof obj["id"] === "string" ? obj["id"] : "",
3627
- entry: entryVal !== null && typeof entryVal === "object" && !Array.isArray(entryVal) ? entryVal : { value: entryVal },
3628
- score: typeof obj["score"] === "number" ? obj["score"] : 1
3629
- });
3630
- }
3631
- return out;
3632
- }
5671
+ var Cortex = Dendrite;
3633
5672
 
3634
5673
  // src/engram-sqlite.ts
3635
5674
  var SCHEMA3 = `
@@ -4167,11 +6206,12 @@ var PostgresEngram = class extends Engram {
4167
6206
  };
4168
6207
 
4169
6208
  // src/index.ts
4170
- var VERSION = true ? "0.1.2" : "0.0.0-dev";
6209
+ var VERSION = true ? "0.1.4" : "0.0.0-dev";
4171
6210
  // Annotate the CommonJS export names for ESM import in node:
4172
6211
  0 && (module.exports = {
4173
6212
  AXON_TYPES,
4174
6213
  Axon,
6214
+ COSMO_INTENT_SYSTEM_PROMPT,
4175
6215
  Cortex,
4176
6216
  CortexProtocolError,
4177
6217
  Dendrite,
@@ -4192,14 +6232,19 @@ var VERSION = true ? "0.1.2" : "0.0.0-dev";
4192
6232
  MemoryRegistryStore,
4193
6233
  MemorySynapse,
4194
6234
  NatsSynapse,
6235
+ PATHWAY_TYPES,
6236
+ Pathway,
6237
+ PathwayClosedError,
4195
6238
  PostgresEngram,
4196
6239
  PostgresRegistryStore,
4197
6240
  SYNAPSE_TYPES,
4198
6241
  SignalType,
4199
6242
  SqliteEngram,
4200
6243
  SqliteRegistryStore,
6244
+ TERMINAL_TYPES,
4201
6245
  VERSION,
4202
6246
  agentOutputSignal,
6247
+ ambientTrace,
4203
6248
  anthropicNeuron,
4204
6249
  bidSignal,
4205
6250
  clarificationAnswerSignal,
@@ -4212,6 +6257,7 @@ var VERSION = true ? "0.1.2" : "0.0.0-dev";
4212
6257
  critiqueSignal,
4213
6258
  decode,
4214
6259
  deepMerge,
6260
+ defaultRetryOn,
4215
6261
  deregisterSignal,
4216
6262
  directedTo,
4217
6263
  discoverSignal,
@@ -4220,6 +6266,7 @@ var VERSION = true ? "0.1.2" : "0.0.0-dev";
4220
6266
  errorSignal,
4221
6267
  escalationSignal,
4222
6268
  finalSignal,
6269
+ followupPrompt,
4223
6270
  heartbeatSignal,
4224
6271
  huggingFaceNeuron,
4225
6272
  imprintSignal,
@@ -4247,8 +6294,13 @@ var VERSION = true ? "0.1.2" : "0.0.0-dev";
4247
6294
  recalledSignal,
4248
6295
  registerSignal,
4249
6296
  reply,
6297
+ runWithTraceContext,
4250
6298
  standardMcpServers,
6299
+ stopSignal,
6300
+ stoppedSignal,
4251
6301
  synapseFromUrl,
6302
+ taskAwardedSignal,
6303
+ taskDeclinedSignal,
4252
6304
  taskOfferSignal,
4253
6305
  taskSignal,
4254
6306
  thoughtDeltaSignal,