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