@cosmonapse/sdk 0.1.3 → 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
@@ -78,6 +78,7 @@ __export(index_exports, {
78
78
  critiqueSignal: () => critiqueSignal,
79
79
  decode: () => decode,
80
80
  deepMerge: () => deepMerge,
81
+ defaultRetryOn: () => defaultRetryOn,
81
82
  deregisterSignal: () => deregisterSignal,
82
83
  directedTo: () => directedTo,
83
84
  discoverSignal: () => discoverSignal,
@@ -116,6 +117,8 @@ __export(index_exports, {
116
117
  reply: () => reply,
117
118
  runWithTraceContext: () => runWithTraceContext,
118
119
  standardMcpServers: () => standardMcpServers,
120
+ stopSignal: () => stopSignal,
121
+ stoppedSignal: () => stoppedSignal,
119
122
  synapseFromUrl: () => synapseFromUrl,
120
123
  taskAwardedSignal: () => taskAwardedSignal,
121
124
  taskDeclinedSignal: () => taskDeclinedSignal,
@@ -185,7 +188,13 @@ var SignalType = {
185
188
  IMPRINT: "IMPRINT",
186
189
  IMPRINTED: "IMPRINTED",
187
190
  // Discovery [C]
188
- 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"
189
198
  };
190
199
  var AXON_TYPES = /* @__PURE__ */ new Set([
191
200
  SignalType.AGENT_OUTPUT,
@@ -222,7 +231,11 @@ var SYNAPSE_TYPES = /* @__PURE__ */ new Set([
222
231
  SignalType.RECALL,
223
232
  SignalType.RECALLED,
224
233
  SignalType.IMPRINT,
225
- 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
226
239
  ]);
227
240
  function normalizeDirected(d) {
228
241
  if (d === null || d === void 0) return null;
@@ -684,6 +697,237 @@ function imprintedSignal(args) {
684
697
  meta: args.meta ?? {}
685
698
  });
686
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
+ }
687
931
 
688
932
  // src/synapse.ts
689
933
  var MemorySubscription = class {
@@ -2041,6 +2285,49 @@ var EngramOverloaded = class extends EngramError {
2041
2285
  };
2042
2286
  var Engram = class {
2043
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);
2304
+ }
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 {
2322
+ }
2323
+ }
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
+ }
2044
2331
  /** Return false if this Engram cannot satisfy the query. Default: serve all. */
2045
2332
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
2046
2333
  async canServe(_query) {
@@ -2139,6 +2426,7 @@ var InMemoryEngram = class extends Engram {
2139
2426
  async imprint(op, entry, opts = {}) {
2140
2427
  const t0 = Date.now();
2141
2428
  const mergeKey = opts.mergeKey ?? null;
2429
+ const traceId = opts.traceId;
2142
2430
  const tookMs = () => Date.now() - t0;
2143
2431
  if (opts.imprintId !== void 0) {
2144
2432
  const seen = this.imprintSeen.get(opts.imprintId);
@@ -2161,6 +2449,7 @@ var InMemoryEngram = class extends Engram {
2161
2449
  this.store(ent);
2162
2450
  resultingId = ent.id;
2163
2451
  version = ent.version;
2452
+ this.sagaRecord(traceId, "delete", { id: ent.id });
2164
2453
  } else if (op === "append") {
2165
2454
  let ent = this.makeEntry(entry, mergeKey);
2166
2455
  while (this.entries.has(ent.id)) {
@@ -2169,11 +2458,18 @@ var InMemoryEngram = class extends Engram {
2169
2458
  this.store(ent);
2170
2459
  resultingId = ent.id;
2171
2460
  version = ent.version;
2461
+ this.sagaRecord(traceId, "delete", { id: ent.id });
2172
2462
  } else if (op === "upsert") {
2173
2463
  const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
2174
2464
  const targetId = existingIds[existingIds.length - 1];
2175
2465
  const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
2176
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
+ );
2177
2473
  const next = this.makeEntry({ ...entry, id: old.id }, mergeKey);
2178
2474
  next.createdAt = old.createdAt;
2179
2475
  next.version = old.version + 1;
@@ -2185,6 +2481,7 @@ var InMemoryEngram = class extends Engram {
2185
2481
  this.store(ent);
2186
2482
  resultingId = ent.id;
2187
2483
  version = ent.version;
2484
+ this.sagaRecord(traceId, "delete", { id: ent.id });
2188
2485
  }
2189
2486
  } else if (op === "merge") {
2190
2487
  const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
@@ -2193,6 +2490,12 @@ var InMemoryEngram = class extends Engram {
2193
2490
  if (old === void 0) {
2194
2491
  return receipt(this.engramId, op, { error: `no entry for merge_key='${mergeKey}'`, tookMs: tookMs() });
2195
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
+ );
2196
2499
  const now = (/* @__PURE__ */ new Date()).toISOString();
2197
2500
  const next = {
2198
2501
  id: old.id,
@@ -2219,6 +2522,13 @@ var InMemoryEngram = class extends Engram {
2219
2522
  if (targetId === null || !this.entries.has(targetId)) {
2220
2523
  return receipt(this.engramId, op, { tookMs: tookMs() });
2221
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
+ );
2222
2532
  this.evict(targetId);
2223
2533
  resultingId = targetId;
2224
2534
  version = null;
@@ -3125,201 +3435,6 @@ function parseMcpIntents(raw) {
3125
3435
  return raw;
3126
3436
  }
3127
3437
 
3128
- // src/pathway.ts
3129
- var TERMINAL_TYPES = /* @__PURE__ */ new Set([
3130
- SignalType.FINAL,
3131
- SignalType.ERROR
3132
- ]);
3133
- var WAIT_TYPES = /* @__PURE__ */ new Set([
3134
- SignalType.AGENT_OUTPUT,
3135
- SignalType.CLARIFICATION,
3136
- SignalType.PERMISSION,
3137
- SignalType.ERROR,
3138
- SignalType.FINAL
3139
- ]);
3140
- var SCOPE_TERMINAL_TYPES = /* @__PURE__ */ new Set([
3141
- SignalType.FINAL,
3142
- SignalType.ERROR,
3143
- SignalType.CLARIFICATION,
3144
- SignalType.PERMISSION
3145
- ]);
3146
- var PATHWAY_TYPES = new Set(
3147
- Object.values(SignalType).filter(
3148
- (t) => t !== SignalType.TASK && t !== SignalType.REGISTER && t !== SignalType.DEREGISTER && t !== SignalType.HEARTBEAT && t !== SignalType.DISCOVER
3149
- )
3150
- );
3151
- var PathwayClosedError = class extends Error {
3152
- constructor(message) {
3153
- super(message);
3154
- this.name = "PathwayClosedError";
3155
- }
3156
- };
3157
- var Pathway = class {
3158
- traceId;
3159
- parentId;
3160
- role;
3161
- scope;
3162
- scopeFilter;
3163
- onCloseHook;
3164
- handlers = /* @__PURE__ */ new Map();
3165
- waiters = [];
3166
- buffered = [];
3167
- closed_ = false;
3168
- // Async iteration: a pull queue of pending `next()` resolvers and a push
3169
- // queue of undelivered values. `null` is the close sentinel.
3170
- iterPush = [];
3171
- iterPull = [];
3172
- constructor(opts) {
3173
- const scope = opts.scope ?? "all";
3174
- if (scope !== "all" && scope !== "terminal") {
3175
- throw new Error(`scope must be 'all' or 'terminal', got '${scope}'`);
3176
- }
3177
- this.traceId = opts.traceId;
3178
- this.parentId = opts.parentId ?? null;
3179
- this.role = opts.role ?? "originator";
3180
- this.scope = scope;
3181
- this.scopeFilter = scope === "terminal" ? SCOPE_TERMINAL_TYPES : null;
3182
- this.onCloseHook = opts.onClose;
3183
- }
3184
- get closed() {
3185
- return this.closed_;
3186
- }
3187
- // -- consumer shape #1: wait ---------------------------------------
3188
- /** Resolve on the next AGENT_OUTPUT, CLARIFICATION, PERMISSION, ERROR or
3189
- * FINAL. Rejects with PathwayClosedError if the Pathway closes first, and
3190
- * with a TimeoutError-named Error if `timeoutMs` elapses. */
3191
- async wait(timeoutMs) {
3192
- return this.waitForTypes(WAIT_TYPES, timeoutMs);
3193
- }
3194
- /** Resolve on the next Signal of the given type. */
3195
- async waitFor(type, timeoutMs) {
3196
- return this.waitForTypes(/* @__PURE__ */ new Set([type]), timeoutMs);
3197
- }
3198
- async waitForTypes(types, timeoutMs) {
3199
- for (let i = 0; i < this.buffered.length; i++) {
3200
- const sig = this.buffered[i];
3201
- if (types.has(sig.type)) {
3202
- this.buffered.splice(i, 1);
3203
- return sig;
3204
- }
3205
- }
3206
- if (this.closed_) {
3207
- throw new PathwayClosedError(`Pathway for trace '${this.traceId}' is closed`);
3208
- }
3209
- return new Promise((resolve, reject) => {
3210
- const waiter = { types, resolve, reject, settled: false };
3211
- let timer = null;
3212
- const settle = (fn) => (a) => {
3213
- if (waiter.settled) return;
3214
- waiter.settled = true;
3215
- if (timer !== null) clearTimeout(timer);
3216
- this.waiters = this.waiters.filter((w) => w !== waiter);
3217
- fn(a);
3218
- };
3219
- waiter.resolve = settle(resolve);
3220
- waiter.reject = settle(reject);
3221
- if (timeoutMs !== void 0) {
3222
- timer = setTimeout(() => {
3223
- const err = new Error(
3224
- `Pathway.wait timed out after ${timeoutMs}ms on trace '${this.traceId}'`
3225
- );
3226
- err.name = "TimeoutError";
3227
- waiter.reject(err);
3228
- }, timeoutMs);
3229
- }
3230
- this.waiters.push(waiter);
3231
- });
3232
- }
3233
- // -- consumer shape #2: callbacks ----------------------------------
3234
- /** Register a callback fired for each Signal of the given type. */
3235
- on(type, fn) {
3236
- const list = this.handlers.get(type) ?? [];
3237
- list.push(fn);
3238
- this.handlers.set(type, list);
3239
- return fn;
3240
- }
3241
- // -- consumer shape #3: async iteration ----------------------------
3242
- [Symbol.asyncIterator]() {
3243
- return {
3244
- next: () => {
3245
- if (this.iterPush.length > 0) {
3246
- const v = this.iterPush.shift();
3247
- return Promise.resolve(
3248
- v === null ? { value: void 0, done: true } : { value: v, done: false }
3249
- );
3250
- }
3251
- if (this.closed_) {
3252
- return Promise.resolve({ value: void 0, done: true });
3253
- }
3254
- return new Promise((resolve) => this.iterPull.push(resolve));
3255
- }
3256
- };
3257
- }
3258
- iterEmit(v) {
3259
- const pull = this.iterPull.shift();
3260
- if (pull) {
3261
- pull(v === null ? { value: void 0, done: true } : { value: v, done: false });
3262
- } else {
3263
- this.iterPush.push(v);
3264
- }
3265
- }
3266
- // -- lifecycle ------------------------------------------------------
3267
- /** Close the Pathway. Idempotent. Pending waits reject with
3268
- * PathwayClosedError; iteration completes; the onClose hook fires once. */
3269
- async close() {
3270
- if (this.closed_) return;
3271
- this.closed_ = true;
3272
- for (const w of [...this.waiters]) {
3273
- w.reject(
3274
- new PathwayClosedError(
3275
- `Pathway for trace '${this.traceId}' closed before a matching Signal arrived`
3276
- )
3277
- );
3278
- }
3279
- this.waiters = [];
3280
- this.iterEmit(null);
3281
- if (this.onCloseHook) {
3282
- try {
3283
- await this.onCloseHook(this);
3284
- } catch {
3285
- }
3286
- }
3287
- }
3288
- /** `await using pathway = ...` support. */
3289
- async [Symbol.asyncDispose]() {
3290
- await this.close();
3291
- }
3292
- // -- internal: signal delivery (called by the owning Dendrite) ------
3293
- /** @internal */
3294
- async _deliver(signal) {
3295
- if (this.closed_) return;
3296
- if (this.scopeFilter !== null && !this.scopeFilter.has(signal.type)) {
3297
- await this.fireHandlers(signal);
3298
- if (TERMINAL_TYPES.has(signal.type)) await this.close();
3299
- return;
3300
- }
3301
- let consumed = false;
3302
- for (const w of [...this.waiters]) {
3303
- if (w.types.has(signal.type)) {
3304
- w.resolve(signal);
3305
- consumed = true;
3306
- }
3307
- }
3308
- if (!consumed) this.buffered.push(signal);
3309
- await this.fireHandlers(signal);
3310
- this.iterEmit(signal);
3311
- if (TERMINAL_TYPES.has(signal.type)) await this.close();
3312
- }
3313
- async fireHandlers(signal) {
3314
- for (const h of this.handlers.get(signal.type) ?? []) {
3315
- try {
3316
- await h(signal);
3317
- } catch {
3318
- }
3319
- }
3320
- }
3321
- };
3322
-
3323
3438
  // src/engram-client.ts
3324
3439
  function deferred() {
3325
3440
  let resolve;
@@ -3579,6 +3694,11 @@ var Dendrite = class _Dendrite {
3579
3694
  /** Hosted Engrams keyed by engramId, plus a kind index so RECALL/IMPRINT
3580
3695
  * addressed by engramKind reach every matching host. */
3581
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();
3582
3702
  engramKindIndex = /* @__PURE__ */ new Map();
3583
3703
  /** Engrams learned from peer REGISTER signals (possibly out-of-process). */
3584
3704
  _engramRegistrations = /* @__PURE__ */ new Map();
@@ -4032,6 +4152,7 @@ var Dendrite = class _Dendrite {
4032
4152
  }
4033
4153
  await this.ensureInboundSub(SignalType.TASK_AWARDED);
4034
4154
  await this.ensureInboundSub(SignalType.DISCOVER);
4155
+ await this.ensureInboundSub(SignalType.STOP);
4035
4156
  if (this.autoBid) await this.ensureInboundSub(SignalType.TASK_OFFER);
4036
4157
  for (const axon of this._axons.values()) {
4037
4158
  await this.mirrorToStore(axon, "registered");
@@ -4047,6 +4168,8 @@ var Dendrite = class _Dendrite {
4047
4168
  }
4048
4169
  await this.ensureInboundSub(SignalType.RECALL);
4049
4170
  await this.ensureInboundSub(SignalType.IMPRINT);
4171
+ await this.ensureInboundSub(SignalType.FINAL);
4172
+ await this.ensureInboundSub(SignalType.ERROR);
4050
4173
  await this.ensureInboundSub(SignalType.REGISTER);
4051
4174
  for (const engram of this._engrams.values()) {
4052
4175
  try {
@@ -4065,6 +4188,7 @@ var Dendrite = class _Dendrite {
4065
4188
  await this.ensureInboundSub(t);
4066
4189
  }
4067
4190
  }
4191
+ await this.ensureInboundSub(SignalType.STOP);
4068
4192
  this.running = true;
4069
4193
  if (this._axons.size > 0 && this.heartbeatMs > 0) {
4070
4194
  this.startHeartbeatLoop();
@@ -4292,7 +4416,10 @@ var Dendrite = class _Dendrite {
4292
4416
  * Pathway, return the Signal. Use `scope: "terminal"` to wait only for
4293
4417
  * FINAL / ERROR / CLARIFICATION / PERMISSION. */
4294
4418
  async dispatchAndWait(args) {
4295
- const { timeoutMs, ...rest } = args;
4419
+ const { timeoutMs, retry, ...rest } = args;
4420
+ if (retry) {
4421
+ return this.runWithRetry({ ...rest, retry, ...timeoutMs !== void 0 ? { timeoutMs } : {} });
4422
+ }
4296
4423
  const pathway = await this.dispatch(rest);
4297
4424
  try {
4298
4425
  return await pathway.wait(timeoutMs ?? 3e4);
@@ -4891,9 +5018,11 @@ var Dendrite = class _Dendrite {
4891
5018
  if (!axon) return;
4892
5019
  target = axon.neuronId;
4893
5020
  }
5021
+ const ac = new AbortController();
5022
+ this.registerTraceAbort(task.trace_id, ac);
4894
5023
  let reply2;
4895
5024
  try {
4896
- reply2 = await axon.handleTask(task);
5025
+ reply2 = await this.raceAbort(axon.handleTask(task), ac.signal);
4897
5026
  } catch (err) {
4898
5027
  reply2 = errorSignal({
4899
5028
  traceId: task.trace_id,
@@ -4903,6 +5032,11 @@ var Dendrite = class _Dendrite {
4903
5032
  message: err instanceof Error ? err.message : String(err),
4904
5033
  recoverable: false
4905
5034
  });
5035
+ } finally {
5036
+ this.unregisterTraceAbort(task.trace_id, ac);
5037
+ }
5038
+ if (reply2 === null) {
5039
+ return;
4906
5040
  }
4907
5041
  await this.publish(reply2);
4908
5042
  if (reply2.type === SignalType.AGENT_OUTPUT && task.payload["finalize"]) {
@@ -4919,6 +5053,200 @@ var Dendrite = class _Dendrite {
4919
5053
  }
4920
5054
  }
4921
5055
  }
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);
5062
+ }
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
+ }
5104
+ }
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 {
5125
+ }
5126
+ }
5127
+ const pw = this.pathways.get(traceId);
5128
+ if (pw && !pw.closed) {
5129
+ didWork = true;
5130
+ try {
5131
+ await pw.close();
5132
+ } catch {
5133
+ }
5134
+ }
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
+ }
5149
+ }
5150
+ }
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;
5163
+ }
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);
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;
5195
+ }
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();
5226
+ }
5227
+ } catch (err) {
5228
+ outcome = err instanceof Error ? err : new Error(String(err));
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));
5247
+ }
5248
+ throw new Error("runWithRetry: exhausted attempts unexpectedly");
5249
+ }
4922
5250
  async emitRegister(axon) {
4923
5251
  await this.publish(
4924
5252
  registerSignal({
@@ -5042,6 +5370,21 @@ var Dendrite = class _Dendrite {
5042
5370
  if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
5043
5371
  return;
5044
5372
  }
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;
5387
+ }
5045
5388
  if (signal.type === SignalType.RECALLED || signal.type === SignalType.IMPRINTED) {
5046
5389
  this.engramClient.deliver(signal);
5047
5390
  }
@@ -5068,6 +5411,13 @@ var Dendrite = class _Dendrite {
5068
5411
  if ((signal.type === SignalType.FINAL || signal.type === SignalType.ERROR) && signal.trace_id) {
5069
5412
  await this.cancelOpPathways(signal.trace_id);
5070
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 {
5418
+ }
5419
+ }
5420
+ this.traceAborts.delete(signal.trace_id);
5071
5421
  }
5072
5422
  if (signal.type === SignalType.TASK_AWARDED) {
5073
5423
  const target = signal.directed?.id ?? null;
@@ -5168,6 +5518,7 @@ var Dendrite = class _Dendrite {
5168
5518
  try {
5169
5519
  const receipt2 = await engram.imprint(op, entry, {
5170
5520
  imprintId: signal.id,
5521
+ traceId: signal.trace_id,
5171
5522
  ...mergeKey !== void 0 ? { mergeKey } : {}
5172
5523
  });
5173
5524
  reply2 = imprintedSignal({
@@ -5855,7 +6206,7 @@ var PostgresEngram = class extends Engram {
5855
6206
  };
5856
6207
 
5857
6208
  // src/index.ts
5858
- var VERSION = true ? "0.1.3" : "0.0.0-dev";
6209
+ var VERSION = true ? "0.1.4" : "0.0.0-dev";
5859
6210
  // Annotate the CommonJS export names for ESM import in node:
5860
6211
  0 && (module.exports = {
5861
6212
  AXON_TYPES,
@@ -5906,6 +6257,7 @@ var VERSION = true ? "0.1.3" : "0.0.0-dev";
5906
6257
  critiqueSignal,
5907
6258
  decode,
5908
6259
  deepMerge,
6260
+ defaultRetryOn,
5909
6261
  deregisterSignal,
5910
6262
  directedTo,
5911
6263
  discoverSignal,
@@ -5944,6 +6296,8 @@ var VERSION = true ? "0.1.3" : "0.0.0-dev";
5944
6296
  reply,
5945
6297
  runWithTraceContext,
5946
6298
  standardMcpServers,
6299
+ stopSignal,
6300
+ stoppedSignal,
5947
6301
  synapseFromUrl,
5948
6302
  taskAwardedSignal,
5949
6303
  taskDeclinedSignal,