@zq-silk/yui 0.13.9 → 0.14.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/ARCHITECTURE.md +69 -428
- package/README.md +108 -298
- package/dist/agent/managedRuntimeEnvironment.js +2 -2
- package/dist/cli/commandCatalog.js +39 -77
- package/dist/cli/interactionCandidates.js +4 -4
- package/dist/cli/interactionPolicy.js +7 -10
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/updateCommand.js +6 -27
- package/dist/cli/updateOrchestrator.js +63 -484
- package/dist/cli/updatePorts.js +15 -333
- package/dist/cli/upgradeCommand.js +8 -65
- package/dist/cli.js +57 -242
- package/dist/commands/configCommands.js +11 -62
- package/dist/commands/executionAuditCommands.js +23 -23
- package/dist/commands/globalRoleCommands.js +2 -7
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/projectCommands.js +5 -5
- package/dist/commands/releaseCommands.js +3 -10
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/sessionCommands.js +2 -2
- package/dist/commands/taskActor.js +31 -39
- package/dist/commands/taskChangeSetCommands.js +5 -12
- package/dist/commands/taskCommands.js +464 -1249
- package/dist/commands/taskContextCommand.js +35 -36
- package/dist/commands/taskExecutionCommands.js +33 -46
- package/dist/commands/taskInputCommands.js +20 -42
- package/dist/commands/taskNextActionCommand.js +9 -9
- package/dist/commands/taskOverviewCommand.js +56 -95
- package/dist/commands/taskRoleRuntimeStatus.js +77 -107
- package/dist/commands/taskUpstreamCommands.js +3 -3
- package/dist/commands/taskWorkspaceCommands.js +7 -79
- package/dist/commands/telemetryCommands.js +32 -124
- package/dist/commands/workflowCommands.js +3 -3
- package/dist/completion/fileCompletionManager.js +1 -1
- package/dist/config/configCatalog.js +4 -6
- package/dist/config/yuiConfig.js +6 -43
- package/dist/context/dispatchContext.js +14 -30
- package/dist/context/roleSessionContext.js +2 -2
- package/dist/context/sessionBootstrapManifest.js +2 -2
- package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
- package/dist/context/turnInputContract.js +204 -0
- package/dist/context/wakeNotification.js +2 -2
- package/dist/controller/agentRuntimeObserver.js +18 -18
- package/dist/controller/clientRuntime.js +13 -20
- package/dist/controller/controller.js +89 -116
- package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
- package/dist/controller/jobControl.js +17 -17
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +38 -59
- package/dist/controller/resourceInventoryRpc.js +3 -3
- package/dist/controller/resourceInventoryWorker.js +1 -1
- package/dist/controller/runtime.js +46 -53
- package/dist/controller/runtimeEventInbox.js +23 -31
- package/dist/controller/runtimeEventProcessor.js +15 -15
- package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
- package/dist/controller/runtimeLaunchCoordinator.js +3 -3
- package/dist/controller/runtimeObservationHook.js +11 -8
- package/dist/controller/sessionNotify.js +11 -11
- package/dist/controller/structuredProviderObservation.js +89 -16
- package/dist/coordination/workMailbox.js +309 -579
- package/dist/coordination/workMailboxQueue.js +5 -6
- package/dist/core/controllerClient.js +1 -91
- package/dist/core/controllerServer.js +3 -6
- package/dist/doctor/doctor.js +51 -314
- package/dist/execution/candidateConvergence.js +3 -3
- package/dist/execution/executionGroup.js +32 -33
- package/dist/execution/executionHealth.js +36 -38
- package/dist/execution/resourceBroker.js +12 -12
- package/dist/executor/agentExecutor.js +30 -265
- package/dist/executor/effectiveLaunch.js +9 -19
- package/dist/executor/executorRegistry.js +43 -17
- package/dist/executor/fileRoleLaunchPlanner.js +56 -56
- package/dist/executor/launchPlan.js +3 -3
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +20 -25
- package/dist/input/inputRequest.js +12 -12
- package/dist/integration/changeSet.js +9 -13
- package/dist/integration/changeSetManifest.js +1 -1
- package/dist/integration/gitIntegrationService.js +2 -2
- package/dist/integration/integrationQueueService.js +5 -21
- package/dist/integration/overlapDiagnostics.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/job/jobRunner.js +1 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
- package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
- package/dist/message/message.js +6 -6
- package/dist/observability/executionAudit.js +74 -86
- package/dist/observability/faultClassification.js +7 -24
- package/dist/observability/orchestrationMetrics.js +5 -5
- package/dist/observability/runtimeIdentity.js +34 -101
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +3 -3
- package/dist/release/releaseWorkflowEngine.js +6 -10
- package/dist/release/releaseWorkflowPorts.js +2 -2
- package/dist/release/workflowFileLock.js +1 -1
- package/dist/repository/checkoutSwap.js +1 -1
- package/dist/repository/gitWorkspace.js +0 -13
- package/dist/repository/project.js +1 -1
- package/dist/repository/projectMaintenanceLock.js +1 -2
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +21 -25
- package/dist/repository/taskWorkspaceIdentity.js +8 -21
- package/dist/repository/taskWorkspacePreparer.js +63 -620
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewFindingLedger.js +1 -1
- package/dist/review/reviewOutcomeClassifier.js +35 -50
- package/dist/review/reviewRound.js +14 -25
- package/dist/review/reviewerAvailability.js +4 -4
- package/dist/review/taskFinalReviewContractResolution.js +41 -0
- package/dist/role/role.js +6 -3
- package/dist/runtime/agentDriver.js +4 -0
- package/dist/runtime/agentHost.js +151 -36
- package/dist/runtime/builtinAgentDrivers.js +79 -2
- package/dist/runtime/codexAppServerRuntime.js +122 -0
- package/dist/runtime/continuationManager.js +3 -5
- package/dist/runtime/exactControlPlane.js +17 -42
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +4 -4
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/processExitOutbox.js +0 -32
- package/dist/runtime/promptEnvelope.js +9 -9
- package/dist/runtime/providerContinuation.js +2 -2
- package/dist/runtime/providerContinuationReconciliationService.js +2 -2
- package/dist/runtime/providerRuntimeIdentity.js +76 -24
- package/dist/runtime/recentTurnIds.js +38 -0
- package/dist/runtime/runtimeContinuationProjection.js +2 -2
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +58 -21
- package/dist/runtime/runtimeProjection.js +19 -27
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/structuredProviderHost.js +177 -12
- package/dist/runtime/tmuxAdapters.js +46 -11
- package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
- package/dist/scheduler/leaderWakeupProcessor.js +147 -445
- package/dist/scheduler/operatorEvent.js +4 -6
- package/dist/scheduler/resourceQueueProjection.js +71 -0
- package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
- package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
- package/dist/scheduler/taskExecutionProjection.js +99 -99
- package/dist/scheduler/taskObservabilityProjection.js +10 -10
- package/dist/scheduler/taskWake.js +4 -4
- package/dist/scheduler/wakeReason.js +6 -10
- package/dist/scheduler/wakeupQueue.js +0 -9
- package/dist/setup/setupCommand.js +2 -2
- package/dist/storage/currentTaskStore.js +46 -0
- package/dist/storage/persistenceWorker.js +1 -1
- package/dist/storage/sqliteSchema.js +81 -485
- package/dist/storage/sqliteStore.js +206 -365
- package/dist/storage/storageSchema.js +1 -15
- package/dist/storage/storageVersions.js +8 -9
- package/dist/storage/storeRpc.js +10 -34
- package/dist/storage/taskStore.js +63 -3415
- package/dist/storage/upgrade/recordVersions.js +55 -149
- package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
- package/dist/task/completionReadiness.js +5 -5
- package/dist/task/deliveryGuard.js +10 -10
- package/dist/task/nextAction.js +74 -77
- package/dist/task/task.js +2 -4
- package/dist/task/taskRecordReference.js +3 -3
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +53 -53
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +8 -14
- package/dist/turn/turn.js +356 -0
- package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
- package/dist/verification/gateArtifactStore.js +2 -2
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +29 -54
- package/dist/web/assets/client/i18n.js +17 -37
- package/dist/web/assets/client/view.js +37 -37
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +5 -5
- package/dist/web/assets/styles/widgets.js +1 -2
- package/dist/web/webSnapshot.js +65 -60
- package/dist/workItem/workItem.js +11 -11
- package/dist/workspace/workItemChangeSetManager.js +1 -1
- package/docs/task-local-identity.md +5 -5
- package/i18n/README.zh-CN.md +41 -54
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +85 -83
- package/skills/yui-operator/SKILL.md +16 -16
- package/skills/yui-reviewer/SKILL.md +10 -10
- package/skills/yui-runtime/SKILL.md +17 -18
- package/skills/yui-worker/SKILL.md +16 -18
- package/dist/context/runContextContract.js +0 -162
- package/dist/executor/turnCompletion.js +0 -126
- package/dist/review/taskFinalReviewContractEvent.js +0 -1
- package/dist/review/taskFinalReviewContractRebind.js +0 -367
- package/dist/run/agentRun.js +0 -356
- package/dist/run/rejectedYieldAttempt.js +0 -221
- package/dist/run/runControlRequest.js +0 -50
- package/dist/run/yieldReceipt.js +0 -65
- package/dist/runtime/turnCompletion.js +0 -3
- package/dist/scheduler/actionability.js +0 -323
- package/dist/scheduler/activeRoleRunDelivery.js +0 -868
- package/dist/scheduler/activeTaskProgress.js +0 -126
- package/dist/storage/compatibleTaskStore.js +0 -248
- package/dist/storage/migration/baseline.js +0 -78
- package/dist/storage/migration/classifier.js +0 -51
- package/dist/storage/migration/compatibleCodec.js +0 -53
- package/dist/storage/migration/engine.js +0 -147
- package/dist/storage/migration/index.js +0 -33
- package/dist/storage/migration/planner.js +0 -154
- package/dist/storage/migration/productionRegistry.js +0 -3931
- package/dist/storage/migration/registry.js +0 -169
- package/dist/storage/migration/report.js +0 -54
- package/dist/storage/migration/types.js +0 -31
- package/dist/storage/upgrade/homeClassification.js +0 -353
- package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
- package/dist/storage/upgrade/migrationReceipt.js +0 -67
- package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
- package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
- package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
- package/dist/storage/upgrade/recordVersionScan.js +0 -176
- package/dist/storage/upgrade/sqliteFileSet.js +0 -50
- package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
- package/dist/storage/upgrade/switchProgress.js +0 -80
- package/dist/storage/upgrade/upgradeReceipt.js +0 -161
- package/dist/storage/upgradeCoordination.js +0 -187
- package/dist/storage/upgradeFence.js +0 -366
- package/dist/telemetry/telemetryCompaction.js +0 -252
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { createTurnInput } from "../context/turnInputContract.js";
|
|
4
|
+
import { buildTurnContextPack, buildTurnContextDelta, contextSnapshotDeltaRefIds, expandTurnContextRef, freezeExecutionStageContextSnapshot, freezeReviewStageContextSnapshot, freezeTurnContextSnapshot } from "../context/turnContextPack.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";
|
|
8
8
|
import { createTaskRecordRetirement, isTaskRecordRetired, taskRecordRetirement } from "../task/taskRecordRetirement.js";
|
|
9
|
-
import {
|
|
10
|
-
import { routeRoleEvent } from "../scheduler/operatorEvent.js";
|
|
9
|
+
import { isRoleTurnStalled, TURN_PROGRESS_EVENT, TURN_RECOVERED_EVENT } from "../scheduler/roleTurnStall.js";
|
|
11
10
|
import { readCommandText } from "./textInput.js";
|
|
12
11
|
import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
|
|
13
|
-
import { createRoleSessionSet, roleAgentSessionResumeMode,
|
|
12
|
+
import { createRoleSessionSet, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
14
13
|
import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
|
|
15
14
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
16
15
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
@@ -18,12 +17,10 @@ import { formatTimestamp } from "../output/timePresentation.js";
|
|
|
18
17
|
import { renderRoleDetails } from "../output/rolePresentation.js";
|
|
19
18
|
import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js";
|
|
20
19
|
import { cancelInputRequest } from "../input/inputRequest.js";
|
|
21
|
-
import {
|
|
20
|
+
import { retireExactActiveTurn, terminalizeExactTaskTurn, validateExactTurnReviewRound } from "../lifecycle/exactTurnTerminalization.js";
|
|
22
21
|
import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole } from "../role/role.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { createRejectedYieldAttempt, rejectedYieldAttemptEventPayload, rejectedYieldAttemptFromTaskEvent, RUN_YIELD_REJECTED_EVENT } from "../run/rejectedYieldAttempt.js";
|
|
26
|
-
import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryRunningReviewExecutionLane, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
|
|
22
|
+
import { createTurn, withTurnContextSnapshot } from "../turn/turn.js";
|
|
23
|
+
import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, recordReviewWorkspaceDisposition, retryRunningReviewExecutionLane, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
|
|
27
24
|
import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../review/deltaRecheck.js";
|
|
28
25
|
import { isAcceptedTaskReviewBaseline } from "../review/reviewAcceptance.js";
|
|
29
26
|
import { projectReviewerAvailability } from "../review/reviewerAvailability.js";
|
|
@@ -35,14 +32,13 @@ import { createMilestone } from "../milestone/milestone.js";
|
|
|
35
32
|
import { runPublicationCommand } from "./taskPublicationCommands.js";
|
|
36
33
|
import { enqueueWork, settleExactWorkExecution } from "../coordination/workMailboxQueue.js";
|
|
37
34
|
import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
|
|
38
|
-
import {
|
|
35
|
+
import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
39
36
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
40
37
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
41
38
|
import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
42
39
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
43
40
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
44
41
|
import { assertProjectActive, resolveProject } from "../repository/project.js";
|
|
45
|
-
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
46
42
|
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, planWorkItemExplorationStage, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, retryFailedWorkItem, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
47
43
|
import { addExecutionLane, createExecutionGroup, queueExecutionLaneRetry, resolveExecutionGroup, restartExecutionLane, skipPendingExecutionLanes, updateExecutionLane, WORK_ITEM_EXECUTION_MODES } from "../execution/executionGroup.js";
|
|
48
44
|
import { DEFAULT_EXECUTION_STAGE_TOKEN_BUDGET_PER_LANE, executionStageSpendClosed, observedExecutionResourceUsage, planResourceAdmissions, projectExecutionStageResources, resolveResourceBrokerPolicy, routeExecutionStage } from "../execution/resourceBroker.js";
|
|
@@ -50,7 +46,7 @@ import { resolveControllerTaskConcurrency, resolveRuntimeHealth } from "../confi
|
|
|
50
46
|
import { assertIndependentVerificationRoles, candidateConvergenceEvidenceSufficient, candidateConvergenceDirective, validateCandidateConvergenceResolution } from "../execution/candidateConvergence.js";
|
|
51
47
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
52
48
|
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
53
|
-
import {
|
|
49
|
+
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractResolution.js";
|
|
54
50
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
55
51
|
import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
|
|
56
52
|
import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
|
|
@@ -58,16 +54,15 @@ import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
|
|
|
58
54
|
import { runTaskContextCommand } from "./taskContextCommand.js";
|
|
59
55
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
60
56
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
61
|
-
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel,
|
|
62
|
-
import { assertNoOpenInputRequests,
|
|
57
|
+
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastTurnLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
58
|
+
import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
63
59
|
import { runGrantCommand } from "./grantCommands.js";
|
|
64
60
|
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
65
|
-
import { taskLocalActor as resolveTaskLocalActor,
|
|
61
|
+
import { taskLocalActor as resolveTaskLocalActor, taskLeaderActionTurnId } from "./taskActor.js";
|
|
66
62
|
import { enqueueOperatorEvent } from "../scheduler/operatorEvent.js";
|
|
67
63
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
68
64
|
import { renderWakeReason, wakeReason } from "../scheduler/wakeReason.js";
|
|
69
|
-
import {
|
|
70
|
-
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
65
|
+
import { projectQueuedResourceLaneIdentities } from "../scheduler/resourceQueueProjection.js";
|
|
71
66
|
import { buildTaskOverview, parseTaskListOptions, renderTaskOverview } from "./taskOverviewCommand.js";
|
|
72
67
|
const LEADER_ROLE = "leader";
|
|
73
68
|
/** Exact Task-final identity or evidence changed between queueing and dispatch. */
|
|
@@ -82,7 +77,7 @@ function taskFinalReviewDispatchDrift(error) {
|
|
|
82
77
|
return new TaskFinalReviewDispatchDriftError(error instanceof Error ? error.message : String(error), error instanceof CliError ? error : undefined);
|
|
83
78
|
}
|
|
84
79
|
/** `final` is a Task delivery policy, not a per-WorkItem ReviewRound rule. */
|
|
85
|
-
function
|
|
80
|
+
function workItemReviewConfig(config) {
|
|
86
81
|
return config?.trigger === "final" ? null : config;
|
|
87
82
|
}
|
|
88
83
|
function storedTaskFinalReviewContract(store, taskId) {
|
|
@@ -90,7 +85,7 @@ function storedTaskFinalReviewContract(store, taskId) {
|
|
|
90
85
|
}
|
|
91
86
|
function storedTaskFinalReviewContractResolution(store, taskId) {
|
|
92
87
|
try {
|
|
93
|
-
return resolveRecordedTaskFinalReviewContract(taskId, store.listWorkItems(taskId), store.listReviewRounds(taskId)
|
|
88
|
+
return resolveRecordedTaskFinalReviewContract(taskId, store.listWorkItems(taskId), store.listReviewRounds(taskId));
|
|
94
89
|
}
|
|
95
90
|
catch (error) {
|
|
96
91
|
throw dataError(error instanceof Error
|
|
@@ -119,8 +114,8 @@ function taskFinalReviewContractForMutation(store, taskId, options) {
|
|
|
119
114
|
}
|
|
120
115
|
if (stored === undefined)
|
|
121
116
|
return supplied;
|
|
122
|
-
//
|
|
123
|
-
//
|
|
117
|
+
// The exact contract expresses a durable Reviewer policy. The persisted
|
|
118
|
+
// contract remains the audit authority, but ordinary
|
|
124
119
|
// protocol/storage-compatible CLIs may continue the Task without presenting
|
|
125
120
|
// a release-bound capability on every mutation.
|
|
126
121
|
if (supplied === undefined)
|
|
@@ -236,7 +231,7 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
236
231
|
case "role": return taskRoleCommand(rest, store, options);
|
|
237
232
|
case "work": return taskWorkCommand(rest, store, options);
|
|
238
233
|
case "review": return taskReviewCommand(rest, store, options);
|
|
239
|
-
case "
|
|
234
|
+
case "turn": return taskTurnCommand(rest, store, options);
|
|
240
235
|
case "brief": return taskBriefCommand(rest, store, options);
|
|
241
236
|
case "decision": return taskDecisionCommand(rest, store, options);
|
|
242
237
|
case "milestone": return taskMilestoneCommand(rest, store, options);
|
|
@@ -317,7 +312,7 @@ export function normalizedExecutionLanePlan(input) {
|
|
|
317
312
|
return { expanding, resumingQueued, roles, strategy, capacity, requestedCount, laneIds };
|
|
318
313
|
}
|
|
319
314
|
function activeResourceLaneIdentities(store) {
|
|
320
|
-
return store.listActiveTaskIds().flatMap((taskId) => (store.
|
|
315
|
+
return store.listActiveTaskIds().flatMap((taskId) => (store.listTurns(taskId).flatMap((run) => {
|
|
321
316
|
if (run.status !== "active"
|
|
322
317
|
|| run.executionGroupId === undefined
|
|
323
318
|
|| run.executionLaneId === undefined)
|
|
@@ -455,14 +450,6 @@ function updateTaskCommand(args, store, options) {
|
|
|
455
450
|
notifyMailbox(options.runtime, taskMailbox(result.id), result.id);
|
|
456
451
|
return `Updated task ${result.id}\n`;
|
|
457
452
|
}
|
|
458
|
-
/** Compatibility helper for call sites that cannot yet handle foreground enter. */
|
|
459
|
-
export function runTaskOutputCommand(args, store, options = {}) {
|
|
460
|
-
const execution = runTaskCommand(args, store, options);
|
|
461
|
-
if (execution.kind !== "output") {
|
|
462
|
-
throw runtimeError("Task Role foreground runtime control requires the CLI.");
|
|
463
|
-
}
|
|
464
|
-
return execution.output;
|
|
465
|
-
}
|
|
466
453
|
export function submitOperatorMessage(body, taskId, store, options = {}) {
|
|
467
454
|
const now = clock(options);
|
|
468
455
|
const result = store.transaction((tx) => {
|
|
@@ -601,7 +588,7 @@ function showTaskCommand(args, store) {
|
|
|
601
588
|
milestones: milestones.length,
|
|
602
589
|
events: events.length,
|
|
603
590
|
workItems: work.length,
|
|
604
|
-
|
|
591
|
+
turns: store.listTurns(task.id).length,
|
|
605
592
|
changeSets: changeSets.length,
|
|
606
593
|
integrations: integrations.length,
|
|
607
594
|
publications: publications.length,
|
|
@@ -640,7 +627,7 @@ function showTaskCommand(args, store) {
|
|
|
640
627
|
`Milestones: ${counts.milestones}`,
|
|
641
628
|
`Events: ${counts.events}`,
|
|
642
629
|
`Work items: ${counts.workItems}`,
|
|
643
|
-
`
|
|
630
|
+
`Turns: ${counts.turns}`,
|
|
644
631
|
`ChangeSets: ${counts.changeSets}`,
|
|
645
632
|
`Integration Attempts: ${counts.integrations}`,
|
|
646
633
|
`Publication references: ${counts.publications} (${verifiedMergedPublications} verified merged)`,
|
|
@@ -700,7 +687,6 @@ function completeTaskCommand(args, store, options) {
|
|
|
700
687
|
return {
|
|
701
688
|
task,
|
|
702
689
|
changed: false,
|
|
703
|
-
runtimeCleanupTargets: [],
|
|
704
690
|
completionAdvisories: [],
|
|
705
691
|
finalReview: undefined
|
|
706
692
|
};
|
|
@@ -722,15 +708,13 @@ function completeTaskCommand(args, store, options) {
|
|
|
722
708
|
return {
|
|
723
709
|
task,
|
|
724
710
|
changed: false,
|
|
725
|
-
runtimeCleanupTargets: [],
|
|
726
711
|
completionAdvisories: [],
|
|
727
712
|
finalReview,
|
|
728
713
|
};
|
|
729
714
|
}
|
|
730
|
-
// Completion is a
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
const runtimeCleanupTargets = settleTaskExecutionForCompletion(tx, task.id, roles, summary, now);
|
|
715
|
+
// Completion is a Task decision only. The current Provider Turn remains
|
|
716
|
+
// responsible for closing its own Turn, and the reusable Session keeps
|
|
717
|
+
// its independent lifecycle.
|
|
734
718
|
// Issue 06: re-validate the full completion readiness inside the
|
|
735
719
|
// transaction (the CAS fence) after final-review preparation. This is the
|
|
736
720
|
// same pure projection `task next-action` displays, now with the finding
|
|
@@ -781,7 +765,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
781
765
|
// discarded at this lifecycle boundary.
|
|
782
766
|
tx.removeWorkMailbox(taskMailbox(task.id));
|
|
783
767
|
// Role mailboxes are also derived wake state. A Worker result or runtime
|
|
784
|
-
// signal queued while the final
|
|
768
|
+
// signal queued while the final Turn was being settled must not survive a
|
|
785
769
|
// completed Task and become actionable after a later explicit reopen.
|
|
786
770
|
for (const role of roles) {
|
|
787
771
|
tx.removeWorkMailbox(roleMailbox(task.id, role.name));
|
|
@@ -789,17 +773,11 @@ function completeTaskCommand(args, store, options) {
|
|
|
789
773
|
return {
|
|
790
774
|
task: completed,
|
|
791
775
|
changed: true,
|
|
792
|
-
runtimeCleanupTargets,
|
|
793
776
|
completionAdvisories: readiness.advisories,
|
|
794
777
|
finalReview: undefined
|
|
795
778
|
};
|
|
796
779
|
});
|
|
797
780
|
if (result.changed) {
|
|
798
|
-
for (const target of result.runtimeCleanupTargets) {
|
|
799
|
-
// Cleanup owns an independent lifecycle lane. Never fall back to the
|
|
800
|
-
// Task signal for older runtime ports: a completed Task must not wake.
|
|
801
|
-
options.runtime?.notifyMailboxChanged?.(target);
|
|
802
|
-
}
|
|
803
781
|
notifyMailbox(options.runtime, { kind: "operator" }, result.task.id);
|
|
804
782
|
}
|
|
805
783
|
if (result.finalReview !== undefined) {
|
|
@@ -826,45 +804,29 @@ function completeTaskCommand(args, store, options) {
|
|
|
826
804
|
function reopenTaskCommand(args, store, options) {
|
|
827
805
|
exactPositionals(args, 1, "Task reopen usage: yui task reopen <id>.");
|
|
828
806
|
const now = clock(options);
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
throw usageError(`Task is not completed: ${task.id}.`);
|
|
849
|
-
const active = reopenTask(task, now);
|
|
850
|
-
tx.saveTask(active);
|
|
851
|
-
const reopenedReason = wakeReason("task-reopened");
|
|
852
|
-
enqueueWork(tx, leaderMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
853
|
-
enqueueWork(tx, taskMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
854
|
-
recordTaskEvent(tx, task.id, "task.reopened", { status: active.status }, now);
|
|
855
|
-
return { task: active, changed: true };
|
|
856
|
-
});
|
|
857
|
-
if (result.changed) {
|
|
858
|
-
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
859
|
-
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
860
|
-
}
|
|
861
|
-
return result.changed
|
|
862
|
-
? `Reopened task ${result.task.id}\n`
|
|
863
|
-
: `Task ${result.task.id} is already active\n`;
|
|
864
|
-
}
|
|
865
|
-
finally {
|
|
866
|
-
release();
|
|
807
|
+
const result = store.transaction((tx) => {
|
|
808
|
+
const task = requireTask(tx, args[0]);
|
|
809
|
+
if (task.status === "active")
|
|
810
|
+
return { task, changed: false };
|
|
811
|
+
if (task.status === "archived")
|
|
812
|
+
throw usageError(`Task is archived: ${task.id}.`);
|
|
813
|
+
if (task.status !== "completed")
|
|
814
|
+
throw usageError(`Task is not completed: ${task.id}.`);
|
|
815
|
+
const active = reopenTask(task, now);
|
|
816
|
+
tx.saveTask(active);
|
|
817
|
+
const reopenedReason = wakeReason("task-reopened");
|
|
818
|
+
enqueueWork(tx, leaderMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
819
|
+
enqueueWork(tx, taskMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
820
|
+
recordTaskEvent(tx, task.id, "task.reopened", { status: active.status }, now);
|
|
821
|
+
return { task: active, changed: true };
|
|
822
|
+
});
|
|
823
|
+
if (result.changed) {
|
|
824
|
+
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
825
|
+
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
867
826
|
}
|
|
827
|
+
return result.changed
|
|
828
|
+
? `Reopened task ${result.task.id}\n`
|
|
829
|
+
: `Task ${result.task.id} is already active\n`;
|
|
868
830
|
}
|
|
869
831
|
function archiveTaskCommand(args, store, options) {
|
|
870
832
|
const request = validateTaskArchiveRequest(args, store, options);
|
|
@@ -895,11 +857,20 @@ function archiveTaskCommand(args, store, options) {
|
|
|
895
857
|
throw usageError(`Task ${task.id} still has managed worktrees; clean them before archiving.`);
|
|
896
858
|
}
|
|
897
859
|
const activeRole = tx.listRoles(task.id)
|
|
898
|
-
.find((role) => tx.
|
|
860
|
+
.find((role) => tx.getActiveTurn(task.id, role.name) !== null);
|
|
899
861
|
if (activeRole !== undefined) {
|
|
900
|
-
throw usageError(`Task ${task.id} still has an active
|
|
862
|
+
throw usageError(`Task ${task.id} still has an active Turn for Role ${activeRole.name}; `
|
|
901
863
|
+ "stop its runtime before archiving.");
|
|
902
864
|
}
|
|
865
|
+
const liveSessionRole = tx.listRoles(task.id).find((role) => {
|
|
866
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
867
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
868
|
+
return session !== undefined && session.status !== "ended";
|
|
869
|
+
});
|
|
870
|
+
if (liveSessionRole !== undefined) {
|
|
871
|
+
throw usageError(`Task ${task.id} still has a live Session for Role ${liveSessionRole.name}; `
|
|
872
|
+
+ "stop that Session before archiving.");
|
|
873
|
+
}
|
|
903
874
|
const archived = archiveTask(task, now, { by: actor });
|
|
904
875
|
tx.saveTask(archived);
|
|
905
876
|
tx.clearPendingWakeup(task.id);
|
|
@@ -960,13 +931,12 @@ function retireTaskCommand(args, store, options) {
|
|
|
960
931
|
throw usageError(`Task ${task.id} has an active DurableJob: ${activeRetireJob.id}/${activeRetireJob.status}.`);
|
|
961
932
|
}
|
|
962
933
|
assertTaskRetirementProof(tx, task, options.taskRetirementProof);
|
|
963
|
-
for (const run of tx.
|
|
964
|
-
const terminal =
|
|
934
|
+
for (const run of tx.listTurns(task.id).filter(({ status: runStatus }) => (runStatus === "active"))) {
|
|
935
|
+
const terminal = terminalizeExactTaskTurn(tx, {
|
|
965
936
|
taskId: task.id,
|
|
966
937
|
roleName: run.roleName,
|
|
967
938
|
agentId: run.effective.agentId,
|
|
968
|
-
|
|
969
|
-
receiptId: agentRunDeliveryReceiptId(run),
|
|
939
|
+
turnId: run.id,
|
|
970
940
|
mailboxDisposition: "discard",
|
|
971
941
|
outcome: {
|
|
972
942
|
status: "failed",
|
|
@@ -974,7 +944,7 @@ function retireTaskCommand(args, store, options) {
|
|
|
974
944
|
}
|
|
975
945
|
}, now);
|
|
976
946
|
if (terminal.disposition !== "applied") {
|
|
977
|
-
throw usageError(`Task
|
|
947
|
+
throw usageError(`Task Turn changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
978
948
|
}
|
|
979
949
|
}
|
|
980
950
|
for (const item of tx.listWorkItems(task.id)) {
|
|
@@ -1080,8 +1050,8 @@ function reconcileTaskCommand(args, store, options) {
|
|
|
1080
1050
|
function taskMessageCommand(args, store, options) {
|
|
1081
1051
|
const [command, ...rest] = args;
|
|
1082
1052
|
if (command === "send") {
|
|
1083
|
-
const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->) [--wake-policy leader|none]
|
|
1084
|
-
const parsed = parseTail(rest, new Set(["--body-file", "--wake-policy"
|
|
1053
|
+
const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->) [--wake-policy leader|none].";
|
|
1054
|
+
const parsed = parseTail(rest, new Set(["--body-file", "--wake-policy"]), usage);
|
|
1085
1055
|
if (parsed.positionals.length < 1 || parsed.positionals.length > 2)
|
|
1086
1056
|
throw usageError(usage);
|
|
1087
1057
|
const body = readCommandText(parsed.positionals[1], parsed.options.get("--body-file"), "--body", usage);
|
|
@@ -1096,13 +1066,6 @@ function taskMessageCommand(args, store, options) {
|
|
|
1096
1066
|
else {
|
|
1097
1067
|
throw usageError(`--wake-policy must be 'leader' or 'none': ${wakePolicyRaw}.`);
|
|
1098
1068
|
}
|
|
1099
|
-
const deliveryModeRaw = parsed.options.get("--delivery-mode") ?? "followup";
|
|
1100
|
-
if (deliveryModeRaw !== "followup" && deliveryModeRaw !== "steer") {
|
|
1101
|
-
throw usageError(`--delivery-mode must be 'followup' or 'steer': ${deliveryModeRaw}.`);
|
|
1102
|
-
}
|
|
1103
|
-
if (deliveryModeRaw === "steer" && wakePolicy === "none") {
|
|
1104
|
-
throw usageError("--delivery-mode steer cannot be combined with --wake-policy none.");
|
|
1105
|
-
}
|
|
1106
1069
|
const now = clock(options);
|
|
1107
1070
|
const result = store.transaction((tx) => {
|
|
1108
1071
|
const task = requireTask(tx, parsed.positionals[0]);
|
|
@@ -1119,19 +1082,10 @@ function taskMessageCommand(args, store, options) {
|
|
|
1119
1082
|
if (task.status === "active"
|
|
1120
1083
|
&& actor !== "leader"
|
|
1121
1084
|
&& wakePolicy !== "none") {
|
|
1122
|
-
enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [messageRef(task.id, message.id)],
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
deliveryMode: "steer-if-safe",
|
|
1127
|
-
lane: "user-correction"
|
|
1128
|
-
}
|
|
1129
|
-
: {
|
|
1130
|
-
source: actor,
|
|
1131
|
-
dedupeKey: `message:${task.id}:${message.id}`,
|
|
1132
|
-
deliveryMode: "followup",
|
|
1133
|
-
lane: "normal"
|
|
1134
|
-
});
|
|
1085
|
+
enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [messageRef(task.id, message.id)], {
|
|
1086
|
+
source: actor,
|
|
1087
|
+
dedupeKey: `message:${task.id}:${message.id}`
|
|
1088
|
+
});
|
|
1135
1089
|
}
|
|
1136
1090
|
return { task, message, actor };
|
|
1137
1091
|
});
|
|
@@ -1317,14 +1271,16 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1317
1271
|
const now = clock(options);
|
|
1318
1272
|
const request = store.transaction((tx) => {
|
|
1319
1273
|
const task = requireTask(tx, parsed.positionals[0]);
|
|
1320
|
-
|
|
1274
|
+
if (task.status !== "active" && task.status !== "completed") {
|
|
1275
|
+
throw usageError(`Task Role Session stop requires an active or completed Task: ${task.id}.`, usage);
|
|
1276
|
+
}
|
|
1321
1277
|
const actor = taskActor(tx, options, task.id);
|
|
1322
1278
|
const role = requireRole(tx, task.id, parsed.positionals[1]);
|
|
1323
1279
|
if (actor === "leader" && role.name === LEADER_ROLE) {
|
|
1324
1280
|
throw usageError("A Leader cannot stop the Session executing its own current command.", usage);
|
|
1325
1281
|
}
|
|
1326
|
-
if (tx.
|
|
1327
|
-
throw usageError(`Task Role has an active
|
|
1282
|
+
if (tx.getActiveTurn(task.id, role.name) !== null) {
|
|
1283
|
+
throw usageError(`Task Role has an active Turn; settle or retire it before stopping the Session: ${task.id}/${role.name}.`, usage);
|
|
1328
1284
|
}
|
|
1329
1285
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1330
1286
|
const session = sessions?.sessions[sessions.activeAgentId];
|
|
@@ -1454,7 +1410,7 @@ function listTaskRoles(args, store, options) {
|
|
|
1454
1410
|
{ header: "Health", minWidth: 6, maxWidth: 15 },
|
|
1455
1411
|
{ header: "Open input", minWidth: 5, maxWidth: 10 },
|
|
1456
1412
|
{ header: "Active work", minWidth: 10, maxWidth: 34 },
|
|
1457
|
-
{ header: "Last
|
|
1413
|
+
{ header: "Last turn", minWidth: 10, maxWidth: 28 },
|
|
1458
1414
|
{ header: "Native session", minWidth: 10, maxWidth: 28 },
|
|
1459
1415
|
{ header: "tmux", minWidth: 6, maxWidth: 22 }
|
|
1460
1416
|
], statuses.map((status) => [
|
|
@@ -1463,7 +1419,7 @@ function listTaskRoles(args, store, options) {
|
|
|
1463
1419
|
status.health,
|
|
1464
1420
|
taskRoleOpenInputLabel(status),
|
|
1465
1421
|
taskRoleActiveWorkLabel(status),
|
|
1466
|
-
|
|
1422
|
+
taskRoleLastTurnLabel(status),
|
|
1467
1423
|
taskRoleNativeSessionLabel(status),
|
|
1468
1424
|
taskRoleTmuxLabel(status)
|
|
1469
1425
|
]), defaultTableWidth())}\n`, { roles: statuses });
|
|
@@ -1562,8 +1518,8 @@ function removeTaskRole(args, store, options) {
|
|
|
1562
1518
|
taskId: task.id,
|
|
1563
1519
|
roleName: role.name
|
|
1564
1520
|
}, "removal");
|
|
1565
|
-
if (tx.
|
|
1566
|
-
throw usageError(`Task Role has an active
|
|
1521
|
+
if (tx.getActiveTurn(task.id, role.name) !== null) {
|
|
1522
|
+
throw usageError(`Task Role has an active Turn and cannot be removed: ${task.id}/${role.name}.`);
|
|
1567
1523
|
}
|
|
1568
1524
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1569
1525
|
if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status === "active")) {
|
|
@@ -1612,7 +1568,7 @@ function bindTaskRole(args, store, options) {
|
|
|
1612
1568
|
const switched = (() => {
|
|
1613
1569
|
try {
|
|
1614
1570
|
return switchActiveRoleAgent(bound, existing, agent.id, {
|
|
1615
|
-
|
|
1571
|
+
activeTurn: tx.getActiveTurn(task.id, role.name) !== null,
|
|
1616
1572
|
nativeProcessRunning: currentSession !== undefined
|
|
1617
1573
|
&& currentSession.status === "active"
|
|
1618
1574
|
}, now);
|
|
@@ -1705,9 +1661,9 @@ function transferTaskRoleAuthority(args, store, options, action) {
|
|
|
1705
1661
|
throw new Error(`Provider Activation is not live: ${task.id}/${role.name}.`);
|
|
1706
1662
|
}
|
|
1707
1663
|
if (action === "takeover") {
|
|
1708
|
-
const
|
|
1709
|
-
if (
|
|
1710
|
-
throw new Error(`Task Role has no active managed
|
|
1664
|
+
const activeTurn = tx.getActiveTurn(task.id, role.name);
|
|
1665
|
+
if (activeTurn === null) {
|
|
1666
|
+
throw new Error(`Task Role has no active managed Turn for takeover: ${task.id}/${role.name}.`);
|
|
1711
1667
|
}
|
|
1712
1668
|
}
|
|
1713
1669
|
if (action === "takeover"
|
|
@@ -1866,8 +1822,8 @@ function updateWorkScope(args, store, options) {
|
|
|
1866
1822
|
const item = requireWorkItem(tx, parsed.positionals[0], options);
|
|
1867
1823
|
const task = requireTask(tx, item.taskId);
|
|
1868
1824
|
taskActor(tx, options, task.id);
|
|
1869
|
-
if (tx.
|
|
1870
|
-
throw usageError(`Stop the active Work Item
|
|
1825
|
+
if (tx.getActiveTurn(task.id, item.assignee ?? "") !== null) {
|
|
1826
|
+
throw usageError(`Stop the active Work Item Turn before changing scope: ${item.id}.`);
|
|
1871
1827
|
}
|
|
1872
1828
|
const requestedProjectIds = (parsed.multiOptions.get("--project") ?? []).map((reference) => {
|
|
1873
1829
|
const project = resolveProject(task.projectBindings.map(({ projectId }) => requireProject(tx, projectId)), reference);
|
|
@@ -1928,7 +1884,7 @@ function updateWork(args, store, options) {
|
|
|
1928
1884
|
? taskFinalReviewContractForMutation(tx, task.id, options)
|
|
1929
1885
|
: undefined;
|
|
1930
1886
|
const candidatePolicy = taskFinalContract === undefined
|
|
1931
|
-
?
|
|
1887
|
+
? workItemReviewConfig(configuredReview)
|
|
1932
1888
|
: taskFinalReviewConfig(taskFinalContract);
|
|
1933
1889
|
const projectDelivery = task.projectBindings.length > 0
|
|
1934
1890
|
&& current.writeProjectIds.length > 0;
|
|
@@ -1987,14 +1943,10 @@ function updateWork(args, store, options) {
|
|
|
1987
1943
|
};
|
|
1988
1944
|
}
|
|
1989
1945
|
throw usageError(`Assigned Work Item execution cannot use task work update: ${current.id}. `
|
|
1990
|
-
+ "Use dispatch
|
|
1946
|
+
+ "Use dispatch, wait for the Turn result, then let the Task Leader accept or reject it; "
|
|
1991
1947
|
+ "use task work retire for an explicit Leader retirement.");
|
|
1992
1948
|
});
|
|
1993
1949
|
notifyMailbox(options.runtime, taskMailbox(result.item.taskId), result.item.taskId);
|
|
1994
|
-
if (result.reviewDispatch?.run !== null
|
|
1995
|
-
&& result.reviewDispatch?.run !== undefined) {
|
|
1996
|
-
notifyReviewMailbox(options, options.runtime, roleMailbox(result.reviewDispatch.run.taskId, result.reviewDispatch.run.roleName), result.reviewDispatch.run.taskId);
|
|
1997
|
-
}
|
|
1998
1950
|
if (result.item.status === "awaiting_acceptance" && status === "completed") {
|
|
1999
1951
|
const failure = result.reviewDispatch?.round.status === "failed"
|
|
2000
1952
|
? `Review could not start: ${result.reviewDispatch.round.summary}\n`
|
|
@@ -2066,7 +2018,7 @@ function dispatchWork(args, store, options) {
|
|
|
2066
2018
|
: undefined;
|
|
2067
2019
|
if (item.status === "failed" && existingGroup !== undefined) {
|
|
2068
2020
|
throw usageError(`Work Item ${item.id} retains unresolved ExecutionGroup ${existingGroup.id}; `
|
|
2069
|
-
+ "retry the exact failed
|
|
2021
|
+
+ "retry the exact failed Turn so completed Lane results remain reusable.");
|
|
2070
2022
|
}
|
|
2071
2023
|
const expanding = item.status === "running" && existingGroup !== undefined;
|
|
2072
2024
|
const continuingExploration = currentGroup?.stage !== undefined
|
|
@@ -2110,31 +2062,37 @@ function dispatchWork(args, store, options) {
|
|
|
2110
2062
|
if (expanding)
|
|
2111
2063
|
taskActor(tx, options, task.id);
|
|
2112
2064
|
assertWorkItemDependenciesCompleted(tx, item);
|
|
2113
|
-
const
|
|
2114
|
-
|
|
2065
|
+
const leaderOwned = item.assignee === "leader";
|
|
2066
|
+
const workspace = leaderOwned
|
|
2067
|
+
? tx.getTaskWorkspace(task.id)
|
|
2068
|
+
: tx.getWorkItemWorkspace(task.id, item.id);
|
|
2069
|
+
if (!leaderOwned && workspace?.owner.type === "work-item"
|
|
2115
2070
|
&& workspace.owner.workItemId !== item.id) {
|
|
2116
2071
|
throw usageError(`Task Role ${item.assignee} still uses the isolated worktree for ${workspace.owner.workItemId}; `
|
|
2117
2072
|
+ `cleanup ${workspace.owner.workItemId} before dispatching ${item.id}.`);
|
|
2118
2073
|
}
|
|
2119
2074
|
if (task.projectBindings.length > 0) {
|
|
2120
|
-
const
|
|
2121
|
-
&& workspace.owner.
|
|
2075
|
+
const ownsExpectedWorkspace = leaderOwned
|
|
2076
|
+
? workspace?.owner.type === "task" && workspace.owner.taskId === task.id
|
|
2077
|
+
: workspace?.owner.type === "work-item" && workspace.owner.workItemId === item.id;
|
|
2078
|
+
const writable = workspace !== null && ownsExpectedWorkspace
|
|
2122
2079
|
? workspace.entries
|
|
2123
2080
|
.filter(({ access }) => access === "write")
|
|
2124
2081
|
.map(({ projectId }) => projectId)
|
|
2125
2082
|
.sort()
|
|
2126
2083
|
: [];
|
|
2127
|
-
const visible = workspace
|
|
2128
|
-
&& workspace.owner.workItemId === item.id
|
|
2084
|
+
const visible = workspace !== null && ownsExpectedWorkspace
|
|
2129
2085
|
? workspace.entries.map(({ projectId }) => projectId).sort()
|
|
2130
2086
|
: [];
|
|
2131
2087
|
if (!isDeepStrictEqual(writable, [...item.writeProjectIds].sort())
|
|
2132
2088
|
|| !isDeepStrictEqual(visible, task.projectBindings.map(({ projectId }) => projectId).sort())) {
|
|
2133
|
-
throw usageError(
|
|
2089
|
+
throw usageError(leaderOwned
|
|
2090
|
+
? `Leader-owned Work Item ${item.id} must cover the Task main-worktree Project scope.`
|
|
2091
|
+
: `Work Item ${item.id} must be isolated with its approved Project scope before dispatch.`);
|
|
2134
2092
|
}
|
|
2135
2093
|
}
|
|
2136
2094
|
const rawInput = trimmed(parsed.options.get("--input")) ?? item.objective;
|
|
2137
|
-
const runWorkspace = workspace ??
|
|
2095
|
+
const runWorkspace = workspace ?? undefined;
|
|
2138
2096
|
const lanePlan = normalizedExecutionLanePlan({
|
|
2139
2097
|
assignee: item.assignee,
|
|
2140
2098
|
requestedRoles: requestedLaneRoles,
|
|
@@ -2142,7 +2100,7 @@ function dispatchWork(args, store, options) {
|
|
|
2142
2100
|
existingGroup,
|
|
2143
2101
|
resolvedRetryGroup,
|
|
2144
2102
|
status: item.status,
|
|
2145
|
-
nextGroupId: `execution-group-${tx.
|
|
2103
|
+
nextGroupId: `execution-group-${tx.peekNextTurnId(task.id)}`,
|
|
2146
2104
|
retryLaneId: undefined,
|
|
2147
2105
|
phase: "dispatch"
|
|
2148
2106
|
});
|
|
@@ -2171,17 +2129,17 @@ function dispatchWork(args, store, options) {
|
|
|
2171
2129
|
}
|
|
2172
2130
|
const roles = laneRoles.map((name) => requireRole(tx, task.id, name));
|
|
2173
2131
|
const plans = roles.map((role) => {
|
|
2174
|
-
if (tx.
|
|
2175
|
-
throw usageError(`${task.id}/${role.name} already has an active
|
|
2132
|
+
if (tx.getActiveTurn(task.id, role.name) !== null) {
|
|
2133
|
+
throw usageError(`${task.id}/${role.name} already has an active turn.`);
|
|
2176
2134
|
}
|
|
2177
2135
|
return {
|
|
2178
2136
|
role,
|
|
2179
|
-
|
|
2137
|
+
turnId: tx.nextTurnId(task.id)
|
|
2180
2138
|
};
|
|
2181
2139
|
});
|
|
2182
2140
|
let group = existingGroup;
|
|
2183
2141
|
if (group === undefined) {
|
|
2184
|
-
const groupId = `execution-group-${plans[0].
|
|
2142
|
+
const groupId = `execution-group-${plans[0].turnId}`;
|
|
2185
2143
|
const stage = !startingExploration && !continuingExploration
|
|
2186
2144
|
? undefined
|
|
2187
2145
|
: (() => {
|
|
@@ -2248,7 +2206,7 @@ function dispatchWork(args, store, options) {
|
|
|
2248
2206
|
: group.lanes.slice(0, plans.length);
|
|
2249
2207
|
if (lanes.length !== plans.length)
|
|
2250
2208
|
throw new Error(`ExecutionGroup Lane planning drift: ${group.id}.`);
|
|
2251
|
-
const
|
|
2209
|
+
const createdTurns = [];
|
|
2252
2210
|
const queuedAdmissions = [];
|
|
2253
2211
|
let runningGroup = group;
|
|
2254
2212
|
const config = tx.getConfig();
|
|
@@ -2268,7 +2226,7 @@ function dispatchWork(args, store, options) {
|
|
|
2268
2226
|
usage: observedExecutionResourceUsage({
|
|
2269
2227
|
group,
|
|
2270
2228
|
stageGroups: [...item.executionGroups, group],
|
|
2271
|
-
|
|
2229
|
+
turns: tx.listTurns(task.id),
|
|
2272
2230
|
events: tx.listEvents(task.id)
|
|
2273
2231
|
}),
|
|
2274
2232
|
now
|
|
@@ -2348,30 +2306,21 @@ function dispatchWork(args, store, options) {
|
|
|
2348
2306
|
activeResources.push(request);
|
|
2349
2307
|
const runningLane = lane.status === "failed"
|
|
2350
2308
|
? restartExecutionLane(runningGroup, lane.id, {
|
|
2351
|
-
|
|
2309
|
+
turnId: plan.turnId,
|
|
2352
2310
|
effective,
|
|
2353
2311
|
workspace: laneWorkspace,
|
|
2354
2312
|
directive: frozenDirective
|
|
2355
2313
|
}, now)
|
|
2356
2314
|
: updateExecutionLane(runningGroup, lane.id, {
|
|
2357
2315
|
status: "running",
|
|
2358
|
-
|
|
2316
|
+
turnId: plan.turnId,
|
|
2359
2317
|
effective,
|
|
2360
2318
|
workspace: laneWorkspace,
|
|
2361
2319
|
directive: frozenDirective
|
|
2362
2320
|
}, now);
|
|
2363
2321
|
runningGroup = runningLane;
|
|
2364
|
-
const
|
|
2365
|
-
|
|
2366
|
-
roleName: plan.role.name,
|
|
2367
|
-
purpose: "execution",
|
|
2368
|
-
action: item.status === "failed" ? "repair-work-item" : "execute-work-item",
|
|
2369
|
-
subject: {
|
|
2370
|
-
taskId: task.id,
|
|
2371
|
-
workItemId: item.id,
|
|
2372
|
-
executionGroupId: runningGroup.id,
|
|
2373
|
-
executionLaneId: lane.id
|
|
2374
|
-
},
|
|
2322
|
+
const input = createTurnInput({
|
|
2323
|
+
source: { type: "yui", channel: "workitem-dispatch" },
|
|
2375
2324
|
directive: `${frozenDirective}\nFrozen target: ${group.target.fingerprint}.`
|
|
2376
2325
|
+ (group.stage === undefined
|
|
2377
2326
|
? ""
|
|
@@ -2384,7 +2333,7 @@ function dispatchWork(args, store, options) {
|
|
|
2384
2333
|
: `\n${candidateConvergenceDirective(group.stage)}`)),
|
|
2385
2334
|
deltaRefIds: []
|
|
2386
2335
|
});
|
|
2387
|
-
|
|
2336
|
+
createdTurns.push(createTurn(plan.turnId, task.id, plan.role.name, dispatchMode, input, now, {
|
|
2388
2337
|
workItemId: item.id,
|
|
2389
2338
|
executionGroupId: runningGroup.id,
|
|
2390
2339
|
executionLaneId: lane.id,
|
|
@@ -2439,37 +2388,37 @@ function dispatchWork(args, store, options) {
|
|
|
2439
2388
|
tx.saveManagedWorkspace(prepared);
|
|
2440
2389
|
}
|
|
2441
2390
|
}
|
|
2442
|
-
for (let index = 0; index <
|
|
2443
|
-
const
|
|
2444
|
-
const snapshot =
|
|
2391
|
+
for (let index = 0; index < createdTurns.length; index += 1) {
|
|
2392
|
+
const unboundTurn = createdTurns[index];
|
|
2393
|
+
const snapshot = freezeTurnContextSnapshot(tx, {
|
|
2445
2394
|
taskId: task.id,
|
|
2446
|
-
roleName:
|
|
2395
|
+
roleName: unboundTurn.roleName,
|
|
2447
2396
|
purpose: "execution",
|
|
2448
2397
|
workItemId: item.id
|
|
2449
2398
|
}, now, "controller", runningGroup.stage?.contextSnapshotRef);
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2452
|
-
const role = requireRole(tx, task.id,
|
|
2453
|
-
tx.
|
|
2454
|
-
tx.
|
|
2455
|
-
enqueueWork(tx, roleMailbox(task.id, role.name), "
|
|
2456
|
-
|
|
2399
|
+
const turnWithLineage = withTurnContextSnapshot(unboundTurn, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
2400
|
+
createdTurns[index] = turnWithLineage;
|
|
2401
|
+
const role = requireRole(tx, task.id, unboundTurn.roleName);
|
|
2402
|
+
tx.saveTurn(turnWithLineage);
|
|
2403
|
+
tx.saveActiveTurn(turnWithLineage);
|
|
2404
|
+
enqueueWork(tx, roleMailbox(task.id, role.name), "turn-dispatched", now, [
|
|
2405
|
+
turnRef(task.id, turnWithLineage.id),
|
|
2457
2406
|
workItemRef(task.id, item.id)
|
|
2458
2407
|
]);
|
|
2459
|
-
recordTaskEvent(tx, task.id, "
|
|
2408
|
+
recordTaskEvent(tx, task.id, "turn.dispatched", turnLaunchEventPayload(turnWithLineage), now);
|
|
2460
2409
|
}
|
|
2461
|
-
return {
|
|
2410
|
+
return { turns: createdTurns, queuedAdmissions, group: runningGroup };
|
|
2462
2411
|
});
|
|
2463
|
-
for (const
|
|
2464
|
-
notifyMailbox(options.runtime, roleMailbox(
|
|
2412
|
+
for (const turn of dispatch.turns) {
|
|
2413
|
+
notifyMailbox(options.runtime, roleMailbox(turn.taskId, turn.roleName), turn.taskId);
|
|
2465
2414
|
}
|
|
2466
2415
|
const backpressure = dispatch.queuedAdmissions.length === 0
|
|
2467
2416
|
? ""
|
|
2468
2417
|
: `; ${dispatch.queuedAdmissions.length} Lane(s) retained pending by Resource Broker`;
|
|
2469
|
-
const first = dispatch.
|
|
2418
|
+
const first = dispatch.turns[0];
|
|
2470
2419
|
return first === undefined
|
|
2471
2420
|
? `No Lane admitted; ${dispatch.queuedAdmissions.join("; ")}\n`
|
|
2472
|
-
: `Dispatch queued for ${first.taskId}/${first.roleName} (${first.id})${dispatch.
|
|
2421
|
+
: `Dispatch queued for ${first.taskId}/${first.roleName} (${first.id})${dispatch.turns.length > 1 ? `; ${dispatch.turns.length} Lanes` : ""}${backpressure}\n`;
|
|
2473
2422
|
}
|
|
2474
2423
|
function resolveWorkExecutionGroup(args, store, options) {
|
|
2475
2424
|
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>].";
|
|
@@ -2509,7 +2458,7 @@ function resolveWorkExecutionGroup(args, store, options) {
|
|
|
2509
2458
|
}
|
|
2510
2459
|
const acceptedLaneIds = decision === "accept"
|
|
2511
2460
|
? selectedLaneIds ?? group.lanes
|
|
2512
|
-
.filter((lane) => lane.status === "
|
|
2461
|
+
.filter((lane) => lane.status === "completed")
|
|
2513
2462
|
.map(({ id }) => id)
|
|
2514
2463
|
: undefined;
|
|
2515
2464
|
if (earlyStopMarginalValue !== undefined) {
|
|
@@ -2525,7 +2474,7 @@ function resolveWorkExecutionGroup(args, store, options) {
|
|
|
2525
2474
|
usage: observedExecutionResourceUsage({
|
|
2526
2475
|
group,
|
|
2527
2476
|
stageGroups: item.executionGroups,
|
|
2528
|
-
|
|
2477
|
+
turns: tx.listTurns(task.id),
|
|
2529
2478
|
events: tx.listEvents(task.id)
|
|
2530
2479
|
}),
|
|
2531
2480
|
now
|
|
@@ -2594,10 +2543,10 @@ function resolveWorkExecutionGroup(args, store, options) {
|
|
|
2594
2543
|
}
|
|
2595
2544
|
const eligible = resolved.lanes
|
|
2596
2545
|
.filter((lane) => resolved.resolution?.selectedLaneIds.includes(lane.id) ?? false)
|
|
2597
|
-
.map((lane) => lane.
|
|
2598
|
-
.find((run) => run !== null && run !== undefined && run.status === "
|
|
2546
|
+
.map((lane) => lane.turnId === undefined ? null : tx.getTurn(task.id, lane.turnId))
|
|
2547
|
+
.find((run) => run !== null && run !== undefined && run.status === "completed");
|
|
2599
2548
|
if (eligible === undefined) {
|
|
2600
|
-
throw usageError(`ExecutionGroup ${group.id} has no
|
|
2549
|
+
throw usageError(`ExecutionGroup ${group.id} has no completed selected Lane Turn to capture.`);
|
|
2601
2550
|
}
|
|
2602
2551
|
const candidateWorkspace = options.candidateWorkspace;
|
|
2603
2552
|
if (candidateWorkspace === undefined && options.yuiHome !== undefined) {
|
|
@@ -2606,11 +2555,11 @@ function resolveWorkExecutionGroup(args, store, options) {
|
|
|
2606
2555
|
const configuredReview = tx.getReviewConfig();
|
|
2607
2556
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
2608
2557
|
const candidatePolicy = taskFinalContract === undefined
|
|
2609
|
-
?
|
|
2558
|
+
? workItemReviewConfig(configuredReview)
|
|
2610
2559
|
: taskFinalReviewConfig(taskFinalContract);
|
|
2611
2560
|
const candidate = submitWorkItemCandidate(groupedItem, {
|
|
2612
2561
|
summary: resolutionSummary,
|
|
2613
|
-
source: { type: "
|
|
2562
|
+
source: { type: "turn", turnId: eligible.id },
|
|
2614
2563
|
executionGroupId: resolved.id,
|
|
2615
2564
|
executionLaneId: eligible.executionLaneId,
|
|
2616
2565
|
...(candidatePolicy === null ? {} : { reviewPolicy: candidatePolicy }),
|
|
@@ -2627,7 +2576,7 @@ function resolveWorkExecutionGroup(args, store, options) {
|
|
|
2627
2576
|
? queueReviewRound(tx, candidate, candidatePolicy, "policy", now)
|
|
2628
2577
|
: null;
|
|
2629
2578
|
enqueueWork(tx, leaderMailbox(task.id), "candidate-ready", now, [
|
|
2630
|
-
|
|
2579
|
+
turnRef(task.id, eligible.id),
|
|
2631
2580
|
workItemRef(task.id, item.id)
|
|
2632
2581
|
]);
|
|
2633
2582
|
return {
|
|
@@ -2640,9 +2589,6 @@ function resolveWorkExecutionGroup(args, store, options) {
|
|
|
2640
2589
|
if (result.waitingForStragglers) {
|
|
2641
2590
|
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 });
|
|
2642
2591
|
}
|
|
2643
|
-
if (result.reviewDispatch?.run !== null && result.reviewDispatch?.run !== undefined) {
|
|
2644
|
-
notifyReviewMailbox(options, options.runtime, roleMailbox(result.reviewDispatch.run.taskId, result.reviewDispatch.run.roleName), result.reviewDispatch.run.taskId);
|
|
2645
|
-
}
|
|
2646
2592
|
return output(`Resolved ExecutionGroup ${result.group.id} as ${decision}; Work Item ${result.item.id} is ${result.item.status}.\n`, { workItem: result.item, executionGroup: result.group });
|
|
2647
2593
|
}
|
|
2648
2594
|
function aggregateExecutionLaneSummary(leaderSummary, group, selectedLaneIds) {
|
|
@@ -2697,7 +2643,7 @@ function executionTargetForReviewRound(task, round, item, candidate) {
|
|
|
2697
2643
|
kind: taskScope ? "task-final-review" : "work-item",
|
|
2698
2644
|
taskId: task.id,
|
|
2699
2645
|
...(taskScope ? {} : { workItemId: item.id, candidateId: candidate.id }),
|
|
2700
|
-
// A Task-final ReviewRound is itself the immutable semantic target.
|
|
2646
|
+
// A Task-final ReviewRound is itself the immutable semantic target. Turns
|
|
2701
2647
|
// retry that same revision; a changed frozen Task creates a new Round.
|
|
2702
2648
|
revision: taskScope ? 1 : candidate.workItemRevision,
|
|
2703
2649
|
projects,
|
|
@@ -2775,8 +2721,8 @@ function acceptWork(args, store, options) {
|
|
|
2775
2721
|
recordTaskEvent(tx, item.taskId, "work.accepted", {
|
|
2776
2722
|
workItemId: item.id,
|
|
2777
2723
|
candidateId: candidate.id,
|
|
2778
|
-
...(candidate.source.type === "
|
|
2779
|
-
? {
|
|
2724
|
+
...(candidate.source.type === "turn"
|
|
2725
|
+
? { turnId: candidate.source.turnId }
|
|
2780
2726
|
: { workItemRevision: String(candidate.workItemRevision) }),
|
|
2781
2727
|
acceptedBy: actor,
|
|
2782
2728
|
summary,
|
|
@@ -2898,20 +2844,19 @@ function retireWork(args, store, options) {
|
|
|
2898
2844
|
+ `${activeWorkItemJob.id}/${activeWorkItemJob.status}. `
|
|
2899
2845
|
+ "Cancel or acknowledge it before retiring.");
|
|
2900
2846
|
}
|
|
2901
|
-
for (const run of tx.
|
|
2902
|
-
const terminal =
|
|
2847
|
+
for (const run of tx.listTurns(task.id).filter((candidate) => (candidate.status === "active" && candidate.workItemId === item.id))) {
|
|
2848
|
+
const terminal = terminalizeExactTaskTurn(tx, {
|
|
2903
2849
|
taskId: task.id,
|
|
2904
2850
|
roleName: run.roleName,
|
|
2905
2851
|
agentId: run.effective.agentId,
|
|
2906
|
-
|
|
2907
|
-
receiptId: agentRunDeliveryReceiptId(run),
|
|
2852
|
+
turnId: run.id,
|
|
2908
2853
|
outcome: {
|
|
2909
2854
|
status: "failed",
|
|
2910
2855
|
summary: `Work Item retired: ${summary}`
|
|
2911
2856
|
}
|
|
2912
2857
|
}, now);
|
|
2913
2858
|
if (terminal.disposition !== "applied") {
|
|
2914
|
-
throw usageError(`Work Item
|
|
2859
|
+
throw usageError(`Work Item Turn changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
2915
2860
|
}
|
|
2916
2861
|
}
|
|
2917
2862
|
const next = retireWorkItem(item, {
|
|
@@ -3017,19 +2962,16 @@ function reviewWork(args, store, options) {
|
|
|
3017
2962
|
const resourceQueued = activeRound.executionGroup !== undefined
|
|
3018
2963
|
&& activeRound.executionGroup.resolution === undefined
|
|
3019
2964
|
&& activeRound.executionGroup.lanes.some((lane) => (lane.status === "pending"
|
|
3020
|
-
&& lane.
|
|
3021
|
-
if ((activeRound.status === "pending" && activeRound.
|
|
2965
|
+
&& lane.turnId === undefined));
|
|
2966
|
+
if ((activeRound.status === "pending" && activeRound.reviewerTurnId === undefined)
|
|
3022
2967
|
|| (activeRound.status === "running" && resourceQueued)) {
|
|
3023
|
-
return { round: activeRound,
|
|
2968
|
+
return { round: activeRound, resumed: true };
|
|
3024
2969
|
}
|
|
3025
2970
|
throw usageError(`ReviewRound is already active: ${activeRound.id}/${activeRound.status}.`);
|
|
3026
2971
|
}
|
|
3027
2972
|
const queued = queueReviewRound(tx, item, config, requestedBy, now);
|
|
3028
2973
|
return { ...queued, resumed: false };
|
|
3029
2974
|
});
|
|
3030
|
-
if (result.run !== null) {
|
|
3031
|
-
notifyReviewMailbox(options, options.runtime, roleMailbox(result.run.taskId, result.run.roleName), result.run.taskId);
|
|
3032
|
-
}
|
|
3033
2975
|
if (result.resumed) {
|
|
3034
2976
|
return output(`Review request ${result.round.id} has pending Lanes; resuming dispatch.\n`, { reviewRound: result.round });
|
|
3035
2977
|
}
|
|
@@ -3039,8 +2981,8 @@ function reviewWork(args, store, options) {
|
|
|
3039
2981
|
}
|
|
3040
2982
|
/**
|
|
3041
2983
|
* Task-control recovery for a failed Task-final ReviewRound that never
|
|
3042
|
-
* created a Reviewer
|
|
3043
|
-
* that command requires an exact failed
|
|
2984
|
+
* created a Reviewer Turn. This is deliberately separate from `task turn retry`:
|
|
2985
|
+
* that command requires an exact failed Turn and remains the only retry
|
|
3044
2986
|
* path for a failed provider execution. Here the old terminal Round is an
|
|
3045
2987
|
* immutable anchor and one fresh Round is created only after the same frozen
|
|
3046
2988
|
* committed Integration/ChangeSet provenance and Reviewer independence fences
|
|
@@ -3048,8 +2990,6 @@ function reviewWork(args, store, options) {
|
|
|
3048
2990
|
*/
|
|
3049
2991
|
function taskReviewCommand(args, store, options) {
|
|
3050
2992
|
const [command, ...rest] = args;
|
|
3051
|
-
if (command === "rebind")
|
|
3052
|
-
return rebindTaskFinalReviewContract(rest, store, options);
|
|
3053
2993
|
if (command === "request")
|
|
3054
2994
|
return requestTaskReviewRound(rest, store, options);
|
|
3055
2995
|
if (command === "force-fresh")
|
|
@@ -3064,115 +3004,6 @@ function taskReviewCommand(args, store, options) {
|
|
|
3064
3004
|
? "Task review command is required."
|
|
3065
3005
|
: `Unknown command: task review ${command}`);
|
|
3066
3006
|
}
|
|
3067
|
-
export function parseTaskFinalReviewContractRebindRequest(args) {
|
|
3068
|
-
const usage = "Task review rebind usage: yui task review rebind <task> "
|
|
3069
|
-
+ "--from-control <digest> --to-control <digest> "
|
|
3070
|
-
+ "--from-release <release-id> --to-release <release-id>.";
|
|
3071
|
-
const parsed = parseTail(args, new Set(["--from-control", "--to-control", "--from-release", "--to-release"]), usage);
|
|
3072
|
-
exactPositionals(parsed.positionals, 1, usage);
|
|
3073
|
-
return {
|
|
3074
|
-
taskId: parsed.positionals[0],
|
|
3075
|
-
fromControlPlaneDigest: requiredOption(parsed.options, "--from-control"),
|
|
3076
|
-
toControlPlaneDigest: requiredOption(parsed.options, "--to-control"),
|
|
3077
|
-
fromReleaseId: requiredOption(parsed.options, "--from-release"),
|
|
3078
|
-
toReleaseId: requiredOption(parsed.options, "--to-release")
|
|
3079
|
-
};
|
|
3080
|
-
}
|
|
3081
|
-
function rebindTaskFinalReviewContract(args, store, options) {
|
|
3082
|
-
const request = parseTaskFinalReviewContractRebindRequest(args);
|
|
3083
|
-
const environment = options.environment ?? {};
|
|
3084
|
-
const usage = "Task review rebind usage: yui task review rebind <task> "
|
|
3085
|
-
+ "--from-control <digest> --to-control <digest> "
|
|
3086
|
-
+ "--from-release <release-id> --to-release <release-id>.";
|
|
3087
|
-
if (!isCurrentGlobalOperator(store, environment)) {
|
|
3088
|
-
throw usageError("Task-final Review contract rebind requires the authenticated global Operator session.", usage);
|
|
3089
|
-
}
|
|
3090
|
-
const proof = options.taskFinalReviewRebindProof;
|
|
3091
|
-
if (proof === undefined
|
|
3092
|
-
|| proof.schemaVersion !== 1
|
|
3093
|
-
|| proof.taskId !== request.taskId
|
|
3094
|
-
|| proof.fromControlPlaneDigest !== request.fromControlPlaneDigest
|
|
3095
|
-
|| proof.toControlPlaneDigest !== request.toControlPlaneDigest
|
|
3096
|
-
|| proof.fromRelease.releaseId !== request.fromReleaseId
|
|
3097
|
-
|| proof.toRelease.releaseId !== request.toReleaseId) {
|
|
3098
|
-
throw usageError("Task-final Review contract rebind proof is missing or does not match the explicit request.", usage);
|
|
3099
|
-
}
|
|
3100
|
-
const now = clock(options);
|
|
3101
|
-
const result = store.transaction((tx) => {
|
|
3102
|
-
const task = requireTask(tx, request.taskId);
|
|
3103
|
-
if (task.status !== "active") {
|
|
3104
|
-
throw usageError(`Task is not active: ${task.id}.`);
|
|
3105
|
-
}
|
|
3106
|
-
if (task.projectBindings.length === 0) {
|
|
3107
|
-
throw usageError(`Task final-review contract requires a Project-backed Task: ${task.id}.`);
|
|
3108
|
-
}
|
|
3109
|
-
if (!isCurrentGlobalOperator(tx, environment)) {
|
|
3110
|
-
throw usageError("Task-final Review contract rebind Operator identity drifted before commit.");
|
|
3111
|
-
}
|
|
3112
|
-
const resolution = storedTaskFinalReviewContractResolution(tx, task.id);
|
|
3113
|
-
if (resolution === undefined) {
|
|
3114
|
-
throw usageError(`Task final-review contract is missing for ${task.id}.`);
|
|
3115
|
-
}
|
|
3116
|
-
const exactExisting = resolution.rebinds.at(-1);
|
|
3117
|
-
if (resolution.effective.controlPlaneDigest === request.toControlPlaneDigest) {
|
|
3118
|
-
if (exactExisting !== undefined
|
|
3119
|
-
&& exactExisting.fromContract.controlPlaneDigest === request.fromControlPlaneDigest
|
|
3120
|
-
&& exactExisting.toContract.controlPlaneDigest === request.toControlPlaneDigest
|
|
3121
|
-
&& exactExisting.fromRelease.releaseId === request.fromReleaseId
|
|
3122
|
-
&& exactExisting.toRelease.releaseId === request.toReleaseId
|
|
3123
|
-
&& exactExisting.handoverId === proof.handoverId) {
|
|
3124
|
-
return { rebind: exactExisting, changed: false };
|
|
3125
|
-
}
|
|
3126
|
-
throw usageError(`Task final-review contract already targets ${request.toControlPlaneDigest} without the requested proof tuple.`);
|
|
3127
|
-
}
|
|
3128
|
-
if (resolution.effective.controlPlaneDigest !== request.fromControlPlaneDigest) {
|
|
3129
|
-
throw usageError(`Task final-review contract source control-plane digest drifted for ${task.id}.`);
|
|
3130
|
-
}
|
|
3131
|
-
const activeRound = tx.listReviewRounds(task.id).find((round) => ((round.scope ?? "work-item") === "task"
|
|
3132
|
-
&& (round.status === "pending" || round.status === "running")));
|
|
3133
|
-
if (activeRound !== undefined) {
|
|
3134
|
-
throw usageError(`Task final-review contract cannot rebind while ReviewRound ${activeRound.id} is ${activeRound.status}.`);
|
|
3135
|
-
}
|
|
3136
|
-
const activeLeaderRun = tx.getActiveAgentRun(task.id, "leader");
|
|
3137
|
-
if (activeLeaderRun !== null) {
|
|
3138
|
-
throw usageError(`Task final-review contract cannot rebind while Leader Run ${activeLeaderRun.id} is active.`);
|
|
3139
|
-
}
|
|
3140
|
-
const leaderSessions = tx.getTaskRoleSessionSet(task.id, "leader");
|
|
3141
|
-
if (leaderSessions !== null) {
|
|
3142
|
-
if (leaderSessions.inFlight !== null) {
|
|
3143
|
-
throw usageError("Task final-review contract cannot rebind while the Leader runtime has unsettled Run state.");
|
|
3144
|
-
}
|
|
3145
|
-
const liveLeaderSession = Object.values(leaderSessions.sessions).find(({ status }) => status === "active");
|
|
3146
|
-
if (liveLeaderSession !== undefined) {
|
|
3147
|
-
throw usageError(`Task final-review contract cannot rebind while Leader Session ${liveLeaderSession.agentId} is ${liveLeaderSession.status}.`);
|
|
3148
|
-
}
|
|
3149
|
-
if (Object.keys(leaderSessions.sessions).length > 0
|
|
3150
|
-
|| leaderSessions.providerBinding !== null) {
|
|
3151
|
-
tx.saveTaskRoleSessionSet(retireTaskRoleSessionsForWorkspace(leaderSessions, now));
|
|
3152
|
-
}
|
|
3153
|
-
}
|
|
3154
|
-
const rebind = createTaskFinalReviewContractRebind({
|
|
3155
|
-
taskId: task.id,
|
|
3156
|
-
reviewerRoleName: resolution.effective.reviewerRoleName,
|
|
3157
|
-
fromContract: resolution.effective,
|
|
3158
|
-
toControlPlaneDigest: proof.toControlPlaneDigest,
|
|
3159
|
-
fromRelease: proof.fromRelease,
|
|
3160
|
-
toRelease: proof.toRelease,
|
|
3161
|
-
handoverId: proof.handoverId,
|
|
3162
|
-
authorizedBy: `operator:${environment.YUI_AGENT_ID ?? "unknown"}`
|
|
3163
|
-
});
|
|
3164
|
-
const event = recordTaskEventRecord(tx, task.id, TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT, taskFinalReviewContractRebindPayload(rebind), now);
|
|
3165
|
-
enqueueWork(tx, leaderMailbox(task.id), wakeReason("review-contract-rebound", event.id), now, [eventRef(task.id, event.id)]);
|
|
3166
|
-
return { rebind, event, changed: true };
|
|
3167
|
-
});
|
|
3168
|
-
if (result.changed) {
|
|
3169
|
-
notifyMailbox(options.runtime, leaderMailbox(request.taskId), request.taskId);
|
|
3170
|
-
}
|
|
3171
|
-
return output(result.changed
|
|
3172
|
-
? `Rebound Task-final Review contract for ${request.taskId} from `
|
|
3173
|
-
+ `${request.fromControlPlaneDigest} to ${request.toControlPlaneDigest}.\n`
|
|
3174
|
-
: `Task-final Review contract rebind is already recorded for ${request.taskId}.\n`, result);
|
|
3175
|
-
}
|
|
3176
3007
|
function resolveReviewExecutionGroup(args, store, options) {
|
|
3177
3008
|
const usage = "Task review group resolve usage: yui task review group resolve <task>/<review-round> --decision <accept|reject|blocked> --summary <text> [--lane <lane-id> ...].";
|
|
3178
3009
|
if (args[0] !== "resolve")
|
|
@@ -3203,7 +3034,7 @@ function resolveReviewExecutionGroup(args, store, options) {
|
|
|
3203
3034
|
...(decision === "accept"
|
|
3204
3035
|
? {
|
|
3205
3036
|
selectedLaneIds: selectedLaneIds ?? group.lanes
|
|
3206
|
-
.filter((lane) => lane.status === "
|
|
3037
|
+
.filter((lane) => lane.status === "completed")
|
|
3207
3038
|
.map(({ id }) => id)
|
|
3208
3039
|
}
|
|
3209
3040
|
: selectedLaneIds === undefined ? {} : { selectedLaneIds })
|
|
@@ -3323,7 +3154,7 @@ function disposeReviewFindingCommand(args, store, options) {
|
|
|
3323
3154
|
const command = {
|
|
3324
3155
|
disposition,
|
|
3325
3156
|
by: actor === "leader"
|
|
3326
|
-
?
|
|
3157
|
+
? taskLeaderActionTurnId(tx, task.id, options.environment, options.yuiHome) ?? actor
|
|
3327
3158
|
: actor,
|
|
3328
3159
|
...(parsed.options.get("--note") === undefined ? {} : { note: parsed.options.get("--note") }),
|
|
3329
3160
|
...(parsed.options.get("--work-item") === undefined ? {} : { workItemId: parsed.options.get("--work-item") }),
|
|
@@ -3496,7 +3327,7 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3496
3327
|
if (requestedLaneRoles.length === 0) {
|
|
3497
3328
|
if (exact.status === "running"
|
|
3498
3329
|
&& exact.executionGroup?.resolution === undefined
|
|
3499
|
-
&& exact.executionGroup?.lanes.some((lane) => (lane.status === "pending" && lane.
|
|
3330
|
+
&& exact.executionGroup?.lanes.some((lane) => (lane.status === "pending" && lane.turnId === undefined)))
|
|
3500
3331
|
return exact;
|
|
3501
3332
|
assertTaskReviewRequestLane(tx, task.id, reviewerRoleName, exact);
|
|
3502
3333
|
return exact;
|
|
@@ -3525,7 +3356,7 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3525
3356
|
for (const laneRoleName of laneRoles) {
|
|
3526
3357
|
const existingLane = exact.executionGroup.lanes.find(({ roleName }) => (roleName === laneRoleName));
|
|
3527
3358
|
if (existingLane !== undefined) {
|
|
3528
|
-
if (existingLane.
|
|
3359
|
+
if (existingLane.turnId === undefined)
|
|
3529
3360
|
continue;
|
|
3530
3361
|
if (existingLane.status !== "running")
|
|
3531
3362
|
continue;
|
|
@@ -3651,7 +3482,7 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3651
3482
|
});
|
|
3652
3483
|
if ("kind" in round && round.kind === "busy") {
|
|
3653
3484
|
return output(`Reviewer ${round.reviewerRoleName} is busy (${round.phase}`
|
|
3654
|
-
+ `${round.
|
|
3485
|
+
+ `${round.activeTurnId === undefined ? "" : `; Turn ${round.activeTurnId}`}); `
|
|
3655
3486
|
+ `${round.activeReviewRoundId === undefined
|
|
3656
3487
|
? ""
|
|
3657
3488
|
: `active ReviewRound ${round.activeReviewRoundId}; `}`
|
|
@@ -3668,7 +3499,7 @@ const TASK_FINAL_FORCE_FRESH_EVENT = "review.task-final-force-fresh-requested";
|
|
|
3668
3499
|
/**
|
|
3669
3500
|
* Creates a distinct full Task-final ReviewRound only when the exact previous
|
|
3670
3501
|
* terminal Round durably proves that no semantic review was produced. The
|
|
3671
|
-
* source Round,
|
|
3502
|
+
* source Round, Turn, findings, workspace, and terminal report remain immutable
|
|
3672
3503
|
* history; the linking Event is both the audit record and the idempotence key.
|
|
3673
3504
|
*/
|
|
3674
3505
|
function forceFreshTaskReviewRound(args, store, options) {
|
|
@@ -3769,7 +3600,7 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3769
3600
|
tx.saveReviewRound(task.id, created);
|
|
3770
3601
|
recordTaskEvent(tx, task.id, TASK_FINAL_FORCE_FRESH_EVENT, {
|
|
3771
3602
|
sourceReviewRoundId: source.id,
|
|
3772
|
-
...(source.
|
|
3603
|
+
...(source.reviewerTurnId === undefined ? {} : { sourceReviewerTurnId: source.reviewerTurnId }),
|
|
3773
3604
|
reviewRoundId: created.id,
|
|
3774
3605
|
reviewerRoleName: created.reviewerRoleName,
|
|
3775
3606
|
taskCandidate: JSON.stringify(created.taskCandidate),
|
|
@@ -3787,7 +3618,7 @@ function forceFreshTaskReviewRound(args, store, options) {
|
|
|
3787
3618
|
* Conservatively classifies an immutable Task-final Review as replaceable.
|
|
3788
3619
|
* A failed Round keeps the existing no-semantic-evidence behavior. A completed
|
|
3789
3620
|
* Round needs stronger, mutually corroborating evidence: an explicit internal
|
|
3790
|
-
* context/workspace failure, its exact
|
|
3621
|
+
* context/workspace failure, its exact completed Turn, matching Lane
|
|
3791
3622
|
* output, and the mechanically emitted empty completion Event.
|
|
3792
3623
|
* This is command eligibility only; it never rewrites the source outcome or
|
|
3793
3624
|
* changes the global semantic classifier used by the finding ledger.
|
|
@@ -3809,7 +3640,7 @@ export function classifyForceFreshReviewRecovery(store, round) {
|
|
|
3809
3640
|
*/
|
|
3810
3641
|
function validateDeltaRecheckRequest(store, taskId, candidate, preflight) {
|
|
3811
3642
|
if (preflight === undefined) {
|
|
3812
|
-
throw usageError("Delta-recheck assessment is missing; the CLI preflight did not
|
|
3643
|
+
throw usageError("Delta-recheck assessment is missing; the CLI preflight did not turn. "
|
|
3813
3644
|
+ "Request a full Review or retry with a current CLI.");
|
|
3814
3645
|
}
|
|
3815
3646
|
const previous = store.getReviewRound(taskId, preflight.record.previousReviewRoundId);
|
|
@@ -3840,42 +3671,42 @@ function assertTaskReviewRequestLane(store, taskId, reviewerRoleName, reusableRo
|
|
|
3840
3671
|
roleName: reviewerRoleName
|
|
3841
3672
|
}));
|
|
3842
3673
|
const hasMailboxWork = (mailbox) => (mailbox !== null && workMailboxHasWork(mailbox));
|
|
3843
|
-
const activePointer = store.
|
|
3844
|
-
const
|
|
3674
|
+
const activePointer = store.getActiveTurn(taskId, reviewerRoleName);
|
|
3675
|
+
const activeTurns = store.listTurns(taskId).filter((entry) => (entry.roleName === reviewerRoleName && entry.status === "active"));
|
|
3845
3676
|
if (reusableRound === undefined || reusableRound.status === "pending") {
|
|
3846
|
-
if (activePointer !== null ||
|
|
3677
|
+
if (activePointer !== null || activeTurns.length > 0
|
|
3847
3678
|
|| hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
3848
3679
|
throw usageError(`Reviewer has unrelated active execution: ${reviewerRoleName}.`);
|
|
3849
3680
|
}
|
|
3850
3681
|
return;
|
|
3851
3682
|
}
|
|
3852
3683
|
if (reusableRound.status === "completed") {
|
|
3853
|
-
if (activePointer !== null ||
|
|
3684
|
+
if (activePointer !== null || activeTurns.length > 0
|
|
3854
3685
|
|| hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
3855
3686
|
throw usageError(`Reviewer has unrelated active execution: ${reviewerRoleName}.`);
|
|
3856
3687
|
}
|
|
3857
3688
|
return;
|
|
3858
3689
|
}
|
|
3859
|
-
const
|
|
3860
|
-
const activeMatches =
|
|
3861
|
-
&& activePointer?.id ===
|
|
3690
|
+
const reviewerTurnId = reusableRound.reviewerTurnId;
|
|
3691
|
+
const activeMatches = reviewerTurnId !== undefined
|
|
3692
|
+
&& activePointer?.id === reviewerTurnId
|
|
3862
3693
|
&& activePointer.status === "active"
|
|
3863
|
-
&&
|
|
3864
|
-
&&
|
|
3694
|
+
&& activeTurns.length === 1
|
|
3695
|
+
&& activeTurns[0].id === reviewerTurnId;
|
|
3865
3696
|
if (!activeMatches) {
|
|
3866
3697
|
throw usageError(`Existing Task-final ReviewRound ${reusableRound.id} is running without its exact Reviewer execution.`);
|
|
3867
3698
|
}
|
|
3868
|
-
const
|
|
3699
|
+
const exactTurn = turnRef(taskId, reviewerTurnId);
|
|
3869
3700
|
const reviewerPending = reviewerMailbox === null ? null : nextPendingBatch(reviewerMailbox);
|
|
3870
3701
|
const processingMatches = reviewerMailbox?.processing !== null
|
|
3871
3702
|
&& reviewerMailbox?.processing !== undefined
|
|
3872
3703
|
&& reviewerPending === null
|
|
3873
3704
|
&& reviewerMailbox.processing.executionRef !== undefined
|
|
3874
|
-
&& isDeepStrictEqual(reviewerMailbox.processing.executionRef,
|
|
3705
|
+
&& isDeepStrictEqual(reviewerMailbox.processing.executionRef, exactTurn);
|
|
3875
3706
|
const pendingMatches = reviewerPending !== null
|
|
3876
3707
|
&& reviewerMailbox?.processing === null
|
|
3877
3708
|
&& reviewerPending.requestCount === 1
|
|
3878
|
-
&& reviewerPending.refs.some((ref) => isDeepStrictEqual(ref,
|
|
3709
|
+
&& reviewerPending.refs.some((ref) => isDeepStrictEqual(ref, exactTurn));
|
|
3879
3710
|
if (hasMailboxWork(reviewerMailbox) && !processingMatches && !pendingMatches) {
|
|
3880
3711
|
throw usageError(`Reviewer mailbox has unrelated active work: ${reviewerRoleName}.`);
|
|
3881
3712
|
}
|
|
@@ -3899,11 +3730,11 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3899
3730
|
if ((round.scope ?? "work-item") !== "task") {
|
|
3900
3731
|
throw usageError(`ReviewRound ${round.id} is not a failed Task-final ReviewRound.`);
|
|
3901
3732
|
}
|
|
3902
|
-
if (round.
|
|
3733
|
+
if (round.reviewerTurnId !== undefined) {
|
|
3903
3734
|
if (round.status === "completed") {
|
|
3904
3735
|
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
3905
3736
|
}
|
|
3906
|
-
throw usageError(`ReviewRound ${round.id} has Reviewer
|
|
3737
|
+
throw usageError(`ReviewRound ${round.id} has Reviewer Turn ${round.reviewerTurnId}; use task turn retry instead.`);
|
|
3907
3738
|
}
|
|
3908
3739
|
if (round.status !== "failed" && round.status !== "pending") {
|
|
3909
3740
|
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
@@ -3962,10 +3793,10 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3962
3793
|
roleName: reviewer.name
|
|
3963
3794
|
}));
|
|
3964
3795
|
const hasMailboxWork = (mailbox) => (mailbox !== null && workMailboxHasWork(mailbox));
|
|
3965
|
-
const
|
|
3966
|
-
const activePointer = tx.
|
|
3967
|
-
if (activePointer !== null ||
|
|
3968
|
-
throw usageError(`Reviewer Role already has an active
|
|
3796
|
+
const activeReviewerTurns = tx.listTurns(task.id).filter((entry) => (entry.roleName === reviewer.name && entry.status === "active"));
|
|
3797
|
+
const activePointer = tx.getActiveTurn(task.id, reviewer.name);
|
|
3798
|
+
if (activePointer !== null || activeReviewerTurns.length > 0) {
|
|
3799
|
+
throw usageError(`Reviewer Role already has an active Turn: ${reviewer.name}.`);
|
|
3969
3800
|
}
|
|
3970
3801
|
if (hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
3971
3802
|
throw usageError(`Reviewer has unrelated mailbox work: ${reviewer.name}.`);
|
|
@@ -3988,32 +3819,28 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3988
3819
|
? `Task-final Review retry requested as ${result.round.id}\n`
|
|
3989
3820
|
: `Task-final Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round });
|
|
3990
3821
|
}
|
|
3991
|
-
function
|
|
3822
|
+
function taskTurnCommand(args, store, options) {
|
|
3992
3823
|
const [command, ...rest] = args;
|
|
3993
3824
|
if (command === "list")
|
|
3994
|
-
return output(
|
|
3825
|
+
return output(listTurns(rest, store, options));
|
|
3995
3826
|
if (command === "show")
|
|
3996
|
-
return
|
|
3827
|
+
return showTurn(rest, store, options);
|
|
3997
3828
|
if (command === "context")
|
|
3998
|
-
return
|
|
3829
|
+
return turnContextCommand(rest, store, options);
|
|
3999
3830
|
if (command === "retry")
|
|
4000
|
-
return
|
|
3831
|
+
return retryTurn(rest, store, options);
|
|
4001
3832
|
if (command === "settle")
|
|
4002
|
-
return
|
|
4003
|
-
if (command === "yield")
|
|
4004
|
-
return yieldRun(rest, store, options);
|
|
4005
|
-
if (command === "yield-status")
|
|
4006
|
-
return yieldRunStatus(rest, store, options);
|
|
3833
|
+
return settleStaleFinalReviewTurn(rest, store, options);
|
|
4007
3834
|
if (command === "checkpoint")
|
|
4008
|
-
return output(
|
|
3835
|
+
return output(checkpointTurn(rest, store, options));
|
|
4009
3836
|
if (command === "retire")
|
|
4010
|
-
return
|
|
3837
|
+
return retireTurn(rest, store, options);
|
|
4011
3838
|
throw usageError(command === undefined
|
|
4012
|
-
? "Task
|
|
4013
|
-
: `Unknown command: task
|
|
3839
|
+
? "Task turn command is required."
|
|
3840
|
+
: `Unknown command: task turn ${command}`);
|
|
4014
3841
|
}
|
|
4015
|
-
function
|
|
4016
|
-
const usage = "Task
|
|
3842
|
+
function retireTurn(args, store, options) {
|
|
3843
|
+
const usage = "Task turn retire usage: yui task turn retire <task>/<turn> --reason <text> [--expected-progress-at <timestamp>] [--agent-id <id>] [--adapter-id <id>] [--native-session-id <id>] [--launch-id <id>].";
|
|
4017
3844
|
const parsed = parseTail(args, new Set([
|
|
4018
3845
|
"--reason",
|
|
4019
3846
|
"--expected-progress-at",
|
|
@@ -4025,23 +3852,23 @@ function retireRun(args, store, options) {
|
|
|
4025
3852
|
]), usage);
|
|
4026
3853
|
exactPositionals(parsed.positionals, 1, usage);
|
|
4027
3854
|
const reason = requiredOption(parsed.options, "--reason");
|
|
4028
|
-
const reference = taskRecordReference(parsed.positionals[0], "
|
|
3855
|
+
const reference = taskRecordReference(parsed.positionals[0], "turn", "Turn reference", options);
|
|
4029
3856
|
const now = clock(options);
|
|
4030
3857
|
const result = store.transaction((tx) => {
|
|
4031
3858
|
const task = requireTask(tx, reference.taskId);
|
|
4032
3859
|
assertTaskOpen(task);
|
|
4033
3860
|
const actor = taskActor(tx, options, task.id);
|
|
4034
|
-
let run = tx.
|
|
3861
|
+
let run = tx.getTurn(task.id, reference.localId);
|
|
4035
3862
|
if (run === null)
|
|
4036
|
-
throw dataError(`
|
|
3863
|
+
throw dataError(`Turn not found: ${task.id}/${reference.localId}.`);
|
|
4037
3864
|
const events = tx.listEvents(task.id);
|
|
4038
|
-
if (isTaskRecordRetired(events, "
|
|
4039
|
-
return { task, run, changed: false };
|
|
3865
|
+
if (isTaskRecordRetired(events, "turn", run.id)) {
|
|
3866
|
+
return { task, turn: run, changed: false };
|
|
4040
3867
|
}
|
|
4041
3868
|
if (run.status === "active") {
|
|
4042
3869
|
if (actor === "leader"
|
|
4043
|
-
&&
|
|
4044
|
-
throw usageError("A Task Leader cannot retire its own current authority
|
|
3870
|
+
&& taskLeaderActionTurnId(tx, task.id, options.environment, options.yuiHome) === run.id) {
|
|
3871
|
+
throw usageError("A Task Leader cannot retire its own current authority Turn.", usage);
|
|
4045
3872
|
}
|
|
4046
3873
|
const expectedProgressAt = requiredOption(parsed.options, parsed.options.has("--expected-progress-at")
|
|
4047
3874
|
? "--expected-progress-at"
|
|
@@ -4056,31 +3883,31 @@ function retireRun(args, store, options) {
|
|
|
4056
3883
|
const sessions = tx.getTaskRoleSessionSet(task.id, run.roleName);
|
|
4057
3884
|
const session = sessions?.sessions[run.effective.agentId];
|
|
4058
3885
|
if (session?.nativeSessionId !== undefined && nativeSessionId === undefined) {
|
|
4059
|
-
throw usageError("--native-session-id is required for this active
|
|
3886
|
+
throw usageError("--native-session-id is required for this active Turn.", usage);
|
|
4060
3887
|
}
|
|
4061
3888
|
if (session?.nativeSessionId === undefined && launchId === undefined) {
|
|
4062
|
-
throw usageError("--launch-id is required for an opaque active
|
|
3889
|
+
throw usageError("--launch-id is required for an opaque active Turn.", usage);
|
|
4063
3890
|
}
|
|
4064
|
-
const terminal =
|
|
3891
|
+
const terminal = retireExactActiveTurn(tx, {
|
|
4065
3892
|
taskId: task.id,
|
|
4066
3893
|
roleName: run.roleName,
|
|
4067
|
-
|
|
3894
|
+
turnId: run.id,
|
|
4068
3895
|
agentId,
|
|
4069
3896
|
adapterId,
|
|
4070
3897
|
...(nativeSessionId === undefined ? {} : { nativeSessionId }),
|
|
4071
3898
|
...(launchId === undefined ? {} : { launchId }),
|
|
4072
3899
|
expectedProgressAt,
|
|
4073
|
-
reason: `
|
|
3900
|
+
reason: `Turn retired: ${reason}`
|
|
4074
3901
|
}, now);
|
|
4075
|
-
if (terminal.disposition !== "applied" || terminal.
|
|
3902
|
+
if (terminal.disposition !== "applied" || terminal.turn === null) {
|
|
4076
3903
|
throw usageError(terminal.disposition === "blocked"
|
|
4077
|
-
? `
|
|
4078
|
-
: `
|
|
3904
|
+
? `Turn retirement is blocked: ${run.id}/${terminal.reason ?? "unsafe"}.`
|
|
3905
|
+
: `Turn changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
4079
3906
|
}
|
|
4080
|
-
run = terminal.
|
|
3907
|
+
run = terminal.turn;
|
|
4081
3908
|
}
|
|
4082
|
-
recordTaskEvent(tx, task.id, "
|
|
4083
|
-
|
|
3909
|
+
recordTaskEvent(tx, task.id, "turn.retired", {
|
|
3910
|
+
turnId: run.id,
|
|
4084
3911
|
reason,
|
|
4085
3912
|
...(parsed.options.get("--expected-progress-at") === undefined
|
|
4086
3913
|
&& parsed.options.get("--progress-at") === undefined
|
|
@@ -4102,69 +3929,69 @@ function retireRun(args, store, options) {
|
|
|
4102
3929
|
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
4103
3930
|
eventId: tx.nextEventId(task.id),
|
|
4104
3931
|
taskId: task.id,
|
|
4105
|
-
recordKind: "
|
|
3932
|
+
recordKind: "turn",
|
|
4106
3933
|
recordId: run.id,
|
|
4107
3934
|
reason,
|
|
4108
3935
|
retiredBy: actor
|
|
4109
3936
|
}, now));
|
|
4110
|
-
return { task, run, changed: true };
|
|
3937
|
+
return { task, turn: run, changed: true };
|
|
4111
3938
|
});
|
|
4112
3939
|
if (result.changed)
|
|
4113
3940
|
options.runtime?.notifyStateChanged(result.task.id);
|
|
4114
|
-
return output(`Retired
|
|
4115
|
-
|
|
3941
|
+
return output(`Retired Turn ${result.task.id}/${result.turn.id}\n`, {
|
|
3942
|
+
turn: result.turn,
|
|
4116
3943
|
retired: true
|
|
4117
3944
|
});
|
|
4118
3945
|
}
|
|
4119
|
-
function
|
|
3946
|
+
function turnContextCommand(args, store, options) {
|
|
4120
3947
|
const [first, ...rest] = args;
|
|
4121
3948
|
if (first === "expand") {
|
|
4122
|
-
const usage = "Task
|
|
3949
|
+
const usage = "Task turn context expand usage: yui task turn context expand <task>/<turn> <ref-id> [--store <store>] [--mode full].";
|
|
4123
3950
|
const parsed = parseTail(rest, new Set(["--store", "--mode"]), usage);
|
|
4124
3951
|
exactPositionals(parsed.positionals, 2, usage);
|
|
4125
3952
|
const mode = parsed.options.get("--mode");
|
|
4126
3953
|
if (mode !== undefined && mode !== "full") {
|
|
4127
|
-
throw usageError("
|
|
3954
|
+
throw usageError("Turn Context expansion mode must be full.", usage);
|
|
4128
3955
|
}
|
|
4129
|
-
const { taskId,
|
|
4130
|
-
|
|
4131
|
-
const expanded = store.transaction((tx) =>
|
|
3956
|
+
const { taskId, turnId } = parseTurnContextReference(parsed.positionals[0]);
|
|
3957
|
+
authorizeTurnContext(store, taskId, turnId, options.environment);
|
|
3958
|
+
const expanded = store.transaction((tx) => expandTurnContextRef(tx, taskId, turnId, parsed.positionals[1], optionalNonEmptyOption(parsed.options, "--store")));
|
|
4132
3959
|
return output(`${JSON.stringify(expanded, null, 2)}\n`, { context: expanded });
|
|
4133
3960
|
}
|
|
4134
3961
|
if (first === "delta") {
|
|
4135
3962
|
if (rest.length !== 3 || rest[1] !== "--after") {
|
|
4136
|
-
throw usageError("Task
|
|
3963
|
+
throw usageError("Task turn context delta usage: yui task turn context delta <task>/<turn> --after <cursor>.");
|
|
4137
3964
|
}
|
|
4138
|
-
const { taskId,
|
|
4139
|
-
|
|
4140
|
-
const delta = store.transaction((tx) => (
|
|
3965
|
+
const { taskId, turnId } = parseTurnContextReference(rest[0]);
|
|
3966
|
+
authorizeTurnContext(store, taskId, turnId, options.environment);
|
|
3967
|
+
const delta = store.transaction((tx) => (buildTurnContextDelta(tx, taskId, turnId, rest[2])));
|
|
4141
3968
|
return output(`${JSON.stringify(delta, null, 2)}\n`, { contextDelta: delta });
|
|
4142
3969
|
}
|
|
4143
3970
|
if (first === undefined || rest.length !== 0) {
|
|
4144
|
-
throw usageError("Task
|
|
3971
|
+
throw usageError("Task turn context usage: yui task turn context <task>/<turn>.");
|
|
4145
3972
|
}
|
|
4146
|
-
const { taskId,
|
|
4147
|
-
|
|
4148
|
-
const pack = store.transaction((tx) =>
|
|
3973
|
+
const { taskId, turnId } = parseTurnContextReference(first);
|
|
3974
|
+
authorizeTurnContext(store, taskId, turnId, options.environment);
|
|
3975
|
+
const pack = store.transaction((tx) => buildTurnContextPack(tx, taskId, turnId));
|
|
4149
3976
|
return output(`${JSON.stringify(pack, null, 2)}\n`, { context: pack });
|
|
4150
3977
|
}
|
|
4151
|
-
function
|
|
4152
|
-
const [taskId,
|
|
4153
|
-
if (taskId === undefined || taskId.length === 0 ||
|
|
3978
|
+
function parseTurnContextReference(value) {
|
|
3979
|
+
const [taskId, turnId, extra] = value.split("/");
|
|
3980
|
+
if (taskId === undefined || taskId.length === 0 || turnId === undefined || turnId.length === 0
|
|
4154
3981
|
|| extra !== undefined) {
|
|
4155
|
-
throw usageError(`
|
|
3982
|
+
throw usageError(`Turn context reference is invalid: ${value}.`);
|
|
4156
3983
|
}
|
|
4157
|
-
return { taskId,
|
|
3984
|
+
return { taskId, turnId };
|
|
4158
3985
|
}
|
|
4159
|
-
function
|
|
3986
|
+
function authorizeTurnContext(store, taskId, turnId, environment) {
|
|
4160
3987
|
const managed = environment?.YUI_SESSION_SCOPE !== undefined
|
|
4161
3988
|
|| environment?.YUI_TASK_ID !== undefined
|
|
4162
3989
|
|| environment?.YUI_ROLE !== undefined
|
|
4163
|
-
|| environment?.
|
|
3990
|
+
|| environment?.YUI_TURN_ID !== undefined;
|
|
4164
3991
|
if (!managed)
|
|
4165
3992
|
return;
|
|
4166
|
-
const run = store.
|
|
4167
|
-
const active = run === null ? null : store.
|
|
3993
|
+
const run = store.getTurn(taskId, turnId);
|
|
3994
|
+
const active = run === null ? null : store.getActiveTurn(taskId, run.roleName);
|
|
4168
3995
|
if (environment?.YUI_SESSION_SCOPE !== "task"
|
|
4169
3996
|
|| environment.YUI_TASK_ID !== taskId
|
|
4170
3997
|
|| run === null
|
|
@@ -4172,27 +3999,32 @@ function authorizeRunContext(store, taskId, runId, environment) {
|
|
|
4172
3999
|
|| environment.YUI_AGENT_ID !== run.effective.agentId
|
|
4173
4000
|
|| environment.YUI_ADAPTER_ID !== run.effective.adapterId
|
|
4174
4001
|
|| run.status !== "active"
|
|
4175
|
-
|| active?.id !==
|
|
4176
|
-
throw usageError(`
|
|
4002
|
+
|| active?.id !== turnId) {
|
|
4003
|
+
throw usageError(`Turn Context access is not authorized: ${taskId}/${turnId}.`);
|
|
4177
4004
|
}
|
|
4178
4005
|
const callerKey = environment.YUI_JOB_CALLER_KEY;
|
|
4179
4006
|
const expectedCallerKeyHash = store.getJobCallerKeyHash(taskId, run.roleName, run.effective.agentId);
|
|
4180
4007
|
if (callerKey === undefined
|
|
4181
4008
|
|| expectedCallerKeyHash === null
|
|
4182
4009
|
|| createHash("sha256").update(callerKey).digest("hex") !== expectedCallerKeyHash) {
|
|
4183
|
-
throw usageError(`
|
|
4010
|
+
throw usageError(`Turn Context caller key is not authorized: ${taskId}/${turnId}.`);
|
|
4184
4011
|
}
|
|
4185
4012
|
}
|
|
4186
|
-
function
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
const
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
const
|
|
4193
|
-
|
|
4194
|
-
|
|
4013
|
+
function listTurns(args, store, options) {
|
|
4014
|
+
const usage = "Task turn list usage: yui task turn list <task|task/work>.";
|
|
4015
|
+
exactPositionals(args, 1, usage);
|
|
4016
|
+
const reference = args[0];
|
|
4017
|
+
const task = store.getTask(reference);
|
|
4018
|
+
const item = task === null ? requireWorkItem(store, reference, options) : null;
|
|
4019
|
+
const taskId = task?.id ?? item.taskId;
|
|
4020
|
+
const turns = store.listTurns(taskId).filter((turn) => (item === null || turn.workItemId === item.id));
|
|
4021
|
+
if (turns.length === 0)
|
|
4022
|
+
return "No Turns found.\n";
|
|
4023
|
+
const events = store.listEvents(taskId);
|
|
4024
|
+
return `${renderTable(`Turns: ${item?.id ?? taskId}`, [
|
|
4025
|
+
{ header: "Turn", minWidth: 6, maxWidth: 20 },
|
|
4195
4026
|
{ header: "Role", minWidth: 4, maxWidth: 22 },
|
|
4027
|
+
{ header: "Subject", minWidth: 7, maxWidth: 24 },
|
|
4196
4028
|
{ header: "Purpose", minWidth: 6, maxWidth: 10 },
|
|
4197
4029
|
{ header: "Mode", minWidth: 4, maxWidth: 8 },
|
|
4198
4030
|
{ header: "Effective", minWidth: 10, maxWidth: 30 },
|
|
@@ -4201,38 +4033,39 @@ function listRuns(args, store, options) {
|
|
|
4201
4033
|
{ header: "Status", minWidth: 6, maxWidth: 12 },
|
|
4202
4034
|
{ header: "History", minWidth: 7, maxWidth: 9 },
|
|
4203
4035
|
{ header: "Summary", minWidth: 8, maxWidth: 58 }
|
|
4204
|
-
],
|
|
4036
|
+
], turns.map((run) => [
|
|
4205
4037
|
run.id,
|
|
4206
4038
|
run.roleName,
|
|
4039
|
+
run.workItemId ?? (run.reviewRoundId === undefined ? "task" : `review:${run.reviewRoundId}`),
|
|
4207
4040
|
run.purpose,
|
|
4208
4041
|
run.mode,
|
|
4209
4042
|
`${run.effective.agentId}/${run.effective.adapterId} r${run.effective.sourceDesiredRevision}`,
|
|
4210
4043
|
run.effective.profileAccess,
|
|
4211
4044
|
run.effective.permission.strategy,
|
|
4212
4045
|
run.status,
|
|
4213
|
-
isTaskRecordRetired(events, "
|
|
4214
|
-
run.
|
|
4046
|
+
isTaskRecordRetired(events, "turn", run.id) ? "retired" : "active",
|
|
4047
|
+
run.result?.output ?? "-"
|
|
4215
4048
|
]), defaultTableWidth())}\n`;
|
|
4216
4049
|
}
|
|
4217
4050
|
/**
|
|
4218
|
-
* Settles only the known bootstrap split where a failed Task-final
|
|
4051
|
+
* Settles only the known bootstrap split where a failed Task-final Turn
|
|
4219
4052
|
* still owns a running ReviewRound, but the committed Task heads have moved
|
|
4220
4053
|
* on. This is deliberately narrower than retry: it cannot manufacture a
|
|
4221
4054
|
* review or fail an arbitrary Round, and every identity/mailbox fence is
|
|
4222
4055
|
* checked before the old Round changes. The next normal Task completion then
|
|
4223
4056
|
* creates one fresh Round over the newer frozen Task heads.
|
|
4224
4057
|
*/
|
|
4225
|
-
function
|
|
4226
|
-
exactPositionals(args, 1, "Task
|
|
4058
|
+
function settleStaleFinalReviewTurn(args, store, options) {
|
|
4059
|
+
exactPositionals(args, 1, "Task turn settle usage: yui task turn settle <task>/<turn>.");
|
|
4227
4060
|
const now = clock(options);
|
|
4228
|
-
const previous = store.transaction((tx) =>
|
|
4061
|
+
const previous = store.transaction((tx) => requireTurn(tx, args[0], options));
|
|
4229
4062
|
const result = store.transaction((tx) => {
|
|
4230
|
-
const run = tx.
|
|
4063
|
+
const run = tx.getTurn(previous.taskId, previous.id);
|
|
4231
4064
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
4232
|
-
throw usageError(`
|
|
4065
|
+
throw usageError(`Turn ${previous.id} is not a failed review Turn.`);
|
|
4233
4066
|
}
|
|
4234
4067
|
if (run.reviewRoundId === undefined) {
|
|
4235
|
-
throw usageError(`Review
|
|
4068
|
+
throw usageError(`Review Turn ${run.id} has no ReviewRound.`);
|
|
4236
4069
|
}
|
|
4237
4070
|
const task = requireTask(tx, run.taskId);
|
|
4238
4071
|
if (task.status !== "active")
|
|
@@ -4240,30 +4073,30 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4240
4073
|
const actor = taskActor(tx, options, task.id);
|
|
4241
4074
|
const round = tx.getReviewRound(task.id, run.reviewRoundId);
|
|
4242
4075
|
if (round === null) {
|
|
4243
|
-
throw dataError(`ReviewRound not found for
|
|
4076
|
+
throw dataError(`ReviewRound not found for Turn ${run.id}: ${run.reviewRoundId}.`);
|
|
4244
4077
|
}
|
|
4245
4078
|
if ((round.scope ?? "work-item") !== "task") {
|
|
4246
|
-
throw usageError(`Review
|
|
4079
|
+
throw usageError(`Review Turn ${run.id} is not a Task-final review; request a new WorkItem review `
|
|
4247
4080
|
+ "for a new Candidate.");
|
|
4248
4081
|
}
|
|
4249
4082
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
4250
4083
|
if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)) {
|
|
4251
4084
|
throw usageError(`Task final-review contract does not match ReviewRound ${round.id}.`);
|
|
4252
4085
|
}
|
|
4253
|
-
// This read-only compare-and-swap fence covers the exact
|
|
4086
|
+
// This read-only compare-and-swap fence covers the exact Turn/Round,
|
|
4254
4087
|
// Candidate, stored Review workspace, frozen Project scope, and frozen
|
|
4255
4088
|
// Project heads before any mailbox or Round write.
|
|
4256
|
-
const validation =
|
|
4089
|
+
const validation = validateExactTurnReviewRound(tx, run, { allowTerminal: true });
|
|
4257
4090
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
4258
|
-
throw usageError(`Review
|
|
4091
|
+
throw usageError(`Review Turn ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
4259
4092
|
}
|
|
4260
|
-
const
|
|
4093
|
+
const activeReviewerTurn = tx.listTurns(task.id).find((entry) => (entry.purpose === "review"
|
|
4261
4094
|
&& entry.roleName === round.reviewerRoleName
|
|
4262
4095
|
&& entry.status === "active"));
|
|
4263
|
-
const
|
|
4264
|
-
if (
|
|
4265
|
-
const active =
|
|
4266
|
-
throw usageError(`${task.id}/${round.reviewerRoleName} already has active
|
|
4096
|
+
const activeRoleTurn = tx.getActiveTurn(task.id, round.reviewerRoleName);
|
|
4097
|
+
if (activeReviewerTurn !== undefined || activeRoleTurn !== null) {
|
|
4098
|
+
const active = activeReviewerTurn ?? activeRoleTurn;
|
|
4099
|
+
throw usageError(`${task.id}/${round.reviewerRoleName} already has active Turn ${active.id}.`);
|
|
4267
4100
|
}
|
|
4268
4101
|
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
|
|
4269
4102
|
.filter((entry) => (entry.scope ?? "work-item") === "task"));
|
|
@@ -4280,13 +4113,13 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4280
4113
|
throw usageError(`Final ReviewRound ${round.id} freezes the current Task candidate; use exact retry.`);
|
|
4281
4114
|
}
|
|
4282
4115
|
if (round.status === "failed") {
|
|
4283
|
-
const summary = run.
|
|
4116
|
+
const summary = run.result?.output.trim();
|
|
4284
4117
|
if (summary !== undefined
|
|
4285
4118
|
&& round.summary === summary
|
|
4286
4119
|
&& round.report === summary
|
|
4287
4120
|
&& (round.checks?.length ?? 0) === 0
|
|
4288
4121
|
&& round.evidenceCommit === undefined) {
|
|
4289
|
-
return { run, round, changed: false };
|
|
4122
|
+
return { turn: run, round, changed: false };
|
|
4290
4123
|
}
|
|
4291
4124
|
throw usageError(`Final ReviewRound is already terminal: ${round.id}/${round.status}.`);
|
|
4292
4125
|
}
|
|
@@ -4296,11 +4129,11 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4296
4129
|
const reviewerTarget = roleMailbox(task.id, round.reviewerRoleName);
|
|
4297
4130
|
const reviewerMailbox = tx.getWorkMailbox(reviewerTarget);
|
|
4298
4131
|
const reviewerPending = reviewerMailbox === null ? null : nextPendingBatch(reviewerMailbox);
|
|
4299
|
-
const
|
|
4132
|
+
const exactTurnRef = turnRef(task.id, run.id);
|
|
4300
4133
|
if (reviewerMailbox?.processing !== null && reviewerMailbox?.processing !== undefined) {
|
|
4301
4134
|
const processing = reviewerMailbox.processing;
|
|
4302
4135
|
if (processing.executionRef === undefined
|
|
4303
|
-
|| !isDeepStrictEqual(processing.executionRef,
|
|
4136
|
+
|| !isDeepStrictEqual(processing.executionRef, exactTurnRef)
|
|
4304
4137
|
|| reviewerPending !== null) {
|
|
4305
4138
|
throw usageError(`Reviewer mailbox has unrelated processing work: ${round.reviewerRoleName}.`);
|
|
4306
4139
|
}
|
|
@@ -4308,7 +4141,7 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4308
4141
|
else if (reviewerPending !== null) {
|
|
4309
4142
|
const pending = reviewerPending;
|
|
4310
4143
|
if (pending.requestCount !== 1
|
|
4311
|
-
|| !pending.refs.some((ref) => isDeepStrictEqual(ref,
|
|
4144
|
+
|| !pending.refs.some((ref) => isDeepStrictEqual(ref, exactTurnRef))) {
|
|
4312
4145
|
throw usageError(`Reviewer mailbox has unrelated pending work: ${round.reviewerRoleName}.`);
|
|
4313
4146
|
}
|
|
4314
4147
|
}
|
|
@@ -4323,10 +4156,10 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4323
4156
|
if (runtimeMailbox !== null && workMailboxHasWork(runtimeMailbox)) {
|
|
4324
4157
|
throw usageError(`Reviewer runtime lifecycle has pending work: ${round.reviewerRoleName}.`);
|
|
4325
4158
|
}
|
|
4326
|
-
// A matching pending/processing dispatch belongs to this failed
|
|
4159
|
+
// A matching pending/processing dispatch belongs to this failed Turn and
|
|
4327
4160
|
// is safe to settle here. It is never merged with unrelated mailbox work.
|
|
4328
4161
|
if (reviewerMailbox !== null && reviewerMailbox !== undefined) {
|
|
4329
|
-
const settlement = settleExactWorkExecution(tx, reviewerTarget,
|
|
4162
|
+
const settlement = settleExactWorkExecution(tx, reviewerTarget, exactTurnRef);
|
|
4330
4163
|
if (settlement === "absent") {
|
|
4331
4164
|
throw usageError(`Reviewer mailbox changed before stale final settlement: ${round.reviewerRoleName}.`);
|
|
4332
4165
|
}
|
|
@@ -4334,53 +4167,53 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4334
4167
|
// Preserve any report/check/evidence already attached to the old Round;
|
|
4335
4168
|
// terminalization only adds the missing failure boundary and end time.
|
|
4336
4169
|
const summary = round.summary
|
|
4337
|
-
?? run.
|
|
4338
|
-
?? `Review
|
|
4170
|
+
?? run.result?.output
|
|
4171
|
+
?? `Review Turn ${run.id} failed before delivery; committed Task heads changed.`;
|
|
4339
4172
|
const terminal = finishReviewRound(round, "failed", summary, now, {
|
|
4340
4173
|
report: round.report ?? summary,
|
|
4341
4174
|
checks: round.checks ?? [],
|
|
4342
4175
|
...(round.evidenceCommit === undefined ? {} : { evidenceCommit: round.evidenceCommit })
|
|
4343
4176
|
});
|
|
4344
4177
|
tx.saveReviewRound(task.id, terminal);
|
|
4345
|
-
recordTaskEvent(tx, task.id, "
|
|
4346
|
-
|
|
4178
|
+
recordTaskEvent(tx, task.id, "turn.review-stale-settled", {
|
|
4179
|
+
turnId: run.id,
|
|
4347
4180
|
reviewRoundId: round.id,
|
|
4348
4181
|
previousTaskCandidate: JSON.stringify(round.taskCandidate),
|
|
4349
4182
|
currentTaskCandidate: JSON.stringify(currentTaskCandidate),
|
|
4350
4183
|
settledBy: actor,
|
|
4351
4184
|
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
|
|
4352
4185
|
}, now);
|
|
4353
|
-
return { run, round: terminal, changed: true };
|
|
4186
|
+
return { turn: run, round: terminal, changed: true };
|
|
4354
4187
|
});
|
|
4355
4188
|
return output(result.changed
|
|
4356
|
-
? `Settled obsolete final Review ${result.round.id} from failed
|
|
4357
|
-
: `Obsolete final Review already settled: ${result.round.id}/${result.
|
|
4189
|
+
? `Settled obsolete final Review ${result.round.id} from failed Turn ${result.turn.id}\n`
|
|
4190
|
+
: `Obsolete final Review already settled: ${result.round.id}/${result.turn.id}\n`, { reviewRound: result.round, reviewTurn: result.turn });
|
|
4358
4191
|
}
|
|
4359
|
-
function
|
|
4360
|
-
exactPositionals(args, 1, "Task
|
|
4192
|
+
function retryTurn(args, store, options) {
|
|
4193
|
+
exactPositionals(args, 1, "Task turn retry usage: yui task turn retry <task>/<turn>.");
|
|
4361
4194
|
const now = clock(options);
|
|
4362
|
-
const previous = store.transaction((tx) =>
|
|
4195
|
+
const previous = store.transaction((tx) => requireTurn(tx, args[0], options));
|
|
4363
4196
|
if (previous.purpose === "review") {
|
|
4364
4197
|
return retryFailedReviewRun(previous, store, options, now);
|
|
4365
4198
|
}
|
|
4366
4199
|
const retried = store.transaction((tx) => {
|
|
4367
4200
|
if (previous.status !== "failed") {
|
|
4368
|
-
throw usageError(`
|
|
4201
|
+
throw usageError(`Turn ${previous.id} is not retryable from ${previous.status}.`);
|
|
4369
4202
|
}
|
|
4370
4203
|
const task = requireTask(tx, previous.taskId);
|
|
4371
4204
|
if (task.status !== "active")
|
|
4372
4205
|
throw usageError(`Task is not active: ${task.id}.`);
|
|
4373
|
-
assertTaskExecutionEnabled(task, "retrying a
|
|
4206
|
+
assertTaskExecutionEnabled(task, "retrying a Turn");
|
|
4374
4207
|
const role = requireRole(tx, task.id, previous.roleName);
|
|
4375
|
-
if (tx.
|
|
4376
|
-
throw usageError(`${task.id}/${role.name} already has an active
|
|
4208
|
+
if (tx.getActiveTurn(task.id, role.name) !== null) {
|
|
4209
|
+
throw usageError(`${task.id}/${role.name} already has an active turn.`);
|
|
4377
4210
|
}
|
|
4378
4211
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
4379
4212
|
const retryItem = previous.workItemId === undefined
|
|
4380
4213
|
? null
|
|
4381
4214
|
: tx.getWorkItem(task.id, previous.workItemId);
|
|
4382
4215
|
if (previous.workItemId !== undefined && retryItem === null) {
|
|
4383
|
-
throw dataError(`Work item not found for
|
|
4216
|
+
throw dataError(`Work item not found for Turn ${previous.id}: ${previous.workItemId}.`);
|
|
4384
4217
|
}
|
|
4385
4218
|
const retryLaneBefore = previous.executionLaneId === undefined
|
|
4386
4219
|
? undefined
|
|
@@ -4398,9 +4231,9 @@ function retryRun(args, store, options) {
|
|
|
4398
4231
|
&& retryLaneBefore !== undefined
|
|
4399
4232
|
&& retryLaneBefore.id === previous.executionLaneId
|
|
4400
4233
|
&& retryLaneBefore.status === "failed"
|
|
4401
|
-
&& retryLaneBefore.
|
|
4234
|
+
&& retryLaneBefore.turnId === previous.id);
|
|
4402
4235
|
if (!exactCurrentLane) {
|
|
4403
|
-
throw usageError(`
|
|
4236
|
+
throw usageError(`Turn ${previous.id} no longer owns the current failed Execution Lane.`);
|
|
4404
4237
|
}
|
|
4405
4238
|
const groupedRunningRetry = retryItem?.status === "running"
|
|
4406
4239
|
&& retriesExecutionLane
|
|
@@ -4425,7 +4258,7 @@ function retryRun(args, store, options) {
|
|
|
4425
4258
|
&& (retryGroup === undefined
|
|
4426
4259
|
|| retryGroup.id !== previous.executionGroupId
|
|
4427
4260
|
|| retryLane === undefined))) {
|
|
4428
|
-
throw dataError(`
|
|
4261
|
+
throw dataError(`Turn ${previous.id} execution lineage no longer matches its Work Item.`);
|
|
4429
4262
|
}
|
|
4430
4263
|
const retryManagedWorkspace = retryLane === undefined
|
|
4431
4264
|
? runWorkspace
|
|
@@ -4443,9 +4276,9 @@ function retryRun(args, store, options) {
|
|
|
4443
4276
|
writableProjectIds: [...(retryItem?.writeProjectIds ?? [])]
|
|
4444
4277
|
};
|
|
4445
4278
|
const retryDirective = retryLane?.directive
|
|
4446
|
-
?? previous.
|
|
4279
|
+
?? previous.inputs[0].input.directive
|
|
4447
4280
|
?? retryItem?.objective
|
|
4448
|
-
?? `Retry
|
|
4281
|
+
?? `Retry Turn ${previous.id}.`;
|
|
4449
4282
|
let queuedReason;
|
|
4450
4283
|
if (retryGroup !== undefined && retryLane !== undefined && retryItem !== null) {
|
|
4451
4284
|
const config = tx.getConfig();
|
|
@@ -4466,7 +4299,7 @@ function retryRun(args, store, options) {
|
|
|
4466
4299
|
usage: observedExecutionResourceUsage({
|
|
4467
4300
|
group: retryGroup,
|
|
4468
4301
|
stageGroups,
|
|
4469
|
-
|
|
4302
|
+
turns: tx.listTurns(task.id),
|
|
4470
4303
|
events: tx.listEvents(task.id)
|
|
4471
4304
|
}),
|
|
4472
4305
|
now
|
|
@@ -4496,12 +4329,12 @@ function retryRun(args, store, options) {
|
|
|
4496
4329
|
if (admission.decision === "queued")
|
|
4497
4330
|
queuedReason = admission.reason;
|
|
4498
4331
|
}
|
|
4499
|
-
const
|
|
4332
|
+
const turnId = queuedReason === undefined ? tx.nextTurnId(task.id) : undefined;
|
|
4500
4333
|
const runningGroup = retryGroup === undefined || retryLane === undefined
|
|
4501
4334
|
? undefined
|
|
4502
4335
|
: queuedReason === undefined
|
|
4503
4336
|
? restartExecutionLane(retryGroup, retryLane.id, {
|
|
4504
|
-
|
|
4337
|
+
turnId: turnId,
|
|
4505
4338
|
effective,
|
|
4506
4339
|
...(laneWorkspace === undefined ? {} : { workspace: laneWorkspace }),
|
|
4507
4340
|
directive: retryDirective
|
|
@@ -4540,32 +4373,24 @@ function retryRun(args, store, options) {
|
|
|
4540
4373
|
reason: queuedReason
|
|
4541
4374
|
};
|
|
4542
4375
|
}
|
|
4543
|
-
const retrySnapshot =
|
|
4376
|
+
const retrySnapshot = freezeTurnContextSnapshot(tx, {
|
|
4544
4377
|
taskId: task.id,
|
|
4545
4378
|
roleName: role.name,
|
|
4546
4379
|
purpose: "execution",
|
|
4547
4380
|
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId })
|
|
4548
4381
|
}, now, "controller", retryGroup?.stage?.contextSnapshotRef);
|
|
4549
|
-
const
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
action: previous.workItemId === undefined ? previous.assignment.action : "repair-work-item",
|
|
4554
|
-
subject: {
|
|
4555
|
-
taskId: task.id,
|
|
4556
|
-
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId }),
|
|
4557
|
-
...(runningGroup === undefined ? {} : {
|
|
4558
|
-
executionGroupId: runningGroup.id,
|
|
4559
|
-
executionLaneId: retryLane.id
|
|
4560
|
-
})
|
|
4382
|
+
const input = createTurnInput({
|
|
4383
|
+
source: {
|
|
4384
|
+
type: "yui",
|
|
4385
|
+
channel: previous.workItemId === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
4561
4386
|
},
|
|
4562
|
-
...(previous.
|
|
4387
|
+
...(previous.inputs[0].input.directive === undefined
|
|
4563
4388
|
? {}
|
|
4564
|
-
: { directive: previous.
|
|
4389
|
+
: { directive: previous.inputs[0].input.directive }),
|
|
4565
4390
|
contextSnapshotRef: contextSnapshotRef(retrySnapshot),
|
|
4566
4391
|
deltaRefIds: contextSnapshotDeltaRefIds(tx, retrySnapshot)
|
|
4567
4392
|
});
|
|
4568
|
-
const created =
|
|
4393
|
+
const created = createTurn(turnId, task.id, role.name, roleAgentSessionResumeMode(sessions, effective.agentId, effective), input, now, {
|
|
4569
4394
|
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId }),
|
|
4570
4395
|
...(runningGroup === undefined ? {} : {
|
|
4571
4396
|
executionGroupId: runningGroup.id,
|
|
@@ -4574,8 +4399,8 @@ function retryRun(args, store, options) {
|
|
|
4574
4399
|
...(retryManagedWorkspace === undefined ? {} : { workspace: retryManagedWorkspace }),
|
|
4575
4400
|
effective
|
|
4576
4401
|
});
|
|
4577
|
-
tx.
|
|
4578
|
-
tx.
|
|
4402
|
+
tx.saveTurn(created);
|
|
4403
|
+
tx.saveActiveTurn(created);
|
|
4579
4404
|
if (previous.workItemId !== undefined && retriedItemWithGroup !== null) {
|
|
4580
4405
|
const item = retriedItemWithGroup;
|
|
4581
4406
|
const workspace = tx.getWorkItemWorkspace(task.id, item.id);
|
|
@@ -4585,18 +4410,18 @@ function retryRun(args, store, options) {
|
|
|
4585
4410
|
+ `cannot retry ${item.id}.`);
|
|
4586
4411
|
}
|
|
4587
4412
|
}
|
|
4588
|
-
enqueueWork(tx, roleMailbox(task.id, role.name), "
|
|
4589
|
-
recordTaskEvent(tx, task.id, "
|
|
4590
|
-
...
|
|
4591
|
-
|
|
4413
|
+
enqueueWork(tx, roleMailbox(task.id, role.name), "turn-retried", now, [turnRef(task.id, created.id)]);
|
|
4414
|
+
recordTaskEvent(tx, task.id, "turn.retried", {
|
|
4415
|
+
...turnLaunchEventPayload(created),
|
|
4416
|
+
previousTurnId: previous.id
|
|
4592
4417
|
}, now);
|
|
4593
|
-
return { kind: "
|
|
4418
|
+
return { kind: "turn", turn: created };
|
|
4594
4419
|
});
|
|
4595
4420
|
if (retried.kind === "queued") {
|
|
4596
4421
|
return output(`Retry retained pending for ${retried.taskId}/${retried.laneId}: ${retried.reason}\n`);
|
|
4597
4422
|
}
|
|
4598
|
-
notifyMailbox(options.runtime, roleMailbox(retried.
|
|
4599
|
-
return output(`Retry queued as ${retried.
|
|
4423
|
+
notifyMailbox(options.runtime, roleMailbox(retried.turn.taskId, retried.turn.roleName), retried.turn.taskId);
|
|
4424
|
+
return output(`Retry queued as ${retried.turn.id} for ${retried.turn.taskId}/${retried.turn.roleName}\n`);
|
|
4600
4425
|
}
|
|
4601
4426
|
function actualTaskReviewCandidateForMutation(store, task, options) {
|
|
4602
4427
|
if (options.actualTaskReviewCandidate === undefined) {
|
|
@@ -4646,7 +4471,7 @@ function taskReviewProducerCollision(provenance, reviewerRoleName) {
|
|
|
4646
4471
|
*
|
|
4647
4472
|
* When `expected` is supplied this is also the final dispatch compare-and-swap
|
|
4648
4473
|
* fence: every bound Project must still point at the exact frozen physical
|
|
4649
|
-
* head. Drift fails closed before a Reviewer
|
|
4474
|
+
* head. Drift fails closed before a Reviewer Turn is created.
|
|
4650
4475
|
*/
|
|
4651
4476
|
function taskReviewProvenance(store, task, options, expected) {
|
|
4652
4477
|
const candidate = actualTaskReviewCandidateForMutation(store, task, options);
|
|
@@ -4707,13 +4532,13 @@ function taskReviewProvenance(store, task, options, expected) {
|
|
|
4707
4532
|
recordProducer(LEADER_ROLE, item.id);
|
|
4708
4533
|
continue;
|
|
4709
4534
|
}
|
|
4710
|
-
const sourceRun = store.
|
|
4535
|
+
const sourceRun = store.getTurn(task.id, itemCandidate.source.turnId);
|
|
4711
4536
|
if (sourceRun === null
|
|
4712
4537
|
|| sourceRun.workItemId !== item.id
|
|
4713
4538
|
|| sourceRun.purpose !== "execution"
|
|
4714
|
-
|| sourceRun.status !== "
|
|
4715
|
-
throw dataError(`Committed producer Candidate
|
|
4716
|
-
+ `${item.id}/${itemCandidate.source.
|
|
4539
|
+
|| sourceRun.status !== "completed") {
|
|
4540
|
+
throw dataError(`Committed producer Candidate Turn is unavailable: `
|
|
4541
|
+
+ `${item.id}/${itemCandidate.source.turnId}.`);
|
|
4717
4542
|
}
|
|
4718
4543
|
recordProducer(sourceRun.roleName, item.id);
|
|
4719
4544
|
}
|
|
@@ -4771,7 +4596,7 @@ function queueTaskReviewRound(store, task, config, taskCandidate, options, now,
|
|
|
4771
4596
|
const availability = projectReviewerAvailability(store, task.id, config.roleName);
|
|
4772
4597
|
if (availability.kind === "busy") {
|
|
4773
4598
|
throw usageError(`Reviewer ${config.roleName} is busy (${availability.phase}`
|
|
4774
|
-
+ `${availability.
|
|
4599
|
+
+ `${availability.activeTurnId === undefined ? "" : `; Turn ${availability.activeTurnId}`}); `
|
|
4775
4600
|
+ `retry after ${availability.retryAfterSeconds}s.`);
|
|
4776
4601
|
}
|
|
4777
4602
|
let reviewer = store.getRole(task.id, config.roleName);
|
|
@@ -4837,8 +4662,8 @@ function resumablePendingFinalTaskReview(store, task, round, config, taskCandida
|
|
|
4837
4662
|
|| round.reviewerRoleName !== taskFinalContract.reviewerRoleName) {
|
|
4838
4663
|
throw usageError(`Pending final ReviewRound Reviewer identity changed: ${round.id}.`);
|
|
4839
4664
|
}
|
|
4840
|
-
if (round.
|
|
4841
|
-
throw usageError(`Pending final ReviewRound already records Reviewer
|
|
4665
|
+
if (round.reviewerTurnId !== undefined) {
|
|
4666
|
+
throw usageError(`Pending final ReviewRound already records Reviewer Turn ${round.reviewerTurnId}: ${round.id}.`);
|
|
4842
4667
|
}
|
|
4843
4668
|
if (!isSameTaskReviewCandidate(round.taskCandidate, taskCandidate)) {
|
|
4844
4669
|
throw usageError(`Final ReviewRound ${round.id} freezes a candidate that is no longer the current Task candidate.`);
|
|
@@ -4848,8 +4673,8 @@ function resumablePendingFinalTaskReview(store, task, round, config, taskCandida
|
|
|
4848
4673
|
if (reviewer === null || reviewer.name !== round.reviewerRoleName) {
|
|
4849
4674
|
throw usageError(`Pending final ReviewRound Reviewer identity changed: ${round.id}.`);
|
|
4850
4675
|
}
|
|
4851
|
-
if (store.
|
|
4852
|
-
throw usageError(`Reviewer Role already has an active
|
|
4676
|
+
if (store.getActiveTurn(task.id, reviewer.name) !== null) {
|
|
4677
|
+
throw usageError(`Reviewer Role already has an active Turn: ${reviewer.name}.`);
|
|
4853
4678
|
}
|
|
4854
4679
|
assertPendingFinalReviewWorkspaceEvidence(store, task, round);
|
|
4855
4680
|
const provenance = taskReviewProvenance(store, task, options, taskCandidate);
|
|
@@ -4890,20 +4715,20 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
4890
4715
|
}
|
|
4891
4716
|
}
|
|
4892
4717
|
/**
|
|
4893
|
-
* Task-control retry of an exact failed Task-final review
|
|
4894
|
-
*
|
|
4718
|
+
* Task-control retry of an exact failed Task-final review Turn. The old failed
|
|
4719
|
+
* Turn remains the attempt trail, while the semantic ReviewRound is reset to
|
|
4895
4720
|
* pending under its existing identity. Every identity and frozen-head fence is
|
|
4896
4721
|
* checked inside one transaction so a partial fail-old-without-reset state can
|
|
4897
4722
|
* never be committed.
|
|
4898
4723
|
*/
|
|
4899
4724
|
function retryFailedReviewRun(previous, store, options, now) {
|
|
4900
4725
|
const result = store.transaction((tx) => {
|
|
4901
|
-
const run = tx.
|
|
4726
|
+
const run = tx.getTurn(previous.taskId, previous.id);
|
|
4902
4727
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
4903
|
-
throw usageError(`
|
|
4728
|
+
throw usageError(`Turn ${previous.id} is not a failed review Turn.`);
|
|
4904
4729
|
}
|
|
4905
4730
|
if (run.reviewRoundId === undefined) {
|
|
4906
|
-
throw usageError(`Review
|
|
4731
|
+
throw usageError(`Review Turn ${run.id} has no ReviewRound.`);
|
|
4907
4732
|
}
|
|
4908
4733
|
const task = requireTask(tx, run.taskId);
|
|
4909
4734
|
if (task.status !== "active")
|
|
@@ -4914,7 +4739,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4914
4739
|
throw dataError(`ReviewRound not found for run ${run.id}: ${run.reviewRoundId}.`);
|
|
4915
4740
|
}
|
|
4916
4741
|
if ((round.scope ?? "work-item") !== "task") {
|
|
4917
|
-
throw usageError(`Review
|
|
4742
|
+
throw usageError(`Review Turn ${run.id} is not a Task-final review; request a new WorkItem review `
|
|
4918
4743
|
+ "for a new Candidate.");
|
|
4919
4744
|
}
|
|
4920
4745
|
const retryLane = run.executionLaneId === undefined
|
|
@@ -4922,9 +4747,9 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4922
4747
|
: round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
|
|
4923
4748
|
if (run.executionGroupId !== undefined && (round.executionGroup?.id !== run.executionGroupId
|
|
4924
4749
|
|| retryLane === undefined
|
|
4925
|
-
|| retryLane.
|
|
4750
|
+
|| retryLane.turnId !== run.id
|
|
4926
4751
|
|| retryLane.roleName !== run.roleName)) {
|
|
4927
|
-
throw usageError(`Review
|
|
4752
|
+
throw usageError(`Review Turn ${run.id} no longer owns its exact Review Lane attempt.`);
|
|
4928
4753
|
}
|
|
4929
4754
|
const panelGroup = round.executionGroup !== undefined
|
|
4930
4755
|
&& (round.executionGroup.lanes.length > 1
|
|
@@ -4933,7 +4758,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4933
4758
|
&& panelGroup
|
|
4934
4759
|
&& retryLane?.status === "failed";
|
|
4935
4760
|
if (round.status === "running" && panelGroup && !runningPanelLaneRetry) {
|
|
4936
|
-
throw usageError(`Review
|
|
4761
|
+
throw usageError(`Review Turn ${run.id} is not the current failed Lane attempt in running Round ${round.id}.`);
|
|
4937
4762
|
}
|
|
4938
4763
|
const retryReviewerRoleName = retryLane?.roleName ?? round.reviewerRoleName;
|
|
4939
4764
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
@@ -4977,12 +4802,12 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4977
4802
|
}));
|
|
4978
4803
|
const hasMailboxWork = (mailbox) => (mailbox !== null && workMailboxHasWork(mailbox));
|
|
4979
4804
|
const reviewer = requireRole(tx, task.id, retryReviewerRoleName);
|
|
4980
|
-
const activePointer = tx.
|
|
4981
|
-
const
|
|
4805
|
+
const activePointer = tx.getActiveTurn(task.id, reviewer.name);
|
|
4806
|
+
const activeReviewerTurns = tx.listTurns(task.id).filter((entry) => (entry.roleName === reviewer.name && entry.status === "active"));
|
|
4982
4807
|
// Issue 06: a completed same-Round retry is a no-write idempotent result.
|
|
4983
4808
|
if (round.status === "completed") {
|
|
4984
|
-
if (activePointer !== null ||
|
|
4985
|
-
throw usageError(`Reviewer Role already has an active
|
|
4809
|
+
if (activePointer !== null || activeReviewerTurns.length > 0) {
|
|
4810
|
+
throw usageError(`Reviewer Role already has an active Turn: ${reviewer.name}.`);
|
|
4986
4811
|
}
|
|
4987
4812
|
if (hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
4988
4813
|
throw usageError(`Reviewer has unrelated mailbox work: ${reviewer.name}.`);
|
|
@@ -4990,24 +4815,24 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4990
4815
|
return { round, previousRun: run, created: false };
|
|
4991
4816
|
}
|
|
4992
4817
|
// Issue 06: a running same Round is reusable only with its exact active
|
|
4993
|
-
//
|
|
4818
|
+
// Turn and mailbox execution. A stranded Turn (no active pointer) falls
|
|
4994
4819
|
// through and resets the Round after the identity fences below.
|
|
4995
4820
|
if (round.status === "running" && !runningPanelLaneRetry) {
|
|
4996
|
-
const
|
|
4997
|
-
const activeMatches =
|
|
4821
|
+
const reviewerTurnId = round.reviewerTurnId;
|
|
4822
|
+
const activeMatches = reviewerTurnId !== undefined
|
|
4998
4823
|
&& activePointer !== null
|
|
4999
|
-
&& activePointer.id ===
|
|
4824
|
+
&& activePointer.id === reviewerTurnId
|
|
5000
4825
|
&& activePointer.status === "active"
|
|
5001
|
-
&&
|
|
5002
|
-
&&
|
|
5003
|
-
const processingMatches =
|
|
4826
|
+
&& activeReviewerTurns.length === 1
|
|
4827
|
+
&& activeReviewerTurns[0].id === reviewerTurnId;
|
|
4828
|
+
const processingMatches = reviewerTurnId !== undefined
|
|
5004
4829
|
&& reviewerMailbox?.processing?.executionRef !== undefined
|
|
5005
|
-
&& isDeepStrictEqual(reviewerMailbox.processing.executionRef,
|
|
4830
|
+
&& isDeepStrictEqual(reviewerMailbox.processing.executionRef, turnRef(task.id, reviewerTurnId))
|
|
5006
4831
|
&& reviewerPending === null;
|
|
5007
|
-
const pendingMatches =
|
|
4832
|
+
const pendingMatches = reviewerTurnId !== undefined
|
|
5008
4833
|
&& reviewerMailbox?.processing === null
|
|
5009
4834
|
&& reviewerPending?.requestCount === 1
|
|
5010
|
-
&& reviewerPending.refs.some((ref) => (isDeepStrictEqual(ref,
|
|
4835
|
+
&& reviewerPending.refs.some((ref) => (isDeepStrictEqual(ref, turnRef(task.id, reviewerTurnId))));
|
|
5011
4836
|
if (activePointer !== null
|
|
5012
4837
|
&& (!activeMatches
|
|
5013
4838
|
|| (!processingMatches && !pendingMatches)
|
|
@@ -5019,16 +4844,16 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
5019
4844
|
}
|
|
5020
4845
|
// Issue 06: an already-pending Round is the idempotent retry result.
|
|
5021
4846
|
if (round.status === "pending") {
|
|
5022
|
-
if (activePointer !== null ||
|
|
5023
|
-
throw usageError(`Reviewer Role already has an active
|
|
4847
|
+
if (activePointer !== null || activeReviewerTurns.length > 0) {
|
|
4848
|
+
throw usageError(`Reviewer Role already has an active Turn: ${reviewer.name}.`);
|
|
5024
4849
|
}
|
|
5025
4850
|
if (hasMailboxWork(reviewerMailbox) || hasMailboxWork(runtimeMailbox)) {
|
|
5026
4851
|
throw usageError(`Reviewer has unrelated mailbox work: ${reviewer.name}.`);
|
|
5027
4852
|
}
|
|
5028
4853
|
return { round, previousRun: run, created: false };
|
|
5029
4854
|
}
|
|
5030
|
-
if (activePointer !== null ||
|
|
5031
|
-
throw usageError(`Reviewer Role already has an active
|
|
4855
|
+
if (activePointer !== null || activeReviewerTurns.length > 0) {
|
|
4856
|
+
throw usageError(`Reviewer Role already has an active Turn: ${reviewer.name}.`);
|
|
5032
4857
|
}
|
|
5033
4858
|
if (runtimeMailbox?.processing !== null && runtimeMailbox?.processing !== undefined) {
|
|
5034
4859
|
throw usageError(`Reviewer runtime lifecycle is pending: ${reviewer.name}.`);
|
|
@@ -5036,36 +4861,36 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
5036
4861
|
if (runtimeMailbox !== null && workMailboxHasWork(runtimeMailbox)) {
|
|
5037
4862
|
throw usageError(`Reviewer runtime lifecycle has pending work: ${reviewer.name}.`);
|
|
5038
4863
|
}
|
|
5039
|
-
const validation =
|
|
4864
|
+
const validation = validateExactTurnReviewRound(tx, run, { allowTerminal: true });
|
|
5040
4865
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
5041
|
-
throw usageError(`Review
|
|
4866
|
+
throw usageError(`Review Turn ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
5042
4867
|
}
|
|
5043
|
-
// A stranded pre-delivery
|
|
4868
|
+
// A stranded pre-delivery Turn can leave its exact pending or processing
|
|
5044
4869
|
// dispatch behind; settle only that exact reference while holding the
|
|
5045
4870
|
// same aggregate lock. Any merged or unrelated batch fails closed.
|
|
5046
|
-
const
|
|
4871
|
+
const exactOldTurnRef = turnRef(task.id, run.id);
|
|
5047
4872
|
if (reviewerMailbox?.processing !== null && reviewerMailbox?.processing !== undefined) {
|
|
5048
4873
|
if (reviewerMailbox.processing.executionRef === undefined
|
|
5049
|
-
|| !isDeepStrictEqual(reviewerMailbox.processing.executionRef,
|
|
4874
|
+
|| !isDeepStrictEqual(reviewerMailbox.processing.executionRef, exactOldTurnRef)
|
|
5050
4875
|
|| reviewerPending !== null) {
|
|
5051
4876
|
throw usageError(`Reviewer mailbox is busy: ${reviewer.name}.`);
|
|
5052
4877
|
}
|
|
5053
|
-
settleExactWorkExecution(tx, reviewerMailboxTarget,
|
|
4878
|
+
settleExactWorkExecution(tx, reviewerMailboxTarget, exactOldTurnRef);
|
|
5054
4879
|
}
|
|
5055
4880
|
else if (reviewerPending !== null) {
|
|
5056
|
-
const exact = reviewerPending.refs.some((ref) => (isDeepStrictEqual(ref,
|
|
4881
|
+
const exact = reviewerPending.refs.some((ref) => (isDeepStrictEqual(ref, exactOldTurnRef)));
|
|
5057
4882
|
if (!exact)
|
|
5058
4883
|
throw usageError(`Reviewer mailbox has unrelated pending work: ${reviewer.name}.`);
|
|
5059
4884
|
if (reviewerPending.requestCount !== 1) {
|
|
5060
4885
|
throw usageError(`Reviewer mailbox has merged pending work: ${reviewer.name}.`);
|
|
5061
4886
|
}
|
|
5062
|
-
settleExactWorkExecution(tx, reviewerMailboxTarget,
|
|
4887
|
+
settleExactWorkExecution(tx, reviewerMailboxTarget, exactOldTurnRef);
|
|
5063
4888
|
}
|
|
5064
4889
|
if (runningPanelLaneRetry) {
|
|
5065
4890
|
const resetRound = retryRunningReviewExecutionLane(round, retryLane.id, run.id, now);
|
|
5066
4891
|
tx.saveReviewRound(task.id, resetRound);
|
|
5067
|
-
recordTaskEvent(tx, task.id, "
|
|
5068
|
-
|
|
4892
|
+
recordTaskEvent(tx, task.id, "turn.review-retried", {
|
|
4893
|
+
turnId: run.id,
|
|
5069
4894
|
reviewRoundId: round.id,
|
|
5070
4895
|
executionLaneId: retryLane.id
|
|
5071
4896
|
}, now);
|
|
@@ -5076,8 +4901,8 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
5076
4901
|
let roundToReset = round;
|
|
5077
4902
|
if (round.status !== "failed") {
|
|
5078
4903
|
const summary = round.summary
|
|
5079
|
-
?? run.
|
|
5080
|
-
?? `Review
|
|
4904
|
+
?? run.result?.output
|
|
4905
|
+
?? `Review Turn ${run.id} failed before delivery.`;
|
|
5081
4906
|
roundToReset = finishReviewRound(round, "failed", summary, now, {
|
|
5082
4907
|
report: round.report ?? summary,
|
|
5083
4908
|
checks: round.checks ?? [],
|
|
@@ -5090,8 +4915,8 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
5090
4915
|
// stable across execution-attempt failures.
|
|
5091
4916
|
const resetRound = retryTaskReviewRound(roundToReset, requestedBy, run.executionLaneId);
|
|
5092
4917
|
tx.saveReviewRound(task.id, resetRound);
|
|
5093
|
-
recordTaskEvent(tx, task.id, "
|
|
5094
|
-
|
|
4918
|
+
recordTaskEvent(tx, task.id, "turn.review-retried", {
|
|
4919
|
+
turnId: run.id,
|
|
5095
4920
|
reviewRoundId: round.id
|
|
5096
4921
|
}, now);
|
|
5097
4922
|
return { round: resetRound, previousRun: run, created: true };
|
|
@@ -5100,39 +4925,31 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
5100
4925
|
? `Review retry requested as ${result.round.id}\n`
|
|
5101
4926
|
: `Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round });
|
|
5102
4927
|
}
|
|
5103
|
-
/**
|
|
5104
|
-
function
|
|
5105
|
-
const usage = "Task
|
|
4928
|
+
/** Turn details are retained audit evidence; continuation uses durable Task state. */
|
|
4929
|
+
function showTurn(args, store, options) {
|
|
4930
|
+
const usage = "Task turn show usage: yui task turn show <task>/<turn> [--json].";
|
|
5106
4931
|
const asJson = args.includes("--json");
|
|
5107
4932
|
const positionals = args.filter((arg) => arg !== "--json");
|
|
5108
4933
|
exactPositionals(positionals, 1, usage);
|
|
5109
4934
|
const data = store.transaction((tx) => {
|
|
5110
|
-
const run =
|
|
4935
|
+
const run = requireTurn(tx, positionals[0], options);
|
|
5111
4936
|
const retirement = tx.listEvents(run.taskId)
|
|
5112
4937
|
.map(taskRecordRetirement)
|
|
5113
|
-
.find((entry) => entry?.recordKind === "
|
|
5114
|
-
|
|
5115
|
-
.map(rejectedYieldAttemptFromTaskEvent)
|
|
5116
|
-
.filter((attempt) => (attempt !== null && attempt.runId === run.id));
|
|
5117
|
-
return {
|
|
5118
|
-
run,
|
|
5119
|
-
retirement,
|
|
5120
|
-
rejectedYieldAttemptCount: allRejectedYieldAttempts.length,
|
|
5121
|
-
rejectedYieldAttempts: allRejectedYieldAttempts.slice(-16)
|
|
5122
|
-
};
|
|
4938
|
+
.find((entry) => entry?.recordKind === "turn" && entry.recordId === run.id) ?? null;
|
|
4939
|
+
return { turn: run, retirement };
|
|
5123
4940
|
});
|
|
5124
4941
|
if (asJson) {
|
|
5125
4942
|
return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
|
|
5126
4943
|
}
|
|
5127
4944
|
return {
|
|
5128
4945
|
kind: "output",
|
|
5129
|
-
output:
|
|
4946
|
+
output: renderTurnShow(data.turn, data.retirement),
|
|
5130
4947
|
data
|
|
5131
4948
|
};
|
|
5132
4949
|
}
|
|
5133
|
-
function
|
|
4950
|
+
function renderTurnShow(run, retirement) {
|
|
5134
4951
|
const lines = [
|
|
5135
|
-
`
|
|
4952
|
+
`Turn: ${run.id}`,
|
|
5136
4953
|
`Task: ${run.taskId}`,
|
|
5137
4954
|
`Role: ${run.roleName}`,
|
|
5138
4955
|
`Purpose: ${run.purpose}`,
|
|
@@ -5144,589 +4961,48 @@ function renderRunShow(run, retirement, rejectedYieldAttemptCount, rejectedYield
|
|
|
5144
4961
|
]),
|
|
5145
4962
|
`Effective: ${run.effective.agentId}/${run.effective.adapterId} r${run.effective.sourceDesiredRevision}`,
|
|
5146
4963
|
`Created: ${run.createdAt}`,
|
|
5147
|
-
...(run.
|
|
5148
|
-
...(run.
|
|
5149
|
-
...(run.deliveredAt === undefined
|
|
4964
|
+
...(run.result === undefined ? [] : [`Ended: ${run.result.completedAt}`]),
|
|
4965
|
+
...(run.result === undefined || run.result.output.trim().length === 0
|
|
5150
4966
|
? []
|
|
5151
|
-
: [`
|
|
5152
|
-
...(run.summary === undefined || run.summary.trim().length === 0
|
|
5153
|
-
? []
|
|
5154
|
-
: [`Summary: ${run.summary}`]),
|
|
5155
|
-
...(rejectedYieldAttemptCount === 0
|
|
5156
|
-
? []
|
|
5157
|
-
: [
|
|
5158
|
-
`Rejected yield attempts: ${rejectedYieldAttempts.length} shown of ${rejectedYieldAttemptCount} (unaccepted diagnostic evidence only)`,
|
|
5159
|
-
...rejectedYieldAttempts.slice(-5).flatMap((attempt) => [
|
|
5160
|
-
` ${attempt.attemptedAt}: ${attempt.rejectionReason} (${attempt.attemptDigest.slice(0, 16)})`,
|
|
5161
|
-
` Reviewer output: ${truncateRunShowText(attempt.summary)}`,
|
|
5162
|
-
` Checks: ${attempt.checks.length === 0
|
|
5163
|
-
? "none"
|
|
5164
|
-
: attempt.checks.map(({ name, outcome }) => `${name}:${outcome}`).join(", ")}`
|
|
5165
|
-
])
|
|
5166
|
-
])
|
|
4967
|
+
: [`Summary: ${run.result.output}`])
|
|
5167
4968
|
];
|
|
5168
4969
|
return `${lines.join("\n")}\n`;
|
|
5169
4970
|
}
|
|
5170
|
-
function truncateRunShowText(value) {
|
|
5171
|
-
const normalized = value.replaceAll(/\s+/gu, " ").trim();
|
|
5172
|
-
return normalized.length <= 400 ? normalized : `${normalized.slice(0, 399)}…`;
|
|
5173
|
-
}
|
|
5174
|
-
/**
|
|
5175
|
-
* Issue 04: builds the terminal yield outcome from the command inputs. The
|
|
5176
|
-
* same construction feeds both the first commit and the idempotent replay, so
|
|
5177
|
-
* a resend always hashes to the same digest.
|
|
5178
|
-
*/
|
|
5179
|
-
function buildYieldOutcome(run, inputSummary, options) {
|
|
5180
|
-
let yieldedReport;
|
|
5181
|
-
if (run.purpose === "review"
|
|
5182
|
-
|| (run.purpose === "execution" && run.executionGroupId !== undefined)) {
|
|
5183
|
-
yieldedReport = parseReviewYieldReport(inputSummary);
|
|
5184
|
-
}
|
|
5185
|
-
const summary = yieldedReport?.summary ?? inputSummary;
|
|
5186
|
-
if (yieldedReport === undefined)
|
|
5187
|
-
return { summary };
|
|
5188
|
-
return {
|
|
5189
|
-
summary,
|
|
5190
|
-
reviewResult: {
|
|
5191
|
-
report: yieldedReport.report,
|
|
5192
|
-
checks: yieldedReport.checks,
|
|
5193
|
-
...(yieldedReport.findings === undefined ? {} : { findings: yieldedReport.findings }),
|
|
5194
|
-
...(yieldedReport.evidence === undefined ? {} : { evidence: yieldedReport.evidence }),
|
|
5195
|
-
...(run.purpose === "review"
|
|
5196
|
-
&& options.reviewWorkspaceResult?.evidenceCommit === undefined
|
|
5197
|
-
? {}
|
|
5198
|
-
: run.purpose === "review"
|
|
5199
|
-
? { evidenceCommit: options.reviewWorkspaceResult.evidenceCommit }
|
|
5200
|
-
: yieldedReport.evidenceCommit === undefined
|
|
5201
|
-
? {}
|
|
5202
|
-
: { evidenceCommit: yieldedReport.evidenceCommit }),
|
|
5203
|
-
...(options.executionLaneGitSnapshot === undefined
|
|
5204
|
-
|| options.executionLaneGitSnapshot === null
|
|
5205
|
-
? {}
|
|
5206
|
-
: { gitSnapshot: options.executionLaneGitSnapshot }),
|
|
5207
|
-
...(yieldedReport.deltaDisposition === undefined
|
|
5208
|
-
? {}
|
|
5209
|
-
: { deltaDisposition: yieldedReport.deltaDisposition }),
|
|
5210
|
-
...(yieldedReport.deltaReasoning === undefined
|
|
5211
|
-
? {}
|
|
5212
|
-
: { deltaReasoning: yieldedReport.deltaReasoning })
|
|
5213
|
-
}
|
|
5214
|
-
};
|
|
5215
|
-
}
|
|
5216
|
-
/**
|
|
5217
|
-
* Issue 04: replays an already-committed yield. Returns the committed receipt
|
|
5218
|
-
* for the same outcome, fails closed for a different outcome, or returns
|
|
5219
|
-
* `null` to keep the legacy "already terminal" behavior.
|
|
5220
|
-
*/
|
|
5221
|
-
function replayYieldReceipt(run, inputSummary, options) {
|
|
5222
|
-
if (run.yieldReceipt === undefined)
|
|
5223
|
-
return null;
|
|
5224
|
-
const outcome = buildYieldOutcome(run, inputSummary, options);
|
|
5225
|
-
const match = matchYieldReceipt(run.yieldReceipt, {
|
|
5226
|
-
status: "yielded",
|
|
5227
|
-
summary: outcome.summary,
|
|
5228
|
-
...(outcome.reviewResult === undefined ? {} : { reviewResult: outcome.reviewResult })
|
|
5229
|
-
});
|
|
5230
|
-
if (match === null)
|
|
5231
|
-
return null;
|
|
5232
|
-
if (match.kind === "digest-mismatch") {
|
|
5233
|
-
throw usageError(`Run ${run.id} is already terminal with a different yield outcome. `
|
|
5234
|
-
+ `Existing receipt: ${match.existing.receiptId} (request ${match.existing.requestId}).`);
|
|
5235
|
-
}
|
|
5236
|
-
return output(`Run ${run.id} yield already committed.\n`
|
|
5237
|
-
+ `Receipt: ${match.receipt.receiptId}\n`
|
|
5238
|
-
+ `Request: ${match.receipt.requestId}\n`, { receipt: match.receipt });
|
|
5239
|
-
}
|
|
5240
|
-
/**
|
|
5241
|
-
* Issue 04: `yui task run yield-status <task>/<run>` — returns the committed
|
|
5242
|
-
* yield receipt for a terminal Run, or the current status for an active Run.
|
|
5243
|
-
*/
|
|
5244
|
-
function yieldRunStatus(args, store, options) {
|
|
5245
|
-
const usage = "Task run yield-status usage: yui task run yield-status <task>/<run>.";
|
|
5246
|
-
const parsed = parseTail(args, new Set(), usage);
|
|
5247
|
-
exactPositionals(parsed.positionals, 1, usage);
|
|
5248
|
-
const run = requireRun(store, parsed.positionals[0], options);
|
|
5249
|
-
if (run.status === "active") {
|
|
5250
|
-
return output(`Run ${run.id} is active; no yield receipt yet.\n`, {
|
|
5251
|
-
runId: run.id,
|
|
5252
|
-
status: run.status
|
|
5253
|
-
});
|
|
5254
|
-
}
|
|
5255
|
-
if (run.yieldReceipt === undefined) {
|
|
5256
|
-
return output(`Run ${run.id} is ${run.status}; no yield receipt recorded.\n`, {
|
|
5257
|
-
runId: run.id,
|
|
5258
|
-
status: run.status
|
|
5259
|
-
});
|
|
5260
|
-
}
|
|
5261
|
-
return output(`Run ${run.id} yield receipt:\n`
|
|
5262
|
-
+ `Receipt: ${run.yieldReceipt.receiptId}\n`
|
|
5263
|
-
+ `Request: ${run.yieldReceipt.requestId}\n`
|
|
5264
|
-
+ `Committed: ${run.yieldReceipt.committedAt}\n`, { receipt: run.yieldReceipt });
|
|
5265
|
-
}
|
|
5266
|
-
function recordRejectedReviewYield(store, input) {
|
|
5267
|
-
if (input.run.purpose !== "review" || input.run.reviewRoundId === undefined) {
|
|
5268
|
-
throw new Error(`Only a Review Run can record rejected yield evidence: ${input.run.id}.`);
|
|
5269
|
-
}
|
|
5270
|
-
const receiptId = agentRunDeliveryReceiptId(input.run);
|
|
5271
|
-
const sessions = store.getTaskRoleSessionSet(input.run.taskId, input.run.roleName);
|
|
5272
|
-
const durableSession = sessions?.sessions[input.run.effective.agentId];
|
|
5273
|
-
const attempt = createRejectedYieldAttempt({
|
|
5274
|
-
taskId: input.run.taskId,
|
|
5275
|
-
runId: input.run.id,
|
|
5276
|
-
roleName: input.run.roleName,
|
|
5277
|
-
reviewRoundId: input.run.reviewRoundId,
|
|
5278
|
-
receiptId,
|
|
5279
|
-
rejectionReason: input.rejectionReason,
|
|
5280
|
-
summary: input.outcome.summary,
|
|
5281
|
-
...(input.outcome.reviewResult === undefined
|
|
5282
|
-
? {}
|
|
5283
|
-
: { reviewResult: input.outcome.reviewResult }),
|
|
5284
|
-
...(input.nativeSessionId === undefined
|
|
5285
|
-
? {}
|
|
5286
|
-
: { nativeSessionId: input.nativeSessionId }),
|
|
5287
|
-
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
5288
|
-
...(durableSession?.nativeSessionId === undefined
|
|
5289
|
-
? {}
|
|
5290
|
-
: { durableNativeSessionId: durableSession.nativeSessionId }),
|
|
5291
|
-
...(durableSession?.launchId === undefined
|
|
5292
|
-
? {}
|
|
5293
|
-
: { durableLaunchId: durableSession.launchId }),
|
|
5294
|
-
...(sessions?.inFlight?.runId === undefined
|
|
5295
|
-
? {}
|
|
5296
|
-
: { inFlightRunId: sessions.inFlight.runId }),
|
|
5297
|
-
...(sessions?.inFlight?.receiptId === undefined
|
|
5298
|
-
? {}
|
|
5299
|
-
: { inFlightReceiptId: sessions.inFlight.receiptId }),
|
|
5300
|
-
...(input.run.assignment.contextSnapshotRef === undefined
|
|
5301
|
-
? {}
|
|
5302
|
-
: { contextSnapshot: input.run.assignment.contextSnapshotRef }),
|
|
5303
|
-
activeRun: input.activeRun === null
|
|
5304
|
-
? null
|
|
5305
|
-
: {
|
|
5306
|
-
id: input.activeRun.id,
|
|
5307
|
-
receiptId: agentRunDeliveryReceiptId(input.activeRun),
|
|
5308
|
-
...(input.activeRun.assignment.contextSnapshotRef === undefined
|
|
5309
|
-
? {}
|
|
5310
|
-
: { contextSnapshot: input.activeRun.assignment.contextSnapshotRef })
|
|
5311
|
-
},
|
|
5312
|
-
attemptedAt: input.now
|
|
5313
|
-
});
|
|
5314
|
-
const existing = store.listEvents(input.run.taskId).find((event) => (event.type === RUN_YIELD_REJECTED_EVENT
|
|
5315
|
-
&& event.payload.runId === input.run.id
|
|
5316
|
-
&& event.payload.receiptId === receiptId
|
|
5317
|
-
&& event.payload.attemptDigest === attempt.attemptDigest));
|
|
5318
|
-
if (existing !== undefined) {
|
|
5319
|
-
const persisted = rejectedYieldAttemptFromTaskEvent(existing);
|
|
5320
|
-
if (persisted === null) {
|
|
5321
|
-
throw new Error(`Rejected yield attempt is malformed: ${existing.id}.`);
|
|
5322
|
-
}
|
|
5323
|
-
return { attempt: persisted, event: existing, created: false };
|
|
5324
|
-
}
|
|
5325
|
-
const event = createTaskEvent(store.nextEventId(input.run.taskId), input.run.taskId, RUN_YIELD_REJECTED_EVENT, rejectedYieldAttemptEventPayload(attempt), input.now);
|
|
5326
|
-
store.saveEvent(input.run.taskId, event);
|
|
5327
|
-
routeRoleEvent(store, event, input.run.roleName, "run-yield-rejected", input.now);
|
|
5328
|
-
return { attempt, event, created: true };
|
|
5329
|
-
}
|
|
5330
|
-
function yieldRun(args, store, options) {
|
|
5331
|
-
const usage = "Task run yield usage: yui task run yield <task>/<run> (--summary <text>|--summary-file <path|->).";
|
|
5332
|
-
const parsed = parseTail(args, new Set(["--summary", "--summary-file"]), usage);
|
|
5333
|
-
exactPositionals(parsed.positionals, 1, usage);
|
|
5334
|
-
const inputSummary = readCommandText(parsed.options.get("--summary"), parsed.options.get("--summary-file"), "--summary", usage);
|
|
5335
|
-
const now = clock(options);
|
|
5336
|
-
// Issue 04: an already-terminal Run may be a lost-response resend. Match
|
|
5337
|
-
// the presented outcome against the committed receipt before opening the
|
|
5338
|
-
// transaction; the receipt is immutable once committed.
|
|
5339
|
-
const existing = requireRun(store, parsed.positionals[0], options);
|
|
5340
|
-
if (existing.status !== "active") {
|
|
5341
|
-
const replayed = replayYieldReceipt(existing, inputSummary, options);
|
|
5342
|
-
if (replayed !== null)
|
|
5343
|
-
return replayed;
|
|
5344
|
-
throw usageError(`Run ${existing.id} is already terminal: ${existing.status}.`);
|
|
5345
|
-
}
|
|
5346
|
-
const yielded = store.transaction((tx) => {
|
|
5347
|
-
const active = requireRun(tx, parsed.positionals[0], options);
|
|
5348
|
-
if (active.status !== "active") {
|
|
5349
|
-
throw usageError(`Run ${active.id} is already terminal: ${active.status}.`);
|
|
5350
|
-
}
|
|
5351
|
-
if (active.pushedAt === undefined) {
|
|
5352
|
-
throw usageError(`Run ${active.id} delivery is still pending.`);
|
|
5353
|
-
}
|
|
5354
|
-
const task = requireTask(tx, active.taskId);
|
|
5355
|
-
if (task.status !== "active")
|
|
5356
|
-
throw usageError(inactiveTaskMessage(task, "yielding a run"));
|
|
5357
|
-
const role = requireRole(tx, task.id, active.roleName);
|
|
5358
|
-
const pointer = activeRunPointer(tx, active);
|
|
5359
|
-
const taskFinalContract = active.purpose === "execution"
|
|
5360
|
-
&& active.workItemId !== undefined
|
|
5361
|
-
? taskFinalReviewContractForMutation(tx, task.id, options)
|
|
5362
|
-
: undefined;
|
|
5363
|
-
const wasStalled = isRoleRunStalled(tx.listEvents(task.id), active.id);
|
|
5364
|
-
let yieldedReport;
|
|
5365
|
-
if (active.purpose === "review"
|
|
5366
|
-
|| (active.purpose === "execution" && active.executionGroupId !== undefined)) {
|
|
5367
|
-
try {
|
|
5368
|
-
yieldedReport = parseReviewYieldReport(inputSummary);
|
|
5369
|
-
}
|
|
5370
|
-
catch (error) {
|
|
5371
|
-
throw usageError(error instanceof Error ? error.message : String(error));
|
|
5372
|
-
}
|
|
5373
|
-
}
|
|
5374
|
-
const groupedLaneRun = active.executionGroupId !== undefined
|
|
5375
|
-
&& active.executionLaneId !== undefined;
|
|
5376
|
-
if (active.purpose !== "review"
|
|
5377
|
-
&& groupedLaneRun
|
|
5378
|
-
&& options.yuiHome !== undefined
|
|
5379
|
-
&& options.executionLaneGitSnapshot === undefined) {
|
|
5380
|
-
throw usageError(`Managed Execution Lane Git snapshot preflight is missing: ${active.id}.`);
|
|
5381
|
-
}
|
|
5382
|
-
if (active.purpose === "review") {
|
|
5383
|
-
if (options.reviewWorkspaceResult === undefined) {
|
|
5384
|
-
throw usageError(`Review Run requires managed workspace preflight: ${active.id}.`);
|
|
5385
|
-
}
|
|
5386
|
-
if (yieldedReport?.evidenceCommit !== undefined
|
|
5387
|
-
&& yieldedReport.evidenceCommit !== options.reviewWorkspaceResult.evidenceCommit) {
|
|
5388
|
-
throw usageError(`Reported Review evidence commit does not match the managed workspace: ${active.id}.`);
|
|
5389
|
-
}
|
|
5390
|
-
}
|
|
5391
|
-
const yieldOutcome = buildYieldOutcome(active, inputSummary, options);
|
|
5392
|
-
const summary = yieldOutcome.summary;
|
|
5393
|
-
const terminalization = terminalizeExactTaskRun(tx, {
|
|
5394
|
-
taskId: task.id,
|
|
5395
|
-
roleName: role.name,
|
|
5396
|
-
agentId: active.effective.agentId,
|
|
5397
|
-
runId: active.id,
|
|
5398
|
-
receiptId: agentRunDeliveryReceiptId(active),
|
|
5399
|
-
...(active.purpose !== "review" || options.environment?.YUI_NATIVE_SESSION_ID === undefined
|
|
5400
|
-
? {}
|
|
5401
|
-
: { nativeSessionId: options.environment.YUI_NATIVE_SESSION_ID }),
|
|
5402
|
-
...(active.purpose !== "review" || options.environment?.YUI_LAUNCH_ID === undefined
|
|
5403
|
-
? {}
|
|
5404
|
-
: { launchId: options.environment.YUI_LAUNCH_ID }),
|
|
5405
|
-
outcome: { status: "yielded", summary },
|
|
5406
|
-
...(yieldOutcome.reviewResult === undefined
|
|
5407
|
-
? {}
|
|
5408
|
-
: { reviewResult: yieldOutcome.reviewResult })
|
|
5409
|
-
}, now);
|
|
5410
|
-
if (terminalization.disposition !== "applied" || terminalization.run === null) {
|
|
5411
|
-
if (active.purpose === "review" && active.reviewRoundId !== undefined) {
|
|
5412
|
-
const rejected = recordRejectedReviewYield(tx, {
|
|
5413
|
-
run: active,
|
|
5414
|
-
activeRun: pointer,
|
|
5415
|
-
rejectionReason: terminalization.reason ?? "obsolete",
|
|
5416
|
-
outcome: yieldOutcome,
|
|
5417
|
-
...(options.environment?.YUI_NATIVE_SESSION_ID === undefined
|
|
5418
|
-
? {}
|
|
5419
|
-
: { nativeSessionId: options.environment.YUI_NATIVE_SESSION_ID }),
|
|
5420
|
-
...(options.environment?.YUI_LAUNCH_ID === undefined
|
|
5421
|
-
? {}
|
|
5422
|
-
: { launchId: options.environment.YUI_LAUNCH_ID }),
|
|
5423
|
-
now
|
|
5424
|
-
});
|
|
5425
|
-
return {
|
|
5426
|
-
kind: "rejected",
|
|
5427
|
-
run: active,
|
|
5428
|
-
rejected,
|
|
5429
|
-
notifyLeader: rejected.created
|
|
5430
|
-
};
|
|
5431
|
-
}
|
|
5432
|
-
throw usageError(`Run ${active.id} no longer matches its exact execution fence: `
|
|
5433
|
-
+ `${terminalization.reason ?? "obsolete"}.`);
|
|
5434
|
-
}
|
|
5435
|
-
const terminal = terminalization.run;
|
|
5436
|
-
let reviewGroupPending = false;
|
|
5437
|
-
let reviewGroupReady = false;
|
|
5438
|
-
if (wasStalled) {
|
|
5439
|
-
recordTaskEvent(tx, task.id, RUN_RECOVERED_EVENT, {
|
|
5440
|
-
runId: terminal.id,
|
|
5441
|
-
roleName: role.name,
|
|
5442
|
-
progressAt: now.toISOString(),
|
|
5443
|
-
kind: "yield"
|
|
5444
|
-
}, now);
|
|
5445
|
-
}
|
|
5446
|
-
if (active.purpose === "review") {
|
|
5447
|
-
const round = active.reviewRoundId === undefined
|
|
5448
|
-
? null
|
|
5449
|
-
: tx.getReviewRound(task.id, active.reviewRoundId);
|
|
5450
|
-
const panel = round?.executionGroup !== undefined
|
|
5451
|
-
&& (round.executionGroup.lanes.length > 1
|
|
5452
|
-
|| round.executionGroup.strategy.mode === "adaptive");
|
|
5453
|
-
if (round === null || round.reviewerRunId === undefined) {
|
|
5454
|
-
throw usageError(`ReviewRun did not complete its exact ReviewRound: ${active.id}.`);
|
|
5455
|
-
}
|
|
5456
|
-
if (panel) {
|
|
5457
|
-
reviewGroupPending = round.status !== "completed";
|
|
5458
|
-
reviewGroupReady = reviewGroupPending
|
|
5459
|
-
&& round.executionGroup.lanes.every(({ status }) => (["yielded", "completed", "failed"].includes(status)));
|
|
5460
|
-
if (reviewGroupReady) {
|
|
5461
|
-
const panelGroup = round.executionGroup;
|
|
5462
|
-
recordTaskEvent(tx, task.id, "review-group-ready", {
|
|
5463
|
-
reviewRoundId: round.id,
|
|
5464
|
-
...(round.workItemId === undefined ? {} : { workItemId: round.workItemId }),
|
|
5465
|
-
...(round.candidateId === undefined ? {} : { candidateId: round.candidateId }),
|
|
5466
|
-
executionGroupId: panelGroup.id,
|
|
5467
|
-
terminalLanes: String(panelGroup.lanes
|
|
5468
|
-
.filter(({ status }) => ["yielded", "completed", "failed"].includes(status)).length),
|
|
5469
|
-
laneCount: String(panelGroup.lanes.length)
|
|
5470
|
-
}, now);
|
|
5471
|
-
}
|
|
5472
|
-
}
|
|
5473
|
-
else {
|
|
5474
|
-
if (round.status !== "completed" || round.reviewerRunId !== active.id) {
|
|
5475
|
-
throw usageError(`ReviewRun did not complete its exact ReviewRound: ${active.id}.`);
|
|
5476
|
-
}
|
|
5477
|
-
recordTaskEvent(tx, task.id, "review.completed", {
|
|
5478
|
-
reviewRoundId: round.id,
|
|
5479
|
-
...(round.workItemId === undefined ? {} : { workItemId: round.workItemId }),
|
|
5480
|
-
...(round.candidateId === undefined ? {} : { candidateId: round.candidateId }),
|
|
5481
|
-
reviewBaseCommit: round.reviewBaseCommit,
|
|
5482
|
-
evidenceCommit: round.evidenceCommit ?? "none",
|
|
5483
|
-
checks: round.checks?.map(({ name, outcome }) => `${name}:${outcome}`)
|
|
5484
|
-
.join(",") || "none",
|
|
5485
|
-
...(round.deltaRecheck === undefined
|
|
5486
|
-
? {}
|
|
5487
|
-
: {
|
|
5488
|
-
reviewMode: "delta-recheck",
|
|
5489
|
-
deltaDisposition: round.deltaRecheck.disposition ?? "requires-full-review",
|
|
5490
|
-
previousReviewRoundId: round.deltaRecheck.previousReviewRoundId,
|
|
5491
|
-
diffDigest: round.deltaRecheck.diffDigest
|
|
5492
|
-
})
|
|
5493
|
-
}, now);
|
|
5494
|
-
}
|
|
5495
|
-
}
|
|
5496
|
-
let automaticReview = null;
|
|
5497
|
-
let submittedItem = null;
|
|
5498
|
-
let executionGroupPending = false;
|
|
5499
|
-
let executionGroupReady = false;
|
|
5500
|
-
if (active.purpose === "review") {
|
|
5501
|
-
// Saving the terminal review Run completes its ReviewRound in the same
|
|
5502
|
-
// aggregate transaction. The WorkItem remains the candidate under review.
|
|
5503
|
-
}
|
|
5504
|
-
else if (active.workItemId !== undefined) {
|
|
5505
|
-
const item = tx.getWorkItem(task.id, active.workItemId);
|
|
5506
|
-
if (item === null)
|
|
5507
|
-
throw dataError(`Work item not found for run ${active.id}: ${active.workItemId}.`);
|
|
5508
|
-
const currentGroup = currentWorkItemExecutionGroup(item);
|
|
5509
|
-
const multiLaneGroup = currentGroup !== undefined
|
|
5510
|
-
&& (currentGroup.stage !== undefined
|
|
5511
|
-
|| currentGroup.lanes.length > 1
|
|
5512
|
-
|| currentGroup.strategy.mode === "adaptive")
|
|
5513
|
-
&& currentGroup.resolution === undefined;
|
|
5514
|
-
if (multiLaneGroup) {
|
|
5515
|
-
executionGroupPending = true;
|
|
5516
|
-
executionGroupReady = currentGroup.lanes.every(({ status }) => (["yielded", "completed", "failed", "skipped"].includes(status)));
|
|
5517
|
-
if (executionGroupReady) {
|
|
5518
|
-
recordTaskEvent(tx, task.id, "execution-group-ready", {
|
|
5519
|
-
workItemId: item.id,
|
|
5520
|
-
executionGroupId: currentGroup.id,
|
|
5521
|
-
terminalLanes: String(currentGroup.lanes.length),
|
|
5522
|
-
laneCount: String(currentGroup.lanes.length)
|
|
5523
|
-
}, now);
|
|
5524
|
-
}
|
|
5525
|
-
}
|
|
5526
|
-
else {
|
|
5527
|
-
const configuredReview = tx.getReviewConfig();
|
|
5528
|
-
const candidatePolicy = taskFinalContract === undefined
|
|
5529
|
-
? legacyWorkItemReviewConfig(configuredReview)
|
|
5530
|
-
: taskFinalReviewConfig(taskFinalContract);
|
|
5531
|
-
const projectDelivery = task.projectBindings.length > 0
|
|
5532
|
-
&& item.writeProjectIds.length > 0;
|
|
5533
|
-
const candidateRequired = role.name !== LEADER_ROLE
|
|
5534
|
-
|| candidatePolicy !== null
|
|
5535
|
-
|| projectDelivery;
|
|
5536
|
-
let candidateSourceItem = item;
|
|
5537
|
-
if (candidateRequired
|
|
5538
|
-
&& currentGroup !== undefined
|
|
5539
|
-
&& currentGroup.resolution === undefined) {
|
|
5540
|
-
const resolvedSingle = resolveExecutionGroup(currentGroup, {
|
|
5541
|
-
decision: "accept",
|
|
5542
|
-
summary,
|
|
5543
|
-
selectedLaneIds: [active.executionLaneId]
|
|
5544
|
-
}, now);
|
|
5545
|
-
candidateSourceItem = updateWorkItemExecutionGroup(item, resolvedSingle, now);
|
|
5546
|
-
tx.saveWorkItem(task.id, candidateSourceItem);
|
|
5547
|
-
}
|
|
5548
|
-
const yieldedItem = candidateRequired
|
|
5549
|
-
? submitWorkItemCandidate(candidateSourceItem, {
|
|
5550
|
-
summary,
|
|
5551
|
-
source: { type: "run", runId: terminal.id },
|
|
5552
|
-
...(candidatePolicy === null ? {} : { reviewPolicy: candidatePolicy }),
|
|
5553
|
-
...(taskFinalContract === undefined
|
|
5554
|
-
? {}
|
|
5555
|
-
: { taskFinalReviewContract: taskFinalContract }),
|
|
5556
|
-
...(active.executionGroupId === undefined
|
|
5557
|
-
? {}
|
|
5558
|
-
: {
|
|
5559
|
-
executionGroupId: active.executionGroupId,
|
|
5560
|
-
executionLaneId: active.executionLaneId
|
|
5561
|
-
}),
|
|
5562
|
-
...(active.workspace === undefined
|
|
5563
|
-
|| (active.workspace.owner.type === "task"
|
|
5564
|
-
&& active.workspace.entries.length === 0)
|
|
5565
|
-
? {}
|
|
5566
|
-
: { workspace: active.workspace }),
|
|
5567
|
-
...(options.candidateGitSnapshot === undefined
|
|
5568
|
-
? {}
|
|
5569
|
-
: { gitSnapshot: options.candidateGitSnapshot })
|
|
5570
|
-
}, now)
|
|
5571
|
-
: updateWorkItemStatus(item, "completed", now, summary);
|
|
5572
|
-
tx.saveWorkItem(task.id, yieldedItem);
|
|
5573
|
-
if (candidateRequired)
|
|
5574
|
-
submittedItem = yieldedItem;
|
|
5575
|
-
if (candidatePolicy?.trigger === "always") {
|
|
5576
|
-
automaticReview = { item: yieldedItem, config: candidatePolicy };
|
|
5577
|
-
}
|
|
5578
|
-
}
|
|
5579
|
-
}
|
|
5580
|
-
else if (role.name !== LEADER_ROLE) {
|
|
5581
|
-
throw usageError(`Run ${active.id} is not a work run.`);
|
|
5582
|
-
}
|
|
5583
|
-
const reviewDispatch = automaticReview === null
|
|
5584
|
-
? null
|
|
5585
|
-
: queueReviewRound(tx, automaticReview.item, automaticReview.config, "policy", now);
|
|
5586
|
-
const leaderHandoff = active.purpose === "review"
|
|
5587
|
-
? (reviewGroupPending
|
|
5588
|
-
? (reviewGroupReady ? "review-group-ready" : null)
|
|
5589
|
-
: "review-result")
|
|
5590
|
-
: submittedItem === null
|
|
5591
|
-
&& executionGroupPending
|
|
5592
|
-
? (executionGroupReady ? "execution-group-ready" : null)
|
|
5593
|
-
: submittedItem === null
|
|
5594
|
-
|| (reviewDispatch !== null && reviewDispatch.run !== null)
|
|
5595
|
-
? null
|
|
5596
|
-
: reviewDispatch?.round.status === "failed"
|
|
5597
|
-
? "review-failed"
|
|
5598
|
-
: "candidate-ready";
|
|
5599
|
-
if (leaderHandoff !== null) {
|
|
5600
|
-
enqueueWork(tx, leaderMailbox(task.id), leaderHandoff, now, [
|
|
5601
|
-
runRef(task.id, terminal.id),
|
|
5602
|
-
...(terminal.workItemId === undefined ? [] : [workItemRef(task.id, terminal.workItemId)])
|
|
5603
|
-
]);
|
|
5604
|
-
}
|
|
5605
|
-
// Issue 05: record the Leader's terminal receipt so the Scheduler can
|
|
5606
|
-
// suppress no-change `task-orphaned` wakes. The disposition and digest are
|
|
5607
|
-
// machine-derived from the post-yield projection; the Leader does not
|
|
5608
|
-
// need to cooperate for the admission check to work.
|
|
5609
|
-
if (role.name === LEADER_ROLE) {
|
|
5610
|
-
const receipt = leaderYieldReceipt(tx, task, terminal, now);
|
|
5611
|
-
if (receipt !== null) {
|
|
5612
|
-
tx.saveAgentRun(receipt);
|
|
5613
|
-
return {
|
|
5614
|
-
kind: "yielded",
|
|
5615
|
-
run: receipt,
|
|
5616
|
-
reviewDispatch,
|
|
5617
|
-
notifyLeader: leaderHandoff !== null
|
|
5618
|
-
};
|
|
5619
|
-
}
|
|
5620
|
-
}
|
|
5621
|
-
return {
|
|
5622
|
-
kind: "yielded",
|
|
5623
|
-
run: terminal,
|
|
5624
|
-
reviewDispatch,
|
|
5625
|
-
notifyLeader: leaderHandoff !== null
|
|
5626
|
-
};
|
|
5627
|
-
});
|
|
5628
|
-
if (yielded.kind === "rejected") {
|
|
5629
|
-
if (yielded.notifyLeader) {
|
|
5630
|
-
notifyMailbox(options.runtime, leaderMailbox(yielded.run.taskId), yielded.run.taskId);
|
|
5631
|
-
}
|
|
5632
|
-
throw usageError(`Run ${yielded.run.id} no longer matches its exact execution fence: `
|
|
5633
|
-
+ `${yielded.rejected.attempt.rejectionReason}. `
|
|
5634
|
-
+ `Rejected Reviewer output was recorded as ${yielded.rejected.event.id} `
|
|
5635
|
-
+ "(unaccepted diagnostic evidence only).");
|
|
5636
|
-
}
|
|
5637
|
-
if (yielded.notifyLeader) {
|
|
5638
|
-
notifyMailbox(options.runtime, leaderMailbox(yielded.run.taskId), yielded.run.taskId);
|
|
5639
|
-
}
|
|
5640
|
-
if (yielded.reviewDispatch?.run !== null
|
|
5641
|
-
&& yielded.reviewDispatch?.run !== undefined) {
|
|
5642
|
-
notifyReviewMailbox(options, options.runtime, roleMailbox(yielded.reviewDispatch.run.taskId, yielded.reviewDispatch.run.roleName), yielded.reviewDispatch.run.taskId);
|
|
5643
|
-
}
|
|
5644
|
-
return output(`Yielded ${yielded.run.id}: ${yielded.run.summary ?? inputSummary}\n`, {
|
|
5645
|
-
run: yielded.run,
|
|
5646
|
-
...(yielded.reviewDispatch === null
|
|
5647
|
-
? {}
|
|
5648
|
-
: { reviewRound: yielded.reviewDispatch.round })
|
|
5649
|
-
});
|
|
5650
|
-
}
|
|
5651
4971
|
/**
|
|
5652
|
-
*
|
|
5653
|
-
*
|
|
5654
|
-
*
|
|
5655
|
-
*
|
|
5656
|
-
*/
|
|
5657
|
-
function leaderYieldReceipt(tx, task, terminal, now) {
|
|
5658
|
-
try {
|
|
5659
|
-
const projection = buildTaskExecutionProjection(tx, task.id, task);
|
|
5660
|
-
if (projection === null)
|
|
5661
|
-
return null;
|
|
5662
|
-
const disposition = deriveLeaderRunDisposition(projection.status, task.status);
|
|
5663
|
-
const digest = computeActionabilityDigest(collectTaskActionability(tx, task.id, now));
|
|
5664
|
-
const waitReason = disposition === "waiting" || disposition === "blocked"
|
|
5665
|
-
? leaderWaitReason(projection)
|
|
5666
|
-
: undefined;
|
|
5667
|
-
return {
|
|
5668
|
-
...terminal,
|
|
5669
|
-
disposition,
|
|
5670
|
-
observedActionabilityDigest: digest,
|
|
5671
|
-
...(waitReason === undefined ? {} : { waitReason }),
|
|
5672
|
-
updatedAt: now.toISOString()
|
|
5673
|
-
};
|
|
5674
|
-
}
|
|
5675
|
-
catch {
|
|
5676
|
-
return null;
|
|
5677
|
-
}
|
|
5678
|
-
}
|
|
5679
|
-
function leaderWaitReason(projection) {
|
|
5680
|
-
const blocker = projection.blockers[0];
|
|
5681
|
-
if (blocker !== undefined) {
|
|
5682
|
-
return { kind: blocker.kind, ref: blocker.id };
|
|
5683
|
-
}
|
|
5684
|
-
if (projection.status === "waiting-on-agents") {
|
|
5685
|
-
return { kind: "delegated-work" };
|
|
5686
|
-
}
|
|
5687
|
-
if (projection.status === "waiting-user") {
|
|
5688
|
-
return { kind: "input" };
|
|
5689
|
-
}
|
|
5690
|
-
return undefined;
|
|
5691
|
-
}
|
|
5692
|
-
/**
|
|
5693
|
-
* Records a structured progress checkpoint for an active Run. This is a durable
|
|
5694
|
-
* run fact, not a Task Message: it advances the Run's durable-progress clock so
|
|
5695
|
-
* a healthy but long-running Run keeps proving it is alive without adding
|
|
5696
|
-
* collaboration-narrative noise. It never yields, mutates the Run, or wakes the
|
|
4972
|
+
* Records a structured progress checkpoint for an active Turn. This is a durable
|
|
4973
|
+
* Turn fact, not a Task Message: it advances the Turn's durable-progress clock so
|
|
4974
|
+
* a healthy but long-running Turn keeps proving it is alive without adding
|
|
4975
|
+
* collaboration-narrative noise. It never completes, mutates the Turn, or wakes the
|
|
5697
4976
|
* Leader.
|
|
5698
4977
|
*/
|
|
5699
|
-
function
|
|
5700
|
-
const usage = "Task
|
|
4978
|
+
function checkpointTurn(args, store, options) {
|
|
4979
|
+
const usage = "Task turn checkpoint usage: yui task turn checkpoint <turn> (--note <text>|--note-file <path|->).";
|
|
5701
4980
|
const parsed = parseTail(args, new Set(["--note", "--note-file"]), usage);
|
|
5702
4981
|
exactPositionals(parsed.positionals, 1, usage);
|
|
5703
4982
|
const note = readCommandText(parsed.options.get("--note"), parsed.options.get("--note-file"), "--note", usage);
|
|
5704
4983
|
const now = clock(options);
|
|
5705
4984
|
const event = store.transaction((tx) => {
|
|
5706
|
-
const run =
|
|
4985
|
+
const run = requireTurn(tx, parsed.positionals[0], options);
|
|
5707
4986
|
if (run.status !== "active") {
|
|
5708
|
-
throw usageError(`
|
|
5709
|
-
}
|
|
5710
|
-
if (run.deliveredAt === undefined) {
|
|
5711
|
-
throw usageError(`Run ${run.id} delivery is still pending.`);
|
|
4987
|
+
throw usageError(`Turn ${run.id} is already terminal: ${run.status}.`);
|
|
5712
4988
|
}
|
|
5713
4989
|
const task = requireTask(tx, run.taskId);
|
|
5714
4990
|
if (task.status !== "active")
|
|
5715
|
-
throw usageError(inactiveTaskMessage(task, "checkpointing a
|
|
5716
|
-
const pointer =
|
|
4991
|
+
throw usageError(inactiveTaskMessage(task, "checkpointing a Turn"));
|
|
4992
|
+
const pointer = activeTurnPointer(tx, run);
|
|
5717
4993
|
if (pointer?.id !== run.id) {
|
|
5718
|
-
throw usageError(`
|
|
4994
|
+
throw usageError(`Turn is not active for ${task.id}/${run.roleName}: ${run.id}.`);
|
|
5719
4995
|
}
|
|
5720
4996
|
const events = tx.listEvents(task.id);
|
|
5721
|
-
const recovered =
|
|
5722
|
-
const progress = recordTaskEventRecord(tx, task.id,
|
|
5723
|
-
|
|
4997
|
+
const recovered = isRoleTurnStalled(events, run.id);
|
|
4998
|
+
const progress = recordTaskEventRecord(tx, task.id, TURN_PROGRESS_EVENT, {
|
|
4999
|
+
turnId: run.id,
|
|
5724
5000
|
note: truncateEventNote(note),
|
|
5725
5001
|
...(run.workItemId === undefined ? {} : { workItemId: run.workItemId })
|
|
5726
5002
|
}, now);
|
|
5727
5003
|
if (recovered) {
|
|
5728
|
-
recordTaskEventRecord(tx, task.id,
|
|
5729
|
-
|
|
5004
|
+
recordTaskEventRecord(tx, task.id, TURN_RECOVERED_EVENT, {
|
|
5005
|
+
turnId: run.id,
|
|
5730
5006
|
roleName: run.roleName,
|
|
5731
5007
|
progressAt: now.toISOString(),
|
|
5732
5008
|
kind: "checkpoint"
|
|
@@ -5744,13 +5020,13 @@ export function queueReviewRound(store, item, config, requestedBy, now) {
|
|
|
5744
5020
|
const pending = createReviewRound(store.nextReviewRoundId(item.taskId), item.taskId, item.id, candidate.id, config.roleName, requestedBy, candidate.gitSnapshot.reviewBaseCommit, now);
|
|
5745
5021
|
store.saveReviewRound(item.taskId, pending);
|
|
5746
5022
|
const producerRoleName = item.assignee
|
|
5747
|
-
?? (candidate.source.type === "
|
|
5748
|
-
? store.
|
|
5023
|
+
?? (candidate.source.type === "turn"
|
|
5024
|
+
? store.getTurn(item.taskId, candidate.source.turnId)?.roleName
|
|
5749
5025
|
: LEADER_ROLE);
|
|
5750
5026
|
if (producerRoleName === config.roleName) {
|
|
5751
5027
|
const failed = finishReviewRound(pending, "failed", `Reviewer Role must be separate from the Candidate producer: ${config.roleName}.`, now);
|
|
5752
5028
|
store.saveReviewRound(item.taskId, failed);
|
|
5753
|
-
return { round: failed
|
|
5029
|
+
return { round: failed };
|
|
5754
5030
|
}
|
|
5755
5031
|
let reviewer = store.getRole(item.taskId, config.roleName);
|
|
5756
5032
|
if (reviewer === null) {
|
|
@@ -5758,18 +5034,18 @@ export function queueReviewRound(store, item, config, requestedBy, now) {
|
|
|
5758
5034
|
if (globalRole === null) {
|
|
5759
5035
|
const failed = finishReviewRound(pending, "failed", `Global Role not found: ${config.roleName}.`, now);
|
|
5760
5036
|
store.saveReviewRound(item.taskId, failed);
|
|
5761
|
-
return { round: failed
|
|
5037
|
+
return { round: failed };
|
|
5762
5038
|
}
|
|
5763
5039
|
const task = requireTask(store, item.taskId);
|
|
5764
5040
|
reviewer = createTaskRole(store, task, config.roleName, undefined, now, config.roleName);
|
|
5765
5041
|
store.saveRole(task.id, reviewer);
|
|
5766
5042
|
}
|
|
5767
|
-
if (store.
|
|
5768
|
-
const failed = finishReviewRound(pending, "failed", `Reviewer Role already has an active
|
|
5043
|
+
if (store.getActiveTurn(item.taskId, reviewer.name) !== null) {
|
|
5044
|
+
const failed = finishReviewRound(pending, "failed", `Reviewer Role already has an active Turn: ${reviewer.name}.`, now);
|
|
5769
5045
|
store.saveReviewRound(item.taskId, failed);
|
|
5770
|
-
return { round: failed
|
|
5046
|
+
return { round: failed };
|
|
5771
5047
|
}
|
|
5772
|
-
return { round: pending
|
|
5048
|
+
return { round: pending };
|
|
5773
5049
|
}
|
|
5774
5050
|
export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, options = {}) {
|
|
5775
5051
|
const now = clock(options);
|
|
@@ -5834,8 +5110,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5834
5110
|
&& round.reviewerRoleName !== taskFinalContract.reviewerRoleName) {
|
|
5835
5111
|
throw new TaskFinalReviewDispatchDriftError(`Task final-review Reviewer identity does not match ReviewRound ${round.id}.`);
|
|
5836
5112
|
}
|
|
5837
|
-
if (round.status === "pending" && round.
|
|
5838
|
-
throw new TaskFinalReviewDispatchDriftError(`Pending final ReviewRound already records Reviewer
|
|
5113
|
+
if (round.status === "pending" && round.reviewerTurnId !== undefined) {
|
|
5114
|
+
throw new TaskFinalReviewDispatchDriftError(`Pending final ReviewRound already records Reviewer Turn ${round.reviewerTurnId}: `
|
|
5839
5115
|
+ `${round.id}.`);
|
|
5840
5116
|
}
|
|
5841
5117
|
let currentTaskCandidate;
|
|
@@ -5885,8 +5161,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5885
5161
|
&& (entry.executionGroup?.lanes.some(({ roleName }) => requestedReviewers.has(roleName))
|
|
5886
5162
|
?? requestedReviewers.has(entry.reviewerRoleName))
|
|
5887
5163
|
&& !(entry.status === "running"
|
|
5888
|
-
&& entry.
|
|
5889
|
-
&& tx.
|
|
5164
|
+
&& entry.reviewerTurnId !== undefined
|
|
5165
|
+
&& tx.getTurn(task.id, entry.reviewerTurnId)?.status === "failed")));
|
|
5890
5166
|
if (conflicting !== undefined) {
|
|
5891
5167
|
throw new TaskFinalReviewDispatchDriftError(`Another active Task-final ReviewRound already exists: ${conflicting.id}/${conflicting.reviewerRoleName}.`);
|
|
5892
5168
|
}
|
|
@@ -5913,8 +5189,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5913
5189
|
}
|
|
5914
5190
|
const candidateLabel = taskScope
|
|
5915
5191
|
? "frozen Task candidate"
|
|
5916
|
-
: candidate.source.type === "
|
|
5917
|
-
? `candidate
|
|
5192
|
+
: candidate.source.type === "turn"
|
|
5193
|
+
? `candidate Turn ${candidate.source.turnId}`
|
|
5918
5194
|
: `revision ${candidate.workItemRevision}`;
|
|
5919
5195
|
const frozenHeads = taskScope
|
|
5920
5196
|
? round.taskCandidate.projects
|
|
@@ -5932,7 +5208,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5932
5208
|
}
|
|
5933
5209
|
const diffByProject = options.deltaRecheckDiff;
|
|
5934
5210
|
if (diffByProject === undefined) {
|
|
5935
|
-
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck diff is missing for ${round.id}; the CLI preflight did not
|
|
5211
|
+
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck diff is missing for ${round.id}; the CLI preflight did not turn.`);
|
|
5936
5212
|
}
|
|
5937
5213
|
try {
|
|
5938
5214
|
verifyDeltaRecheckDiff(round.deltaRecheck, diffByProject);
|
|
@@ -5976,8 +5252,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5976
5252
|
: []),
|
|
5977
5253
|
"You may freely edit source/tests, run local build or test commands, and optionally commit diagnostic evidence only inside this stable Reviewer workspace at the exact ReviewRound snapshot.",
|
|
5978
5254
|
"Do not push, integrate, mutate Task state, touch the Candidate or Worker workspace, another Task/workspace, a stable checkout, or the real Yui control-plane home.",
|
|
5979
|
-
"
|
|
5980
|
-
"Report reviewBaseCommit, exact checks/results, material findings, and uncertainty.
|
|
5255
|
+
"End the Provider turn with complete findings, evidence, checks actually run, uncertainty, and recommended next actions in clear Markdown or JSON. Yui preserves the full report automatically; no fixed wording or field list is required. If you include evidenceCommit, it must match the managed Review workspace.",
|
|
5256
|
+
"Report reviewBaseCommit, exact checks/results, material findings, and uncertainty. This Turn result completes only the Round and creates no Candidate or ChangeSet.",
|
|
5981
5257
|
"The Leader alone interprets and routes evidence: original Worker when open, a small Repair WorkItem when needed, or Leader/Integration for merge and local fixes; never merge review evidence yourself."
|
|
5982
5258
|
].join("\n");
|
|
5983
5259
|
const executionTarget = executionTargetForReviewRound(task, round, item, candidate);
|
|
@@ -5987,19 +5263,19 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5987
5263
|
strategy: { mode: "fixed", count: 1 },
|
|
5988
5264
|
lanes: [{ roleName: reviewer.name, reviewRoundId: round.id }]
|
|
5989
5265
|
}, now);
|
|
5990
|
-
const dispatchLanes = runningGroup.lanes.filter((lane) => (lane.status === "pending" && lane.
|
|
5266
|
+
const dispatchLanes = runningGroup.lanes.filter((lane) => (lane.status === "pending" && lane.turnId === undefined));
|
|
5991
5267
|
const laneRoles = dispatchLanes.map(({ roleName }) => roleName);
|
|
5992
5268
|
const reviewers = laneRoles.map((roleName) => {
|
|
5993
5269
|
const laneReviewer = tx.getRole(taskId, roleName);
|
|
5994
5270
|
if (laneReviewer === null) {
|
|
5995
5271
|
throw usageError(`Reviewer Role not found: ${taskId}/${roleName}.`);
|
|
5996
5272
|
}
|
|
5997
|
-
if (tx.
|
|
5998
|
-
throw usageError(`Reviewer Role already has an active
|
|
5273
|
+
if (tx.getActiveTurn(taskId, roleName) !== null) {
|
|
5274
|
+
throw usageError(`Reviewer Role already has an active Turn: ${roleName}.`);
|
|
5999
5275
|
}
|
|
6000
5276
|
return laneReviewer;
|
|
6001
5277
|
});
|
|
6002
|
-
const
|
|
5278
|
+
const createdTurns = [];
|
|
6003
5279
|
const config = tx.getConfig();
|
|
6004
5280
|
const controllerConcurrency = resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
|
|
6005
5281
|
const capacity = runningGroup.strategy.mode === "fixed"
|
|
@@ -6077,30 +5353,23 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
6077
5353
|
continue;
|
|
6078
5354
|
}
|
|
6079
5355
|
activeResources.push(request);
|
|
6080
|
-
const
|
|
6081
|
-
const
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
action: "review-round",
|
|
6086
|
-
subject: {
|
|
6087
|
-
taskId,
|
|
6088
|
-
...(item === undefined ? {} : { workItemId: item.id }),
|
|
6089
|
-
reviewRoundId: round.id,
|
|
6090
|
-
executionGroupId: runningGroup.id,
|
|
6091
|
-
executionLaneId: lane.id
|
|
5356
|
+
const turnId = tx.nextTurnId(taskId);
|
|
5357
|
+
const input = createTurnInput({
|
|
5358
|
+
source: {
|
|
5359
|
+
type: "yui",
|
|
5360
|
+
channel: item === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
6092
5361
|
},
|
|
6093
5362
|
directive: `${rawInput}\nFrozen target: ${runningGroup.target.fingerprint}.`,
|
|
6094
5363
|
deltaRefIds: []
|
|
6095
5364
|
});
|
|
6096
5365
|
runningGroup = updateExecutionLane(runningGroup, lane.id, {
|
|
6097
5366
|
status: "running",
|
|
6098
|
-
|
|
5367
|
+
turnId,
|
|
6099
5368
|
reviewRoundId: round.id,
|
|
6100
5369
|
effective,
|
|
6101
5370
|
workspace: laneWorkspace
|
|
6102
5371
|
}, now);
|
|
6103
|
-
|
|
5372
|
+
createdTurns.push(createTurn(turnId, taskId, laneReviewer.name, roleAgentSessionResumeMode(sessions, effective.agentId, effective), input, now, {
|
|
6104
5373
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
6105
5374
|
purpose: "review",
|
|
6106
5375
|
reviewRoundId: round.id,
|
|
@@ -6113,8 +5382,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
6113
5382
|
const roundWithGroup = round.executionGroup === undefined
|
|
6114
5383
|
? attachReviewExecutionGroup(round, runningGroup)
|
|
6115
5384
|
: updateReviewExecutionGroup(round, runningGroup);
|
|
6116
|
-
const persistedRound = round.status === "pending" &&
|
|
6117
|
-
? startReviewRound(roundWithGroup,
|
|
5385
|
+
const persistedRound = round.status === "pending" && createdTurns.length > 0
|
|
5386
|
+
? startReviewRound(roundWithGroup, createdTurns[0].id)
|
|
6118
5387
|
: roundWithGroup;
|
|
6119
5388
|
// Save the aggregate before adopting prepared Lane workspaces. The
|
|
6120
5389
|
// managed-workspace validator deliberately requires durable Group/Lane
|
|
@@ -6132,29 +5401,29 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
6132
5401
|
tx.saveManagedWorkspace(prepared);
|
|
6133
5402
|
}
|
|
6134
5403
|
}
|
|
6135
|
-
for (let index = 0; index <
|
|
6136
|
-
const
|
|
6137
|
-
const snapshot =
|
|
5404
|
+
for (let index = 0; index < createdTurns.length; index += 1) {
|
|
5405
|
+
const unboundTurn = createdTurns[index];
|
|
5406
|
+
const snapshot = freezeTurnContextSnapshot(tx, {
|
|
6138
5407
|
taskId,
|
|
6139
|
-
roleName:
|
|
5408
|
+
roleName: unboundTurn.roleName,
|
|
6140
5409
|
purpose: "review",
|
|
6141
5410
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
6142
5411
|
reviewRoundId: round.id
|
|
6143
5412
|
}, now, "controller", reviewBaseline === undefined
|
|
6144
5413
|
? undefined
|
|
6145
5414
|
: contextSnapshotRef(reviewBaseline));
|
|
6146
|
-
const created =
|
|
6147
|
-
|
|
6148
|
-
const laneReviewer = requireRole(tx, taskId,
|
|
6149
|
-
tx.
|
|
6150
|
-
tx.
|
|
5415
|
+
const created = withTurnContextSnapshot(unboundTurn, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
5416
|
+
createdTurns[index] = created;
|
|
5417
|
+
const laneReviewer = requireRole(tx, taskId, unboundTurn.roleName);
|
|
5418
|
+
tx.saveTurn(created);
|
|
5419
|
+
tx.saveActiveTurn(created);
|
|
6151
5420
|
enqueueWork(tx, roleMailbox(taskId, laneReviewer.name), "review-requested", now, [
|
|
6152
|
-
|
|
5421
|
+
turnRef(taskId, created.id),
|
|
6153
5422
|
...(item === undefined ? [] : [workItemRef(taskId, item.id)])
|
|
6154
5423
|
]);
|
|
6155
|
-
recordTaskEvent(tx, taskId, "
|
|
5424
|
+
recordTaskEvent(tx, taskId, "turn.review-dispatched", turnLaunchEventPayload(created), now);
|
|
6156
5425
|
}
|
|
6157
|
-
return
|
|
5426
|
+
return createdTurns;
|
|
6158
5427
|
});
|
|
6159
5428
|
for (const run of runs) {
|
|
6160
5429
|
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
@@ -6283,7 +5552,7 @@ function appendMessage(store, taskId, body, kind, author, now, context = {}) {
|
|
|
6283
5552
|
recordTaskEvent(store, taskId, "message.sent", {
|
|
6284
5553
|
messageId: message.id,
|
|
6285
5554
|
kind: message.kind,
|
|
6286
|
-
...(message.
|
|
5555
|
+
...(message.turnId === undefined ? {} : { turnId: message.turnId })
|
|
6287
5556
|
}, now);
|
|
6288
5557
|
return message;
|
|
6289
5558
|
}
|
|
@@ -6291,15 +5560,15 @@ function recordTaskEvent(store, taskId, type, payload, now) {
|
|
|
6291
5560
|
return recordTaskEventRecord(store, taskId, type, payload, now);
|
|
6292
5561
|
}
|
|
6293
5562
|
function leaderActionEventPayload(store, taskId, options) {
|
|
6294
|
-
const
|
|
6295
|
-
return
|
|
5563
|
+
const turnId = taskLeaderActionTurnId(store, taskId, options.environment, options.yuiHome);
|
|
5564
|
+
return turnId === undefined ? {} : { leaderTurnId: turnId };
|
|
6296
5565
|
}
|
|
6297
5566
|
function recordTaskEventRecord(store, taskId, type, payload, now) {
|
|
6298
5567
|
const event = createTaskEvent(store.nextEventId(taskId), taskId, type, payload, now);
|
|
6299
5568
|
store.saveEvent(taskId, event);
|
|
6300
5569
|
return event;
|
|
6301
5570
|
}
|
|
6302
|
-
/** Keeps a free-text
|
|
5571
|
+
/** Keeps a free-text Turn-fact note bounded so an event payload stays compact. */
|
|
6303
5572
|
function truncateEventNote(note) {
|
|
6304
5573
|
const normalized = note.trim();
|
|
6305
5574
|
return normalized.length <= 280 ? normalized : `${normalized.slice(0, 279)}…`;
|
|
@@ -6321,9 +5590,9 @@ function roleLaunchEventPayload(role, sessions) {
|
|
|
6321
5590
|
: "pending-next-launch"
|
|
6322
5591
|
};
|
|
6323
5592
|
}
|
|
6324
|
-
function
|
|
5593
|
+
function turnLaunchEventPayload(run) {
|
|
6325
5594
|
return {
|
|
6326
|
-
|
|
5595
|
+
turnId: run.id,
|
|
6327
5596
|
role: run.roleName,
|
|
6328
5597
|
purpose: run.purpose,
|
|
6329
5598
|
mode: run.mode,
|
|
@@ -6381,18 +5650,18 @@ function requireWorkItem(store, workItemId, options) {
|
|
|
6381
5650
|
}
|
|
6382
5651
|
return item;
|
|
6383
5652
|
}
|
|
6384
|
-
function
|
|
6385
|
-
const reference = taskRecordReference(
|
|
6386
|
-
const run = store.
|
|
5653
|
+
function requireTurn(store, turnId, options) {
|
|
5654
|
+
const reference = taskRecordReference(turnId, "turn", "Turn reference", options);
|
|
5655
|
+
const run = store.getTurn(reference.taskId, reference.localId);
|
|
6387
5656
|
if (run === null) {
|
|
6388
|
-
throw usageError(`
|
|
5657
|
+
throw usageError(`Turn not found: ${reference.taskId}/${reference.localId}.`);
|
|
6389
5658
|
}
|
|
6390
5659
|
return run;
|
|
6391
5660
|
}
|
|
6392
|
-
function
|
|
5661
|
+
function activeTurnPointer(store, run) {
|
|
6393
5662
|
return run.executionGroupId !== undefined && run.executionLaneId !== undefined
|
|
6394
|
-
? store.
|
|
6395
|
-
: store.
|
|
5663
|
+
? store.getActiveExecutionLaneTurn(run.taskId, run.executionGroupId, run.executionLaneId)
|
|
5664
|
+
: store.getActiveTurn(run.taskId, run.roleName);
|
|
6396
5665
|
}
|
|
6397
5666
|
function requireReviewRound(store, reviewRoundId, options) {
|
|
6398
5667
|
const reference = taskRecordReference(reviewRoundId, "reviewRound", "ReviewRound reference", options);
|
|
@@ -6425,8 +5694,8 @@ function assertWorkItemDependenciesCompleted(store, item) {
|
|
|
6425
5694
|
}
|
|
6426
5695
|
}
|
|
6427
5696
|
}
|
|
6428
|
-
function
|
|
6429
|
-
return [...
|
|
5697
|
+
function chronologicalTurns(turns) {
|
|
5698
|
+
return [...turns].sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
6430
5699
|
|| left.id.localeCompare(right.id)));
|
|
6431
5700
|
}
|
|
6432
5701
|
function isTerminalWorkItemStatus(status) {
|
|
@@ -6903,7 +6172,7 @@ function taskContinuationCommand(args, store) {
|
|
|
6903
6172
|
continuationId: identity.continuationId,
|
|
6904
6173
|
generation: identity.generation,
|
|
6905
6174
|
driver: identity.providerNamespace,
|
|
6906
|
-
|
|
6175
|
+
turnId: continuation.turnId,
|
|
6907
6176
|
execution: continuation.execution,
|
|
6908
6177
|
outcome: continuation.outcome,
|
|
6909
6178
|
attachment: continuation.attachment,
|
|
@@ -6994,13 +6263,13 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
6994
6263
|
{ header: "Wake", minWidth: 8, maxWidth: 18 },
|
|
6995
6264
|
{ header: "Status", minWidth: 8, maxWidth: 12 },
|
|
6996
6265
|
{ header: "Reasons", minWidth: 10, maxWidth: 40 },
|
|
6997
|
-
{ header: "
|
|
6266
|
+
{ header: "Turn", minWidth: 10, maxWidth: 20 },
|
|
6998
6267
|
{ header: "Dispatched", minWidth: 10, maxWidth: 28 }
|
|
6999
6268
|
], wakes.map((wake) => [
|
|
7000
6269
|
wake.id,
|
|
7001
6270
|
wake.status,
|
|
7002
6271
|
wake.reasons.map(renderWakeReason).join(", "),
|
|
7003
|
-
wake.
|
|
6272
|
+
wake.turnId ?? "-",
|
|
7004
6273
|
presentTime(wake.createdAt, timeZone)
|
|
7005
6274
|
]), defaultTableWidth())}\n`, { taskId: task.id, wakes });
|
|
7006
6275
|
}
|
|
@@ -7020,14 +6289,14 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
7020
6289
|
};
|
|
7021
6290
|
const events = store.listEvents(task.id).filter((e) => inWindow(e.createdAt));
|
|
7022
6291
|
const messages = store.listMessages(task.id).filter((m) => inWindow(m.createdAt));
|
|
7023
|
-
const
|
|
6292
|
+
const turns = store.listTurns(task.id).filter((turn) => inWindow(turn.createdAt));
|
|
7024
6293
|
const lines = [
|
|
7025
6294
|
`Wake: ${wake.id}`,
|
|
7026
6295
|
`Task: ${task.id}`,
|
|
7027
6296
|
`Status: ${wake.status}`,
|
|
7028
6297
|
`Reasons: ${wake.reasons.map(renderWakeReason).join(", ")}`,
|
|
7029
6298
|
`Delta window: ${wake.fromCursor} → ${wake.toCursor}`,
|
|
7030
|
-
...(wake.
|
|
6299
|
+
...(wake.turnId === undefined ? [] : [`Turn: ${wake.turnId}`]),
|
|
7031
6300
|
`Dispatched: ${presentTime(wake.createdAt, timeZone)}`,
|
|
7032
6301
|
...(wake.consumedAt === undefined
|
|
7033
6302
|
? []
|
|
@@ -7036,15 +6305,15 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
7036
6305
|
...events.map((e) => ` ${e.id} ${e.type} ${presentTime(e.createdAt, timeZone)}`),
|
|
7037
6306
|
`Messages (${messages.length}):`,
|
|
7038
6307
|
...messages.map((m) => ` ${m.id} [${taskMessageAuthorLabel(m.author)}] ${presentTime(m.createdAt, timeZone)}`),
|
|
7039
|
-
`
|
|
7040
|
-
...
|
|
6308
|
+
`Turns (${turns.length}):`,
|
|
6309
|
+
...turns.map((turn) => ` ${turn.id} [${turn.status}/${turn.purpose}] ${turn.roleName} ${presentTime(turn.createdAt, timeZone)}`)
|
|
7041
6310
|
];
|
|
7042
6311
|
return output(lines.join("\n").concat("\n"), {
|
|
7043
6312
|
taskId: task.id,
|
|
7044
6313
|
wake,
|
|
7045
6314
|
events,
|
|
7046
6315
|
messages,
|
|
7047
|
-
|
|
6316
|
+
turns
|
|
7048
6317
|
});
|
|
7049
6318
|
}
|
|
7050
6319
|
throw usageError(command === undefined
|
|
@@ -7218,60 +6487,6 @@ function messageOf(error) {
|
|
|
7218
6487
|
function taskMailbox(taskId) {
|
|
7219
6488
|
return { kind: "task", taskId };
|
|
7220
6489
|
}
|
|
7221
|
-
function settleTaskExecutionForCompletion(store, taskId, roles, summary, now) {
|
|
7222
|
-
const activeRuns = store.listAgentRuns(taskId).filter((run) => run.status === "active");
|
|
7223
|
-
for (const run of activeRuns) {
|
|
7224
|
-
store.saveAgentRun(run.roleName === LEADER_ROLE && run.pushedAt !== undefined
|
|
7225
|
-
? yieldAgentRun(run, summary, now)
|
|
7226
|
-
: failAgentRun(run, `Task completed: ${summary}`, now));
|
|
7227
|
-
if (run.executionGroupId !== undefined && run.executionLaneId !== undefined) {
|
|
7228
|
-
store.clearActiveExecutionLaneRun(taskId, run.executionGroupId, run.executionLaneId);
|
|
7229
|
-
}
|
|
7230
|
-
}
|
|
7231
|
-
// Historical pointer projections are execution state, not Task progress.
|
|
7232
|
-
for (const run of store.listAgentRuns(taskId)) {
|
|
7233
|
-
if (run.executionGroupId !== undefined && run.executionLaneId !== undefined) {
|
|
7234
|
-
store.clearActiveExecutionLaneRun(taskId, run.executionGroupId, run.executionLaneId);
|
|
7235
|
-
}
|
|
7236
|
-
}
|
|
7237
|
-
const targets = [];
|
|
7238
|
-
for (const role of roles) {
|
|
7239
|
-
store.clearActiveAgentRun(taskId, role.name);
|
|
7240
|
-
let sessions = store.getTaskRoleSessionSet(taskId, role.name);
|
|
7241
|
-
const activeSession = sessions?.sessions[sessions.activeAgentId];
|
|
7242
|
-
const cleanupRequired = ownedRuntimeSessionRequiresCleanup(activeSession);
|
|
7243
|
-
if (sessions !== null) {
|
|
7244
|
-
if (sessions.inFlight !== null) {
|
|
7245
|
-
sessions = terminalizeTaskRoleRunSession(sessions, sessions.inFlight, now);
|
|
7246
|
-
}
|
|
7247
|
-
const current = sessions.sessions[sessions.activeAgentId];
|
|
7248
|
-
if (current !== undefined && current.status === "active") {
|
|
7249
|
-
sessions = updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "ended", now, "stopped");
|
|
7250
|
-
}
|
|
7251
|
-
store.saveTaskRoleSessionSet(sessions);
|
|
7252
|
-
}
|
|
7253
|
-
if (!cleanupRequired)
|
|
7254
|
-
continue;
|
|
7255
|
-
const target = runtimeLifecycleTarget({
|
|
7256
|
-
scope: "task",
|
|
7257
|
-
taskId,
|
|
7258
|
-
roleName: role.name
|
|
7259
|
-
});
|
|
7260
|
-
enqueueWork(store, target, RUNTIME_CLEANUP_REQUIRED_REASON, now, [taskRef(taskId)]);
|
|
7261
|
-
targets.push(target);
|
|
7262
|
-
}
|
|
7263
|
-
return targets;
|
|
7264
|
-
}
|
|
7265
|
-
function ownedRuntimeSessionRequiresCleanup(session) {
|
|
7266
|
-
if (session === undefined || session.status === "ended") {
|
|
7267
|
-
return false;
|
|
7268
|
-
}
|
|
7269
|
-
// Older synthetic session fixtures can represent a ready native session
|
|
7270
|
-
// without a lifecycle generation. Do not manufacture a cleanup obligation
|
|
7271
|
-
// for those records; coordinated launches always carry launchId, while a
|
|
7272
|
-
// running session remains actionable even when an older record lacks it.
|
|
7273
|
-
return session.launchId !== undefined;
|
|
7274
|
-
}
|
|
7275
6490
|
function roleMailbox(taskId, roleName) {
|
|
7276
6491
|
return { kind: "role", taskId, roleName };
|
|
7277
6492
|
}
|
|
@@ -7281,8 +6496,8 @@ function leaderMailbox(taskId) {
|
|
|
7281
6496
|
function taskRef(id) {
|
|
7282
6497
|
return { type: "task", id };
|
|
7283
6498
|
}
|
|
7284
|
-
function
|
|
7285
|
-
return { type: "
|
|
6499
|
+
function turnRef(taskId, id) {
|
|
6500
|
+
return { type: "turn", taskId, id };
|
|
7286
6501
|
}
|
|
7287
6502
|
function workItemRef(taskId, id) {
|
|
7288
6503
|
return { type: "work-item", taskId, id };
|