@zq-silk/yui 0.10.0 → 0.11.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/README.md +53 -0
- package/dist/cli/commandCatalog.js +28 -7
- package/dist/cli.js +50 -47
- package/dist/commands/projectCommands.js +69 -6
- package/dist/commands/taskCommands.js +639 -89
- package/dist/commands/taskContextCommand.js +78 -27
- package/dist/commands/taskNextActionCommand.js +13 -2
- package/dist/commands/taskOverviewCommand.js +21 -5
- package/dist/commands/taskUpstreamCommands.js +136 -0
- package/dist/context/runContextPack.js +184 -17
- package/dist/controller/agentRuntimeObserver.js +31 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +7 -13
- package/dist/execution/candidateConvergence.js +623 -0
- package/dist/execution/executionGroup.js +255 -13
- package/dist/execution/executionHealth.js +324 -0
- package/dist/execution/resourceBroker.js +425 -0
- package/dist/executor/fileRoleLaunchPlanner.js +6 -9
- package/dist/executor/workspacePreflightClassification.js +117 -0
- package/dist/lifecycle/exactRunTerminalization.js +13 -2
- package/dist/lifecycle/taskRoleSessionReset.js +4 -2
- package/dist/repository/taskBaseFreshness.js +26 -1
- package/dist/repository/taskWorkspacePreparer.js +17 -1
- package/dist/review/reviewRound.js +27 -6
- package/dist/run/agentRun.js +2 -2
- package/dist/run/recoveryProjection.js +15 -0
- package/dist/runtime/runtimeContinuationProjection.js +7 -0
- package/dist/runtime/tmuxAdapters.js +8 -2
- package/dist/scheduler/actionability.js +169 -3
- package/dist/scheduler/activeTaskProgress.js +15 -10
- package/dist/scheduler/leaderWakeupProcessor.js +17 -1
- package/dist/scheduler/taskExecutionProjection.js +105 -8
- package/dist/scheduler/taskObservabilityProjection.js +282 -0
- package/dist/storage/migration/productionRegistry.js +14 -0
- package/dist/storage/sqliteStore.js +12 -0
- package/dist/storage/taskStore.js +1 -1
- package/dist/task/completionReadiness.js +1 -1
- package/dist/task/nextAction.js +314 -2
- package/dist/web/assets/client/components.js +116 -0
- package/dist/web/assets/client/i18n.js +66 -0
- package/dist/web/assets/client/view.js +15 -0
- package/dist/web/assets/styles/cards.js +23 -0
- package/dist/web/assets/styles/responsive.js +2 -0
- package/dist/web/webSnapshot.js +8 -2
- package/dist/workItem/workItem.js +262 -5
- package/i18n/README.zh-CN.md +42 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { createRunAssignment } from "../context/runContextContract.js";
|
|
4
|
-
import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeRunContextSnapshot } from "../context/runContextPack.js";
|
|
4
|
+
import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeExecutionStageContextSnapshot, freezeReviewStageContextSnapshot, freezeRunContextSnapshot } from "../context/runContextPack.js";
|
|
5
5
|
import { contextSnapshotRef } from "../context/contextSnapshot.js";
|
|
6
6
|
import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
|
|
7
7
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
@@ -23,7 +23,7 @@ import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchAct
|
|
|
23
23
|
import { agentRunDeliveryReceiptId, createAgentRun, withAgentRunContextSnapshot } from "../run/agentRun.js";
|
|
24
24
|
import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
|
|
25
25
|
import { matchYieldReceipt } from "../run/yieldReceipt.js";
|
|
26
|
-
import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
|
|
26
|
+
import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryRunningReviewExecutionLane, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
|
|
27
27
|
import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../review/deltaRecheck.js";
|
|
28
28
|
import { buildTaskFinalReviewFindingContext, dispositionReviewFinding, planRepairGroups, reconcileReviewFindings, reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
|
|
29
29
|
import { LEADER_FINDING_DISPOSITIONS } from "../review/reviewFinding.js";
|
|
@@ -34,7 +34,7 @@ import { runPublicationCommand } from "./taskPublicationCommands.js";
|
|
|
34
34
|
import { enqueueWork, settleExactWorkExecution } from "../coordination/workMailboxQueue.js";
|
|
35
35
|
import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
|
|
36
36
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
37
|
-
import { blockingProviderContinuations, projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
37
|
+
import { blockingProviderContinuations, projectProviderContinuations, runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
|
|
38
38
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
39
39
|
import { activateTask, addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
40
40
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
@@ -42,8 +42,11 @@ import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publica
|
|
|
42
42
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
43
43
|
import { resolveProject } from "../repository/project.js";
|
|
44
44
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
45
|
-
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
46
|
-
import { addExecutionLane, createExecutionGroup, resolveExecutionGroup, restartExecutionLane, updateExecutionLane } from "../execution/executionGroup.js";
|
|
45
|
+
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, planWorkItemExplorationStage, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
46
|
+
import { addExecutionLane, createExecutionGroup, queueExecutionLaneRetry, resolveExecutionGroup, restartExecutionLane, skipPendingExecutionLanes, updateExecutionLane, WORK_ITEM_EXECUTION_MODES } from "../execution/executionGroup.js";
|
|
47
|
+
import { executionStageSpendClosed, observedExecutionResourceUsage, planResourceAdmissions, projectExecutionStageResources, resolveResourceBrokerPolicy, routeExecutionStage } from "../execution/resourceBroker.js";
|
|
48
|
+
import { resolveContextBudget, resolveControllerTaskConcurrency, resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
49
|
+
import { assertIndependentVerificationRoles, candidateConvergenceEvidenceSufficient, candidateConvergenceDirective, validateCandidateConvergenceResolution } from "../execution/candidateConvergence.js";
|
|
47
50
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
48
51
|
import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
49
52
|
import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, createTaskFinalReviewContractRebind, resolveRecordedTaskFinalReviewContract, taskFinalReviewContractRebindPayload } from "../review/taskFinalReviewContractRebind.js";
|
|
@@ -62,7 +65,7 @@ import { taskActor as resolveTaskActor, taskLeaderActionRunId } from "./taskActo
|
|
|
62
65
|
import { createTaskTerminalNotification } from "../scheduler/operatorNotification.js";
|
|
63
66
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
64
67
|
import { renderWakeReason, wakeReason } from "../scheduler/wakeReason.js";
|
|
65
|
-
import { collectTaskActionability, computeActionabilityDigest, deriveLeaderRunDisposition } from "../scheduler/actionability.js";
|
|
68
|
+
import { collectTaskActionability, computeActionabilityDigest, deriveLeaderRunDisposition, projectQueuedResourceLaneIdentities } from "../scheduler/actionability.js";
|
|
66
69
|
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
67
70
|
import { buildTaskOverview, parseTaskListOptions, renderTaskOverview } from "./taskOverviewCommand.js";
|
|
68
71
|
const LEADER_ROLE = "leader";
|
|
@@ -277,30 +280,76 @@ export function normalizedExecutionLaneRoles(assignee, requestedRoles, expanding
|
|
|
277
280
|
? [...requestedRoles]
|
|
278
281
|
: [assignee, ...requestedRoles];
|
|
279
282
|
}
|
|
283
|
+
/** A bounded same-stage retry repeats the frozen topology and Lane owners. */
|
|
284
|
+
export function resolvedExecutionStageRetryGroup(group) {
|
|
285
|
+
if (group?.stage === undefined || group.resolution === undefined)
|
|
286
|
+
return undefined;
|
|
287
|
+
if (group.resolution.decision !== "retry"
|
|
288
|
+
&& group.resolution.decision !== "blocked")
|
|
289
|
+
return undefined;
|
|
290
|
+
return group.stage.stage === "resolve" && group.resolution.decision === "retry"
|
|
291
|
+
? undefined
|
|
292
|
+
: group;
|
|
293
|
+
}
|
|
280
294
|
/** Derive the complete Worker Lane plan once for both preflight and mutation. */
|
|
281
295
|
export function normalizedExecutionLanePlan(input) {
|
|
282
296
|
const retry = input.phase === "retry" || input.retryLaneId !== undefined;
|
|
283
|
-
const
|
|
297
|
+
const queued = !retry && input.status === "running" && input.existingGroup !== undefined
|
|
298
|
+
&& input.requestedRoles.length === 0
|
|
299
|
+
? input.existingGroup.lanes.filter(({ status, effective }) => (status === "pending" && effective !== undefined))
|
|
300
|
+
: [];
|
|
301
|
+
const resumingQueued = queued.length > 0;
|
|
302
|
+
const expanding = !retry
|
|
303
|
+
&& !resumingQueued
|
|
304
|
+
&& input.status === "running"
|
|
305
|
+
&& input.existingGroup !== undefined;
|
|
284
306
|
const roles = retry
|
|
285
307
|
? []
|
|
286
|
-
:
|
|
308
|
+
: resumingQueued
|
|
309
|
+
? queued.map(({ roleName }) => roleName)
|
|
310
|
+
: input.resolvedRetryGroup !== undefined && input.requestedRoles.length === 0
|
|
311
|
+
? input.resolvedRetryGroup.lanes.map(({ roleName }) => roleName)
|
|
312
|
+
: normalizedExecutionLaneRoles(input.assignee, input.requestedRoles, expanding);
|
|
287
313
|
const strategy = input.existingGroup?.strategy
|
|
288
314
|
?? input.requestedStrategy
|
|
315
|
+
?? input.resolvedRetryGroup?.strategy
|
|
289
316
|
?? { mode: "fixed", count: roles.length };
|
|
290
317
|
const capacity = strategy.mode === "fixed" ? strategy.count : strategy.max;
|
|
291
318
|
const requestedCount = retry
|
|
292
319
|
? input.existingGroup?.lanes.length ?? 0
|
|
293
|
-
:
|
|
294
|
-
? input.existingGroup.lanes.length
|
|
295
|
-
:
|
|
320
|
+
: resumingQueued
|
|
321
|
+
? input.existingGroup.lanes.length
|
|
322
|
+
: expanding
|
|
323
|
+
? input.existingGroup.lanes.length + roles.length
|
|
324
|
+
: roles.length;
|
|
296
325
|
const laneIds = input.retryLaneId !== undefined
|
|
297
326
|
? [input.retryLaneId]
|
|
298
327
|
: retry
|
|
299
328
|
? []
|
|
300
|
-
:
|
|
301
|
-
?
|
|
302
|
-
:
|
|
303
|
-
|
|
329
|
+
: resumingQueued
|
|
330
|
+
? queued.map(({ id }) => id)
|
|
331
|
+
: input.existingGroup === undefined
|
|
332
|
+
? roles.map((_, index) => `${input.nextGroupId}-lane-${index + 1}`)
|
|
333
|
+
: roles.map((_, index) => `${input.existingGroup.id}-lane-${input.existingGroup.lanes.length + index + 1}`);
|
|
334
|
+
return { expanding, resumingQueued, roles, strategy, capacity, requestedCount, laneIds };
|
|
335
|
+
}
|
|
336
|
+
function activeResourceLaneIdentities(store) {
|
|
337
|
+
return store.listActiveTaskIds().flatMap((taskId) => (store.listAgentRuns(taskId).flatMap((run) => {
|
|
338
|
+
if (run.status !== "active"
|
|
339
|
+
|| run.executionGroupId === undefined
|
|
340
|
+
|| run.executionLaneId === undefined)
|
|
341
|
+
return [];
|
|
342
|
+
return [{
|
|
343
|
+
taskId,
|
|
344
|
+
...(run.workItemId === undefined ? {} : { workItemId: run.workItemId }),
|
|
345
|
+
executionGroupId: run.executionGroupId,
|
|
346
|
+
executionLaneId: run.executionLaneId,
|
|
347
|
+
providerId: run.effective.adapterId,
|
|
348
|
+
agentId: run.effective.agentId,
|
|
349
|
+
...(run.effective.model === undefined ? {} : { model: run.effective.model }),
|
|
350
|
+
requestedAt: run.createdAt
|
|
351
|
+
}];
|
|
352
|
+
})));
|
|
304
353
|
}
|
|
305
354
|
function taskProjectCommand(args, store, options) {
|
|
306
355
|
const [command, ...rest] = args;
|
|
@@ -2007,13 +2056,45 @@ function updateWork(args, store, options) {
|
|
|
2007
2056
|
});
|
|
2008
2057
|
}
|
|
2009
2058
|
function dispatchWork(args, store, options) {
|
|
2010
|
-
const usage = "Task work dispatch usage: yui task work dispatch <task>/<work> [--input <text>] [--strategy fixed:<count>|adaptive:<max>] [--lane-role <role> ...].";
|
|
2011
|
-
const parsed = parseMultiValueTail(args, new Set([
|
|
2059
|
+
const usage = "Task work dispatch usage: yui task work dispatch <task>/<work> [--input <text>] [--strategy fixed:<count>|adaptive:<max>] [--lane-role <role> ...] [--mode <single|parallel-diverse|ensemble-replicated|adversarial|adaptive-exploration>] [--max-rounds <count>] [--stage-max-attempts <count>] [--stage-max-tokens <count>] [--stage-max-tool-calls <count>] [--stage-max-seconds <count>] [--stage-quorum <count>] [--stage-straggler-seconds <count>] [--stage-min-marginal-value <0-100>].";
|
|
2060
|
+
const parsed = parseMultiValueTail(args, new Set([
|
|
2061
|
+
"--input",
|
|
2062
|
+
"--strategy",
|
|
2063
|
+
"--mode",
|
|
2064
|
+
"--max-rounds",
|
|
2065
|
+
"--stage-max-attempts",
|
|
2066
|
+
"--stage-max-tokens",
|
|
2067
|
+
"--stage-max-tool-calls",
|
|
2068
|
+
"--stage-max-seconds",
|
|
2069
|
+
"--stage-quorum",
|
|
2070
|
+
"--stage-straggler-seconds",
|
|
2071
|
+
"--stage-min-marginal-value"
|
|
2072
|
+
]), new Set(["--lane-role"]), usage);
|
|
2012
2073
|
exactPositionals(parsed.positionals, 1, usage);
|
|
2013
2074
|
const requestedStrategy = parseExecutionStrategy(parsed.options.get("--strategy"), usage);
|
|
2014
2075
|
const requestedLaneRoles = parsed.multiOptions.get("--lane-role") ?? [];
|
|
2076
|
+
const requestedMode = parseWorkItemExecutionMode(parsed.options.get("--mode"), usage);
|
|
2077
|
+
const requestedMaxRounds = parseOptionalPositiveInteger(parsed.options.get("--max-rounds"), "--max-rounds", usage);
|
|
2078
|
+
const requestedStageMaxAttempts = parseOptionalPositiveInteger(parsed.options.get("--stage-max-attempts"), "--stage-max-attempts", usage);
|
|
2079
|
+
const requestedStageMaxTokens = parseOptionalPositiveInteger(parsed.options.get("--stage-max-tokens"), "--stage-max-tokens", usage);
|
|
2080
|
+
const requestedStageMaxToolCalls = parseOptionalPositiveInteger(parsed.options.get("--stage-max-tool-calls"), "--stage-max-tool-calls", usage);
|
|
2081
|
+
const requestedStageMaxSeconds = parseOptionalPositiveInteger(parsed.options.get("--stage-max-seconds"), "--stage-max-seconds", usage);
|
|
2082
|
+
const requestedStageQuorum = parseOptionalPositiveInteger(parsed.options.get("--stage-quorum"), "--stage-quorum", usage);
|
|
2083
|
+
const requestedStageStragglerSeconds = parseOptionalPositiveInteger(parsed.options.get("--stage-straggler-seconds"), "--stage-straggler-seconds", usage);
|
|
2084
|
+
const requestedStageMinMarginalValue = parseOptionalPercentage(parsed.options.get("--stage-min-marginal-value"), "--stage-min-marginal-value", usage);
|
|
2085
|
+
const requestedResourceOptions = [
|
|
2086
|
+
requestedStageMaxTokens,
|
|
2087
|
+
requestedStageMaxToolCalls,
|
|
2088
|
+
requestedStageMaxSeconds,
|
|
2089
|
+
requestedStageQuorum,
|
|
2090
|
+
requestedStageStragglerSeconds,
|
|
2091
|
+
requestedStageMinMarginalValue
|
|
2092
|
+
];
|
|
2093
|
+
const explorationMode = requestedMode === undefined || requestedMode === "single"
|
|
2094
|
+
? undefined
|
|
2095
|
+
: requestedMode;
|
|
2015
2096
|
const now = clock(options);
|
|
2016
|
-
const
|
|
2097
|
+
const dispatch = store.transaction((tx) => {
|
|
2017
2098
|
const item = requireWorkItem(tx, parsed.positionals[0], options);
|
|
2018
2099
|
const task = requireTask(tx, item.taskId);
|
|
2019
2100
|
if (task.status !== "active") {
|
|
@@ -2023,10 +2104,44 @@ function dispatchWork(args, store, options) {
|
|
|
2023
2104
|
const existingGroup = currentGroup?.resolution === undefined
|
|
2024
2105
|
? currentGroup
|
|
2025
2106
|
: undefined;
|
|
2107
|
+
if (item.status === "failed" && existingGroup !== undefined) {
|
|
2108
|
+
throw usageError(`Work Item ${item.id} retains unresolved ExecutionGroup ${existingGroup.id}; `
|
|
2109
|
+
+ "retry the exact failed AgentRun so completed Lane results remain reusable.");
|
|
2110
|
+
}
|
|
2026
2111
|
const expanding = item.status === "running" && existingGroup !== undefined;
|
|
2027
|
-
|
|
2112
|
+
const continuingExploration = currentGroup?.stage !== undefined
|
|
2113
|
+
&& currentGroup.resolution !== undefined
|
|
2114
|
+
&& (item.status === "running" || item.status === "failed");
|
|
2115
|
+
const resolvedRetryGroup = resolvedExecutionStageRetryGroup(currentGroup);
|
|
2116
|
+
const startingExploration = item.executionGroups.length === 0
|
|
2117
|
+
&& explorationMode !== undefined;
|
|
2118
|
+
if (!expanding
|
|
2119
|
+
&& !continuingExploration
|
|
2120
|
+
&& item.status !== "pending"
|
|
2121
|
+
&& item.status !== "failed") {
|
|
2028
2122
|
throw usageError(`Work item ${item.id} cannot be dispatched from ${item.status}.`);
|
|
2029
2123
|
}
|
|
2124
|
+
if (requestedMode === "single" && currentGroup?.stage !== undefined) {
|
|
2125
|
+
throw usageError("An exploration WorkItem cannot return to single mode.");
|
|
2126
|
+
}
|
|
2127
|
+
if (explorationMode !== undefined
|
|
2128
|
+
&& item.executionGroups.length > 0
|
|
2129
|
+
&& currentGroup?.stage === undefined) {
|
|
2130
|
+
throw usageError("A WorkItem cannot enter exploration after single-mode ExecutionGroups.");
|
|
2131
|
+
}
|
|
2132
|
+
if (!startingExploration
|
|
2133
|
+
&& !continuingExploration
|
|
2134
|
+
&& (requestedMaxRounds !== undefined
|
|
2135
|
+
|| requestedStageMaxAttempts !== undefined
|
|
2136
|
+
|| requestedResourceOptions.some((value) => value !== undefined))) {
|
|
2137
|
+
throw usageError("Exploration limits require an explicit non-single --mode.");
|
|
2138
|
+
}
|
|
2139
|
+
if (existingGroup !== undefined && (requestedMode !== undefined
|
|
2140
|
+
|| requestedMaxRounds !== undefined
|
|
2141
|
+
|| requestedStageMaxAttempts !== undefined
|
|
2142
|
+
|| requestedResourceOptions.some((value) => value !== undefined))) {
|
|
2143
|
+
throw usageError(`Execution stage configuration is frozen: ${existingGroup.id}.`);
|
|
2144
|
+
}
|
|
2030
2145
|
if (item.assignee === undefined) {
|
|
2031
2146
|
throw usageError(`Work Item has no Task Role assignee: ${item.id}. `
|
|
2032
2147
|
+ `The Task Leader must run "yui task work update ${item.id} running", `
|
|
@@ -2066,6 +2181,7 @@ function dispatchWork(args, store, options) {
|
|
|
2066
2181
|
requestedRoles: requestedLaneRoles,
|
|
2067
2182
|
requestedStrategy,
|
|
2068
2183
|
existingGroup,
|
|
2184
|
+
resolvedRetryGroup,
|
|
2069
2185
|
status: item.status,
|
|
2070
2186
|
nextGroupId: `execution-group-${tx.peekNextAgentRunId(task.id)}`,
|
|
2071
2187
|
retryLaneId: undefined,
|
|
@@ -2088,7 +2204,10 @@ function dispatchWork(args, store, options) {
|
|
|
2088
2204
|
if (requestedCount > available) {
|
|
2089
2205
|
throw usageError(`ExecutionGroup Lane count ${requestedCount} exceeds its ${strategy.mode} capacity ${available}.`);
|
|
2090
2206
|
}
|
|
2091
|
-
if (strategy.mode === "fixed"
|
|
2207
|
+
if (strategy.mode === "fixed"
|
|
2208
|
+
&& !lanePlan.expanding
|
|
2209
|
+
&& !lanePlan.resumingQueued
|
|
2210
|
+
&& laneRoles.length !== strategy.count) {
|
|
2092
2211
|
throw usageError(`fixed:${strategy.count} requires exactly ${strategy.count} --lane-role values.`);
|
|
2093
2212
|
}
|
|
2094
2213
|
const roles = laneRoles.map((name) => requireRole(tx, task.id, name));
|
|
@@ -2103,36 +2222,121 @@ function dispatchWork(args, store, options) {
|
|
|
2103
2222
|
});
|
|
2104
2223
|
let group = existingGroup;
|
|
2105
2224
|
if (group === undefined) {
|
|
2106
|
-
|
|
2225
|
+
const groupId = `execution-group-${plans[0].runId}`;
|
|
2226
|
+
const stage = !startingExploration && !continuingExploration
|
|
2227
|
+
? undefined
|
|
2228
|
+
: (() => {
|
|
2229
|
+
const config = tx.getConfig();
|
|
2230
|
+
const contextBudget = resolveContextBudget(config.contextBudget);
|
|
2231
|
+
const health = resolveRuntimeHealth(config.runtimeHealth);
|
|
2232
|
+
const maxWallClockSeconds = requestedStageMaxSeconds
|
|
2233
|
+
?? Math.floor(health.stallWindowMs / 1_000);
|
|
2234
|
+
const quorum = requestedStageQuorum
|
|
2235
|
+
?? (strategy.mode === "fixed" ? strategy.count : 1);
|
|
2236
|
+
if (quorum > available) {
|
|
2237
|
+
throw usageError(`Execution stage quorum ${quorum} exceeds Lane capacity ${available}.`);
|
|
2238
|
+
}
|
|
2239
|
+
return planWorkItemExplorationStage(item, {
|
|
2240
|
+
...(explorationMode === undefined ? {} : { mode: explorationMode }),
|
|
2241
|
+
...(requestedMaxRounds === undefined ? {} : { maxRounds: requestedMaxRounds }),
|
|
2242
|
+
...(requestedStageMaxAttempts === undefined
|
|
2243
|
+
? {}
|
|
2244
|
+
: { maxAttempts: requestedStageMaxAttempts }),
|
|
2245
|
+
...(startingExploration
|
|
2246
|
+
? { convergence: { schemaVersion: 1 } }
|
|
2247
|
+
: {}),
|
|
2248
|
+
resourceBudget: {
|
|
2249
|
+
maxTokens: requestedStageMaxTokens ?? contextBudget.hardTokens * available,
|
|
2250
|
+
maxToolCalls: requestedStageMaxToolCalls ?? 100 * available,
|
|
2251
|
+
maxWallClockSeconds
|
|
2252
|
+
},
|
|
2253
|
+
resources: {
|
|
2254
|
+
schemaVersion: 1,
|
|
2255
|
+
quorum,
|
|
2256
|
+
deadlineAt: new Date(now.getTime() + maxWallClockSeconds * 1_000).toISOString(),
|
|
2257
|
+
stragglerAfterSeconds: requestedStageStragglerSeconds
|
|
2258
|
+
?? Math.floor(health.quietAfterMs / 1_000),
|
|
2259
|
+
minimumMarginalValuePercent: requestedStageMinMarginalValue ?? 10
|
|
2260
|
+
},
|
|
2261
|
+
strategy,
|
|
2262
|
+
contextSnapshotRef: contextSnapshotRef(freezeExecutionStageContextSnapshot(tx, {
|
|
2263
|
+
taskId: task.id,
|
|
2264
|
+
workItemId: item.id,
|
|
2265
|
+
executionGroupId: groupId
|
|
2266
|
+
}, now))
|
|
2267
|
+
});
|
|
2268
|
+
})();
|
|
2269
|
+
group = createExecutionGroup(groupId, task.id, {
|
|
2107
2270
|
purpose: "execution",
|
|
2108
2271
|
target: executionTargetForWorkItem(task.id, item.id, item.revision, item, workspace),
|
|
2109
2272
|
strategy,
|
|
2110
|
-
lanes: laneRoles.map((roleName) => ({ roleName }))
|
|
2273
|
+
lanes: laneRoles.map((roleName) => ({ roleName })),
|
|
2274
|
+
...(stage === undefined ? {} : { stage })
|
|
2111
2275
|
}, now);
|
|
2112
2276
|
}
|
|
2113
|
-
else if (expanding) {
|
|
2277
|
+
else if (lanePlan.expanding) {
|
|
2114
2278
|
for (const roleName of laneRoles) {
|
|
2115
2279
|
group = addExecutionLane(group, { roleName }, now);
|
|
2116
2280
|
}
|
|
2117
2281
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2282
|
+
if (group.stage !== undefined) {
|
|
2283
|
+
assertIndependentVerificationRoles(item, group.stage, laneRoles);
|
|
2284
|
+
}
|
|
2285
|
+
const lanes = lanePlan.resumingQueued
|
|
2286
|
+
? lanePlan.laneIds.map((laneId) => group.lanes.find(({ id }) => id === laneId))
|
|
2287
|
+
: lanePlan.expanding
|
|
2288
|
+
? group.lanes.slice(-plans.length)
|
|
2289
|
+
: group.lanes.slice(0, plans.length);
|
|
2121
2290
|
if (lanes.length !== plans.length)
|
|
2122
2291
|
throw new Error(`ExecutionGroup Lane planning drift: ${group.id}.`);
|
|
2123
2292
|
const createdRuns = [];
|
|
2293
|
+
const queuedAdmissions = [];
|
|
2124
2294
|
let runningGroup = group;
|
|
2295
|
+
const config = tx.getConfig();
|
|
2296
|
+
const controllerConcurrency = resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
|
|
2297
|
+
const brokerPolicy = resolveResourceBrokerPolicy({
|
|
2298
|
+
maxActiveLanes: controllerConcurrency,
|
|
2299
|
+
maxActiveLanesPerProvider: controllerConcurrency,
|
|
2300
|
+
maxQueuedLanesPerGroup: Math.max(controllerConcurrency, available)
|
|
2301
|
+
});
|
|
2302
|
+
const activeResources = activeResourceLaneIdentities(tx);
|
|
2303
|
+
const queuedResources = projectQueuedResourceLaneIdentities(tx, now);
|
|
2304
|
+
const stageResources = group.stage === undefined
|
|
2305
|
+
? undefined
|
|
2306
|
+
: projectExecutionStageResources({
|
|
2307
|
+
group,
|
|
2308
|
+
stageGroups: [...item.executionGroups, group],
|
|
2309
|
+
usage: observedExecutionResourceUsage({
|
|
2310
|
+
group,
|
|
2311
|
+
stageGroups: [...item.executionGroups, group],
|
|
2312
|
+
runs: tx.listAgentRuns(task.id),
|
|
2313
|
+
events: tx.listEvents(task.id)
|
|
2314
|
+
}),
|
|
2315
|
+
now
|
|
2316
|
+
});
|
|
2317
|
+
const stageSpendClosed = stageResources !== undefined
|
|
2318
|
+
&& executionStageSpendClosed(stageResources);
|
|
2125
2319
|
for (let index = 0; index < plans.length; index += 1) {
|
|
2126
2320
|
const plan = plans[index];
|
|
2127
2321
|
const lane = lanes[index];
|
|
2128
2322
|
const laneIsolated = plans.length > 1 || group.lanes.length > 1 || group.strategy.mode === "adaptive";
|
|
2323
|
+
const storedLaneWorkspace = laneIsolated
|
|
2324
|
+
? tx.getManagedWorkspace({
|
|
2325
|
+
type: "execution-lane",
|
|
2326
|
+
taskId: task.id,
|
|
2327
|
+
executionGroupId: group.id,
|
|
2328
|
+
executionLaneId: lane.id,
|
|
2329
|
+
purpose: "execution",
|
|
2330
|
+
workItemId: item.id
|
|
2331
|
+
}) ?? undefined
|
|
2332
|
+
: undefined;
|
|
2129
2333
|
const laneManagedWorkspace = laneIsolated
|
|
2130
|
-
? options.executionLaneWorkspaces?.get(lane.id)
|
|
2334
|
+
? options.executionLaneWorkspaces?.get(lane.id) ?? storedLaneWorkspace
|
|
2131
2335
|
: runWorkspace;
|
|
2132
2336
|
if (laneIsolated && laneManagedWorkspace === undefined && options.yuiHome !== undefined) {
|
|
2133
2337
|
throw usageError(`Execution Lane workspace preflight is missing: ${group.id}/${lane.id}.`);
|
|
2134
2338
|
}
|
|
2135
|
-
const effective = resolveEffectiveLaunch({
|
|
2339
|
+
const effective = lane.effective ?? resolveEffectiveLaunch({
|
|
2136
2340
|
role: plan.role,
|
|
2137
2341
|
purpose: "execution",
|
|
2138
2342
|
...(laneManagedWorkspace === undefined ? {} : { workspace: laneManagedWorkspace }),
|
|
@@ -2146,19 +2350,56 @@ function dispatchWork(args, store, options) {
|
|
|
2146
2350
|
root: laneManagedWorkspace.root,
|
|
2147
2351
|
writableProjectIds: [...item.writeProjectIds]
|
|
2148
2352
|
};
|
|
2353
|
+
const request = {
|
|
2354
|
+
taskId: task.id,
|
|
2355
|
+
workItemId: item.id,
|
|
2356
|
+
executionGroupId: group.id,
|
|
2357
|
+
executionLaneId: lane.id,
|
|
2358
|
+
providerId: effective.adapterId,
|
|
2359
|
+
agentId: effective.agentId,
|
|
2360
|
+
...(effective.model === undefined ? {} : { model: effective.model }),
|
|
2361
|
+
requestedAt: lane.effective === undefined ? now.toISOString() : lane.updatedAt
|
|
2362
|
+
};
|
|
2363
|
+
const admission = planResourceAdmissions({
|
|
2364
|
+
policy: brokerPolicy,
|
|
2365
|
+
active: activeResources,
|
|
2366
|
+
queued: queuedResources.filter(({ taskId, executionGroupId, executionLaneId }) => !(taskId === request.taskId
|
|
2367
|
+
&& executionGroupId === request.executionGroupId
|
|
2368
|
+
&& executionLaneId === request.executionLaneId)),
|
|
2369
|
+
requests: [request]
|
|
2370
|
+
})[0];
|
|
2371
|
+
const frozenDirective = lane.directive ?? rawInput;
|
|
2372
|
+
if (stageSpendClosed || admission.decision !== "admitted") {
|
|
2373
|
+
if (lane.effective === undefined || lane.directive === undefined) {
|
|
2374
|
+
runningGroup = updateExecutionLane(runningGroup, lane.id, {
|
|
2375
|
+
effective,
|
|
2376
|
+
workspace: laneWorkspace,
|
|
2377
|
+
directive: frozenDirective
|
|
2378
|
+
}, now);
|
|
2379
|
+
}
|
|
2380
|
+
queuedAdmissions.push(stageSpendClosed
|
|
2381
|
+
? `${lane.id}: stage budget/deadline reached`
|
|
2382
|
+
: `${lane.id}: ${admission.reason}`);
|
|
2383
|
+
if (!queuedResources.some(({ taskId, executionGroupId, executionLaneId }) => (taskId === request.taskId
|
|
2384
|
+
&& executionGroupId === request.executionGroupId
|
|
2385
|
+
&& executionLaneId === request.executionLaneId)))
|
|
2386
|
+
queuedResources.push(request);
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
activeResources.push(request);
|
|
2149
2390
|
const runningLane = lane.status === "failed"
|
|
2150
|
-
|| lane.status === "yielded"
|
|
2151
|
-
|| lane.status === "completed"
|
|
2152
2391
|
? restartExecutionLane(runningGroup, lane.id, {
|
|
2153
2392
|
runId: plan.runId,
|
|
2154
2393
|
effective,
|
|
2155
|
-
workspace: laneWorkspace
|
|
2394
|
+
workspace: laneWorkspace,
|
|
2395
|
+
directive: frozenDirective
|
|
2156
2396
|
}, now)
|
|
2157
2397
|
: updateExecutionLane(runningGroup, lane.id, {
|
|
2158
2398
|
status: "running",
|
|
2159
2399
|
runId: plan.runId,
|
|
2160
2400
|
effective,
|
|
2161
|
-
workspace: laneWorkspace
|
|
2401
|
+
workspace: laneWorkspace,
|
|
2402
|
+
directive: frozenDirective
|
|
2162
2403
|
}, now);
|
|
2163
2404
|
runningGroup = runningLane;
|
|
2164
2405
|
const assignment = createRunAssignment({
|
|
@@ -2172,7 +2413,16 @@ function dispatchWork(args, store, options) {
|
|
|
2172
2413
|
executionGroupId: runningGroup.id,
|
|
2173
2414
|
executionLaneId: lane.id
|
|
2174
2415
|
},
|
|
2175
|
-
directive: `${
|
|
2416
|
+
directive: `${frozenDirective}\nFrozen target: ${group.target.fingerprint}.`
|
|
2417
|
+
+ (group.stage === undefined
|
|
2418
|
+
? ""
|
|
2419
|
+
: `\nExploration stage: ${group.stage.stage}; round=${group.stage.round}; attempt=${group.stage.stageAttempt}.`
|
|
2420
|
+
+ (group.stage.resources === undefined
|
|
2421
|
+
? ""
|
|
2422
|
+
: `\nResource budget: tokens=${group.stage.budget.maxTokens}; tools=${group.stage.budget.maxToolCalls}; wall=${group.stage.budget.maxWallClockSeconds}s; quorum=${group.stage.resources.quorum}; deadline=${group.stage.resources.deadlineAt}; straggler=${group.stage.resources.stragglerAfterSeconds}s.`)
|
|
2423
|
+
+ (group.stage.convergence === undefined
|
|
2424
|
+
? ""
|
|
2425
|
+
: `\n${candidateConvergenceDirective(group.stage)}`)),
|
|
2176
2426
|
deltaRefIds: []
|
|
2177
2427
|
});
|
|
2178
2428
|
createdRuns.push(createAgentRun(plan.runId, task.id, plan.role.name, dispatchMode, assignment, now, {
|
|
@@ -2237,10 +2487,10 @@ function dispatchWork(args, store, options) {
|
|
|
2237
2487
|
roleName: unboundRun.roleName,
|
|
2238
2488
|
purpose: "execution",
|
|
2239
2489
|
workItemId: item.id
|
|
2240
|
-
}, now);
|
|
2490
|
+
}, now, "controller", runningGroup.stage?.contextSnapshotRef);
|
|
2241
2491
|
const runWithLineage = withAgentRunContextSnapshot(unboundRun, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
2242
2492
|
createdRuns[index] = runWithLineage;
|
|
2243
|
-
const role =
|
|
2493
|
+
const role = requireRole(tx, task.id, unboundRun.roleName);
|
|
2244
2494
|
tx.saveAgentRun(runWithLineage);
|
|
2245
2495
|
tx.saveActiveAgentRun(runWithLineage);
|
|
2246
2496
|
tx.saveRole(task.id, updateRoleStatus(role, "running", now));
|
|
@@ -2250,22 +2500,29 @@ function dispatchWork(args, store, options) {
|
|
|
2250
2500
|
]);
|
|
2251
2501
|
recordTaskEvent(tx, task.id, "run.dispatched", runLaunchEventPayload(runWithLineage), now);
|
|
2252
2502
|
}
|
|
2253
|
-
return createdRuns;
|
|
2503
|
+
return { runs: createdRuns, queuedAdmissions, group: runningGroup };
|
|
2254
2504
|
});
|
|
2255
|
-
for (const run of runs)
|
|
2505
|
+
for (const run of dispatch.runs) {
|
|
2256
2506
|
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
2257
|
-
|
|
2258
|
-
|
|
2507
|
+
}
|
|
2508
|
+
const backpressure = dispatch.queuedAdmissions.length === 0
|
|
2509
|
+
? ""
|
|
2510
|
+
: `; ${dispatch.queuedAdmissions.length} Lane(s) retained pending by Resource Broker`;
|
|
2511
|
+
const first = dispatch.runs[0];
|
|
2512
|
+
return first === undefined
|
|
2513
|
+
? `No Lane admitted; ${dispatch.queuedAdmissions.join("; ")}\n`
|
|
2514
|
+
: `Dispatch queued for ${first.taskId}/${first.roleName} (${first.id})${dispatch.runs.length > 1 ? `; ${dispatch.runs.length} Lanes` : ""}${backpressure}\n`;
|
|
2259
2515
|
}
|
|
2260
2516
|
function resolveWorkExecutionGroup(args, store, options) {
|
|
2261
|
-
const usage = "Task work group resolve usage: yui task work group resolve <task>/<work> --decision <accept|reject|blocked> --summary <text> [--lane <lane-id> ...].";
|
|
2517
|
+
const usage = "Task work group resolve usage: yui task work group resolve <task>/<work> --decision <accept|reject|retry|blocked> --summary <text> [--lane <lane-id> ...] [--early-stop <marginal-value-percent>].";
|
|
2262
2518
|
if (args[0] !== "resolve")
|
|
2263
2519
|
throw usageError(usage);
|
|
2264
|
-
const parsed = parseMultiValueTail(args.slice(1), new Set(["--decision", "--summary"]), new Set(["--lane"]), usage);
|
|
2520
|
+
const parsed = parseMultiValueTail(args.slice(1), new Set(["--decision", "--summary", "--early-stop"]), new Set(["--lane"]), usage);
|
|
2265
2521
|
exactPositionals(parsed.positionals, 1, usage);
|
|
2266
2522
|
const decision = parseExecutionResolutionDecision(parsed.options.get("--decision"), usage);
|
|
2267
2523
|
const summary = requiredOption(parsed.options, "--summary");
|
|
2268
2524
|
const selectedLaneIds = parsed.multiOptions.get("--lane");
|
|
2525
|
+
const earlyStopMarginalValue = parseOptionalPercentage(parsed.options.get("--early-stop"), "--early-stop", usage);
|
|
2269
2526
|
const now = clock(options);
|
|
2270
2527
|
const result = store.transaction((tx) => {
|
|
2271
2528
|
const item = requireWorkItem(tx, parsed.positionals[0], options);
|
|
@@ -2275,19 +2532,73 @@ function resolveWorkExecutionGroup(args, store, options) {
|
|
|
2275
2532
|
if (taskActor(options, task.id) !== "leader") {
|
|
2276
2533
|
throw usageError("Only the Task Leader may resolve an ExecutionGroup.");
|
|
2277
2534
|
}
|
|
2278
|
-
|
|
2535
|
+
let group = currentWorkItemExecutionGroup(item);
|
|
2279
2536
|
if (group === undefined
|
|
2280
|
-
|| (group.
|
|
2537
|
+
|| (group.stage === undefined
|
|
2538
|
+
&& group.lanes.length < 2
|
|
2539
|
+
&& group.strategy.mode !== "adaptive")) {
|
|
2281
2540
|
throw usageError(`Work Item ${item.id} has no resolvable ExecutionGroup.`);
|
|
2282
2541
|
}
|
|
2283
2542
|
if (item.status !== "running") {
|
|
2284
2543
|
throw usageError(`Work Item ${item.id} cannot resolve from ${item.status}.`);
|
|
2285
2544
|
}
|
|
2545
|
+
if (decision !== "accept" && group.stage?.resources !== undefined) {
|
|
2546
|
+
const activeLaneIds = group.lanes
|
|
2547
|
+
.filter(({ status }) => status === "running")
|
|
2548
|
+
.map(({ id }) => id);
|
|
2549
|
+
if (activeLaneIds.length > 0) {
|
|
2550
|
+
throw usageError(`Active Lanes must settle before ${decision}; Resource Broker will not kill them: ${activeLaneIds.join(", ")}.`);
|
|
2551
|
+
}
|
|
2552
|
+
group = skipPendingExecutionLanes(group, group.lanes.filter(({ status }) => status === "pending").map(({ id }) => id), `Leader resolved the stage as ${decision} before queued Lanes started.`, now);
|
|
2553
|
+
}
|
|
2286
2554
|
const acceptedLaneIds = decision === "accept"
|
|
2287
2555
|
? selectedLaneIds ?? group.lanes
|
|
2288
2556
|
.filter((lane) => lane.status === "yielded" || lane.status === "completed")
|
|
2289
2557
|
.map(({ id }) => id)
|
|
2290
2558
|
: undefined;
|
|
2559
|
+
if (earlyStopMarginalValue !== undefined) {
|
|
2560
|
+
if (decision !== "accept" || acceptedLaneIds === undefined) {
|
|
2561
|
+
throw usageError("--early-stop requires --decision accept.");
|
|
2562
|
+
}
|
|
2563
|
+
if (group.stage?.resources === undefined) {
|
|
2564
|
+
throw usageError("--early-stop requires a T6 resource-scheduled exploration stage.");
|
|
2565
|
+
}
|
|
2566
|
+
const resources = projectExecutionStageResources({
|
|
2567
|
+
group,
|
|
2568
|
+
stageGroups: item.executionGroups,
|
|
2569
|
+
usage: observedExecutionResourceUsage({
|
|
2570
|
+
group,
|
|
2571
|
+
stageGroups: item.executionGroups,
|
|
2572
|
+
runs: tx.listAgentRuns(task.id),
|
|
2573
|
+
events: tx.listEvents(task.id)
|
|
2574
|
+
}),
|
|
2575
|
+
now
|
|
2576
|
+
});
|
|
2577
|
+
const routing = routeExecutionStage({
|
|
2578
|
+
group,
|
|
2579
|
+
resources,
|
|
2580
|
+
evidenceSufficient: candidateConvergenceEvidenceSufficient(item, group, acceptedLaneIds),
|
|
2581
|
+
disagreement: "unknown",
|
|
2582
|
+
marginalValuePercent: earlyStopMarginalValue
|
|
2583
|
+
});
|
|
2584
|
+
if (!routing.earlyTerminationAllowed) {
|
|
2585
|
+
throw usageError("Early termination is forbidden until quorum and acceptance-level evidence are sufficient.");
|
|
2586
|
+
}
|
|
2587
|
+
if (routing.action !== "resolve" && routing.retainActiveLaneIds.length === 0) {
|
|
2588
|
+
throw usageError(routing.reason);
|
|
2589
|
+
}
|
|
2590
|
+
group = skipPendingExecutionLanes(group, routing.cancelPendingLaneIds, `Stopped after quorum and sufficient evidence; observed marginal value ${earlyStopMarginalValue}%.`, now);
|
|
2591
|
+
if (routing.retainActiveLaneIds.length > 0) {
|
|
2592
|
+
const waiting = updateWorkItemExecutionGroup(item, group, now);
|
|
2593
|
+
tx.saveWorkItem(task.id, waiting);
|
|
2594
|
+
return {
|
|
2595
|
+
item: waiting,
|
|
2596
|
+
group,
|
|
2597
|
+
reviewDispatch: null,
|
|
2598
|
+
waitingForStragglers: true
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2291
2602
|
const resolutionSummary = acceptedLaneIds === undefined
|
|
2292
2603
|
? summary
|
|
2293
2604
|
: aggregateExecutionLaneSummary(summary, group, acceptedLaneIds);
|
|
@@ -2298,6 +2609,7 @@ function resolveWorkExecutionGroup(args, store, options) {
|
|
|
2298
2609
|
? { selectedLaneIds: acceptedLaneIds }
|
|
2299
2610
|
: selectedLaneIds === undefined ? {} : { selectedLaneIds })
|
|
2300
2611
|
}, now);
|
|
2612
|
+
validateCandidateConvergenceResolution(item, resolved);
|
|
2301
2613
|
const groupedItem = updateWorkItemExecutionGroup(item, resolved, now);
|
|
2302
2614
|
tx.saveWorkItem(task.id, groupedItem);
|
|
2303
2615
|
if (decision !== "accept") {
|
|
@@ -2306,7 +2618,23 @@ function resolveWorkExecutionGroup(args, store, options) {
|
|
|
2306
2618
|
enqueueWork(tx, leaderMailbox(task.id), "work-group-resolved", now, [
|
|
2307
2619
|
workItemRef(task.id, item.id)
|
|
2308
2620
|
]);
|
|
2309
|
-
return {
|
|
2621
|
+
return {
|
|
2622
|
+
item: failed,
|
|
2623
|
+
group: resolved,
|
|
2624
|
+
reviewDispatch: null,
|
|
2625
|
+
waitingForStragglers: false
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
if (resolved.stage !== undefined && resolved.stage.stage !== "resolve") {
|
|
2629
|
+
enqueueWork(tx, leaderMailbox(task.id), "work-group-resolved", now, [
|
|
2630
|
+
workItemRef(task.id, item.id)
|
|
2631
|
+
]);
|
|
2632
|
+
return {
|
|
2633
|
+
item: groupedItem,
|
|
2634
|
+
group: resolved,
|
|
2635
|
+
reviewDispatch: null,
|
|
2636
|
+
waitingForStragglers: false
|
|
2637
|
+
};
|
|
2310
2638
|
}
|
|
2311
2639
|
const eligible = resolved.lanes
|
|
2312
2640
|
.filter((lane) => resolved.resolution?.selectedLaneIds.includes(lane.id) ?? false)
|
|
@@ -2346,8 +2674,16 @@ function resolveWorkExecutionGroup(args, store, options) {
|
|
|
2346
2674
|
runRef(task.id, eligible.id),
|
|
2347
2675
|
workItemRef(task.id, item.id)
|
|
2348
2676
|
]);
|
|
2349
|
-
return {
|
|
2677
|
+
return {
|
|
2678
|
+
item: candidate,
|
|
2679
|
+
group: resolved,
|
|
2680
|
+
reviewDispatch,
|
|
2681
|
+
waitingForStragglers: false
|
|
2682
|
+
};
|
|
2350
2683
|
});
|
|
2684
|
+
if (result.waitingForStragglers) {
|
|
2685
|
+
return output(`Stopped pending spend for ${result.group.id}; active stragglers are retained until they settle: ${result.group.lanes.filter(({ status }) => status === "running").map(({ id }) => id).join(", ")}\n`, { workItem: result.item, executionGroup: result.group });
|
|
2686
|
+
}
|
|
2351
2687
|
if (result.reviewDispatch?.run !== null && result.reviewDispatch?.run !== undefined) {
|
|
2352
2688
|
notifyReviewMailbox(options, options.runtime, roleMailbox(result.reviewDispatch.run.taskId, result.reviewDispatch.run.roleName), result.reviewDispatch.run.taskId);
|
|
2353
2689
|
}
|
|
@@ -2728,7 +3064,12 @@ function reviewWork(args, store, options) {
|
|
|
2728
3064
|
&& round.candidateId === candidate.id
|
|
2729
3065
|
&& (round.status === "pending" || round.status === "running")))).at(-1);
|
|
2730
3066
|
if (activeRound !== undefined) {
|
|
2731
|
-
|
|
3067
|
+
const resourceQueued = activeRound.executionGroup !== undefined
|
|
3068
|
+
&& activeRound.executionGroup.resolution === undefined
|
|
3069
|
+
&& activeRound.executionGroup.lanes.some((lane) => (lane.status === "pending"
|
|
3070
|
+
&& lane.runId === undefined));
|
|
3071
|
+
if ((activeRound.status === "pending" && activeRound.reviewerRunId === undefined)
|
|
3072
|
+
|| (activeRound.status === "running" && resourceQueued)) {
|
|
2732
3073
|
return { round: activeRound, run: null, resumed: true };
|
|
2733
3074
|
}
|
|
2734
3075
|
throw usageError(`ReviewRound is already active: ${activeRound.id}/${activeRound.status}.`);
|
|
@@ -2740,7 +3081,7 @@ function reviewWork(args, store, options) {
|
|
|
2740
3081
|
notifyReviewMailbox(options, options.runtime, roleMailbox(result.run.taskId, result.run.roleName), result.run.taskId);
|
|
2741
3082
|
}
|
|
2742
3083
|
if (result.resumed) {
|
|
2743
|
-
return output(`Review request ${result.round.id}
|
|
3084
|
+
return output(`Review request ${result.round.id} has pending Lanes; resuming dispatch.\n`, { reviewRound: result.round });
|
|
2744
3085
|
}
|
|
2745
3086
|
return result.round.status === "failed"
|
|
2746
3087
|
? output(`Review could not start for ${result.round.workItemId}: ${result.round.summary}\n`, { reviewRound: result.round })
|
|
@@ -3206,6 +3547,10 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3206
3547
|
}
|
|
3207
3548
|
assertNoConflictingTaskReviewRound(taskRounds, exact.id);
|
|
3208
3549
|
if (requestedLaneRoles.length === 0) {
|
|
3550
|
+
if (exact.status === "running"
|
|
3551
|
+
&& exact.executionGroup?.resolution === undefined
|
|
3552
|
+
&& exact.executionGroup?.lanes.some((lane) => (lane.status === "pending" && lane.runId === undefined)))
|
|
3553
|
+
return exact;
|
|
3209
3554
|
assertTaskReviewRequestLane(tx, task.id, reviewerRoleName, exact);
|
|
3210
3555
|
return exact;
|
|
3211
3556
|
}
|
|
@@ -4022,6 +4367,15 @@ function retryRun(args, store, options) {
|
|
|
4022
4367
|
const task = requireTask(tx, previous.taskId);
|
|
4023
4368
|
if (task.status !== "active")
|
|
4024
4369
|
throw usageError(`Task is not active: ${task.id}.`);
|
|
4370
|
+
if (runOwnsBlockingProviderContinuation(tx.listEvents(task.id), {
|
|
4371
|
+
taskId: previous.taskId,
|
|
4372
|
+
roleName: previous.roleName,
|
|
4373
|
+
runId: previous.id,
|
|
4374
|
+
agentId: previous.effective.agentId
|
|
4375
|
+
})) {
|
|
4376
|
+
throw usageError(`Run ${previous.id} still owns an unsettled Provider continuation; `
|
|
4377
|
+
+ "reconcile it before retrying the Lane.");
|
|
4378
|
+
}
|
|
4025
4379
|
const role = requireRole(tx, task.id, previous.roleName);
|
|
4026
4380
|
if (tx.getActiveAgentRun(task.id, role.name) !== null) {
|
|
4027
4381
|
throw usageError(`${task.id}/${role.name} already has an active run.`);
|
|
@@ -4041,10 +4395,21 @@ function retryRun(args, store, options) {
|
|
|
4041
4395
|
const currentRetryGroup = retryItem === null
|
|
4042
4396
|
? undefined
|
|
4043
4397
|
: currentWorkItemExecutionGroup(retryItem);
|
|
4398
|
+
const retriesExecutionLane = previous.executionGroupId !== undefined;
|
|
4399
|
+
const exactCurrentLane = !retriesExecutionLane || (retryItem !== null
|
|
4400
|
+
&& currentRetryGroup !== undefined
|
|
4401
|
+
&& currentRetryGroup.id === previous.executionGroupId
|
|
4402
|
+
&& currentRetryGroup.resolution === undefined
|
|
4403
|
+
&& retryLaneBefore !== undefined
|
|
4404
|
+
&& retryLaneBefore.id === previous.executionLaneId
|
|
4405
|
+
&& retryLaneBefore.status === "failed"
|
|
4406
|
+
&& retryLaneBefore.runId === previous.id);
|
|
4407
|
+
if (!exactCurrentLane) {
|
|
4408
|
+
throw usageError(`Run ${previous.id} no longer owns the current failed Execution Lane.`);
|
|
4409
|
+
}
|
|
4044
4410
|
const groupedRunningRetry = retryItem?.status === "running"
|
|
4045
|
-
&&
|
|
4046
|
-
&&
|
|
4047
|
-
&& retryLaneBefore?.status === "failed";
|
|
4411
|
+
&& retriesExecutionLane
|
|
4412
|
+
&& exactCurrentLane;
|
|
4048
4413
|
if (retryItem !== null && retryItem.status !== "failed" && !groupedRunningRetry) {
|
|
4049
4414
|
throw usageError(`Work Item ${retryItem.id} is not retryable from ${retryItem.status}.`);
|
|
4050
4415
|
}
|
|
@@ -4053,7 +4418,6 @@ function retryRun(args, store, options) {
|
|
|
4053
4418
|
? tx.getTaskWorkspace(task.id)
|
|
4054
4419
|
: tx.getWorkItemWorkspace(task.id, retryItem.id))
|
|
4055
4420
|
?? undefined;
|
|
4056
|
-
const runId = tx.nextAgentRunId(task.id);
|
|
4057
4421
|
const retryGroup = retryItem === null || previous.executionGroupId === undefined
|
|
4058
4422
|
? undefined
|
|
4059
4423
|
: workItemExecutionGroupById(retryItem, previous.executionGroupId);
|
|
@@ -4077,20 +4441,81 @@ function retryRun(args, store, options) {
|
|
|
4077
4441
|
...(retryManagedWorkspace === undefined ? {} : { workspace: retryManagedWorkspace }),
|
|
4078
4442
|
...(retryItem === null ? {} : { workItemWriteProjectIds: retryItem.writeProjectIds })
|
|
4079
4443
|
});
|
|
4444
|
+
const laneWorkspace = retryManagedWorkspace === undefined
|
|
4445
|
+
? undefined
|
|
4446
|
+
: {
|
|
4447
|
+
root: retryManagedWorkspace.root,
|
|
4448
|
+
writableProjectIds: [...(retryItem?.writeProjectIds ?? [])]
|
|
4449
|
+
};
|
|
4450
|
+
const retryDirective = retryLane?.directive
|
|
4451
|
+
?? previous.assignment.directive
|
|
4452
|
+
?? retryItem?.objective
|
|
4453
|
+
?? `Retry Run ${previous.id}.`;
|
|
4454
|
+
let queuedReason;
|
|
4455
|
+
if (retryGroup !== undefined && retryLane !== undefined && retryItem !== null) {
|
|
4456
|
+
const config = tx.getConfig();
|
|
4457
|
+
const controllerConcurrency = resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
|
|
4458
|
+
const capacity = retryGroup.strategy.mode === "fixed"
|
|
4459
|
+
? retryGroup.strategy.count
|
|
4460
|
+
: retryGroup.strategy.max;
|
|
4461
|
+
const policy = resolveResourceBrokerPolicy({
|
|
4462
|
+
maxActiveLanes: controllerConcurrency,
|
|
4463
|
+
maxActiveLanesPerProvider: controllerConcurrency,
|
|
4464
|
+
maxQueuedLanesPerGroup: Math.max(controllerConcurrency, capacity)
|
|
4465
|
+
});
|
|
4466
|
+
if (retryGroup.stage !== undefined) {
|
|
4467
|
+
const stageGroups = retryItem.executionGroups;
|
|
4468
|
+
const resources = projectExecutionStageResources({
|
|
4469
|
+
group: retryGroup,
|
|
4470
|
+
stageGroups,
|
|
4471
|
+
usage: observedExecutionResourceUsage({
|
|
4472
|
+
group: retryGroup,
|
|
4473
|
+
stageGroups,
|
|
4474
|
+
runs: tx.listAgentRuns(task.id),
|
|
4475
|
+
events: tx.listEvents(task.id)
|
|
4476
|
+
}),
|
|
4477
|
+
now
|
|
4478
|
+
});
|
|
4479
|
+
if (executionStageSpendClosed(resources)) {
|
|
4480
|
+
throw usageError(`Execution stage budget/deadline is exhausted; resolve Group ${retryGroup.id} instead of retrying.`);
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
const admission = planResourceAdmissions({
|
|
4484
|
+
policy,
|
|
4485
|
+
active: activeResourceLaneIdentities(tx),
|
|
4486
|
+
queued: projectQueuedResourceLaneIdentities(tx, now),
|
|
4487
|
+
requests: [{
|
|
4488
|
+
taskId: task.id,
|
|
4489
|
+
workItemId: retryItem.id,
|
|
4490
|
+
executionGroupId: retryGroup.id,
|
|
4491
|
+
executionLaneId: retryLane.id,
|
|
4492
|
+
providerId: effective.adapterId,
|
|
4493
|
+
agentId: effective.agentId,
|
|
4494
|
+
...(effective.model === undefined ? {} : { model: effective.model }),
|
|
4495
|
+
requestedAt: now.toISOString()
|
|
4496
|
+
}]
|
|
4497
|
+
})[0];
|
|
4498
|
+
if (admission.decision === "blocked") {
|
|
4499
|
+
throw usageError(`Execution Lane retry queue is full: ${admission.reason}.`);
|
|
4500
|
+
}
|
|
4501
|
+
if (admission.decision === "queued")
|
|
4502
|
+
queuedReason = admission.reason;
|
|
4503
|
+
}
|
|
4504
|
+
const runId = queuedReason === undefined ? tx.nextAgentRunId(task.id) : undefined;
|
|
4080
4505
|
const runningGroup = retryGroup === undefined || retryLane === undefined
|
|
4081
4506
|
? undefined
|
|
4082
|
-
:
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
? {}
|
|
4087
|
-
:
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4507
|
+
: queuedReason === undefined
|
|
4508
|
+
? restartExecutionLane(retryGroup, retryLane.id, {
|
|
4509
|
+
runId: runId,
|
|
4510
|
+
effective,
|
|
4511
|
+
...(laneWorkspace === undefined ? {} : { workspace: laneWorkspace }),
|
|
4512
|
+
directive: retryDirective
|
|
4513
|
+
}, now)
|
|
4514
|
+
: queueExecutionLaneRetry(retryGroup, retryLane.id, {
|
|
4515
|
+
effective,
|
|
4516
|
+
...(laneWorkspace === undefined ? {} : { workspace: laneWorkspace }),
|
|
4517
|
+
directive: retryDirective
|
|
4518
|
+
}, now);
|
|
4094
4519
|
// Restart the bound lane and reopen the failed WorkItem as two ordered
|
|
4095
4520
|
// single-step record revisions, matching the dispatch path. Folding both
|
|
4096
4521
|
// transforms into one save would move the WorkItem two revisions at once,
|
|
@@ -4111,14 +4536,23 @@ function retryRun(args, store, options) {
|
|
|
4111
4536
|
tx.saveWorkItem(task.id, retriedItemWithGroup);
|
|
4112
4537
|
}
|
|
4113
4538
|
}
|
|
4539
|
+
if (queuedReason !== undefined) {
|
|
4540
|
+
return {
|
|
4541
|
+
kind: "queued",
|
|
4542
|
+
taskId: task.id,
|
|
4543
|
+
roleName: role.name,
|
|
4544
|
+
laneId: retryLane.id,
|
|
4545
|
+
reason: queuedReason
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
4114
4548
|
const retrySnapshot = freezeRunContextSnapshot(tx, {
|
|
4115
4549
|
taskId: task.id,
|
|
4116
4550
|
roleName: role.name,
|
|
4117
4551
|
purpose: "execution",
|
|
4118
4552
|
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId })
|
|
4119
|
-
}, now);
|
|
4553
|
+
}, now, "controller", retryGroup?.stage?.contextSnapshotRef);
|
|
4120
4554
|
const assignment = createRunAssignment({
|
|
4121
|
-
runId,
|
|
4555
|
+
runId: runId,
|
|
4122
4556
|
roleName: role.name,
|
|
4123
4557
|
purpose: previous.assignment.purpose,
|
|
4124
4558
|
action: previous.workItemId === undefined ? previous.assignment.action : "repair-work-item",
|
|
@@ -4164,6 +4598,9 @@ function retryRun(args, store, options) {
|
|
|
4164
4598
|
}, now);
|
|
4165
4599
|
return { kind: "run", run: created };
|
|
4166
4600
|
});
|
|
4601
|
+
if (retried.kind === "queued") {
|
|
4602
|
+
return output(`Retry retained pending for ${retried.taskId}/${retried.laneId}: ${retried.reason}\n`);
|
|
4603
|
+
}
|
|
4167
4604
|
notifyMailbox(options.runtime, roleMailbox(retried.run.taskId, retried.run.roleName), retried.run.taskId);
|
|
4168
4605
|
return output(`Retry queued as ${retried.run.id} for ${retried.run.taskId}/${retried.run.roleName}\n`);
|
|
4169
4606
|
}
|
|
@@ -4504,6 +4941,15 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4504
4941
|
const task = requireTask(tx, run.taskId);
|
|
4505
4942
|
if (task.status !== "active")
|
|
4506
4943
|
throw usageError(`Task is not active: ${task.id}.`);
|
|
4944
|
+
if (runOwnsBlockingProviderContinuation(tx.listEvents(task.id), {
|
|
4945
|
+
taskId: run.taskId,
|
|
4946
|
+
roleName: run.roleName,
|
|
4947
|
+
runId: run.id,
|
|
4948
|
+
agentId: run.effective.agentId
|
|
4949
|
+
})) {
|
|
4950
|
+
throw usageError(`Review Run ${run.id} still owns an unsettled Provider continuation; `
|
|
4951
|
+
+ "reconcile it before retrying the Lane.");
|
|
4952
|
+
}
|
|
4507
4953
|
if (taskActor(options, task.id) !== "leader") {
|
|
4508
4954
|
throw usageError("Only the Task Leader may retry a failed final review Run.");
|
|
4509
4955
|
}
|
|
@@ -4515,6 +4961,25 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4515
4961
|
throw usageError(`Review Run ${run.id} is not a Task-final review; request a new WorkItem review `
|
|
4516
4962
|
+ "for a new Candidate.");
|
|
4517
4963
|
}
|
|
4964
|
+
const retryLane = run.executionLaneId === undefined
|
|
4965
|
+
? undefined
|
|
4966
|
+
: round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
|
|
4967
|
+
if (run.executionGroupId !== undefined && (round.executionGroup?.id !== run.executionGroupId
|
|
4968
|
+
|| retryLane === undefined
|
|
4969
|
+
|| retryLane.runId !== run.id
|
|
4970
|
+
|| retryLane.roleName !== run.roleName)) {
|
|
4971
|
+
throw usageError(`Review Run ${run.id} no longer owns its exact Review Lane attempt.`);
|
|
4972
|
+
}
|
|
4973
|
+
const panelGroup = round.executionGroup !== undefined
|
|
4974
|
+
&& (round.executionGroup.lanes.length > 1
|
|
4975
|
+
|| round.executionGroup.strategy.mode === "adaptive");
|
|
4976
|
+
const runningPanelLaneRetry = round.status === "running"
|
|
4977
|
+
&& panelGroup
|
|
4978
|
+
&& retryLane?.status === "failed";
|
|
4979
|
+
if (round.status === "running" && panelGroup && !runningPanelLaneRetry) {
|
|
4980
|
+
throw usageError(`Review Run ${run.id} is not the current failed Lane attempt in running Round ${round.id}.`);
|
|
4981
|
+
}
|
|
4982
|
+
const retryReviewerRoleName = retryLane?.roleName ?? round.reviewerRoleName;
|
|
4518
4983
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
4519
4984
|
if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)) {
|
|
4520
4985
|
throw usageError(`Task final-review contract does not match ReviewRound ${round.id}.`);
|
|
@@ -4539,23 +5004,23 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4539
5004
|
if (laterRound !== undefined && !isSameTaskReviewCandidate(laterRound.taskCandidate, round.taskCandidate)) {
|
|
4540
5005
|
throw usageError(`A newer final Task candidate already has ReviewRound ${laterRound.id}.`);
|
|
4541
5006
|
}
|
|
4542
|
-
const allReviewerRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id).filter((entry) => (entry.reviewerRoleName ===
|
|
5007
|
+
const allReviewerRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id).filter((entry) => (entry.reviewerRoleName === retryReviewerRoleName)));
|
|
4543
5008
|
assertNoConflictingTaskReviewRound(tx.listReviewRounds(task.id), round.id);
|
|
4544
5009
|
const activeRound = allReviewerRounds.find((entry) => (entry.id !== round.id
|
|
4545
5010
|
&& (entry.status === "pending" || entry.status === "running")));
|
|
4546
5011
|
if (activeRound !== undefined) {
|
|
4547
5012
|
throw usageError(`Reviewer already has an active review round for this candidate: ${activeRound.id}.`);
|
|
4548
5013
|
}
|
|
4549
|
-
const reviewerMailboxTarget = roleMailbox(task.id,
|
|
5014
|
+
const reviewerMailboxTarget = roleMailbox(task.id, retryReviewerRoleName);
|
|
4550
5015
|
const reviewerMailbox = tx.getWorkMailbox(reviewerMailboxTarget);
|
|
4551
5016
|
const reviewerPending = reviewerMailbox === null ? null : nextPendingBatch(reviewerMailbox);
|
|
4552
5017
|
const runtimeMailbox = tx.getWorkMailbox(runtimeLifecycleTarget({
|
|
4553
5018
|
scope: "task",
|
|
4554
5019
|
taskId: task.id,
|
|
4555
|
-
roleName:
|
|
5020
|
+
roleName: retryReviewerRoleName
|
|
4556
5021
|
}));
|
|
4557
5022
|
const hasMailboxWork = (mailbox) => (mailbox !== null && workMailboxHasWork(mailbox));
|
|
4558
|
-
const reviewer = requireRole(tx, task.id,
|
|
5023
|
+
const reviewer = requireRole(tx, task.id, retryReviewerRoleName);
|
|
4559
5024
|
const activePointer = tx.getActiveAgentRun(task.id, reviewer.name);
|
|
4560
5025
|
const activeReviewerRuns = tx.listAgentRuns(task.id).filter((entry) => (entry.roleName === reviewer.name && entry.status === "active"));
|
|
4561
5026
|
// Issue 06: a completed same-Round retry is a no-write idempotent result.
|
|
@@ -4571,7 +5036,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4571
5036
|
// Issue 06: a running same Round is reusable only with its exact active
|
|
4572
5037
|
// Run and mailbox execution. A stranded Run (no active pointer) falls
|
|
4573
5038
|
// through and resets the Round after the identity fences below.
|
|
4574
|
-
if (round.status === "running") {
|
|
5039
|
+
if (round.status === "running" && !runningPanelLaneRetry) {
|
|
4575
5040
|
const reviewerRunId = round.reviewerRunId;
|
|
4576
5041
|
const activeMatches = reviewerRunId !== undefined
|
|
4577
5042
|
&& activePointer !== null
|
|
@@ -4640,6 +5105,16 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4640
5105
|
}
|
|
4641
5106
|
settleExactWorkExecution(tx, reviewerMailboxTarget, exactOldRunRef);
|
|
4642
5107
|
}
|
|
5108
|
+
if (runningPanelLaneRetry) {
|
|
5109
|
+
const resetRound = retryRunningReviewExecutionLane(round, retryLane.id, run.id, now);
|
|
5110
|
+
tx.saveReviewRound(task.id, resetRound);
|
|
5111
|
+
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
5112
|
+
runId: run.id,
|
|
5113
|
+
reviewRoundId: round.id,
|
|
5114
|
+
executionLaneId: retryLane.id
|
|
5115
|
+
}, now);
|
|
5116
|
+
return { round: resetRound, previousRun: run, created: true };
|
|
5117
|
+
}
|
|
4643
5118
|
// Terminalize the old stranded Round only after every identity and mailbox
|
|
4644
5119
|
// fence has passed. The outer transaction rolls back if Round creation fails.
|
|
4645
5120
|
let roundToReset = round;
|
|
@@ -4657,7 +5132,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4657
5132
|
// Issue 06: infra retry resets the same semantic Round to pending instead
|
|
4658
5133
|
// of manufacturing a new Round, so Round count and finding identity stay
|
|
4659
5134
|
// stable across execution-attempt failures.
|
|
4660
|
-
const resetRound = retryTaskReviewRound(roundToReset);
|
|
5135
|
+
const resetRound = retryTaskReviewRound(roundToReset, run.executionLaneId);
|
|
4661
5136
|
tx.saveReviewRound(task.id, resetRound);
|
|
4662
5137
|
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
4663
5138
|
runId: run.id,
|
|
@@ -5121,12 +5596,13 @@ function yieldRun(args, store, options) {
|
|
|
5121
5596
|
throw dataError(`Work item not found for run ${active.id}: ${active.workItemId}.`);
|
|
5122
5597
|
const currentGroup = currentWorkItemExecutionGroup(item);
|
|
5123
5598
|
const multiLaneGroup = currentGroup !== undefined
|
|
5124
|
-
&& (currentGroup.
|
|
5599
|
+
&& (currentGroup.stage !== undefined
|
|
5600
|
+
|| currentGroup.lanes.length > 1
|
|
5125
5601
|
|| currentGroup.strategy.mode === "adaptive")
|
|
5126
5602
|
&& currentGroup.resolution === undefined;
|
|
5127
5603
|
if (multiLaneGroup) {
|
|
5128
5604
|
executionGroupPending = true;
|
|
5129
|
-
executionGroupReady = currentGroup.lanes.every(({ status }) => (["yielded", "completed", "failed"].includes(status)));
|
|
5605
|
+
executionGroupReady = currentGroup.lanes.every(({ status }) => (["yielded", "completed", "failed", "skipped"].includes(status)));
|
|
5130
5606
|
if (executionGroupReady) {
|
|
5131
5607
|
recordTaskEvent(tx, task.id, "execution-group-ready", {
|
|
5132
5608
|
workItemId: item.id,
|
|
@@ -5262,7 +5738,7 @@ function leaderYieldReceipt(tx, task, terminal, now) {
|
|
|
5262
5738
|
if (projection === null)
|
|
5263
5739
|
return null;
|
|
5264
5740
|
const disposition = deriveLeaderRunDisposition(projection.status, task.status);
|
|
5265
|
-
const digest = computeActionabilityDigest(collectTaskActionability(tx, task.id));
|
|
5741
|
+
const digest = computeActionabilityDigest(collectTaskActionability(tx, task.id, now));
|
|
5266
5742
|
const waitReason = disposition === "waiting" || disposition === "blocked"
|
|
5267
5743
|
? leaderWaitReason(projection)
|
|
5268
5744
|
: undefined;
|
|
@@ -5465,13 +5941,6 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5465
5941
|
}
|
|
5466
5942
|
throw roleNotFound(round.reviewerRoleName);
|
|
5467
5943
|
}
|
|
5468
|
-
if (tx.getActiveAgentRun(taskId, reviewer.name) !== null) {
|
|
5469
|
-
const message = `Reviewer Role already has an active run: ${reviewer.name}.`;
|
|
5470
|
-
if ((round.scope ?? "work-item") === "task") {
|
|
5471
|
-
throw new TaskFinalReviewDispatchDriftError(message);
|
|
5472
|
-
}
|
|
5473
|
-
throw usageError(message);
|
|
5474
|
-
}
|
|
5475
5944
|
const storedWorkspace = tx.getReviewRoundWorkspace(taskId, round.id);
|
|
5476
5945
|
if (storedWorkspace === null
|
|
5477
5946
|
|| !isDeepStrictEqual(storedWorkspace, round.workspace)
|
|
@@ -5603,17 +6072,35 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5603
6072
|
return laneReviewer;
|
|
5604
6073
|
});
|
|
5605
6074
|
const createdRuns = [];
|
|
6075
|
+
const config = tx.getConfig();
|
|
6076
|
+
const controllerConcurrency = resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
|
|
6077
|
+
const capacity = runningGroup.strategy.mode === "fixed"
|
|
6078
|
+
? runningGroup.strategy.count
|
|
6079
|
+
: runningGroup.strategy.max;
|
|
6080
|
+
const brokerPolicy = resolveResourceBrokerPolicy({
|
|
6081
|
+
maxActiveLanes: controllerConcurrency,
|
|
6082
|
+
maxActiveLanesPerProvider: controllerConcurrency,
|
|
6083
|
+
maxQueuedLanesPerGroup: Math.max(controllerConcurrency, capacity)
|
|
6084
|
+
});
|
|
6085
|
+
const activeResources = activeResourceLaneIdentities(tx);
|
|
6086
|
+
const queuedResources = projectQueuedResourceLaneIdentities(tx, now);
|
|
6087
|
+
const reviewBaseline = dispatchLanes.length === 0
|
|
6088
|
+
? undefined
|
|
6089
|
+
: freezeReviewStageContextSnapshot(tx, {
|
|
6090
|
+
taskId,
|
|
6091
|
+
reviewRoundId: round.id,
|
|
6092
|
+
executionGroupId: runningGroup.id
|
|
6093
|
+
}, now);
|
|
5606
6094
|
for (let index = 0; index < reviewers.length; index += 1) {
|
|
5607
6095
|
const lane = dispatchLanes[index];
|
|
5608
6096
|
const laneReviewer = reviewers[index];
|
|
5609
|
-
const runId = tx.nextAgentRunId(taskId);
|
|
5610
6097
|
const laneManagedWorkspace = runningGroup.lanes.length > 1 || runningGroup.strategy.mode === "adaptive"
|
|
5611
6098
|
? options.executionLaneWorkspaces?.get(lane.id)
|
|
5612
6099
|
: round.workspace;
|
|
5613
6100
|
if (laneManagedWorkspace === undefined && options.yuiHome !== undefined) {
|
|
5614
6101
|
throw usageError(`Review Lane workspace preflight is missing: ${runningGroup.id}/${lane.id}.`);
|
|
5615
6102
|
}
|
|
5616
|
-
const effective = resolveEffectiveLaunch({
|
|
6103
|
+
const effective = lane.effective ?? resolveEffectiveLaunch({
|
|
5617
6104
|
role: laneReviewer,
|
|
5618
6105
|
purpose: "review",
|
|
5619
6106
|
workspace: laneManagedWorkspace ?? round.workspace,
|
|
@@ -5629,6 +6116,40 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5629
6116
|
.filter(({ access }) => access === "write")
|
|
5630
6117
|
.map(({ projectId }) => projectId)
|
|
5631
6118
|
};
|
|
6119
|
+
const request = {
|
|
6120
|
+
taskId,
|
|
6121
|
+
...(item === undefined ? {} : { workItemId: item.id }),
|
|
6122
|
+
executionGroupId: runningGroup.id,
|
|
6123
|
+
executionLaneId: lane.id,
|
|
6124
|
+
providerId: effective.adapterId,
|
|
6125
|
+
agentId: effective.agentId,
|
|
6126
|
+
...(effective.model === undefined ? {} : { model: effective.model }),
|
|
6127
|
+
requestedAt: lane.effective === undefined ? now.toISOString() : lane.updatedAt
|
|
6128
|
+
};
|
|
6129
|
+
const admission = planResourceAdmissions({
|
|
6130
|
+
policy: brokerPolicy,
|
|
6131
|
+
active: activeResources,
|
|
6132
|
+
queued: queuedResources.filter(({ taskId: queuedTaskId, executionGroupId, executionLaneId }) => !(queuedTaskId === request.taskId
|
|
6133
|
+
&& executionGroupId === request.executionGroupId
|
|
6134
|
+
&& executionLaneId === request.executionLaneId)),
|
|
6135
|
+
requests: [request]
|
|
6136
|
+
})[0];
|
|
6137
|
+
if (admission.decision !== "admitted") {
|
|
6138
|
+
if (lane.effective === undefined) {
|
|
6139
|
+
runningGroup = updateExecutionLane(runningGroup, lane.id, {
|
|
6140
|
+
reviewRoundId: round.id,
|
|
6141
|
+
effective,
|
|
6142
|
+
workspace: laneWorkspace
|
|
6143
|
+
}, now);
|
|
6144
|
+
}
|
|
6145
|
+
if (!queuedResources.some(({ taskId: queuedTaskId, executionGroupId, executionLaneId }) => (queuedTaskId === request.taskId
|
|
6146
|
+
&& executionGroupId === request.executionGroupId
|
|
6147
|
+
&& executionLaneId === request.executionLaneId)))
|
|
6148
|
+
queuedResources.push(request);
|
|
6149
|
+
continue;
|
|
6150
|
+
}
|
|
6151
|
+
activeResources.push(request);
|
|
6152
|
+
const runId = tx.nextAgentRunId(taskId);
|
|
5632
6153
|
const assignment = createRunAssignment({
|
|
5633
6154
|
runId,
|
|
5634
6155
|
roleName: laneReviewer.name,
|
|
@@ -5664,7 +6185,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5664
6185
|
const roundWithGroup = round.executionGroup === undefined
|
|
5665
6186
|
? attachReviewExecutionGroup(round, runningGroup)
|
|
5666
6187
|
: updateReviewExecutionGroup(round, runningGroup);
|
|
5667
|
-
const persistedRound = round.status === "pending"
|
|
6188
|
+
const persistedRound = round.status === "pending" && createdRuns.length > 0
|
|
5668
6189
|
? startReviewRound(roundWithGroup, createdRuns[0].id)
|
|
5669
6190
|
: roundWithGroup;
|
|
5670
6191
|
// Save the aggregate before adopting prepared Lane workspaces. The
|
|
@@ -5691,10 +6212,12 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5691
6212
|
purpose: "review",
|
|
5692
6213
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5693
6214
|
reviewRoundId: round.id
|
|
5694
|
-
}, now
|
|
6215
|
+
}, now, "controller", reviewBaseline === undefined
|
|
6216
|
+
? undefined
|
|
6217
|
+
: contextSnapshotRef(reviewBaseline));
|
|
5695
6218
|
const created = withAgentRunContextSnapshot(unboundRun, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
5696
6219
|
createdRuns[index] = created;
|
|
5697
|
-
const laneReviewer =
|
|
6220
|
+
const laneReviewer = requireRole(tx, taskId, unboundRun.roleName);
|
|
5698
6221
|
tx.saveAgentRun(created);
|
|
5699
6222
|
tx.saveActiveAgentRun(created);
|
|
5700
6223
|
tx.saveRole(taskId, updateRoleStatus(laneReviewer, "running", now));
|
|
@@ -5709,7 +6232,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5709
6232
|
for (const run of runs) {
|
|
5710
6233
|
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
5711
6234
|
}
|
|
5712
|
-
return runs[0];
|
|
6235
|
+
return runs[0] ?? null;
|
|
5713
6236
|
}
|
|
5714
6237
|
export function failPendingReviewRound(taskId, reviewRoundId, summary, store, options = {}) {
|
|
5715
6238
|
const now = clock(options);
|
|
@@ -6730,6 +7253,32 @@ function parseExecutionStrategy(value, usage) {
|
|
|
6730
7253
|
}
|
|
6731
7254
|
throw usageError(`Invalid execution strategy: ${value}. Use fixed:<count> or adaptive:<max>.`, usage);
|
|
6732
7255
|
}
|
|
7256
|
+
function parseWorkItemExecutionMode(value, usage) {
|
|
7257
|
+
if (value === undefined)
|
|
7258
|
+
return undefined;
|
|
7259
|
+
const normalized = value.trim().toLowerCase();
|
|
7260
|
+
if (WORK_ITEM_EXECUTION_MODES.includes(normalized))
|
|
7261
|
+
return normalized;
|
|
7262
|
+
throw usageError(`Invalid WorkItem execution mode: ${value}. Use ${WORK_ITEM_EXECUTION_MODES.join(", ")}.`, usage);
|
|
7263
|
+
}
|
|
7264
|
+
function parseOptionalPositiveInteger(value, label, usage) {
|
|
7265
|
+
if (value === undefined)
|
|
7266
|
+
return undefined;
|
|
7267
|
+
const parsed = Number(value);
|
|
7268
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
7269
|
+
throw usageError(`${label} must be a positive integer.`, usage);
|
|
7270
|
+
}
|
|
7271
|
+
return parsed;
|
|
7272
|
+
}
|
|
7273
|
+
function parseOptionalPercentage(value, label, usage) {
|
|
7274
|
+
if (value === undefined)
|
|
7275
|
+
return undefined;
|
|
7276
|
+
const parsed = Number(value);
|
|
7277
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 100) {
|
|
7278
|
+
throw usageError(`${label} must be an integer from 0 to 100.`, usage);
|
|
7279
|
+
}
|
|
7280
|
+
return parsed;
|
|
7281
|
+
}
|
|
6733
7282
|
function sameExecutionStrategy(left, right) {
|
|
6734
7283
|
return left.mode === right.mode
|
|
6735
7284
|
&& (left.mode === "fixed"
|
|
@@ -6737,9 +7286,10 @@ function sameExecutionStrategy(left, right) {
|
|
|
6737
7286
|
: right.mode === "adaptive" && left.max === right.max);
|
|
6738
7287
|
}
|
|
6739
7288
|
function parseExecutionResolutionDecision(value, usage) {
|
|
6740
|
-
if (value === "accept" || value === "reject" || value === "blocked")
|
|
7289
|
+
if (value === "accept" || value === "reject" || value === "retry" || value === "blocked") {
|
|
6741
7290
|
return value;
|
|
6742
|
-
|
|
7291
|
+
}
|
|
7292
|
+
throw usageError("--decision must be accept, reject, retry, or blocked.", usage);
|
|
6743
7293
|
}
|
|
6744
7294
|
function trimmed(value) {
|
|
6745
7295
|
const normalized = value?.trim();
|