@cat-factory/orchestration 0.192.0 → 0.194.0
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/container/dependencies.d.ts +5 -1
- package/dist/container/dependencies.d.ts.map +1 -1
- package/dist/container/engine-collaborators.d.ts.map +1 -1
- package/dist/container/engine-collaborators.js +4 -7
- package/dist/container/engine-collaborators.js.map +1 -1
- package/dist/container/execution-service.d.ts.map +1 -1
- package/dist/container/execution-service.js +3 -0
- package/dist/container/execution-service.js.map +1 -1
- package/dist/container/inline-model-deps.d.ts +20 -0
- package/dist/container/inline-model-deps.d.ts.map +1 -0
- package/dist/container/inline-model-deps.js +38 -0
- package/dist/container/inline-model-deps.js.map +1 -0
- package/dist/container/modules.d.ts.map +1 -1
- package/dist/container/modules.js +23 -39
- package/dist/container/modules.js.map +1 -1
- package/dist/container/review-modules.d.ts.map +1 -1
- package/dist/container/review-modules.js +10 -28
- package/dist/container/review-modules.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/inlineBlockModel.d.ts +51 -0
- package/dist/inlineBlockModel.d.ts.map +1 -0
- package/dist/inlineBlockModel.js +31 -0
- package/dist/inlineBlockModel.js.map +1 -0
- package/dist/modules/board/BoardService.d.ts +2 -0
- package/dist/modules/board/BoardService.d.ts.map +1 -1
- package/dist/modules/board/BoardService.js +4 -0
- package/dist/modules/board/BoardService.js.map +1 -1
- package/dist/modules/board/publicBoardReads.d.ts +12 -0
- package/dist/modules/board/publicBoardReads.d.ts.map +1 -1
- package/dist/modules/board/publicBoardReads.js +19 -4
- package/dist/modules/board/publicBoardReads.js.map +1 -1
- package/dist/modules/bugHunt/BugHuntAssessorService.d.ts +8 -4
- package/dist/modules/bugHunt/BugHuntAssessorService.d.ts.map +1 -1
- package/dist/modules/bugHunt/BugHuntAssessorService.js +6 -10
- package/dist/modules/bugHunt/BugHuntAssessorService.js.map +1 -1
- package/dist/modules/docInterview/DocInterviewService.d.ts +8 -4
- package/dist/modules/docInterview/DocInterviewService.d.ts.map +1 -1
- package/dist/modules/docInterview/DocInterviewService.js +4 -13
- package/dist/modules/docInterview/DocInterviewService.js.map +1 -1
- package/dist/modules/execution/AgentContextBuilder.d.ts +14 -12
- package/dist/modules/execution/AgentContextBuilder.d.ts.map +1 -1
- package/dist/modules/execution/AgentContextBuilder.js +8 -28
- package/dist/modules/execution/AgentContextBuilder.js.map +1 -1
- package/dist/modules/execution/AgentDispatchController.d.ts +115 -0
- package/dist/modules/execution/AgentDispatchController.d.ts.map +1 -0
- package/dist/modules/execution/AgentDispatchController.js +307 -0
- package/dist/modules/execution/AgentDispatchController.js.map +1 -0
- package/dist/modules/execution/ExecutionService.d.ts +18 -105
- package/dist/modules/execution/ExecutionService.d.ts.map +1 -1
- package/dist/modules/execution/ExecutionService.js +70 -589
- package/dist/modules/execution/ExecutionService.js.map +1 -1
- package/dist/modules/execution/ExecutionServiceDependencies.d.ts +19 -3
- package/dist/modules/execution/ExecutionServiceDependencies.d.ts.map +1 -1
- package/dist/modules/execution/ForkChatService.d.ts +8 -4
- package/dist/modules/execution/ForkChatService.d.ts.map +1 -1
- package/dist/modules/execution/ForkChatService.js +4 -13
- package/dist/modules/execution/ForkChatService.js.map +1 -1
- package/dist/modules/execution/IterationCapController.d.ts +57 -0
- package/dist/modules/execution/IterationCapController.d.ts.map +1 -0
- package/dist/modules/execution/IterationCapController.js +141 -0
- package/dist/modules/execution/IterationCapController.js.map +1 -0
- package/dist/modules/execution/JudgeService.d.ts +8 -4
- package/dist/modules/execution/JudgeService.d.ts.map +1 -1
- package/dist/modules/execution/JudgeService.js +4 -13
- package/dist/modules/execution/JudgeService.js.map +1 -1
- package/dist/modules/execution/RunAdmission.d.ts +1 -1
- package/dist/modules/execution/RunAdmission.d.ts.map +1 -1
- package/dist/modules/execution/RunAdmission.js +5 -2
- package/dist/modules/execution/RunAdmission.js.map +1 -1
- package/dist/modules/execution/RunDispatcher.d.ts +31 -195
- package/dist/modules/execution/RunDispatcher.d.ts.map +1 -1
- package/dist/modules/execution/RunDispatcher.js +47 -313
- package/dist/modules/execution/RunDispatcher.js.map +1 -1
- package/dist/modules/execution/RunDispatcherDependencies.d.ts +142 -0
- package/dist/modules/execution/RunDispatcherDependencies.d.ts.map +1 -0
- package/dist/modules/execution/RunDispatcherDependencies.js +27 -0
- package/dist/modules/execution/RunDispatcherDependencies.js.map +1 -0
- package/dist/modules/execution/RunLifecycleController.d.ts +141 -0
- package/dist/modules/execution/RunLifecycleController.d.ts.map +1 -0
- package/dist/modules/execution/RunLifecycleController.js +489 -0
- package/dist/modules/execution/RunLifecycleController.js.map +1 -0
- package/dist/modules/execution/TesterQualityReviewService.d.ts +8 -4
- package/dist/modules/execution/TesterQualityReviewService.d.ts.map +1 -1
- package/dist/modules/execution/TesterQualityReviewService.js +4 -13
- package/dist/modules/execution/TesterQualityReviewService.js.map +1 -1
- package/dist/modules/execution/dispatchPromptSettings.d.ts +42 -2
- package/dist/modules/execution/dispatchPromptSettings.d.ts.map +1 -1
- package/dist/modules/execution/dispatchPromptSettings.js +37 -0
- package/dist/modules/execution/dispatchPromptSettings.js.map +1 -1
- package/dist/modules/execution/run-action-controllers.d.ts +18 -0
- package/dist/modules/execution/run-action-controllers.d.ts.map +1 -0
- package/dist/modules/execution/run-action-controllers.js +19 -0
- package/dist/modules/execution/run-action-controllers.js.map +1 -0
- package/dist/modules/execution/run-context-admission.d.ts +1 -1
- package/dist/modules/execution/run-context-admission.d.ts.map +1 -1
- package/dist/modules/execution/run-context-admission.js +2 -0
- package/dist/modules/execution/run-context-admission.js.map +1 -1
- package/dist/modules/initiative/InitiativeInterviewService.d.ts +8 -4
- package/dist/modules/initiative/InitiativeInterviewService.d.ts.map +1 -1
- package/dist/modules/initiative/InitiativeInterviewService.js +4 -13
- package/dist/modules/initiative/InitiativeInterviewService.js.map +1 -1
- package/dist/modules/kaizen/KaizenService.d.ts +14 -9
- package/dist/modules/kaizen/KaizenService.d.ts.map +1 -1
- package/dist/modules/kaizen/KaizenService.js +11 -16
- package/dist/modules/kaizen/KaizenService.js.map +1 -1
- package/dist/modules/modelPresets/ModelPresetService.d.ts +54 -2
- package/dist/modules/modelPresets/ModelPresetService.d.ts.map +1 -1
- package/dist/modules/modelPresets/ModelPresetService.js +103 -4
- package/dist/modules/modelPresets/ModelPresetService.js.map +1 -1
- package/dist/modules/review/IterativeReviewService.d.ts +8 -4
- package/dist/modules/review/IterativeReviewService.d.ts.map +1 -1
- package/dist/modules/review/IterativeReviewService.js +4 -13
- package/dist/modules/review/IterativeReviewService.js.map +1 -1
- package/package.json +11 -11
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import { allPullRequests
|
|
2
|
-
import { companionFor } from '@cat-factory/agents';
|
|
3
|
-
import { assertPipelineLaunchable } from '../pipelines/pipelineShape.js';
|
|
1
|
+
import { allPullRequests } from '@cat-factory/contracts';
|
|
4
2
|
import { producerWasSkipped, shouldRunGatedStep } from './stepGating.logic.js';
|
|
5
3
|
import { resolveIndividualVendors, } from './individualVendors.logic.js';
|
|
6
|
-
import { assertFound,
|
|
7
|
-
import {
|
|
8
|
-
import { isTesterKind } from './ci.logic.js';
|
|
9
|
-
import { DEFAULT_FOLLOW_UP_MAX_LOOPS, FOLLOW_UP_PRODUCER_KIND } from './followUp.logic.js';
|
|
4
|
+
import { assertFound, RunContendedError, ReviewContendedError, } from '@cat-factory/kernel';
|
|
5
|
+
import { noopLogger, runBestEffort } from '@cat-factory/kernel';
|
|
10
6
|
import { AgentContextBuilder } from './AgentContextBuilder.js';
|
|
11
7
|
import { CompanionController } from './CompanionController.js';
|
|
12
8
|
import { StepGraph } from './StepGraph.js';
|
|
13
|
-
import { claimLiveRunOrConflict, handOffLiveRun } from './runStart.js';
|
|
14
9
|
import { RunStateMachine } from './RunStateMachine.js';
|
|
15
10
|
import { RunDispatcher } from './RunDispatcher.js';
|
|
16
11
|
import { RunAdmission } from './RunAdmission.js';
|
|
17
12
|
import { StepDecisionController } from './StepDecisionController.js';
|
|
13
|
+
import { buildRunActionControllers } from './run-action-controllers.js';
|
|
18
14
|
import { inferTechnicalLabel } from './technical.logic.js';
|
|
19
15
|
import { MergeResolver } from './MergeResolver.js';
|
|
20
16
|
import { PostMergeBoardController } from './PostMergeBoardController.js';
|
|
@@ -27,16 +23,13 @@ import { PrReviewController } from './PrReviewController.js';
|
|
|
27
23
|
import { BrainstormActions, ClarityReviewActions, RequirementReviewActions, } from './gate-window-facades.js';
|
|
28
24
|
import { TesterController } from './TesterController.js';
|
|
29
25
|
import { RalphController } from './RalphController.js';
|
|
30
|
-
import { isRalphKind, resolveRalphConfig, seedRalphState } from './ralph.logic.js';
|
|
31
26
|
import { HumanTestController } from './HumanTestController.js';
|
|
32
27
|
import { VisualConfirmationController } from './VisualConfirmationController.js';
|
|
33
28
|
import { InitiativeInterviewController } from './InitiativeInterviewController.js';
|
|
34
29
|
import { DocInterviewController } from './DocInterviewController.js';
|
|
35
30
|
import { isReentrantDecisionResume } from './reentrancy.logic.js';
|
|
36
31
|
import { RunMergePolicy } from './RunMergePolicy.js';
|
|
37
|
-
import { descendantIds } from '../board/board.logic.js';
|
|
38
32
|
import { requireWorkspace } from '@cat-factory/kernel';
|
|
39
|
-
import { buildResumedInstance, planResumedSteps, planRestartFromStep } from './retry.logic.js';
|
|
40
33
|
import { PrVerificationReportController } from './PrVerificationReportController.js';
|
|
41
34
|
/**
|
|
42
35
|
* The execution engine. It orchestrates a pipeline of agent-performed steps and
|
|
@@ -153,7 +146,8 @@ export class ExecutionService {
|
|
|
153
146
|
* their drops without a null-check. `noopLogger` when a facade wired none.
|
|
154
147
|
*/
|
|
155
148
|
log;
|
|
156
|
-
subscriptionActivations
|
|
149
|
+
// No `subscriptionActivations` field: its only reader is the run-lifecycle surface, which now
|
|
150
|
+
// takes the repository straight off the deps object (see {@link RunLifecycleController}).
|
|
157
151
|
pokeInitiativeLoop;
|
|
158
152
|
resolveWorkspaceModelDefault;
|
|
159
153
|
/**
|
|
@@ -166,6 +160,13 @@ export class ExecutionService {
|
|
|
166
160
|
runDispatcher;
|
|
167
161
|
/** The human decision surface on a parked run (approve / reject / merge / …). */
|
|
168
162
|
stepDecisions;
|
|
163
|
+
/**
|
|
164
|
+
* The surfaces a HUMAN drives on a whole run: its lifecycle (launch / re-launch / resume / end,
|
|
165
|
+
* which share the claim-then-hand-off order) and the three-way iteration-cap resolution both
|
|
166
|
+
* automatic-rework gates park for. Extracted so this service keeps the per-step machine; the
|
|
167
|
+
* public methods that reach them are thin pass-throughs.
|
|
168
|
+
*/
|
|
169
|
+
runActions;
|
|
169
170
|
/** Maintains the run's verification report on its PR (a hook on step settlement). */
|
|
170
171
|
prVerificationReport;
|
|
171
172
|
constructor(dependencies) {
|
|
@@ -263,7 +264,7 @@ export class ExecutionService {
|
|
|
263
264
|
clock,
|
|
264
265
|
clockNow: () => this.clock.now(),
|
|
265
266
|
resolveRiskPolicy: (ws, block) => this.resolveRiskPolicy(ws, block),
|
|
266
|
-
dispatchIterationCap: (ws, blockId, choice, handlers) => this.dispatchIterationCap(ws, blockId, choice, handlers),
|
|
267
|
+
dispatchIterationCap: (ws, blockId, choice, handlers) => this.runActions.iterationCap.dispatchIterationCap(ws, blockId, choice, handlers),
|
|
267
268
|
testerQualityReviewer,
|
|
268
269
|
environmentProvisioning,
|
|
269
270
|
environmentTeardown,
|
|
@@ -320,7 +321,6 @@ export class ExecutionService {
|
|
|
320
321
|
this.notifications = notificationService;
|
|
321
322
|
this.issueWriteback = issueWriteback;
|
|
322
323
|
this.log = logger ?? noopLogger;
|
|
323
|
-
this.subscriptionActivations = subscriptionActivationRepository;
|
|
324
324
|
this.pokeInitiativeLoop = pokeInitiativeLoop;
|
|
325
325
|
this.resolveWorkspaceModelDefault = resolveWorkspaceModelDefault;
|
|
326
326
|
this.stepDecisions = new StepDecisionController({
|
|
@@ -339,6 +339,28 @@ export class ExecutionService {
|
|
|
339
339
|
failRun: (ws, id, message, kind, detail, reason) => this.failRun(ws, id, message, kind, detail, reason),
|
|
340
340
|
finalizeMerge: (ws, blockId) => this.finalizeMerge(ws, blockId),
|
|
341
341
|
});
|
|
342
|
+
// The two surfaces a HUMAN drives on a whole run (its lifecycle + the iteration-cap
|
|
343
|
+
// resolution), built as one pair by the sibling factory: the cap gate's `stop-reset` branch is
|
|
344
|
+
// a run cancel, so it is bound to the lifecycle controller there rather than back through this
|
|
345
|
+
// instance. The engine methods they reach into are passed BOUND, as with the gate windows.
|
|
346
|
+
this.runActions = buildRunActionControllers({
|
|
347
|
+
admission: this.admission,
|
|
348
|
+
blockRepository: this.blockRepository,
|
|
349
|
+
clock: this.clock,
|
|
350
|
+
contextBuilder: this.contextBuilder,
|
|
351
|
+
events: this.events,
|
|
352
|
+
executionRepository: this.executionRepository,
|
|
353
|
+
idGenerator: this.idGenerator,
|
|
354
|
+
pipelineRepository: this.pipelineRepository,
|
|
355
|
+
runStateMachine: this.runStateMachine,
|
|
356
|
+
stepGraph: this.stepGraph,
|
|
357
|
+
workRunner: this.workRunner,
|
|
358
|
+
subscriptionActivations: subscriptionActivationRepository,
|
|
359
|
+
requireWorkspace: (ws) => this.requireWorkspace(ws),
|
|
360
|
+
requireBlock: (ws, id) => this.requireBlock(ws, id),
|
|
361
|
+
failRun: (ws, id, message, kind, detail, reason) => this.failRun(ws, id, message, kind, detail, reason),
|
|
362
|
+
inferBlockTechnical: (ws, block, producer, companionStep) => this.inferBlockTechnical(ws, block, producer, companionStep),
|
|
363
|
+
});
|
|
342
364
|
}
|
|
343
365
|
/**
|
|
344
366
|
* Re-compose and re-publish a run's verification report OUT OF BAND, after the run itself has
|
|
@@ -504,198 +526,6 @@ export class ExecutionService {
|
|
|
504
526
|
const resolveDefault = this.resolveWorkspaceModelDefault;
|
|
505
527
|
return resolveIndividualVendors(blockModelId, agentKinds, resolveDefault ? (kind) => resolveDefault(workspaceId, kind, modelPresetId) : undefined, hasPersonalSubscription);
|
|
506
528
|
}
|
|
507
|
-
/** Start a pipeline against a block, replacing any prior run on it. */
|
|
508
|
-
async start(workspaceId, blockId, pipelineId, options = {}) {
|
|
509
|
-
const { initiatedBy, activate, origin = 'manual', intakeOrigin, gatesOverride } = options;
|
|
510
|
-
await this.requireWorkspace(workspaceId);
|
|
511
|
-
const block = await this.requireBlock(workspaceId, blockId);
|
|
512
|
-
const pipeline = assertFound(await this.pipelineRepository.get(workspaceId, pipelineId), 'Pipeline', pipelineId);
|
|
513
|
-
// Launch-constraint gate (start-only, NOT part of the shared retry re-validation): reject a
|
|
514
|
-
// manual start of a recurring-only pipeline (or a scheduled fire of a one-off-only one), and
|
|
515
|
-
// a bug-intake pipeline that isn't recurring. Before any side effects.
|
|
516
|
-
assertPipelineLaunchable(pipeline.agentKinds, pipeline.availability, origin, pipeline.enabled);
|
|
517
|
-
// Per-run gate override must be parallel to the pipeline's steps (one boolean per step,
|
|
518
|
-
// original-index-aligned like `pipeline.gates`). A mismatch means a preset's review mapping
|
|
519
|
-
// is out of step with the pipeline it targets — reject up front, before any side effects.
|
|
520
|
-
if (gatesOverride && gatesOverride.length !== pipeline.agentKinds.length) {
|
|
521
|
-
throw new ValidationError(`Gate override has ${gatesOverride.length} entr${gatesOverride.length === 1 ? 'y' : 'ies'} but pipeline '${pipeline.id}' has ${pipeline.agentKinds.length} step(s).`);
|
|
522
|
-
}
|
|
523
|
-
// Shared config/resource preconditions (pipeline shape, frame type, tester infra, binary
|
|
524
|
-
// storage, agent backend, provider/preset satisfiability, budget) — the SAME gate a retry
|
|
525
|
-
// runs, so the two can't drift. See assertRunnable.
|
|
526
|
-
await this.admission.assertRunnable(workspaceId, block, pipeline, initiatedBy);
|
|
527
|
-
// A Ralph-loop step needs a programmatic completion command (its exit condition); refuse to
|
|
528
|
-
// start a misconfigured run rather than dispatch a validation-less coding pass that never
|
|
529
|
-
// gates. The command is a per-task agent-config value (the SPA also requires it at creation).
|
|
530
|
-
if (pipeline.agentKinds.some(isRalphKind) &&
|
|
531
|
-
!resolveRalphConfig(block.agentConfig).validationCommand) {
|
|
532
|
-
throw new ValidationError('A Ralph loop task needs a validation command (its completion criterion) before it can ' +
|
|
533
|
-
'start. Set one in the task configuration.');
|
|
534
|
-
}
|
|
535
|
-
// START-ONLY gates below: a retry REPLACES the failed run rather than adding a new one, so
|
|
536
|
-
// the concurrency limit doesn't apply to it, and a re-drive of an already-started task isn't
|
|
537
|
-
// re-gated on its dependencies.
|
|
538
|
-
// Enforce the workspace's per-service running-task limit (off by default) — a clear,
|
|
539
|
-
// actionable error before any side effects, so the human knows why the start was refused.
|
|
540
|
-
await this.admission.assertWithinTaskLimit(workspaceId, block);
|
|
541
|
-
// Hard dependency gate: a task cannot start while any block it `dependsOn` is unfinished
|
|
542
|
-
// (not yet `done`/merged). Enforced server-side so it holds for manual starts, recurring
|
|
543
|
-
// fires, auto-start propagation and direct API calls alike — the frontend's runnable
|
|
544
|
-
// check is only a hint. Before any side effects so nothing is torn down on a refusal.
|
|
545
|
-
await this.admission.assertDependenciesMet(workspaceId, block);
|
|
546
|
-
// Mint the activation next: if the credential can't be unlocked, fail before
|
|
547
|
-
// tearing down the block's prior run or creating a new one.
|
|
548
|
-
const executionId = this.idGenerator.next('exec');
|
|
549
|
-
await activate?.(executionId);
|
|
550
|
-
// Read the block's prior run once: a manual re-start of an already-running block REPLACES
|
|
551
|
-
// it (the board offers "start" on a live block), so we pass its id to `insertLive` as the
|
|
552
|
-
// `replaceId` it supersedes atomically. A genuinely-CONCURRENT second start reads the SAME
|
|
553
|
-
// prior (or none), so only one insert wins and the other is rejected 409 — the loser's
|
|
554
|
-
// `replaceId` deletes only what it read, never the winner's fresh row (see insertLive).
|
|
555
|
-
const prior = await this.executionRepository.getByBlock(workspaceId, blockId);
|
|
556
|
-
// Replacing the block's prior run: clear its per-run activation now (it never reaches
|
|
557
|
-
// the terminal cleanup in emitInstance when it's still running), so a replaced run's
|
|
558
|
-
// system-encrypted token copy doesn't linger to its TTL. Keyed by the OLD run id, so
|
|
559
|
-
// the activation just minted for the new run is untouched.
|
|
560
|
-
if (this.subscriptionActivations && prior && prior.id !== executionId) {
|
|
561
|
-
// Best-effort + idempotent, mirroring the terminal cleanup in RunStateMachine.emit: a
|
|
562
|
-
// failure here must never derail the start. In mothership mode this repo is remote and
|
|
563
|
-
// `deleteByExecution` is not yet allow-listed (it throws `unknown_method`), so an
|
|
564
|
-
// unguarded call would otherwise break re-running any block; the TTL sweep reclaims the
|
|
565
|
-
// stale activation row as the backstop.
|
|
566
|
-
try {
|
|
567
|
-
await this.subscriptionActivations.deleteByExecution(prior.id);
|
|
568
|
-
}
|
|
569
|
-
catch {
|
|
570
|
-
// Swallow — see above.
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
// NB: do NOT `deleteByBlock` here — `claimLiveRunOrConflict` (below) atomically clears the
|
|
574
|
-
// block's terminal rows AND the `prior` run it replaces, then inserts the new live run, so a
|
|
575
|
-
// concurrent double-start is rejected by the live-run index instead of both wiping each
|
|
576
|
-
// other's row (see insertLive).
|
|
577
|
-
// Build the run only from the ENABLED steps. A step the pipeline marked
|
|
578
|
-
// `enabled[i] === false` is kept in the saved pipeline (so it can be toggled back
|
|
579
|
-
// on later) but skipped here entirely. Gates/thresholds are read by the kind's
|
|
580
|
-
// ORIGINAL index `i`, so they stay aligned to the kind even when earlier steps are
|
|
581
|
-
// skipped; the first SURVIVING step is the one that starts working.
|
|
582
|
-
const steps = pipeline.agentKinds
|
|
583
|
-
.map((kind, i) => ({ kind, i }))
|
|
584
|
-
.filter(({ i }) => pipeline.enabled?.[i] !== false)
|
|
585
|
-
.map(({ kind, i }, position) => {
|
|
586
|
-
const companionDef = companionFor(kind);
|
|
587
|
-
return {
|
|
588
|
-
agentKind: kind,
|
|
589
|
-
state: position === 0 ? 'working' : 'pending',
|
|
590
|
-
progress: 0,
|
|
591
|
-
decision: null,
|
|
592
|
-
// A gated step pauses for human approval once its proposal is ready (see
|
|
593
|
-
// recordStepResult). A per-run override (the initiative-preset seam) wins over the
|
|
594
|
-
// pipeline's own gate for this step; else the pipeline definition at run start. Both
|
|
595
|
-
// read by the step's ORIGINAL index `i`, so they stay aligned to the kind even when
|
|
596
|
-
// earlier steps are disabled.
|
|
597
|
-
requiresApproval: gatesOverride?.[i] ?? pipeline.gates?.[i] ?? false,
|
|
598
|
-
approval: null,
|
|
599
|
-
// A consensus-enabled step runs through the multi-model mechanism (the consensus
|
|
600
|
-
// executor reads this off the context). Copied from the pipeline at run start.
|
|
601
|
-
...(pipeline.consensus?.[i] ? { consensus: pipeline.consensus[i] } : {}),
|
|
602
|
-
// Estimate gating: when set+enabled the step is skipped at runtime unless the
|
|
603
|
-
// block estimate (written by an earlier task-estimator step) meets the threshold.
|
|
604
|
-
...(pipeline.gating?.[i] ? { gating: pipeline.gating[i] } : {}),
|
|
605
|
-
// The extensible per-step options bag (the new home for per-step parameters — see
|
|
606
|
-
// stepOptionsSchema). Copied from the pipeline at run start, keyed by the step's
|
|
607
|
-
// ORIGINAL index `i`, so it stays aligned to the kind even when earlier steps are
|
|
608
|
-
// disabled. Today it carries the requirements-review `autoRecommend` toggle.
|
|
609
|
-
...(pipeline.stepOptions?.[i] ? { stepOptions: pipeline.stepOptions[i] } : {}),
|
|
610
|
-
// A companion step carries its quality bar + rework budget, seeded from the
|
|
611
|
-
// pipeline's per-step threshold (else the companion's default).
|
|
612
|
-
...(companionDef
|
|
613
|
-
? {
|
|
614
|
-
companion: {
|
|
615
|
-
threshold: pipeline.thresholds?.[i] ?? companionDef.defaultThreshold,
|
|
616
|
-
maxAttempts: DEFAULT_COMPANION_MAX_ATTEMPTS,
|
|
617
|
-
attempts: 0,
|
|
618
|
-
verdicts: [],
|
|
619
|
-
},
|
|
620
|
-
}
|
|
621
|
-
: {}),
|
|
622
|
-
// The Follow-up companion is on by default for a `coder` step; the pipeline's
|
|
623
|
-
// per-step `followUps[i] === false` toggle disables it. Seeded empty here; the
|
|
624
|
-
// harness streams items in as the Coder surfaces them (see pollAgentJob).
|
|
625
|
-
...(kind === FOLLOW_UP_PRODUCER_KIND && pipeline.followUps?.[i] !== false
|
|
626
|
-
? {
|
|
627
|
-
followUps: {
|
|
628
|
-
enabled: true,
|
|
629
|
-
items: [],
|
|
630
|
-
loops: 0,
|
|
631
|
-
maxLoops: DEFAULT_FOLLOW_UP_MAX_LOOPS,
|
|
632
|
-
},
|
|
633
|
-
}
|
|
634
|
-
: {}),
|
|
635
|
-
// The test quality-control companion is on by default for a Tester step; the
|
|
636
|
-
// pipeline's per-step `testerQuality[i].enabled === false` disables it. `maxAttempts`
|
|
637
|
-
// is seeded with the default ceiling here and refreshed from the task's resolved
|
|
638
|
-
// merge preset on the first report (TesterController). Optional estimate gating is
|
|
639
|
-
// carried through so it can be evaluated against the block estimate at gate time.
|
|
640
|
-
...(isTesterKind(kind) && pipeline.testerQuality?.[i]?.enabled !== false
|
|
641
|
-
? {
|
|
642
|
-
testerQuality: {
|
|
643
|
-
enabled: true,
|
|
644
|
-
attempts: 0,
|
|
645
|
-
maxAttempts: DEFAULT_RISK_POLICY.maxTesterQualityIterations,
|
|
646
|
-
verdicts: [],
|
|
647
|
-
...(pipeline.testerQuality?.[i]?.gating
|
|
648
|
-
? { gating: pipeline.testerQuality[i].gating }
|
|
649
|
-
: {}),
|
|
650
|
-
},
|
|
651
|
-
}
|
|
652
|
-
: {}),
|
|
653
|
-
// A `ralph` step carries its persistent-loop state — the iteration count, the budget,
|
|
654
|
-
// and the programmatic completion command — seeded from the block's per-task agent
|
|
655
|
-
// config. Riding the persisted step is what lets a mid-loop run survive a restart
|
|
656
|
-
// (both durable drivers + sweepers re-drive from it). See ralph.logic.ts.
|
|
657
|
-
...(isRalphKind(kind)
|
|
658
|
-
? { ralph: seedRalphState(resolveRalphConfig(block.agentConfig)) }
|
|
659
|
-
: {}),
|
|
660
|
-
};
|
|
661
|
-
});
|
|
662
|
-
if (steps.length === 0) {
|
|
663
|
-
throw new ValidationError('Pipeline has no enabled steps to run.');
|
|
664
|
-
}
|
|
665
|
-
// For a visual (UI-test) pipeline on a frontend frame, resolve its backend bindings ONCE at
|
|
666
|
-
// start and stamp both the resolved bindings and the non-fatal advisories (duplicate env vars,
|
|
667
|
-
// or a partial-live set of bound services) on the run. The bindings are a frozen snapshot so
|
|
668
|
-
// the SPA's run/step detail projects what the run ACTUALLY drove against (truthful after the
|
|
669
|
-
// envs are torn down). Only paid for a visual pipeline — the same condition the tester infra
|
|
670
|
-
// gate keys off — so a plain backend run does no extra env read. Absent → no notes/bindings.
|
|
671
|
-
const frontendRun = pipelineHasVisualStep({ agentKinds: pipeline.agentKinds })
|
|
672
|
-
? await this.contextBuilder.resolveFrontendRunInfo(workspaceId, block)
|
|
673
|
-
: undefined;
|
|
674
|
-
const instance = {
|
|
675
|
-
id: executionId,
|
|
676
|
-
blockId,
|
|
677
|
-
pipelineId: pipeline.id,
|
|
678
|
-
pipelineName: pipeline.name,
|
|
679
|
-
steps,
|
|
680
|
-
currentStep: 0,
|
|
681
|
-
status: 'running',
|
|
682
|
-
initiatedBy: initiatedBy ?? null,
|
|
683
|
-
// Only a headless start carries an explicit intake origin; `ui` is the read-time
|
|
684
|
-
// default, so an ordinary board/schedule start stores nothing extra.
|
|
685
|
-
...(intakeOrigin != null ? { intakeOrigin } : {}),
|
|
686
|
-
createdAt: this.clock.now(),
|
|
687
|
-
...(frontendRun?.notes.length ? { notes: frontendRun.notes } : {}),
|
|
688
|
-
...(frontendRun?.bindings.length ? { frontendBindings: frontendRun.bindings } : {}),
|
|
689
|
-
};
|
|
690
|
-
await claimLiveRunOrConflict(this.runStartDeps, workspaceId, instance, prior?.id);
|
|
691
|
-
await this.blockRepository.update(workspaceId, blockId, {
|
|
692
|
-
status: 'in_progress',
|
|
693
|
-
progress: 0,
|
|
694
|
-
executionId: instance.id,
|
|
695
|
-
});
|
|
696
|
-
await handOffLiveRun(this.runStartDeps, workspaceId, instance, block);
|
|
697
|
-
return instance;
|
|
698
|
-
}
|
|
699
529
|
/**
|
|
700
530
|
* Advance a single run by exactly one step and report what happened. This is
|
|
701
531
|
* the durable driver's entry point: it reloads the run from storage (so it is
|
|
@@ -944,126 +774,9 @@ export class ExecutionService {
|
|
|
944
774
|
? this.architectureBrainstormKind
|
|
945
775
|
: this.requirementsBrainstormKind;
|
|
946
776
|
}
|
|
947
|
-
/**
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
* keeping whatever reference artifact each gate persists (the requirements doc on its
|
|
951
|
-
* own table; a companion's producer output on its branch). Shared by the requirements
|
|
952
|
-
* gate (`requirementsReview.resolveExceeded`, via {@link ReviewGateController}) and the
|
|
953
|
-
* companion gate ({@link resolveCompanionExceeded}) so the three-way choice lives in one place.
|
|
954
|
-
*/
|
|
955
|
-
async dispatchIterationCap(workspaceId, blockId, choice, handlers) {
|
|
956
|
-
if (choice === 'extra-round') {
|
|
957
|
-
await handlers.extraRound();
|
|
958
|
-
}
|
|
959
|
-
else if (choice === 'proceed') {
|
|
960
|
-
await handlers.proceed();
|
|
961
|
-
}
|
|
962
|
-
else {
|
|
963
|
-
// stop-reset: tear down the run + reset the block to phase zero (editable).
|
|
964
|
-
await this.cancel(workspaceId, blockId);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
/**
|
|
968
|
-
* Resolve a companion step parked at its automatic-rework cap (`companion.exceeded`):
|
|
969
|
-
* grant one more round, proceed accepting the producer's current output, or stop the
|
|
970
|
-
* task and reset it to phase zero. The companion mirror of the requirements
|
|
971
|
-
* iteration-cap resolution (`requirementsReview.resolveExceeded`), sharing the iteration-cap dispatch + the
|
|
972
|
-
* gate-resume plumbing. Idempotent — an already-resolved gate returns the instance
|
|
973
|
-
* unchanged. Scoped by execution + approval id (the execution controller surface),
|
|
974
|
-
* since a companion gate is not block-addressed like the requirements window.
|
|
975
|
-
*/
|
|
976
|
-
async resolveCompanionExceeded(workspaceId, executionId, approvalId, choice) {
|
|
977
|
-
await this.requireWorkspace(workspaceId);
|
|
978
|
-
// Optimistic-concurrency human-action write (race-audit 2.2 controller-half): both non-cancel
|
|
979
|
-
// branches persist under `mutateInstance` (load fresh → re-find the gate → mutate → CAS), so a
|
|
980
|
-
// concurrent driver poll — or a `stopRun`/`cancel` racing this resolve — can't be clobbered by a
|
|
981
|
-
// blind full-row upsert, and a cancelled run is never resurrected. The pure in-memory mutation
|
|
982
|
-
// runs inside the CAS; the non-idempotent side effects (block writes, `technical` inference,
|
|
983
|
-
// driver signal, emit) run once after, on the winning snapshot — the same pure/side-effect split
|
|
984
|
-
// `approveStep` and the review gate-resume use. The validation snapshot below gives a fast
|
|
985
|
-
// 404/409 (and the idempotent already-resolved early return).
|
|
986
|
-
const snapshot = assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
|
|
987
|
-
const snapStep = snapshot.steps.find((s) => s.approval?.id === approvalId);
|
|
988
|
-
if (!snapStep || !snapStep.approval)
|
|
989
|
-
throw new NotFoundError('Approval', approvalId);
|
|
990
|
-
if (!snapStep.companion?.exceeded) {
|
|
991
|
-
throw new ConflictError(`Approval '${approvalId}' is not a companion iteration-cap gate`);
|
|
992
|
-
}
|
|
993
|
-
if (snapStep.approval.status === 'approved')
|
|
994
|
-
return snapshot;
|
|
995
|
-
// The state the caller sees: the winning post-mutation snapshot for extra-round/proceed, or
|
|
996
|
-
// the pre-cancel snapshot for stop-reset (the run row is deleted, so there's nothing to re-read).
|
|
997
|
-
let result = snapshot;
|
|
998
|
-
await this.dispatchIterationCap(workspaceId, snapshot.blockId, choice, {
|
|
999
|
-
// Grant one more automatic rework: raise the budget by one, clear the cap flag, then loop
|
|
1000
|
-
// the producer back through the companion to re-grade (`loopCompanionProducer` re-arms the
|
|
1001
|
-
// run `running`). The last verdict's feedback drives the rework.
|
|
1002
|
-
extraRound: async () => {
|
|
1003
|
-
let signalId;
|
|
1004
|
-
const persisted = await this.runStateMachine.mutateInstance(workspaceId, executionId, (inst) => {
|
|
1005
|
-
const i = inst.steps.findIndex((s) => s.approval?.id === approvalId);
|
|
1006
|
-
const s = inst.steps[i];
|
|
1007
|
-
if (!s?.companion || !s.approval)
|
|
1008
|
-
throw new NotFoundError('Approval', approvalId);
|
|
1009
|
-
// Another writer already resolved this gate: no-op (idempotent) and skip the signal.
|
|
1010
|
-
if (s.approval.status === 'approved') {
|
|
1011
|
-
signalId = undefined;
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
s.companion.maxAttempts += 1;
|
|
1015
|
-
s.companion.exceeded = undefined;
|
|
1016
|
-
const producer = inst.steps[this.stepGraph.companionProducerIndex(inst, i)];
|
|
1017
|
-
// Capture the approval id BEFORE `loopCompanionProducer`: it resets the companion
|
|
1018
|
-
// step for re-run (`resetStepForRerun`), which NULLS `s.approval`, so reading
|
|
1019
|
-
// `s.approval.id` after would throw. The signal targets the gate's original approval.
|
|
1020
|
-
signalId = s.approval.id;
|
|
1021
|
-
this.stepGraph.loopCompanionProducer(inst, i, {
|
|
1022
|
-
previousProposal: producer?.output ?? '',
|
|
1023
|
-
feedback: s.companion.verdicts.at(-1)?.feedback ?? '',
|
|
1024
|
-
});
|
|
1025
|
-
});
|
|
1026
|
-
result = persisted;
|
|
1027
|
-
if (!signalId)
|
|
1028
|
-
return;
|
|
1029
|
-
await this.runStateMachine.updateBlockProgress(workspaceId, persisted, 'in_progress');
|
|
1030
|
-
await this.workRunner.signalDecision(workspaceId, persisted.id, signalId, 'extra-round');
|
|
1031
|
-
await this.runStateMachine.emitInstance(workspaceId, persisted);
|
|
1032
|
-
},
|
|
1033
|
-
// Proceed: accept the producer's current output and advance past the gate.
|
|
1034
|
-
proceed: async () => {
|
|
1035
|
-
let stepIndex = -1;
|
|
1036
|
-
const persisted = await this.runStateMachine.mutateInstance(workspaceId, executionId, (inst) => {
|
|
1037
|
-
stepIndex = inst.steps.findIndex((s) => s.approval?.id === approvalId);
|
|
1038
|
-
const s = inst.steps[stepIndex];
|
|
1039
|
-
if (!s?.companion || !s.approval)
|
|
1040
|
-
throw new NotFoundError('Approval', approvalId);
|
|
1041
|
-
if (s.approval.status === 'approved') {
|
|
1042
|
-
stepIndex = -1;
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
s.companion.exceeded = undefined;
|
|
1046
|
-
s.approval.status = 'approved';
|
|
1047
|
-
this.runStateMachine.advanceRunPastGate(inst, stepIndex);
|
|
1048
|
-
});
|
|
1049
|
-
result = persisted;
|
|
1050
|
-
if (stepIndex === -1)
|
|
1051
|
-
return;
|
|
1052
|
-
// The spec-companion never reached its automatic PASS branch, but both signals are
|
|
1053
|
-
// persisted (the producer's `noBusinessSpecs` + this step's `technicalCorroborated`),
|
|
1054
|
-
// so infer the block's `technical` label here too — best-effort, human-authority
|
|
1055
|
-
// preserved — before settling the advance.
|
|
1056
|
-
const step = persisted.steps[stepIndex];
|
|
1057
|
-
if (step.agentKind === 'spec-companion') {
|
|
1058
|
-
const producer = persisted.steps[this.stepGraph.companionProducerIndex(persisted, stepIndex)];
|
|
1059
|
-
const block = await this.blockRepository.get(workspaceId, persisted.blockId);
|
|
1060
|
-
if (producer && block)
|
|
1061
|
-
await this.inferBlockTechnical(workspaceId, block, producer, step);
|
|
1062
|
-
}
|
|
1063
|
-
await this.runStateMachine.settleAdvancedGate(workspaceId, persisted, stepIndex);
|
|
1064
|
-
},
|
|
1065
|
-
});
|
|
1066
|
-
return result;
|
|
777
|
+
/** @see IterationCapController.resolveCompanionExceeded */
|
|
778
|
+
resolveCompanionExceeded(workspaceId, executionId, approvalId, choice) {
|
|
779
|
+
return this.runActions.iterationCap.resolveCompanionExceeded(workspaceId, executionId, approvalId, choice);
|
|
1067
780
|
}
|
|
1068
781
|
// The clarity / human-testing / visual-confirmation gate-window actions now live on the
|
|
1069
782
|
// per-feature sub-facades (`clarityReview` / `humanTest` / `visualConfirm`); see the getters
|
|
@@ -1237,269 +950,37 @@ export class ExecutionService {
|
|
|
1237
950
|
failRun(workspaceId, executionId, message, kind = 'agent', detail = null, reason = null) {
|
|
1238
951
|
return this.runStateMachine.failRun(workspaceId, executionId, message, kind, detail, reason);
|
|
1239
952
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
/**
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
// STORED steps (what the retry actually re-drives), not the current pipeline definition, so
|
|
1272
|
-
// an out-of-band pipeline edit can't skew the gate and a deleted pipeline needs no special
|
|
1273
|
-
// case. Before any side effects.
|
|
1274
|
-
await this.admission.assertRunnable(workspaceId, block, this.admission.runnableShapeOf(previous.steps), initiatedBy ?? previous.initiatedBy);
|
|
1275
|
-
const { steps, currentStep } = planResumedSteps(previous);
|
|
1276
|
-
// Mint the activation before replacing the failed run, so a bad password aborts
|
|
1277
|
-
// the retry without losing the retryable terminal run.
|
|
1278
|
-
const newId = this.idGenerator.next('exec');
|
|
1279
|
-
const replaceId = previous.id;
|
|
1280
|
-
await activate?.(newId);
|
|
1281
|
-
// Replace the terminal failed run for this block with the resumed one (single run per
|
|
1282
|
-
// block, matching the board's by-block projection). This mints a FRESH run id; the
|
|
1283
|
-
// atomic `claimLiveRunOrConflict` below replaces `previous` (via `replaceId`) and clears
|
|
1284
|
-
// any terminal rows in the SAME transaction, so a concurrent double-retry is serialised by
|
|
1285
|
-
// the live-run index (the loser gets a 409) instead of both deleting-then-inserting.
|
|
1286
|
-
const instance = buildResumedInstance({
|
|
1287
|
-
previous,
|
|
1288
|
-
id: newId,
|
|
1289
|
-
plan: { steps, currentStep },
|
|
1290
|
-
initiatedBy,
|
|
1291
|
-
now: this.clock.now(),
|
|
1292
|
-
});
|
|
1293
|
-
await claimLiveRunOrConflict(this.runStartDeps, workspaceId, instance, replaceId);
|
|
1294
|
-
const done = steps.filter((s) => s.state === 'done').length;
|
|
1295
|
-
await this.blockRepository.update(workspaceId, previous.blockId, {
|
|
1296
|
-
status: 'in_progress',
|
|
1297
|
-
progress: steps.length > 0 ? done / steps.length : 0,
|
|
1298
|
-
executionId: instance.id,
|
|
1299
|
-
});
|
|
1300
|
-
await handOffLiveRun(this.runStartDeps, workspaceId, instance, block);
|
|
1301
|
-
return instance;
|
|
1302
|
-
}
|
|
1303
|
-
/**
|
|
1304
|
-
* Restart a run from a human-chosen step: re-run from `fromStepIndex` onward,
|
|
1305
|
-
* regardless of how far the run had progressed (a `done`, `failed`, `blocked`,
|
|
1306
|
-
* `paused` or still-`running` run are all valid sources). Unlike {@link retry}
|
|
1307
|
-
* (which resumes at the first FAILURE) this rewinds to an arbitrary step the user
|
|
1308
|
-
* picked — so it can re-run steps that already completed.
|
|
1309
|
-
*
|
|
1310
|
-
* What is preserved vs reset:
|
|
1311
|
-
* - Steps BEFORE `fromStepIndex` keep their `output`/approval/timing untouched, so
|
|
1312
|
-
* the engine still hands the restarted step its predecessors' work as
|
|
1313
|
-
* `priorOutputs` (and their resolved `decisions`) — a useful handoff.
|
|
1314
|
-
* - The chosen step and every later one are reset to a clean, re-runnable state,
|
|
1315
|
-
* dropping each step's iteration counters (companion attempts, gate/test attempts,
|
|
1316
|
-
* eviction recoveries) so the restart starts those loops from zero.
|
|
1317
|
-
* - A block's incorporated requirements are NOT touched: they live on the
|
|
1318
|
-
* requirement-review record, so a restarted spec-writer/coder still receives the
|
|
1319
|
-
* incorporated document (or the base description when none was generated). When the
|
|
1320
|
-
* chosen step is the `requirements-review` gate ITSELF, re-running it mints a fresh
|
|
1321
|
-
* iteration-1 review (the reviewer's `review()` replaces the prior one), which is
|
|
1322
|
-
* exactly the "reset the iterations counter from this step" semantics.
|
|
1323
|
-
*
|
|
1324
|
-
* Like {@link retry} a fresh instance id is minted (the durable runner addresses one
|
|
1325
|
-
* driver per execution id). Any still-live driver/container for the run being
|
|
1326
|
-
* replaced is torn down first, so restarting a RUNNING run never orphans a container
|
|
1327
|
-
* or a parked Workflows instance.
|
|
1328
|
-
*/
|
|
1329
|
-
async restartFromStep(workspaceId, executionId, fromStepIndex,
|
|
1330
|
-
/** The restarting user (their personal subscription is used for individual-usage
|
|
1331
|
-
* models). Falls back to the original initiator when omitted. */
|
|
1332
|
-
initiatedBy,
|
|
1333
|
-
/** Mint the per-run personal-credential activation (see {@link start}). */
|
|
1334
|
-
activate) {
|
|
1335
|
-
await this.requireWorkspace(workspaceId);
|
|
1336
|
-
const previous = assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
|
|
1337
|
-
const block = await this.requireBlock(workspaceId, previous.blockId);
|
|
1338
|
-
if (!Number.isInteger(fromStepIndex) ||
|
|
1339
|
-
fromStepIndex < 0 ||
|
|
1340
|
-
fromStepIndex >= previous.steps.length) {
|
|
1341
|
-
throw new ValidationError(`Step ${fromStepIndex} is out of range for this run (it has ${previous.steps.length} step(s)).`);
|
|
1342
|
-
}
|
|
1343
|
-
// Run the SAME config/resource preconditions start()/retry() do, over the STORED steps this
|
|
1344
|
-
// restart re-drives (frame type, tester infra, binary storage, agent backend, provider/preset
|
|
1345
|
-
// satisfiability, budget). A restart re-dispatches provider-bearing steps just like a retry,
|
|
1346
|
-
// so it must be gated identically — otherwise a run whose preset can't run every step (e.g. a
|
|
1347
|
-
// subscription-only model an inline reviewer can't drive) strands mid-run instead of being
|
|
1348
|
-
// refused up front. Before any teardown/side effects.
|
|
1349
|
-
await this.admission.assertRunnable(workspaceId, block, this.admission.runnableShapeOf(previous.steps), initiatedBy ?? previous.initiatedBy);
|
|
1350
|
-
// Tear down whatever was driving the run we're about to replace — its per-run
|
|
1351
|
-
// container AND its durable driver — before minting the restart. A `done`/`failed`
|
|
1352
|
-
// run is already terminal (a no-op teardown), but a still-`running` run would
|
|
1353
|
-
// otherwise leak a container and a live Workflows/pg-boss driver.
|
|
1354
|
-
await this.runStateMachine.stopRunContainer(workspaceId, previous);
|
|
1355
|
-
await this.workRunner.cancelRun(workspaceId, executionId);
|
|
1356
|
-
const { steps, currentStep } = planRestartFromStep(previous, fromStepIndex);
|
|
1357
|
-
// Mint the activation before replacing the prior run, so a bad password aborts the
|
|
1358
|
-
// restart without losing the source run.
|
|
1359
|
-
const newId = this.idGenerator.next('exec');
|
|
1360
|
-
const replaceId = previous.id;
|
|
1361
|
-
await activate?.(newId);
|
|
1362
|
-
// Like retry(), this mints a FRESH run id. `claimLiveRunOrConflict` atomically supersedes
|
|
1363
|
-
// the torn-down source run (`replaceId`, which here may still be LIVE — running/paused/
|
|
1364
|
-
// blocked) and clears terminal rows in one transaction, so a concurrent start that already
|
|
1365
|
-
// created a NEW live run for the block loses (409) instead of being silently clobbered.
|
|
1366
|
-
const instance = buildResumedInstance({
|
|
1367
|
-
previous,
|
|
1368
|
-
id: newId,
|
|
1369
|
-
plan: { steps, currentStep },
|
|
1370
|
-
initiatedBy,
|
|
1371
|
-
now: this.clock.now(),
|
|
1372
|
-
});
|
|
1373
|
-
await claimLiveRunOrConflict(this.runStartDeps, workspaceId, instance, replaceId);
|
|
1374
|
-
const done = steps.filter((s) => s.state === 'done').length;
|
|
1375
|
-
await this.blockRepository.update(workspaceId, previous.blockId, {
|
|
1376
|
-
status: 'in_progress',
|
|
1377
|
-
progress: steps.length > 0 ? done / steps.length : 0,
|
|
1378
|
-
executionId: instance.id,
|
|
1379
|
-
});
|
|
1380
|
-
await handOffLiveRun(this.runStartDeps, workspaceId, instance, block);
|
|
1381
|
-
return instance;
|
|
1382
|
-
}
|
|
1383
|
-
/**
|
|
1384
|
-
* The bound callbacks the two run-start funnels (`runStart.ts`) need, so they depend on no
|
|
1385
|
-
* concrete repository or service. Those funnels own the ORDER between a run's atomic claim and
|
|
1386
|
-
* its hand-off and are documented there; every start path calls both, writing the block state
|
|
1387
|
-
* that path owns in between.
|
|
1388
|
-
*/
|
|
1389
|
-
get runStartDeps() {
|
|
1390
|
-
return {
|
|
1391
|
-
insertLive: (ws, instance, options) => this.executionRepository.insertLive(ws, instance, options),
|
|
1392
|
-
startRun: (ws, id) => this.workRunner.startRun(ws, id),
|
|
1393
|
-
emitInstance: (ws, instance) => this.runStateMachine.emitInstance(ws, instance),
|
|
1394
|
-
publishRunStarted: (ws, instance, block) => this.runStateMachine.publishRunStarted(ws, instance, block),
|
|
1395
|
-
};
|
|
1396
|
-
}
|
|
1397
|
-
/**
|
|
1398
|
-
* Resume every run paused by the spend safeguard in this workspace. Flips them
|
|
1399
|
-
* back to `running` and re-drives the durable runner. If the budget is still
|
|
1400
|
-
* exhausted the spend gate will simply pause them again on their next step.
|
|
1401
|
-
*/
|
|
1402
|
-
async resumePaused(workspaceId) {
|
|
1403
|
-
await this.requireWorkspace(workspaceId);
|
|
1404
|
-
// Lean projection: only the paused runs' ids are needed to re-drive them — no `detail` decode.
|
|
1405
|
-
const live = await this.executionRepository.listLive(workspaceId);
|
|
1406
|
-
const paused = live.filter((e) => e.status === 'paused');
|
|
1407
|
-
for (const p of paused) {
|
|
1408
|
-
// Optimistic-concurrency write: only flip + re-drive a run that is STILL paused at
|
|
1409
|
-
// write time, so a resume racing the driver (or a concurrent resume) can't clobber a
|
|
1410
|
-
// run another writer already advanced. A vanished/contended run is skipped (the next
|
|
1411
|
-
// sweep retries) rather than failing the whole batch.
|
|
1412
|
-
let flipped = false;
|
|
1413
|
-
const resumed = await this.runStateMachine
|
|
1414
|
-
.mutateInstance(workspaceId, p.id, (inst) => {
|
|
1415
|
-
flipped = inst.status === 'paused';
|
|
1416
|
-
if (flipped)
|
|
1417
|
-
inst.status = 'running';
|
|
1418
|
-
})
|
|
1419
|
-
.catch(() => null);
|
|
1420
|
-
if (resumed && flipped) {
|
|
1421
|
-
// `startRun` re-drives runners that re-create the run from scratch (pg-boss re-enqueues
|
|
1422
|
-
// the same id). On Cloudflare the paused run's Workflows instance is still ALIVE parked
|
|
1423
|
-
// on a `waitForEvent`, so `startRun`'s `create` no-ops there; `signalResume` delivers the
|
|
1424
|
-
// event that wakes it immediately instead of waiting out the periodic budget re-check.
|
|
1425
|
-
await this.workRunner.startRun(workspaceId, resumed.id);
|
|
1426
|
-
await this.workRunner.signalResume?.(workspaceId, resumed.id);
|
|
1427
|
-
await this.runStateMachine.emitInstance(workspaceId, resumed);
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
// Clear the workspace-scoped `budget_paused` card now the pause is being lifted (F3). If the
|
|
1431
|
-
// budget is still exhausted a resumed run re-pauses and re-raises it on its next step.
|
|
1432
|
-
await this.runStateMachine.clearBudgetPaused(workspaceId);
|
|
1433
|
-
return this.executionRepository.listByWorkspace(workspaceId);
|
|
1434
|
-
}
|
|
1435
|
-
/** Cancel the run on a block, returning it to `planned`. */
|
|
1436
|
-
async cancel(workspaceId, blockId) {
|
|
1437
|
-
await this.requireWorkspace(workspaceId);
|
|
1438
|
-
await this.requireBlock(workspaceId, blockId);
|
|
1439
|
-
// Tear down the durable run (if any) AND its per-run container before removing
|
|
1440
|
-
// the record, so a cancel never leaves a container running until its watchdog.
|
|
1441
|
-
const existing = await this.executionRepository.getByBlock(workspaceId, blockId);
|
|
1442
|
-
if (existing) {
|
|
1443
|
-
await this.runStateMachine.stopRunContainer(workspaceId, existing);
|
|
1444
|
-
await this.workRunner.cancelRun(workspaceId, existing.id);
|
|
1445
|
-
}
|
|
1446
|
-
await this.executionRepository.deleteByBlock(workspaceId, blockId);
|
|
1447
|
-
await this.blockRepository.update(workspaceId, blockId, {
|
|
1448
|
-
status: 'planned',
|
|
1449
|
-
progress: 0,
|
|
1450
|
-
executionId: null,
|
|
1451
|
-
});
|
|
1452
|
-
// The run record is gone and the block is back to planned; the client can't
|
|
1453
|
-
// reconstruct that from a per-instance event, so signal a coarse refresh. Name the block
|
|
1454
|
-
// so the refresh fans out to every board mounting its shared service.
|
|
1455
|
-
await this.events.boardChanged(workspaceId, 'cancel', blockId);
|
|
1456
|
-
return this.requireBlock(workspaceId, blockId);
|
|
1457
|
-
}
|
|
1458
|
-
/**
|
|
1459
|
-
* Explicitly stop a *running* run by id (the unified `POST /agent-runs/:id/stop`
|
|
1460
|
-
* surface): kill its per-run container, tear down the durable driver, then record
|
|
1461
|
-
* a terminal `cancelled` failure so the board shows the run stopped (with retry)
|
|
1462
|
-
* rather than spinning forever. Idempotent — a run already terminal is returned
|
|
1463
|
-
* as-is. `opts.reason`/`opts.kind` let the orphan sweep reuse this with its own
|
|
1464
|
-
* wording instead of the user-facing default.
|
|
1465
|
-
*/
|
|
1466
|
-
async stopRun(workspaceId, executionId, opts = {}) {
|
|
1467
|
-
await this.requireWorkspace(workspaceId);
|
|
1468
|
-
const instance = assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
|
|
1469
|
-
if (instance.status === 'failed' || instance.status === 'done')
|
|
1470
|
-
return instance;
|
|
1471
|
-
await this.runStateMachine.stopRunContainer(workspaceId, instance);
|
|
1472
|
-
await this.workRunner.cancelRun(workspaceId, executionId);
|
|
1473
|
-
await this.failRun(workspaceId, executionId, opts.reason ?? 'Stopped by the user.', opts.kind ?? 'cancelled');
|
|
1474
|
-
return assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
|
|
1475
|
-
}
|
|
1476
|
-
/**
|
|
1477
|
-
* Tear down every run under a block subtree — kill each container, terminate each
|
|
1478
|
-
* durable driver, and delete the run record — so deleting a service/module never
|
|
1479
|
-
* orphans a container or a Workflows instance. Best-effort and silent: the board
|
|
1480
|
-
* delete that follows emits the coarse refresh, so no per-run event is needed.
|
|
1481
|
-
*
|
|
1482
|
-
* Returns the workspace block list it loaded so the immediately-following `removeBlock`
|
|
1483
|
-
* can reuse it instead of re-listing the whole board (this teardown deletes only run
|
|
1484
|
-
* records, never blocks, so the list is still current) — see {@link PreloadedBlocks}.
|
|
1485
|
-
*/
|
|
1486
|
-
async teardownForBlockTree(workspaceId, rootId) {
|
|
1487
|
-
const blocks = await this.blockRepository.listByWorkspace(workspaceId);
|
|
1488
|
-
// Resolve every run in one query and index by block id, rather than a per-block
|
|
1489
|
-
// getByBlock (N+1) over the whole subtree.
|
|
1490
|
-
const runsByBlock = new Map((await this.executionRepository.listByWorkspace(workspaceId)).map((run) => [
|
|
1491
|
-
run.blockId,
|
|
1492
|
-
run,
|
|
1493
|
-
]));
|
|
1494
|
-
for (const blockId of descendantIds(blocks, rootId)) {
|
|
1495
|
-
const run = runsByBlock.get(blockId);
|
|
1496
|
-
if (!run)
|
|
1497
|
-
continue;
|
|
1498
|
-
await this.runStateMachine.stopRunContainer(workspaceId, run);
|
|
1499
|
-
await this.workRunner.cancelRun(workspaceId, run.id);
|
|
1500
|
-
await this.executionRepository.deleteByBlock(workspaceId, blockId);
|
|
1501
|
-
}
|
|
1502
|
-
return { workspaceId, blocks };
|
|
953
|
+
// ---- run-lifecycle pass-throughs ----------------------------------------
|
|
954
|
+
// Launching, re-launching, resuming and ending a run all live on
|
|
955
|
+
// {@link RunLifecycleController} (they share the claim-then-hand-off order documented there);
|
|
956
|
+
// these thin delegations keep this service the single surface the HTTP layer talks to.
|
|
957
|
+
/** @see RunLifecycleController.start */
|
|
958
|
+
start(workspaceId, blockId, pipelineId, options = {}) {
|
|
959
|
+
return this.runActions.lifecycle.start(workspaceId, blockId, pipelineId, options);
|
|
960
|
+
}
|
|
961
|
+
/** @see RunLifecycleController.retry */
|
|
962
|
+
retry(workspaceId, executionId, initiatedBy, activate) {
|
|
963
|
+
return this.runActions.lifecycle.retry(workspaceId, executionId, initiatedBy, activate);
|
|
964
|
+
}
|
|
965
|
+
/** @see RunLifecycleController.restartFromStep */
|
|
966
|
+
restartFromStep(workspaceId, executionId, fromStepIndex, initiatedBy, activate) {
|
|
967
|
+
return this.runActions.lifecycle.restartFromStep(workspaceId, executionId, fromStepIndex, initiatedBy, activate);
|
|
968
|
+
}
|
|
969
|
+
/** @see RunLifecycleController.resumePaused */
|
|
970
|
+
resumePaused(workspaceId) {
|
|
971
|
+
return this.runActions.lifecycle.resumePaused(workspaceId);
|
|
972
|
+
}
|
|
973
|
+
/** @see RunLifecycleController.cancel */
|
|
974
|
+
cancel(workspaceId, blockId) {
|
|
975
|
+
return this.runActions.lifecycle.cancel(workspaceId, blockId);
|
|
976
|
+
}
|
|
977
|
+
/** @see RunLifecycleController.stopRun */
|
|
978
|
+
stopRun(workspaceId, executionId, opts = {}) {
|
|
979
|
+
return this.runActions.lifecycle.stopRun(workspaceId, executionId, opts);
|
|
980
|
+
}
|
|
981
|
+
/** @see RunLifecycleController.teardownForBlockTree */
|
|
982
|
+
teardownForBlockTree(workspaceId, rootId) {
|
|
983
|
+
return this.runActions.lifecycle.teardownForBlockTree(workspaceId, rootId);
|
|
1503
984
|
}
|
|
1504
985
|
}
|
|
1505
986
|
//# sourceMappingURL=ExecutionService.js.map
|