@almadar/runtime 3.1.1 → 3.1.3

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.
@@ -96,6 +96,11 @@ declare function findInitialState(trait: TraitDefinition): string;
96
96
  declare function createInitialTraitState(trait: TraitDefinition): TraitState;
97
97
  /**
98
98
  * Find a matching transition from the current state for the given event.
99
+ *
100
+ * **Guard-unaware**: returns the first transition matching `from` /
101
+ * `event` regardless of any attached guard. Use `findMatchingTransitions`
102
+ * when multiple sibling transitions share the same `from`/`event` and
103
+ * disambiguate by guard.
99
104
  */
100
105
  declare function findTransition(trait: TraitDefinition, currentState: string, eventKey: string): TraitDefinition['transitions'][0] | undefined;
101
106
  /**
@@ -164,6 +169,19 @@ interface ProcessEventOptions {
164
169
  declare function processEvent(options: ProcessEventOptions): TransitionResult;
165
170
  declare class StateMachineManager {
166
171
  private traits;
172
+ /**
173
+ * State map keyed by `${traitName}::${entityId | __singleton__}`.
174
+ *
175
+ * Each entity instance of an orbital gets its own copy of every
176
+ * trait's state machine. This is what enables N parallel subagents
177
+ * (one OrbitalProcess row per dispatched orbital) to all run their
178
+ * own Build/Validate/Fix cycle without colliding on shared trait
179
+ * state — the original cause of GAP-AGB-2.
180
+ *
181
+ * Initial state is created lazily on first event for each scope, so
182
+ * adding a trait does not pre-allocate state for every potential
183
+ * entity (and there's no need to know entity ids up front).
184
+ */
167
185
  private states;
168
186
  private config;
169
187
  private observer?;
@@ -184,18 +202,35 @@ declare class StateMachineManager {
184
202
  * Remove a trait from the manager.
185
203
  */
186
204
  removeTrait(traitName: string): void;
205
+ /**
206
+ * Get-or-init the state row for a (trait, entityId) pair.
207
+ * Returns undefined if the trait isn't registered.
208
+ */
209
+ private getOrInitState;
187
210
  /**
188
211
  * Get current state for a trait.
212
+ *
213
+ * For single-entity orbitals, omit `entityId` — returns the singleton
214
+ * scope. For multi-entity orbitals (e.g. agent OrbitalSubagent), pass
215
+ * the entity row id to look up that specific instance's state.
189
216
  */
190
- getState(traitName: string): TraitState | undefined;
217
+ getState(traitName: string, entityId?: string): TraitState | undefined;
191
218
  /**
192
- * Get all current states.
219
+ * Get a flat traitName→state map. For multi-entity orbitals this
220
+ * returns one entry per trait collapsed onto the singleton scope (or
221
+ * the first observed scope if singleton is empty). Use
222
+ * `getAllStatesByScope()` to get the full per-entity view.
193
223
  */
194
224
  getAllStates(): Map<string, TraitState>;
225
+ /**
226
+ * Get every per-entity state row, grouped by trait. Useful for
227
+ * inspecting parallel subagent state.
228
+ */
229
+ getAllStatesByScope(): Map<string, Map<string, TraitState>>;
195
230
  /**
196
231
  * Check if a trait can handle an event from its current state.
197
232
  */
198
- canHandleEvent(traitName: string, eventKey: string): boolean;
233
+ canHandleEvent(traitName: string, eventKey: string, entityId?: string): boolean;
199
234
  /**
200
235
  * Send an event to all traits.
201
236
  *
@@ -214,31 +249,28 @@ declare class StateMachineManager {
214
249
  */
215
250
  enqueueEvent(eventKey: string, payload?: EventPayload, entityData?: EntityRow): void;
216
251
  /**
217
- * Drain a single trait's event queue, processing events sequentially.
218
- *
219
- * This is the core actor loop: each event is fully processed (including
220
- * awaiting all effects) before the next event is dequeued. If the queue
221
- * is already being drained for this trait, this call is a no-op (the
222
- * running drain will pick up newly enqueued events).
223
- *
224
- * @param traitName - Which trait's queue to drain
225
- * @param executeEffects - Async callback to run effects for a successful transition
252
+ * Drain a single (trait, entity) pair's event queue, processing
253
+ * events sequentially. Pass `entityId` to drain a specific entity
254
+ * instance; omit it to drain the singleton scope.
226
255
  */
227
- drainQueue(traitName: string, executeEffects: (traitName: string, result: TransitionResult, payload?: EventPayload) => Promise<void>): Promise<void>;
256
+ drainQueue(traitName: string, executeEffects: (traitName: string, result: TransitionResult, payload?: EventPayload) => Promise<void>, entityId?: string): Promise<void>;
228
257
  /**
229
258
  * Check whether a trait's queue is currently being drained.
230
259
  */
231
- isProcessing(traitName: string): boolean;
260
+ isProcessing(traitName: string, entityId?: string): boolean;
232
261
  /**
233
262
  * Get the number of pending events in a trait's queue.
234
263
  */
235
- getQueueLength(traitName: string): number;
264
+ getQueueLength(traitName: string, entityId?: string): number;
236
265
  /**
237
266
  * Reset a trait to its initial state.
267
+ *
268
+ * If `entityId` is provided, resets only that entity's scope.
269
+ * Otherwise resets every entity scope known for the trait.
238
270
  */
239
- resetTrait(traitName: string): void;
271
+ resetTrait(traitName: string, entityId?: string): void;
240
272
  /**
241
- * Reset all traits to initial states.
273
+ * Reset all traits to initial states (every entity scope).
242
274
  */
243
275
  resetAll(): void;
244
276
  }
@@ -1,4 +1,4 @@
1
1
  import 'express';
2
- export { v as EffectResult, L as LoaderConfig, w as LocalPersistenceAdapter, O as OrbitalEventRequest, c as OrbitalEventResponse, x as OrbitalServerRuntime, d as OrbitalServerRuntimeConfig, P as PersistenceAdapter, R as RegisteredOrbital, i as RuntimeOrbital, j as RuntimeOrbitalSchema, k as RuntimeTrait, y as RuntimeTraitTick, z as createOrbitalServerRuntime } from './OrbitalServerRuntime-QXgOGiS8.js';
2
+ export { v as EffectResult, L as LoaderConfig, w as LocalPersistenceAdapter, O as OrbitalEventRequest, c as OrbitalEventResponse, x as OrbitalServerRuntime, d as OrbitalServerRuntimeConfig, P as PersistenceAdapter, R as RegisteredOrbital, i as RuntimeOrbital, j as RuntimeOrbitalSchema, k as RuntimeTrait, y as RuntimeTraitTick, z as createOrbitalServerRuntime } from './OrbitalServerRuntime-DWJYcIJG.js';
3
3
  import './types-CM6txTNy.js';
4
4
  import '@almadar/core';
@@ -1,4 +1,4 @@
1
- import { EventBus, createUnifiedLoader, preprocessSchema, StateMachineManager, createContextFromBindings, EffectExecutor } from './chunk-GU35X5AW.js';
1
+ import { EventBus, createUnifiedLoader, preprocessSchema, StateMachineManager, createContextFromBindings, EffectExecutor } from './chunk-QQIRPNHH.js';
2
2
  import './chunk-PZ5AY32C.js';
3
3
  import { Router } from 'express';
4
4
  import * as fs from 'fs';
@@ -304,17 +304,20 @@ function createInitialTraitState(trait) {
304
304
  context: {}
305
305
  };
306
306
  }
307
- function findTransition(trait, currentState, eventKey) {
307
+ function findMatchingTransitions(trait, currentState, eventKey) {
308
308
  if (!trait.transitions || trait.transitions.length === 0) {
309
- return void 0;
309
+ return [];
310
310
  }
311
- return trait.transitions.find((t) => {
311
+ return trait.transitions.filter((t) => {
312
312
  if (Array.isArray(t.from)) {
313
313
  return t.from.includes(currentState) && t.event === eventKey;
314
314
  }
315
315
  return t.from === currentState && t.event === eventKey;
316
316
  });
317
317
  }
318
+ function findTransition(trait, currentState, eventKey) {
319
+ return findMatchingTransitions(trait, currentState, eventKey)[0];
320
+ }
318
321
  function normalizeEventKey(eventKey) {
319
322
  if (!eventKey) return "";
320
323
  return eventKey.startsWith("UI:") ? eventKey.slice(3) : eventKey;
@@ -331,8 +334,8 @@ function processEvent(options) {
331
334
  contextExtensions
332
335
  } = options;
333
336
  const normalizedEvent = normalizeEventKey(eventKey);
334
- const transition = findTransition(trait, traitState.currentState, normalizedEvent);
335
- if (!transition) {
337
+ const candidates = findMatchingTransitions(trait, traitState.currentState, normalizedEvent);
338
+ if (candidates.length === 0) {
336
339
  smLog.debug("noTransition", { trait: trait.name, event: normalizedEvent, currentState: traitState.currentState });
337
340
  return {
338
341
  executed: false,
@@ -341,8 +344,22 @@ function processEvent(options) {
341
344
  effects: []
342
345
  };
343
346
  }
344
- smLog.debug("processEvent", { trait: trait.name, event: normalizedEvent, currentState: traitState.currentState, to: transition.to });
345
- if (transition.guard) {
347
+ let lastFailedGuardTransition;
348
+ for (const transition of candidates) {
349
+ smLog.debug("processEvent", { trait: trait.name, event: normalizedEvent, currentState: traitState.currentState, to: transition.to });
350
+ if (!transition.guard) {
351
+ return {
352
+ executed: true,
353
+ newState: transition.to,
354
+ previousState: traitState.currentState,
355
+ effects: transition.effects || [],
356
+ transition: {
357
+ from: traitState.currentState,
358
+ to: transition.to,
359
+ event: normalizedEvent
360
+ }
361
+ };
362
+ }
346
363
  const ctx = createContextFromBindings({
347
364
  entity: entityData,
348
365
  payload,
@@ -354,20 +371,21 @@ function processEvent(options) {
354
371
  ctx
355
372
  );
356
373
  smLog.debug("guard:evaluate", { trait: trait.name, event: normalizedEvent, guardResult: guardPasses });
357
- if (!guardPasses) {
374
+ if (guardPasses) {
358
375
  return {
359
- executed: false,
360
- newState: traitState.currentState,
376
+ executed: true,
377
+ newState: transition.to,
361
378
  previousState: traitState.currentState,
362
- effects: [],
379
+ effects: transition.effects || [],
363
380
  transition: {
364
381
  from: traitState.currentState,
365
382
  to: transition.to,
366
383
  event: normalizedEvent
367
384
  },
368
- guardResult: false
385
+ guardResult: true
369
386
  };
370
387
  }
388
+ lastFailedGuardTransition = transition;
371
389
  } catch (error) {
372
390
  if (guardMode === "strict") {
373
391
  console.error(
@@ -388,27 +406,62 @@ function processEvent(options) {
388
406
  };
389
407
  }
390
408
  console.error("[StateMachineCore] Guard evaluation error:", error);
409
+ return {
410
+ executed: true,
411
+ newState: transition.to,
412
+ previousState: traitState.currentState,
413
+ effects: transition.effects || [],
414
+ transition: {
415
+ from: traitState.currentState,
416
+ to: transition.to,
417
+ event: normalizedEvent
418
+ },
419
+ guardResult: true
420
+ };
391
421
  }
392
422
  }
393
423
  return {
394
- executed: true,
395
- newState: transition.to,
424
+ executed: false,
425
+ newState: traitState.currentState,
396
426
  previousState: traitState.currentState,
397
- effects: transition.effects || [],
398
- transition: {
427
+ effects: [],
428
+ transition: lastFailedGuardTransition ? {
399
429
  from: traitState.currentState,
400
- to: transition.to,
430
+ to: lastFailedGuardTransition.to,
401
431
  event: normalizedEvent
402
- },
403
- guardResult: transition.guard ? true : void 0
432
+ } : void 0,
433
+ guardResult: false
404
434
  };
405
435
  }
436
+ var SINGLETON_SCOPE = "__singleton__";
437
+ function scopeOf(entityData) {
438
+ const id = entityData && entityData["id"];
439
+ return id ?? SINGLETON_SCOPE;
440
+ }
441
+ function compositeKey(traitName, scope) {
442
+ return `${traitName}::${scope}`;
443
+ }
406
444
  var StateMachineManager = class {
407
445
  traits = /* @__PURE__ */ new Map();
446
+ /**
447
+ * State map keyed by `${traitName}::${entityId | __singleton__}`.
448
+ *
449
+ * Each entity instance of an orbital gets its own copy of every
450
+ * trait's state machine. This is what enables N parallel subagents
451
+ * (one OrbitalProcess row per dispatched orbital) to all run their
452
+ * own Build/Validate/Fix cycle without colliding on shared trait
453
+ * state — the original cause of GAP-AGB-2.
454
+ *
455
+ * Initial state is created lazily on first event for each scope, so
456
+ * adding a trait does not pre-allocate state for every potential
457
+ * entity (and there's no need to know entity ids up front).
458
+ */
408
459
  states = /* @__PURE__ */ new Map();
409
460
  config;
410
461
  observer;
411
- // Actor-model per-trait queues
462
+ // Actor-model queues, also keyed by `${traitName}::${entityId}` so
463
+ // each entity gets its own actor loop and concurrent subagents do
464
+ // not serialize through a shared queue.
412
465
  queues = /* @__PURE__ */ new Map();
413
466
  processing = /* @__PURE__ */ new Set();
414
467
  constructor(traits = [], config = {}, observer) {
@@ -431,33 +484,101 @@ var StateMachineManager = class {
431
484
  */
432
485
  addTrait(trait) {
433
486
  this.traits.set(trait.name, trait);
434
- this.states.set(trait.name, createInitialTraitState(trait));
435
487
  }
436
488
  /**
437
489
  * Remove a trait from the manager.
438
490
  */
439
491
  removeTrait(traitName) {
440
492
  this.traits.delete(traitName);
441
- this.states.delete(traitName);
493
+ const prefix = `${traitName}::`;
494
+ for (const key of [...this.states.keys()]) {
495
+ if (key.startsWith(prefix)) this.states.delete(key);
496
+ }
497
+ for (const key of [...this.queues.keys()]) {
498
+ if (key.startsWith(prefix)) this.queues.delete(key);
499
+ }
500
+ for (const key of [...this.processing]) {
501
+ if (key.startsWith(prefix)) this.processing.delete(key);
502
+ }
503
+ }
504
+ /**
505
+ * Get-or-init the state row for a (trait, entityId) pair.
506
+ * Returns undefined if the trait isn't registered.
507
+ */
508
+ getOrInitState(traitName, scope) {
509
+ const trait = this.traits.get(traitName);
510
+ if (!trait) return void 0;
511
+ const key = compositeKey(traitName, scope);
512
+ let state = this.states.get(key);
513
+ if (!state) {
514
+ state = createInitialTraitState(trait);
515
+ this.states.set(key, state);
516
+ }
517
+ return state;
442
518
  }
443
519
  /**
444
520
  * Get current state for a trait.
521
+ *
522
+ * For single-entity orbitals, omit `entityId` — returns the singleton
523
+ * scope. For multi-entity orbitals (e.g. agent OrbitalSubagent), pass
524
+ * the entity row id to look up that specific instance's state.
445
525
  */
446
- getState(traitName) {
447
- return this.states.get(traitName);
526
+ getState(traitName, entityId) {
527
+ const scope = entityId ?? SINGLETON_SCOPE;
528
+ const key = compositeKey(traitName, scope);
529
+ const existing = this.states.get(key);
530
+ if (existing) return existing;
531
+ return this.getOrInitState(traitName, scope);
448
532
  }
449
533
  /**
450
- * Get all current states.
534
+ * Get a flat traitName→state map. For multi-entity orbitals this
535
+ * returns one entry per trait collapsed onto the singleton scope (or
536
+ * the first observed scope if singleton is empty). Use
537
+ * `getAllStatesByScope()` to get the full per-entity view.
451
538
  */
452
539
  getAllStates() {
453
- return new Map(this.states);
540
+ const out = /* @__PURE__ */ new Map();
541
+ for (const traitName of this.traits.keys()) {
542
+ const singleton = this.states.get(compositeKey(traitName, SINGLETON_SCOPE));
543
+ if (singleton) {
544
+ out.set(traitName, singleton);
545
+ continue;
546
+ }
547
+ for (const [key, state] of this.states) {
548
+ if (key.startsWith(`${traitName}::`)) {
549
+ out.set(traitName, state);
550
+ break;
551
+ }
552
+ }
553
+ }
554
+ return out;
555
+ }
556
+ /**
557
+ * Get every per-entity state row, grouped by trait. Useful for
558
+ * inspecting parallel subagent state.
559
+ */
560
+ getAllStatesByScope() {
561
+ const grouped = /* @__PURE__ */ new Map();
562
+ for (const [key, state] of this.states) {
563
+ const sep = key.indexOf("::");
564
+ if (sep < 0) continue;
565
+ const traitName = key.slice(0, sep);
566
+ const scope = key.slice(sep + 2);
567
+ let bucket = grouped.get(traitName);
568
+ if (!bucket) {
569
+ bucket = /* @__PURE__ */ new Map();
570
+ grouped.set(traitName, bucket);
571
+ }
572
+ bucket.set(scope, state);
573
+ }
574
+ return grouped;
454
575
  }
455
576
  /**
456
577
  * Check if a trait can handle an event from its current state.
457
578
  */
458
- canHandleEvent(traitName, eventKey) {
579
+ canHandleEvent(traitName, eventKey, entityId) {
459
580
  const trait = this.traits.get(traitName);
460
- const state = this.states.get(traitName);
581
+ const state = this.getOrInitState(traitName, entityId ?? SINGLETON_SCOPE);
461
582
  if (!trait || !state) return false;
462
583
  return !!findTransition(trait, state.currentState, normalizeEventKey(eventKey));
463
584
  }
@@ -468,9 +589,11 @@ var StateMachineManager = class {
468
589
  */
469
590
  sendEvent(eventKey, payload, entityData) {
470
591
  const results = [];
592
+ const scope = scopeOf(entityData);
471
593
  for (const [traitName, trait] of this.traits) {
472
- const traitState = this.states.get(traitName);
594
+ const traitState = this.getOrInitState(traitName, scope);
473
595
  if (!traitState) continue;
596
+ const key = compositeKey(traitName, scope);
474
597
  const result = processEvent({
475
598
  traitState,
476
599
  trait,
@@ -482,7 +605,7 @@ var StateMachineManager = class {
482
605
  contextExtensions: this.config.contextExtensions
483
606
  });
484
607
  if (result.executed) {
485
- this.states.set(traitName, {
608
+ this.states.set(key, {
486
609
  ...traitState,
487
610
  currentState: result.newState,
488
611
  previousState: result.previousState,
@@ -516,31 +639,29 @@ var StateMachineManager = class {
516
639
  * at a time per trait, effects fully awaited before the next event).
517
640
  */
518
641
  enqueueEvent(eventKey, payload, entityData) {
642
+ const scope = scopeOf(entityData);
519
643
  for (const [traitName] of this.traits) {
520
- const queue = this.queues.get(traitName) ?? [];
644
+ const key = compositeKey(traitName, scope);
645
+ const queue = this.queues.get(key) ?? [];
521
646
  queue.push({ eventKey, payload, entityData });
522
- this.queues.set(traitName, queue);
647
+ this.queues.set(key, queue);
523
648
  }
524
649
  }
525
650
  /**
526
- * Drain a single trait's event queue, processing events sequentially.
527
- *
528
- * This is the core actor loop: each event is fully processed (including
529
- * awaiting all effects) before the next event is dequeued. If the queue
530
- * is already being drained for this trait, this call is a no-op (the
531
- * running drain will pick up newly enqueued events).
532
- *
533
- * @param traitName - Which trait's queue to drain
534
- * @param executeEffects - Async callback to run effects for a successful transition
651
+ * Drain a single (trait, entity) pair's event queue, processing
652
+ * events sequentially. Pass `entityId` to drain a specific entity
653
+ * instance; omit it to drain the singleton scope.
535
654
  */
536
- async drainQueue(traitName, executeEffects) {
537
- if (this.processing.has(traitName)) return;
538
- this.processing.add(traitName);
539
- const queue = this.queues.get(traitName) ?? [];
655
+ async drainQueue(traitName, executeEffects, entityId) {
656
+ const scope = entityId ?? SINGLETON_SCOPE;
657
+ const key = compositeKey(traitName, scope);
658
+ if (this.processing.has(key)) return;
659
+ this.processing.add(key);
660
+ const queue = this.queues.get(key) ?? [];
540
661
  while (queue.length > 0) {
541
662
  const entry = queue.shift();
542
663
  const trait = this.traits.get(traitName);
543
- const traitState = this.states.get(traitName);
664
+ const traitState = this.getOrInitState(traitName, scope);
544
665
  if (!trait || !traitState) continue;
545
666
  const result = processEvent({
546
667
  traitState,
@@ -553,7 +674,7 @@ var StateMachineManager = class {
553
674
  contextExtensions: this.config.contextExtensions
554
675
  });
555
676
  if (result.executed) {
556
- this.states.set(traitName, {
677
+ this.states.set(key, {
557
678
  ...traitState,
558
679
  currentState: result.newState,
559
680
  previousState: result.previousState,
@@ -573,35 +694,51 @@ var StateMachineManager = class {
573
694
  await executeEffects(traitName, result, entry.payload);
574
695
  }
575
696
  }
576
- this.processing.delete(traitName);
697
+ this.processing.delete(key);
577
698
  }
578
699
  /**
579
700
  * Check whether a trait's queue is currently being drained.
580
701
  */
581
- isProcessing(traitName) {
582
- return this.processing.has(traitName);
702
+ isProcessing(traitName, entityId) {
703
+ return this.processing.has(compositeKey(traitName, entityId ?? SINGLETON_SCOPE));
583
704
  }
584
705
  /**
585
706
  * Get the number of pending events in a trait's queue.
586
707
  */
587
- getQueueLength(traitName) {
588
- return this.queues.get(traitName)?.length ?? 0;
708
+ getQueueLength(traitName, entityId) {
709
+ return this.queues.get(compositeKey(traitName, entityId ?? SINGLETON_SCOPE))?.length ?? 0;
589
710
  }
590
711
  /**
591
712
  * Reset a trait to its initial state.
713
+ *
714
+ * If `entityId` is provided, resets only that entity's scope.
715
+ * Otherwise resets every entity scope known for the trait.
592
716
  */
593
- resetTrait(traitName) {
717
+ resetTrait(traitName, entityId) {
594
718
  const trait = this.traits.get(traitName);
595
- if (trait) {
596
- this.states.set(traitName, createInitialTraitState(trait));
719
+ if (!trait) return;
720
+ if (entityId) {
721
+ this.states.set(compositeKey(traitName, entityId), createInitialTraitState(trait));
722
+ return;
723
+ }
724
+ const prefix = `${traitName}::`;
725
+ for (const key of [...this.states.keys()]) {
726
+ if (key.startsWith(prefix)) {
727
+ this.states.set(key, createInitialTraitState(trait));
728
+ }
597
729
  }
598
730
  }
599
731
  /**
600
- * Reset all traits to initial states.
732
+ * Reset all traits to initial states (every entity scope).
601
733
  */
602
734
  resetAll() {
603
735
  for (const [traitName, trait] of this.traits) {
604
- this.states.set(traitName, createInitialTraitState(trait));
736
+ const prefix = `${traitName}::`;
737
+ for (const key of [...this.states.keys()]) {
738
+ if (key.startsWith(prefix)) {
739
+ this.states.set(key, createInitialTraitState(trait));
740
+ }
741
+ }
605
742
  }
606
743
  }
607
744
  };
@@ -2397,5 +2534,5 @@ function parseNamespacedEvent(eventName) {
2397
2534
  }
2398
2535
 
2399
2536
  export { EffectExecutor, EventBus, HANDLER_MANIFEST, StateMachineManager, containsBindings, createContextFromBindings, createInitialTraitState, createTestExecutor, createUnifiedLoader, extractBindings, findInitialState, findTransition, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isNamespacedEvent, normalizeEventKey, parseNamespacedEvent, preprocessSchema, processEvent };
2400
- //# sourceMappingURL=chunk-GU35X5AW.js.map
2401
- //# sourceMappingURL=chunk-GU35X5AW.js.map
2537
+ //# sourceMappingURL=chunk-QQIRPNHH.js.map
2538
+ //# sourceMappingURL=chunk-QQIRPNHH.js.map