@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2

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 (174) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/auth.d.ts +2 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +401 -314
  5. package/dist/auth.js.map +1 -1
  6. package/dist/capabilities/connection.js +1 -1
  7. package/dist/capabilities/intake.js +1 -1
  8. package/dist/capabilities/version-control.js +1 -1
  9. package/dist/factory.d.ts.map +1 -1
  10. package/dist/factory.js +441 -39455
  11. package/dist/factory.js.map +1 -1
  12. package/dist/index.js +2 -39466
  13. package/dist/integrations/base.js +1 -1
  14. package/dist/integrations/github/config.js +36 -22
  15. package/dist/integrations/github/config.js.map +1 -1
  16. package/dist/integrations/github/factory-session.js +33 -37
  17. package/dist/integrations/github/factory-session.js.map +1 -1
  18. package/dist/integrations/github/integration.js +1037 -3624
  19. package/dist/integrations/github/integration.js.map +1 -1
  20. package/dist/integrations/github/issue-triage.js +71 -71
  21. package/dist/integrations/github/issue-triage.js.map +1 -1
  22. package/dist/integrations/github/pat.js +47 -36
  23. package/dist/integrations/github/pat.js.map +1 -1
  24. package/dist/integrations/github/provenance.js +72 -101
  25. package/dist/integrations/github/provenance.js.map +1 -1
  26. package/dist/integrations/github/routes.d.ts.map +1 -1
  27. package/dist/integrations/github/routes.js +1272 -1833
  28. package/dist/integrations/github/routes.js.map +1 -1
  29. package/dist/integrations/github/rules.js +203 -471
  30. package/dist/integrations/github/rules.js.map +1 -1
  31. package/dist/integrations/github/sandbox.d.ts +2 -0
  32. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  33. package/dist/integrations/github/sandbox.js +401 -326
  34. package/dist/integrations/github/sandbox.js.map +1 -1
  35. package/dist/integrations/github/session-subscriptions.js +165 -223
  36. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  37. package/dist/integrations/github/subscriptions.js +43 -51
  38. package/dist/integrations/github/subscriptions.js.map +1 -1
  39. package/dist/integrations/github/token-refresh.js +15 -18
  40. package/dist/integrations/github/token-refresh.js.map +1 -1
  41. package/dist/integrations/github/webhook.js +359 -343
  42. package/dist/integrations/github/webhook.js.map +1 -1
  43. package/dist/integrations/linear/agent-tools.js +77 -100
  44. package/dist/integrations/linear/agent-tools.js.map +1 -1
  45. package/dist/integrations/linear/integration.js +611 -1309
  46. package/dist/integrations/linear/integration.js.map +1 -1
  47. package/dist/integrations/linear/routes.js +227 -230
  48. package/dist/integrations/linear/routes.js.map +1 -1
  49. package/dist/integrations/linear/rules.js +126 -346
  50. package/dist/integrations/linear/rules.js.map +1 -1
  51. package/dist/integrations/linear/storage.js +1 -1
  52. package/dist/integrations/platform/api-client.js +131 -135
  53. package/dist/integrations/platform/api-client.js.map +1 -1
  54. package/dist/integrations/platform/github/event-worker.js +275 -573
  55. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  56. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  57. package/dist/integrations/platform/github/integration.js +845 -3942
  58. package/dist/integrations/platform/github/integration.js.map +1 -1
  59. package/dist/integrations/platform/linear/integration.js +457 -1309
  60. package/dist/integrations/platform/linear/integration.js.map +1 -1
  61. package/dist/integrations/workos/integration.js +80 -85
  62. package/dist/integrations/workos/integration.js.map +1 -1
  63. package/dist/routes/config.d.ts.map +1 -1
  64. package/dist/routes/config.js +933 -915
  65. package/dist/routes/config.js.map +1 -1
  66. package/dist/routes/custom-provider-source.js +110 -93
  67. package/dist/routes/custom-provider-source.js.map +1 -1
  68. package/dist/routes/fs.js +401 -349
  69. package/dist/routes/fs.js.map +1 -1
  70. package/dist/routes/intake.js +165 -165
  71. package/dist/routes/intake.js.map +1 -1
  72. package/dist/routes/oauth.js +447 -25166
  73. package/dist/routes/oauth.js.map +1 -1
  74. package/dist/routes/projects.js +437 -376
  75. package/dist/routes/projects.js.map +1 -1
  76. package/dist/routes/provider-credentials.js +76 -58
  77. package/dist/routes/provider-credentials.js.map +1 -1
  78. package/dist/routes/route.js +12 -8
  79. package/dist/routes/route.js.map +1 -1
  80. package/dist/routes/skills.js +160 -173
  81. package/dist/routes/skills.js.map +1 -1
  82. package/dist/routes/surface.js +250 -28618
  83. package/dist/routes/surface.js.map +1 -1
  84. package/dist/routes/tenant-credentials.js +141 -166
  85. package/dist/routes/tenant-credentials.js.map +1 -1
  86. package/dist/routes/work-items.js +654 -862
  87. package/dist/routes/work-items.js.map +1 -1
  88. package/dist/rules/binding-context.js +23 -35
  89. package/dist/rules/binding-context.js.map +1 -1
  90. package/dist/rules/defaults.js +200 -309
  91. package/dist/rules/defaults.js.map +1 -1
  92. package/dist/rules/dispatcher.d.ts.map +1 -1
  93. package/dist/rules/dispatcher.js +438 -677
  94. package/dist/rules/dispatcher.js.map +1 -1
  95. package/dist/rules/index.js +5 -599
  96. package/dist/rules/processor.js +344 -596
  97. package/dist/rules/processor.js.map +1 -1
  98. package/dist/rules/resolve.js +21 -18
  99. package/dist/rules/resolve.js.map +1 -1
  100. package/dist/rules/start-coordinator.js +168 -157
  101. package/dist/rules/start-coordinator.js.map +1 -1
  102. package/dist/rules/tools.js +50 -85
  103. package/dist/rules/tools.js.map +1 -1
  104. package/dist/rules/transition-service.js +183 -454
  105. package/dist/rules/transition-service.js.map +1 -1
  106. package/dist/rules/types.js +33 -29
  107. package/dist/rules/types.js.map +1 -1
  108. package/dist/rules/validation.js +266 -281
  109. package/dist/rules/validation.js.map +1 -1
  110. package/dist/sandbox/fleet.js +272 -250
  111. package/dist/sandbox/fleet.js.map +1 -1
  112. package/dist/sandbox/reattach.js +13 -6
  113. package/dist/sandbox/reattach.js.map +1 -1
  114. package/dist/server-error.js +12 -16
  115. package/dist/server-error.js.map +1 -1
  116. package/dist/skills/service.js +30 -35
  117. package/dist/skills/service.js.map +1 -1
  118. package/dist/spa-static.d.ts.map +1 -1
  119. package/dist/spa-static.js +91 -64
  120. package/dist/spa-static.js.map +1 -1
  121. package/dist/state-signing.js +73 -44
  122. package/dist/state-signing.js.map +1 -1
  123. package/dist/storage/domains/audit/agent-audit.js +60 -59
  124. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  125. package/dist/storage/domains/audit/base.js +169 -112
  126. package/dist/storage/domains/audit/base.js.map +1 -1
  127. package/dist/storage/domains/audit/domain.js +155 -153
  128. package/dist/storage/domains/audit/domain.js.map +1 -1
  129. package/dist/storage/domains/credentials/base.js +259 -243
  130. package/dist/storage/domains/credentials/base.js.map +1 -1
  131. package/dist/storage/domains/custom-providers/base.js +138 -143
  132. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  133. package/dist/storage/domains/intake/base.js +75 -57
  134. package/dist/storage/domains/intake/base.js.map +1 -1
  135. package/dist/storage/domains/integrations/base.js +310 -245
  136. package/dist/storage/domains/integrations/base.js.map +1 -1
  137. package/dist/storage/domains/memory-settings/base.js +117 -102
  138. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  139. package/dist/storage/domains/model-packs/base.js +89 -88
  140. package/dist/storage/domains/model-packs/base.js.map +1 -1
  141. package/dist/storage/domains/projects/base.js +97 -92
  142. package/dist/storage/domains/projects/base.js.map +1 -1
  143. package/dist/storage/domains/queue-health/base.js +111 -76
  144. package/dist/storage/domains/queue-health/base.js.map +1 -1
  145. package/dist/storage/domains/source-control/base.d.ts +9 -0
  146. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  147. package/dist/storage/domains/source-control/base.js +777 -702
  148. package/dist/storage/domains/source-control/base.js.map +1 -1
  149. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  150. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  151. package/dist/storage/domains/source-control/inmemory.js +322 -344
  152. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  153. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  154. package/dist/storage/domains/work-items/base.js +1266 -1132
  155. package/dist/storage/domains/work-items/base.js.map +1 -1
  156. package/dist/storage/domains/work-items/metrics.js +202 -184
  157. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  158. package/dist/timing.d.ts +15 -0
  159. package/dist/timing.d.ts.map +1 -0
  160. package/dist/timing.js +34 -0
  161. package/dist/timing.js.map +1 -0
  162. package/dist/workspace.d.ts +2 -3
  163. package/dist/workspace.d.ts.map +1 -1
  164. package/dist/workspace.js +236 -438
  165. package/dist/workspace.js.map +1 -1
  166. package/factory-skills/factory-review/SKILL.md +9 -2
  167. package/package.json +9 -9
  168. package/dist/capabilities/connection.js.map +0 -1
  169. package/dist/capabilities/intake.js.map +0 -1
  170. package/dist/capabilities/version-control.js.map +0 -1
  171. package/dist/index.js.map +0 -1
  172. package/dist/integrations/base.js.map +0 -1
  173. package/dist/integrations/linear/storage.js.map +0 -1
  174. package/dist/rules/index.js.map +0 -1
@@ -1,695 +1,456 @@
1
- // src/rules/dispatcher.ts
1
+ import { FACTORY_RULE_STAGES } from "./types.js";
2
+ import { validateFactoryRuleDecision } from "./validation.js";
3
+ import { resolveSkillInvocation } from "../skills/service.js";
2
4
  import { randomUUID } from "crypto";
3
5
  import { RequestContext } from "@mastra/core/request-context";
4
-
5
- // src/skills/service.ts
6
- import { formatSkillActivation } from "@mastra/core/workspace";
7
- var SkillInvocationError = class extends Error {
8
- code;
9
- constructor(code, message) {
10
- super(message);
11
- this.name = "SkillInvocationError";
12
- this.code = code;
13
- }
14
- };
15
- function escapeSkillBoundary(value) {
16
- return value.replaceAll("</skill>", "&lt;/skill&gt;");
17
- }
18
- async function resolveSkillInvocation(controller, input) {
19
- const session = await controller.getSessionByResource(input.resourceId, input.scope);
20
- if (!session) throw new SkillInvocationError("session_not_found", "Agent controller session not found.");
21
- const skills = session.getWorkspace().skills;
22
- await skills?.maybeRefresh();
23
- const skill = await skills?.get(input.name);
24
- if (!skill || skill["user-invocable"] === false) {
25
- throw new SkillInvocationError("skill_not_found", `Skill not found: ${input.name}.`);
26
- }
27
- const args = input.arguments?.trim();
28
- const content = `${formatSkillActivation(skill)}${args ? `
29
-
30
- ARGUMENTS: ${args}` : ""}`.trim();
31
- return {
32
- session,
33
- skillName: skill.name,
34
- message: `<skill name="${skill.name}">
35
- ${escapeSkillBoundary(content)}
36
- </skill>`
37
- };
38
- }
39
-
40
- // src/rules/types.ts
41
- var FACTORY_RULE_STAGES = ["intake", "triage", "planning", "execute", "review", "done", "canceled"];
42
- var FACTORY_RULE_BOARDS = ["work", "review"];
43
-
44
- // src/rules/validation.ts
45
- var MAX_FACTORY_RULE_CAUSAL_DEPTH = 8;
46
- var MAX_IDEMPOTENCY_KEY_LENGTH = 256;
47
- var MAX_REASON_LENGTH = 512;
48
- var MAX_TITLE_LENGTH = 512;
49
- var MAX_MESSAGE_LENGTH = 8192;
50
- var MAX_ARGUMENTS_LENGTH = 4096;
51
- var MAX_ROLE_LENGTH = 32;
52
- var MAX_SKILL_NAME_LENGTH = 128;
53
- var MAX_SOURCE_KEY_LENGTH = 256;
54
- var MAX_URL_LENGTH = 2048;
55
- var MAX_METADATA_JSON_LENGTH = 16384;
56
- var MAX_JSON_DEPTH = 8;
57
- var MAX_JSON_COLLECTION_SIZE = 100;
58
- var IDENTIFIER_RE = /^[a-z0-9][a-z0-9_-]*$/i;
59
- var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
60
- var SENSITIVE_KEY_RE = /(?:authorization|cookie|credential|password|secret|token)/i;
61
- var WORK_ITEM_SOURCES = ["github-issue", "github-pr", "linear-issue", "manual"];
62
- var REJECTION_CODES = [
63
- "forbidden",
64
- "invalid_transition",
65
- "missing_binding",
66
- "stale",
67
- "timeout",
68
- "rule_error",
69
- "causal_depth_exceeded",
70
- "repeated_transition"
71
- ];
72
- var FactoryRuleValidationError = class extends Error {
73
- code = "invalid_factory_rule";
74
- constructor(message) {
75
- super(message);
76
- this.name = "FactoryRuleValidationError";
77
- }
78
- };
79
- function isPlainObject(value) {
80
- if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
81
- const prototype = Object.getPrototypeOf(value);
82
- return prototype === Object.prototype || prototype === null;
83
- }
84
- function assertExactKeys(value, keys, label) {
85
- const allowed = new Set(keys);
86
- if (Object.keys(value).some((key) => !allowed.has(key))) {
87
- throw new FactoryRuleValidationError(`${label} contains an unsupported field.`);
88
- }
89
- }
90
- function boundedString(value, label, max, pattern) {
91
- if (typeof value !== "string") throw new FactoryRuleValidationError(`${label} must be a string.`);
92
- const normalized = value.trim();
93
- if (normalized.length === 0 || normalized.length > max || pattern && !pattern.test(normalized)) {
94
- throw new FactoryRuleValidationError(`${label} is invalid.`);
95
- }
96
- return normalized;
97
- }
98
- function optionalBoundedString(value, label, max) {
99
- if (value === void 0) return void 0;
100
- return boundedString(value, label, max);
101
- }
102
- function enumValue(value, allowed, label) {
103
- if (typeof value !== "string" || !allowed.includes(value)) {
104
- throw new FactoryRuleValidationError(`${label} is invalid.`);
105
- }
106
- return value;
107
- }
108
- function normalizeFactoryRuleJsonValue(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
109
- if (value === null || typeof value === "boolean" || typeof value === "string") return value;
110
- if (typeof value === "number") {
111
- if (!Number.isFinite(value)) throw new FactoryRuleValidationError("Rule metadata must contain finite numbers.");
112
- return value;
113
- }
114
- if (depth >= MAX_JSON_DEPTH || typeof value !== "object" && !Array.isArray(value)) {
115
- throw new FactoryRuleValidationError("Rule metadata is not bounded JSON.");
116
- }
117
- if (seen.has(value)) throw new FactoryRuleValidationError("Rule metadata must not contain cycles.");
118
- seen.add(value);
119
- try {
120
- if (Array.isArray(value)) {
121
- if (value.length > MAX_JSON_COLLECTION_SIZE) {
122
- throw new FactoryRuleValidationError("Rule metadata contains too many entries.");
123
- }
124
- return value.map((entry) => normalizeFactoryRuleJsonValue(entry, depth + 1, seen));
125
- }
126
- if (!isPlainObject(value)) throw new FactoryRuleValidationError("Rule metadata must use plain objects.");
127
- const entries = Object.entries(value);
128
- if (entries.length > MAX_JSON_COLLECTION_SIZE) {
129
- throw new FactoryRuleValidationError("Rule metadata contains too many fields.");
130
- }
131
- const sanitized = {};
132
- for (const [key, entry] of entries) {
133
- const normalizedKey = boundedString(key, "Rule metadata key", 128, IDENTIFIER_RE);
134
- sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey) ? "[REDACTED]" : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);
135
- }
136
- return sanitized;
137
- } finally {
138
- seen.delete(value);
139
- }
140
- }
141
- function sanitizeMetadata(value) {
142
- if (value === void 0) return void 0;
143
- const sanitized = normalizeFactoryRuleJsonValue(value);
144
- if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError("Rule metadata must be an object.");
145
- if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {
146
- throw new FactoryRuleValidationError("Rule metadata is too large.");
147
- }
148
- return sanitized;
149
- }
150
- function commonCommitFields(value) {
151
- return {
152
- idempotencyKey: boundedString(value.idempotencyKey, "Factory decision idempotencyKey", MAX_IDEMPOTENCY_KEY_LENGTH)
153
- };
154
- }
155
- function validateFactoryRuleDecision(value, causalDepth = 0) {
156
- if (causalDepth > MAX_FACTORY_RULE_CAUSAL_DEPTH) {
157
- throw new FactoryRuleValidationError("Factory rule causal depth exceeded.");
158
- }
159
- if (!isPlainObject(value)) throw new FactoryRuleValidationError("Factory rule decision must be an object.");
160
- const type = value.type;
161
- if (typeof type !== "string") throw new FactoryRuleValidationError("Factory rule decision type is required.");
162
- switch (type) {
163
- case "reject": {
164
- assertExactKeys(value, ["type", "code", "reason"], "Factory reject decision");
165
- return {
166
- type,
167
- code: enumValue(value.code, REJECTION_CODES, "Factory rejection code"),
168
- reason: boundedString(value.reason, "Factory rejection reason", MAX_REASON_LENGTH)
169
- };
170
- }
171
- case "transition": {
172
- assertExactKeys(value, ["type", "idempotencyKey", "board", "stage"], "Factory transition decision");
173
- return {
174
- type,
175
- ...commonCommitFields(value),
176
- board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory transition board"),
177
- stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory transition stage")
178
- };
179
- }
180
- case "upsertLinkedWorkItem": {
181
- assertExactKeys(
182
- value,
183
- ["type", "idempotencyKey", "board", "source", "sourceKey", "title", "url", "stage", "metadata"],
184
- "Factory linked work item decision"
185
- );
186
- const url = value.url;
187
- if (url !== null && (typeof url !== "string" || url.length > MAX_URL_LENGTH || !/^https?:\/\//.test(url))) {
188
- throw new FactoryRuleValidationError("Factory linked work item URL is invalid.");
189
- }
190
- const metadata = sanitizeMetadata(value.metadata);
191
- return {
192
- type,
193
- ...commonCommitFields(value),
194
- board: enumValue(value.board, FACTORY_RULE_BOARDS, "Factory linked work item board"),
195
- source: enumValue(value.source, WORK_ITEM_SOURCES, "Factory linked work item source"),
196
- sourceKey: boundedString(value.sourceKey, "Factory linked work item sourceKey", MAX_SOURCE_KEY_LENGTH),
197
- title: boundedString(value.title, "Factory linked work item title", MAX_TITLE_LENGTH),
198
- url,
199
- stage: enumValue(value.stage, FACTORY_RULE_STAGES, "Factory linked work item stage"),
200
- ...metadata ? { metadata } : {}
201
- };
202
- }
203
- case "invokeSkill": {
204
- assertExactKeys(
205
- value,
206
- ["type", "idempotencyKey", "role", "skillName", "arguments", "precedingMessage"],
207
- "Factory invoke skill decision"
208
- );
209
- const args = optionalBoundedString(value.arguments, "Factory skill arguments", MAX_ARGUMENTS_LENGTH);
210
- const precedingMessage = optionalBoundedString(
211
- value.precedingMessage,
212
- "Factory skill preceding message",
213
- MAX_MESSAGE_LENGTH
214
- );
215
- return {
216
- type,
217
- ...commonCommitFields(value),
218
- role: boundedString(value.role, "Factory skill role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
219
- skillName: boundedString(value.skillName, "Factory skill name", MAX_SKILL_NAME_LENGTH, SKILL_NAME_RE),
220
- ...args ? { arguments: args } : {},
221
- ...precedingMessage ? { precedingMessage } : {}
222
- };
223
- }
224
- case "sendMessage": {
225
- assertExactKeys(
226
- value,
227
- ["type", "idempotencyKey", "role", "message", "priority", "idleBehavior", "prepareBinding"],
228
- "Factory send message decision"
229
- );
230
- const priority = value.priority === void 0 ? void 0 : enumValue(value.priority, ["medium", "high", "urgent"], "Factory message priority");
231
- const idleBehavior = value.idleBehavior === void 0 ? void 0 : enumValue(value.idleBehavior, ["persist", "wake"], "Factory message idle behavior");
232
- if (value.prepareBinding !== void 0 && typeof value.prepareBinding !== "boolean") {
233
- throw new FactoryRuleValidationError("Factory message prepareBinding must be a boolean.");
234
- }
235
- return {
236
- type,
237
- ...commonCommitFields(value),
238
- role: boundedString(value.role, "Factory message role", MAX_ROLE_LENGTH, IDENTIFIER_RE),
239
- message: boundedString(value.message, "Factory message", MAX_MESSAGE_LENGTH),
240
- ...priority ? { priority } : {},
241
- ...idleBehavior ? { idleBehavior } : {},
242
- ...value.prepareBinding === true ? { prepareBinding: true } : {}
243
- };
244
- }
245
- case "notify": {
246
- assertExactKeys(value, ["type", "idempotencyKey", "title", "body", "level"], "Factory notify decision");
247
- const body = optionalBoundedString(value.body, "Factory notification body", MAX_MESSAGE_LENGTH);
248
- const level = value.level === void 0 ? void 0 : enumValue(value.level, ["info", "warning", "error"], "Factory notification level");
249
- return {
250
- type,
251
- ...commonCommitFields(value),
252
- title: boundedString(value.title, "Factory notification title", MAX_TITLE_LENGTH),
253
- ...body ? { body } : {},
254
- ...level ? { level } : {}
255
- };
256
- }
257
- default:
258
- throw new FactoryRuleValidationError("Factory rule decision type is unsupported.");
259
- }
260
- }
261
-
262
- // src/rules/dispatcher.ts
263
- var LEASE_MS = 3e4;
264
- var POLL_MS = 1e3;
265
- var BATCH_SIZE = 10;
266
- var MAX_ATTEMPTS = 5;
267
- var MAX_ERROR_LENGTH = 512;
268
- var MAX_BACKOFF_MS = 6e4;
6
+ //#region src/rules/dispatcher.ts
7
+ const LEASE_MS = 3e4;
8
+ const POLL_MS = 1e3;
9
+ const BATCH_SIZE = 10;
10
+ const MAX_ATTEMPTS = 5;
11
+ const MAX_ERROR_LENGTH = 512;
12
+ const MAX_BACKOFF_MS = 6e4;
269
13
  function sanitizeDispatchError(error) {
270
- const message = error instanceof Error ? error.message : String(error);
271
- return message.replace(/\b(?:bearer|token|api[-_ ]?key|authorization)\s*[:=]?\s*[^\s,;]+/gi, "[redacted]").slice(0, MAX_ERROR_LENGTH);
14
+ return (error instanceof Error ? error.message : String(error)).replace(/\b(?:bearer|token|api[-_ ]?key|authorization)\s*[:=]?\s*[^\s,;]+/gi, "[redacted]").slice(0, MAX_ERROR_LENGTH);
272
15
  }
273
16
  function retryAt(now, attempts) {
274
- return new Date(now.getTime() + Math.min(1e3 * 2 ** Math.max(0, attempts - 1), MAX_BACKOFF_MS));
17
+ return new Date(now.getTime() + Math.min(1e3 * 2 ** Math.max(0, attempts - 1), MAX_BACKOFF_MS));
275
18
  }
276
19
  function externalSourceForDecision(decision) {
277
- const [integrationId, type] = decision.source === "github-pr" ? ["github", "pull-request"] : decision.source === "github-issue" ? ["github", "issue"] : decision.source === "linear-issue" ? ["linear", "issue"] : ["factory", "manual"];
278
- return { integrationId, type, externalId: decision.sourceKey, url: decision.url ?? void 0 };
20
+ const [integrationId, type] = decision.source === "github-pr" ? ["github", "pull-request"] : decision.source === "github-issue" ? ["github", "issue"] : decision.source === "linear-issue" ? ["linear", "issue"] : ["factory", "manual"];
21
+ return {
22
+ integrationId,
23
+ type,
24
+ externalId: decision.sourceKey,
25
+ url: decision.url ?? void 0
26
+ };
279
27
  }
280
28
  function deferredActor(record) {
281
- const actor = record.actor;
282
- if (actor?.type === "github" && typeof actor.login === "string" && typeof actor.trusted === "boolean" && typeof actor.factoryAuthored === "boolean") {
283
- return {
284
- type: "github",
285
- login: actor.login,
286
- trusted: actor.trusted,
287
- factoryAuthored: actor.factoryAuthored
288
- };
289
- }
290
- return { type: "system", id: "factory-rule-dispatcher" };
29
+ const actor = record.actor;
30
+ if (actor?.type === "github" && typeof actor.login === "string" && typeof actor.trusted === "boolean" && typeof actor.factoryAuthored === "boolean") return {
31
+ type: "github",
32
+ login: actor.login,
33
+ trusted: actor.trusted,
34
+ factoryAuthored: actor.factoryAuthored
35
+ };
36
+ return {
37
+ type: "system",
38
+ id: "factory-rule-dispatcher"
39
+ };
291
40
  }
292
41
  function leaseIdentity(record, ownerId) {
293
- return { id: record.id, orgId: record.orgId, factoryProjectId: record.factoryProjectId, ownerId };
42
+ return {
43
+ id: record.id,
44
+ orgId: record.orgId,
45
+ factoryProjectId: record.factoryProjectId,
46
+ ownerId
47
+ };
294
48
  }
295
49
  async function awaitNotification(result, requireDelivery = false) {
296
- await result.persisted;
297
- if (!result.accepted) {
298
- if (requireDelivery) throw new Error("Factory notification was persisted without agent delivery.");
299
- return;
300
- }
301
- const accepted = await result.accepted;
302
- if (!requireDelivery) return;
303
- if (accepted.action === "wake") {
304
- await accepted.output.consumeStream();
305
- return;
306
- }
307
- if (accepted.action !== "deliver") {
308
- throw new Error(`Factory notification did not reach the agent (${String(accepted.action)}).`);
309
- }
50
+ await result.persisted;
51
+ if (!result.accepted) {
52
+ if (requireDelivery) throw new Error("Factory notification was persisted without agent delivery.");
53
+ return;
54
+ }
55
+ const accepted = await result.accepted;
56
+ if (!requireDelivery) return;
57
+ if (accepted.action === "wake") {
58
+ await accepted.output.consumeStream();
59
+ return;
60
+ }
61
+ if (accepted.action !== "deliver") throw new Error(`Factory notification did not reach the agent (${String(accepted.action)}).`);
310
62
  }
311
63
  var FactoryDecisionDispatcher = class {
312
- #controller;
313
- #transitionService;
314
- #storage;
315
- #ownerId;
316
- #reconcileToolResults;
317
- #prepareBinding;
318
- #primeCredentials;
319
- #timer;
320
- #activeRun;
321
- constructor(options) {
322
- this.#controller = options.controller;
323
- this.#transitionService = options.transitionService;
324
- this.#storage = options.storage;
325
- this.#ownerId = options.ownerId ?? `factory-dispatcher:${randomUUID()}`;
326
- this.#reconcileToolResults = options.reconcileToolResults;
327
- this.#prepareBinding = options.prepareBinding;
328
- this.#primeCredentials = options.primeCredentials;
329
- }
330
- start() {
331
- if (this.#timer) return;
332
- void this.#tick();
333
- this.#timer = setInterval(() => void this.#tick(), POLL_MS);
334
- this.#timer.unref?.();
335
- }
336
- async stop() {
337
- if (this.#timer) clearInterval(this.#timer);
338
- this.#timer = void 0;
339
- await this.#activeRun;
340
- }
341
- async runOnce(now = /* @__PURE__ */ new Date()) {
342
- await this.#reconcileToolResults?.();
343
- const leaseExpiresAt = new Date(now.getTime() + LEASE_MS);
344
- const [decisions, starts] = await Promise.all([
345
- this.#storage.claimDeferredDecisions({
346
- ownerId: this.#ownerId,
347
- now,
348
- leaseExpiresAt,
349
- limit: BATCH_SIZE
350
- }),
351
- this.#storage.claimPendingStarts({
352
- ownerId: this.#ownerId,
353
- now,
354
- leaseExpiresAt,
355
- limit: BATCH_SIZE
356
- })
357
- ]);
358
- await Promise.all([
359
- ...decisions.map((decision) => this.#dispatchDecision(decision, now)),
360
- ...starts.map((start) => this.#dispatchPendingStart(start, now))
361
- ]);
362
- }
363
- async #tick() {
364
- if (this.#activeRun) return;
365
- this.#activeRun = this.runOnce().catch((error) => {
366
- console.error("Factory decision dispatch cycle failed", sanitizeDispatchError(error));
367
- });
368
- try {
369
- await this.#activeRun;
370
- } finally {
371
- this.#activeRun = void 0;
372
- }
373
- }
374
- async #dispatchDecision(record, now) {
375
- try {
376
- const decision = validateFactoryRuleDecision(record.decision, record.causalChain.length);
377
- if (decision.type === "reject") throw new Error("Deferred Factory decisions cannot reject.");
378
- await this.#withLease(
379
- async (leaseExpiresAt) => this.#storage.renewDeferredDecisionLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),
380
- async () => this.#executeDecision(record, decision)
381
- );
382
- const completed = await this.#storage.completeDeferredDecision(leaseIdentity(record, this.#ownerId), /* @__PURE__ */ new Date());
383
- if (!completed) throw new Error("Factory decision lease was lost before completion.");
384
- } catch (error) {
385
- const terminal = record.attempts >= MAX_ATTEMPTS;
386
- await this.#storage.failDeferredDecision({
387
- ...leaseIdentity(record, this.#ownerId),
388
- now: /* @__PURE__ */ new Date(),
389
- availableAt: retryAt(now, record.attempts),
390
- lastError: sanitizeDispatchError(error),
391
- terminal
392
- });
393
- }
394
- }
395
- async #executeDecision(record, decision) {
396
- const nextChain = [
397
- ...record.causalChain,
398
- { ingressId: record.idempotencyKey, decisionType: decision.type }
399
- ];
400
- if (nextChain.length > MAX_FACTORY_RULE_CAUSAL_DEPTH) throw new Error("Factory rule causal depth exceeded.");
401
- switch (decision.type) {
402
- case "transition": {
403
- const item = await this.#requireItem(record);
404
- const result = await this.#transitionService.transition({
405
- orgId: record.orgId,
406
- factoryProjectId: record.factoryProjectId,
407
- workItemId: item.id,
408
- board: decision.board,
409
- stage: decision.stage,
410
- expectedRevision: item.revision,
411
- actor: { type: "system", id: "factory-rule-dispatcher" },
412
- ingress: { type: "rule", identity: `decision:${record.idempotencyKey}` },
413
- cause: "rule_decision",
414
- causalChain: nextChain
415
- });
416
- if (result.status === "rejected") throw new Error(`${result.code}: ${result.reason}`);
417
- return;
418
- }
419
- case "upsertLinkedWorkItem": {
420
- await this.#upsertLinkedItem(record, decision, nextChain);
421
- return;
422
- }
423
- case "invokeSkill": {
424
- const binding = await this.#requireOrPrepareBinding(record, decision.role);
425
- const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;
426
- const startedBy = item?.sessions[binding.role]?.startedBy;
427
- if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);
428
- await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });
429
- const requestContext = new RequestContext();
430
- requestContext.set("user", { workosId: startedBy, organizationId: record.orgId });
431
- const resolved = await resolveSkillInvocation(this.#controller, {
432
- resourceId: binding.resourceId,
433
- name: decision.skillName,
434
- arguments: decision.arguments
435
- });
436
- const session = resolved.session;
437
- await this.#switchThread(session, binding);
438
- const delivered = await session.thread.listActiveMessages();
439
- if (delivered.some((message) => message.id === record.id)) return;
440
- if (decision.precedingMessage) {
441
- await awaitNotification(
442
- await session.sendNotificationSignal(
443
- {
444
- source: "factory",
445
- kind: "stage-transition",
446
- summary: decision.precedingMessage,
447
- priority: "medium",
448
- payload: { message: decision.precedingMessage },
449
- sourceId: `${record.id}:stage-transition`,
450
- dedupeKey: `${record.idempotencyKey}:stage-transition`
451
- },
452
- {
453
- ifActive: { behavior: "deliver" },
454
- ifIdle: { behavior: "persist" },
455
- requestContext
456
- }
457
- )
458
- );
459
- }
460
- const result = session.sendSignal(
461
- {
462
- id: record.id,
463
- type: "user",
464
- tagName: "user",
465
- contents: resolved.message
466
- },
467
- { requestContext }
468
- );
469
- await result.accepted;
470
- return;
471
- }
472
- case "sendMessage": {
473
- const binding = decision.prepareBinding ? await this.#requireOrPrepareBinding(record, decision.role) : await this.#requireBinding(record, decision.role);
474
- const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;
475
- const startedBy = item?.sessions[binding.role]?.startedBy;
476
- if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);
477
- await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });
478
- const requestContext = new RequestContext();
479
- requestContext.set("user", { workosId: startedBy, organizationId: record.orgId });
480
- const session = await this.#requireSession(binding);
481
- await awaitNotification(
482
- await session.sendNotificationSignal(
483
- {
484
- source: "factory",
485
- kind: "rule-message",
486
- summary: decision.message,
487
- priority: decision.priority ?? "high",
488
- payload: { message: decision.message },
489
- sourceId: record.id,
490
- dedupeKey: record.idempotencyKey
491
- },
492
- {
493
- ifActive: { behavior: "deliver" },
494
- ifIdle: { behavior: decision.idleBehavior ?? "wake" },
495
- requestContext
496
- }
497
- ),
498
- true
499
- );
500
- return;
501
- }
502
- case "notify": {
503
- const binding = await this.#requireBinding(record);
504
- const session = await this.#requireSession(binding);
505
- await awaitNotification(
506
- await session.sendNotificationSignal({
507
- source: "factory",
508
- kind: "rule-notification",
509
- summary: decision.title,
510
- payload: { body: decision.body, level: decision.level },
511
- sourceId: record.id,
512
- dedupeKey: record.idempotencyKey
513
- })
514
- );
515
- }
516
- }
517
- }
518
- async #upsertLinkedItem(record, decision, causalChain) {
519
- const result = await this.#storage.upsert({
520
- orgId: record.orgId,
521
- userId: "factory-rule-dispatcher",
522
- factoryProjectId: record.factoryProjectId,
523
- input: {
524
- externalSource: externalSourceForDecision(decision),
525
- parentWorkItemId: record.workItemId,
526
- title: decision.title,
527
- stages: ["intake"],
528
- sessions: {},
529
- metadata: { ...decision.metadata, factoryRuleMaterializationKey: record.idempotencyKey }
530
- },
531
- reuseMode: "preserve"
532
- });
533
- const materializedByDecision = result.item.metadata?.factoryRuleMaterializationKey === record.idempotencyKey;
534
- if (!materializedByDecision && (decision.stage === "intake" || !result.item.stages.includes("intake"))) return;
535
- const board = decision.board;
536
- let expectedRevision = result.item.revision;
537
- if (materializedByDecision) {
538
- const initial = await this.#transitionService.transition({
539
- orgId: record.orgId,
540
- factoryProjectId: record.factoryProjectId,
541
- workItemId: result.item.id,
542
- board,
543
- stage: "intake",
544
- expectedRevision,
545
- actor: deferredActor(record),
546
- ingress: { type: "rule", identity: `decision:${record.idempotencyKey}:${result.item.id}:initial-entry` },
547
- cause: "linked_item_materialized",
548
- causalChain,
549
- initialEntry: true
550
- });
551
- if (initial.status === "rejected") {
552
- if (result.created) await this.#storage.delete({ orgId: record.orgId, id: result.item.id });
553
- throw new Error(`${initial.code}: ${initial.reason}`);
554
- }
555
- expectedRevision = initial.revision;
556
- }
557
- if (decision.stage === "intake") return;
558
- const moved = await this.#transitionService.transition({
559
- orgId: record.orgId,
560
- factoryProjectId: record.factoryProjectId,
561
- workItemId: result.item.id,
562
- board,
563
- stage: decision.stage,
564
- expectedRevision,
565
- actor: { type: "system", id: "factory-rule-dispatcher" },
566
- ingress: { type: "rule", identity: `decision:${record.idempotencyKey}:${result.item.id}:destination` },
567
- cause: materializedByDecision ? "linked_item_materialized" : "linked_item_reconciled",
568
- causalChain
569
- });
570
- if (moved.status === "rejected") throw new Error(`${moved.code}: ${moved.reason}`);
571
- }
572
- async #requireItem(record) {
573
- if (!record.workItemId) throw new Error("Factory decision is not linked to a work item.");
574
- const item = await this.#storage.get({ orgId: record.orgId, id: record.workItemId });
575
- if (!item) throw new Error("Factory work item not found.");
576
- return item;
577
- }
578
- async #findBinding(record, role) {
579
- if (!record.workItemId) throw new Error("Factory decision is not linked to a work item.");
580
- const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId, record.workItemId);
581
- return bindings.filter((candidate) => candidate.status === "active" && (role === void 0 || candidate.role === role)).sort((left, right) => {
582
- if (role === void 0 && left.role === "work" && right.role !== "work") return -1;
583
- if (role === void 0 && right.role === "work" && left.role !== "work") return 1;
584
- return right.createdAt.getTime() - left.createdAt.getTime() || left.id.localeCompare(right.id);
585
- })[0];
586
- }
587
- async #requireBinding(record, role) {
588
- const binding = await this.#findBinding(record, role);
589
- if (!binding) throw new Error(role ? `No active Factory binding for role ${role}.` : "No active Factory binding.");
590
- return binding;
591
- }
592
- async #requireOrPrepareBinding(record, role) {
593
- const binding = await this.#findBinding(record, role);
594
- if (binding) {
595
- const session = await this.#controller.getSessionByResource(binding.resourceId);
596
- if (session) return binding;
597
- }
598
- if (!this.#prepareBinding) {
599
- throw new Error(binding ? "Bound Factory session not found." : `No active Factory binding for role ${role}.`);
600
- }
601
- const item = await this.#requireItem(record);
602
- await this.#prepareBinding({ record, item, role });
603
- return this.#requireBinding(record, role);
604
- }
605
- async #requireSession(binding) {
606
- const session = await this.#controller.getSessionByResource(binding.resourceId);
607
- if (!session) throw new Error("Bound Factory session not found.");
608
- await this.#switchThread(session, binding);
609
- return session;
610
- }
611
- async #switchThread(session, binding) {
612
- await session.thread.switch({ threadId: binding.threadId });
613
- }
614
- async #withLease(renew, effect) {
615
- let renewalFailure;
616
- let renewal = Promise.resolve();
617
- const timer = setInterval(
618
- () => {
619
- renewal = renewal.then(async () => {
620
- try {
621
- const renewed = await renew(new Date(Date.now() + LEASE_MS));
622
- if (!renewed) renewalFailure = new Error("Factory dispatch lease was lost during execution.");
623
- } catch (error) {
624
- renewalFailure = error;
625
- }
626
- });
627
- },
628
- Math.floor(LEASE_MS / 3)
629
- );
630
- timer.unref?.();
631
- try {
632
- await effect();
633
- await renewal;
634
- if (renewalFailure) throw renewalFailure;
635
- } finally {
636
- clearInterval(timer);
637
- await renewal;
638
- }
639
- }
640
- async #dispatchPendingStart(record, now) {
641
- try {
642
- await this.#withLease(
643
- async (leaseExpiresAt) => this.#storage.renewPendingStartLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),
644
- async () => {
645
- if (record.message === null) return;
646
- const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId);
647
- const binding = bindings.find(
648
- (candidate) => candidate.id === record.bindingId && candidate.status === "active"
649
- );
650
- if (!binding) throw new Error("Prepared Factory binding is unavailable or revoked.");
651
- const session = await this.#requireSession(binding);
652
- await awaitNotification(
653
- await session.sendNotificationSignal(
654
- {
655
- source: "factory",
656
- kind: "run-kickoff",
657
- summary: record.message,
658
- priority: "high",
659
- payload: { message: record.message },
660
- sourceId: record.id,
661
- dedupeKey: `factory-kickoff:${record.kickoffKey}`
662
- },
663
- { ifActive: { behavior: "deliver" }, ifIdle: { behavior: "wake" } }
664
- ),
665
- true
666
- );
667
- }
668
- );
669
- const completed = await this.#storage.completePendingStart(leaseIdentity(record, this.#ownerId), /* @__PURE__ */ new Date());
670
- if (!completed) throw new Error("Factory kickoff lease was lost before completion.");
671
- } catch (error) {
672
- await this.#storage.failPendingStart({
673
- ...leaseIdentity(record, this.#ownerId),
674
- now: /* @__PURE__ */ new Date(),
675
- availableAt: retryAt(now, record.attempts),
676
- lastError: sanitizeDispatchError(error),
677
- terminal: record.attempts >= MAX_ATTEMPTS
678
- });
679
- }
680
- }
64
+ #controller;
65
+ #transitionService;
66
+ #storage;
67
+ #ownerId;
68
+ #reconcileToolResults;
69
+ #prepareBinding;
70
+ #primeCredentials;
71
+ #timer;
72
+ #activeRun;
73
+ constructor(options) {
74
+ this.#controller = options.controller;
75
+ this.#transitionService = options.transitionService;
76
+ this.#storage = options.storage;
77
+ this.#ownerId = options.ownerId ?? `factory-dispatcher:${randomUUID()}`;
78
+ this.#reconcileToolResults = options.reconcileToolResults;
79
+ this.#prepareBinding = options.prepareBinding;
80
+ this.#primeCredentials = options.primeCredentials;
81
+ }
82
+ start() {
83
+ if (this.#timer) return;
84
+ this.#tick();
85
+ this.#timer = setInterval(() => void this.#tick(), POLL_MS);
86
+ this.#timer.unref?.();
87
+ }
88
+ async stop() {
89
+ if (this.#timer) clearInterval(this.#timer);
90
+ this.#timer = void 0;
91
+ await this.#activeRun;
92
+ }
93
+ async runOnce(now = /* @__PURE__ */ new Date()) {
94
+ await this.#reconcileToolResults?.();
95
+ const leaseExpiresAt = new Date(now.getTime() + LEASE_MS);
96
+ const [decisions, starts] = await Promise.all([this.#storage.claimDeferredDecisions({
97
+ ownerId: this.#ownerId,
98
+ now,
99
+ leaseExpiresAt,
100
+ limit: BATCH_SIZE
101
+ }), this.#storage.claimPendingStarts({
102
+ ownerId: this.#ownerId,
103
+ now,
104
+ leaseExpiresAt,
105
+ limit: BATCH_SIZE
106
+ })]);
107
+ await Promise.all([...decisions.map((decision) => this.#dispatchDecision(decision, now)), ...starts.map((start) => this.#dispatchPendingStart(start, now))]);
108
+ }
109
+ async #tick() {
110
+ if (this.#activeRun) return;
111
+ this.#activeRun = this.runOnce().catch((error) => {
112
+ console.error("Factory decision dispatch cycle failed", sanitizeDispatchError(error));
113
+ });
114
+ try {
115
+ await this.#activeRun;
116
+ } finally {
117
+ this.#activeRun = void 0;
118
+ }
119
+ }
120
+ async #dispatchDecision(record, now) {
121
+ try {
122
+ const decision = validateFactoryRuleDecision(record.decision, record.causalChain.length);
123
+ if (decision.type === "reject") throw new Error("Deferred Factory decisions cannot reject.");
124
+ await this.#withLease(async (leaseExpiresAt) => this.#storage.renewDeferredDecisionLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt), async () => this.#executeDecision(record, decision));
125
+ if (!await this.#storage.completeDeferredDecision(leaseIdentity(record, this.#ownerId), /* @__PURE__ */ new Date())) throw new Error("Factory decision lease was lost before completion.");
126
+ } catch (error) {
127
+ const terminal = record.attempts >= MAX_ATTEMPTS;
128
+ await this.#storage.failDeferredDecision({
129
+ ...leaseIdentity(record, this.#ownerId),
130
+ now: /* @__PURE__ */ new Date(),
131
+ availableAt: retryAt(now, record.attempts),
132
+ lastError: sanitizeDispatchError(error),
133
+ terminal
134
+ });
135
+ }
136
+ }
137
+ async #executeDecision(record, decision) {
138
+ const nextChain = [...record.causalChain, {
139
+ ingressId: record.idempotencyKey,
140
+ decisionType: decision.type
141
+ }];
142
+ if (nextChain.length > 8) throw new Error("Factory rule causal depth exceeded.");
143
+ switch (decision.type) {
144
+ case "transition": {
145
+ const item = await this.#requireItem(record);
146
+ const result = await this.#transitionService.transition({
147
+ orgId: record.orgId,
148
+ factoryProjectId: record.factoryProjectId,
149
+ workItemId: item.id,
150
+ board: decision.board,
151
+ stage: decision.stage,
152
+ expectedRevision: item.revision,
153
+ actor: {
154
+ type: "system",
155
+ id: "factory-rule-dispatcher"
156
+ },
157
+ ingress: {
158
+ type: "rule",
159
+ identity: `decision:${record.idempotencyKey}`
160
+ },
161
+ cause: "rule_decision",
162
+ causalChain: nextChain
163
+ });
164
+ if (result.status === "rejected") throw new Error(`${result.code}: ${result.reason}`);
165
+ return;
166
+ }
167
+ case "upsertLinkedWorkItem":
168
+ await this.#upsertLinkedItem(record, decision, nextChain);
169
+ return;
170
+ case "invokeSkill": {
171
+ const binding = await this.#requireOrPrepareBinding(record, decision.role);
172
+ const startedBy = (record.workItemId ? await this.#storage.get({
173
+ orgId: record.orgId,
174
+ id: record.workItemId
175
+ }) : null)?.sessions[binding.role]?.startedBy;
176
+ if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);
177
+ await this.#primeCredentials?.({
178
+ orgId: record.orgId,
179
+ userId: startedBy
180
+ });
181
+ const requestContext = new RequestContext();
182
+ requestContext.set("user", {
183
+ workosId: startedBy,
184
+ organizationId: record.orgId
185
+ });
186
+ const resolved = await resolveSkillInvocation(this.#controller, {
187
+ resourceId: binding.resourceId,
188
+ name: decision.skillName,
189
+ arguments: decision.arguments
190
+ });
191
+ const session = resolved.session;
192
+ await this.#switchThread(session, binding);
193
+ if ((await session.thread.listActiveMessages()).some((message) => message.id === record.id)) return;
194
+ if (decision.precedingMessage) await awaitNotification(await session.sendNotificationSignal({
195
+ source: "factory",
196
+ kind: "stage-transition",
197
+ summary: decision.precedingMessage,
198
+ priority: "medium",
199
+ payload: { message: decision.precedingMessage },
200
+ sourceId: `${record.id}:stage-transition`,
201
+ dedupeKey: `${record.idempotencyKey}:stage-transition`
202
+ }, {
203
+ ifActive: { behavior: "deliver" },
204
+ ifIdle: { behavior: "persist" },
205
+ requestContext
206
+ }));
207
+ await session.sendSignal({
208
+ id: record.id,
209
+ type: "user",
210
+ tagName: "user",
211
+ contents: resolved.message
212
+ }, { requestContext }).accepted;
213
+ return;
214
+ }
215
+ case "sendMessage": {
216
+ const binding = decision.prepareBinding ? await this.#requireOrPrepareBinding(record, decision.role) : await this.#requireBinding(record, decision.role);
217
+ const startedBy = (record.workItemId ? await this.#storage.get({
218
+ orgId: record.orgId,
219
+ id: record.workItemId
220
+ }) : null)?.sessions[binding.role]?.startedBy;
221
+ if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);
222
+ await this.#primeCredentials?.({
223
+ orgId: record.orgId,
224
+ userId: startedBy
225
+ });
226
+ const requestContext = new RequestContext();
227
+ requestContext.set("user", {
228
+ workosId: startedBy,
229
+ organizationId: record.orgId
230
+ });
231
+ await awaitNotification(await (await this.#requireSession(binding)).sendNotificationSignal({
232
+ source: "factory",
233
+ kind: "rule-message",
234
+ summary: decision.message,
235
+ priority: decision.priority ?? "high",
236
+ payload: { message: decision.message },
237
+ sourceId: record.id,
238
+ dedupeKey: record.idempotencyKey
239
+ }, {
240
+ ifActive: { behavior: "deliver" },
241
+ ifIdle: { behavior: decision.idleBehavior ?? "wake" },
242
+ requestContext
243
+ }), true);
244
+ return;
245
+ }
246
+ case "notify": {
247
+ const binding = await this.#requireBinding(record);
248
+ await awaitNotification(await (await this.#requireSession(binding)).sendNotificationSignal({
249
+ source: "factory",
250
+ kind: "rule-notification",
251
+ summary: decision.title,
252
+ payload: {
253
+ body: decision.body,
254
+ level: decision.level
255
+ },
256
+ sourceId: record.id,
257
+ dedupeKey: record.idempotencyKey
258
+ }));
259
+ }
260
+ }
261
+ }
262
+ async #upsertLinkedItem(record, decision, causalChain) {
263
+ const result = await this.#storage.upsert({
264
+ orgId: record.orgId,
265
+ userId: "factory-rule-dispatcher",
266
+ factoryProjectId: record.factoryProjectId,
267
+ input: {
268
+ externalSource: externalSourceForDecision(decision),
269
+ parentWorkItemId: record.workItemId,
270
+ title: decision.title,
271
+ stages: ["intake"],
272
+ sessions: {},
273
+ metadata: {
274
+ ...decision.metadata,
275
+ factoryRuleMaterializationKey: record.idempotencyKey
276
+ }
277
+ },
278
+ reuseMode: "preserve"
279
+ });
280
+ const materializedByDecision = result.item.metadata?.factoryRuleMaterializationKey === record.idempotencyKey;
281
+ if (!materializedByDecision && (decision.stage === "intake" || !result.item.stages.includes("intake"))) return;
282
+ const board = decision.board;
283
+ let expectedRevision = result.item.revision;
284
+ if (materializedByDecision) {
285
+ const initial = await this.#transitionService.transition({
286
+ orgId: record.orgId,
287
+ factoryProjectId: record.factoryProjectId,
288
+ workItemId: result.item.id,
289
+ board,
290
+ stage: "intake",
291
+ expectedRevision,
292
+ actor: deferredActor(record),
293
+ ingress: {
294
+ type: "rule",
295
+ identity: `decision:${record.idempotencyKey}:${result.item.id}:initial-entry`
296
+ },
297
+ cause: "linked_item_materialized",
298
+ causalChain,
299
+ initialEntry: true
300
+ });
301
+ if (initial.status === "rejected") {
302
+ if (result.created) await this.#storage.delete({
303
+ orgId: record.orgId,
304
+ id: result.item.id
305
+ });
306
+ throw new Error(`${initial.code}: ${initial.reason}`);
307
+ }
308
+ expectedRevision = initial.revision;
309
+ }
310
+ if (decision.stage === "intake") return;
311
+ const moved = await this.#transitionService.transition({
312
+ orgId: record.orgId,
313
+ factoryProjectId: record.factoryProjectId,
314
+ workItemId: result.item.id,
315
+ board,
316
+ stage: decision.stage,
317
+ expectedRevision,
318
+ actor: {
319
+ type: "system",
320
+ id: "factory-rule-dispatcher"
321
+ },
322
+ ingress: {
323
+ type: "rule",
324
+ identity: `decision:${record.idempotencyKey}:${result.item.id}:destination`
325
+ },
326
+ cause: materializedByDecision ? "linked_item_materialized" : "linked_item_reconciled",
327
+ causalChain
328
+ });
329
+ if (moved.status === "rejected") throw new Error(`${moved.code}: ${moved.reason}`);
330
+ }
331
+ async #requireItem(record) {
332
+ if (!record.workItemId) throw new Error("Factory decision is not linked to a work item.");
333
+ const item = await this.#storage.get({
334
+ orgId: record.orgId,
335
+ id: record.workItemId
336
+ });
337
+ if (!item) throw new Error("Factory work item not found.");
338
+ return item;
339
+ }
340
+ async #findBinding(record, role) {
341
+ if (!record.workItemId) throw new Error("Factory decision is not linked to a work item.");
342
+ return (await this.#storage.listRunBindings(record.orgId, record.factoryProjectId, record.workItemId)).filter((candidate) => candidate.status === "active" && (role === void 0 || candidate.role === role)).sort((left, right) => {
343
+ if (role === void 0 && left.role === "work" && right.role !== "work") return -1;
344
+ if (role === void 0 && right.role === "work" && left.role !== "work") return 1;
345
+ return right.createdAt.getTime() - left.createdAt.getTime() || left.id.localeCompare(right.id);
346
+ })[0];
347
+ }
348
+ async #requireBinding(record, role) {
349
+ const binding = await this.#findBinding(record, role);
350
+ if (!binding) throw new Error(role ? `No active Factory binding for role ${role}.` : "No active Factory binding.");
351
+ return binding;
352
+ }
353
+ async #requireOrPrepareBinding(record, role) {
354
+ const binding = await this.#findBinding(record, role);
355
+ if (binding) {
356
+ if (await this.#controller.getSessionByResource(binding.resourceId)) return binding;
357
+ }
358
+ if (!this.#prepareBinding) throw new Error(binding ? "Bound Factory session not found." : `No active Factory binding for role ${role}.`);
359
+ const item = await this.#requireItem(record);
360
+ await this.#prepareBinding({
361
+ record,
362
+ item,
363
+ role
364
+ });
365
+ return this.#requireBinding(record, role);
366
+ }
367
+ async #requireSession(binding) {
368
+ const session = await this.#controller.getSessionByResource(binding.resourceId);
369
+ if (!session) throw new Error("Bound Factory session not found.");
370
+ await this.#switchThread(session, binding);
371
+ return session;
372
+ }
373
+ async #switchThread(session, binding) {
374
+ await session.thread.switch({ threadId: binding.threadId });
375
+ }
376
+ async #withLease(renew, effect) {
377
+ let renewalFailure;
378
+ let renewal = Promise.resolve();
379
+ const timer = setInterval(() => {
380
+ renewal = renewal.then(async () => {
381
+ try {
382
+ if (!await renew(new Date(Date.now() + LEASE_MS))) renewalFailure = /* @__PURE__ */ new Error("Factory dispatch lease was lost during execution.");
383
+ } catch (error) {
384
+ renewalFailure = error;
385
+ }
386
+ });
387
+ }, Math.floor(LEASE_MS / 3));
388
+ timer.unref?.();
389
+ try {
390
+ await effect();
391
+ await renewal;
392
+ if (renewalFailure) throw renewalFailure;
393
+ } finally {
394
+ clearInterval(timer);
395
+ await renewal;
396
+ }
397
+ }
398
+ async #dispatchPendingStart(record, now) {
399
+ try {
400
+ await this.#withLease(async (leaseExpiresAt) => this.#storage.renewPendingStartLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt), async () => {
401
+ if (record.message === null) return;
402
+ const binding = (await this.#storage.listRunBindings(record.orgId, record.factoryProjectId)).find((candidate) => candidate.id === record.bindingId && candidate.status === "active");
403
+ if (!binding) throw new Error("Prepared Factory binding is unavailable or revoked.");
404
+ const startedBy = (await this.#storage.get({
405
+ orgId: record.orgId,
406
+ id: binding.workItemId
407
+ }))?.sessions[binding.role]?.startedBy;
408
+ if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);
409
+ await this.#primeCredentials?.({
410
+ orgId: record.orgId,
411
+ userId: startedBy
412
+ });
413
+ const requestContext = new RequestContext();
414
+ requestContext.set("user", {
415
+ workosId: startedBy,
416
+ organizationId: record.orgId
417
+ });
418
+ await awaitNotification(await (await this.#requireSession(binding)).sendNotificationSignal({
419
+ source: "factory",
420
+ kind: "run-kickoff",
421
+ summary: record.message,
422
+ priority: "high",
423
+ payload: { message: record.message },
424
+ sourceId: record.id,
425
+ dedupeKey: `factory-kickoff:${record.kickoffKey}`
426
+ }, {
427
+ ifActive: { behavior: "deliver" },
428
+ ifIdle: { behavior: "wake" },
429
+ requestContext
430
+ }), true);
431
+ });
432
+ if (!await this.#storage.completePendingStart(leaseIdentity(record, this.#ownerId), /* @__PURE__ */ new Date())) throw new Error("Factory kickoff lease was lost before completion.");
433
+ } catch (error) {
434
+ await this.#storage.failPendingStart({
435
+ ...leaseIdentity(record, this.#ownerId),
436
+ now: /* @__PURE__ */ new Date(),
437
+ availableAt: retryAt(now, record.attempts),
438
+ lastError: sanitizeDispatchError(error),
439
+ terminal: record.attempts >= MAX_ATTEMPTS
440
+ });
441
+ }
442
+ }
681
443
  };
682
- var FACTORY_DISPATCH_CONSTANTS = {
683
- leaseMs: LEASE_MS,
684
- pollMs: POLL_MS,
685
- batchSize: BATCH_SIZE,
686
- maxAttempts: MAX_ATTEMPTS,
687
- maxErrorLength: MAX_ERROR_LENGTH,
688
- maxBackoffMs: MAX_BACKOFF_MS,
689
- stages: FACTORY_RULE_STAGES
690
- };
691
- export {
692
- FACTORY_DISPATCH_CONSTANTS,
693
- FactoryDecisionDispatcher
444
+ const FACTORY_DISPATCH_CONSTANTS = {
445
+ leaseMs: LEASE_MS,
446
+ pollMs: POLL_MS,
447
+ batchSize: BATCH_SIZE,
448
+ maxAttempts: MAX_ATTEMPTS,
449
+ maxErrorLength: MAX_ERROR_LENGTH,
450
+ maxBackoffMs: MAX_BACKOFF_MS,
451
+ stages: FACTORY_RULE_STAGES
694
452
  };
453
+ //#endregion
454
+ export { FACTORY_DISPATCH_CONSTANTS, FactoryDecisionDispatcher };
455
+
695
456
  //# sourceMappingURL=dispatcher.js.map