@mastra/factory 0.10.0-alpha.2 → 0.10.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/dist/factory.js +2 -2
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +193 -127
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/routes/attention.d.ts +17 -0
- package/dist/routes/attention.d.ts.map +1 -0
- package/dist/routes/attention.js +305 -0
- package/dist/routes/attention.js.map +1 -0
- package/dist/routes/surface.d.ts +2 -3
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +60 -43
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +34 -13
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatch-errors.d.ts +13 -0
- package/dist/rules/dispatch-errors.d.ts.map +1 -0
- package/dist/rules/dispatch-errors.js +77 -0
- package/dist/rules/dispatch-errors.js.map +1 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +60 -35
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +30 -14
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/terminal-cleanup.d.ts +1 -1
- package/dist/rules/terminal-cleanup.d.ts.map +1 -1
- package/dist/rules/terminal-cleanup.js +2 -2
- package/dist/rules/terminal-cleanup.js.map +1 -1
- package/dist/rules/transition-service.d.ts +2 -1
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +2 -0
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +9 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/session/factory-session.d.ts +4 -0
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +10 -2
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/first-exec-capture.d.ts +16 -10
- package/dist/session/first-exec-capture.d.ts.map +1 -1
- package/dist/session/first-exec-capture.js +74 -14
- package/dist/session/first-exec-capture.js.map +1 -1
- package/dist/session/first-message-capture.d.ts +14 -10
- package/dist/session/first-message-capture.d.ts.map +1 -1
- package/dist/session/first-message-capture.js +17 -11
- package/dist/session/first-message-capture.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +3 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +8 -2
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +101 -10
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +458 -35
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/work-item-branch.d.ts +23 -0
- package/dist/work-item-branch.d.ts.map +1 -0
- package/dist/work-item-branch.js +39 -0
- package/dist/work-item-branch.js.map +1 -0
- package/factory-skills/factory-rereview/SKILL.md +2 -0
- package/factory-skills/factory-review/SKILL.md +2 -0
- package/package.json +7 -7
package/dist/rules/dispatcher.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { FACTORY_RULE_STAGES } from "./types.js";
|
|
2
2
|
import { validateFactoryRuleDecision } from "./validation.js";
|
|
3
|
-
import { resolvePromptInvocation, resolveSkillInvocation } from "../skills/service.js";
|
|
4
3
|
import { FACTORY_RULE_MATERIALIZATION_KEY } from "../storage/domains/work-items/base.js";
|
|
4
|
+
import { FactoryDispatchError, factoryDispatchFailureCode } from "./dispatch-errors.js";
|
|
5
|
+
import { resolvePromptInvocation, resolveSkillInvocation } from "../skills/service.js";
|
|
5
6
|
import { randomUUID } from "crypto";
|
|
6
7
|
import { RequestContext } from "@mastra/core/request-context";
|
|
7
8
|
//#region src/rules/dispatcher.ts
|
|
@@ -65,19 +66,25 @@ function leaseIdentity(record, ownerId) {
|
|
|
65
66
|
ownerId
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
|
-
async function awaitNotification(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
async function awaitNotification(send, requireDelivery = false) {
|
|
70
|
+
try {
|
|
71
|
+
const notification = await send();
|
|
72
|
+
const [, accepted] = await Promise.all([notification.persisted, notification.accepted]);
|
|
73
|
+
if (!accepted) {
|
|
74
|
+
if (requireDelivery) throw new FactoryDispatchError("notification_delivery_failed", "Factory notification was persisted without agent delivery.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!requireDelivery) return;
|
|
78
|
+
if (accepted.action === "wake") {
|
|
79
|
+
if (!accepted.output) throw new FactoryDispatchError("notification_delivery_failed", "Factory notification wake had no output.");
|
|
80
|
+
await accepted.output.consumeStream();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (accepted.action !== "deliver") throw new FactoryDispatchError("notification_delivery_failed", `Factory notification did not reach the agent (${String(accepted.action)}).`);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof FactoryDispatchError) throw error;
|
|
86
|
+
throw new FactoryDispatchError("notification_delivery_failed", `Factory notification delivery failed: ${sanitizeDispatchError(error)}`, { cause: error });
|
|
79
87
|
}
|
|
80
|
-
if (accepted.action !== "deliver") throw new Error(`Factory notification did not reach the agent (${String(accepted.action)}).`);
|
|
81
88
|
}
|
|
82
89
|
var FactoryDecisionDispatcher = class {
|
|
83
90
|
#controller;
|
|
@@ -213,6 +220,7 @@ var FactoryDecisionDispatcher = class {
|
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
222
|
async #dispatchDecision(record, now) {
|
|
223
|
+
let executionCompleted = false;
|
|
216
224
|
try {
|
|
217
225
|
const decision = validateFactoryRuleDecision(record.decision, record.causalChain.length);
|
|
218
226
|
if (decision.type === "reject") throw new Error("Deferred Factory decisions cannot reject.");
|
|
@@ -222,6 +230,7 @@ var FactoryDecisionDispatcher = class {
|
|
|
222
230
|
}
|
|
223
231
|
await this.#supersedeProposals(record, decision);
|
|
224
232
|
await this.#withLease(async (leaseExpiresAt) => this.#storage.renewDeferredDecisionLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt), async () => this.#executeDecision(record, decision));
|
|
233
|
+
executionCompleted = true;
|
|
225
234
|
if (!await this.#storage.completeDeferredDecision(leaseIdentity(record, this.#ownerId), /* @__PURE__ */ new Date())) throw new Error("Factory decision lease was lost before completion.");
|
|
226
235
|
} catch (error) {
|
|
227
236
|
const terminal = record.attempts >= MAX_ATTEMPTS;
|
|
@@ -230,7 +239,9 @@ var FactoryDecisionDispatcher = class {
|
|
|
230
239
|
now: /* @__PURE__ */ new Date(),
|
|
231
240
|
availableAt: retryAt(now, record.attempts),
|
|
232
241
|
lastError: sanitizeDispatchError(error),
|
|
233
|
-
|
|
242
|
+
failureCode: factoryDispatchFailureCode(error),
|
|
243
|
+
terminal,
|
|
244
|
+
advanceDeliveryGeneration: !executionCompleted
|
|
234
245
|
});
|
|
235
246
|
}
|
|
236
247
|
}
|
|
@@ -244,12 +255,12 @@ var FactoryDecisionDispatcher = class {
|
|
|
244
255
|
async #supersedeProposals(record, decision) {
|
|
245
256
|
if (decision.type !== "invokeSkill" || !record.workItemId) return;
|
|
246
257
|
try {
|
|
247
|
-
await this.#storage.
|
|
258
|
+
await this.#storage.supersedeDecisionsForWorkItem({
|
|
248
259
|
orgId: record.orgId,
|
|
249
260
|
factoryProjectId: record.factoryProjectId,
|
|
250
261
|
workItemId: record.workItemId,
|
|
251
262
|
role: decision.role,
|
|
252
|
-
|
|
263
|
+
supersededAt: /* @__PURE__ */ new Date()
|
|
253
264
|
});
|
|
254
265
|
} catch (error) {
|
|
255
266
|
console.error("Factory proposal supersede failed", sanitizeDispatchError(error));
|
|
@@ -296,8 +307,9 @@ var FactoryDecisionDispatcher = class {
|
|
|
296
307
|
...decision.reenter ? { reenter: true } : {}
|
|
297
308
|
});
|
|
298
309
|
if (result.status === "rejected") throw new Error(`${result.code}: ${result.reason}`);
|
|
299
|
-
|
|
300
|
-
|
|
310
|
+
const transitionMessage = decision.message;
|
|
311
|
+
if (!transitionMessage) return;
|
|
312
|
+
const binding = await this.#findBinding(record, transitionMessage.role);
|
|
301
313
|
if (!binding) return;
|
|
302
314
|
const startedBy = item.sessions[binding.role]?.startedBy;
|
|
303
315
|
if (!startedBy) return;
|
|
@@ -310,12 +322,14 @@ var FactoryDecisionDispatcher = class {
|
|
|
310
322
|
workosId: startedBy,
|
|
311
323
|
organizationId: record.orgId
|
|
312
324
|
});
|
|
313
|
-
|
|
325
|
+
const session = await this.#findSession(binding);
|
|
326
|
+
if (!session) return;
|
|
327
|
+
await awaitNotification(() => session.sendNotificationSignal({
|
|
314
328
|
source: "factory",
|
|
315
329
|
kind: "rule-message",
|
|
316
|
-
summary:
|
|
330
|
+
summary: transitionMessage.text,
|
|
317
331
|
priority: "high",
|
|
318
|
-
payload: { message:
|
|
332
|
+
payload: { message: transitionMessage.text },
|
|
319
333
|
sourceId: record.id,
|
|
320
334
|
dedupeKey: record.idempotencyKey
|
|
321
335
|
}, {
|
|
@@ -354,14 +368,16 @@ var FactoryDecisionDispatcher = class {
|
|
|
354
368
|
});
|
|
355
369
|
const session = resolved.session;
|
|
356
370
|
await this.#switchThread(session, binding);
|
|
357
|
-
|
|
371
|
+
const deliveryId = record.deliveryGeneration === 0 ? record.id : `${record.id}:retry:${record.deliveryGeneration}`;
|
|
372
|
+
if ((await session.thread.listActiveMessages()).some((message) => message.id === deliveryId)) return;
|
|
358
373
|
if (decision.cancelInFlight) session.abort();
|
|
359
|
-
|
|
374
|
+
const precedingMessage = decision.precedingMessage;
|
|
375
|
+
if (precedingMessage) await awaitNotification(() => session.sendNotificationSignal({
|
|
360
376
|
source: "factory",
|
|
361
377
|
kind: "stage-transition",
|
|
362
|
-
summary:
|
|
378
|
+
summary: precedingMessage,
|
|
363
379
|
priority: "medium",
|
|
364
|
-
payload: { message:
|
|
380
|
+
payload: { message: precedingMessage },
|
|
365
381
|
sourceId: `${record.id}:stage-transition`,
|
|
366
382
|
dedupeKey: `${record.idempotencyKey}:stage-transition`
|
|
367
383
|
}, {
|
|
@@ -387,7 +403,7 @@ var FactoryDecisionDispatcher = class {
|
|
|
387
403
|
});
|
|
388
404
|
const sendKickoff = async () => {
|
|
389
405
|
const settled = await session.sendSignal({
|
|
390
|
-
id:
|
|
406
|
+
id: deliveryId,
|
|
391
407
|
type: "user",
|
|
392
408
|
tagName: "user",
|
|
393
409
|
contents: resolved.message
|
|
@@ -401,7 +417,7 @@ var FactoryDecisionDispatcher = class {
|
|
|
401
417
|
try {
|
|
402
418
|
let settled = await sendKickoff();
|
|
403
419
|
if (settled.action === "deliver") {
|
|
404
|
-
if (!(await session.thread.listActiveMessages()).some((message) => message.id ===
|
|
420
|
+
if (!(await session.thread.listActiveMessages()).some((message) => message.id === deliveryId)) {
|
|
405
421
|
if (!await waitForAgentEndOrTimeout(agentEnd)) throw new Error("Factory skill invocation is waiting on a run that has not ended.");
|
|
406
422
|
armAgentEnd();
|
|
407
423
|
settled = await sendKickoff();
|
|
@@ -435,7 +451,8 @@ var FactoryDecisionDispatcher = class {
|
|
|
435
451
|
workosId: startedBy,
|
|
436
452
|
organizationId: record.orgId
|
|
437
453
|
});
|
|
438
|
-
|
|
454
|
+
const session = await this.#requireSession(binding);
|
|
455
|
+
await awaitNotification(() => session.sendNotificationSignal({
|
|
439
456
|
source: "factory",
|
|
440
457
|
kind: "rule-message",
|
|
441
458
|
summary: decision.message,
|
|
@@ -452,7 +469,8 @@ var FactoryDecisionDispatcher = class {
|
|
|
452
469
|
}
|
|
453
470
|
case "notify": {
|
|
454
471
|
const binding = await this.#requireBinding(record);
|
|
455
|
-
|
|
472
|
+
const session = await this.#requireSession(binding);
|
|
473
|
+
await awaitNotification(() => session.sendNotificationSignal({
|
|
456
474
|
source: "factory",
|
|
457
475
|
kind: "rule-notification",
|
|
458
476
|
summary: decision.title,
|
|
@@ -554,7 +572,7 @@ var FactoryDecisionDispatcher = class {
|
|
|
554
572
|
}
|
|
555
573
|
async #requireBinding(record, role) {
|
|
556
574
|
const binding = await this.#findBinding(record, role);
|
|
557
|
-
if (!binding) throw new
|
|
575
|
+
if (!binding) throw new FactoryDispatchError("session_unavailable", role ? `No active Factory binding for role ${role}.` : "No active Factory binding.");
|
|
558
576
|
return binding;
|
|
559
577
|
}
|
|
560
578
|
async #requireOrPrepareBinding(record, role) {
|
|
@@ -562,7 +580,7 @@ var FactoryDecisionDispatcher = class {
|
|
|
562
580
|
if (binding) {
|
|
563
581
|
if (await this.#controller.getSessionByResource(binding.resourceId)) return binding;
|
|
564
582
|
}
|
|
565
|
-
if (!this.#prepareBinding) throw new
|
|
583
|
+
if (!this.#prepareBinding) throw new FactoryDispatchError("session_unavailable", binding ? "Bound Factory session not found." : `No active Factory binding for role ${role}.`);
|
|
566
584
|
const item = await this.#requireItem(record);
|
|
567
585
|
await this.#prepareBinding({
|
|
568
586
|
record,
|
|
@@ -571,12 +589,17 @@ var FactoryDecisionDispatcher = class {
|
|
|
571
589
|
});
|
|
572
590
|
return this.#requireBinding(record, role);
|
|
573
591
|
}
|
|
574
|
-
async #
|
|
592
|
+
async #findSession(binding) {
|
|
575
593
|
const session = await this.#controller.getSessionByResource(binding.resourceId);
|
|
576
|
-
if (!session)
|
|
594
|
+
if (!session) return void 0;
|
|
577
595
|
await this.#switchThread(session, binding);
|
|
578
596
|
return session;
|
|
579
597
|
}
|
|
598
|
+
async #requireSession(binding) {
|
|
599
|
+
const session = await this.#findSession(binding);
|
|
600
|
+
if (!session) throw new FactoryDispatchError("session_unavailable", "Bound Factory session not found.");
|
|
601
|
+
return session;
|
|
602
|
+
}
|
|
580
603
|
async #switchThread(session, binding) {
|
|
581
604
|
await session.thread.switch({ threadId: binding.threadId });
|
|
582
605
|
}
|
|
@@ -607,7 +630,7 @@ var FactoryDecisionDispatcher = class {
|
|
|
607
630
|
await this.#withLease(async (leaseExpiresAt) => this.#storage.renewPendingStartLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt), async () => {
|
|
608
631
|
if (record.message === null) return;
|
|
609
632
|
const binding = (await this.#storage.listRunBindings(record.orgId, record.factoryProjectId)).find((candidate) => candidate.id === record.bindingId && candidate.status === "active");
|
|
610
|
-
if (!binding) throw new
|
|
633
|
+
if (!binding) throw new FactoryDispatchError("session_unavailable", "Prepared Factory binding is unavailable or revoked.");
|
|
611
634
|
const startedBy = (await this.#storage.get({
|
|
612
635
|
orgId: record.orgId,
|
|
613
636
|
id: binding.workItemId
|
|
@@ -622,7 +645,8 @@ var FactoryDecisionDispatcher = class {
|
|
|
622
645
|
workosId: startedBy,
|
|
623
646
|
organizationId: record.orgId
|
|
624
647
|
});
|
|
625
|
-
|
|
648
|
+
const session = await this.#requireSession(binding);
|
|
649
|
+
await awaitNotification(() => session.sendNotificationSignal({
|
|
626
650
|
source: "factory",
|
|
627
651
|
kind: "run-kickoff",
|
|
628
652
|
summary: record.message,
|
|
@@ -643,6 +667,7 @@ var FactoryDecisionDispatcher = class {
|
|
|
643
667
|
now: /* @__PURE__ */ new Date(),
|
|
644
668
|
availableAt: retryAt(now, record.attempts),
|
|
645
669
|
lastError: sanitizeDispatchError(error),
|
|
670
|
+
failureCode: factoryDispatchFailureCode(error),
|
|
646
671
|
terminal: record.attempts >= MAX_ATTEMPTS
|
|
647
672
|
});
|
|
648
673
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.js","names":["#controller","#transitionService","#storage","#ownerId","#isAutoRunEnabled","#reconcileToolResults","#prepareBinding","#primeCredentials","#maxInFlight","#staleBindingSweepIntervalMs","#staleBindingTtlMs","#reconcileIntervalMs","#inFlight","#timer","#tick","#activeClaim","#claimAndStart","#maybeSweepStaleBindings","#maybeReconcileToolResults","#track","#dispatchPendingStart","#dispatchDecision","#reconcileInFlight","#lastReconcileAt","#lastStaleBindingSweepAt","#needsApproval","#supersedeProposals","#withLease","#executeDecision","#requireItem","#findBinding","#requireSession","#upsertLinkedItem","#requireOrPrepareBinding","#switchThread","#requireBinding"],"sources":["../../src/rules/dispatcher.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController, AgentControllerEventListener } from '@mastra/core/agent-controller';\nimport { RequestContext } from '@mastra/core/request-context';\n\nimport { resolvePromptInvocation, resolveSkillInvocation } from '../skills/service.js';\nimport type { SkillSession } from '../skills/service.js';\nimport type {\n FactoryDeferredDecisionRecord,\n FactoryPendingStartRecord,\n FactoryRunBindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { FACTORY_RULE_MATERIALIZATION_KEY } from '../storage/domains/work-items/base.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport type { FactoryCommitDecision, FactoryRuleActor, FactoryRuleCausalEntry } from './types.js';\nimport { FACTORY_RULE_STAGES } from './types.js';\nimport { MAX_FACTORY_RULE_CAUSAL_DEPTH, validateFactoryRuleDecision } from './validation.js';\n\nconst LEASE_MS = 30_000;\nconst POLL_MS = 1_000;\nconst BATCH_SIZE = 10;\nconst MAX_ATTEMPTS = 5;\nconst MAX_ERROR_LENGTH = 512;\nconst MAX_BACKOFF_MS = 60_000;\nconst SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS = 10 * 60_000;\n// Dispatches can legitimately run for minutes. Woken skill invocations hold\n// capacity until their agent run reaches a terminal state; binding preparation\n// also runs detached from the poll loop under this concurrency cap.\nconst MAX_IN_FLIGHT = 25;\n// Staleness sweep: legacy/leaked active bindings (item deleted, transition\n// path bypassed, or pre-dating terminal-stage revocation) are revoked on a\n// slow cadence so the per-tick reconcile walk stays bounded.\nconst STALE_BINDING_SWEEP_INTERVAL_MS = 10 * 60_000;\nconst STALE_BINDING_TTL_MS = 24 * 60 * 60_000;\n// The bound-thread reconcile walk reads a cursor + messages per binding; it\n// exists to catch results missed at run end, so it runs on a slow cadence off\n// the claim path rather than on every 1s tick.\nconst RECONCILE_INTERVAL_MS = 30_000;\n\nfunction waitForAgentEndOrTimeout(agentEnd: Promise<void>): Promise<boolean> {\n return new Promise(resolve => {\n const timeout = setTimeout(() => resolve(false), SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS);\n timeout.unref?.();\n void agentEnd.then(() => {\n clearTimeout(timeout);\n resolve(true);\n });\n });\n}\n\ninterface DispatcherSession extends SkillSession {\n thread: {\n switch(input: { threadId: string }): Promise<unknown>;\n listActiveMessages(): Promise<Array<{ id: string }>>;\n };\n abort(): void;\n sendSignal(\n input: { id: string; type: 'user'; tagName: 'user'; contents: string },\n options: { requestContext: RequestContext; requireDelivery?: boolean },\n ): { accepted: Promise<{ accepted: true; runId?: string; action?: string }> };\n subscribe(listener: AgentControllerEventListener): () => void;\n}\n\ntype FactoryController = Pick<AgentController<MastraCodeState>, 'getSessionByResource'>;\n\nexport interface FactoryBindingPreparationInput {\n record: FactoryDeferredDecisionRecord;\n item: WorkItemRow;\n role: string;\n}\n\nexport interface FactoryDecisionDispatcherOptions {\n controller: FactoryController;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n storage: WorkItemsStorage;\n ownerId?: string;\n /** `false` parks `invokeSkill` effects as `proposed`; every other effect still runs. */\n isAutoRunEnabled: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n reconcileToolResults?: () => Promise<void>;\n prepareBinding?: (input: FactoryBindingPreparationInput) => Promise<void>;\n primeCredentials?: (tenant: { orgId: string; userId: string }) => Promise<void>;\n maxInFlight?: number;\n /** How often the stale-binding sweep runs. Defaults to 10 minutes. */\n staleBindingSweepIntervalMs?: number;\n /** Active bindings older than this are revoked by the sweep. Defaults to 24 hours. */\n staleBindingTtlMs?: number;\n /** How often the bound-thread reconcile walk runs. Defaults to 30 seconds. */\n reconcileIntervalMs?: number;\n}\n\nfunction positiveMs(value: number | undefined, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;\n}\n\nfunction sanitizeDispatchError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n return message\n .replace(/\\b(?:bearer|token|api[-_ ]?key|authorization)\\s*[:=]?\\s*[^\\s,;]+/gi, '[redacted]')\n .slice(0, MAX_ERROR_LENGTH);\n}\n\nfunction retryAt(now: Date, attempts: number): Date {\n return new Date(now.getTime() + Math.min(1_000 * 2 ** Math.max(0, attempts - 1), MAX_BACKOFF_MS));\n}\n\nfunction externalSourceForDecision(decision: Extract<FactoryCommitDecision, { type: 'upsertLinkedWorkItem' }>) {\n const [integrationId, type] =\n decision.source === 'github-pr'\n ? ['github', 'pull-request']\n : decision.source === 'github-issue'\n ? ['github', 'issue']\n : decision.source === 'linear-issue'\n ? ['linear', 'issue']\n : ['factory', 'manual'];\n return { integrationId, type, externalId: decision.sourceKey, url: decision.url ?? undefined };\n}\n\nfunction deferredActor(record: FactoryDeferredDecisionRecord): FactoryRuleActor {\n const actor = record.actor;\n if (\n actor?.type === 'github' &&\n typeof actor.login === 'string' &&\n typeof actor.trusted === 'boolean' &&\n typeof actor.factoryAuthored === 'boolean'\n ) {\n return {\n type: 'github',\n login: actor.login,\n trusted: actor.trusted,\n factoryAuthored: actor.factoryAuthored,\n };\n }\n return { type: 'system', id: 'factory-rule-dispatcher' };\n}\n\nfunction leaseIdentity(\n record: Pick<FactoryDeferredDecisionRecord | FactoryPendingStartRecord, 'id' | 'orgId' | 'factoryProjectId'>,\n ownerId: string,\n) {\n return { id: record.id, orgId: record.orgId, factoryProjectId: record.factoryProjectId, ownerId };\n}\n\nasync function awaitNotification(\n result: Awaited<ReturnType<SkillSession['sendNotificationSignal']>>,\n requireDelivery = false,\n): Promise<void> {\n await result.persisted;\n if (!result.accepted) {\n if (requireDelivery) throw new Error('Factory notification was persisted without agent delivery.');\n return;\n }\n const accepted = await result.accepted;\n if (!requireDelivery) return;\n if (accepted.action === 'wake') {\n await accepted.output.consumeStream();\n return;\n }\n if (accepted.action !== 'deliver') {\n throw new Error(`Factory notification did not reach the agent (${String(accepted.action)}).`);\n }\n}\n\nexport class FactoryDecisionDispatcher {\n readonly #controller: FactoryController;\n readonly #transitionService: Pick<FactoryTransitionService, 'transition'>;\n readonly #storage: WorkItemsStorage;\n readonly #ownerId: string;\n readonly #isAutoRunEnabled: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n readonly #reconcileToolResults?: () => Promise<void>;\n readonly #prepareBinding?: (input: FactoryBindingPreparationInput) => Promise<void>;\n readonly #primeCredentials?: (tenant: { orgId: string; userId: string }) => Promise<void>;\n readonly #maxInFlight: number;\n readonly #staleBindingSweepIntervalMs: number;\n readonly #staleBindingTtlMs: number;\n #lastStaleBindingSweepAt?: Date;\n readonly #reconcileIntervalMs: number;\n #lastReconcileAt?: Date;\n #reconcileInFlight?: Promise<void>;\n #timer?: ReturnType<typeof setInterval>;\n #activeClaim?: Promise<void>;\n readonly #inFlight = new Set<Promise<void>>();\n\n constructor(options: FactoryDecisionDispatcherOptions) {\n this.#controller = options.controller;\n this.#transitionService = options.transitionService;\n this.#storage = options.storage;\n this.#ownerId = options.ownerId ?? `factory-dispatcher:${randomUUID()}`;\n this.#isAutoRunEnabled = options.isAutoRunEnabled;\n this.#reconcileToolResults = options.reconcileToolResults;\n this.#prepareBinding = options.prepareBinding;\n this.#primeCredentials = options.primeCredentials;\n const maxInFlight = options.maxInFlight ?? MAX_IN_FLIGHT;\n this.#maxInFlight = Number.isFinite(maxInFlight) && maxInFlight > 0 ? Math.floor(maxInFlight) : MAX_IN_FLIGHT;\n this.#staleBindingSweepIntervalMs = positiveMs(\n options.staleBindingSweepIntervalMs,\n STALE_BINDING_SWEEP_INTERVAL_MS,\n );\n this.#staleBindingTtlMs = positiveMs(options.staleBindingTtlMs, STALE_BINDING_TTL_MS);\n this.#reconcileIntervalMs = positiveMs(options.reconcileIntervalMs, RECONCILE_INTERVAL_MS);\n }\n\n start(): void {\n if (this.#timer) return;\n void this.#tick();\n this.#timer = setInterval(() => void this.#tick(), POLL_MS);\n this.#timer.unref?.();\n }\n\n async stop(): Promise<void> {\n if (this.#timer) clearInterval(this.#timer);\n this.#timer = undefined;\n await this.#activeClaim;\n await Promise.allSettled([...this.#inFlight]);\n }\n\n async runOnce(now = new Date()): Promise<void> {\n await Promise.all(await this.#claimAndStart(now));\n }\n\n /**\n * Claims a batch and starts dispatches without awaiting their completion.\n * Dispatches can legitimately take minutes (skill kickoffs consume the\n * agent's run stream; binding preparation provisions sandboxes), so awaiting\n * them here would freeze the poll loop and starve every other queued\n * decision. In-flight records stay protected from re-claim by lease renewal.\n */\n async #claimAndStart(now: Date): Promise<Array<Promise<void>>> {\n // Fire-and-forget like the reconcile walk: the sweep reads every active\n // binding, so awaiting it would stretch the tick as the active set grows.\n void this.#maybeSweepStaleBindings(now);\n this.#maybeReconcileToolResults(now);\n const capacity = this.#maxInFlight - this.#inFlight.size;\n if (capacity <= 0) return [];\n const limit = Math.min(BATCH_SIZE, capacity);\n const leaseExpiresAt = new Date(now.getTime() + LEASE_MS);\n // Starts are claimed before deferred decisions: a pending start is a user\n // waiting on a brand-new session, while a deferred decision is a background\n // continuation of one that is already running. A deep decision queue must\n // never starve new sessions out of the tick.\n const starts = await this.#storage.claimPendingStarts({\n ownerId: this.#ownerId,\n now,\n leaseExpiresAt,\n limit,\n });\n const decisionsLimit = limit - starts.length;\n const decisions =\n decisionsLimit > 0\n ? await this.#storage.claimDeferredDecisions({\n ownerId: this.#ownerId,\n now,\n leaseExpiresAt,\n limit: decisionsLimit,\n })\n : [];\n return [\n ...starts.map(start => this.#track(this.#dispatchPendingStart(start, now))),\n ...decisions.map(decision => this.#track(this.#dispatchDecision(decision, now))),\n ];\n }\n\n /**\n * Throttled, coalesced, non-blocking bound-thread reconcile: dispatch\n * claiming never waits behind cursor + message reads, and overlapping runs\n * are skipped while one is still in flight.\n */\n #maybeReconcileToolResults(now: Date): void {\n if (!this.#reconcileToolResults || this.#reconcileInFlight) return;\n if (this.#lastReconcileAt && now.getTime() - this.#lastReconcileAt.getTime() < this.#reconcileIntervalMs) return;\n this.#lastReconcileAt = now;\n const run = this.#reconcileToolResults()\n .catch(error => {\n console.error('Factory tool-result reconcile failed', sanitizeDispatchError(error));\n })\n .finally(() => {\n this.#reconcileInFlight = undefined;\n });\n this.#reconcileInFlight = run;\n this.#track(run);\n }\n\n /** Slow-cadence revocation of leaked/legacy bindings; failures never block the claim path. */\n async #maybeSweepStaleBindings(now: Date): Promise<void> {\n // The first tick only anchors the cadence: sweeping at boot would race the\n // startup reconcile that is still draining trailing tool results.\n if (!this.#lastStaleBindingSweepAt) {\n this.#lastStaleBindingSweepAt = now;\n return;\n }\n if (now.getTime() - this.#lastStaleBindingSweepAt.getTime() < this.#staleBindingSweepIntervalMs) return;\n this.#lastStaleBindingSweepAt = now;\n try {\n const revoked = await this.#storage.revokeStaleRunBindings({\n olderThan: new Date(now.getTime() - this.#staleBindingTtlMs),\n now,\n });\n if (revoked > 0) console.info(`Factory stale-binding sweep revoked ${revoked} binding(s)`);\n } catch (error) {\n console.error('Factory stale-binding sweep failed', sanitizeDispatchError(error));\n }\n }\n\n #track(dispatch: Promise<void>): Promise<void> {\n this.#inFlight.add(dispatch);\n void dispatch.catch(() => {}).then(() => this.#inFlight.delete(dispatch));\n return dispatch;\n }\n\n async #tick(): Promise<void> {\n if (this.#activeClaim) return;\n this.#activeClaim = this.#claimAndStart(new Date()).then(\n dispatches => {\n for (const dispatch of dispatches) {\n dispatch.catch(error => {\n console.error('Factory decision dispatch failed', sanitizeDispatchError(error));\n });\n }\n },\n error => {\n console.error('Factory decision dispatch cycle failed', sanitizeDispatchError(error));\n },\n );\n try {\n await this.#activeClaim;\n } finally {\n this.#activeClaim = undefined;\n }\n }\n\n async #dispatchDecision(record: FactoryDeferredDecisionRecord, now: Date): Promise<void> {\n try {\n const decision = validateFactoryRuleDecision(record.decision, record.causalChain.length);\n if (decision.type === 'reject') throw new Error('Deferred Factory decisions cannot reject.');\n if (await this.#needsApproval(record, decision)) {\n const proposed = await this.#storage.proposeDeferredDecision(leaseIdentity(record, this.#ownerId), new Date());\n if (!proposed) throw new Error('Factory decision lease was lost before approval could be requested.');\n return;\n }\n await this.#supersedeProposals(record, decision);\n await this.#withLease(\n async leaseExpiresAt =>\n this.#storage.renewDeferredDecisionLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),\n async () => this.#executeDecision(record, decision),\n );\n const completed = await this.#storage.completeDeferredDecision(leaseIdentity(record, this.#ownerId), new Date());\n if (!completed) throw new Error('Factory decision lease was lost before completion.');\n } catch (error) {\n const terminal = record.attempts >= MAX_ATTEMPTS;\n await this.#storage.failDeferredDecision({\n ...leaseIdentity(record, this.#ownerId),\n now: new Date(),\n availableAt: retryAt(now, record.attempts),\n lastError: sanitizeDispatchError(error),\n terminal,\n });\n }\n }\n\n /**\n * A proposal is a question: \"should this run start?\" Once that run is\n * starting anyway — because a person approved a later copy, or armed the item\n * — the question has been answered and the card must stop asking it. Left\n * alone the badge outlives the work it describes, and the one affordance that\n * means \"the loop is stopped, answer this\" cries wolf.\n */\n async #supersedeProposals(record: FactoryDeferredDecisionRecord, decision: FactoryCommitDecision): Promise<void> {\n if (decision.type !== 'invokeSkill' || !record.workItemId) return;\n try {\n await this.#storage.dismissProposalsForWorkItem({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: record.workItemId,\n role: decision.role,\n dismissedAt: new Date(),\n });\n } catch (error) {\n // Best-effort: a stale badge is not worth failing the run it describes.\n console.error('Factory proposal supersede failed', sanitizeDispatchError(error));\n }\n }\n\n /** Starting an agent run spends the project's compute and executes its code — the one effect a human owns. */\n async #needsApproval(record: FactoryDeferredDecisionRecord, decision: FactoryCommitDecision): Promise<boolean> {\n if (decision.type !== 'invokeSkill' || record.approvedAt !== null) return false;\n if (await this.#isAutoRunEnabled({ orgId: record.orgId, factoryProjectId: record.factoryProjectId })) return false;\n // Withholding auto-run decides what the Factory may pick up on its own, not\n // whether it may finish work a person already handed it. Once someone starts\n // an item, the runs that carry it to review are that same request continuing.\n const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;\n return item?.autonomyArmedAt == null;\n }\n\n async #executeDecision(record: FactoryDeferredDecisionRecord, decision: FactoryCommitDecision): Promise<void> {\n const nextChain: FactoryRuleCausalEntry[] = [\n ...(record.causalChain as FactoryRuleCausalEntry[]),\n { ingressId: record.idempotencyKey, decisionType: decision.type },\n ];\n if (nextChain.length > MAX_FACTORY_RULE_CAUSAL_DEPTH) throw new Error('Factory rule causal depth exceeded.');\n\n switch (decision.type) {\n case 'transition': {\n const item = await this.#requireItem(record);\n const result = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: item.id,\n board: decision.board,\n stage: decision.stage,\n expectedRevision: item.revision,\n actor: { type: 'system', id: 'factory-rule-dispatcher' },\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}` },\n cause: 'rule_decision',\n causalChain: nextChain,\n ...(decision.reenter ? { reenter: true } : {}),\n });\n if (result.status === 'rejected') throw new Error(`${result.code}: ${result.reason}`);\n if (!decision.message) return;\n // Best-effort recipient lookup: no active binding (or no authenticated\n // session owner) means nobody is engaged with this item, so the\n // transition itself is the whole effect. A retry after a delivery\n // failure is safe because the transition replays by ingress identity.\n const binding = await this.#findBinding(record, decision.message.role);\n if (!binding) return;\n const startedBy = item.sessions[binding.role]?.startedBy;\n if (!startedBy) return;\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'rule-message',\n summary: decision.message.text,\n priority: 'high',\n payload: { message: decision.message.text },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: 'wake' },\n requestContext,\n },\n ),\n true,\n );\n return;\n }\n case 'upsertLinkedWorkItem': {\n await this.#upsertLinkedItem(record, decision, nextChain);\n return;\n }\n case 'invokeSkill': {\n const binding = await this.#requireOrPrepareBinding(record, decision.role);\n const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const resolved =\n decision.skillName === undefined\n ? await resolvePromptInvocation(this.#controller, {\n resourceId: binding.resourceId,\n prompt: decision.prompt,\n })\n : await resolveSkillInvocation(this.#controller, {\n resourceId: binding.resourceId,\n name: decision.skillName,\n arguments: decision.arguments,\n });\n const session = resolved.session as DispatcherSession;\n await this.#switchThread(session, binding);\n const delivered = await session.thread.listActiveMessages();\n if (delivered.some(message => message.id === record.id)) return;\n if (decision.cancelInFlight) session.abort();\n if (decision.precedingMessage) {\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'stage-transition',\n summary: decision.precedingMessage,\n priority: 'medium',\n payload: { message: decision.precedingMessage },\n sourceId: `${record.id}:stage-transition`,\n dedupeKey: `${record.idempotencyKey}:stage-transition`,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: 'persist' },\n requestContext,\n },\n ),\n );\n }\n let resolveAgentEnd!: () => void;\n let agentEnd!: Promise<void>;\n // The run's own verdict, not the delivery's. A signal can reach the\n // agent perfectly and the run still die on a provider error or be\n // cancelled mid-flight; without this the decision reports success and\n // the break is invisible on the card.\n let endReason: 'complete' | 'aborted' | 'error' | 'suspended' | undefined;\n // Re-armed before a redelivery so the second send waits on its own run's\n // ending rather than seeing the one that already resolved.\n const armAgentEnd = () => {\n endReason = undefined;\n agentEnd = new Promise<void>(resolve => {\n resolveAgentEnd = resolve;\n });\n };\n armAgentEnd();\n const unsubscribe = session.subscribe(event => {\n if (event.type === 'agent_end') {\n endReason = event.reason;\n resolveAgentEnd();\n }\n });\n\n const sendKickoff = async () => {\n const result = session.sendSignal(\n {\n id: record.id,\n type: 'user',\n tagName: 'user',\n contents: resolved.message,\n },\n // Without `requireDelivery` the session resolves `accepted` on the\n // next tick and swallows wake failures, so a kickoff that never\n // reached the agent would be marked succeeded and the thread would\n // stay empty forever.\n { requestContext, requireDelivery: true },\n );\n const settled = await result.accepted;\n if (settled.action !== 'wake' && settled.action !== 'deliver') {\n // An undefined action means the session did not verify delivery at\n // all — with `requireDelivery` set that is a contract violation, not\n // a success.\n throw new Error(`Factory skill invocation signal did not reach the agent (${String(settled.action)}).`);\n }\n return settled;\n };\n\n try {\n let settled = await sendKickoff();\n if (settled.action === 'deliver') {\n // `deliver` means the signal was queued onto a run that was already\n // in flight. If that run ends before draining its queue the prompt\n // is dropped silently: no turn starts, no error surfaces, and the\n // decision reports success while the card sits in its new stage with\n // nobody working. Signals persist under their own id (the same\n // identity the replay guard above reads), so confirm the message\n // actually landed in the thread rather than trusting the ack.\n const landed = await session.thread.listActiveMessages();\n if (!landed.some(message => message.id === record.id)) {\n // The condition that resolves this is the in-flight run ending, so\n // wait for exactly that and redeliver into the idle session. A\n // backoff cannot work here: retries are sized in seconds and a turn\n // takes minutes, so every attempt lands on the same busy run and\n // the card burns its whole budget without the session ever having\n // had a chance to be free.\n if (!(await waitForAgentEndOrTimeout(agentEnd))) {\n throw new Error('Factory skill invocation is waiting on a run that has not ended.');\n }\n armAgentEnd();\n settled = await sendKickoff();\n if (settled.action !== 'wake') {\n throw new Error('Factory skill invocation was queued onto an ending run and never reached the agent.');\n }\n }\n }\n // A landed `deliver` still runs on the in-flight session, so the run's\n // terminal outcome matters as much as a fresh wake's: a run that ends\n // in error after accepting the prompt has still failed this decision.\n {\n const observed = await waitForAgentEndOrTimeout(agentEnd);\n if (!observed) {\n console.warn('Factory skill run terminal event was not observed before timeout', {\n decisionId: record.id,\n runId: settled.action === 'wake' ? settled.runId : undefined,\n });\n } else if (endReason === 'error') {\n throw new Error('Factory skill run ended in error.');\n } else if (endReason === 'aborted') {\n // Retryable, though an abort reads as deliberate. The stream does\n // not say who aborted, and in practice the dominant cause is the\n // process going away underneath the run — an operator restarting\n // the server — not anyone deciding this work should stop. Treating\n // that as terminal dead-ends the card at attempt 1 with nothing on\n // the board to press. A spurious retry is bounded by MAX_ATTEMPTS;\n // a dead card costs a human a manual nudge.\n throw new Error('Factory skill run was aborted before it finished.');\n }\n }\n } finally {\n unsubscribe();\n }\n return;\n }\n case 'sendMessage': {\n const binding = decision.prepareBinding\n ? await this.#requireOrPrepareBinding(record, decision.role)\n : await this.#requireBinding(record, decision.role);\n const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'rule-message',\n summary: decision.message,\n priority: decision.priority ?? 'high',\n payload: { message: decision.message },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: decision.idleBehavior ?? 'wake' },\n requestContext,\n },\n ),\n true,\n );\n return;\n }\n case 'notify': {\n const binding = await this.#requireBinding(record);\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal({\n source: 'factory',\n kind: 'rule-notification',\n summary: decision.title,\n payload: { body: decision.body, level: decision.level },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n }),\n );\n }\n }\n }\n\n async #upsertLinkedItem(\n record: FactoryDeferredDecisionRecord,\n decision: Extract<FactoryCommitDecision, { type: 'upsertLinkedWorkItem' }>,\n causalChain: FactoryRuleCausalEntry[],\n ): Promise<void> {\n const result = await this.#storage.upsert({\n orgId: record.orgId,\n userId: 'factory-rule-dispatcher',\n factoryProjectId: record.factoryProjectId,\n input: {\n externalSource: externalSourceForDecision(decision),\n parentWorkItemId: record.workItemId,\n title: decision.title,\n stages: ['intake'],\n sessions: {},\n metadata: { ...decision.metadata, [FACTORY_RULE_MATERIALIZATION_KEY]: record.idempotencyKey },\n },\n reuseMode: 'preserve',\n });\n const materializedByDecision = result.item.metadata?.[FACTORY_RULE_MATERIALIZATION_KEY] === record.idempotencyKey;\n if (!materializedByDecision && (decision.stage === 'intake' || !result.item.stages.includes('intake'))) return;\n\n const board = decision.board;\n let expectedRevision = result.item.revision;\n if (materializedByDecision) {\n const initial = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: result.item.id,\n board,\n stage: 'intake',\n expectedRevision,\n actor: deferredActor(record),\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}:${result.item.id}:initial-entry` },\n cause: 'linked_item_materialized',\n causalChain,\n initialEntry: true,\n });\n if (initial.status === 'rejected') {\n if (result.created) await this.#storage.delete({ orgId: record.orgId, id: result.item.id });\n throw new Error(`${initial.code}: ${initial.reason}`);\n }\n expectedRevision = initial.revision;\n }\n if (decision.stage === 'intake') return;\n\n const moved = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: result.item.id,\n board,\n stage: decision.stage,\n expectedRevision,\n actor: { type: 'system', id: 'factory-rule-dispatcher' },\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}:${result.item.id}:destination` },\n cause: materializedByDecision ? 'linked_item_materialized' : 'linked_item_reconciled',\n causalChain,\n });\n if (moved.status === 'rejected') throw new Error(`${moved.code}: ${moved.reason}`);\n }\n\n async #requireItem(record: FactoryDeferredDecisionRecord) {\n if (!record.workItemId) throw new Error('Factory decision is not linked to a work item.');\n const item = await this.#storage.get({ orgId: record.orgId, id: record.workItemId });\n if (!item) throw new Error('Factory work item not found.');\n return item;\n }\n\n async #findBinding(\n record: FactoryDeferredDecisionRecord,\n role?: string,\n ): Promise<FactoryRunBindingRecord | undefined> {\n if (!record.workItemId) throw new Error('Factory decision is not linked to a work item.');\n const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId, record.workItemId);\n return bindings\n .filter(candidate => candidate.status === 'active' && (role === undefined || candidate.role === role))\n .sort((left, right) => {\n if (role === undefined && left.role === 'work' && right.role !== 'work') return -1;\n if (role === undefined && right.role === 'work' && left.role !== 'work') return 1;\n return right.createdAt.getTime() - left.createdAt.getTime() || left.id.localeCompare(right.id);\n })[0];\n }\n\n async #requireBinding(record: FactoryDeferredDecisionRecord, role?: string): Promise<FactoryRunBindingRecord> {\n const binding = await this.#findBinding(record, role);\n if (!binding) throw new Error(role ? `No active Factory binding for role ${role}.` : 'No active Factory binding.');\n return binding;\n }\n\n async #requireOrPrepareBinding(\n record: FactoryDeferredDecisionRecord,\n role: string,\n ): Promise<FactoryRunBindingRecord> {\n const binding = await this.#findBinding(record, role);\n if (binding) {\n const session = await this.#controller.getSessionByResource(binding.resourceId);\n if (session) return binding;\n }\n if (!this.#prepareBinding) {\n throw new Error(binding ? 'Bound Factory session not found.' : `No active Factory binding for role ${role}.`);\n }\n const item = await this.#requireItem(record);\n await this.#prepareBinding({ record, item, role });\n return this.#requireBinding(record, role);\n }\n\n async #requireSession(binding: FactoryRunBindingRecord): Promise<DispatcherSession> {\n const session = (await this.#controller.getSessionByResource(binding.resourceId)) as DispatcherSession | undefined;\n if (!session) throw new Error('Bound Factory session not found.');\n await this.#switchThread(session, binding);\n return session;\n }\n\n async #switchThread(session: SkillSession, binding: FactoryRunBindingRecord): Promise<void> {\n await (session as DispatcherSession).thread.switch({ threadId: binding.threadId });\n }\n\n async #withLease(\n renew: (leaseExpiresAt: Date) => Promise<unknown | null>,\n effect: () => Promise<void>,\n ): Promise<void> {\n let renewalFailure: unknown;\n let renewal = Promise.resolve();\n const timer = setInterval(\n () => {\n renewal = renewal.then(async () => {\n try {\n const renewed = await renew(new Date(Date.now() + LEASE_MS));\n if (!renewed) renewalFailure = new Error('Factory dispatch lease was lost during execution.');\n } catch (error) {\n renewalFailure = error;\n }\n });\n },\n Math.floor(LEASE_MS / 3),\n );\n timer.unref?.();\n try {\n await effect();\n await renewal;\n if (renewalFailure) throw renewalFailure;\n } finally {\n clearInterval(timer);\n await renewal;\n }\n }\n\n async #dispatchPendingStart(record: FactoryPendingStartRecord, now: Date): Promise<void> {\n try {\n await this.#withLease(\n async leaseExpiresAt =>\n this.#storage.renewPendingStartLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),\n async () => {\n if (record.message === null) return;\n const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId);\n const binding = bindings.find(\n candidate => candidate.id === record.bindingId && candidate.status === 'active',\n );\n if (!binding) throw new Error('Prepared Factory binding is unavailable or revoked.');\n // Wake runs build the Factory workspace, which requires the\n // authenticated session owner on the request context.\n const item = await this.#storage.get({ orgId: record.orgId, id: binding.workItemId });\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'run-kickoff',\n summary: record.message!,\n priority: 'high',\n payload: { message: record.message },\n sourceId: record.id,\n dedupeKey: `factory-kickoff:${record.kickoffKey}`,\n },\n { ifActive: { behavior: 'deliver' }, ifIdle: { behavior: 'wake' }, requestContext },\n ),\n true,\n );\n },\n );\n const completed = await this.#storage.completePendingStart(leaseIdentity(record, this.#ownerId), new Date());\n if (!completed) throw new Error('Factory kickoff lease was lost before completion.');\n } catch (error) {\n await this.#storage.failPendingStart({\n ...leaseIdentity(record, this.#ownerId),\n now: new Date(),\n availableAt: retryAt(now, record.attempts),\n lastError: sanitizeDispatchError(error),\n terminal: record.attempts >= MAX_ATTEMPTS,\n });\n }\n }\n}\n\nexport const FACTORY_DISPATCH_CONSTANTS = {\n leaseMs: LEASE_MS,\n pollMs: POLL_MS,\n batchSize: BATCH_SIZE,\n maxAttempts: MAX_ATTEMPTS,\n maxErrorLength: MAX_ERROR_LENGTH,\n maxBackoffMs: MAX_BACKOFF_MS,\n skillCompletionObservationTimeoutMs: SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS,\n maxInFlight: MAX_IN_FLIGHT,\n stages: FACTORY_RULE_STAGES,\n} as const;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dispatcher.js","names":["#controller","#transitionService","#storage","#ownerId","#isAutoRunEnabled","#reconcileToolResults","#prepareBinding","#primeCredentials","#maxInFlight","#staleBindingSweepIntervalMs","#staleBindingTtlMs","#reconcileIntervalMs","#inFlight","#timer","#tick","#activeClaim","#claimAndStart","#maybeSweepStaleBindings","#maybeReconcileToolResults","#track","#dispatchPendingStart","#dispatchDecision","#reconcileInFlight","#lastReconcileAt","#lastStaleBindingSweepAt","#needsApproval","#supersedeProposals","#withLease","#executeDecision","#requireItem","#findBinding","#findSession","#upsertLinkedItem","#requireOrPrepareBinding","#switchThread","#requireBinding","#requireSession"],"sources":["../../src/rules/dispatcher.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController, AgentControllerEventListener, Session } from '@mastra/core/agent-controller';\nimport { RequestContext } from '@mastra/core/request-context';\n\nimport { resolvePromptInvocation, resolveSkillInvocation } from '../skills/service.js';\nimport type { SkillSession } from '../skills/service.js';\nimport type {\n FactoryDeferredDecisionRecord,\n FactoryPendingStartRecord,\n FactoryRunBindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { FACTORY_RULE_MATERIALIZATION_KEY } from '../storage/domains/work-items/base.js';\nimport { FactoryDispatchError, factoryDispatchFailureCode } from './dispatch-errors.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport type { FactoryCommitDecision, FactoryRuleActor, FactoryRuleCausalEntry } from './types.js';\nimport { FACTORY_RULE_STAGES } from './types.js';\nimport { MAX_FACTORY_RULE_CAUSAL_DEPTH, validateFactoryRuleDecision } from './validation.js';\n\nconst LEASE_MS = 30_000;\nconst POLL_MS = 1_000;\nconst BATCH_SIZE = 10;\nconst MAX_ATTEMPTS = 5;\nconst MAX_ERROR_LENGTH = 512;\nconst MAX_BACKOFF_MS = 60_000;\nconst SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS = 10 * 60_000;\n// Dispatches can legitimately run for minutes. Woken skill invocations hold\n// capacity until their agent run reaches a terminal state; binding preparation\n// also runs detached from the poll loop under this concurrency cap.\nconst MAX_IN_FLIGHT = 25;\n// Staleness sweep: legacy/leaked active bindings (item deleted, transition\n// path bypassed, or pre-dating terminal-stage revocation) are revoked on a\n// slow cadence so the per-tick reconcile walk stays bounded.\nconst STALE_BINDING_SWEEP_INTERVAL_MS = 10 * 60_000;\nconst STALE_BINDING_TTL_MS = 24 * 60 * 60_000;\n// The bound-thread reconcile walk reads a cursor + messages per binding; it\n// exists to catch results missed at run end, so it runs on a slow cadence off\n// the claim path rather than on every 1s tick.\nconst RECONCILE_INTERVAL_MS = 30_000;\n\nfunction waitForAgentEndOrTimeout(agentEnd: Promise<void>): Promise<boolean> {\n return new Promise(resolve => {\n const timeout = setTimeout(() => resolve(false), SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS);\n timeout.unref?.();\n void agentEnd.then(() => {\n clearTimeout(timeout);\n resolve(true);\n });\n });\n}\n\ninterface ThreadSwitchSession {\n thread: {\n switch(input: { threadId: string }): Promise<unknown>;\n };\n}\n\ninterface FactoryNotificationResult {\n persisted?: Promise<unknown>;\n accepted?: Promise<{\n action?: string;\n output?: { consumeStream(): Promise<unknown> };\n }>;\n}\n\ninterface DispatcherSession extends SkillSession {\n thread: {\n switch(input: { threadId: string }): Promise<unknown>;\n listActiveMessages(): Promise<Array<{ id: string }>>;\n };\n abort(): void;\n sendSignal(\n input: { id: string; type: 'user'; tagName: 'user'; contents: string },\n options: { requestContext: RequestContext; requireDelivery?: boolean },\n ): { accepted: Promise<{ accepted: true; runId?: string; action?: string }> };\n subscribe(listener: AgentControllerEventListener): () => void;\n}\n\ntype FactoryController = Pick<AgentController<MastraCodeState>, 'getSessionByResource'>;\ntype BoundDispatcherSession = Session<MastraCodeState>;\n\nexport interface FactoryBindingPreparationInput {\n record: FactoryDeferredDecisionRecord;\n item: WorkItemRow;\n role: string;\n}\n\nexport interface FactoryDecisionDispatcherOptions {\n controller: FactoryController;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n storage: WorkItemsStorage;\n ownerId?: string;\n /** `false` parks `invokeSkill` effects as `proposed`; every other effect still runs. */\n isAutoRunEnabled: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n reconcileToolResults?: () => Promise<void>;\n prepareBinding?: (input: FactoryBindingPreparationInput) => Promise<void>;\n primeCredentials?: (tenant: { orgId: string; userId: string }) => Promise<void>;\n maxInFlight?: number;\n /** How often the stale-binding sweep runs. Defaults to 10 minutes. */\n staleBindingSweepIntervalMs?: number;\n /** Active bindings older than this are revoked by the sweep. Defaults to 24 hours. */\n staleBindingTtlMs?: number;\n /** How often the bound-thread reconcile walk runs. Defaults to 30 seconds. */\n reconcileIntervalMs?: number;\n}\n\nfunction positiveMs(value: number | undefined, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;\n}\n\nfunction sanitizeDispatchError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n return message\n .replace(/\\b(?:bearer|token|api[-_ ]?key|authorization)\\s*[:=]?\\s*[^\\s,;]+/gi, '[redacted]')\n .slice(0, MAX_ERROR_LENGTH);\n}\n\nfunction retryAt(now: Date, attempts: number): Date {\n return new Date(now.getTime() + Math.min(1_000 * 2 ** Math.max(0, attempts - 1), MAX_BACKOFF_MS));\n}\n\nfunction externalSourceForDecision(decision: Extract<FactoryCommitDecision, { type: 'upsertLinkedWorkItem' }>) {\n const [integrationId, type] =\n decision.source === 'github-pr'\n ? ['github', 'pull-request']\n : decision.source === 'github-issue'\n ? ['github', 'issue']\n : decision.source === 'linear-issue'\n ? ['linear', 'issue']\n : ['factory', 'manual'];\n return { integrationId, type, externalId: decision.sourceKey, url: decision.url ?? undefined };\n}\n\nfunction deferredActor(record: FactoryDeferredDecisionRecord): FactoryRuleActor {\n const actor = record.actor;\n if (\n actor?.type === 'github' &&\n typeof actor.login === 'string' &&\n typeof actor.trusted === 'boolean' &&\n typeof actor.factoryAuthored === 'boolean'\n ) {\n return {\n type: 'github',\n login: actor.login,\n trusted: actor.trusted,\n factoryAuthored: actor.factoryAuthored,\n };\n }\n return { type: 'system', id: 'factory-rule-dispatcher' };\n}\n\nfunction leaseIdentity(\n record: Pick<FactoryDeferredDecisionRecord | FactoryPendingStartRecord, 'id' | 'orgId' | 'factoryProjectId'>,\n ownerId: string,\n) {\n return { id: record.id, orgId: record.orgId, factoryProjectId: record.factoryProjectId, ownerId };\n}\n\nasync function awaitNotification(\n send: () => Promise<FactoryNotificationResult>,\n requireDelivery = false,\n): Promise<void> {\n try {\n const notification = await send();\n const [, accepted] = await Promise.all([notification.persisted, notification.accepted]);\n if (!accepted) {\n if (requireDelivery) {\n throw new FactoryDispatchError(\n 'notification_delivery_failed',\n 'Factory notification was persisted without agent delivery.',\n );\n }\n return;\n }\n if (!requireDelivery) return;\n if (accepted.action === 'wake') {\n if (!accepted.output) {\n throw new FactoryDispatchError('notification_delivery_failed', 'Factory notification wake had no output.');\n }\n await accepted.output.consumeStream();\n return;\n }\n if (accepted.action !== 'deliver') {\n throw new FactoryDispatchError(\n 'notification_delivery_failed',\n `Factory notification did not reach the agent (${String(accepted.action)}).`,\n );\n }\n } catch (error) {\n if (error instanceof FactoryDispatchError) throw error;\n throw new FactoryDispatchError(\n 'notification_delivery_failed',\n `Factory notification delivery failed: ${sanitizeDispatchError(error)}`,\n { cause: error },\n );\n }\n}\n\nexport class FactoryDecisionDispatcher {\n readonly #controller: FactoryController;\n readonly #transitionService: Pick<FactoryTransitionService, 'transition'>;\n readonly #storage: WorkItemsStorage;\n readonly #ownerId: string;\n readonly #isAutoRunEnabled: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n readonly #reconcileToolResults?: () => Promise<void>;\n readonly #prepareBinding?: (input: FactoryBindingPreparationInput) => Promise<void>;\n readonly #primeCredentials?: (tenant: { orgId: string; userId: string }) => Promise<void>;\n readonly #maxInFlight: number;\n readonly #staleBindingSweepIntervalMs: number;\n readonly #staleBindingTtlMs: number;\n #lastStaleBindingSweepAt?: Date;\n readonly #reconcileIntervalMs: number;\n #lastReconcileAt?: Date;\n #reconcileInFlight?: Promise<void>;\n #timer?: ReturnType<typeof setInterval>;\n #activeClaim?: Promise<void>;\n readonly #inFlight = new Set<Promise<void>>();\n\n constructor(options: FactoryDecisionDispatcherOptions) {\n this.#controller = options.controller;\n this.#transitionService = options.transitionService;\n this.#storage = options.storage;\n this.#ownerId = options.ownerId ?? `factory-dispatcher:${randomUUID()}`;\n this.#isAutoRunEnabled = options.isAutoRunEnabled;\n this.#reconcileToolResults = options.reconcileToolResults;\n this.#prepareBinding = options.prepareBinding;\n this.#primeCredentials = options.primeCredentials;\n const maxInFlight = options.maxInFlight ?? MAX_IN_FLIGHT;\n this.#maxInFlight = Number.isFinite(maxInFlight) && maxInFlight > 0 ? Math.floor(maxInFlight) : MAX_IN_FLIGHT;\n this.#staleBindingSweepIntervalMs = positiveMs(\n options.staleBindingSweepIntervalMs,\n STALE_BINDING_SWEEP_INTERVAL_MS,\n );\n this.#staleBindingTtlMs = positiveMs(options.staleBindingTtlMs, STALE_BINDING_TTL_MS);\n this.#reconcileIntervalMs = positiveMs(options.reconcileIntervalMs, RECONCILE_INTERVAL_MS);\n }\n\n start(): void {\n if (this.#timer) return;\n void this.#tick();\n this.#timer = setInterval(() => void this.#tick(), POLL_MS);\n this.#timer.unref?.();\n }\n\n async stop(): Promise<void> {\n if (this.#timer) clearInterval(this.#timer);\n this.#timer = undefined;\n await this.#activeClaim;\n await Promise.allSettled([...this.#inFlight]);\n }\n\n async runOnce(now = new Date()): Promise<void> {\n await Promise.all(await this.#claimAndStart(now));\n }\n\n /**\n * Claims a batch and starts dispatches without awaiting their completion.\n * Dispatches can legitimately take minutes (skill kickoffs consume the\n * agent's run stream; binding preparation provisions sandboxes), so awaiting\n * them here would freeze the poll loop and starve every other queued\n * decision. In-flight records stay protected from re-claim by lease renewal.\n */\n async #claimAndStart(now: Date): Promise<Array<Promise<void>>> {\n // Fire-and-forget like the reconcile walk: the sweep reads every active\n // binding, so awaiting it would stretch the tick as the active set grows.\n void this.#maybeSweepStaleBindings(now);\n this.#maybeReconcileToolResults(now);\n const capacity = this.#maxInFlight - this.#inFlight.size;\n if (capacity <= 0) return [];\n const limit = Math.min(BATCH_SIZE, capacity);\n const leaseExpiresAt = new Date(now.getTime() + LEASE_MS);\n // Starts are claimed before deferred decisions: a pending start is a user\n // waiting on a brand-new session, while a deferred decision is a background\n // continuation of one that is already running. A deep decision queue must\n // never starve new sessions out of the tick.\n const starts = await this.#storage.claimPendingStarts({\n ownerId: this.#ownerId,\n now,\n leaseExpiresAt,\n limit,\n });\n const decisionsLimit = limit - starts.length;\n const decisions =\n decisionsLimit > 0\n ? await this.#storage.claimDeferredDecisions({\n ownerId: this.#ownerId,\n now,\n leaseExpiresAt,\n limit: decisionsLimit,\n })\n : [];\n return [\n ...starts.map(start => this.#track(this.#dispatchPendingStart(start, now))),\n ...decisions.map(decision => this.#track(this.#dispatchDecision(decision, now))),\n ];\n }\n\n /**\n * Throttled, coalesced, non-blocking bound-thread reconcile: dispatch\n * claiming never waits behind cursor + message reads, and overlapping runs\n * are skipped while one is still in flight.\n */\n #maybeReconcileToolResults(now: Date): void {\n if (!this.#reconcileToolResults || this.#reconcileInFlight) return;\n if (this.#lastReconcileAt && now.getTime() - this.#lastReconcileAt.getTime() < this.#reconcileIntervalMs) return;\n this.#lastReconcileAt = now;\n const run = this.#reconcileToolResults()\n .catch(error => {\n console.error('Factory tool-result reconcile failed', sanitizeDispatchError(error));\n })\n .finally(() => {\n this.#reconcileInFlight = undefined;\n });\n this.#reconcileInFlight = run;\n this.#track(run);\n }\n\n /** Slow-cadence revocation of leaked/legacy bindings; failures never block the claim path. */\n async #maybeSweepStaleBindings(now: Date): Promise<void> {\n // The first tick only anchors the cadence: sweeping at boot would race the\n // startup reconcile that is still draining trailing tool results.\n if (!this.#lastStaleBindingSweepAt) {\n this.#lastStaleBindingSweepAt = now;\n return;\n }\n if (now.getTime() - this.#lastStaleBindingSweepAt.getTime() < this.#staleBindingSweepIntervalMs) return;\n this.#lastStaleBindingSweepAt = now;\n try {\n const revoked = await this.#storage.revokeStaleRunBindings({\n olderThan: new Date(now.getTime() - this.#staleBindingTtlMs),\n now,\n });\n if (revoked > 0) console.info(`Factory stale-binding sweep revoked ${revoked} binding(s)`);\n } catch (error) {\n console.error('Factory stale-binding sweep failed', sanitizeDispatchError(error));\n }\n }\n\n #track(dispatch: Promise<void>): Promise<void> {\n this.#inFlight.add(dispatch);\n void dispatch.catch(() => {}).then(() => this.#inFlight.delete(dispatch));\n return dispatch;\n }\n\n async #tick(): Promise<void> {\n if (this.#activeClaim) return;\n this.#activeClaim = this.#claimAndStart(new Date()).then(\n dispatches => {\n for (const dispatch of dispatches) {\n dispatch.catch(error => {\n console.error('Factory decision dispatch failed', sanitizeDispatchError(error));\n });\n }\n },\n error => {\n console.error('Factory decision dispatch cycle failed', sanitizeDispatchError(error));\n },\n );\n try {\n await this.#activeClaim;\n } finally {\n this.#activeClaim = undefined;\n }\n }\n\n async #dispatchDecision(record: FactoryDeferredDecisionRecord, now: Date): Promise<void> {\n let executionCompleted = false;\n try {\n const decision = validateFactoryRuleDecision(record.decision, record.causalChain.length);\n if (decision.type === 'reject') throw new Error('Deferred Factory decisions cannot reject.');\n if (await this.#needsApproval(record, decision)) {\n const proposed = await this.#storage.proposeDeferredDecision(leaseIdentity(record, this.#ownerId), new Date());\n if (!proposed) throw new Error('Factory decision lease was lost before approval could be requested.');\n return;\n }\n await this.#supersedeProposals(record, decision);\n await this.#withLease(\n async leaseExpiresAt =>\n this.#storage.renewDeferredDecisionLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),\n async () => this.#executeDecision(record, decision),\n );\n executionCompleted = true;\n const completed = await this.#storage.completeDeferredDecision(leaseIdentity(record, this.#ownerId), new Date());\n if (!completed) throw new Error('Factory decision lease was lost before completion.');\n } catch (error) {\n const terminal = record.attempts >= MAX_ATTEMPTS;\n await this.#storage.failDeferredDecision({\n ...leaseIdentity(record, this.#ownerId),\n now: new Date(),\n availableAt: retryAt(now, record.attempts),\n lastError: sanitizeDispatchError(error),\n failureCode: factoryDispatchFailureCode(error),\n terminal,\n advanceDeliveryGeneration: !executionCompleted,\n });\n }\n }\n\n /**\n * A proposal is a question: \"should this run start?\" Once that run is\n * starting anyway — because a person approved a later copy, or armed the item\n * — the question has been answered and the card must stop asking it. Left\n * alone the badge outlives the work it describes, and the one affordance that\n * means \"the loop is stopped, answer this\" cries wolf.\n */\n async #supersedeProposals(record: FactoryDeferredDecisionRecord, decision: FactoryCommitDecision): Promise<void> {\n if (decision.type !== 'invokeSkill' || !record.workItemId) return;\n try {\n await this.#storage.supersedeDecisionsForWorkItem({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: record.workItemId,\n role: decision.role,\n supersededAt: new Date(),\n });\n } catch (error) {\n // Best-effort: a stale badge is not worth failing the run it describes.\n console.error('Factory proposal supersede failed', sanitizeDispatchError(error));\n }\n }\n\n /** Starting an agent run spends the project's compute and executes its code — the one effect a human owns. */\n async #needsApproval(record: FactoryDeferredDecisionRecord, decision: FactoryCommitDecision): Promise<boolean> {\n if (decision.type !== 'invokeSkill' || record.approvedAt !== null) return false;\n if (await this.#isAutoRunEnabled({ orgId: record.orgId, factoryProjectId: record.factoryProjectId })) return false;\n // Withholding auto-run decides what the Factory may pick up on its own, not\n // whether it may finish work a person already handed it. Once someone starts\n // an item, the runs that carry it to review are that same request continuing.\n const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;\n return item?.autonomyArmedAt == null;\n }\n\n async #executeDecision(record: FactoryDeferredDecisionRecord, decision: FactoryCommitDecision): Promise<void> {\n const nextChain: FactoryRuleCausalEntry[] = [\n ...(record.causalChain as FactoryRuleCausalEntry[]),\n { ingressId: record.idempotencyKey, decisionType: decision.type },\n ];\n if (nextChain.length > MAX_FACTORY_RULE_CAUSAL_DEPTH) throw new Error('Factory rule causal depth exceeded.');\n\n switch (decision.type) {\n case 'transition': {\n const item = await this.#requireItem(record);\n const result = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: item.id,\n board: decision.board,\n stage: decision.stage,\n expectedRevision: item.revision,\n actor: { type: 'system', id: 'factory-rule-dispatcher' },\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}` },\n cause: 'rule_decision',\n causalChain: nextChain,\n ...(decision.reenter ? { reenter: true } : {}),\n });\n if (result.status === 'rejected') throw new Error(`${result.code}: ${result.reason}`);\n const transitionMessage = decision.message;\n if (!transitionMessage) return;\n // Best-effort recipient lookup: no active binding (or no authenticated\n // session owner) means nobody is engaged with this item, so the\n // transition itself is the whole effect. A retry after a delivery\n // failure is safe because the transition replays by ingress identity.\n const binding = await this.#findBinding(record, transitionMessage.role);\n if (!binding) return;\n const startedBy = item.sessions[binding.role]?.startedBy;\n if (!startedBy) return;\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#findSession(binding);\n if (!session) return;\n await awaitNotification(\n () =>\n session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'rule-message',\n summary: transitionMessage.text,\n priority: 'high',\n payload: { message: transitionMessage.text },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: 'wake' },\n requestContext,\n },\n ),\n true,\n );\n return;\n }\n case 'upsertLinkedWorkItem': {\n await this.#upsertLinkedItem(record, decision, nextChain);\n return;\n }\n case 'invokeSkill': {\n const binding = await this.#requireOrPrepareBinding(record, decision.role);\n const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const resolved =\n decision.skillName === undefined\n ? await resolvePromptInvocation(this.#controller, {\n resourceId: binding.resourceId,\n prompt: decision.prompt,\n })\n : await resolveSkillInvocation(this.#controller, {\n resourceId: binding.resourceId,\n name: decision.skillName,\n arguments: decision.arguments,\n });\n const session = resolved.session as DispatcherSession;\n await this.#switchThread(session, binding);\n const deliveryId =\n record.deliveryGeneration === 0 ? record.id : `${record.id}:retry:${record.deliveryGeneration}`;\n const delivered = await session.thread.listActiveMessages();\n if (delivered.some(message => message.id === deliveryId)) return;\n if (decision.cancelInFlight) session.abort();\n const precedingMessage = decision.precedingMessage;\n if (precedingMessage) {\n await awaitNotification(() =>\n session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'stage-transition',\n summary: precedingMessage,\n priority: 'medium',\n payload: { message: precedingMessage },\n sourceId: `${record.id}:stage-transition`,\n dedupeKey: `${record.idempotencyKey}:stage-transition`,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: 'persist' },\n requestContext,\n },\n ),\n );\n }\n let resolveAgentEnd!: () => void;\n let agentEnd!: Promise<void>;\n // The run's own verdict, not the delivery's. A signal can reach the\n // agent perfectly and the run still die on a provider error or be\n // cancelled mid-flight; without this the decision reports success and\n // the break is invisible on the card.\n let endReason: 'complete' | 'aborted' | 'error' | 'suspended' | undefined;\n // Re-armed before a redelivery so the second send waits on its own run's\n // ending rather than seeing the one that already resolved.\n const armAgentEnd = () => {\n endReason = undefined;\n agentEnd = new Promise<void>(resolve => {\n resolveAgentEnd = resolve;\n });\n };\n armAgentEnd();\n const unsubscribe = session.subscribe(event => {\n if (event.type === 'agent_end') {\n endReason = event.reason;\n resolveAgentEnd();\n }\n });\n\n const sendKickoff = async () => {\n const result = session.sendSignal(\n {\n id: deliveryId,\n type: 'user',\n tagName: 'user',\n contents: resolved.message,\n },\n // Without `requireDelivery` the session resolves `accepted` on the\n // next tick and swallows wake failures, so a kickoff that never\n // reached the agent would be marked succeeded and the thread would\n // stay empty forever.\n { requestContext, requireDelivery: true },\n );\n const settled = await result.accepted;\n if (settled.action !== 'wake' && settled.action !== 'deliver') {\n // An undefined action means the session did not verify delivery at\n // all — with `requireDelivery` set that is a contract violation, not\n // a success.\n throw new Error(`Factory skill invocation signal did not reach the agent (${String(settled.action)}).`);\n }\n return settled;\n };\n\n try {\n let settled = await sendKickoff();\n if (settled.action === 'deliver') {\n // `deliver` means the signal was queued onto a run that was already\n // in flight. If that run ends before draining its queue the prompt\n // is dropped silently: no turn starts, no error surfaces, and the\n // decision reports success while the card sits in its new stage with\n // nobody working. Signals persist under their generation-scoped id\n // (the same identity the replay guard above reads), so confirm the\n // message actually landed in the thread rather than trusting the ack.\n const landed = await session.thread.listActiveMessages();\n if (!landed.some(message => message.id === deliveryId)) {\n // The condition that resolves this is the in-flight run ending, so\n // wait for exactly that and redeliver into the idle session. A\n // backoff cannot work here: retries are sized in seconds and a turn\n // takes minutes, so every attempt lands on the same busy run and\n // the card burns its whole budget without the session ever having\n // had a chance to be free.\n if (!(await waitForAgentEndOrTimeout(agentEnd))) {\n throw new Error('Factory skill invocation is waiting on a run that has not ended.');\n }\n armAgentEnd();\n settled = await sendKickoff();\n if (settled.action !== 'wake') {\n throw new Error('Factory skill invocation was queued onto an ending run and never reached the agent.');\n }\n }\n }\n // A landed `deliver` still runs on the in-flight session, so the run's\n // terminal outcome matters as much as a fresh wake's: a run that ends\n // in error after accepting the prompt has still failed this decision.\n {\n const observed = await waitForAgentEndOrTimeout(agentEnd);\n if (!observed) {\n console.warn('Factory skill run terminal event was not observed before timeout', {\n decisionId: record.id,\n runId: settled.action === 'wake' ? settled.runId : undefined,\n });\n } else if (endReason === 'error') {\n throw new Error('Factory skill run ended in error.');\n } else if (endReason === 'aborted') {\n // Retryable, though an abort reads as deliberate. The stream does\n // not say who aborted, and in practice the dominant cause is the\n // process going away underneath the run — an operator restarting\n // the server — not anyone deciding this work should stop. Treating\n // that as terminal dead-ends the card at attempt 1 with nothing on\n // the board to press. A spurious retry is bounded by MAX_ATTEMPTS;\n // a dead card costs a human a manual nudge.\n throw new Error('Factory skill run was aborted before it finished.');\n }\n }\n } finally {\n unsubscribe();\n }\n return;\n }\n case 'sendMessage': {\n const binding = decision.prepareBinding\n ? await this.#requireOrPrepareBinding(record, decision.role)\n : await this.#requireBinding(record, decision.role);\n const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n () =>\n session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'rule-message',\n summary: decision.message,\n priority: decision.priority ?? 'high',\n payload: { message: decision.message },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: decision.idleBehavior ?? 'wake' },\n requestContext,\n },\n ),\n true,\n );\n return;\n }\n case 'notify': {\n const binding = await this.#requireBinding(record);\n const session = await this.#requireSession(binding);\n await awaitNotification(() =>\n session.sendNotificationSignal({\n source: 'factory',\n kind: 'rule-notification',\n summary: decision.title,\n payload: { body: decision.body, level: decision.level },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n }),\n );\n }\n }\n }\n\n async #upsertLinkedItem(\n record: FactoryDeferredDecisionRecord,\n decision: Extract<FactoryCommitDecision, { type: 'upsertLinkedWorkItem' }>,\n causalChain: FactoryRuleCausalEntry[],\n ): Promise<void> {\n const result = await this.#storage.upsert({\n orgId: record.orgId,\n userId: 'factory-rule-dispatcher',\n factoryProjectId: record.factoryProjectId,\n input: {\n externalSource: externalSourceForDecision(decision),\n parentWorkItemId: record.workItemId,\n title: decision.title,\n stages: ['intake'],\n sessions: {},\n metadata: { ...decision.metadata, [FACTORY_RULE_MATERIALIZATION_KEY]: record.idempotencyKey },\n },\n reuseMode: 'preserve',\n });\n const materializedByDecision = result.item.metadata?.[FACTORY_RULE_MATERIALIZATION_KEY] === record.idempotencyKey;\n if (!materializedByDecision && (decision.stage === 'intake' || !result.item.stages.includes('intake'))) return;\n\n const board = decision.board;\n let expectedRevision = result.item.revision;\n if (materializedByDecision) {\n const initial = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: result.item.id,\n board,\n stage: 'intake',\n expectedRevision,\n actor: deferredActor(record),\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}:${result.item.id}:initial-entry` },\n cause: 'linked_item_materialized',\n causalChain,\n initialEntry: true,\n });\n if (initial.status === 'rejected') {\n if (result.created) await this.#storage.delete({ orgId: record.orgId, id: result.item.id });\n throw new Error(`${initial.code}: ${initial.reason}`);\n }\n expectedRevision = initial.revision;\n }\n if (decision.stage === 'intake') return;\n\n const moved = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: result.item.id,\n board,\n stage: decision.stage,\n expectedRevision,\n actor: { type: 'system', id: 'factory-rule-dispatcher' },\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}:${result.item.id}:destination` },\n cause: materializedByDecision ? 'linked_item_materialized' : 'linked_item_reconciled',\n causalChain,\n });\n if (moved.status === 'rejected') throw new Error(`${moved.code}: ${moved.reason}`);\n }\n\n async #requireItem(record: FactoryDeferredDecisionRecord) {\n if (!record.workItemId) throw new Error('Factory decision is not linked to a work item.');\n const item = await this.#storage.get({ orgId: record.orgId, id: record.workItemId });\n if (!item) throw new Error('Factory work item not found.');\n return item;\n }\n\n async #findBinding(\n record: FactoryDeferredDecisionRecord,\n role?: string,\n ): Promise<FactoryRunBindingRecord | undefined> {\n if (!record.workItemId) throw new Error('Factory decision is not linked to a work item.');\n const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId, record.workItemId);\n return bindings\n .filter(candidate => candidate.status === 'active' && (role === undefined || candidate.role === role))\n .sort((left, right) => {\n if (role === undefined && left.role === 'work' && right.role !== 'work') return -1;\n if (role === undefined && right.role === 'work' && left.role !== 'work') return 1;\n return right.createdAt.getTime() - left.createdAt.getTime() || left.id.localeCompare(right.id);\n })[0];\n }\n\n async #requireBinding(record: FactoryDeferredDecisionRecord, role?: string): Promise<FactoryRunBindingRecord> {\n const binding = await this.#findBinding(record, role);\n if (!binding) {\n throw new FactoryDispatchError(\n 'session_unavailable',\n role ? `No active Factory binding for role ${role}.` : 'No active Factory binding.',\n );\n }\n return binding;\n }\n\n async #requireOrPrepareBinding(\n record: FactoryDeferredDecisionRecord,\n role: string,\n ): Promise<FactoryRunBindingRecord> {\n const binding = await this.#findBinding(record, role);\n if (binding) {\n const session = await this.#controller.getSessionByResource(binding.resourceId);\n if (session) return binding;\n }\n if (!this.#prepareBinding) {\n throw new FactoryDispatchError(\n 'session_unavailable',\n binding ? 'Bound Factory session not found.' : `No active Factory binding for role ${role}.`,\n );\n }\n const item = await this.#requireItem(record);\n await this.#prepareBinding({ record, item, role });\n return this.#requireBinding(record, role);\n }\n\n async #findSession(binding: FactoryRunBindingRecord): Promise<BoundDispatcherSession | undefined> {\n const session = await this.#controller.getSessionByResource(binding.resourceId);\n if (!session) return undefined;\n await this.#switchThread(session, binding);\n return session;\n }\n\n async #requireSession(binding: FactoryRunBindingRecord): Promise<BoundDispatcherSession> {\n const session = await this.#findSession(binding);\n if (!session) throw new FactoryDispatchError('session_unavailable', 'Bound Factory session not found.');\n return session;\n }\n\n async #switchThread(session: ThreadSwitchSession, binding: FactoryRunBindingRecord): Promise<void> {\n await session.thread.switch({ threadId: binding.threadId });\n }\n\n async #withLease(\n renew: (leaseExpiresAt: Date) => Promise<unknown | null>,\n effect: () => Promise<void>,\n ): Promise<void> {\n let renewalFailure: unknown;\n let renewal = Promise.resolve();\n const timer = setInterval(\n () => {\n renewal = renewal.then(async () => {\n try {\n const renewed = await renew(new Date(Date.now() + LEASE_MS));\n if (!renewed) renewalFailure = new Error('Factory dispatch lease was lost during execution.');\n } catch (error) {\n renewalFailure = error;\n }\n });\n },\n Math.floor(LEASE_MS / 3),\n );\n timer.unref?.();\n try {\n await effect();\n await renewal;\n if (renewalFailure) throw renewalFailure;\n } finally {\n clearInterval(timer);\n await renewal;\n }\n }\n\n async #dispatchPendingStart(record: FactoryPendingStartRecord, now: Date): Promise<void> {\n try {\n await this.#withLease(\n async leaseExpiresAt =>\n this.#storage.renewPendingStartLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),\n async () => {\n if (record.message === null) return;\n const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId);\n const binding = bindings.find(\n candidate => candidate.id === record.bindingId && candidate.status === 'active',\n );\n if (!binding) {\n throw new FactoryDispatchError(\n 'session_unavailable',\n 'Prepared Factory binding is unavailable or revoked.',\n );\n }\n // Wake runs build the Factory workspace, which requires the\n // authenticated session owner on the request context.\n const item = await this.#storage.get({ orgId: record.orgId, id: binding.workItemId });\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n () =>\n session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'run-kickoff',\n summary: record.message!,\n priority: 'high',\n payload: { message: record.message },\n sourceId: record.id,\n dedupeKey: `factory-kickoff:${record.kickoffKey}`,\n },\n { ifActive: { behavior: 'deliver' }, ifIdle: { behavior: 'wake' }, requestContext },\n ),\n true,\n );\n },\n );\n const completed = await this.#storage.completePendingStart(leaseIdentity(record, this.#ownerId), new Date());\n if (!completed) throw new Error('Factory kickoff lease was lost before completion.');\n } catch (error) {\n await this.#storage.failPendingStart({\n ...leaseIdentity(record, this.#ownerId),\n now: new Date(),\n availableAt: retryAt(now, record.attempts),\n lastError: sanitizeDispatchError(error),\n failureCode: factoryDispatchFailureCode(error),\n terminal: record.attempts >= MAX_ATTEMPTS,\n });\n }\n }\n}\n\nexport const FACTORY_DISPATCH_CONSTANTS = {\n leaseMs: LEASE_MS,\n pollMs: POLL_MS,\n batchSize: BATCH_SIZE,\n maxAttempts: MAX_ATTEMPTS,\n maxErrorLength: MAX_ERROR_LENGTH,\n maxBackoffMs: MAX_BACKOFF_MS,\n skillCompletionObservationTimeoutMs: SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS,\n maxInFlight: MAX_IN_FLIGHT,\n stages: FACTORY_RULE_STAGES,\n} as const;\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"processor.d.ts","sourceRoot":"","sources":["../../src/rules/processor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"processor.d.ts","sourceRoot":"","sources":["../../src/rules/processor.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAEpF,OAAO,KAAK,EACV,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,EACpB,SAAS,EACV,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,uCAAuC,CAAC;AAIpH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAExE,OAAO,KAAK,EAIV,oBAAoB,EAEpB,YAAY,EAEb,MAAM,YAAY,CAAC;AAyBpB,KAAK,sBAAsB,GAAG;IAC5B,YAAY,CAAC,KAAK,EAAE;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE;YAAE,SAAS,CAAC,EAAE;gBAAE,KAAK,CAAC,EAAE,IAAI,CAAA;aAAE,CAAA;SAAE,CAAC;QAC1C,OAAO,EAAE;YAAE,KAAK,EAAE,WAAW,CAAC;YAAC,SAAS,EAAE,KAAK,CAAA;SAAE,CAAC;KACnD,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,eAAe,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAChE,CAAC;AA0KF,qBAAa,0BAA2B,YAAW,SAAS,CAAC,eAAe,CAAC;IAKzE,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJ1B,QAAQ,CAAC,EAAE,mBAAY;IACvB,QAAQ,CAAC,OAAO,mBAAY;gBAGT,OAAO,EAAE;QACxB,KAAK,EAAE,YAAY,CAAC;QACpB,OAAO,EAAE,gBAAgB,CAAC;QAC1B,iBAAiB,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,CAAC,CAAC;QACjE,aAAa,CAAC,EAAE,sBAAsB,CAAC;QACvC,2BAA2B,CAAC,EAAE,CAAC,KAAK,EAAE;YACpC,OAAO,EAAE,uBAAuB,CAAC;YACjC,IAAI,EAAE,WAAW,CAAC;YAClB,kBAAkB,EAAE,MAAM,CAAC;YAC3B,UAAU,EAAE,MAAM,CAAC;YACnB,QAAQ,EAAE,MAAM,CAAC;YACjB,SAAS,EAAE,oBAAoB,CAAC;YAChC,UAAU,EAAE,oBAAoB,CAAC;YACjC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;SAC7B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACrB;IAGG,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAY9E,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAkFnF,wBAAwB,IAAI,OAAO,CAAC,IAAI,CAAC;IAMzC,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;YAoCzD,cAAc;YA8Bd,gBAAgB;CAkG/B"}
|
package/dist/rules/processor.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { resolveFactoryToolRule } from "./resolve.js";
|
|
2
|
-
import {
|
|
2
|
+
import { factoryRuleStage } from "./types.js";
|
|
3
3
|
import { normalizeFactoryRuleJsonValue, validateFactoryRuleDecisions } from "./validation.js";
|
|
4
|
+
import { workItemSource } from "./transition-service.js";
|
|
4
5
|
import { getFactorySessionCoordinates } from "./binding-context.js";
|
|
5
6
|
import { createHash } from "crypto";
|
|
7
|
+
import { resolveRequestThinkingLevel } from "@mastra/code-sdk/agents/model";
|
|
6
8
|
//#region src/rules/processor.ts
|
|
7
9
|
const STATE_ID = "factory-phase";
|
|
8
10
|
const RULE_TIMEOUT_MS = 5e3;
|
|
9
11
|
const TRANSCRIPT_PAGE_SIZE = 50;
|
|
10
12
|
const MAX_LINKED_ITEMS = 5;
|
|
11
13
|
function ruleStage(item) {
|
|
12
|
-
|
|
13
|
-
return stage !== void 0 && isFactoryRuleStage(stage) ? stage : void 0;
|
|
14
|
+
return item ? factoryRuleStage(item.stages) : void 0;
|
|
14
15
|
}
|
|
15
16
|
function itemInRuleStage(item) {
|
|
16
17
|
return ruleStage(item) !== void 0;
|
|
@@ -24,11 +25,15 @@ const PHASE_LABELS = {
|
|
|
24
25
|
done: "Done",
|
|
25
26
|
canceled: "Canceled"
|
|
26
27
|
};
|
|
27
|
-
function
|
|
28
|
-
if (!
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
function reviewRuntimeFromRequestContext(requestContext) {
|
|
29
|
+
if (!requestContext || typeof requestContext.get !== "function") throw new Error("Factory review phase requires a controller request context.");
|
|
30
|
+
const context = requestContext.get("controller");
|
|
31
|
+
const modelId = context?.session?.modelId.trim();
|
|
32
|
+
if (!modelId) throw new Error("Factory review phase requires a selected session model.");
|
|
33
|
+
return {
|
|
34
|
+
modelId,
|
|
35
|
+
thinkingLevel: resolveRequestThinkingLevel(context)
|
|
36
|
+
};
|
|
32
37
|
}
|
|
33
38
|
function workItemSourceKey(item) {
|
|
34
39
|
const source = item.externalSource;
|
|
@@ -178,20 +183,27 @@ var FactoryPhaseStateProcessor = class {
|
|
|
178
183
|
factoryProjectId: binding.factoryProjectId
|
|
179
184
|
})).filter((candidate) => candidate.parentWorkItemId === item.id || item.parentWorkItemId === candidate.id).slice(0, MAX_LINKED_ITEMS);
|
|
180
185
|
const board = boardForItem(item);
|
|
181
|
-
const
|
|
186
|
+
const baseValue = {
|
|
182
187
|
status: "active",
|
|
183
188
|
bindingId: binding.id,
|
|
184
189
|
itemId: item.id,
|
|
185
190
|
revision: item.revision,
|
|
186
191
|
stage,
|
|
187
192
|
role: binding.role,
|
|
188
|
-
board,
|
|
189
193
|
ruleSetVersion: this.options.rules.version
|
|
190
194
|
};
|
|
195
|
+
const value = board === "review" ? {
|
|
196
|
+
...baseValue,
|
|
197
|
+
board,
|
|
198
|
+
...reviewRuntimeFromRequestContext(args.requestContext)
|
|
199
|
+
} : {
|
|
200
|
+
...baseValue,
|
|
201
|
+
board
|
|
202
|
+
};
|
|
191
203
|
const cacheKey = phaseCacheKey(value, linked);
|
|
192
204
|
if (hasBase && (args.tracking?.currentCacheKey ?? args.lastSnapshot?.metadata?.state?.cacheKey) === cacheKey) return;
|
|
193
|
-
const linkedText = linked.length ? `\nLinked items: ${linked.map((candidate) => `${workItemSource(candidate)} ${candidate.title}`).join("; ")}` : "";
|
|
194
|
-
const snapshotContents = `Factory ${board} phase: ${PHASE_LABELS[stage]} (${escapeText(stage)})\nWork item: ${escapeText(item.title)} (${item.id})\nRole: ${escapeText(binding.role)}\nRevision: ${item.revision}\nRules: ${escapeText(this.options.rules.version)}\
|
|
205
|
+
const linkedText = linked.length ? `\nLinked items: ${linked.map((candidate) => `${workItemSource(candidate.externalSource)} ${candidate.title}`).join("; ")}` : "";
|
|
206
|
+
const snapshotContents = `Factory ${board} phase: ${PHASE_LABELS[stage]} (${escapeText(stage)})\nWork item: ${escapeText(item.title)} (${item.id})\nRole: ${escapeText(binding.role)}\nRevision: ${item.revision}\nRules: ${escapeText(this.options.rules.version)}\n` + (value.board === "review" ? `Runtime: model=${escapeText(value.modelId)}, reasoning-setting=${escapeText(value.thinkingLevel)}\n` : "") + `Use factory_transition_work_item with expectedRevision ${item.revision} to request a phase change.${escapeText(linkedText)}`;
|
|
195
207
|
const isDelta = hasBase && prior?.status === "active";
|
|
196
208
|
return {
|
|
197
209
|
id: STATE_ID,
|
|
@@ -206,7 +218,11 @@ var FactoryPhaseStateProcessor = class {
|
|
|
206
218
|
board,
|
|
207
219
|
stage,
|
|
208
220
|
role: binding.role,
|
|
209
|
-
revision: item.revision
|
|
221
|
+
revision: item.revision,
|
|
222
|
+
...value.board === "review" ? {
|
|
223
|
+
modelId: value.modelId,
|
|
224
|
+
thinkingLevel: value.thinkingLevel
|
|
225
|
+
} : {}
|
|
210
226
|
},
|
|
211
227
|
metadata: { value: { phase: value } }
|
|
212
228
|
};
|
|
@@ -305,7 +321,7 @@ var FactoryPhaseStateProcessor = class {
|
|
|
305
321
|
ruleSetVersion: this.options.rules.version,
|
|
306
322
|
item: {
|
|
307
323
|
id: item.id,
|
|
308
|
-
source: workItemSource(item),
|
|
324
|
+
source: workItemSource(item.externalSource),
|
|
309
325
|
sourceKey: workItemSourceKey(item),
|
|
310
326
|
parentWorkItemId: item.parentWorkItemId,
|
|
311
327
|
title: item.title,
|