@mastra/factory 0.15.0 → 0.16.0-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.
Files changed (104) hide show
  1. package/dist/boards/index.d.ts +1 -1
  2. package/dist/boards/index.d.ts.map +1 -1
  3. package/dist/boards/index.js +2 -2
  4. package/dist/boards/index.js.map +1 -1
  5. package/dist/boards/review.d.ts.map +1 -1
  6. package/dist/boards/review.js +4 -1
  7. package/dist/boards/review.js.map +1 -1
  8. package/dist/boards/semantics.d.ts +7 -0
  9. package/dist/boards/semantics.d.ts.map +1 -1
  10. package/dist/boards/semantics.js +15 -1
  11. package/dist/boards/semantics.js.map +1 -1
  12. package/dist/boards/transition-policy.d.ts +4 -0
  13. package/dist/boards/transition-policy.d.ts.map +1 -1
  14. package/dist/boards/transition-policy.js.map +1 -1
  15. package/dist/boards/work-transition-policy.d.ts.map +1 -1
  16. package/dist/boards/work-transition-policy.js +6 -1
  17. package/dist/boards/work-transition-policy.js.map +1 -1
  18. package/dist/capabilities/intake.d.ts +4 -0
  19. package/dist/capabilities/intake.d.ts.map +1 -1
  20. package/dist/factory.d.ts.map +1 -1
  21. package/dist/factory.js +51 -12
  22. package/dist/factory.js.map +1 -1
  23. package/dist/integrations/base.d.ts +4 -2
  24. package/dist/integrations/base.d.ts.map +1 -1
  25. package/dist/integrations/linear/claim.d.ts +8 -0
  26. package/dist/integrations/linear/claim.d.ts.map +1 -0
  27. package/dist/integrations/linear/claim.js +14 -0
  28. package/dist/integrations/linear/claim.js.map +1 -0
  29. package/dist/integrations/linear/default-rules.d.ts +1 -0
  30. package/dist/integrations/linear/default-rules.d.ts.map +1 -1
  31. package/dist/integrations/linear/default-rules.js +2 -0
  32. package/dist/integrations/linear/default-rules.js.map +1 -1
  33. package/dist/integrations/linear/integration.d.ts +24 -6
  34. package/dist/integrations/linear/integration.d.ts.map +1 -1
  35. package/dist/integrations/linear/integration.js +257 -37
  36. package/dist/integrations/linear/integration.js.map +1 -1
  37. package/dist/integrations/linear/routes.d.ts +10 -0
  38. package/dist/integrations/linear/routes.d.ts.map +1 -1
  39. package/dist/integrations/linear/routes.js +74 -12
  40. package/dist/integrations/linear/routes.js.map +1 -1
  41. package/dist/integrations/linear/rules.d.ts +1 -1
  42. package/dist/integrations/linear/rules.d.ts.map +1 -1
  43. package/dist/integrations/linear/rules.js +27 -2
  44. package/dist/integrations/linear/rules.js.map +1 -1
  45. package/dist/integrations/platform/linear/integration.d.ts +34 -1
  46. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  47. package/dist/integrations/platform/linear/integration.js +271 -48
  48. package/dist/integrations/platform/linear/integration.js.map +1 -1
  49. package/dist/routes/intake.d.ts.map +1 -1
  50. package/dist/routes/intake.js +15 -5
  51. package/dist/routes/intake.js.map +1 -1
  52. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  53. package/dist/rules/dispatch-errors.js +8 -0
  54. package/dist/rules/dispatch-errors.js.map +1 -1
  55. package/dist/rules/dispatcher.d.ts +1 -0
  56. package/dist/rules/dispatcher.d.ts.map +1 -1
  57. package/dist/rules/dispatcher.js +179 -105
  58. package/dist/rules/dispatcher.js.map +1 -1
  59. package/dist/rules/start-coordinator.d.ts.map +1 -1
  60. package/dist/rules/start-coordinator.js +4 -1
  61. package/dist/rules/start-coordinator.js.map +1 -1
  62. package/dist/rules/terminal-cleanup.d.ts +14 -1
  63. package/dist/rules/terminal-cleanup.d.ts.map +1 -1
  64. package/dist/rules/terminal-cleanup.js +20 -0
  65. package/dist/rules/terminal-cleanup.js.map +1 -1
  66. package/dist/rules/tools.d.ts +2 -0
  67. package/dist/rules/tools.d.ts.map +1 -1
  68. package/dist/rules/tools.js +30 -2
  69. package/dist/rules/tools.js.map +1 -1
  70. package/dist/rules/transition-service.d.ts +13 -0
  71. package/dist/rules/transition-service.d.ts.map +1 -1
  72. package/dist/rules/transition-service.js +11 -2
  73. package/dist/rules/transition-service.js.map +1 -1
  74. package/dist/rules/types.d.ts +15 -0
  75. package/dist/rules/types.d.ts.map +1 -1
  76. package/dist/rules/types.js.map +1 -1
  77. package/dist/rules/validation.d.ts.map +1 -1
  78. package/dist/rules/validation.js +11 -2
  79. package/dist/rules/validation.js.map +1 -1
  80. package/dist/session/run-audit.d.ts +9 -0
  81. package/dist/session/run-audit.d.ts.map +1 -1
  82. package/dist/session/run-audit.js +37 -6
  83. package/dist/session/run-audit.js.map +1 -1
  84. package/dist/skills/service.d.ts +12 -0
  85. package/dist/skills/service.d.ts.map +1 -1
  86. package/dist/skills/service.js +27 -1
  87. package/dist/skills/service.js.map +1 -1
  88. package/dist/storage/domains/intake/base.d.ts +11 -8
  89. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  90. package/dist/storage/domains/intake/base.js +69 -24
  91. package/dist/storage/domains/intake/base.js.map +1 -1
  92. package/dist/storage/domains/work-items/base.d.ts +41 -2
  93. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  94. package/dist/storage/domains/work-items/base.js +159 -12
  95. package/dist/storage/domains/work-items/base.js.map +1 -1
  96. package/dist/supervisor/session-messaging.d.ts +23 -0
  97. package/dist/supervisor/session-messaging.d.ts.map +1 -0
  98. package/dist/supervisor/session-messaging.js +15 -0
  99. package/dist/supervisor/session-messaging.js.map +1 -0
  100. package/dist/supervisor/write-tools.d.ts +2 -1
  101. package/dist/supervisor/write-tools.d.ts.map +1 -1
  102. package/dist/supervisor/write-tools.js +11 -7
  103. package/dist/supervisor/write-tools.js.map +1 -1
  104. package/package.json +12 -12
@@ -3,11 +3,11 @@ import { boardForWorkItem, resolvePhaseSemantics, workItemPhaseSemantics } from
3
3
  import { FACTORY_RULE_STAGES, externallyAuthoredWorkItem } from "./types.js";
4
4
  import { createBoardRegistry } from "../boards/registry.js";
5
5
  import "../boards/index.js";
6
- import { FACTORY_RULE_MATERIALIZATION_KEY } from "../storage/domains/work-items/base.js";
6
+ import { FACTORY_RULE_MATERIALIZATION_KEY, WorkItemClaimConflictError } from "../storage/domains/work-items/base.js";
7
7
  import { FactoryDispatchError, factoryDispatchFailureCode, factoryDispatchFailureMetadata } from "./dispatch-errors.js";
8
8
  import { isHumanActorId } from "../storage/domains/audit/actors.js";
9
- import { resolvePromptInvocation, resolveSkillInvocation } from "../skills/service.js";
10
- import { recordSessionRunStart } from "../session/run-audit.js";
9
+ import { resolvePromptInvocation, resolveSkillInvocation, resolveSkillResumeInvocation } from "../skills/service.js";
10
+ import { FACTORY_OPEN_RUN_STALE_MS, heartbeatSessionOpenRun, isOpenRunLiveElsewhere, listSessionOpenRuns, recordSessionRunStart, waitForSessionRunAudit } from "../session/run-audit.js";
11
11
  import { withWorkItemFeed } from "../storage/domains/comments/feed-context.js";
12
12
  import { RequestContext } from "@mastra/core/request-context";
13
13
  import { randomUUID } from "crypto";
@@ -103,7 +103,7 @@ function watchRun(session, { timeoutMs, approvePlans, onAgentEnd, label, runStil
103
103
  });
104
104
  observed = await wait();
105
105
  }
106
- if (!observed) throw new Error(`${label} terminal event was not observed before timeout.`);
106
+ if (!observed) throw new FactoryDispatchError("run_terminal_event_missing", `${label} terminal event was not observed before timeout.`);
107
107
  if (endReason === "error") throw new Error(`${label} ended in error.`);
108
108
  if (endReason === "aborted") {
109
109
  if (omFailure) {
@@ -243,6 +243,7 @@ var FactoryDecisionDispatcher = class {
243
243
  #timer;
244
244
  #activeClaim;
245
245
  #inFlight = /* @__PURE__ */ new Set();
246
+ #bindingSkillRuns = /* @__PURE__ */ new Map();
246
247
  constructor(options) {
247
248
  this.#audit = options.audit;
248
249
  this.#controller = options.controller;
@@ -521,93 +522,120 @@ var FactoryDecisionDispatcher = class {
521
522
  case "invokeSkill": {
522
523
  if (await this.#roleSuperseded(record, decision.role)) return;
523
524
  const binding = await this.#requireOrPrepareBinding(record, decision.role);
524
- const item = record.workItemId ? await this.#storage.get({
525
- orgId: record.orgId,
526
- id: record.workItemId
527
- }) : null;
528
- const startedBy = item?.sessions[binding.role]?.startedBy;
529
- if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);
530
- await this.#primeCredentials?.({
531
- orgId: record.orgId,
532
- userId: startedBy
533
- });
534
- const session = await this.#requireSession(binding);
535
- const requestContext = factoryRequestContext({
536
- session,
537
- binding,
538
- userId: startedBy,
539
- orgId: record.orgId
540
- });
541
- const resolved = decision.skillName === void 0 ? await resolvePromptInvocation(this.#controller, {
542
- resourceId: binding.resourceId,
543
- prompt: decision.prompt
544
- }) : await resolveSkillInvocation(this.#controller, {
545
- resourceId: binding.resourceId,
546
- name: decision.skillName,
547
- arguments: decision.arguments
548
- });
549
- await this.#switchThread(session, binding);
550
- const deliveryId = record.deliveryGeneration === 0 ? record.id : `${record.id}:retry:${record.deliveryGeneration}`;
551
- if ((await session.thread.listActiveMessages()).some((message) => message.id === deliveryId)) return;
552
- const kickoffContents = await withWorkItemFeed(this.#feedReader, {
553
- orgId: record.orgId,
554
- factoryProjectId: record.factoryProjectId,
555
- workItemId: record.workItemId
556
- }, resolved.message);
557
- if (decision.cancelInFlight && session.stream.isActive()) session.abort();
558
- const precedingMessage = decision.precedingMessage;
559
- if (precedingMessage) await awaitNotification(() => session.sendNotificationSignal({
560
- source: "factory",
561
- kind: "stage-transition",
562
- summary: precedingMessage,
563
- priority: "medium",
564
- payload: { message: precedingMessage },
565
- sourceId: `${record.id}:stage-transition`,
566
- dedupeKey: `${record.idempotencyKey}:stage-transition`
567
- }, {
568
- ifActive: { behavior: "deliver" },
569
- ifIdle: { behavior: "persist" },
570
- requestContext
571
- }));
572
- const run = watchRun(session, {
573
- timeoutMs: this.#skillCompletionObservationTimeoutMs,
574
- approvePlans: await this.#plansAreAutoApproved(record, item),
575
- onAgentEnd: () => this.#roleSuperseded(record, decision.role),
576
- label: "Factory skill run",
577
- runStillActive: () => this.#controller.listActiveThreadRuns().some((active) => active.threadId === binding.threadId)
578
- });
579
- const sendKickoff = async () => {
580
- const settled = await session.sendSignal({
581
- id: deliveryId,
582
- type: "user",
583
- tagName: "user",
584
- contents: kickoffContents
585
- }, {
586
- requestContext,
587
- requireDelivery: true
588
- }).accepted;
589
- if (settled.action !== "wake" && settled.action !== "deliver") throw new Error(`Factory skill invocation signal did not reach the agent (${String(settled.action)}).`);
590
- return settled;
591
- };
592
- try {
593
- let settled = await sendKickoff();
594
- if (settled.action === "deliver") {
595
- if (!(await session.thread.listActiveMessages()).some((message) => message.id === deliveryId)) {
596
- if (!await run.wait()) throw new Error("Factory skill invocation is waiting on a run that has not ended.");
597
- run.arm();
598
- settled = await sendKickoff();
599
- if (settled.action !== "wake") throw new Error("Factory skill invocation was queued onto an ending run and never reached the agent.");
525
+ await this.#withBindingSkillRun(binding.id, async () => {
526
+ const item = record.workItemId ? await this.#storage.get({
527
+ orgId: record.orgId,
528
+ id: record.workItemId
529
+ }) : null;
530
+ const startedBy = item?.sessions[binding.role]?.startedBy;
531
+ if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);
532
+ await this.#primeCredentials?.({
533
+ orgId: record.orgId,
534
+ userId: startedBy
535
+ });
536
+ const session = await this.#requireSession(binding);
537
+ const requestContext = factoryRequestContext({
538
+ session,
539
+ binding,
540
+ userId: startedBy,
541
+ orgId: record.orgId
542
+ });
543
+ const resolved = decision.skillName === void 0 ? await resolvePromptInvocation(this.#controller, {
544
+ resourceId: binding.resourceId,
545
+ prompt: decision.prompt
546
+ }) : decision.resume === true ? await resolveSkillResumeInvocation(this.#controller, {
547
+ resourceId: binding.resourceId,
548
+ name: decision.skillName,
549
+ arguments: decision.arguments
550
+ }) : await resolveSkillInvocation(this.#controller, {
551
+ resourceId: binding.resourceId,
552
+ name: decision.skillName,
553
+ arguments: decision.arguments
554
+ });
555
+ await this.#switchThread(session, binding);
556
+ const deliveryId = record.deliveryGeneration === 0 ? record.id : `${record.id}:retry:${record.deliveryGeneration}`;
557
+ const runStillActive = () => this.#controller.listActiveThreadRuns().some((active) => active.threadId === binding.threadId);
558
+ while (true) {
559
+ const openRun = watchRun(session, {
560
+ timeoutMs: this.#skillCompletionObservationTimeoutMs,
561
+ approvePlans: false,
562
+ label: "Factory skill run",
563
+ runStillActive
564
+ });
565
+ try {
566
+ const open = (await listSessionOpenRuns(session)).filter((entry) => entry.bindingId === binding.id);
567
+ if (open.length === 0) break;
568
+ if (open.some((entry) => isOpenRunLiveElsewhere(entry, this.#ownerId))) throw new FactoryDispatchError("session_unavailable", `Factory binding ${binding.id} has a run in flight on another dispatcher.`);
569
+ if (!runStillActive()) break;
570
+ if (!await openRun.wait()) break;
571
+ } finally {
572
+ openRun.close();
600
573
  }
574
+ await waitForSessionRunAudit(session);
601
575
  }
602
- await this.#recordRunStart(session, binding, deliveryId, record.approvedBy ?? void 0, run.endReason, item?.title);
576
+ if (await this.#roleSuperseded(record, decision.role)) return;
577
+ 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.`);
578
+ if ((await session.thread.listActiveMessages()).some((message) => message.id === deliveryId)) return;
579
+ const kickoffContents = await withWorkItemFeed(this.#feedReader, {
580
+ orgId: record.orgId,
581
+ factoryProjectId: record.factoryProjectId,
582
+ workItemId: record.workItemId
583
+ }, resolved.message);
584
+ if (decision.cancelInFlight && session.stream.isActive()) session.abort();
585
+ const precedingMessage = decision.precedingMessage;
586
+ if (precedingMessage) await awaitNotification(() => session.sendNotificationSignal({
587
+ source: "factory",
588
+ kind: "stage-transition",
589
+ summary: precedingMessage,
590
+ priority: "medium",
591
+ payload: { message: precedingMessage },
592
+ sourceId: `${record.id}:stage-transition`,
593
+ dedupeKey: `${record.idempotencyKey}:stage-transition`
594
+ }, {
595
+ ifActive: { behavior: "deliver" },
596
+ ifIdle: { behavior: "persist" },
597
+ requestContext
598
+ }));
599
+ const run = watchRun(session, {
600
+ timeoutMs: this.#skillCompletionObservationTimeoutMs,
601
+ approvePlans: await this.#plansAreAutoApproved(record, item),
602
+ onAgentEnd: () => this.#roleSuperseded(record, decision.role),
603
+ label: "Factory skill run",
604
+ runStillActive
605
+ });
606
+ const sendKickoff = async () => {
607
+ const settled = await session.sendSignal({
608
+ id: deliveryId,
609
+ type: "user",
610
+ tagName: "user",
611
+ contents: kickoffContents
612
+ }, {
613
+ requestContext,
614
+ requireDelivery: true
615
+ }).accepted;
616
+ if (settled.action !== "wake" && settled.action !== "deliver") throw new FactoryDispatchError(settled.action ? "notification_delivery_failed" : "skill_delivery_ambiguous", `Factory skill invocation signal did not reach the agent (${String(settled.action)}).`);
617
+ return settled;
618
+ };
603
619
  try {
604
- await run.settle();
605
- } catch (error) {
606
- if (!(await run.supersededAtEnd() ?? await this.#roleSuperseded(record, decision.role))) throw error;
620
+ let settled = await sendKickoff();
621
+ if (settled.action === "deliver") {
622
+ if (!(await session.thread.listActiveMessages()).some((message) => message.id === deliveryId)) {
623
+ 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.");
624
+ run.arm();
625
+ settled = await sendKickoff();
626
+ if (settled.action !== "wake") throw new FactoryDispatchError("skill_delivery_ambiguous", "Factory skill invocation was queued onto an ending run and never reached the agent.");
627
+ }
628
+ }
629
+ await this.#recordRunStart(session, binding, deliveryId, record.approvedBy ?? void 0, run.endReason, item?.title);
630
+ try {
631
+ await this.#withOpenRunHeartbeat(session, deliveryId, () => run.settle());
632
+ } catch (error) {
633
+ if (!(await run.supersededAtEnd() ?? await this.#roleSuperseded(record, decision.role))) throw error;
634
+ }
635
+ } finally {
636
+ run.close();
607
637
  }
608
- } finally {
609
- run.close();
610
- }
638
+ });
611
639
  return;
612
640
  }
613
641
  case "sendMessage": {
@@ -680,24 +708,42 @@ var FactoryDecisionDispatcher = class {
680
708
  factoryProjectId: record.factoryProjectId,
681
709
  decision
682
710
  }) ?? null;
683
- let result = await this.#storage.upsert({
684
- orgId: record.orgId,
685
- userId: "factory-rule-dispatcher",
686
- factoryProjectId: record.factoryProjectId,
687
- input: {
688
- externalSource: externalSourceForDecision(decision),
689
- parentWorkItemId,
690
- title: decision.title,
691
- board: decision.board,
692
- stages: [initialPhase],
693
- sessions: {},
694
- metadata: {
695
- ...decision.metadata,
696
- [FACTORY_RULE_MATERIALIZATION_KEY]: record.idempotencyKey
697
- }
698
- },
699
- reuseMode: "preserve"
700
- });
711
+ let result;
712
+ try {
713
+ result = await this.#storage.upsert({
714
+ orgId: record.orgId,
715
+ userId: "factory-rule-dispatcher",
716
+ factoryProjectId: record.factoryProjectId,
717
+ input: {
718
+ externalSource: externalSourceForDecision(decision),
719
+ ...decision.claimKey ? { claimKey: decision.claimKey } : {},
720
+ parentWorkItemId,
721
+ title: decision.title,
722
+ board: decision.board,
723
+ stages: [initialPhase],
724
+ sessions: {},
725
+ metadata: {
726
+ ...decision.metadata,
727
+ [FACTORY_RULE_MATERIALIZATION_KEY]: record.idempotencyKey
728
+ }
729
+ },
730
+ reuseMode: "preserve"
731
+ });
732
+ } catch (error) {
733
+ if (error instanceof WorkItemClaimConflictError) return;
734
+ throw error;
735
+ }
736
+ if (!result.created && decision.claimKey && !result.item.claimKey) {
737
+ const claimed = await this.#storage.claimWorkItem({
738
+ orgId: record.orgId,
739
+ id: result.item.id,
740
+ claimKey: decision.claimKey
741
+ });
742
+ if (claimed) result = {
743
+ ...result,
744
+ item: claimed
745
+ };
746
+ }
701
747
  const itemBoard = boardForWorkItem(result.item);
702
748
  if (itemBoard !== decision.board) throw new Error(`The work item belongs to board "${itemBoard}", not "${decision.board}".`);
703
749
  if (!result.item.parentWorkItemId && parentWorkItemId && parentWorkItemId !== result.item.id) {
@@ -821,6 +867,11 @@ var FactoryDecisionDispatcher = class {
821
867
  const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId, record.workItemId);
822
868
  const own = bindings.filter((candidate) => candidate.role === role);
823
869
  if (own.some((candidate) => candidate.status === "active")) return false;
870
+ const item = await this.#storage.get({
871
+ orgId: record.orgId,
872
+ id: record.workItemId
873
+ }).catch(() => null);
874
+ if (item && workItemPhaseSemantics(this.#boards, item)?.kind === "terminal") return true;
824
875
  return own.some((revoked) => revoked.revokedAt !== null && revoked.revokedAt.getTime() >= record.createdAt.getTime() && bindings.some((successor) => successor.role !== role && successor.status === "active" && successor.resourceId === revoked.resourceId && successor.sessionId === revoked.sessionId && successor.threadId === revoked.threadId && successor.createdAt.getTime() >= revoked.revokedAt.getTime()));
825
876
  }
826
877
  async #requireOrPrepareBinding(record, role) {
@@ -864,6 +915,15 @@ var FactoryDecisionDispatcher = class {
864
915
  if (session.thread.requireId() === binding.threadId) return;
865
916
  await session.thread.switch({ threadId: binding.threadId });
866
917
  }
918
+ async #withBindingSkillRun(bindingId, effect) {
919
+ const run = (this.#bindingSkillRuns.get(bindingId) ?? Promise.resolve()).catch(() => {}).then(effect);
920
+ this.#bindingSkillRuns.set(bindingId, run);
921
+ try {
922
+ await run;
923
+ } finally {
924
+ if (this.#bindingSkillRuns.get(bindingId) === run) this.#bindingSkillRuns.delete(bindingId);
925
+ }
926
+ }
867
927
  async #withLease(renew, effect) {
868
928
  let renewalFailure;
869
929
  let renewal = Promise.resolve();
@@ -886,6 +946,17 @@ var FactoryDecisionDispatcher = class {
886
946
  await renewal;
887
947
  }
888
948
  }
949
+ /** Keep this process's claim on an open run fresh for as long as it is watching the run. */
950
+ async #withOpenRunHeartbeat(session, kickoffId, effect) {
951
+ if (!this.#audit) return effect();
952
+ const timer = setInterval(() => void heartbeatSessionOpenRun(session, kickoffId), Math.floor(FACTORY_OPEN_RUN_STALE_MS / 3));
953
+ timer.unref?.();
954
+ try {
955
+ return await effect();
956
+ } finally {
957
+ clearInterval(timer);
958
+ }
959
+ }
889
960
  async #recordRunStart(session, binding, kickoffId, approvedBy, observedEnd, workItemName) {
890
961
  if (!this.#audit) return;
891
962
  const humanApproved = isHumanActorId(approvedBy);
@@ -895,6 +966,8 @@ var FactoryDecisionDispatcher = class {
895
966
  observedEnd,
896
967
  run: {
897
968
  kickoffId,
969
+ ownerId: this.#ownerId,
970
+ heartbeatAt: Date.now(),
898
971
  bindingId: binding.id,
899
972
  role: binding.role,
900
973
  startedBy: humanApproved ? approvedBy : "factory-rule-dispatcher",
@@ -987,6 +1060,7 @@ const FACTORY_DISPATCH_CONSTANTS = {
987
1060
  maxBackoffMs: MAX_BACKOFF_MS,
988
1061
  skillCompletionObservationTimeoutMs: SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS,
989
1062
  runRegistryHeartbeatMs: RUN_REGISTRY_HEARTBEAT_MS,
1063
+ openRunStaleMs: FACTORY_OPEN_RUN_STALE_MS,
990
1064
  maxInFlight: MAX_IN_FLIGHT,
991
1065
  stages: FACTORY_RULE_STAGES
992
1066
  };