@mastra/factory 0.17.3-alpha.4 → 0.17.3-alpha.5

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.
@@ -23,6 +23,14 @@ const MAX_BACKOFF_MS = 6e4;
23
23
  const RUN_REGISTRY_HEARTBEAT_MS = 10 * 6e4;
24
24
  const SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS = 360 * 6e4;
25
25
  const MAX_IN_FLIGHT = 25;
26
+ /**
27
+ * Decisions that may start or wake an agent run and so hold a run slot. A
28
+ * transition's message is queued as a separate sendMessage, so the transition
29
+ * itself stays bookkeeping.
30
+ */
31
+ const isRunBearingDecision = (decision) => decision.type === "invokeSkill" || decision.type === "sendMessage";
32
+ const isBookkeepingDecision = (decision) => !isRunBearingDecision(decision);
33
+ const BOOKKEEPING_MAX_IN_FLIGHT = 4;
26
34
  const STALE_BINDING_SWEEP_INTERVAL_MS = 10 * 6e4;
27
35
  const STALE_BINDING_TTL_MS = 1440 * 6e4;
28
36
  const RECONCILE_INTERVAL_MS = 3e4;
@@ -243,7 +251,12 @@ var FactoryDecisionDispatcher = class {
243
251
  #reconcileInFlight;
244
252
  #timer;
245
253
  #activeClaim;
246
- #inFlight = /* @__PURE__ */ new Set();
254
+ /** Starts and run-bearing decisions; capped by `maxInFlight`. */
255
+ #runInFlight = /* @__PURE__ */ new Set();
256
+ /** Fast board bookkeeping (transitions, linked-card upserts); its own small cap. */
257
+ #bookkeepingInFlight = /* @__PURE__ */ new Set();
258
+ /** Background work awaited on stop but outside both caps. */
259
+ #backgroundInFlight = /* @__PURE__ */ new Set();
247
260
  #bindingSkillRuns = /* @__PURE__ */ new Map();
248
261
  constructor(options) {
249
262
  this.#audit = options.audit;
@@ -277,7 +290,11 @@ var FactoryDecisionDispatcher = class {
277
290
  if (this.#timer) clearInterval(this.#timer);
278
291
  this.#timer = void 0;
279
292
  await this.#activeClaim;
280
- await Promise.allSettled([...this.#inFlight]);
293
+ await Promise.allSettled([
294
+ ...this.#runInFlight,
295
+ ...this.#bookkeepingInFlight,
296
+ ...this.#backgroundInFlight
297
+ ]);
281
298
  }
282
299
  async runOnce(now = /* @__PURE__ */ new Date()) {
283
300
  await Promise.all(await this.#claimAndStart(now));
@@ -292,24 +309,39 @@ var FactoryDecisionDispatcher = class {
292
309
  async #claimAndStart(now) {
293
310
  this.#maybeSweepStaleBindings(now);
294
311
  this.#maybeReconcileToolResults(now);
295
- const capacity = this.#maxInFlight - this.#inFlight.size;
296
- if (capacity <= 0) return [];
297
- const limit = Math.min(BATCH_SIZE, capacity);
298
312
  const leaseExpiresAt = new Date(now.getTime() + LEASE_MS);
299
- const starts = await this.#storage.claimPendingStarts({
300
- ownerId: this.#ownerId,
301
- now,
302
- leaseExpiresAt,
303
- limit
304
- });
305
- const decisionsLimit = limit - starts.length;
306
- const decisions = decisionsLimit > 0 ? await this.#storage.claimDeferredDecisions({
307
- ownerId: this.#ownerId,
308
- now,
309
- leaseExpiresAt,
310
- limit: decisionsLimit
311
- }) : [];
312
- return [...starts.map((start) => this.#track(this.#dispatchPendingStart(start, now))), ...decisions.map((decision) => this.#track(this.#dispatchDecision(decision, now)))];
313
+ const dispatches = [];
314
+ const runCapacity = this.#maxInFlight - this.#runInFlight.size;
315
+ if (runCapacity > 0) {
316
+ const limit = Math.min(BATCH_SIZE, runCapacity);
317
+ const starts = await this.#storage.claimPendingStarts({
318
+ ownerId: this.#ownerId,
319
+ now,
320
+ leaseExpiresAt,
321
+ limit
322
+ });
323
+ const decisionsLimit = limit - starts.length;
324
+ const decisions = decisionsLimit > 0 ? await this.#storage.claimDeferredDecisions({
325
+ ownerId: this.#ownerId,
326
+ now,
327
+ leaseExpiresAt,
328
+ limit: decisionsLimit,
329
+ decisionFilter: isRunBearingDecision
330
+ }) : [];
331
+ dispatches.push(...starts.map((start) => this.#track(this.#runInFlight, this.#dispatchPendingStart(start, now))), ...decisions.map((decision) => this.#track(this.#runInFlight, this.#dispatchDecision(decision, now))));
332
+ }
333
+ const bookkeepingCapacity = BOOKKEEPING_MAX_IN_FLIGHT - this.#bookkeepingInFlight.size;
334
+ if (bookkeepingCapacity > 0) {
335
+ const decisions = await this.#storage.claimDeferredDecisions({
336
+ ownerId: this.#ownerId,
337
+ now,
338
+ leaseExpiresAt,
339
+ limit: Math.min(BATCH_SIZE, bookkeepingCapacity),
340
+ decisionFilter: isBookkeepingDecision
341
+ });
342
+ dispatches.push(...decisions.map((decision) => this.#track(this.#bookkeepingInFlight, this.#dispatchDecision(decision, now))));
343
+ }
344
+ return dispatches;
313
345
  }
314
346
  /**
315
347
  * Throttled, coalesced, non-blocking bound-thread reconcile: dispatch
@@ -326,7 +358,7 @@ var FactoryDecisionDispatcher = class {
326
358
  this.#reconcileInFlight = void 0;
327
359
  });
328
360
  this.#reconcileInFlight = run;
329
- this.#track(run);
361
+ this.#track(this.#backgroundInFlight, run);
330
362
  }
331
363
  /** Slow-cadence revocation of leaked/legacy bindings; failures never block the claim path. */
332
364
  async #maybeSweepStaleBindings(now) {
@@ -346,9 +378,9 @@ var FactoryDecisionDispatcher = class {
346
378
  console.error("Factory stale-binding sweep failed", sanitizeDispatchError(error));
347
379
  }
348
380
  }
349
- #track(dispatch) {
350
- this.#inFlight.add(dispatch);
351
- dispatch.catch(() => {}).then(() => this.#inFlight.delete(dispatch));
381
+ #track(pool, dispatch) {
382
+ pool.add(dispatch);
383
+ dispatch.catch(() => {}).then(() => pool.delete(dispatch));
352
384
  return dispatch;
353
385
  }
354
386
  async #tick() {
@@ -486,35 +518,41 @@ var FactoryDecisionDispatcher = class {
486
518
  if (result.status === "rejected") throw new Error(`${result.code}: ${result.reason}`);
487
519
  const transitionMessage = decision.message;
488
520
  if (!transitionMessage) return;
489
- const binding = await this.#findBinding(record, transitionMessage.role);
490
- if (!binding) return;
491
- const startedBy = item.sessions[binding.role]?.startedBy;
492
- if (!startedBy) return;
493
- await this.#primeCredentials?.({
521
+ const moved = await this.#storage.get({
494
522
  orgId: record.orgId,
495
- userId: startedBy
523
+ id: item.id
496
524
  });
497
- const session = await this.#findSession(binding);
498
- if (!session) return;
499
- const requestContext = factoryRequestContext({
500
- session,
501
- binding,
502
- userId: startedBy,
503
- orgId: record.orgId
525
+ if (!moved) return;
526
+ const messageKey = `${record.idempotencyKey}:message`;
527
+ const queued = await this.#storage.commitRuleEvaluation({
528
+ orgId: record.orgId,
529
+ factoryProjectId: record.factoryProjectId,
530
+ workItemId: item.id,
531
+ ingress: {
532
+ identity: `decision:${messageKey}@${moved.revision}`,
533
+ triggerType: "transition.message"
534
+ },
535
+ configVersion: this.#transitionService.configVersion,
536
+ expectedRevision: moved.revision,
537
+ actor: record.actor ?? {
538
+ type: "system",
539
+ id: "factory-rule-dispatcher"
540
+ },
541
+ outcome: { status: "accepted" },
542
+ decisions: [{
543
+ type: "sendMessage",
544
+ idempotencyKey: messageKey,
545
+ message: transitionMessage.text,
546
+ priority: "high",
547
+ idleBehavior: "wake",
548
+ ...transitionMessage.role !== void 0 ? { role: transitionMessage.role } : {}
549
+ }],
550
+ causalChain: nextChain,
551
+ now: /* @__PURE__ */ new Date()
504
552
  });
505
- await awaitNotification(() => session.sendNotificationSignal({
506
- source: "factory",
507
- kind: "rule-message",
508
- summary: transitionMessage.text,
509
- priority: "high",
510
- payload: { message: transitionMessage.text },
511
- sourceId: record.id,
512
- dedupeKey: record.idempotencyKey
513
- }, {
514
- ifActive: { behavior: "deliver" },
515
- ifIdle: { behavior: "wake" },
516
- requestContext
517
- }), true);
553
+ if (queued.status === "missing") return;
554
+ const queuedStatus = queued.result.status;
555
+ if (queuedStatus !== "accepted") throw new Error(`Factory transition message was not queued: ${queuedStatus ?? "unknown"}.`);
518
556
  return;
519
557
  }
520
558
  case "upsertLinkedWorkItem":
@@ -555,6 +593,22 @@ var FactoryDecisionDispatcher = class {
555
593
  });
556
594
  await this.#switchThread(session, binding);
557
595
  const deliveryId = record.deliveryGeneration === 0 ? record.id : `${record.id}:retry:${record.deliveryGeneration}`;
596
+ const kickoffLanded = async () => (await session.thread.listActiveMessages()).some((message) => message.id === deliveryId);
597
+ const kickoffStale = async () => {
598
+ if (await this.#roleSuperseded(record, decision.role)) return true;
599
+ if ((await this.#findBinding(record, decision.role))?.id !== binding.id) return true;
600
+ if (!record.workItemId) return false;
601
+ const current = await this.#storage.get({
602
+ orgId: record.orgId,
603
+ id: record.workItemId
604
+ });
605
+ if (current === null) return true;
606
+ const decidedAt = record.createdAt.getTime();
607
+ const enteredSince = current.stageHistory.filter((entry) => entry.exitedAt === void 0 && new Date(entry.enteredAt).getTime() > decidedAt);
608
+ if (enteredSince.length === 0) return false;
609
+ const siblingStages = new Set((await this.#storage.listDeferredDecisions(record.orgId, record.factoryProjectId)).filter((sibling) => sibling.evaluationId === record.evaluationId && sibling.decision.type === "transition").map((sibling) => sibling.decision.stage));
610
+ return enteredSince.some((entry) => !siblingStages.has(entry.stage));
611
+ };
558
612
  const runStillActive = () => this.#controller.listActiveThreadRuns().some((active) => active.threadId === binding.threadId);
559
613
  while (true) {
560
614
  const openRun = watchRun(session, {
@@ -576,7 +630,8 @@ var FactoryDecisionDispatcher = class {
576
630
  }
577
631
  if (await this.#roleSuperseded(record, decision.role)) return;
578
632
  if ((await this.#requireBinding(record, decision.role)).id !== binding.id) throw new FactoryDispatchError("session_unavailable", `Factory binding ${binding.id} changed while waiting for its open run to end.`);
579
- if ((await session.thread.listActiveMessages()).some((message) => message.id === deliveryId)) return;
633
+ if (await kickoffLanded()) return;
634
+ if (await kickoffStale()) return;
580
635
  const kickoffContents = await withWorkItemFeed(this.#feedReader, {
581
636
  orgId: record.orgId,
582
637
  factoryProjectId: record.factoryProjectId,
@@ -620,8 +675,10 @@ var FactoryDecisionDispatcher = class {
620
675
  try {
621
676
  let settled = await sendKickoff();
622
677
  while (settled.action === "deliver") {
623
- if ((await session.thread.listActiveMessages()).some((message) => message.id === deliveryId)) break;
678
+ if (await kickoffLanded()) break;
624
679
  if (!await run.wait()) throw new FactoryDispatchError("run_terminal_event_missing", "Factory skill invocation is waiting on a run whose terminal event was not observed.");
680
+ if (await kickoffLanded()) break;
681
+ if (await kickoffStale()) return;
625
682
  run.arm();
626
683
  settled = await sendKickoff();
627
684
  }