@zq-silk/yui 0.15.7 → 0.15.8
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 +192 -399
- package/README.md +127 -1149
- package/dist/agent/adapterCatalog.js +15 -2
- package/dist/agent/agent.js +23 -3
- package/dist/agent/argumentPolicy.js +7 -1
- package/dist/agent/connectionPlan.js +62 -0
- package/dist/agent/executionComponents.js +158 -0
- package/dist/agent/launchEnvironment.js +31 -3
- package/dist/agent/managedRuntimeEnvironment.js +3 -5
- package/dist/{turn/turn.js → agentRun/agentRun.js} +166 -109
- package/dist/{turn/turnIdentity.js → agentRun/runIdentity.js} +4 -4
- package/dist/brief/taskBrief.js +12 -0
- package/dist/cli/agentConfigurationPicker.js +13 -0
- package/dist/cli/commandCatalog.js +157 -70
- package/dist/cli/interactionCandidates.js +5 -5
- package/dist/cli/interactionPolicy.js +38 -8
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/managedDiagnostics.js +28 -0
- package/dist/cli/operatorWizard.js +1 -7
- package/dist/cli/roleOptionOrder.js +27 -0
- package/dist/cli/roleWizard.js +50 -14
- package/dist/cli/updateOrchestrator.js +1 -1
- package/dist/cli/updatePorts.js +3 -4
- package/dist/cli.js +178 -95
- package/dist/commands/agentCommands.js +72 -14
- package/dist/commands/capabilityCommands.js +9 -6
- package/dist/commands/configCommands.js +20 -20
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +24 -24
- package/dist/commands/globalRoleCommands.js +1 -1
- package/dist/commands/grantCommands.js +4 -4
- package/dist/commands/operatorCommands.js +1 -7
- package/dist/commands/projectCommands.js +4 -4
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/roleConfiguration.js +25 -5
- package/dist/commands/roleRuntimeGuard.js +4 -5
- package/dist/commands/sessionCommands.js +3 -7
- package/dist/commands/taskActivationCommands.js +259 -0
- package/dist/commands/taskActor.js +28 -49
- package/dist/commands/taskCommands.js +1188 -710
- package/dist/commands/taskContextCommand.js +39 -583
- package/dist/commands/taskExecutionCommands.js +32 -32
- package/dist/commands/taskInputCommands.js +40 -104
- package/dist/commands/taskIntegrationCommands.js +3 -2
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskNextActionCommand.js +8 -8
- package/dist/commands/taskOverviewCommand.js +33 -45
- package/dist/commands/taskRemoteDeliveryCommand.js +2 -2
- package/dist/commands/taskRoleRuntimeStatus.js +133 -102
- package/dist/commands/telemetryCommands.js +36 -38
- package/dist/config/configCatalog.js +4 -4
- package/dist/config/yuiConfig.js +8 -8
- package/dist/context/contextSnapshot.js +10 -10
- package/dist/context/dispatchContext.js +11 -11
- package/dist/context/roleSessionContext.js +6 -3
- package/dist/context/{turnContextPack.js → runContextPack.js} +146 -81
- package/dist/context/{turnInputContract.js → runInputContract.js} +73 -60
- package/dist/context/sessionBootstrapManifest.js +21 -2
- package/dist/context/sourceRunContext.js +30 -0
- package/dist/context/taskContext.js +458 -0
- package/dist/context/wakeNotification.js +27 -27
- package/dist/controller/agentRuntimeObserver.js +21 -24
- package/dist/controller/capabilityBridge.js +17 -6
- package/dist/controller/clientRuntime.js +65 -92
- package/dist/controller/controller.js +114 -188
- package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
- package/dist/controller/jobControl.js +54 -85
- package/dist/controller/resourceInventory.js +8 -27
- package/dist/controller/resourceInventoryLinux.js +12 -13
- package/dist/controller/runtime.js +529 -479
- package/dist/controller/runtimeEventInbox.js +55 -25
- package/dist/controller/runtimeEventProcessor.js +22 -31
- package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
- package/dist/controller/runtimeLaunchCoordinator.js +80 -426
- package/dist/controller/runtimeObservationHook.js +14 -18
- package/dist/controller/sessionNotify.js +16 -24
- package/dist/controller/sessionOwnerReconciliation.js +168 -50
- package/dist/controller/structuredProviderObservation.js +138 -99
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/coordination/workMailboxQueue.js +36 -33
- package/dist/core/boundedRpc.js +8 -1
- package/dist/core/controllerClient.js +20 -1
- package/dist/core/controllerServer.js +4 -4
- package/dist/doctor/doctor.js +13 -2
- package/dist/domain/agentResultTransport.js +9 -9
- package/dist/execution/codexThreadNaming.js +2 -8
- package/dist/execution/executionHealth.js +51 -63
- package/dist/execution/reviewMainRun.js +137 -0
- package/dist/execution/workItemExecution.js +28 -29
- package/dist/execution/workItemExecutionProjection.js +99 -107
- package/dist/execution/workItemMainRun.js +141 -0
- package/dist/executor/agentAdapter.js +227 -20
- package/dist/executor/agentConfigurationCatalog.js +126 -4
- package/dist/executor/agentConfigurationProbe.js +162 -4
- package/dist/executor/agentExecutor.js +79 -78
- package/dist/executor/effectiveLaunch.js +105 -18
- package/dist/executor/executorRegistry.js +29 -44
- package/dist/executor/fileRoleLaunchPlanner.js +229 -154
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +6 -3
- package/dist/input/inputRequest.js +12 -10
- package/dist/integration/gitIntegrationService.js +4 -11
- package/dist/integration/integrationQueueService.js +4 -4
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/kernel/builtinCapabilities.js +247 -12
- package/dist/kernel/capabilityRegistry.js +64 -18
- package/dist/kernel/instanceHost.js +12 -1
- package/dist/kernel/kernelPorts.js +2 -2
- package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
- package/dist/lifecycle/exactRunTerminalization.js +449 -0
- package/dist/message/message.js +62 -6
- package/dist/message/messageContinuation.js +204 -0
- package/dist/observability/executionAudit.js +70 -72
- package/dist/observability/faultClassification.js +2 -2
- package/dist/observability/orchestrationMetrics.js +8 -8
- package/dist/operator/operatorSessionHistory.js +1 -7
- package/dist/output/agentConfigurationPresentation.js +8 -3
- package/dist/output/agentRunConfigurationPresentation.js +128 -0
- package/dist/output/rolePresentation.js +54 -3
- package/dist/plugins/pluginChild.js +104 -0
- package/dist/plugins/pluginIntent.js +26 -0
- package/dist/plugins/pluginInterpreter.js +43 -0
- package/dist/plugins/pluginPackage.js +101 -0
- package/dist/plugins/pluginProcess.js +112 -0
- package/dist/plugins/pluginService.js +380 -0
- package/dist/profile/agentProfile.js +1 -1
- package/dist/repository/gitWorkspace.js +26 -4
- package/dist/repository/project.js +19 -4
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +20 -27
- package/dist/repository/taskWorkspacePreparer.js +344 -83
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +3 -3
- package/dist/resources/projectResource.js +123 -0
- package/dist/resources/projectResourceService.js +421 -0
- package/dist/resources/resourceDiscovery.js +6 -6
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/resourceRegistrar.js +1 -1
- package/dist/resources/resourceTypes.js +1 -1
- package/dist/review/deltaRecheck.js +3 -3
- package/dist/review/reviewAcceptance.js +16 -16
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewRound.js +21 -20
- package/dist/review/reviewerAvailability.js +2 -2
- package/dist/role/role.js +51 -7
- package/dist/role/taskRoleUpdate.js +30 -0
- package/dist/runtime/acpProtocol.js +425 -0
- package/dist/runtime/acpSession.js +731 -0
- package/dist/runtime/acpSessionConfiguration.js +260 -0
- package/dist/runtime/agentDriver.js +30 -11
- package/dist/runtime/agentEndpoint.js +278 -0
- package/dist/runtime/agentEndpointIdentity.js +86 -0
- package/dist/runtime/agentEndpointOwnership.js +239 -0
- package/dist/runtime/agentError.js +2 -10
- package/dist/runtime/agentHost.js +565 -314
- package/dist/runtime/agentRunConfiguration.js +258 -0
- package/dist/runtime/builtinAgentDrivers.js +134 -18
- package/dist/runtime/builtinAgentErrorMappers.js +55 -3
- package/dist/runtime/builtinTranscriptUsage.js +1 -1
- package/dist/runtime/claude-process-owner +0 -0
- package/dist/runtime/codexAppServerRuntime.js +38 -30
- package/dist/runtime/codexInteractiveHost.js +41 -6
- package/dist/runtime/continuationManager.js +2 -6
- package/dist/runtime/executionEnvironment.js +30 -0
- package/dist/runtime/firstProgressAdvisory.js +11 -11
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/jsonLineChannel.js +109 -0
- package/dist/runtime/launchBroker.js +91 -16
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +10 -18
- package/dist/runtime/managedCaller.js +61 -17
- package/dist/runtime/nativeSessionControl.js +102 -0
- package/dist/runtime/ports.js +6 -21
- package/dist/runtime/processExitObservation.js +8 -7
- package/dist/runtime/promptEnvelope.js +17 -6
- package/dist/runtime/providerContinuation.js +3 -9
- package/dist/runtime/providerContinuationReconciliationService.js +4 -13
- package/dist/runtime/providerControl.js +2 -7
- package/dist/runtime/providerRuntimeIdentity.js +110 -222
- package/dist/runtime/providerRuntimeReconciler.js +5 -9
- package/dist/runtime/runtimeBinding.js +0 -1
- package/dist/runtime/runtimeContinuationProjection.js +4 -7
- package/dist/runtime/runtimeDeadlines.js +9 -0
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +29 -65
- package/dist/runtime/runtimeProjection.js +43 -51
- package/dist/runtime/runtimeSessionCandidate.js +1 -3
- package/dist/runtime/sessionLaunchRequest.js +3 -7
- package/dist/runtime/sessionOwnerIdentity.js +7 -54
- package/dist/runtime/sessionOwnerRegistry.js +22 -17
- package/dist/runtime/sessionReconciliation.js +4 -8
- package/dist/runtime/sessionTerminationGuard.js +70 -259
- package/dist/runtime/sessionTokenMetrics.js +5 -16
- package/dist/runtime/structuredProviderHost.js +237 -117
- package/dist/runtime/taskRuntimeIsolation.js +39 -122
- package/dist/runtime/tmuxAdapters.js +39 -86
- package/dist/scheduler/activeRoleRunDelivery.js +354 -0
- package/dist/scheduler/leaderWakeupProcessor.js +75 -266
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +80 -9
- package/dist/scheduler/{roleTurnLiveness.js → roleRunLiveness.js} +26 -30
- package/dist/scheduler/{roleTurnStall.js → roleRunStall.js} +128 -139
- package/dist/scheduler/taskExecutionProjection.js +120 -124
- package/dist/scheduler/taskObservabilityProjection.js +29 -29
- package/dist/scheduler/taskWake.js +11 -4
- package/dist/scheduler/wakeReason.js +9 -1
- package/dist/setup/setupCommand.js +1 -0
- package/dist/storage/migrations/agentRunContract.js +159 -0
- package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
- package/dist/storage/sqliteSchema.js +431 -5
- package/dist/storage/sqliteStore.js +375 -220
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +10 -5
- package/dist/storage/taskStore.js +13 -11
- package/dist/storage/upgrade/upgradeOrchestrator.js +5 -7
- package/dist/surface/surfaceContributions.js +102 -0
- package/dist/task/completionReadiness.js +32 -6
- package/dist/task/deliveryGuard.js +16 -16
- package/dist/task/draftPlan.js +72 -12
- package/dist/task/nextAction.js +144 -128
- package/dist/task/remoteDelivery.js +6 -6
- package/dist/task/task.js +184 -18
- package/dist/task/taskActivation.js +301 -0
- package/dist/task/taskActivationService.js +392 -0
- package/dist/task/taskRecordReference.js +5 -4
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +55 -68
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +2 -2
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app.js +120 -20
- package/dist/web/assets/client/components.js +87 -54
- package/dist/web/assets/client/i18n.js +83 -41
- package/dist/web/assets/client/markdown.js +1 -1
- package/dist/web/assets/client/taskSurface.js +353 -0
- package/dist/web/assets/client/view.js +49 -44
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +22 -4
- package/dist/web/controllerWeb.js +60 -0
- package/dist/web/webMutation.js +28 -0
- package/dist/web/webServer.js +118 -8
- package/dist/web/webSnapshot.js +81 -74
- package/dist/web/webTaskSurface.js +64 -0
- package/dist/workItem/dependencyGate.js +1 -1
- package/dist/workItem/workItem.js +80 -48
- package/dist/workspace/workItemChangeSetManager.js +16 -9
- package/docs/agent-result-consumption.md +94 -0
- package/docs/agent-runtime-drivers.md +91 -0
- package/docs/architecture/README.md +38 -0
- package/docs/architecture/capabilities-and-resources.md +79 -0
- package/docs/managed-turn-and-session-runtime.md +222 -0
- package/docs/observability/README.md +81 -0
- package/docs/plugin-sdk.md +290 -0
- package/docs/provider-runtime.md +163 -0
- package/docs/release-workflow.md +303 -0
- package/docs/roles-and-configuration.md +113 -0
- package/docs/sqlite-control-plane-design.md +76 -0
- package/docs/task-dag-semantics.md +57 -0
- package/docs/task-delivery.md +103 -0
- package/docs/task-local-identity.md +6 -6
- package/docs/testing/verification-levels.md +86 -0
- package/i18n/README.zh-CN.md +98 -739
- package/package.json +2 -2
- package/skills/yui-leader/SKILL.md +130 -103
- package/skills/yui-leader/references/integration.md +39 -0
- package/skills/yui-leader/references/replicated-execution.md +42 -0
- package/skills/yui-leader/references/task-plugins.md +33 -0
- package/skills/yui-operator/SKILL.md +30 -59
- package/skills/yui-reviewer/SKILL.md +35 -36
- package/skills/yui-runtime/SKILL.md +88 -24
- package/skills/yui-runtime/references/publication.md +22 -0
- package/skills/yui-runtime/references/recovery.md +64 -0
- package/skills/yui-worker/SKILL.md +37 -39
- package/dist/cli/roleOptionCatalog.js +0 -68
- package/dist/context/sourceTurnContext.js +0 -30
- package/dist/execution/reviewMainTurn.js +0 -161
- package/dist/execution/workItemMainTurn.js +0 -164
- package/dist/lifecycle/exactTurnTerminalization.js +0 -407
- package/dist/runtime/preallocatedNativeSession.js +0 -13
- package/dist/runtime/runtimeStopReceipt.js +0 -42
- package/dist/scheduler/activeRoleTurnDelivery.js +0 -315
|
@@ -1,27 +1,31 @@
|
|
|
1
|
+
import { roleLaunchEventPayload, saveTaskRoleUpdate } from "../role/taskRoleUpdate.js";
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
+
import { createRunInput } from "../context/runInputContract.js";
|
|
6
|
+
import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeWorkItemExecutionAssignmentContextSnapshot, freezeReviewStageContextSnapshot, freezeRunContextSnapshot } from "../context/runContextPack.js";
|
|
5
7
|
import { contextSnapshotRef } from "../context/contextSnapshot.js";
|
|
6
8
|
import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
|
|
7
9
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
8
10
|
import { createTaskRecordRetirement, isTaskRecordRetired, operationalTaskRecords, taskRecordRetirement } from "../task/taskRecordRetirement.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
+
import { referencedWakeRunIds } from "../context/wakeNotification.js";
|
|
12
|
+
import { isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
|
|
11
13
|
import { readCommandText } from "./textInput.js";
|
|
12
14
|
import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
|
|
13
|
-
import { createRoleSessionSet, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
14
|
-
import {
|
|
15
|
+
import { createRoleSessionSet, roleAgentSessionResumeMode, updateTaskRoleProviderRuntime, taskRoleControlTarget } from "../executor/agentExecutor.js";
|
|
16
|
+
import { transferProviderAuthority, currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
15
17
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
16
18
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
19
|
+
import { agentExecutionComponentLabel } from "../agent/executionComponents.js";
|
|
20
|
+
import { agentRunConfigurationLabel, renderAgentRunConfiguration } from "../output/agentRunConfigurationPresentation.js";
|
|
17
21
|
import { formatTimestamp } from "../output/timePresentation.js";
|
|
18
|
-
import { renderRoleDetails } from "../output/rolePresentation.js";
|
|
19
|
-
import { createTaskMessage, taskMessageAuthorLabel, updateDraftTaskMessage } from "../message/message.js";
|
|
22
|
+
import { renderRoleDetails, renderRoleLaunchComparison } from "../output/rolePresentation.js";
|
|
23
|
+
import { createTaskMessage, expandTaskMessageResult, taskMessageAuthorLabel, updateDraftTaskMessage } from "../message/message.js";
|
|
20
24
|
import { assertDraftTaskExecutionFree, validateDraftWorkItemEdit } from "../task/draftPlan.js";
|
|
21
25
|
import { cancelInputRequest } from "../input/inputRequest.js";
|
|
22
|
-
import {
|
|
26
|
+
import { retireExactActiveRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
|
|
23
27
|
import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole } from "../role/role.js";
|
|
24
|
-
import {
|
|
28
|
+
import { createRun, runPurposeAdmitsTaskState, runExecutionObservation, withRunContextSnapshot } from "../agentRun/agentRun.js";
|
|
25
29
|
import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, finishReviewRound, recordReviewWorkspaceDisposition, retryReviewRound, retryRunningReviewExecutionLane, retryTaskReviewRound, startReplicatedReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
|
|
26
30
|
import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../review/deltaRecheck.js";
|
|
27
31
|
import { isCompletedTaskReviewEvidence } from "../review/reviewAcceptance.js";
|
|
@@ -30,38 +34,44 @@ import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
|
|
|
30
34
|
import { createDecision, supersedeDecision } from "../decision/decision.js";
|
|
31
35
|
import { createMilestone } from "../milestone/milestone.js";
|
|
32
36
|
import { runPublicationCommand } from "./taskPublicationCommands.js";
|
|
37
|
+
import { runTaskActivationCommand } from "./taskActivationCommands.js";
|
|
33
38
|
import { assertTaskRemoteDeliveryProof, projectTaskRemoteDeliveryFromStore, renderTaskRemoteDelivery, runTaskRemoteDeliveryCommand } from "./taskRemoteDeliveryCommand.js";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
39
|
+
import { enqueueRoleRunDispatch, enqueueWork, settleExactWorkExecution } from "../coordination/workMailboxQueue.js";
|
|
40
|
+
import { completeProcessing } from "../coordination/workMailbox.js";
|
|
41
|
+
import { runtimeLifecycleTarget, RUNTIME_SESSION_REPLACE_REQUIRED_REASON } from "../runtime/lifecycleReservation.js";
|
|
37
42
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
38
43
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
39
|
-
import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
44
|
+
import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, taskOwnsManagedWorkspace, updateTaskMetadata } from "../task/task.js";
|
|
40
45
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
41
46
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
42
47
|
import { requireResolvedAgentProfileRuntime } from "../profile/agentProfileRuntime.js";
|
|
43
48
|
import { assertProjectActive, resolveProject } from "../repository/project.js";
|
|
44
|
-
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem,
|
|
49
|
+
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, editWorkItemDefinition, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, prepareWorkItemDispatch, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
45
50
|
import { assertWorkItemDependenciesCompleted as assertWorkItemDependencyGate, WorkItemDependencyGateError } from "../workItem/dependencyGate.js";
|
|
46
|
-
import { createExecutionGroup, createReviewExecutionAssignment, createWorkItemExecutionAssignment, createWorkItemExecutionGroup,
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
51
|
+
import { createExecutionGroup, createReviewExecutionAssignment, createWorkItemExecutionAssignment, createWorkItemExecutionGroup, updateExecutionLane as updateUnifiedExecutionLane, updateWorkItemExecutionLane, workItemExecutionGroupSettled } from "../execution/workItemExecution.js";
|
|
52
|
+
import { dispatchWorkItemSynthesis, selectedWorkItemSynthesisProducers } from "../execution/workItemMainRun.js";
|
|
53
|
+
import { dispatchReviewSynthesis, selectedReviewSynthesisProducers } from "../execution/reviewMainRun.js";
|
|
54
|
+
import { synthesisSourceRunIds } from "../context/runContextPack.js";
|
|
49
55
|
import { projectWorkItemExecution } from "../execution/workItemExecutionProjection.js";
|
|
50
56
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
51
57
|
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractResolution.js";
|
|
52
58
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
53
59
|
import { hasAgentConfigOptions, hasNoRoleMutation, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
|
|
54
60
|
import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
|
|
55
|
-
import {
|
|
61
|
+
import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
|
|
56
62
|
import { runTaskContextCommand } from "./taskContextCommand.js";
|
|
63
|
+
import { listContextMessages } from "../context/taskContext.js";
|
|
64
|
+
import { createProjectResources, validateArtifactInput } from "../resources/projectResourceService.js";
|
|
65
|
+
import { artifactSummary } from "../resources/projectResource.js";
|
|
57
66
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
58
67
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
59
|
-
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel,
|
|
68
|
+
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
60
69
|
import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
61
70
|
import { runGrantCommand } from "./grantCommands.js";
|
|
62
71
|
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
63
|
-
import { taskLocalActor as resolveTaskLocalActor,
|
|
64
|
-
import { currentManagedRuntime } from "../runtime/managedCaller.js";
|
|
72
|
+
import { taskLocalActor as resolveTaskLocalActor, assertTaskDeliveryAuthority } from "./taskActor.js";
|
|
73
|
+
import { currentManagedRuntime, resolveManagedTaskReader } from "../runtime/managedCaller.js";
|
|
74
|
+
import { resolveMessageRecipient, messageContinuationBlocker } from "../message/messageContinuation.js";
|
|
65
75
|
import { enqueueOperatorEvent } from "../scheduler/operatorEvent.js";
|
|
66
76
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
67
77
|
import { renderWakeReason, wakeReason } from "../scheduler/wakeReason.js";
|
|
@@ -181,8 +191,11 @@ export function previewTaskRoleAgentConfigurationMutation(args, store) {
|
|
|
181
191
|
}
|
|
182
192
|
const parsed = parseRoleOptions(tail, new Map([
|
|
183
193
|
...roleOptionSpecs({ update: true, includeAgent: true }),
|
|
184
|
-
["--profile", "value"]
|
|
194
|
+
["--profile", "value"],
|
|
195
|
+
["--environment", "value"],
|
|
196
|
+
["--managed-environment", "flag"]
|
|
185
197
|
]), usage);
|
|
198
|
+
validateTaskRoleEnvironmentOptions(parsed, usage);
|
|
186
199
|
if (parsed.has("--agent") && (parsed.one("--agent")?.trim().length ?? 0) === 0) {
|
|
187
200
|
throw usageError("--agent is required.", usage);
|
|
188
201
|
}
|
|
@@ -201,7 +214,7 @@ export function previewTaskRoleAgentConfigurationMutation(args, store) {
|
|
|
201
214
|
}
|
|
202
215
|
export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
203
216
|
const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote] [--accept-published-tree <publication-id>].";
|
|
204
|
-
const parsed =
|
|
217
|
+
const parsed = parseMultiValueTail(args, new Set(["--summary", "--summary-file", "--accept-published-tree"]), new Set(["--artifact-ref"]), usage, new Set(["--refresh-remote"]));
|
|
205
218
|
exactPositionals(parsed.positionals, 1, usage);
|
|
206
219
|
const inlineSummary = parsed.options.get("--summary");
|
|
207
220
|
const summaryFile = parsed.options.get("--summary-file");
|
|
@@ -213,6 +226,7 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
|
213
226
|
return {
|
|
214
227
|
taskId: parsed.positionals[0],
|
|
215
228
|
summary,
|
|
229
|
+
artifactRefs: parsed.multiOptions.get("--artifact-ref") ?? [],
|
|
216
230
|
...(acceptedPublishedTreePublicationId === undefined
|
|
217
231
|
? {}
|
|
218
232
|
: { acceptedPublishedTreePublicationId })
|
|
@@ -233,6 +247,7 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
|
233
247
|
export function preflightTaskCompletion(taskId, store, options = {}, request = {}) {
|
|
234
248
|
const task = requireTask(store, taskId);
|
|
235
249
|
const actor = taskActor(store, options, task.id);
|
|
250
|
+
assertTaskDeliveryAuthority(store, options.environment, task.id);
|
|
236
251
|
if (task.status === "completed") {
|
|
237
252
|
return { task, actor, completed: true, activeTaskReview: false };
|
|
238
253
|
}
|
|
@@ -279,19 +294,67 @@ function formatCompletionBlockers(taskId, blockers) {
|
|
|
279
294
|
}
|
|
280
295
|
export function runTaskCommand(args, store, options = {}) {
|
|
281
296
|
const [command, ...rest] = args;
|
|
297
|
+
const delivery = command === "complete"
|
|
298
|
+
|| (command === "work" && ["dispatch", "synthesize", "review", "accept"].includes(rest[0] ?? ""))
|
|
299
|
+
|| (command === "review" && !["list", "show"].includes(rest[0] ?? ""))
|
|
300
|
+
|| ((command === "run" || command === "run") && rest[0] === "retry");
|
|
301
|
+
if (delivery && options.environment?.YUI_SESSION_SCOPE === "task"
|
|
302
|
+
&& options.environment.YUI_TASK_ID !== undefined) {
|
|
303
|
+
assertTaskDeliveryAuthority(store, options.environment, options.environment.YUI_TASK_ID);
|
|
304
|
+
}
|
|
282
305
|
switch (command) {
|
|
306
|
+
case "artifact": {
|
|
307
|
+
const [action, taskId, value] = rest;
|
|
308
|
+
if (!taskId || !["list", "show", "save"].includes(action)
|
|
309
|
+
|| rest.length !== (action === "list" ? 2 : 3)) {
|
|
310
|
+
throw usageError("Usage: yui task artifact list <task> | show <task> <artifact-id> | save <task> <artifact-json>");
|
|
311
|
+
}
|
|
312
|
+
if (options.environment?.YUI_SESSION_SCOPE === "task" && options.environment.YUI_TASK_ID !== taskId) {
|
|
313
|
+
throw usageError("Artifact is outside the managed Task scope.");
|
|
314
|
+
}
|
|
315
|
+
requireTask(store, taskId);
|
|
316
|
+
let data;
|
|
317
|
+
if (action === "list")
|
|
318
|
+
data = store.listArtifacts(taskId).map(artifactSummary);
|
|
319
|
+
else if (action === "show") {
|
|
320
|
+
data = store.getArtifact(taskId, value);
|
|
321
|
+
if (data === null)
|
|
322
|
+
throw usageError("Artifact not found in this Task.");
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
taskActor(store, options, taskId);
|
|
326
|
+
let parsed;
|
|
327
|
+
try {
|
|
328
|
+
parsed = JSON.parse(value);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
throw usageError("Artifact input must be JSON.");
|
|
332
|
+
}
|
|
333
|
+
let input;
|
|
334
|
+
try {
|
|
335
|
+
input = validateArtifactInput(parsed);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
throw usageError(`Artifact input is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
339
|
+
}
|
|
340
|
+
data = createProjectResources(store).saveArtifact(taskId, input);
|
|
341
|
+
}
|
|
342
|
+
return output(JSON.stringify(data, null, 2), data);
|
|
343
|
+
}
|
|
283
344
|
case "create": return createTaskCommand(rest, store, options);
|
|
284
345
|
case "update": return output(updateTaskCommand(rest, store, options));
|
|
285
346
|
case "list": return listTaskCommand(rest, store);
|
|
286
347
|
case "show": return showTaskCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
287
|
-
case "context": return runTaskContextCommand(rest, store, options.
|
|
348
|
+
case "context": return runTaskContextCommand(rest, store, options.environment);
|
|
288
349
|
case "next-action": return runTaskNextActionCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
289
350
|
case "remote-delivery": return runTaskRemoteDeliveryCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
290
351
|
case "activate": return output(activateTaskCommand(rest, store, options));
|
|
352
|
+
case "activation": return runTaskActivationCommand(rest, store, options);
|
|
291
353
|
case "complete": return completeTaskCommand(rest, store, options);
|
|
292
354
|
case "reopen": return output(reopenTaskCommand(rest, store, options));
|
|
293
355
|
case "archive": return output(archiveTaskCommand(rest, store, options));
|
|
294
356
|
case "retire": return retireTaskCommand(rest, store, options);
|
|
357
|
+
case "cancel": return cancelTaskCommand(rest, store, options);
|
|
295
358
|
case "reconcile": return output(reconcileTaskCommand(rest, store, options));
|
|
296
359
|
case "message": {
|
|
297
360
|
const execution = taskMessageCommand(rest, store, options);
|
|
@@ -306,7 +369,8 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
306
369
|
case "role": return taskRoleCommand(rest, store, options);
|
|
307
370
|
case "work": return taskWorkCommand(rest, store, options);
|
|
308
371
|
case "review": return taskReviewCommand(rest, store, options);
|
|
309
|
-
case "
|
|
372
|
+
case "run":
|
|
373
|
+
case "run": return taskRunCommand(rest, store, options);
|
|
310
374
|
case "brief": return taskBriefCommand(rest, store, options);
|
|
311
375
|
case "decision": return taskDecisionCommand(rest, store, options);
|
|
312
376
|
case "milestone": return taskMilestoneCommand(rest, store, options);
|
|
@@ -356,21 +420,21 @@ function workItemCandidateProducerRoles(store, item, candidate) {
|
|
|
356
420
|
roles.add(LEADER_ROLE);
|
|
357
421
|
return roles;
|
|
358
422
|
}
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
361
|
-
||
|
|
362
|
-
||
|
|
363
|
-
||
|
|
364
|
-
throw dataError(`WorkItem Candidate producer
|
|
423
|
+
const sourceRun = store.getRun(item.taskId, candidate.source.runId);
|
|
424
|
+
if (sourceRun === null
|
|
425
|
+
|| sourceRun.workItemId !== item.id
|
|
426
|
+
|| sourceRun.purpose !== "execution"
|
|
427
|
+
|| sourceRun.status !== "completed") {
|
|
428
|
+
throw dataError(`WorkItem Candidate producer AgentRun is unavailable: ${item.id}/${candidate.source.runId}.`);
|
|
365
429
|
}
|
|
366
|
-
roles.add(
|
|
367
|
-
if (
|
|
368
|
-
const group = workItemExecutionGroupById(item,
|
|
430
|
+
roles.add(sourceRun.roleName);
|
|
431
|
+
if (sourceRun.sourceExecutionGroupId !== undefined) {
|
|
432
|
+
const group = workItemExecutionGroupById(item, sourceRun.sourceExecutionGroupId);
|
|
369
433
|
if (group === undefined) {
|
|
370
434
|
throw dataError(`WorkItem Candidate ExecutionGroup is unavailable: `
|
|
371
|
-
+ `${item.id}/${
|
|
435
|
+
+ `${item.id}/${sourceRun.sourceExecutionGroupId}.`);
|
|
372
436
|
}
|
|
373
|
-
for (const producer of
|
|
437
|
+
for (const producer of selectedWorkItemSynthesisProducers(store, item, group, synthesisSourceRunIds(store, sourceRun))) {
|
|
374
438
|
roles.add(producer.roleName);
|
|
375
439
|
}
|
|
376
440
|
}
|
|
@@ -506,6 +570,8 @@ export function updateTaskMetadataCommand(store, taskId, patch, options = {}) {
|
|
|
506
570
|
tx.saveTask(updated);
|
|
507
571
|
recordTaskEvent(tx, updated.id, "task.updated", {
|
|
508
572
|
status: updated.status,
|
|
573
|
+
previous: editedFieldValues(current, Object.keys(patch)),
|
|
574
|
+
current: editedFieldValues(updated, Object.keys(patch)),
|
|
509
575
|
...(updated.type === undefined ? {} : { taskType: updated.type })
|
|
510
576
|
}, now);
|
|
511
577
|
enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
|
|
@@ -521,16 +587,17 @@ export function submitOperatorMessage(body, taskId, store, options = {}) {
|
|
|
521
587
|
const task = requireTask(tx, taskId);
|
|
522
588
|
assertTaskOpen(task);
|
|
523
589
|
const message = appendMessage(tx, task.id, body, "operator", { type: "operator" }, now);
|
|
524
|
-
if (task.status
|
|
590
|
+
if (leaderWakingTaskStatus(task.status)) {
|
|
525
591
|
enqueueWork(tx, leaderMailbox(task.id), "operator-input", now, [messageRef(task.id, message.id)]);
|
|
526
592
|
}
|
|
527
593
|
return { task, message, created: false };
|
|
528
594
|
}
|
|
529
595
|
const created = createTaskAggregate(tx, titleFrom(body), {}, now);
|
|
530
596
|
const message = appendMessage(tx, created.task.id, body, "operator", { type: "operator" }, now);
|
|
597
|
+
enqueueWork(tx, leaderMailbox(created.task.id), "operator-input", now, [messageRef(created.task.id, message.id)]);
|
|
531
598
|
return { ...created, message, created: true };
|
|
532
599
|
});
|
|
533
|
-
notifyMailbox(options.runtime, result.task.status
|
|
600
|
+
notifyMailbox(options.runtime, leaderWakingTaskStatus(result.task.status) ? leaderMailbox(result.task.id) : taskMailbox(result.task.id), result.task.id);
|
|
534
601
|
return result.created
|
|
535
602
|
? `Created Draft task ${result.task.id}: ${result.task.title}\nSubmitted message ${result.message.id}\n`
|
|
536
603
|
: `Submitted message ${result.message.id} to ${result.task.id}\n`;
|
|
@@ -657,7 +724,7 @@ function showTaskCommand(args, store, currentTaskCandidate) {
|
|
|
657
724
|
events: events.length,
|
|
658
725
|
workItems: work.length,
|
|
659
726
|
currentWorkItems: currentWorkItemCount,
|
|
660
|
-
|
|
727
|
+
runs: store.listRuns(task.id).length,
|
|
661
728
|
changeSets: changeSets.length,
|
|
662
729
|
integrations: integrations.length,
|
|
663
730
|
publications: publications.length,
|
|
@@ -698,7 +765,7 @@ function showTaskCommand(args, store, currentTaskCandidate) {
|
|
|
698
765
|
`Events: ${counts.events}`,
|
|
699
766
|
`Work items: ${counts.workItems}`,
|
|
700
767
|
`Current work items: ${currentWorkItemCount}`,
|
|
701
|
-
`
|
|
768
|
+
`AgentRuns: ${counts.runs}`,
|
|
702
769
|
`ChangeSets: ${counts.changeSets}`,
|
|
703
770
|
`Integration Attempts: ${counts.integrations}`,
|
|
704
771
|
`Publication references: ${counts.publications} (${verifiedMergedPublications} verified merged)`,
|
|
@@ -721,7 +788,7 @@ function activateTaskCommand(args, store, options) {
|
|
|
721
788
|
const task = requireTask(tx, args[0]);
|
|
722
789
|
if (task.status === "archived")
|
|
723
790
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
724
|
-
if (task.status === "
|
|
791
|
+
if (task.status === "cancelled")
|
|
725
792
|
throw usageError(`Task is retired: ${task.id}.`);
|
|
726
793
|
if (task.status === "completed") {
|
|
727
794
|
throw usageError(`Task ${task.id} is completed; use task reopen before activating it.`);
|
|
@@ -734,13 +801,26 @@ function activateTaskCommand(args, store, options) {
|
|
|
734
801
|
|| activation.task.id !== task.id
|
|
735
802
|
|| activation.task.status !== "active"
|
|
736
803
|
|| !isDeepStrictEqual(activation.task.workspaceIdentity, task.workspaceIdentity)
|
|
737
|
-
|| activation.path !== task.cwd
|
|
738
|
-
|| workspace === null
|
|
739
|
-
|| workspace.owner.type !== "task"
|
|
740
|
-
|| workspace.owner.taskId !== task.id
|
|
741
|
-
|| workspace.root !== task.cwd) {
|
|
804
|
+
|| activation.path !== task.cwd) {
|
|
742
805
|
throw usageError(`Task workspace activation proof does not match ${task.id}.`);
|
|
743
806
|
}
|
|
807
|
+
// An empty environment plan over no bound Project is a legal Task shape,
|
|
808
|
+
// so the proof of adoption is the *absence* of a workspace rather than a
|
|
809
|
+
// ManagedWorkspace record. Demanding one here would have forced every
|
|
810
|
+
// such activation to create a worktree purely to satisfy this check.
|
|
811
|
+
if (taskOwnsManagedWorkspace(task)) {
|
|
812
|
+
if (workspace === null
|
|
813
|
+
|| workspace.owner.type !== "task"
|
|
814
|
+
|| workspace.owner.taskId !== task.id
|
|
815
|
+
|| workspace.root !== task.cwd) {
|
|
816
|
+
throw usageError(`Task workspace activation proof does not match ${task.id}.`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
else if (workspace !== null
|
|
820
|
+
|| task.workspaceIdentity !== undefined
|
|
821
|
+
|| activation.path !== undefined) {
|
|
822
|
+
throw usageError(`Workspace-free Task activation proof claims a workspace: ${task.id}.`);
|
|
823
|
+
}
|
|
744
824
|
return { task, changed: activation.changed };
|
|
745
825
|
}
|
|
746
826
|
throw usageError(`Task ${task.id} activation requires atomic workspace adoption through the CLI preflight.`);
|
|
@@ -790,7 +870,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
790
870
|
};
|
|
791
871
|
}
|
|
792
872
|
// Completion is a Task decision only. The current Provider Turn remains
|
|
793
|
-
// responsible for closing its own
|
|
873
|
+
// responsible for closing its own AgentRun, and the reusable Session keeps
|
|
794
874
|
// its independent lifecycle.
|
|
795
875
|
// Issue 06: re-validate the full completion readiness inside the
|
|
796
876
|
// transaction (the CAS fence) after final-review preparation. This is the
|
|
@@ -803,7 +883,21 @@ function completeTaskCommand(args, store, options) {
|
|
|
803
883
|
if (!readiness.ready) {
|
|
804
884
|
throw usageError(formatCompletionBlockers(task.id, readiness.blockers));
|
|
805
885
|
}
|
|
806
|
-
const
|
|
886
|
+
for (const ref of request.artifactRefs) {
|
|
887
|
+
if (ref.startsWith("artifact-")) {
|
|
888
|
+
fixedArtifactRefs(tx, task.id, [ref]);
|
|
889
|
+
}
|
|
890
|
+
else if (ref.startsWith("turn:")) {
|
|
891
|
+
const run = tx.getRun(task.id, ref.slice("turn:".length));
|
|
892
|
+
if (run === null || run.result === undefined) {
|
|
893
|
+
throw usageError(`Task completion result ref is not readable: ${ref}.`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
else if (!/^https?:\/\/[^\s]+$/u.test(ref)) {
|
|
897
|
+
throw usageError("Completion --artifact-ref must be a saved artifact id, turn:<local-turn-id>, or an explicit HTTP(S) reference URL.");
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const completed = completeTask(task, now, { by: actor, summary, artifactRefs: request.artifactRefs });
|
|
807
901
|
tx.saveTask(completed);
|
|
808
902
|
tx.clearPendingWakeup(task.id);
|
|
809
903
|
tx.clearLeaderFailure(task.id);
|
|
@@ -834,6 +928,10 @@ function completeTaskCommand(args, store, options) {
|
|
|
834
928
|
const terminalEvent = recordTaskEvent(tx, task.id, "task.completed", {
|
|
835
929
|
by: actor,
|
|
836
930
|
summary,
|
|
931
|
+
...(completed.completionArtifactRefs === undefined ? {} : {
|
|
932
|
+
artifactRefs: JSON.stringify(completed.completionArtifactRefs)
|
|
933
|
+
}),
|
|
934
|
+
dispatchHistory: JSON.stringify(taskDispatchMailboxes(tx, task.id)),
|
|
837
935
|
...(completedProjectHeads === undefined
|
|
838
936
|
? {}
|
|
839
937
|
: { projectHeads: completedProjectHeads }),
|
|
@@ -850,7 +948,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
850
948
|
// discarded at this lifecycle boundary.
|
|
851
949
|
tx.removeWorkMailbox(taskMailbox(task.id));
|
|
852
950
|
// Role mailboxes are also derived wake state. A Worker result or runtime
|
|
853
|
-
// signal queued while the final
|
|
951
|
+
// signal queued while the final AgentRun was being settled must not survive a
|
|
854
952
|
// completed Task and become actionable after a later explicit reopen.
|
|
855
953
|
for (const role of roles) {
|
|
856
954
|
tx.removeWorkMailbox(roleMailbox(task.id, role.name));
|
|
@@ -895,19 +993,39 @@ function reopenTaskCommand(args, store, options) {
|
|
|
895
993
|
return { task, changed: false };
|
|
896
994
|
if (task.status === "archived")
|
|
897
995
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
898
|
-
if (task.status !== "completed")
|
|
899
|
-
throw usageError(`Task is not completed: ${task.id}.`);
|
|
996
|
+
if (task.status !== "completed" && task.status !== "cancelled") {
|
|
997
|
+
throw usageError(`Task is not completed or cancelled: ${task.id}.`);
|
|
998
|
+
}
|
|
999
|
+
const actor = taskActor(tx, options, task.id);
|
|
1000
|
+
if (task.status === "cancelled" && actor === "leader") {
|
|
1001
|
+
throw usageError("Restoring a cancelled Task requires user or Operator authority.");
|
|
1002
|
+
}
|
|
1003
|
+
const dispatchHistory = JSON.stringify(taskDispatchMailboxes(tx, task.id));
|
|
1004
|
+
// Reopening changes Task intent, not the disposition of existing inputs.
|
|
1005
|
+
// In particular, external messages and unknown deliveries remain owned by
|
|
1006
|
+
// their existing mailbox fences.
|
|
900
1007
|
const active = reopenTask(task, now);
|
|
901
1008
|
tx.saveTask(active);
|
|
902
1009
|
const reopenedReason = wakeReason("task-reopened");
|
|
903
|
-
|
|
1010
|
+
if (actor !== "leader") {
|
|
1011
|
+
enqueueWork(tx, leaderMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
1012
|
+
}
|
|
904
1013
|
enqueueWork(tx, taskMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
905
|
-
recordTaskEvent(tx, task.id, "task.reopened", {
|
|
906
|
-
|
|
1014
|
+
recordTaskEvent(tx, task.id, "task.reopened", {
|
|
1015
|
+
status: active.status, by: actor, historicalInputs: "preserved", dispatchHistory,
|
|
1016
|
+
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {}),
|
|
1017
|
+
previous: editedFieldValues(task, [
|
|
1018
|
+
"status", "completedAt", "completedBy", "completionSummary", "completionArtifactRefs",
|
|
1019
|
+
"retiredAt", "retiredBy", "retirementSummary", "replacementTaskId", "retirementIsolation"
|
|
1020
|
+
])
|
|
1021
|
+
}, now);
|
|
1022
|
+
return { task: active, changed: true, wakeLeader: actor !== "leader" };
|
|
907
1023
|
});
|
|
908
1024
|
if (result.changed) {
|
|
909
1025
|
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
910
|
-
|
|
1026
|
+
if (result.wakeLeader) {
|
|
1027
|
+
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
1028
|
+
}
|
|
911
1029
|
}
|
|
912
1030
|
return result.changed
|
|
913
1031
|
? `Reopened task ${result.task.id}\n`
|
|
@@ -922,13 +1040,17 @@ function archiveTaskCommand(args, store, options) {
|
|
|
922
1040
|
if (task.status === "archived")
|
|
923
1041
|
return { task, changed: false };
|
|
924
1042
|
if (task.status !== "completed"
|
|
925
|
-
&& task.status !== "
|
|
1043
|
+
&& task.status !== "cancelled") {
|
|
926
1044
|
throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
|
|
927
1045
|
}
|
|
928
1046
|
const remoteDelivery = request.disposition === "integrated"
|
|
929
1047
|
? assertTaskRemoteDeliveryProof(tx, task, options.archiveRemoteDeliveryProof, { forceUnverified: request.forceUnverified })
|
|
930
1048
|
: undefined;
|
|
931
1049
|
assertNoOpenInputRequests(tx, task.id, "archiving the Task");
|
|
1050
|
+
const unsettledWork = tx.listWorkItems(task.id).find((item) => item.status === "open");
|
|
1051
|
+
if (unsettledWork !== undefined) {
|
|
1052
|
+
throw usageError(`Work Item ${unsettledWork.id} must be accepted or explicitly retired before archive.`);
|
|
1053
|
+
}
|
|
932
1054
|
const unresolvedIntegration = tx.listIntegrationAttempts(task.id).find((integration) => (integration.status === "running"
|
|
933
1055
|
|| integration.status === "blocked"
|
|
934
1056
|
|| integration.status === "validating"));
|
|
@@ -945,9 +1067,9 @@ function archiveTaskCommand(args, store, options) {
|
|
|
945
1067
|
throw usageError(`Task ${task.id} still has managed worktrees; clean them before archiving.`);
|
|
946
1068
|
}
|
|
947
1069
|
const activeRole = tx.listRoles(task.id)
|
|
948
|
-
.find((role) => tx.
|
|
1070
|
+
.find((role) => tx.getActiveRun(task.id, role.name) !== null);
|
|
949
1071
|
if (activeRole !== undefined) {
|
|
950
|
-
throw usageError(`Task ${task.id} still has an active
|
|
1072
|
+
throw usageError(`Task ${task.id} still has an active AgentRun for Role ${activeRole.name}; `
|
|
951
1073
|
+ "stop its runtime before archiving.");
|
|
952
1074
|
}
|
|
953
1075
|
const liveSessionRole = tx.listRoles(task.id).find((role) => {
|
|
@@ -1003,6 +1125,41 @@ function archiveTaskCommand(args, store, options) {
|
|
|
1003
1125
|
? `Archived task ${result.task.id}\n`
|
|
1004
1126
|
: `Task ${result.task.id} is already archived\n`;
|
|
1005
1127
|
}
|
|
1128
|
+
function cancelTaskCommand(args, store, options) {
|
|
1129
|
+
const usage = "Task cancel usage: yui task cancel <task> (--summary <text>|--summary-file <path|->).";
|
|
1130
|
+
const parsed = parseTail(args, new Set(["--summary", "--summary-file"]), usage);
|
|
1131
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1132
|
+
const summary = readCommandText(parsed.options.get("--summary"), parsed.options.get("--summary-file"), "--summary", usage);
|
|
1133
|
+
const now = clock(options);
|
|
1134
|
+
const result = store.transaction((tx) => {
|
|
1135
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
1136
|
+
const actor = taskActor(tx, options, task.id);
|
|
1137
|
+
const cancelled = retireTask(task, { by: actor, summary }, now);
|
|
1138
|
+
if (cancelled === task)
|
|
1139
|
+
return task;
|
|
1140
|
+
const dispatchHistory = JSON.stringify(taskDispatchMailboxes(tx, task.id));
|
|
1141
|
+
tx.saveTask(cancelled);
|
|
1142
|
+
tx.clearPendingWakeup(task.id);
|
|
1143
|
+
tx.clearLeaderFailure(task.id);
|
|
1144
|
+
for (const mailbox of tx.listWorkMailboxes()) {
|
|
1145
|
+
if ((mailbox.target.kind === "task" || mailbox.target.kind === "role")
|
|
1146
|
+
&& mailbox.target.taskId === task.id)
|
|
1147
|
+
tx.removeWorkMailbox(mailbox.target);
|
|
1148
|
+
}
|
|
1149
|
+
const event = recordTaskEvent(tx, task.id, "task.cancelled", { by: actor, summary, dispatchHistory }, now);
|
|
1150
|
+
enqueueOperatorEvent(tx, event, "task-terminal", now);
|
|
1151
|
+
// Cancellation is intent, not fabricated proof that old processes stopped.
|
|
1152
|
+
// AgentRuns, inputs, WorkItems and their results remain independently readable.
|
|
1153
|
+
return cancelled;
|
|
1154
|
+
});
|
|
1155
|
+
options.runtime?.notifyStateChanged(result.id);
|
|
1156
|
+
notifyMailbox(options.runtime, { kind: "operator" }, result.id);
|
|
1157
|
+
return output(`Cancelled task ${result.id}\n`, { task: result });
|
|
1158
|
+
}
|
|
1159
|
+
/** Historical evidence only; these snapshots are never dispatch input. */
|
|
1160
|
+
function taskDispatchMailboxes(store, taskId) {
|
|
1161
|
+
return store.listWorkMailboxes().filter(({ target }) => (target.kind === "task" || target.kind === "role") && target.taskId === taskId);
|
|
1162
|
+
}
|
|
1006
1163
|
function retireTaskCommand(args, store, options) {
|
|
1007
1164
|
const usage = "Task retire usage: yui task retire <task> (--summary <text>|--summary-file <path|->) [--replacement <task>].";
|
|
1008
1165
|
const parsed = parseTail(args, new Set(["--summary", "--summary-file", "--replacement"]), usage);
|
|
@@ -1014,7 +1171,7 @@ function retireTaskCommand(args, store, options) {
|
|
|
1014
1171
|
const result = store.transaction((tx) => {
|
|
1015
1172
|
const task = requireTask(tx, taskId);
|
|
1016
1173
|
const actor = taskActor(tx, options, task.id);
|
|
1017
|
-
if (task.status === "
|
|
1174
|
+
if (task.status === "cancelled") {
|
|
1018
1175
|
const same = task.retirementSummary === summary
|
|
1019
1176
|
&& task.replacementTaskId === replacementTaskId;
|
|
1020
1177
|
if (!same)
|
|
@@ -1043,12 +1200,12 @@ function retireTaskCommand(args, store, options) {
|
|
|
1043
1200
|
throw usageError(`Task ${task.id} has an active DurableJob: ${activeRetireJob.id}/${activeRetireJob.status}.`);
|
|
1044
1201
|
}
|
|
1045
1202
|
assertTaskRetirementProof(tx, task, options.taskRetirementProof);
|
|
1046
|
-
for (const run of tx.
|
|
1047
|
-
const terminal =
|
|
1203
|
+
for (const run of tx.listRuns(task.id).filter(({ status: runStatus }) => (runStatus === "active"))) {
|
|
1204
|
+
const terminal = terminalizeExactTaskRun(tx, {
|
|
1048
1205
|
taskId: task.id,
|
|
1049
1206
|
roleName: run.roleName,
|
|
1050
1207
|
agentId: run.effective.agentId,
|
|
1051
|
-
|
|
1208
|
+
runId: run.id,
|
|
1052
1209
|
mailboxDisposition: "discard",
|
|
1053
1210
|
outcome: {
|
|
1054
1211
|
status: "failed",
|
|
@@ -1057,11 +1214,11 @@ function retireTaskCommand(args, store, options) {
|
|
|
1057
1214
|
}
|
|
1058
1215
|
}, now);
|
|
1059
1216
|
if (terminal.disposition !== "applied") {
|
|
1060
|
-
throw usageError(`Task
|
|
1217
|
+
throw usageError(`Task AgentRun changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
1061
1218
|
}
|
|
1062
1219
|
}
|
|
1063
1220
|
for (const item of tx.listWorkItems(task.id)) {
|
|
1064
|
-
if (item.status === "
|
|
1221
|
+
if (item.status === "accepted" || item.status === "retired")
|
|
1065
1222
|
continue;
|
|
1066
1223
|
tx.saveWorkItem(task.id, updateWorkItemStatus(item, "retired", now, `Task retired: ${summary}`));
|
|
1067
1224
|
}
|
|
@@ -1082,12 +1239,14 @@ function retireTaskCommand(args, store, options) {
|
|
|
1082
1239
|
const retired = retireTask(task, {
|
|
1083
1240
|
by: actor,
|
|
1084
1241
|
summary,
|
|
1242
|
+
isolated: true,
|
|
1085
1243
|
...(replacementTaskId === undefined ? {} : { replacementTaskId })
|
|
1086
1244
|
}, now);
|
|
1087
1245
|
tx.saveTask(retired);
|
|
1088
1246
|
const terminalEvent = recordTaskEvent(tx, task.id, "task.retired", {
|
|
1089
1247
|
by: actor,
|
|
1090
1248
|
summary,
|
|
1249
|
+
isolationEstablished: "true",
|
|
1091
1250
|
...(replacementTaskId === undefined ? {} : { replacementTaskId })
|
|
1092
1251
|
}, now);
|
|
1093
1252
|
enqueueOperatorEvent(tx, terminalEvent, "task-terminal", now);
|
|
@@ -1153,10 +1312,13 @@ function formatProjectCommits(projects) {
|
|
|
1153
1312
|
export function validateTaskArchiveRequest(args, store, options = {}) {
|
|
1154
1313
|
const request = parseTaskArchiveArguments(args);
|
|
1155
1314
|
const task = requireTask(store, request.taskId);
|
|
1156
|
-
taskActor(store, options, task.id);
|
|
1315
|
+
const archiveActor = taskActor(store, options, task.id);
|
|
1316
|
+
if (archiveActor === "leader") {
|
|
1317
|
+
throw usageError("Task archive requires independent user or Operator authorization.");
|
|
1318
|
+
}
|
|
1157
1319
|
if (task.status !== "archived"
|
|
1158
1320
|
&& task.status !== "completed"
|
|
1159
|
-
&& task.status !== "
|
|
1321
|
+
&& task.status !== "cancelled") {
|
|
1160
1322
|
throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
|
|
1161
1323
|
}
|
|
1162
1324
|
if (task.status !== "archived") {
|
|
@@ -1179,9 +1341,55 @@ function reconcileTaskCommand(args, store, options) {
|
|
|
1179
1341
|
}
|
|
1180
1342
|
function taskMessageCommand(args, store, options) {
|
|
1181
1343
|
const [command, ...rest] = args;
|
|
1344
|
+
if (command === "handoff") {
|
|
1345
|
+
const usage = "Task message handoff usage: yui task message handoff <task/message> --to <role>.";
|
|
1346
|
+
const parsed = parseTail(rest, new Set(["--to"]), usage);
|
|
1347
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1348
|
+
const ref = taskRecordReference(parsed.positionals[0], "message", "Message reference", options);
|
|
1349
|
+
const now = clock(options);
|
|
1350
|
+
const message = store.transaction((tx) => {
|
|
1351
|
+
assertTaskDeliveryAuthority(tx, options.environment, ref.taskId);
|
|
1352
|
+
const current = tx.listMessages(ref.taskId).find((entry) => entry.id === ref.localId);
|
|
1353
|
+
if (current?.recipient?.ownerRunId === undefined || current.continuation?.runId !== undefined) {
|
|
1354
|
+
throw usageError("Only an unassigned pending Message can be explicitly handed off.");
|
|
1355
|
+
}
|
|
1356
|
+
const recipient = resolveMessageRecipient(tx, ref.taskId, requiredOption(parsed.options, "--to"), {
|
|
1357
|
+
...(current.recipient.workItemId === undefined ? {} : { workItemId: current.recipient.workItemId }),
|
|
1358
|
+
...(current.recipient.reviewRoundId === undefined ? {} : { reviewRoundId: current.recipient.reviewRoundId })
|
|
1359
|
+
});
|
|
1360
|
+
const updated = { ...current, recipient, continuation: {},
|
|
1361
|
+
handovers: [...(current.handovers ?? []), { from: current.recipient, at: now.toISOString() }] };
|
|
1362
|
+
const blocker = messageContinuationBlocker(tx, updated);
|
|
1363
|
+
if (blocker !== undefined)
|
|
1364
|
+
throw usageError(`Message handoff cannot proceed: ${blocker}.`);
|
|
1365
|
+
tx.updateMessage(ref.taskId, updated);
|
|
1366
|
+
recordTaskEvent(tx, ref.taskId, "message.handed-off", {
|
|
1367
|
+
messageId: current.id, fromRole: current.recipient.roleName, toRole: recipient.roleName,
|
|
1368
|
+
ownerRunId: recipient.ownerRunId
|
|
1369
|
+
}, now);
|
|
1370
|
+
enqueueWork(tx, taskMailbox(ref.taskId), "message-continuation", now, [messageRef(ref.taskId, current.id)]);
|
|
1371
|
+
return updated;
|
|
1372
|
+
});
|
|
1373
|
+
notifyMailbox(options.runtime, taskMailbox(ref.taskId), ref.taskId);
|
|
1374
|
+
return output(`Handed off Message ${ref.localId} to ${message.recipient.roleName}.\n`, message);
|
|
1375
|
+
}
|
|
1376
|
+
if (command === "show") {
|
|
1377
|
+
const usage = "Task message show usage: yui task message show <task/message>.";
|
|
1378
|
+
exactPositionals(rest, 1, usage);
|
|
1379
|
+
const ref = taskRecordReference(rest[0], "message", "Message reference", options);
|
|
1380
|
+
const message = listContextMessages(store, ref.taskId, options.environment)
|
|
1381
|
+
.find((entry) => entry.id === ref.localId);
|
|
1382
|
+
if (message === undefined)
|
|
1383
|
+
throw dataError("Message is unavailable in the caller's scope.");
|
|
1384
|
+
const continuationRun = message.continuation?.runId === undefined
|
|
1385
|
+
? null : store.getRun(ref.taskId, message.continuation.runId);
|
|
1386
|
+
const expanded = { ...expandTaskMessageResult(message, (task, run) => store.getRun(task, run)),
|
|
1387
|
+
...(continuationRun === null ? {} : { execution: runExecutionObservation(continuationRun, store.getTaskRoleSessionSet(ref.taskId, continuationRun.roleName)?.providerBinding, store.listEvents(ref.taskId)) }) };
|
|
1388
|
+
return { kind: "output", output: `${JSON.stringify(expanded, null, 2)}\n`, data: expanded };
|
|
1389
|
+
}
|
|
1182
1390
|
if (command === "send") {
|
|
1183
|
-
const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->) [--wake-policy leader|none].";
|
|
1184
|
-
const parsed = parseTail(rest, new Set(["--body-file", "--wake-policy"]), usage);
|
|
1391
|
+
const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->) [--wake-policy leader|none] [--to <role> --work-item <id>|--review-round <id>].";
|
|
1392
|
+
const parsed = parseTail(rest, new Set(["--body-file", "--wake-policy", "--to", "--work-item", "--review-round"]), usage);
|
|
1185
1393
|
if (parsed.positionals.length < 1 || parsed.positionals.length > 2)
|
|
1186
1394
|
throw usageError(usage);
|
|
1187
1395
|
const body = readCommandText(parsed.positionals[1], parsed.options.get("--body-file"), "--body", usage);
|
|
@@ -1196,42 +1404,24 @@ function taskMessageCommand(args, store, options) {
|
|
|
1196
1404
|
else {
|
|
1197
1405
|
throw usageError(`--wake-policy must be 'leader' or 'none': ${wakePolicyRaw}.`);
|
|
1198
1406
|
}
|
|
1199
|
-
const
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
// compatibility) enqueues Leader work. `wakePolicy=none` persists the
|
|
1211
|
-
// message as context without waking the Leader.
|
|
1212
|
-
if (task.status === "active"
|
|
1213
|
-
&& actor !== "leader"
|
|
1214
|
-
&& wakePolicy !== "none") {
|
|
1215
|
-
enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [messageRef(task.id, message.id)], {
|
|
1216
|
-
source: actor,
|
|
1217
|
-
dedupeKey: `message:${task.id}:${message.id}`
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
return { task, message, actor };
|
|
1221
|
-
});
|
|
1222
|
-
if (result.actor !== "leader") {
|
|
1223
|
-
notifyMailbox(options.runtime, result.task.status === "active"
|
|
1224
|
-
? leaderMailbox(result.task.id)
|
|
1225
|
-
: taskMailbox(result.task.id), result.task.id);
|
|
1226
|
-
}
|
|
1227
|
-
return `Sent message ${result.message.id} to ${result.task.id}\n`;
|
|
1407
|
+
const recipientRole = parsed.options.get("--to");
|
|
1408
|
+
const workItemId = parsed.options.get("--work-item");
|
|
1409
|
+
const reviewRoundId = parsed.options.get("--review-round");
|
|
1410
|
+
if (recipientRole === undefined && (workItemId !== undefined || reviewRoundId !== undefined))
|
|
1411
|
+
throw usageError("--to is required for scoped Message delivery.");
|
|
1412
|
+
const result = sendTaskMessageCommand(store, parsed.positionals[0], body, wakePolicy, options, recipientRole === undefined ? undefined : { roleName: recipientRole, workItemId, reviewRoundId });
|
|
1413
|
+
const reason = result.message.continuation?.notDeliveredReason;
|
|
1414
|
+
const delivery = reason !== undefined ? { state: "not-delivered", reason }
|
|
1415
|
+
: recipientRole !== undefined || result.actor !== "leader" && wakePolicy !== "none"
|
|
1416
|
+
? { state: "queued" } : { state: "saved" };
|
|
1417
|
+
return output(`Saved message ${result.message.id} to ${result.task.id} (${delivery.state}${reason === undefined ? "" : `: ${reason}`}).\n`, { taskId: result.task.id, message: result.message, delivery });
|
|
1228
1418
|
}
|
|
1229
1419
|
if (command === "list") {
|
|
1230
1420
|
const messageListUsage = "Task message list usage: yui task message list <id> [--after <timestamp>] [--limit <n>].";
|
|
1231
1421
|
const parsed = parseTail(rest, new Set(["--after", "--limit"]), messageListUsage);
|
|
1232
1422
|
exactPositionals(parsed.positionals, 1, messageListUsage);
|
|
1233
1423
|
const task = requireTask(store, parsed.positionals[0]);
|
|
1234
|
-
let messages = store
|
|
1424
|
+
let messages = listContextMessages(store, task.id, options.environment);
|
|
1235
1425
|
const after = optionalNonEmptyOption(parsed.options, "--after");
|
|
1236
1426
|
if (after !== undefined) {
|
|
1237
1427
|
const afterMs = Date.parse(after);
|
|
@@ -1279,6 +1469,100 @@ function taskMessageCommand(args, store, options) {
|
|
|
1279
1469
|
? "Task message command is required."
|
|
1280
1470
|
: `Unknown command: task message ${command}`);
|
|
1281
1471
|
}
|
|
1472
|
+
/** CLI and authenticated user Surface share the same message and mailbox
|
|
1473
|
+
* transaction. Talking to Leader does not impersonate Leader authority. */
|
|
1474
|
+
/**
|
|
1475
|
+
* The one transaction that turns an inbound Task Message into durable facts and,
|
|
1476
|
+
* when the Task's own lifecycle calls for it, Leader work.
|
|
1477
|
+
*
|
|
1478
|
+
* CLI `task message send` and the Web Task surface both call this, so neither
|
|
1479
|
+
* owns a private notion of what "sending a message" means. The Draft case is the
|
|
1480
|
+
* reason that matters here: a Draft's Leader conversation is its planning Turn,
|
|
1481
|
+
* so a Draft must wake its Leader exactly like an active Task does. Gating the
|
|
1482
|
+
* wake on `active` would leave the Draft entry point saving text that no Leader
|
|
1483
|
+
* ever reads — the message would be persisted and silently go nowhere, which is
|
|
1484
|
+
* indistinguishable to the user from a Provider that never answered.
|
|
1485
|
+
*
|
|
1486
|
+
* The wake carries intent only. Whether the resulting Turn is planning or
|
|
1487
|
+
* execution is decided by the Controller from the Task's own status, so this
|
|
1488
|
+
* function never names a purpose and no second planning path exists.
|
|
1489
|
+
*/
|
|
1490
|
+
export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options = {}, recipient) {
|
|
1491
|
+
if (!body.trim())
|
|
1492
|
+
throw usageError("Message body is required.");
|
|
1493
|
+
if (recipient !== undefined && wakePolicy !== undefined) {
|
|
1494
|
+
throw usageError("--wake-policy applies only to unaddressed Leader Messages; an owner-directed Message uses its exact continuation boundary.");
|
|
1495
|
+
}
|
|
1496
|
+
const now = clock(options);
|
|
1497
|
+
const result = store.transaction((tx) => {
|
|
1498
|
+
const task = requireTask(tx, taskId);
|
|
1499
|
+
if (recipient === undefined)
|
|
1500
|
+
assertTaskOpen(task);
|
|
1501
|
+
const caller = currentManagedRuntime(tx, options.environment, task.id);
|
|
1502
|
+
const roleCaller = caller !== undefined && caller.roleName !== "leader" ? caller : undefined;
|
|
1503
|
+
if (roleCaller !== undefined) {
|
|
1504
|
+
const run = roleCaller.currentRunId === undefined ? null : tx.getRun(task.id, roleCaller.currentRunId);
|
|
1505
|
+
if (run === null || recipient?.roleName !== "leader"
|
|
1506
|
+
|| recipient.workItemId !== run.workItemId || recipient.reviewRoundId !== run.reviewRoundId) {
|
|
1507
|
+
throw usageError("A Worker or Reviewer may send only to Leader within its current Assignment.");
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
const actor = roleCaller === undefined ? taskActor(tx, options, task.id) : "role";
|
|
1511
|
+
if (recipient !== undefined && actor === "leader")
|
|
1512
|
+
assertTaskDeliveryAuthority(tx, options.environment, task.id);
|
|
1513
|
+
const target = recipient === undefined ? undefined
|
|
1514
|
+
: recipient.roleName === "leader" && roleCaller !== undefined ? {
|
|
1515
|
+
roleName: "leader", ...(recipient.workItemId === undefined ? {} : { workItemId: recipient.workItemId }),
|
|
1516
|
+
...(recipient.reviewRoundId === undefined ? {} : { reviewRoundId: recipient.reviewRoundId })
|
|
1517
|
+
} : resolveMessageRecipient(tx, task.id, recipient.roleName, {
|
|
1518
|
+
...(recipient.workItemId === undefined ? {} : { workItemId: recipient.workItemId }),
|
|
1519
|
+
...(recipient.reviewRoundId === undefined ? {} : { reviewRoundId: recipient.reviewRoundId })
|
|
1520
|
+
});
|
|
1521
|
+
const context = {
|
|
1522
|
+
...(wakePolicy === undefined || actor === "leader" || actor === "role" ? {} : { wakePolicy }),
|
|
1523
|
+
...(target === undefined ? {} : { recipient: target }),
|
|
1524
|
+
...(recipient?.workItemId === undefined ? {} : { workItemId: recipient.workItemId })
|
|
1525
|
+
};
|
|
1526
|
+
const message = actor === "leader"
|
|
1527
|
+
? appendMessage(tx, task.id, body, "role-result", { type: "role", roleName: LEADER_ROLE }, now, context)
|
|
1528
|
+
: actor === "role"
|
|
1529
|
+
? appendMessage(tx, task.id, body, "role-result", { type: "role", roleName: roleCaller.roleName }, now, context)
|
|
1530
|
+
: actor === "operator"
|
|
1531
|
+
? appendMessage(tx, task.id, body, "operator", { type: "operator" }, now, context)
|
|
1532
|
+
: appendMessage(tx, task.id, body, "user", { type: "user" }, now, context);
|
|
1533
|
+
const queuedForLeader = target?.ownerRunId === undefined
|
|
1534
|
+
&& leaderWakingTaskStatus(task.status) && actor !== "leader" && wakePolicy !== "none";
|
|
1535
|
+
if (target?.ownerRunId !== undefined) {
|
|
1536
|
+
const reason = messageContinuationBlocker(tx, message);
|
|
1537
|
+
if (reason !== undefined) {
|
|
1538
|
+
message.continuation = { notDeliveredReason: reason };
|
|
1539
|
+
tx.updateMessage(task.id, message);
|
|
1540
|
+
}
|
|
1541
|
+
enqueueWork(tx, taskMailbox(task.id), "message-continuation", now, [messageRef(task.id, message.id)], { dedupeKey: `message:${task.id}:${message.id}` });
|
|
1542
|
+
}
|
|
1543
|
+
else if (queuedForLeader) {
|
|
1544
|
+
enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [messageRef(task.id, message.id)], { source: actor, dedupeKey: `message:${task.id}:${message.id}` });
|
|
1545
|
+
}
|
|
1546
|
+
return { task, message, actor, queuedForLeader };
|
|
1547
|
+
});
|
|
1548
|
+
if (recipient !== undefined) {
|
|
1549
|
+
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
1550
|
+
}
|
|
1551
|
+
else if (result.actor !== "leader") {
|
|
1552
|
+
notifyMailbox(options.runtime, result.queuedForLeader
|
|
1553
|
+
? leaderMailbox(result.task.id) : taskMailbox(result.task.id), result.task.id);
|
|
1554
|
+
}
|
|
1555
|
+
return result;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Whether an inbound Message on a Task in this status is delivered to its
|
|
1559
|
+
* Leader. Draft qualifies because planning is a Leader conversation that
|
|
1560
|
+
* deliberately precedes any delivery environment or Activation (S01); a
|
|
1561
|
+
* terminal Task has no Leader lane and keeps the message as context only.
|
|
1562
|
+
*/
|
|
1563
|
+
export function leaderWakingTaskStatus(status) {
|
|
1564
|
+
return status === "active" || status === "draft";
|
|
1565
|
+
}
|
|
1282
1566
|
function updateMessage(args, store, options) {
|
|
1283
1567
|
const usage = "Task message update usage: yui task message update <task>/<message> (<body>|--body-file <path|->) [--wake-policy leader|none].";
|
|
1284
1568
|
const parsed = parseTail(args, new Set(["--body-file", "--wake-policy"]), usage);
|
|
@@ -1322,9 +1606,14 @@ function updateMessage(args, store, options) {
|
|
|
1322
1606
|
updatedBy: actor,
|
|
1323
1607
|
...(wakePolicy === undefined ? {} : { wakePolicy })
|
|
1324
1608
|
}, now);
|
|
1325
|
-
|
|
1609
|
+
const queuedForLeader = updated.wakePolicy !== "none";
|
|
1610
|
+
if (queuedForLeader) {
|
|
1611
|
+
enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [messageRef(task.id, updated.id)], { source: actor });
|
|
1612
|
+
}
|
|
1613
|
+
return { task, message: updated, queuedForLeader };
|
|
1326
1614
|
});
|
|
1327
|
-
options.runtime
|
|
1615
|
+
notifyMailbox(options.runtime, result.queuedForLeader
|
|
1616
|
+
? leaderMailbox(result.task.id) : taskMailbox(result.task.id), result.task.id);
|
|
1328
1617
|
return output(`Updated Task Message ${result.task.id}/${result.message.id}\n`, {
|
|
1329
1618
|
message: result.message
|
|
1330
1619
|
});
|
|
@@ -1353,7 +1642,7 @@ function retireMessage(args, store, options) {
|
|
|
1353
1642
|
// Remove an isolated pending wake for this exact directive. A merged batch
|
|
1354
1643
|
// is retained because its other signals remain actionable; context and
|
|
1355
1644
|
// actionability projections still filter the retired message below.
|
|
1356
|
-
if (task.status
|
|
1645
|
+
if (leaderWakingTaskStatus(task.status)) {
|
|
1357
1646
|
try {
|
|
1358
1647
|
settleExactWorkExecution(tx, leaderMailbox(task.id), messageRef(task.id, message.id));
|
|
1359
1648
|
}
|
|
@@ -1408,9 +1697,9 @@ function taskRoleCommand(args, store, options) {
|
|
|
1408
1697
|
if (command === "status")
|
|
1409
1698
|
return taskRoleStatus(rest, store, options);
|
|
1410
1699
|
if (command === "show")
|
|
1411
|
-
return
|
|
1700
|
+
return showTaskRole(rest, store);
|
|
1412
1701
|
if (command === "update")
|
|
1413
|
-
return
|
|
1702
|
+
return updateTaskRole(rest, store, options);
|
|
1414
1703
|
if (command === "remove")
|
|
1415
1704
|
return output(removeTaskRole(rest, store, options));
|
|
1416
1705
|
if (command === "bind")
|
|
@@ -1435,20 +1724,76 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1435
1724
|
exactPositionals(rest, 2, "Task Role Session inspect usage: yui task role session inspect <task> <role>.");
|
|
1436
1725
|
const task = requireTask(store, rest[0]);
|
|
1437
1726
|
const role = requireRole(store, task.id, rest[1]);
|
|
1438
|
-
|
|
1727
|
+
const reader = resolveManagedTaskReader(store, options.environment);
|
|
1728
|
+
if (reader === undefined)
|
|
1729
|
+
taskActor(store, options, task.id);
|
|
1730
|
+
else if (reader.taskId !== task.id || (reader.roleName !== "leader" && reader.roleName !== role.name)) {
|
|
1731
|
+
throw usageError("Session inspection is outside the caller's Task/Role.");
|
|
1732
|
+
}
|
|
1439
1733
|
const sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
1440
1734
|
const active = sessions?.sessions[sessions.activeAgentId] ?? null;
|
|
1441
1735
|
const binding = sessions?.providerBinding ?? null;
|
|
1736
|
+
// The live reading, when the CLI took one. Rendered under the persisted
|
|
1737
|
+
// facts rather than merged into them: the Session's own record is what Yui
|
|
1738
|
+
// launched, and this is what the Agent says about it now. An empty render
|
|
1739
|
+
// means there was nothing an Agent reported, and the one-line label above
|
|
1740
|
+
// still states which of the "no value" cases applies.
|
|
1741
|
+
const runConfiguration = active === null ? undefined : options.liveRunConfiguration;
|
|
1742
|
+
const runConfigurationDetail = renderAgentRunConfiguration(runConfiguration);
|
|
1442
1743
|
return output(active === null
|
|
1443
|
-
? `No Session exists for ${task.id}/${role.name}.\n`
|
|
1744
|
+
? `No attached Session exists for ${task.id}/${role.name}.\n`
|
|
1745
|
+
+ (binding === null ? "" : `Retained native execution: ${currentProviderConversation(binding).conversationId}; input=${binding.run?.status ?? "none"}. Use session new to replace from durable context.\n`)
|
|
1444
1746
|
: [
|
|
1445
1747
|
`Session ${task.id}/${role.name}`,
|
|
1446
|
-
|
|
1748
|
+
// The component, not just the connection plan: several products are
|
|
1749
|
+
// reached over `acp`, and the plan alone cannot say which one ran.
|
|
1750
|
+
`Agent: ${active.agentId}/${agentExecutionComponentLabel(active.effective.component)}`
|
|
1751
|
+
+ ` (${active.adapterId} plan)`,
|
|
1447
1752
|
`Native id: ${active.nativeSessionId}`,
|
|
1448
|
-
`Host activation: ${active.runtimeGenerationId ?? "none"}`,
|
|
1449
1753
|
`Session: ${active.status}${active.endReason === undefined ? "" : `/${active.endReason}`}`,
|
|
1450
|
-
`
|
|
1451
|
-
|
|
1754
|
+
`AgentRun: ${binding?.run?.status ?? "none"}`,
|
|
1755
|
+
`Run configuration: ${agentRunConfigurationLabel(runConfiguration)}`
|
|
1756
|
+
].join("\n") + "\n"
|
|
1757
|
+
+ `\n${renderRoleLaunchComparison(role, active.effective)}\n`
|
|
1758
|
+
+ (runConfigurationDetail === "" ? "" : `\n${runConfigurationDetail}\n`), {
|
|
1759
|
+
task,
|
|
1760
|
+
role,
|
|
1761
|
+
session: active,
|
|
1762
|
+
providerBinding: binding,
|
|
1763
|
+
...(runConfiguration === undefined ? {} : { runConfiguration })
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
if (command === "new") {
|
|
1767
|
+
const usage = "Task Role Session new usage: yui task role session new <task> <role> --reason <text>.";
|
|
1768
|
+
const parsed = parseTail(rest, new Set(["--reason"]), usage);
|
|
1769
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
1770
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
1771
|
+
const now = clock(options);
|
|
1772
|
+
const result = store.transaction((tx) => {
|
|
1773
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
1774
|
+
if (task.status === "archived")
|
|
1775
|
+
throw usageError("An archived Task cannot select a new Session.", usage);
|
|
1776
|
+
const actor = taskActor(tx, options, task.id);
|
|
1777
|
+
const role = requireRole(tx, task.id, parsed.positionals[1]);
|
|
1778
|
+
const current = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1779
|
+
const previous = current?.sessions[current.activeAgentId];
|
|
1780
|
+
const target = runtimeLifecycleTarget({ scope: "task", taskId: task.id, roleName: role.name });
|
|
1781
|
+
const mailbox = tx.getWorkMailbox(target);
|
|
1782
|
+
if ([...(mailbox?.pending?.reasons ?? []), ...(mailbox?.processing?.batch.reasons ?? [])]
|
|
1783
|
+
.includes(RUNTIME_SESSION_REPLACE_REQUIRED_REASON))
|
|
1784
|
+
return { taskId: task.id, roleName: role.name, target, alreadyRequested: true };
|
|
1785
|
+
recordTaskEvent(tx, task.id, "runtime.session-replacement-requested", {
|
|
1786
|
+
roleName: role.name, agentId: current?.activeAgentId ?? role.activeAgentId,
|
|
1787
|
+
...(previous === undefined ? {} : { nativeSessionId: previous.nativeSessionId }),
|
|
1788
|
+
reason, requestedBy: actor
|
|
1789
|
+
}, now);
|
|
1790
|
+
enqueueWork(tx, target, RUNTIME_SESSION_REPLACE_REQUIRED_REASON, now, [{ type: "task", id: task.id }]);
|
|
1791
|
+
return { taskId: task.id, roleName: role.name, target, alreadyRequested: false };
|
|
1792
|
+
});
|
|
1793
|
+
notifyMailbox(options.runtime, result.target, result.taskId);
|
|
1794
|
+
return output(`Requested Session replacement for ${result.taskId}/${result.roleName}. `
|
|
1795
|
+
+ "Yui will stop the old execution, retain its history and workspaces, and select a fresh Session. "
|
|
1796
|
+
+ "If replacing your own Session, end this turn; the successor reads durable Task context.\n", result);
|
|
1452
1797
|
}
|
|
1453
1798
|
if (command === "stop") {
|
|
1454
1799
|
const usage = "Task Role Session stop usage: yui task role session stop <task> <role> --reason <text>.";
|
|
@@ -1458,36 +1803,26 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1458
1803
|
const now = clock(options);
|
|
1459
1804
|
const request = store.transaction((tx) => {
|
|
1460
1805
|
const task = requireTask(tx, parsed.positionals[0]);
|
|
1461
|
-
if (
|
|
1462
|
-
throw usageError(`Task Role Session stop
|
|
1806
|
+
if (!["draft", "active", "completed", "cancelled"].includes(task.status)) {
|
|
1807
|
+
throw usageError(`Task Role Session stop is unavailable for an archived Task: ${task.id}.`, usage);
|
|
1463
1808
|
}
|
|
1464
1809
|
const actor = taskActor(tx, options, task.id);
|
|
1465
1810
|
const role = requireRole(tx, task.id, parsed.positionals[1]);
|
|
1466
1811
|
if (actor === "leader" && role.name === LEADER_ROLE) {
|
|
1467
|
-
throw usageError("
|
|
1468
|
-
}
|
|
1469
|
-
if (tx.getActiveTurn(task.id, role.name) !== null) {
|
|
1470
|
-
throw usageError(`Task Role has an active Turn; settle or retire it before stopping the Session: ${task.id}/${role.name}.`, usage);
|
|
1812
|
+
throw usageError("Use task role session new to request your own replacement and end this turn; synchronous self-stop cannot return.", usage);
|
|
1471
1813
|
}
|
|
1472
1814
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1473
|
-
const session = sessions
|
|
1474
|
-
if (session === undefined || session.
|
|
1815
|
+
const session = taskRoleControlTarget(sessions);
|
|
1816
|
+
if (session === undefined || session.adapterId === undefined) {
|
|
1475
1817
|
throw usageError(`Task Role has no active Session: ${task.id}/${role.name}.`, usage);
|
|
1476
1818
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
taskId: task.id,
|
|
1480
|
-
roleName: role.name
|
|
1481
|
-
}));
|
|
1482
|
-
if (lifecycle !== null && workMailboxHasWork(lifecycle)) {
|
|
1483
|
-
throw usageError(`Task Role Session lifecycle is busy: ${task.id}/${role.name}.`, usage);
|
|
1484
|
-
}
|
|
1819
|
+
// A repeated stop is a recovery request, not contention with its own
|
|
1820
|
+
// earlier cleanup obligation.
|
|
1485
1821
|
recordTaskEvent(tx, task.id, "runtime.session-stop-requested", {
|
|
1486
1822
|
roleName: role.name,
|
|
1487
1823
|
agentId: session.agentId,
|
|
1488
1824
|
adapterId: session.adapterId,
|
|
1489
1825
|
nativeSessionId: session.nativeSessionId,
|
|
1490
|
-
runtimeGenerationId: session.runtimeGenerationId ?? "",
|
|
1491
1826
|
reason,
|
|
1492
1827
|
requestedBy: actor
|
|
1493
1828
|
}, now);
|
|
@@ -1497,7 +1832,6 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1497
1832
|
agentId: session.agentId,
|
|
1498
1833
|
adapterId: session.adapterId,
|
|
1499
1834
|
nativeSessionId: session.nativeSessionId,
|
|
1500
|
-
...(session.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: session.runtimeGenerationId }),
|
|
1501
1835
|
sessionUpdatedAt: session.updatedAt
|
|
1502
1836
|
};
|
|
1503
1837
|
});
|
|
@@ -1615,7 +1949,7 @@ function listTaskRoles(args, store, options) {
|
|
|
1615
1949
|
status.health,
|
|
1616
1950
|
taskRoleOpenInputLabel(status),
|
|
1617
1951
|
taskRoleActiveWorkLabel(status),
|
|
1618
|
-
|
|
1952
|
+
taskRoleLastRunLabel(status),
|
|
1619
1953
|
taskRoleNativeSessionLabel(status),
|
|
1620
1954
|
taskRoleTmuxLabel(status)
|
|
1621
1955
|
]), defaultTableWidth())}\n`, { roles: statuses });
|
|
@@ -1633,9 +1967,15 @@ function showTaskRole(args, store) {
|
|
|
1633
1967
|
exactPositionals(args, 2, "Task role show usage: yui task role show <task> <role>.");
|
|
1634
1968
|
const task = requireTask(store, args[0]);
|
|
1635
1969
|
const role = requireRole(store, task.id, args[1]);
|
|
1636
|
-
|
|
1970
|
+
const sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
1971
|
+
return output(renderRoleDetails(`Task Role: ${role.name}`, role, {
|
|
1637
1972
|
kind: "task",
|
|
1638
|
-
sessions
|
|
1973
|
+
sessions
|
|
1974
|
+
}), {
|
|
1975
|
+
role,
|
|
1976
|
+
sessions,
|
|
1977
|
+
runs: store.listRuns(task.id).filter((run) => run.roleName === role.name)
|
|
1978
|
+
.map((run) => ({ id: run.id, status: run.status, effective: run.effective }))
|
|
1639
1979
|
});
|
|
1640
1980
|
}
|
|
1641
1981
|
function updateTaskRole(args, store, options) {
|
|
@@ -1646,8 +1986,11 @@ function updateTaskRole(args, store, options) {
|
|
|
1646
1986
|
}
|
|
1647
1987
|
const parsed = parseRoleOptions(tail, new Map([
|
|
1648
1988
|
...roleOptionSpecs({ update: true, includeAgent: true }),
|
|
1649
|
-
["--profile", "value"]
|
|
1989
|
+
["--profile", "value"],
|
|
1990
|
+
["--environment", "value"],
|
|
1991
|
+
["--managed-environment", "flag"]
|
|
1650
1992
|
]), usage);
|
|
1993
|
+
validateTaskRoleEnvironmentOptions(parsed, usage);
|
|
1651
1994
|
if (parsed.has("--agent") && (parsed.one("--agent")?.trim().length ?? 0) === 0) {
|
|
1652
1995
|
throw usageError("--agent is required.", usage);
|
|
1653
1996
|
}
|
|
@@ -1660,7 +2003,9 @@ function updateTaskRole(args, store, options) {
|
|
|
1660
2003
|
assertTaskOpen(task);
|
|
1661
2004
|
taskActor(tx, options, task.id);
|
|
1662
2005
|
const role = requireRole(tx, task.id, roleName);
|
|
1663
|
-
const
|
|
2006
|
+
const changesEnvironment = parsed.has("--environment") || parsed.has("--managed-environment");
|
|
2007
|
+
const changesLaunchContext = hasRoleLaunchContextOptions(parsed) || parsed.has("--profile")
|
|
2008
|
+
|| changesEnvironment;
|
|
1664
2009
|
const changesAgentConfig = hasAgentConfigOptions(parsed);
|
|
1665
2010
|
if (changesLaunchContext || changesAgentConfig) {
|
|
1666
2011
|
assertRoleRuntimeMutationAllowed(tx, {
|
|
@@ -1668,13 +2013,6 @@ function updateTaskRole(args, store, options) {
|
|
|
1668
2013
|
taskId: task.id,
|
|
1669
2014
|
roleName: role.name
|
|
1670
2015
|
}, "desired launch configuration update");
|
|
1671
|
-
assertLiveRoleSessionAcknowledged({
|
|
1672
|
-
sessions: tx.getTaskRoleSessionSet(task.id, role.name),
|
|
1673
|
-
roleName: role.name,
|
|
1674
|
-
desiredRevision: role.launchRevision,
|
|
1675
|
-
acknowledged: parsed.has(LIVE_SESSION_ACKNOWLEDGEMENT_OPTION),
|
|
1676
|
-
stopCommand: `yui task role session stop ${task.id} ${role.name} --reason "<decision>"`
|
|
1677
|
-
});
|
|
1678
2016
|
}
|
|
1679
2017
|
const profileId = parsed.one("--profile");
|
|
1680
2018
|
const agentProfile = profileId === undefined
|
|
@@ -1692,7 +2030,7 @@ function updateTaskRole(args, store, options) {
|
|
|
1692
2030
|
[bindingUpdate.agentId]: bindingUpdate.binding
|
|
1693
2031
|
}
|
|
1694
2032
|
}, now);
|
|
1695
|
-
|
|
2033
|
+
let next = updateRole(withBinding, {
|
|
1696
2034
|
...roleProfilePatch(parsed)
|
|
1697
2035
|
}, now);
|
|
1698
2036
|
if (bindingUpdate !== undefined) {
|
|
@@ -1701,16 +2039,32 @@ function updateTaskRole(args, store, options) {
|
|
|
1701
2039
|
if (changesLaunchContext) {
|
|
1702
2040
|
validateConfiguredRoleSkills(options.yuiHome, next.skills ?? []);
|
|
1703
2041
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2042
|
+
if (changesEnvironment) {
|
|
2043
|
+
next = updateRole(next, {
|
|
2044
|
+
executionEnvironment: parsed.has("--managed-environment")
|
|
2045
|
+
? null
|
|
2046
|
+
: createProjectResources(tx, () => now).resolveExecutionEnvironment(task.id, parsed.one("--environment"))
|
|
2047
|
+
}, now);
|
|
2048
|
+
}
|
|
2049
|
+
saveTaskRoleUpdate(tx, role, next, now, {
|
|
2050
|
+
source: "task.role.update", actor: taskActor(tx, options, task.id)
|
|
2051
|
+
});
|
|
1707
2052
|
return next;
|
|
1708
2053
|
});
|
|
1709
2054
|
notifyMailbox(options.runtime, taskMailbox(updated.taskId), updated.taskId);
|
|
1710
|
-
|
|
2055
|
+
const sessions = store.getTaskRoleSessionSet(updated.taskId, updated.name);
|
|
2056
|
+
return output(renderRoleDetails(`Updated Task Role: ${updated.name}`, updated, {
|
|
1711
2057
|
kind: "task",
|
|
1712
|
-
sessions
|
|
1713
|
-
});
|
|
2058
|
+
sessions
|
|
2059
|
+
}), { role: updated, sessions });
|
|
2060
|
+
}
|
|
2061
|
+
function validateTaskRoleEnvironmentOptions(parsed, usage) {
|
|
2062
|
+
if (parsed.has("--environment") && parsed.has("--managed-environment")) {
|
|
2063
|
+
throw usageError("--environment and --managed-environment are mutually exclusive.", usage);
|
|
2064
|
+
}
|
|
2065
|
+
if (parsed.has("--environment") && (parsed.one("--environment")?.trim().length ?? 0) === 0) {
|
|
2066
|
+
throw usageError("--environment requires an adopted preparation id.", usage);
|
|
2067
|
+
}
|
|
1714
2068
|
}
|
|
1715
2069
|
function removeTaskRole(args, store, options) {
|
|
1716
2070
|
exactPositionals(args, 2, "Task role remove usage: yui task role remove <task> <role>.");
|
|
@@ -1727,8 +2081,8 @@ function removeTaskRole(args, store, options) {
|
|
|
1727
2081
|
taskId: task.id,
|
|
1728
2082
|
roleName: role.name
|
|
1729
2083
|
}, "removal");
|
|
1730
|
-
if (tx.
|
|
1731
|
-
throw usageError(`Task Role has an active
|
|
2084
|
+
if (tx.getActiveRun(task.id, role.name) !== null) {
|
|
2085
|
+
throw usageError(`Task Role has an active AgentRun and cannot be removed: ${task.id}/${role.name}.`);
|
|
1732
2086
|
}
|
|
1733
2087
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1734
2088
|
if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status === "active")) {
|
|
@@ -1757,7 +2111,7 @@ function bindTaskRole(args, store, options) {
|
|
|
1757
2111
|
}, "desired Agent binding update");
|
|
1758
2112
|
const agent = requireAgent(tx, args[2]);
|
|
1759
2113
|
const binding = role.agentBindings[agent.id]
|
|
1760
|
-
?? createRoleAgentBinding(
|
|
2114
|
+
?? createRoleAgentBinding(agent);
|
|
1761
2115
|
const bound = updateRole(role, {
|
|
1762
2116
|
agentBindings: { ...role.agentBindings, [agent.id]: binding }
|
|
1763
2117
|
}, now);
|
|
@@ -1777,7 +2131,7 @@ function bindTaskRole(args, store, options) {
|
|
|
1777
2131
|
const switched = (() => {
|
|
1778
2132
|
try {
|
|
1779
2133
|
return switchActiveRoleAgent(bound, existing, agent.id, {
|
|
1780
|
-
|
|
2134
|
+
activeRun: tx.getActiveRun(task.id, role.name) !== null,
|
|
1781
2135
|
nativeProcessRunning: currentSession !== undefined
|
|
1782
2136
|
&& currentSession.status === "active"
|
|
1783
2137
|
}, now);
|
|
@@ -1790,6 +2144,10 @@ function bindTaskRole(args, store, options) {
|
|
|
1790
2144
|
recordTaskEvent(tx, task.id, "role.agent-bound", {
|
|
1791
2145
|
role: switched.role.name,
|
|
1792
2146
|
agentId: agent.id,
|
|
2147
|
+
// Re-selecting an Agent must not revive its old management entrance.
|
|
2148
|
+
// This revokes authority, not the Session's independent execution fact.
|
|
2149
|
+
...(role.name === LEADER_ROLE && currentSession?.nativeSessionId !== undefined
|
|
2150
|
+
? { revokedNativeSessionId: currentSession.nativeSessionId } : {}),
|
|
1793
2151
|
...roleLaunchEventPayload(switched.role, switched.sessions)
|
|
1794
2152
|
}, now);
|
|
1795
2153
|
return { role: switched.role, mode: switched.mode };
|
|
@@ -1859,20 +2217,13 @@ function transferTaskRoleAuthority(args, store, options, action) {
|
|
|
1859
2217
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1860
2218
|
const session = sessions?.sessions[role.activeAgentId];
|
|
1861
2219
|
const binding = sessions?.providerBinding;
|
|
1862
|
-
if (sessions === null || sessions === undefined || session === undefined
|
|
1863
|
-
|| binding === null || binding === undefined
|
|
1864
|
-
|| session.runtimeGenerationId === undefined
|
|
1865
|
-
|| session.status === "ended") {
|
|
2220
|
+
if (sessions === null || sessions === undefined || session === undefined || binding === null || binding === undefined || session.status === "ended") {
|
|
1866
2221
|
throw new Error(`Task Role has no live managed Provider: ${task.id}/${role.name}.`);
|
|
1867
2222
|
}
|
|
1868
|
-
const activation = currentProviderActivation(binding);
|
|
1869
|
-
if (activation === null) {
|
|
1870
|
-
throw new Error(`Provider Activation is not live: ${task.id}/${role.name}.`);
|
|
1871
|
-
}
|
|
1872
2223
|
if (action === "takeover") {
|
|
1873
|
-
const
|
|
1874
|
-
if (
|
|
1875
|
-
throw new Error(`Task Role has no active managed
|
|
2224
|
+
const activeRun = tx.getActiveRun(task.id, role.name);
|
|
2225
|
+
if (activeRun === null) {
|
|
2226
|
+
throw new Error(`Task Role has no active managed AgentRun for takeover: ${task.id}/${role.name}.`);
|
|
1876
2227
|
}
|
|
1877
2228
|
}
|
|
1878
2229
|
if (action === "takeover"
|
|
@@ -1893,7 +2244,7 @@ function transferTaskRoleAuthority(args, store, options, action) {
|
|
|
1893
2244
|
expectedEpoch: binding.authority.epoch,
|
|
1894
2245
|
expectedOwner: binding.authority.owner,
|
|
1895
2246
|
owner: desiredOwner,
|
|
1896
|
-
holderId: action === "takeover" ? `human:${randomUUID()}` :
|
|
2247
|
+
holderId: action === "takeover" ? `human:${randomUUID()}` : "controller",
|
|
1897
2248
|
changedAt: now.toISOString()
|
|
1898
2249
|
});
|
|
1899
2250
|
const authority = updatedBinding.authority;
|
|
@@ -1914,7 +2265,6 @@ function transferTaskRoleAuthority(args, store, options, action) {
|
|
|
1914
2265
|
action,
|
|
1915
2266
|
taskId: task.id,
|
|
1916
2267
|
roleName: role.name,
|
|
1917
|
-
runtimeGenerationId: session.runtimeGenerationId,
|
|
1918
2268
|
nativeSessionId: session.nativeSessionId,
|
|
1919
2269
|
authority: {
|
|
1920
2270
|
epoch: authority.epoch,
|
|
@@ -1947,6 +2297,8 @@ function taskWorkCommand(args, store, options) {
|
|
|
1947
2297
|
return output(updateWorkScope(rest, store, options));
|
|
1948
2298
|
if (command === "dispatch")
|
|
1949
2299
|
return output(dispatchWork(rest, store, options));
|
|
2300
|
+
if (command === "synthesize")
|
|
2301
|
+
return synthesizeRuns(rest, "workItem", store, options);
|
|
1950
2302
|
if (command === "review") {
|
|
1951
2303
|
return rest[0] === "retry"
|
|
1952
2304
|
? retryFailedTaskReviewRound(rest.slice(1), store, options)
|
|
@@ -1986,14 +2338,10 @@ function editWork(args, store, options) {
|
|
|
1986
2338
|
const result = store.transaction((tx) => {
|
|
1987
2339
|
const item = requireWorkItem(tx, parsed.positionals[0], options);
|
|
1988
2340
|
const task = requireTask(tx, item.taskId);
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
assertDraftTaskExecutionFree(tx, task);
|
|
2341
|
+
assertTaskOpen(task);
|
|
2342
|
+
if (task.status === "draft")
|
|
2343
|
+
assertDraftTaskExecutionFree(tx, task);
|
|
1993
2344
|
const actor = taskActor(tx, options, task.id);
|
|
1994
|
-
if (actor !== "user" && actor !== "operator") {
|
|
1995
|
-
throw usageError("Only the user or Operator may edit a Draft Work Item.");
|
|
1996
|
-
}
|
|
1997
2345
|
if (item.status === "retired") {
|
|
1998
2346
|
throw usageError(`Work Item is retired: ${item.id}.`);
|
|
1999
2347
|
}
|
|
@@ -2014,7 +2362,7 @@ function editWork(args, store, options) {
|
|
|
2014
2362
|
: parsed.options.has("--clear-role") ? null : undefined;
|
|
2015
2363
|
if (typeof assignee === "string")
|
|
2016
2364
|
requireRole(tx, task.id, assignee);
|
|
2017
|
-
const updated =
|
|
2365
|
+
const updated = editWorkItemDefinition(item, {
|
|
2018
2366
|
...(parsed.options.has("--title")
|
|
2019
2367
|
? { title: requiredOption(parsed.options, "--title") }
|
|
2020
2368
|
: {}),
|
|
@@ -2031,7 +2379,21 @@ function editWork(args, store, options) {
|
|
|
2031
2379
|
...(baseRefs === undefined ? {} : { baseRefs }),
|
|
2032
2380
|
...(assignee === undefined ? {} : { assignee })
|
|
2033
2381
|
}, now);
|
|
2034
|
-
|
|
2382
|
+
if (task.status === "draft")
|
|
2383
|
+
validateDraftWorkItemEdit(tx, task, updated);
|
|
2384
|
+
else {
|
|
2385
|
+
// Requirements can evolve while a frozen Assignment continues. Resource
|
|
2386
|
+
// scope and ownership changes still require an explicit idle boundary.
|
|
2387
|
+
if ((projects !== undefined || baseRefs !== undefined || assignee !== undefined)
|
|
2388
|
+
&& tx.listRuns(task.id).some((run) => run.workItemId === item.id && run.status === "active")) {
|
|
2389
|
+
throw usageError(`Stop the active Work Item AgentRun before changing ownership or workspace scope: ${item.id}.`);
|
|
2390
|
+
}
|
|
2391
|
+
for (const dependencyId of updated.dependsOn) {
|
|
2392
|
+
if (tx.getWorkItem(task.id, dependencyId) === null) {
|
|
2393
|
+
throw usageError(`Work Item dependency not found: ${dependencyId}.`);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2035
2397
|
tx.saveWorkItem(task.id, updated);
|
|
2036
2398
|
const changedFields = [
|
|
2037
2399
|
parsed.options.has("--title") ? "title" : undefined,
|
|
@@ -2051,8 +2413,13 @@ function editWork(args, store, options) {
|
|
|
2051
2413
|
workItemId: updated.id,
|
|
2052
2414
|
revision: String(updated.revision),
|
|
2053
2415
|
fields: changedFields.join(","),
|
|
2416
|
+
previous: editedFieldValues(item, changedFields),
|
|
2417
|
+
current: editedFieldValues(updated, changedFields),
|
|
2054
2418
|
editedBy: actor
|
|
2055
2419
|
}, now);
|
|
2420
|
+
if (actor !== "leader") {
|
|
2421
|
+
enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [workItemRef(task.id, updated.id)], { source: actor });
|
|
2422
|
+
}
|
|
2056
2423
|
return { task, item: updated };
|
|
2057
2424
|
});
|
|
2058
2425
|
options.runtime?.notifyStateChanged(result.task.id);
|
|
@@ -2129,8 +2496,8 @@ function updateWorkScope(args, store, options) {
|
|
|
2129
2496
|
const item = requireWorkItem(tx, parsed.positionals[0], options);
|
|
2130
2497
|
const task = requireTask(tx, item.taskId);
|
|
2131
2498
|
taskActor(tx, options, task.id);
|
|
2132
|
-
if (tx.
|
|
2133
|
-
throw usageError(`Stop the active Work Item
|
|
2499
|
+
if (tx.getActiveRun(task.id, item.assignee ?? "") !== null) {
|
|
2500
|
+
throw usageError(`Stop the active Work Item AgentRun before changing scope: ${item.id}.`);
|
|
2134
2501
|
}
|
|
2135
2502
|
const requestedProjectIds = (parsed.multiOptions.get("--project") ?? []).map((reference) => {
|
|
2136
2503
|
const project = resolveProject(task.projectBindings.map(({ projectId }) => requireProject(tx, projectId)), reference);
|
|
@@ -2160,12 +2527,16 @@ function updateWorkScope(args, store, options) {
|
|
|
2160
2527
|
return `${updated.changed ? "Updated" : "Unchanged"} Work Item Project scope ${updated.item.id}: ${updated.item.writeProjectIds.join(", ") || "read-only"}\n`;
|
|
2161
2528
|
}
|
|
2162
2529
|
function updateWork(args, store, options) {
|
|
2163
|
-
const usage = "Task work update usage: yui task work update <task>/<work> <todo|running|done|failed> [--summary <text>].";
|
|
2164
|
-
const parsed =
|
|
2530
|
+
const usage = "Task work update usage: yui task work update <task>/<work> <todo|running|done|failed> [--summary <text>] [--artifact-ref <artifact-id> ...].";
|
|
2531
|
+
const parsed = parseMultiValueTail(args, new Set(["--summary"]), new Set(["--artifact-ref"]), usage);
|
|
2165
2532
|
exactPositionals(parsed.positionals, 2, usage);
|
|
2166
2533
|
const requested = parsed.positionals[1];
|
|
2167
2534
|
const status = parseWorkStatus(requested);
|
|
2168
2535
|
const summary = trimmed(parsed.options.get("--summary"));
|
|
2536
|
+
const artifactIds = parsed.multiOptions.get("--artifact-ref") ?? [];
|
|
2537
|
+
if (artifactIds.length > 0 && status !== "completed") {
|
|
2538
|
+
throw usageError("--artifact-ref is only valid when submitting a done Candidate.");
|
|
2539
|
+
}
|
|
2169
2540
|
if (["completed", "failed"].includes(status)
|
|
2170
2541
|
&& summary === undefined) {
|
|
2171
2542
|
throw usageError(`--summary is required when work becomes ${requested}.`);
|
|
@@ -2175,19 +2546,16 @@ function updateWork(args, store, options) {
|
|
|
2175
2546
|
const current = requireWorkItem(tx, parsed.positionals[0], options);
|
|
2176
2547
|
const task = requireTask(tx, current.taskId);
|
|
2177
2548
|
assertTaskOpen(task);
|
|
2549
|
+
const artifactRefs = artifactIds.length === 0 ? undefined : fixedArtifactRefs(tx, task.id, artifactIds);
|
|
2178
2550
|
if (current.assignee === undefined) {
|
|
2179
2551
|
taskActor(tx, options, task.id);
|
|
2180
2552
|
if (status === "running") {
|
|
2181
2553
|
assertWorkItemDependenciesCompletedForCommand(tx, current);
|
|
2182
2554
|
}
|
|
2183
|
-
if (status === "completed" && current.status === "awaiting_acceptance") {
|
|
2184
|
-
throw usageError(`Work Item ${current.id} is awaiting acceptance; use task work accept `
|
|
2185
|
-
+ "after the required ReviewRound and Integration evidence.");
|
|
2186
|
-
}
|
|
2187
2555
|
const configuredReview = status === "completed" && !isTerminalWorkItemStatus(current.status)
|
|
2188
2556
|
? tx.getReviewConfig()
|
|
2189
2557
|
: null;
|
|
2190
|
-
const taskFinalContract = status === "completed" && current.status === "
|
|
2558
|
+
const taskFinalContract = status === "completed" && current.status === "open"
|
|
2191
2559
|
? taskFinalReviewContractForMutation(tx, task.id, options)
|
|
2192
2560
|
: undefined;
|
|
2193
2561
|
const candidatePolicy = taskFinalContract === undefined
|
|
@@ -2197,9 +2565,7 @@ function updateWork(args, store, options) {
|
|
|
2197
2565
|
&& current.writeProjectIds.length > 0;
|
|
2198
2566
|
const metadataOnlyTaskFinalDelivery = taskFinalContract !== undefined
|
|
2199
2567
|
&& current.assignee === undefined;
|
|
2200
|
-
const candidateRequired = status === "completed"
|
|
2201
|
-
&& current.status === "running"
|
|
2202
|
-
&& (projectDelivery || candidatePolicy !== null);
|
|
2568
|
+
const candidateRequired = status === "completed" && current.status === "open";
|
|
2203
2569
|
const developWorkspace = tx.getWorkItemWorkspace(task.id, current.id);
|
|
2204
2570
|
if (status === "completed"
|
|
2205
2571
|
&& projectDelivery
|
|
@@ -2211,6 +2577,7 @@ function updateWork(args, store, options) {
|
|
|
2211
2577
|
? submitWorkItemCandidate(current, {
|
|
2212
2578
|
summary: summary,
|
|
2213
2579
|
source: { type: "direct" },
|
|
2580
|
+
...(artifactRefs === undefined ? {} : { artifactRefs }),
|
|
2214
2581
|
...(candidatePolicy === null ? {} : { reviewPolicy: candidatePolicy }),
|
|
2215
2582
|
...(taskFinalContract === undefined
|
|
2216
2583
|
? {}
|
|
@@ -2225,9 +2592,7 @@ function updateWork(args, store, options) {
|
|
|
2225
2592
|
? {}
|
|
2226
2593
|
: { taskMainSnapshot: options.directTaskMainSnapshot })
|
|
2227
2594
|
}, now)
|
|
2228
|
-
: current
|
|
2229
|
-
? retryFailedWorkItem(current, now)
|
|
2230
|
-
: updateWorkItemStatus(current, status, now, isTerminalWorkItemStatus(status) ? summary : undefined);
|
|
2595
|
+
: updateWorkItemStatus(current, "open", now);
|
|
2231
2596
|
tx.saveWorkItem(task.id, updated);
|
|
2232
2597
|
if (summary !== undefined) {
|
|
2233
2598
|
recordTaskEvent(tx, task.id, "work.updated", {
|
|
@@ -2250,29 +2615,29 @@ function updateWork(args, store, options) {
|
|
|
2250
2615
|
};
|
|
2251
2616
|
}
|
|
2252
2617
|
taskActor(tx, options, task.id);
|
|
2253
|
-
if (status !== "completed" || current.status !== "
|
|
2254
|
-
throw usageError(`Assigned Work Item ${current.id} can only submit a completed direct
|
|
2255
|
-
+ "use dispatch, task
|
|
2618
|
+
if (status !== "completed" || current.status !== "open") {
|
|
2619
|
+
throw usageError(`Assigned Work Item ${current.id} can only submit a completed direct AgentRun from running; `
|
|
2620
|
+
+ "use dispatch, task run retry, or task work retire for other transitions.");
|
|
2256
2621
|
}
|
|
2257
|
-
if (tx.
|
|
2258
|
-
throw usageError(`Work Item main
|
|
2622
|
+
if (tx.getActiveRun(task.id, current.assignee) !== null) {
|
|
2623
|
+
throw usageError(`Work Item main AgentRun is still active: ${current.id}/${current.assignee}.`);
|
|
2259
2624
|
}
|
|
2260
2625
|
const sourceGroup = currentWorkItemExecutionGroup(current);
|
|
2261
|
-
const
|
|
2262
|
-
&&
|
|
2263
|
-
&&
|
|
2264
|
-
&&
|
|
2265
|
-
&&
|
|
2266
|
-
&&
|
|
2267
|
-
&&
|
|
2268
|
-
&&
|
|
2269
|
-
if (
|
|
2626
|
+
const mainRun = tx.listRuns(task.id).filter((run) => (run.purpose === "execution"
|
|
2627
|
+
&& run.workItemId === current.id
|
|
2628
|
+
&& run.roleName === current.assignee
|
|
2629
|
+
&& run.executionGroupId === undefined
|
|
2630
|
+
&& run.executionLaneId === undefined
|
|
2631
|
+
&& run.sourceExecutionGroupId === sourceGroup?.id
|
|
2632
|
+
&& run.status === "completed"
|
|
2633
|
+
&& run.result !== undefined)).at(-1);
|
|
2634
|
+
if (mainRun === undefined) {
|
|
2270
2635
|
throw usageError(sourceGroup === undefined
|
|
2271
|
-
? `Work Item ${current.id} has no completed direct main
|
|
2272
|
-
: `Work Item ${current.id} has no completed main
|
|
2636
|
+
? `Work Item ${current.id} has no completed direct main AgentRun.`
|
|
2637
|
+
: `Work Item ${current.id} has no completed main AgentRun for ExecutionGroup ${sourceGroup.id}.`);
|
|
2273
2638
|
}
|
|
2274
|
-
if (
|
|
2275
|
-
throw dataError(`Completed WorkItem main
|
|
2639
|
+
if (mainRun.result === undefined) {
|
|
2640
|
+
throw dataError(`Completed WorkItem main AgentRun has no result: ${mainRun.id}.`);
|
|
2276
2641
|
}
|
|
2277
2642
|
const configuredReview = tx.getReviewConfig();
|
|
2278
2643
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
@@ -2288,8 +2653,9 @@ function updateWork(args, store, options) {
|
|
|
2288
2653
|
throw usageError(`Project-backed Work Item ${current.id} must be isolated before Candidate submission.`);
|
|
2289
2654
|
}
|
|
2290
2655
|
const updated = submitWorkItemCandidate(current, {
|
|
2291
|
-
summary: `Result from
|
|
2292
|
-
source: { type: "
|
|
2656
|
+
summary: `Result from AgentRun ${mainRun.id}.`,
|
|
2657
|
+
source: { type: "run", runId: mainRun.id },
|
|
2658
|
+
...(artifactRefs === undefined ? {} : { artifactRefs }),
|
|
2293
2659
|
...(candidatePolicy === null ? {} : { reviewPolicy: candidatePolicy }),
|
|
2294
2660
|
...(taskFinalContract === undefined
|
|
2295
2661
|
? {}
|
|
@@ -2307,12 +2673,12 @@ function updateWork(args, store, options) {
|
|
|
2307
2673
|
workItemId: updated.id,
|
|
2308
2674
|
status: updated.status,
|
|
2309
2675
|
summary: summary,
|
|
2310
|
-
|
|
2676
|
+
runId: mainRun.id,
|
|
2311
2677
|
...leaderActionEventPayload(tx, task.id, options)
|
|
2312
2678
|
}, now);
|
|
2313
2679
|
enqueueWork(tx, taskMailbox(task.id), "work-updated", now, [
|
|
2314
2680
|
workItemRef(task.id, updated.id),
|
|
2315
|
-
|
|
2681
|
+
runRef(task.id, mainRun.id)
|
|
2316
2682
|
]);
|
|
2317
2683
|
const reviewDispatch = candidatePolicy?.trigger === "always"
|
|
2318
2684
|
? queueReviewRound(tx, updated, candidatePolicy, "policy", now)
|
|
@@ -2324,7 +2690,7 @@ function updateWork(args, store, options) {
|
|
|
2324
2690
|
};
|
|
2325
2691
|
});
|
|
2326
2692
|
notifyMailbox(options.runtime, taskMailbox(result.item.taskId), result.item.taskId);
|
|
2327
|
-
if (result.item.status === "
|
|
2693
|
+
if ((result.item.status === "open" && result.item.candidates.length > 0) && status === "completed") {
|
|
2328
2694
|
const failure = result.reviewDispatch?.round.status === "failed"
|
|
2329
2695
|
? `Review could not start: ${result.reviewDispatch.round.failure?.message ?? result.reviewDispatch.round.id}\n`
|
|
2330
2696
|
: "";
|
|
@@ -2359,13 +2725,13 @@ function dispatchWork(args, store, options) {
|
|
|
2359
2725
|
throw usageError(`Work Item has no Task Role assignee: ${item.id}. `
|
|
2360
2726
|
+ `The Task Leader must run "yui task work update ${item.id} running" and execute it directly.`);
|
|
2361
2727
|
}
|
|
2362
|
-
const lanePlan = planReplicatedWorkItemLanes(item.assignee, requestedLaneRoles, `execution-group-${tx.
|
|
2728
|
+
const lanePlan = planReplicatedWorkItemLanes(item.assignee, requestedLaneRoles, `execution-group-${tx.peekNextRunId(task.id)}`);
|
|
2363
2729
|
const currentGroup = currentWorkItemExecutionGroup(item);
|
|
2364
|
-
if (item.status !== "
|
|
2730
|
+
if (item.status !== "open") {
|
|
2365
2731
|
throw usageError(`Work item ${item.id} cannot be dispatched from ${item.status}.`);
|
|
2366
2732
|
}
|
|
2367
2733
|
if (currentGroup !== undefined && !workItemExecutionGroupSettled(currentGroup)) {
|
|
2368
|
-
throw usageError(`Work Item ${item.id} retains open ExecutionGroup ${currentGroup.id}; retry or settle its exact Lane
|
|
2734
|
+
throw usageError(`Work Item ${item.id} retains open ExecutionGroup ${currentGroup.id}; retry or settle its exact Lane AgentRuns.`);
|
|
2369
2735
|
}
|
|
2370
2736
|
assertWorkItemDependenciesCompletedForCommand(tx, item);
|
|
2371
2737
|
const leaderOwned = item.assignee === "leader";
|
|
@@ -2389,18 +2755,16 @@ function dispatchWork(args, store, options) {
|
|
|
2389
2755
|
}
|
|
2390
2756
|
const roles = lanePlan.roles.map((name) => requireRole(tx, task.id, name));
|
|
2391
2757
|
for (const role of roles) {
|
|
2392
|
-
if (tx.
|
|
2393
|
-
throw usageError(`${task.id}/${role.name} already has an active
|
|
2758
|
+
if (tx.getActiveRun(task.id, role.name) !== null) {
|
|
2759
|
+
throw usageError(`${task.id}/${role.name} already has an active run.`);
|
|
2394
2760
|
}
|
|
2395
2761
|
}
|
|
2396
2762
|
const rawInput = trimmed(parsed.options.get("--input")) ?? item.objective;
|
|
2397
|
-
let workItemForDispatch = item
|
|
2398
|
-
? retryFailedWorkItem(item, now)
|
|
2399
|
-
: item;
|
|
2763
|
+
let workItemForDispatch = prepareWorkItemDispatch(item, now);
|
|
2400
2764
|
if (lanePlan.roles.length === 0) {
|
|
2401
2765
|
const role = requireRole(tx, task.id, item.assignee);
|
|
2402
|
-
if (tx.
|
|
2403
|
-
throw usageError(`${task.id}/${role.name} already has an active
|
|
2766
|
+
if (tx.getActiveRun(task.id, role.name) !== null) {
|
|
2767
|
+
throw usageError(`${task.id}/${role.name} already has an active run.`);
|
|
2404
2768
|
}
|
|
2405
2769
|
const effective = resolveEffectiveLaunch({
|
|
2406
2770
|
role,
|
|
@@ -2408,8 +2772,8 @@ function dispatchWork(args, store, options) {
|
|
|
2408
2772
|
workspace,
|
|
2409
2773
|
workItemWriteProjectIds: item.writeProjectIds
|
|
2410
2774
|
});
|
|
2411
|
-
const
|
|
2412
|
-
const
|
|
2775
|
+
const runId = tx.nextRunId(task.id);
|
|
2776
|
+
const run = createRun(runId, task.id, role.name, roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(task.id, role.name), effective.agentId, effective), createRunInput({
|
|
2413
2777
|
source: { type: "yui", channel: "workitem-dispatch" },
|
|
2414
2778
|
directive: rawInput,
|
|
2415
2779
|
deltaRefIds: []
|
|
@@ -2418,30 +2782,30 @@ function dispatchWork(args, store, options) {
|
|
|
2418
2782
|
workspace,
|
|
2419
2783
|
effective
|
|
2420
2784
|
});
|
|
2421
|
-
const snapshot =
|
|
2785
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
2422
2786
|
taskId: task.id,
|
|
2423
|
-
roleName:
|
|
2787
|
+
roleName: run.roleName,
|
|
2424
2788
|
purpose: "execution",
|
|
2425
2789
|
workItemId: item.id
|
|
2426
2790
|
}, now, "controller");
|
|
2427
|
-
const withContext =
|
|
2428
|
-
if (workItemForDispatch.status !== "
|
|
2429
|
-
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "
|
|
2791
|
+
const withContext = withRunContextSnapshot(run, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
2792
|
+
if (workItemForDispatch.status !== "open") {
|
|
2793
|
+
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "open", now);
|
|
2430
2794
|
}
|
|
2431
2795
|
tx.saveWorkItem(task.id, workItemForDispatch);
|
|
2432
|
-
tx.
|
|
2433
|
-
tx.
|
|
2434
|
-
|
|
2796
|
+
tx.saveRun(withContext);
|
|
2797
|
+
tx.saveActiveRun(withContext);
|
|
2798
|
+
enqueueRoleRunDispatch(tx, {
|
|
2435
2799
|
taskId: task.id,
|
|
2436
2800
|
roleName: role.name,
|
|
2437
|
-
|
|
2801
|
+
runId: withContext.id,
|
|
2438
2802
|
reason: "turn-dispatched",
|
|
2439
2803
|
occurredAt: now
|
|
2440
2804
|
});
|
|
2441
|
-
recordTaskEvent(tx, task.id, "
|
|
2442
|
-
return { kind: "direct",
|
|
2805
|
+
recordTaskEvent(tx, task.id, "run.dispatched", runLaunchEventPayload(withContext), now);
|
|
2806
|
+
return { kind: "direct", runs: [withContext] };
|
|
2443
2807
|
}
|
|
2444
|
-
const groupId = `execution-group-${tx.
|
|
2808
|
+
const groupId = `execution-group-${tx.peekNextRunId(task.id)}`;
|
|
2445
2809
|
if (workItemForDispatch !== item) {
|
|
2446
2810
|
// Freeze the Assignment against the retried WorkItem revision. The
|
|
2447
2811
|
// aggregate transaction rolls this back if a later precondition fails.
|
|
@@ -2470,7 +2834,7 @@ function dispatchWork(args, store, options) {
|
|
|
2470
2834
|
laneId,
|
|
2471
2835
|
managedWorkspace: laneWorkspace,
|
|
2472
2836
|
effective,
|
|
2473
|
-
|
|
2837
|
+
runId: tx.nextRunId(task.id),
|
|
2474
2838
|
dispatchMode: roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(task.id, role.name), effective.agentId, effective)
|
|
2475
2839
|
};
|
|
2476
2840
|
});
|
|
@@ -2503,15 +2867,15 @@ function dispatchWork(args, store, options) {
|
|
|
2503
2867
|
root: plan.managedWorkspace.root,
|
|
2504
2868
|
writableProjectIds: [...item.writeProjectIds]
|
|
2505
2869
|
},
|
|
2506
|
-
|
|
2870
|
+
currentRunId: plan.runId
|
|
2507
2871
|
})), now);
|
|
2508
2872
|
workItemForDispatch = attachWorkItemExecutionGroup(workItemForDispatch, group, now);
|
|
2509
|
-
if (workItemForDispatch.status !== "
|
|
2510
|
-
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "
|
|
2873
|
+
if (workItemForDispatch.status !== "open") {
|
|
2874
|
+
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "open", now);
|
|
2511
2875
|
}
|
|
2512
2876
|
tx.saveWorkItem(task.id, workItemForDispatch);
|
|
2513
|
-
const
|
|
2514
|
-
const
|
|
2877
|
+
const runs = plans.map((plan, index) => {
|
|
2878
|
+
const run = createRun(plan.runId, task.id, plan.role.name, plan.dispatchMode, createRunInput({
|
|
2515
2879
|
source: { type: "yui", channel: "workitem-dispatch" },
|
|
2516
2880
|
directive: assignment.input,
|
|
2517
2881
|
deltaRefIds: []
|
|
@@ -2522,37 +2886,37 @@ function dispatchWork(args, store, options) {
|
|
|
2522
2886
|
workspace: plan.managedWorkspace,
|
|
2523
2887
|
effective: plan.effective
|
|
2524
2888
|
});
|
|
2525
|
-
const snapshot =
|
|
2889
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
2526
2890
|
taskId: task.id,
|
|
2527
|
-
roleName:
|
|
2891
|
+
roleName: run.roleName,
|
|
2528
2892
|
purpose: "execution",
|
|
2529
2893
|
workItemId: item.id
|
|
2530
2894
|
}, now, "controller", assignment.contextSnapshotRef);
|
|
2531
|
-
const withContext =
|
|
2895
|
+
const withContext = withRunContextSnapshot(run, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
2532
2896
|
const prepared = options.executionLaneWorkspaces?.get(group.lanes[index].id);
|
|
2533
2897
|
if (prepared !== undefined && tx.getManagedWorkspace(prepared.owner) === null) {
|
|
2534
2898
|
tx.saveManagedWorkspace(prepared);
|
|
2535
2899
|
}
|
|
2536
|
-
tx.
|
|
2537
|
-
tx.
|
|
2538
|
-
|
|
2900
|
+
tx.saveRun(withContext);
|
|
2901
|
+
tx.saveActiveRun(withContext);
|
|
2902
|
+
enqueueRoleRunDispatch(tx, {
|
|
2539
2903
|
taskId: task.id,
|
|
2540
2904
|
roleName: plan.role.name,
|
|
2541
|
-
|
|
2905
|
+
runId: withContext.id,
|
|
2542
2906
|
reason: "turn-dispatched",
|
|
2543
2907
|
occurredAt: now
|
|
2544
2908
|
});
|
|
2545
|
-
recordTaskEvent(tx, task.id, "
|
|
2909
|
+
recordTaskEvent(tx, task.id, "run.dispatched", runLaunchEventPayload(withContext), now);
|
|
2546
2910
|
return withContext;
|
|
2547
2911
|
});
|
|
2548
|
-
return { kind: "replicated",
|
|
2912
|
+
return { kind: "replicated", runs };
|
|
2549
2913
|
});
|
|
2550
|
-
for (const
|
|
2551
|
-
notifyMailbox(options.runtime, roleMailbox(
|
|
2914
|
+
for (const run of dispatch.runs) {
|
|
2915
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
2552
2916
|
}
|
|
2553
2917
|
return dispatch.kind === "direct"
|
|
2554
|
-
? `Direct WorkItem
|
|
2555
|
-
: `Dispatch queued for ${dispatch.
|
|
2918
|
+
? `Direct WorkItem AgentRun queued as ${dispatch.runs[0].id}\n`
|
|
2919
|
+
: `Dispatch queued for ${dispatch.runs.length} replicated Lanes\n`;
|
|
2556
2920
|
}
|
|
2557
2921
|
function replicatedProducerAssignmentInput(input, requiresCodeRef) {
|
|
2558
2922
|
return [
|
|
@@ -2568,7 +2932,7 @@ function replicatedProducerAssignmentInput(input, requiresCodeRef) {
|
|
|
2568
2932
|
}
|
|
2569
2933
|
function acceptWork(args, store, options) {
|
|
2570
2934
|
const usage = "Task work accept usage: yui task work accept <task>/<work> --summary <text>.";
|
|
2571
|
-
const parsed = parseTail(args, new Set(["--summary"]), usage);
|
|
2935
|
+
const parsed = parseTail(args, new Set(["--summary", "--candidate"]), usage);
|
|
2572
2936
|
exactPositionals(parsed.positionals, 1, usage);
|
|
2573
2937
|
const summary = requiredOption(parsed.options, "--summary");
|
|
2574
2938
|
const now = clock(options);
|
|
@@ -2579,17 +2943,20 @@ function acceptWork(args, store, options) {
|
|
|
2579
2943
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2580
2944
|
}
|
|
2581
2945
|
const actor = taskActor(tx, options, task.id);
|
|
2582
|
-
if (item.status !== "
|
|
2946
|
+
if ((item.status !== "open" || item.candidates.length === 0)) {
|
|
2583
2947
|
throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
|
|
2584
2948
|
}
|
|
2585
2949
|
if (options.workItemIntegrationProof?.workspace.owner.type === "review-round") {
|
|
2586
2950
|
throw usageError("A ReviewRound-owned workspace cannot be used for WorkItem acceptance.");
|
|
2587
2951
|
}
|
|
2588
|
-
const
|
|
2952
|
+
const candidateId = parsed.options.get("--candidate");
|
|
2953
|
+
const candidate = candidateId === undefined ? requireWorkItemCandidate(item)
|
|
2954
|
+
: item.candidates.find(({ id }) => id === candidateId);
|
|
2955
|
+
if (candidate === undefined)
|
|
2956
|
+
throw usageError(`Work Item Candidate not found: ${candidateId}.`);
|
|
2957
|
+
if (candidate.artifactRefs !== undefined && !isDeepStrictEqual(fixedArtifactRefs(tx, task.id, candidate.artifactRefs.map((ref) => ref.artifactId)), candidate.artifactRefs))
|
|
2958
|
+
throw usageError("Candidate Artifact references no longer match their saved immutable results.");
|
|
2589
2959
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
2590
|
-
if (!sameTaskFinalReviewContract(candidate.taskFinalReviewContract, taskFinalContract)) {
|
|
2591
|
-
throw usageError(`Task final-review contract does not match Candidate ${candidate.id}.`);
|
|
2592
|
-
}
|
|
2593
2960
|
const latestReview = reviewRoundsByIdentity(tx.listReviewRounds(item.taskId)
|
|
2594
2961
|
.filter((round) => round.workItemId === item.id
|
|
2595
2962
|
&& round.candidateId === candidate.id)).at(-1);
|
|
@@ -2617,17 +2984,25 @@ function acceptWork(args, store, options) {
|
|
|
2617
2984
|
&& isolatedWorkspace.owner.workItemId === item.id
|
|
2618
2985
|
&& isolatedWorkspace.entries.some(({ access }) => access === "write")) {
|
|
2619
2986
|
assertWorkItemIntegrationProof(tx, item.id, item.assignee, isolatedWorkspace, options.workItemIntegrationProof);
|
|
2987
|
+
for (const proof of options.workItemIntegrationProof.projects) {
|
|
2988
|
+
const candidateCommit = candidate.gitSnapshot?.projects
|
|
2989
|
+
.find(({ projectId }) => projectId === proof.projectId)?.commit;
|
|
2990
|
+
if (candidateCommit === undefined || candidateCommit !== proof.headCommit) {
|
|
2991
|
+
throw usageError(`Selected Candidate ${candidate.id} does not match the integrated result for ${proof.projectId}.`);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2620
2994
|
}
|
|
2621
|
-
const completed = updateWorkItemStatus(item, "
|
|
2995
|
+
const completed = updateWorkItemStatus(item, "accepted", now, summary, candidate.id);
|
|
2622
2996
|
tx.saveWorkItem(item.taskId, completed);
|
|
2623
2997
|
recordTaskEvent(tx, item.taskId, "work.accepted", {
|
|
2624
2998
|
workItemId: item.id,
|
|
2625
2999
|
candidateId: candidate.id,
|
|
2626
|
-
...(candidate.source.type === "
|
|
2627
|
-
? {
|
|
3000
|
+
...(candidate.source.type === "run"
|
|
3001
|
+
? { runId: candidate.source.runId }
|
|
2628
3002
|
: { workItemRevision: String(candidate.workItemRevision) }),
|
|
2629
3003
|
acceptedBy: actor,
|
|
2630
3004
|
summary,
|
|
3005
|
+
...(latestReview === undefined ? {} : { reviewRoundId: latestReview.id }),
|
|
2631
3006
|
...(actor === "leader" ? leaderActionEventPayload(tx, item.taskId, options) : {})
|
|
2632
3007
|
}, now);
|
|
2633
3008
|
return completed;
|
|
@@ -2673,7 +3048,7 @@ function rejectWork(args, store, options) {
|
|
|
2673
3048
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2674
3049
|
}
|
|
2675
3050
|
const actor = taskActor(tx, options, task.id);
|
|
2676
|
-
if (item.status
|
|
3051
|
+
if (item.status === "retired" || item.candidates.length === 0) {
|
|
2677
3052
|
throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
|
|
2678
3053
|
}
|
|
2679
3054
|
const candidate = requireWorkItemCandidate(item);
|
|
@@ -2681,7 +3056,7 @@ function rejectWork(args, store, options) {
|
|
|
2681
3056
|
if (activeReview !== undefined) {
|
|
2682
3057
|
throw usageError(`ReviewRound is still active: ${activeReview.id}/${activeReview.status}.`);
|
|
2683
3058
|
}
|
|
2684
|
-
const failed = updateWorkItemStatus(item, "
|
|
3059
|
+
const { currentCandidateId: _declinedCandidate, ...failed } = updateWorkItemStatus(item, "open", now);
|
|
2685
3060
|
tx.saveWorkItem(item.taskId, failed);
|
|
2686
3061
|
recordTaskEvent(tx, item.taskId, "work.rejected", {
|
|
2687
3062
|
workItemId: item.id,
|
|
@@ -2735,12 +3110,12 @@ function retireWork(args, store, options) {
|
|
|
2735
3110
|
+ `${activeWorkItemJob.id}/${activeWorkItemJob.status}. `
|
|
2736
3111
|
+ "Cancel or acknowledge it before retiring.");
|
|
2737
3112
|
}
|
|
2738
|
-
for (const run of tx.
|
|
2739
|
-
const terminal =
|
|
3113
|
+
for (const run of tx.listRuns(task.id).filter((candidate) => (candidate.status === "active" && candidate.workItemId === item.id))) {
|
|
3114
|
+
const terminal = terminalizeExactTaskRun(tx, {
|
|
2740
3115
|
taskId: task.id,
|
|
2741
3116
|
roleName: run.roleName,
|
|
2742
3117
|
agentId: run.effective.agentId,
|
|
2743
|
-
|
|
3118
|
+
runId: run.id,
|
|
2744
3119
|
outcome: {
|
|
2745
3120
|
status: "failed",
|
|
2746
3121
|
diagnostic: `Work Item retired: ${summary}`,
|
|
@@ -2748,7 +3123,7 @@ function retireWork(args, store, options) {
|
|
|
2748
3123
|
}
|
|
2749
3124
|
}, now);
|
|
2750
3125
|
if (terminal.disposition !== "applied") {
|
|
2751
|
-
throw usageError(`Work Item
|
|
3126
|
+
throw usageError(`Work Item AgentRun changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
2752
3127
|
}
|
|
2753
3128
|
}
|
|
2754
3129
|
}
|
|
@@ -2785,9 +3160,9 @@ function listWork(args, store) {
|
|
|
2785
3160
|
exactPositionals(args, 1, "Task work list usage: yui task work list <task>.");
|
|
2786
3161
|
const task = requireTask(store, args[0]);
|
|
2787
3162
|
const items = store.listWorkItems(task.id);
|
|
2788
|
-
const
|
|
3163
|
+
const runs = store.listRuns(task.id);
|
|
2789
3164
|
const sessionSets = store.listRoleSessionSets(task.id);
|
|
2790
|
-
const executions = items.map((item) => projectWorkItemExecution(item,
|
|
3165
|
+
const executions = items.map((item) => projectWorkItemExecution(item, runs, sessionSets, store));
|
|
2791
3166
|
const rendered = items.length === 0
|
|
2792
3167
|
? "No work items found.\n"
|
|
2793
3168
|
: `${renderTable(`Task work: ${task.id}`, [
|
|
@@ -2814,7 +3189,7 @@ function listWork(args, store) {
|
|
|
2814
3189
|
function showWork(args, store, options) {
|
|
2815
3190
|
exactPositionals(args, 1, "Task work show usage: yui task work show <work>.");
|
|
2816
3191
|
const item = requireWorkItem(store, args[0], options);
|
|
2817
|
-
const execution = projectWorkItemExecution(item, store.
|
|
3192
|
+
const execution = projectWorkItemExecution(item, store.listRuns(item.taskId), store.listRoleSessionSets(item.taskId), store);
|
|
2818
3193
|
const replacement = item.disposition?.replacementWorkItemId;
|
|
2819
3194
|
const rendered = [
|
|
2820
3195
|
`Work Item: ${item.id}`,
|
|
@@ -2835,7 +3210,7 @@ function showWork(args, store, options) {
|
|
|
2835
3210
|
}
|
|
2836
3211
|
function compactWorkItemExecution(projection) {
|
|
2837
3212
|
if (projection.shape === "direct")
|
|
2838
|
-
return `main=${projection.
|
|
3213
|
+
return `main=${projection.mainRun.status}`;
|
|
2839
3214
|
const counts = projection.laneCounts;
|
|
2840
3215
|
return `lanes ${counts.running}/${counts.succeeded}/${counts.needsAttention}/${counts.failed}/${counts.unknown}; ${projection.synthesis.status}`;
|
|
2841
3216
|
}
|
|
@@ -2848,16 +3223,16 @@ function renderWorkItemExecutionProjection(projection) {
|
|
|
2848
3223
|
: [
|
|
2849
3224
|
`Lanes: running=${projection.laneCounts.running}, succeeded=${projection.laneCounts.succeeded}, needs-attention=${projection.laneCounts.needsAttention}, failed=${projection.laneCounts.failed}, unknown=${projection.laneCounts.unknown}`,
|
|
2850
3225
|
...projection.lanes.map((lane) => (` ${lane.laneId} (#${lane.ordinal}, ${lane.roleName}): ${lane.status}; `
|
|
2851
|
-
+ `
|
|
2852
|
-
+ `retry=${lane.
|
|
3226
|
+
+ `run=${lane.currentRunId ?? "unknown"}; session=${lane.session}; `
|
|
3227
|
+
+ `retry=${lane.retryRunId ?? "none"}; settle=${lane.settleRunId ?? "none"}`))
|
|
2853
3228
|
]),
|
|
2854
|
-
`Synthesis: ${projection.synthesis.status}; successful=${projection.synthesis.successfulLaneCount}
|
|
2855
|
-
`Main
|
|
2856
|
-
`Candidate Source: ${projection.candidate.candidateId ?? "none"} [${projection.candidate.status}]; source=${projection.candidate.sourceType ?? "unobserved"}; main=${projection.candidate.
|
|
3229
|
+
`Synthesis: ${projection.synthesis.status}; successful=${projection.synthesis.successfulLaneCount}; sources selected by Leader`,
|
|
3230
|
+
`Main AgentRun: ${projection.mainRun.runId ?? "unobserved"} [${projection.mainRun.status}]; role=${projection.mainRun.roleName ?? "unobserved"}; session=${projection.mainRun.session}; retry=${projection.mainRun.retryRunId ?? "none"}`,
|
|
3231
|
+
`Candidate Source: ${projection.candidate.candidateId ?? "none"} [${projection.candidate.status}]; source=${projection.candidate.sourceType ?? "unobserved"}; main=${projection.candidate.mainRunId ?? "unobserved"}`,
|
|
2857
3232
|
...(projection.candidate.sourceExecutionGroupId === undefined
|
|
2858
3233
|
? []
|
|
2859
3234
|
: [
|
|
2860
|
-
`Candidate Provenance: main ${projection.candidate.
|
|
3235
|
+
`Candidate Provenance: main ${projection.candidate.mainRunId ?? "unobserved"} -> group ${projection.candidate.sourceExecutionGroupId} -> ${projection.candidate.successfulLaneRuns.map(({ laneId, successfulRunId }) => `${laneId} -> ${successfulRunId}`).join(", ") || "unobserved"}`
|
|
2861
3236
|
]),
|
|
2862
3237
|
`Next Action: ${projection.nextAction.kind}; owner=${projection.nextAction.owners.join(", ") || "none"}; target=${projection.nextAction.targetIds.join(", ") || "none"}`
|
|
2863
3238
|
];
|
|
@@ -2882,7 +3257,7 @@ function reviewWork(args, store, options) {
|
|
|
2882
3257
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2883
3258
|
}
|
|
2884
3259
|
const requestedBy = taskActor(tx, options, task.id);
|
|
2885
|
-
if (item.status !== "
|
|
3260
|
+
if ((item.status !== "open" || item.candidates.length === 0)) {
|
|
2886
3261
|
throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
|
|
2887
3262
|
}
|
|
2888
3263
|
const candidate = requireWorkItemCandidate(item);
|
|
@@ -2900,9 +3275,9 @@ function reviewWork(args, store, options) {
|
|
|
2900
3275
|
&& (round.status === "pending" || round.status === "running")))).at(-1);
|
|
2901
3276
|
if (activeRound !== undefined) {
|
|
2902
3277
|
const producerDispatchPending = activeRound.executionGroup?.lanes.some((lane) => (lane.disposition === "open"
|
|
2903
|
-
&& (lane.
|
|
2904
|
-
|| tx.
|
|
2905
|
-
if ((activeRound.status === "pending" && activeRound.
|
|
3278
|
+
&& (lane.currentRunId === undefined
|
|
3279
|
+
|| tx.getRun(task.id, lane.currentRunId)?.status === "failed"))) === true;
|
|
3280
|
+
if ((activeRound.status === "pending" && activeRound.reviewerRunId === undefined)
|
|
2906
3281
|
|| (activeRound.status === "running" && producerDispatchPending)) {
|
|
2907
3282
|
const persistedRoles = activeRound.executionGroup?.lanes
|
|
2908
3283
|
.map(({ roleName }) => roleName) ?? [];
|
|
@@ -2935,7 +3310,7 @@ function reviewWork(args, store, options) {
|
|
|
2935
3310
|
if (result.kind === "busy") {
|
|
2936
3311
|
const busy = result.availability;
|
|
2937
3312
|
return output(`Reviewer ${busy.reviewerRoleName} is busy (${busy.phase}`
|
|
2938
|
-
+ `${busy.
|
|
3313
|
+
+ `${busy.activeRunId === undefined ? "" : `; AgentRun ${busy.activeRunId}`}); `
|
|
2939
3314
|
+ `${busy.activeReviewRoundId === undefined
|
|
2940
3315
|
? ""
|
|
2941
3316
|
: `active ReviewRound ${busy.activeReviewRoundId}; `}`
|
|
@@ -2950,17 +3325,49 @@ function reviewWork(args, store, options) {
|
|
|
2950
3325
|
}
|
|
2951
3326
|
/**
|
|
2952
3327
|
* Task-control recovery for a failed Task-final ReviewRound that never
|
|
2953
|
-
* created a Reviewer
|
|
2954
|
-
* that command requires an exact failed
|
|
3328
|
+
* created a Reviewer AgentRun. This is deliberately separate from `task run retry`:
|
|
3329
|
+
* that command requires an exact failed AgentRun and remains the only retry
|
|
2955
3330
|
* path for a failed provider execution. Here the old terminal Round is an
|
|
2956
3331
|
* immutable anchor and one fresh Round is created only after the same frozen
|
|
2957
3332
|
* committed Integration/ChangeSet provenance and Reviewer independence fences
|
|
2958
3333
|
* pass again.
|
|
2959
3334
|
*/
|
|
3335
|
+
function synthesizeRuns(args, kind, store, options) {
|
|
3336
|
+
const subject = kind === "workItem" ? "work" : "review";
|
|
3337
|
+
const usage = `Usage: yui task ${subject} synthesize <task>/<${subject}> --source-run <task>/<run> ...`;
|
|
3338
|
+
const parsed = parseMultiValueTail(args, new Set(), new Set(["--source-run"]), usage);
|
|
3339
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
3340
|
+
const reference = taskRecordReference(parsed.positionals[0], kind, "Synthesis target", options);
|
|
3341
|
+
const sources = parsed.multiOptions.get("--source-run") ?? [];
|
|
3342
|
+
const sourceRunIds = sources.map((value) => {
|
|
3343
|
+
const source = taskRecordReference(value, "run", "Source AgentRun", options);
|
|
3344
|
+
if (source.taskId !== reference.taskId)
|
|
3345
|
+
throw usageError("Synthesis sources must belong to the same Task.");
|
|
3346
|
+
return source.localId;
|
|
3347
|
+
});
|
|
3348
|
+
const now = clock(options);
|
|
3349
|
+
const run = store.transaction((tx) => {
|
|
3350
|
+
const actor = taskActor(tx, options, reference.taskId);
|
|
3351
|
+
const created = kind === "workItem"
|
|
3352
|
+
? dispatchWorkItemSynthesis(tx, reference.taskId, reference.localId, sourceRunIds, now)
|
|
3353
|
+
: dispatchReviewSynthesis(tx, reference.taskId, reference.localId, sourceRunIds, now);
|
|
3354
|
+
recordTaskEvent(tx, reference.taskId, "run.synthesis-requested", {
|
|
3355
|
+
runId: created.id,
|
|
3356
|
+
requestedBy: actor,
|
|
3357
|
+
sourceRunIds: sourceRunIds.join(","),
|
|
3358
|
+
...(actor === "leader" ? leaderActionEventPayload(tx, reference.taskId, options) : {})
|
|
3359
|
+
}, now);
|
|
3360
|
+
return created;
|
|
3361
|
+
});
|
|
3362
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
3363
|
+
return output(`Dispatched synthesis AgentRun ${run.taskId}/${run.id}\n`, { run });
|
|
3364
|
+
}
|
|
2960
3365
|
function taskReviewCommand(args, store, options) {
|
|
2961
3366
|
const [command, ...rest] = args;
|
|
2962
3367
|
if (command === "request")
|
|
2963
3368
|
return requestTaskReviewRound(rest, store, options);
|
|
3369
|
+
if (command === "synthesize")
|
|
3370
|
+
return synthesizeRuns(rest, "reviewRound", store, options);
|
|
2964
3371
|
if (command === "retry")
|
|
2965
3372
|
return retryFailedTaskReviewRound(rest, store, options);
|
|
2966
3373
|
throw usageError(command === undefined
|
|
@@ -2968,7 +3375,7 @@ function taskReviewCommand(args, store, options) {
|
|
|
2968
3375
|
: `Unknown command: task review ${command}`);
|
|
2969
3376
|
}
|
|
2970
3377
|
function requestTaskReviewRound(args, store, options) {
|
|
2971
|
-
const usage = "Task review request usage: yui task review request <task> --role <
|
|
3378
|
+
const usage = "Task review request usage: yui task review request <task> --role <reviewer-role> "
|
|
2972
3379
|
+ "[--lane-role <producer-role> ...] [--delta-recheck].";
|
|
2973
3380
|
const parsed = parseMultiValueTail(args, new Set(["--role"]), new Set(["--lane-role"]), usage, new Set(["--delta-recheck"]));
|
|
2974
3381
|
exactPositionals(parsed.positionals, 1, usage);
|
|
@@ -2993,8 +3400,8 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
2993
3400
|
throw usageError("Delta-recheck is not supported with a Task-final review contract.");
|
|
2994
3401
|
}
|
|
2995
3402
|
}
|
|
2996
|
-
if (tx.getGlobalRole(reviewerRoleName) === null) {
|
|
2997
|
-
throw usageError(`
|
|
3403
|
+
if (tx.getRole(task.id, reviewerRoleName) === null && tx.getGlobalRole(reviewerRoleName) === null) {
|
|
3404
|
+
throw usageError(`Reviewer Role not found in this Task or global templates: ${reviewerRoleName}.`);
|
|
2998
3405
|
}
|
|
2999
3406
|
const provenance = taskReviewProvenance(tx, task, options);
|
|
3000
3407
|
const producerCollision = taskReviewProducerCollision(provenance, reviewerRoleName);
|
|
@@ -3095,7 +3502,7 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3095
3502
|
});
|
|
3096
3503
|
if ("kind" in round && round.kind === "busy") {
|
|
3097
3504
|
return output(`Reviewer ${round.reviewerRoleName} is busy (${round.phase}`
|
|
3098
|
-
+ `${round.
|
|
3505
|
+
+ `${round.activeRunId === undefined ? "" : `; AgentRun ${round.activeRunId}`}); `
|
|
3099
3506
|
+ `${round.activeReviewRoundId === undefined
|
|
3100
3507
|
? ""
|
|
3101
3508
|
: `active ReviewRound ${round.activeReviewRoundId}; `}`
|
|
@@ -3134,26 +3541,26 @@ function validateDeltaRecheckRequest(store, taskId, candidate, preflight) {
|
|
|
3134
3541
|
return preflight.record;
|
|
3135
3542
|
}
|
|
3136
3543
|
function assertTaskReviewRequestLane(store, taskId, reviewerRoleName, reusableRound) {
|
|
3137
|
-
const activePointer = store.
|
|
3544
|
+
const activePointer = store.getActiveRun(taskId, reviewerRoleName);
|
|
3138
3545
|
if (reusableRound === undefined || reusableRound.status === "completed") {
|
|
3139
3546
|
assertReviewerAvailable(store, taskId, reviewerRoleName);
|
|
3140
3547
|
return;
|
|
3141
3548
|
}
|
|
3142
3549
|
if (reusableRound.status === "pending") {
|
|
3143
3550
|
if (activePointer !== null) {
|
|
3144
|
-
throw usageError(`Reviewer Role already has an active
|
|
3551
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewerRoleName}.`);
|
|
3145
3552
|
}
|
|
3146
3553
|
assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound);
|
|
3147
3554
|
return;
|
|
3148
3555
|
}
|
|
3149
|
-
const
|
|
3150
|
-
if (
|
|
3556
|
+
const reviewerRunId = reusableRound.reviewerRunId;
|
|
3557
|
+
if (reviewerRunId === undefined
|
|
3151
3558
|
&& reusableRound.executionGroup?.lanes.some(({ disposition }) => disposition === "open")) {
|
|
3152
3559
|
assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound);
|
|
3153
3560
|
return;
|
|
3154
3561
|
}
|
|
3155
|
-
const activeMatches =
|
|
3156
|
-
&& activePointer?.id ===
|
|
3562
|
+
const activeMatches = reviewerRunId !== undefined
|
|
3563
|
+
&& activePointer?.id === reviewerRunId
|
|
3157
3564
|
&& activePointer.status === "active";
|
|
3158
3565
|
if (!activeMatches) {
|
|
3159
3566
|
throw usageError(`Existing Task-final ReviewRound ${reusableRound.id} is running without its exact Reviewer execution.`);
|
|
@@ -3168,7 +3575,7 @@ function assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound)
|
|
|
3168
3575
|
return;
|
|
3169
3576
|
}
|
|
3170
3577
|
throw usageError(`Reviewer ${reviewerRoleName} is busy (${availability.phase}`
|
|
3171
|
-
+ `${availability.
|
|
3578
|
+
+ `${availability.activeRunId === undefined ? "" : `; AgentRun ${availability.activeRunId}`}`
|
|
3172
3579
|
+ `${availability.activeReviewRoundId === undefined
|
|
3173
3580
|
? ""
|
|
3174
3581
|
: `; ReviewRound ${availability.activeReviewRoundId}`}).`);
|
|
@@ -3178,10 +3585,10 @@ function reviewerBusyBelongsToRound(busy, round) {
|
|
|
3178
3585
|
return false;
|
|
3179
3586
|
if (busy.phase === "review-slot")
|
|
3180
3587
|
return true;
|
|
3181
|
-
if (busy.phase !== "active-turn" || busy.
|
|
3588
|
+
if (busy.phase !== "active-turn" || busy.activeRunId === undefined)
|
|
3182
3589
|
return false;
|
|
3183
|
-
return round.
|
|
3184
|
-
|| round.executionGroup?.lanes.some(({
|
|
3590
|
+
return round.reviewerRunId === busy.activeRunId
|
|
3591
|
+
|| round.executionGroup?.lanes.some(({ currentRunId }) => (currentRunId === busy.activeRunId)) === true;
|
|
3185
3592
|
}
|
|
3186
3593
|
function retryFailedTaskReviewRound(args, store, options) {
|
|
3187
3594
|
exactPositionals(args, 1, "Task review retry usage: yui task review retry <task>/<review-round>.");
|
|
@@ -3199,11 +3606,11 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3199
3606
|
if ((round.scope ?? "work-item") !== "task") {
|
|
3200
3607
|
throw usageError(`ReviewRound ${round.id} is not a failed Task-final ReviewRound.`);
|
|
3201
3608
|
}
|
|
3202
|
-
if (round.
|
|
3609
|
+
if (round.reviewerRunId !== undefined) {
|
|
3203
3610
|
if (round.status === "completed") {
|
|
3204
3611
|
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
3205
3612
|
}
|
|
3206
|
-
throw usageError(`ReviewRound ${round.id} has Reviewer
|
|
3613
|
+
throw usageError(`ReviewRound ${round.id} has Reviewer AgentRun ${round.reviewerRunId}; use task run retry instead.`);
|
|
3207
3614
|
}
|
|
3208
3615
|
if (round.status !== "failed" && round.status !== "pending") {
|
|
3209
3616
|
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
@@ -3243,9 +3650,9 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3243
3650
|
reviewer = createTaskRole(tx, task, round.reviewerRoleName, undefined, now, round.reviewerRoleName);
|
|
3244
3651
|
tx.saveRole(task.id, reviewer);
|
|
3245
3652
|
}
|
|
3246
|
-
const activePointer = tx.
|
|
3653
|
+
const activePointer = tx.getActiveRun(task.id, reviewer.name);
|
|
3247
3654
|
if (activePointer !== null) {
|
|
3248
|
-
throw usageError(`Reviewer Role already has an active
|
|
3655
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
3249
3656
|
}
|
|
3250
3657
|
assertReviewerAvailable(tx, task.id, reviewer.name, round);
|
|
3251
3658
|
// Issue 06: an already-pending Round is the idempotent retry result.
|
|
@@ -3266,50 +3673,50 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3266
3673
|
? `Task-final Review retry requested as ${result.round.id}\n`
|
|
3267
3674
|
: `Task-final Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round });
|
|
3268
3675
|
}
|
|
3269
|
-
function
|
|
3676
|
+
function taskRunCommand(args, store, options) {
|
|
3270
3677
|
const [command, ...rest] = args;
|
|
3271
3678
|
if (command === "list")
|
|
3272
|
-
return output(
|
|
3679
|
+
return output(listRuns(rest, store, options));
|
|
3273
3680
|
if (command === "show")
|
|
3274
|
-
return
|
|
3681
|
+
return showRun(rest, store, options);
|
|
3275
3682
|
if (command === "context")
|
|
3276
|
-
return
|
|
3683
|
+
return runContextCommand(rest, store, options);
|
|
3277
3684
|
if (command === "retry")
|
|
3278
|
-
return
|
|
3685
|
+
return retryRun(rest, store, options);
|
|
3279
3686
|
if (command === "settle")
|
|
3280
|
-
return
|
|
3687
|
+
return settleRun(rest, store, options);
|
|
3281
3688
|
if (command === "checkpoint")
|
|
3282
|
-
return output(
|
|
3689
|
+
return output(checkpointRun(rest, store, options));
|
|
3283
3690
|
if (command === "retire")
|
|
3284
|
-
return
|
|
3691
|
+
return retireRun(rest, store, options);
|
|
3285
3692
|
throw usageError(command === undefined
|
|
3286
3693
|
? "Task turn command is required."
|
|
3287
|
-
: `Unknown command: task
|
|
3694
|
+
: `Unknown command: task run ${command}`);
|
|
3288
3695
|
}
|
|
3289
|
-
function
|
|
3290
|
-
exactPositionals(args, 1, "Task turn settle usage: yui task
|
|
3291
|
-
const previous = store.transaction((tx) =>
|
|
3696
|
+
function settleRun(args, store, options) {
|
|
3697
|
+
exactPositionals(args, 1, "Task turn settle usage: yui task run settle <task>/<run>.");
|
|
3698
|
+
const previous = store.transaction((tx) => requireRun(tx, args[0], options));
|
|
3292
3699
|
if (previous.purpose === "review") {
|
|
3293
3700
|
if (previous.executionGroupId !== undefined
|
|
3294
3701
|
&& previous.executionLaneId !== undefined) {
|
|
3295
|
-
return
|
|
3702
|
+
return settleFailedReviewExecutionLaneRun(previous, store, options);
|
|
3296
3703
|
}
|
|
3297
|
-
return
|
|
3704
|
+
return settleStaleFinalReviewRun(args, store, options);
|
|
3298
3705
|
}
|
|
3299
|
-
return
|
|
3706
|
+
return settleFailedExecutionLaneRun(previous, store, options);
|
|
3300
3707
|
}
|
|
3301
|
-
function
|
|
3708
|
+
function settleFailedReviewExecutionLaneRun(previous, store, options) {
|
|
3302
3709
|
const now = clock(options);
|
|
3303
3710
|
const result = store.transaction((tx) => {
|
|
3304
|
-
const run = tx.
|
|
3711
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
3305
3712
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
3306
|
-
throw usageError(`
|
|
3713
|
+
throw usageError(`AgentRun ${previous.id} is not a failed review AgentRun.`);
|
|
3307
3714
|
}
|
|
3308
3715
|
if (run.reviewRoundId === undefined
|
|
3309
3716
|
|| run.executionGroupId === undefined
|
|
3310
3717
|
|| run.executionLaneId === undefined
|
|
3311
3718
|
|| run.sourceExecutionGroupId !== undefined) {
|
|
3312
|
-
throw usageError(`
|
|
3719
|
+
throw usageError(`AgentRun ${run.id} is not a failed Review Producer Lane AgentRun.`);
|
|
3313
3720
|
}
|
|
3314
3721
|
const task = requireTask(tx, run.taskId);
|
|
3315
3722
|
if (task.status !== "active")
|
|
@@ -3317,80 +3724,79 @@ function settleFailedReviewExecutionLaneTurn(previous, store, options) {
|
|
|
3317
3724
|
const actor = taskActor(tx, options, task.id);
|
|
3318
3725
|
const round = tx.getReviewRound(task.id, run.reviewRoundId);
|
|
3319
3726
|
if (round === null) {
|
|
3320
|
-
throw dataError(`ReviewRound not found for
|
|
3727
|
+
throw dataError(`ReviewRound not found for AgentRun ${run.id}: ${run.reviewRoundId}.`);
|
|
3321
3728
|
}
|
|
3322
3729
|
const group = round.executionGroup;
|
|
3323
3730
|
if (group === undefined || group.id !== run.executionGroupId) {
|
|
3324
|
-
throw usageError(`
|
|
3731
|
+
throw usageError(`AgentRun ${run.id} no longer belongs to the Review ExecutionGroup.`);
|
|
3325
3732
|
}
|
|
3326
3733
|
const lane = group.lanes.find(({ id }) => id === run.executionLaneId);
|
|
3327
|
-
if (lane === undefined || lane.
|
|
3328
|
-
throw usageError(`
|
|
3734
|
+
if (lane === undefined || lane.currentRunId !== run.id || lane.roleName !== run.roleName) {
|
|
3735
|
+
throw usageError(`AgentRun ${run.id} no longer owns its Review Producer Lane.`);
|
|
3329
3736
|
}
|
|
3330
3737
|
if (lane.disposition === "failed") {
|
|
3331
3738
|
return {
|
|
3332
|
-
|
|
3739
|
+
run: run,
|
|
3333
3740
|
reviewRound: round,
|
|
3334
3741
|
changed: false,
|
|
3335
|
-
|
|
3742
|
+
mainRuns: []
|
|
3336
3743
|
};
|
|
3337
3744
|
}
|
|
3338
3745
|
if (round.status !== "running" || lane.disposition !== "open") {
|
|
3339
|
-
throw usageError(`
|
|
3746
|
+
throw usageError(`AgentRun ${run.id} cannot settle ${round.id}/${group.id}/${lane.id} from `
|
|
3340
3747
|
+ `${round.status}/${lane.disposition}.`);
|
|
3341
3748
|
}
|
|
3342
|
-
const validation =
|
|
3749
|
+
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
3343
3750
|
if (validation.disposition !== "applied") {
|
|
3344
|
-
throw usageError(`Review
|
|
3751
|
+
throw usageError(`Review AgentRun ${run.id} no longer matches its frozen Review Lane: `
|
|
3345
3752
|
+ `${validation.reason ?? "mismatch"}.`);
|
|
3346
3753
|
}
|
|
3347
|
-
if (tx.
|
|
3348
|
-
throw usageError(`Review Producer Lane still has an active
|
|
3754
|
+
if (tx.getActiveExecutionLaneRun(task.id, group.id, lane.id) !== null) {
|
|
3755
|
+
throw usageError(`Review Producer Lane still has an active AgentRun: ${group.id}/${lane.id}.`);
|
|
3349
3756
|
}
|
|
3350
3757
|
const settledGroup = updateUnifiedExecutionLane(group, lane.id, {
|
|
3351
|
-
|
|
3758
|
+
currentRunId: run.id,
|
|
3352
3759
|
disposition: "failed"
|
|
3353
3760
|
}, now);
|
|
3354
3761
|
tx.saveReviewRound(task.id, updateReviewExecutionGroup(round, settledGroup));
|
|
3355
|
-
recordTaskEvent(tx, task.id, "
|
|
3356
|
-
|
|
3762
|
+
recordTaskEvent(tx, task.id, "run.review-settled", {
|
|
3763
|
+
runId: run.id,
|
|
3357
3764
|
reviewRoundId: round.id,
|
|
3358
3765
|
executionGroupId: group.id,
|
|
3359
3766
|
executionLaneId: lane.id,
|
|
3360
3767
|
settledBy: actor,
|
|
3361
3768
|
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
|
|
3362
3769
|
}, now);
|
|
3363
|
-
const reconciliation = reconcileReviewMainTurns(tx, task.id, now);
|
|
3364
3770
|
return {
|
|
3365
|
-
|
|
3771
|
+
run: run,
|
|
3366
3772
|
reviewRound: tx.getReviewRound(task.id, round.id),
|
|
3367
3773
|
changed: true,
|
|
3368
|
-
|
|
3774
|
+
mainRuns: []
|
|
3369
3775
|
};
|
|
3370
3776
|
});
|
|
3371
|
-
for (const
|
|
3372
|
-
notifyMailbox(options.runtime, roleMailbox(
|
|
3777
|
+
for (const run of result.mainRuns) {
|
|
3778
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
3373
3779
|
}
|
|
3374
3780
|
return output(result.changed
|
|
3375
|
-
? `Settled failed Review Producer Lane from
|
|
3376
|
-
: `Failed Review Producer Lane already settled from
|
|
3377
|
-
|
|
3781
|
+
? `Settled failed Review Producer Lane from AgentRun ${result.run.id}\n`
|
|
3782
|
+
: `Failed Review Producer Lane already settled from AgentRun ${result.run.id}\n`, {
|
|
3783
|
+
run: result.run,
|
|
3378
3784
|
reviewRound: result.reviewRound,
|
|
3379
|
-
...(result.
|
|
3785
|
+
...(result.mainRuns.length === 0 ? {} : { mainRuns: result.mainRuns })
|
|
3380
3786
|
});
|
|
3381
3787
|
}
|
|
3382
|
-
function
|
|
3788
|
+
function settleFailedExecutionLaneRun(previous, store, options) {
|
|
3383
3789
|
const now = clock(options);
|
|
3384
3790
|
const result = store.transaction((tx) => {
|
|
3385
|
-
const run = tx.
|
|
3791
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
3386
3792
|
if (run === null || run.status !== "failed" || run.purpose !== "execution") {
|
|
3387
|
-
throw usageError(`
|
|
3793
|
+
throw usageError(`AgentRun ${previous.id} is not a failed execution AgentRun.`);
|
|
3388
3794
|
}
|
|
3389
3795
|
if (run.workItemId === undefined
|
|
3390
3796
|
|| run.executionGroupId === undefined
|
|
3391
3797
|
|| run.executionLaneId === undefined
|
|
3392
3798
|
|| run.sourceExecutionGroupId !== undefined) {
|
|
3393
|
-
throw usageError(`
|
|
3799
|
+
throw usageError(`AgentRun ${run.id} is not a failed WorkItem Execution Lane AgentRun.`);
|
|
3394
3800
|
}
|
|
3395
3801
|
const task = requireTask(tx, run.taskId);
|
|
3396
3802
|
if (task.status !== "active")
|
|
@@ -3398,94 +3804,88 @@ function settleFailedExecutionLaneTurn(previous, store, options) {
|
|
|
3398
3804
|
const actor = taskActor(tx, options, task.id);
|
|
3399
3805
|
const item = tx.getWorkItem(task.id, run.workItemId);
|
|
3400
3806
|
if (item === null)
|
|
3401
|
-
throw dataError(`Work item not found for
|
|
3807
|
+
throw dataError(`Work item not found for AgentRun ${run.id}: ${run.workItemId}.`);
|
|
3402
3808
|
const group = currentWorkItemExecutionGroup(item);
|
|
3403
3809
|
if (group === undefined || group.id !== run.executionGroupId) {
|
|
3404
|
-
throw usageError(`
|
|
3810
|
+
throw usageError(`AgentRun ${run.id} no longer belongs to the current ExecutionGroup.`);
|
|
3405
3811
|
}
|
|
3406
3812
|
const lane = group.lanes.find(({ id }) => id === run.executionLaneId);
|
|
3407
|
-
if (lane === undefined || lane.
|
|
3408
|
-
throw usageError(`
|
|
3813
|
+
if (lane === undefined || lane.currentRunId !== run.id) {
|
|
3814
|
+
throw usageError(`AgentRun ${run.id} no longer owns its Execution Lane.`);
|
|
3409
3815
|
}
|
|
3410
3816
|
if (lane.disposition === "failed") {
|
|
3411
3817
|
return {
|
|
3412
|
-
|
|
3818
|
+
run: run,
|
|
3413
3819
|
workItem: item,
|
|
3414
3820
|
changed: false,
|
|
3415
|
-
|
|
3821
|
+
mainRuns: []
|
|
3416
3822
|
};
|
|
3417
3823
|
}
|
|
3418
|
-
if (item.status !== "
|
|
3419
|
-
throw usageError(`
|
|
3824
|
+
if (item.status !== "open" || lane.disposition !== "open") {
|
|
3825
|
+
throw usageError(`AgentRun ${run.id} cannot settle ${item.id}/${group.id}/${lane.id} from `
|
|
3420
3826
|
+ `${item.status}/${lane.disposition}.`);
|
|
3421
3827
|
}
|
|
3422
|
-
if (tx.
|
|
3423
|
-
throw usageError(`Execution Lane still has an active
|
|
3828
|
+
if (tx.getActiveExecutionLaneRun(task.id, group.id, lane.id) !== null) {
|
|
3829
|
+
throw usageError(`Execution Lane still has an active AgentRun: ${group.id}/${lane.id}.`);
|
|
3424
3830
|
}
|
|
3425
3831
|
const settledGroup = updateWorkItemExecutionLane(group, lane.id, {
|
|
3426
|
-
|
|
3832
|
+
currentRunId: run.id,
|
|
3427
3833
|
disposition: "failed"
|
|
3428
3834
|
}, now);
|
|
3429
3835
|
const settledItem = updateWorkItemExecutionGroup(item, settledGroup, now);
|
|
3430
3836
|
tx.saveWorkItem(task.id, settledItem);
|
|
3431
|
-
recordTaskEvent(tx, task.id, "
|
|
3432
|
-
|
|
3837
|
+
recordTaskEvent(tx, task.id, "run.execution-settled", {
|
|
3838
|
+
runId: run.id,
|
|
3433
3839
|
workItemId: item.id,
|
|
3434
3840
|
executionGroupId: group.id,
|
|
3435
3841
|
executionLaneId: lane.id,
|
|
3436
3842
|
settledBy: actor,
|
|
3437
3843
|
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
|
|
3438
3844
|
}, now);
|
|
3439
|
-
const reconciliation = reconcileWorkItemMainTurns(tx, task.id, now);
|
|
3440
3845
|
return {
|
|
3441
|
-
|
|
3846
|
+
run: run,
|
|
3442
3847
|
workItem: tx.getWorkItem(task.id, item.id) ?? settledItem,
|
|
3443
3848
|
changed: true,
|
|
3444
|
-
|
|
3849
|
+
mainRuns: []
|
|
3445
3850
|
};
|
|
3446
3851
|
});
|
|
3447
|
-
for (const
|
|
3448
|
-
notifyMailbox(options.runtime, roleMailbox(
|
|
3852
|
+
for (const run of result.mainRuns) {
|
|
3853
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
3449
3854
|
}
|
|
3450
3855
|
return output(result.changed
|
|
3451
|
-
? `Settled failed Execution Lane from
|
|
3452
|
-
: `Failed Execution Lane already settled from
|
|
3453
|
-
|
|
3856
|
+
? `Settled failed Execution Lane from AgentRun ${result.run.id}\n`
|
|
3857
|
+
: `Failed Execution Lane already settled from AgentRun ${result.run.id}\n`, {
|
|
3858
|
+
run: result.run,
|
|
3454
3859
|
workItem: result.workItem,
|
|
3455
|
-
...(result.
|
|
3860
|
+
...(result.mainRuns.length === 0 ? {} : { mainRuns: result.mainRuns })
|
|
3456
3861
|
});
|
|
3457
3862
|
}
|
|
3458
|
-
function
|
|
3459
|
-
const usage = "Task turn retire usage: yui task
|
|
3863
|
+
function retireRun(args, store, options) {
|
|
3864
|
+
const usage = "Task turn retire usage: yui task run retire <task>/<run> --reason <text> [--expected-progress-at <timestamp>] [--agent-id <id>] [--adapter-id <id>] [--native-session-id <id>].";
|
|
3460
3865
|
const parsed = parseTail(args, new Set([
|
|
3461
3866
|
"--reason",
|
|
3462
3867
|
"--expected-progress-at",
|
|
3463
3868
|
"--progress-at",
|
|
3464
3869
|
"--agent-id",
|
|
3465
3870
|
"--adapter-id",
|
|
3466
|
-
"--native-session-id"
|
|
3467
|
-
"--launch-id"
|
|
3871
|
+
"--native-session-id"
|
|
3468
3872
|
]), usage);
|
|
3469
3873
|
exactPositionals(parsed.positionals, 1, usage);
|
|
3470
3874
|
const reason = requiredOption(parsed.options, "--reason");
|
|
3471
|
-
const reference = taskRecordReference(parsed.positionals[0], "
|
|
3875
|
+
const reference = taskRecordReference(parsed.positionals[0], "run", "AgentRun reference", options);
|
|
3472
3876
|
const now = clock(options);
|
|
3473
3877
|
const result = store.transaction((tx) => {
|
|
3474
3878
|
const task = requireTask(tx, reference.taskId);
|
|
3475
3879
|
assertTaskOpen(task);
|
|
3476
3880
|
const actor = taskActor(tx, options, task.id);
|
|
3477
|
-
let run = tx.
|
|
3881
|
+
let run = tx.getRun(task.id, reference.localId);
|
|
3478
3882
|
if (run === null)
|
|
3479
|
-
throw dataError(`
|
|
3883
|
+
throw dataError(`AgentRun not found: ${task.id}/${reference.localId}.`);
|
|
3480
3884
|
const events = tx.listEvents(task.id);
|
|
3481
|
-
if (isTaskRecordRetired(events, "
|
|
3482
|
-
return { task,
|
|
3885
|
+
if (isTaskRecordRetired(events, "run", run.id)) {
|
|
3886
|
+
return { task, run: run, changed: false };
|
|
3483
3887
|
}
|
|
3484
3888
|
if (run.status === "active") {
|
|
3485
|
-
if (actor === "leader"
|
|
3486
|
-
&& taskLeaderActionTurnId(tx, task.id, options.environment) === run.id) {
|
|
3487
|
-
throw usageError("A Task Leader cannot retire its own current authority Turn.", usage);
|
|
3488
|
-
}
|
|
3489
3889
|
const expectedProgressAt = requiredOption(parsed.options, parsed.options.has("--expected-progress-at")
|
|
3490
3890
|
? "--expected-progress-at"
|
|
3491
3891
|
: "--progress-at");
|
|
@@ -3495,35 +3895,30 @@ function retireTurn(args, store, options) {
|
|
|
3495
3895
|
const agentId = requiredOption(parsed.options, "--agent-id");
|
|
3496
3896
|
const adapterId = requiredOption(parsed.options, "--adapter-id");
|
|
3497
3897
|
const nativeSessionId = parsed.options.get("--native-session-id");
|
|
3498
|
-
const runtimeGenerationId = parsed.options.get("--launch-id");
|
|
3499
3898
|
const sessions = tx.getTaskRoleSessionSet(task.id, run.roleName);
|
|
3500
3899
|
const session = sessions?.sessions[run.effective.agentId];
|
|
3501
3900
|
if (session?.nativeSessionId !== undefined && nativeSessionId === undefined) {
|
|
3502
|
-
throw usageError("--native-session-id is required for this active
|
|
3503
|
-
}
|
|
3504
|
-
if (session?.nativeSessionId === undefined && runtimeGenerationId === undefined) {
|
|
3505
|
-
throw usageError("--launch-id is required for an opaque active Turn.", usage);
|
|
3901
|
+
throw usageError("--native-session-id is required for this active AgentRun.", usage);
|
|
3506
3902
|
}
|
|
3507
|
-
const terminal =
|
|
3903
|
+
const terminal = retireExactActiveRun(tx, {
|
|
3508
3904
|
taskId: task.id,
|
|
3509
3905
|
roleName: run.roleName,
|
|
3510
|
-
|
|
3906
|
+
runId: run.id,
|
|
3511
3907
|
agentId,
|
|
3512
3908
|
adapterId,
|
|
3513
3909
|
...(nativeSessionId === undefined ? {} : { nativeSessionId }),
|
|
3514
|
-
...(runtimeGenerationId === undefined ? {} : { runtimeGenerationId }),
|
|
3515
3910
|
expectedProgressAt,
|
|
3516
|
-
reason: `
|
|
3911
|
+
reason: `AgentRun retired: ${reason}`
|
|
3517
3912
|
}, now);
|
|
3518
|
-
if (terminal.disposition !== "applied" || terminal.
|
|
3913
|
+
if (terminal.disposition !== "applied" || terminal.run === null) {
|
|
3519
3914
|
throw usageError(terminal.disposition === "blocked"
|
|
3520
|
-
? `
|
|
3521
|
-
: `
|
|
3915
|
+
? `AgentRun retirement is blocked: ${run.id}/${terminal.reason ?? "unsafe"}.`
|
|
3916
|
+
: `AgentRun changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
3522
3917
|
}
|
|
3523
|
-
run = terminal.
|
|
3918
|
+
run = terminal.run;
|
|
3524
3919
|
}
|
|
3525
|
-
recordTaskEvent(tx, task.id, "
|
|
3526
|
-
|
|
3920
|
+
recordTaskEvent(tx, task.id, "run.retired", {
|
|
3921
|
+
runId: run.id,
|
|
3527
3922
|
reason,
|
|
3528
3923
|
...(parsed.options.get("--expected-progress-at") === undefined
|
|
3529
3924
|
&& parsed.options.get("--progress-at") === undefined
|
|
@@ -3535,9 +3930,6 @@ function retireTurn(args, store, options) {
|
|
|
3535
3930
|
...(parsed.options.get("--native-session-id") === undefined
|
|
3536
3931
|
? {}
|
|
3537
3932
|
: { nativeSessionId: parsed.options.get("--native-session-id") }),
|
|
3538
|
-
...(parsed.options.get("--launch-id") === undefined
|
|
3539
|
-
? {}
|
|
3540
|
-
: { runtimeGenerationId: parsed.options.get("--launch-id") }),
|
|
3541
3933
|
...(actor === "leader"
|
|
3542
3934
|
? leaderActionEventPayload(tx, task.id, options)
|
|
3543
3935
|
: { retiredBy: actor })
|
|
@@ -3545,98 +3937,98 @@ function retireTurn(args, store, options) {
|
|
|
3545
3937
|
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
3546
3938
|
eventId: tx.nextEventId(task.id),
|
|
3547
3939
|
taskId: task.id,
|
|
3548
|
-
recordKind: "
|
|
3940
|
+
recordKind: "run",
|
|
3549
3941
|
recordId: run.id,
|
|
3550
3942
|
reason,
|
|
3551
3943
|
retiredBy: actor
|
|
3552
3944
|
}, now));
|
|
3553
|
-
return { task,
|
|
3945
|
+
return { task, run: run, changed: true };
|
|
3554
3946
|
});
|
|
3555
3947
|
if (result.changed)
|
|
3556
3948
|
options.runtime?.notifyStateChanged(result.task.id);
|
|
3557
|
-
return output(`Retired
|
|
3558
|
-
|
|
3949
|
+
return output(`Retired AgentRun ${result.task.id}/${result.run.id}\n`, {
|
|
3950
|
+
run: result.run,
|
|
3559
3951
|
retired: true
|
|
3560
3952
|
});
|
|
3561
3953
|
}
|
|
3562
|
-
function
|
|
3954
|
+
function runContextCommand(args, store, options) {
|
|
3563
3955
|
const [first, ...rest] = args;
|
|
3564
3956
|
if (first === "expand") {
|
|
3565
|
-
const usage = "Task turn context expand usage: yui task
|
|
3957
|
+
const usage = "Task turn context expand usage: yui task run context expand <task>/<run> <ref-id> [--store <store>] [--mode full].";
|
|
3566
3958
|
const parsed = parseTail(rest, new Set(["--store", "--mode"]), usage);
|
|
3567
3959
|
exactPositionals(parsed.positionals, 2, usage);
|
|
3568
3960
|
const mode = parsed.options.get("--mode");
|
|
3569
3961
|
if (mode !== undefined && mode !== "full") {
|
|
3570
|
-
throw usageError("
|
|
3962
|
+
throw usageError("AgentRun Context expansion mode must be full.", usage);
|
|
3571
3963
|
}
|
|
3572
|
-
const { taskId,
|
|
3573
|
-
|
|
3574
|
-
const expanded = store.transaction((tx) =>
|
|
3964
|
+
const { taskId, runId } = parseRunContextReference(parsed.positionals[0]);
|
|
3965
|
+
authorizeRunContext(store, taskId, runId, options.environment);
|
|
3966
|
+
const expanded = store.transaction((tx) => expandRunContextRef(tx, taskId, runId, parsed.positionals[1], optionalNonEmptyOption(parsed.options, "--store")));
|
|
3575
3967
|
return output(`${JSON.stringify(expanded, null, 2)}\n`, { context: expanded });
|
|
3576
3968
|
}
|
|
3577
3969
|
if (first === "delta") {
|
|
3578
3970
|
if (rest.length !== 3 || rest[1] !== "--after") {
|
|
3579
|
-
throw usageError("Task turn context delta usage: yui task
|
|
3971
|
+
throw usageError("Task turn context delta usage: yui task run context delta <task>/<run> --after <cursor>.");
|
|
3580
3972
|
}
|
|
3581
|
-
const { taskId,
|
|
3582
|
-
|
|
3583
|
-
const delta = store.transaction((tx) => (
|
|
3973
|
+
const { taskId, runId } = parseRunContextReference(rest[0]);
|
|
3974
|
+
authorizeRunContext(store, taskId, runId, options.environment);
|
|
3975
|
+
const delta = store.transaction((tx) => (buildRunContextDelta(tx, taskId, runId, rest[2])));
|
|
3584
3976
|
return output(`${JSON.stringify(delta, null, 2)}\n`, { contextDelta: delta });
|
|
3585
3977
|
}
|
|
3586
3978
|
if (first === undefined || rest.length !== 0) {
|
|
3587
|
-
throw usageError("Task turn context usage: yui task
|
|
3979
|
+
throw usageError("Task turn context usage: yui task run context <task>/<run>.");
|
|
3588
3980
|
}
|
|
3589
|
-
const { taskId,
|
|
3590
|
-
|
|
3591
|
-
const pack = store.transaction((tx) =>
|
|
3981
|
+
const { taskId, runId } = parseRunContextReference(first);
|
|
3982
|
+
authorizeRunContext(store, taskId, runId, options.environment);
|
|
3983
|
+
const pack = store.transaction((tx) => buildRunContextPack(tx, taskId, runId));
|
|
3592
3984
|
return output(`${JSON.stringify(pack, null, 2)}\n`, { context: pack });
|
|
3593
3985
|
}
|
|
3594
|
-
function
|
|
3595
|
-
const [taskId,
|
|
3596
|
-
if (taskId === undefined || taskId.length === 0 ||
|
|
3986
|
+
function parseRunContextReference(value) {
|
|
3987
|
+
const [taskId, runId, extra] = value.split("/");
|
|
3988
|
+
if (taskId === undefined || taskId.length === 0 || runId === undefined || runId.length === 0
|
|
3597
3989
|
|| extra !== undefined) {
|
|
3598
|
-
throw usageError(`
|
|
3990
|
+
throw usageError(`AgentRun context reference is invalid: ${value}.`);
|
|
3599
3991
|
}
|
|
3600
|
-
return { taskId,
|
|
3992
|
+
return { taskId, runId };
|
|
3601
3993
|
}
|
|
3602
3994
|
/**
|
|
3603
|
-
* A
|
|
3995
|
+
* A AgentRun Context Pack is information the Role's own runtime reads in order to
|
|
3604
3996
|
* work. Authorization is therefore scope-shaped, not schedule-shaped: the
|
|
3605
|
-
* caller must be the current runtime of the Task Role that owns the
|
|
3997
|
+
* caller must be the current runtime of the Task Role that owns the AgentRun,
|
|
3606
3998
|
* proven by its per-Session caller key against durable state.
|
|
3607
3999
|
*
|
|
3608
|
-
* It deliberately does not require the
|
|
3609
|
-
* Agent whose
|
|
4000
|
+
* It deliberately does not require the AgentRun to still be the active one. An
|
|
4001
|
+
* Agent whose AgentRun has advanced must still be able to read the context it was
|
|
3610
4002
|
* given; losing read access to its own Role's history is what forces an
|
|
3611
4003
|
* otherwise healthy Agent to stop and escalate to a human.
|
|
3612
4004
|
*/
|
|
3613
|
-
function
|
|
4005
|
+
function authorizeRunContext(store, taskId, runId, environment) {
|
|
3614
4006
|
const managed = environment?.YUI_SESSION_SCOPE !== undefined
|
|
3615
4007
|
|| environment?.YUI_TASK_ID !== undefined
|
|
3616
4008
|
|| environment?.YUI_ROLE !== undefined;
|
|
3617
4009
|
if (!managed)
|
|
3618
4010
|
return;
|
|
3619
|
-
const run = store.
|
|
4011
|
+
const run = store.getRun(taskId, runId);
|
|
3620
4012
|
if (run === null) {
|
|
3621
|
-
throw usageError(`
|
|
4013
|
+
throw usageError(`AgentRun Context access is not authorized: ${taskId}/${runId}.`);
|
|
3622
4014
|
}
|
|
3623
4015
|
if (currentManagedRuntime(store, environment, taskId, run.roleName) === undefined) {
|
|
3624
|
-
throw usageError(`
|
|
4016
|
+
throw usageError(`AgentRun Context access requires the current runtime of ${taskId}/${run.roleName}.`);
|
|
3625
4017
|
}
|
|
3626
4018
|
}
|
|
3627
|
-
function
|
|
3628
|
-
const usage = "Task turn list usage: yui task
|
|
4019
|
+
function listRuns(args, store, options) {
|
|
4020
|
+
const usage = "Task turn list usage: yui task run list <task|task/work>.";
|
|
3629
4021
|
exactPositionals(args, 1, usage);
|
|
3630
4022
|
const reference = args[0];
|
|
3631
4023
|
const task = store.getTask(reference);
|
|
3632
4024
|
const item = task === null ? requireWorkItem(store, reference, options) : null;
|
|
3633
4025
|
const taskId = task?.id ?? item.taskId;
|
|
3634
|
-
const
|
|
3635
|
-
if (
|
|
3636
|
-
return "No
|
|
4026
|
+
const runs = store.listRuns(taskId).filter((run) => (item === null || run.workItemId === item.id));
|
|
4027
|
+
if (runs.length === 0)
|
|
4028
|
+
return "No AgentRuns found.\n";
|
|
3637
4029
|
const events = store.listEvents(taskId);
|
|
3638
|
-
return `${renderTable(`
|
|
3639
|
-
{ header: "
|
|
4030
|
+
return `${renderTable(`AgentRuns: ${item?.id ?? taskId}`, [
|
|
4031
|
+
{ header: "AgentRun", minWidth: 6, maxWidth: 20 },
|
|
3640
4032
|
{ header: "Role", minWidth: 4, maxWidth: 22 },
|
|
3641
4033
|
{ header: "Subject", minWidth: 7, maxWidth: 24 },
|
|
3642
4034
|
{ header: "Purpose", minWidth: 6, maxWidth: 10 },
|
|
@@ -3647,7 +4039,7 @@ function listTurns(args, store, options) {
|
|
|
3647
4039
|
{ header: "Status", minWidth: 6, maxWidth: 12 },
|
|
3648
4040
|
{ header: "History", minWidth: 7, maxWidth: 9 },
|
|
3649
4041
|
{ header: "Summary", minWidth: 8, maxWidth: 58 }
|
|
3650
|
-
],
|
|
4042
|
+
], runs.map((run) => [
|
|
3651
4043
|
run.id,
|
|
3652
4044
|
run.roleName,
|
|
3653
4045
|
run.workItemId ?? (run.reviewRoundId === undefined ? "task" : `review:${run.reviewRoundId}`),
|
|
@@ -3657,29 +4049,29 @@ function listTurns(args, store, options) {
|
|
|
3657
4049
|
run.effective.profileAccess,
|
|
3658
4050
|
run.effective.permission.strategy,
|
|
3659
4051
|
run.status,
|
|
3660
|
-
isTaskRecordRetired(events, "
|
|
4052
|
+
isTaskRecordRetired(events, "run", run.id) ? "retired" : "active",
|
|
3661
4053
|
run.result?.output ?? run.result?.diagnostic ?? "-"
|
|
3662
4054
|
]), defaultTableWidth())}\n`;
|
|
3663
4055
|
}
|
|
3664
4056
|
/**
|
|
3665
|
-
* Settles only the known bootstrap split where a failed Task-final
|
|
4057
|
+
* Settles only the known bootstrap split where a failed Task-final AgentRun
|
|
3666
4058
|
* still owns a running ReviewRound, but the committed Task heads have moved
|
|
3667
4059
|
* on. This is deliberately narrower than retry: it cannot manufacture a
|
|
3668
4060
|
* review or fail an arbitrary Round, and every identity/mailbox fence is
|
|
3669
4061
|
* checked before the old Round changes. The next normal Task completion then
|
|
3670
4062
|
* creates one fresh Round over the newer frozen Task heads.
|
|
3671
4063
|
*/
|
|
3672
|
-
function
|
|
3673
|
-
exactPositionals(args, 1, "Task turn settle usage: yui task
|
|
4064
|
+
function settleStaleFinalReviewRun(args, store, options) {
|
|
4065
|
+
exactPositionals(args, 1, "Task turn settle usage: yui task run settle <task>/<run>.");
|
|
3674
4066
|
const now = clock(options);
|
|
3675
|
-
const previous = store.transaction((tx) =>
|
|
4067
|
+
const previous = store.transaction((tx) => requireRun(tx, args[0], options));
|
|
3676
4068
|
const result = store.transaction((tx) => {
|
|
3677
|
-
const run = tx.
|
|
4069
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
3678
4070
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
3679
|
-
throw usageError(`
|
|
4071
|
+
throw usageError(`AgentRun ${previous.id} is not a failed review AgentRun.`);
|
|
3680
4072
|
}
|
|
3681
4073
|
if (run.reviewRoundId === undefined) {
|
|
3682
|
-
throw usageError(`Review
|
|
4074
|
+
throw usageError(`Review AgentRun ${run.id} has no ReviewRound.`);
|
|
3683
4075
|
}
|
|
3684
4076
|
const task = requireTask(tx, run.taskId);
|
|
3685
4077
|
if (task.status !== "active")
|
|
@@ -3687,26 +4079,26 @@ function settleStaleFinalReviewTurn(args, store, options) {
|
|
|
3687
4079
|
const actor = taskActor(tx, options, task.id);
|
|
3688
4080
|
const round = tx.getReviewRound(task.id, run.reviewRoundId);
|
|
3689
4081
|
if (round === null) {
|
|
3690
|
-
throw dataError(`ReviewRound not found for
|
|
4082
|
+
throw dataError(`ReviewRound not found for AgentRun ${run.id}: ${run.reviewRoundId}.`);
|
|
3691
4083
|
}
|
|
3692
4084
|
if ((round.scope ?? "work-item") !== "task") {
|
|
3693
|
-
throw usageError(`Review
|
|
4085
|
+
throw usageError(`Review AgentRun ${run.id} is not a Task-final review; request a new WorkItem review `
|
|
3694
4086
|
+ "for a new Candidate.");
|
|
3695
4087
|
}
|
|
3696
4088
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
3697
4089
|
if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)) {
|
|
3698
4090
|
throw usageError(`Task final-review contract does not match ReviewRound ${round.id}.`);
|
|
3699
4091
|
}
|
|
3700
|
-
// This read-only compare-and-swap fence covers the exact
|
|
4092
|
+
// This read-only compare-and-swap fence covers the exact AgentRun/Round,
|
|
3701
4093
|
// Candidate, stored Review workspace, frozen Project scope, and frozen
|
|
3702
4094
|
// Project heads before any mailbox or Round write.
|
|
3703
|
-
const validation =
|
|
4095
|
+
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
3704
4096
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
3705
|
-
throw usageError(`Review
|
|
4097
|
+
throw usageError(`Review AgentRun ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
3706
4098
|
}
|
|
3707
|
-
const
|
|
3708
|
-
if (
|
|
3709
|
-
throw usageError(`${task.id}/${round.reviewerRoleName} already has active
|
|
4099
|
+
const activeRoleRun = tx.getActiveRun(task.id, round.reviewerRoleName);
|
|
4100
|
+
if (activeRoleRun !== null) {
|
|
4101
|
+
throw usageError(`${task.id}/${round.reviewerRoleName} already has active AgentRun ${activeRoleRun.id}.`);
|
|
3710
4102
|
}
|
|
3711
4103
|
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
|
|
3712
4104
|
.filter((entry) => (entry.scope ?? "work-item") === "task"));
|
|
@@ -3724,7 +4116,7 @@ function settleStaleFinalReviewTurn(args, store, options) {
|
|
|
3724
4116
|
}
|
|
3725
4117
|
if (round.status === "failed") {
|
|
3726
4118
|
if (round.failure?.kind === "execution") {
|
|
3727
|
-
return {
|
|
4119
|
+
return { run: run, round, changed: false };
|
|
3728
4120
|
}
|
|
3729
4121
|
throw usageError(`Final ReviewRound is already terminal: ${round.id}/${round.status}.`);
|
|
3730
4122
|
}
|
|
@@ -3732,53 +4124,54 @@ function settleStaleFinalReviewTurn(args, store, options) {
|
|
|
3732
4124
|
throw usageError(`Final ReviewRound is not stranded running: ${round.id}/${round.status}.`);
|
|
3733
4125
|
}
|
|
3734
4126
|
assertReviewerAvailable(tx, task.id, round.reviewerRoleName, round);
|
|
3735
|
-
const summary = `Review
|
|
4127
|
+
const summary = `Review AgentRun ${run.id} failed before delivery; committed Task heads changed.`;
|
|
3736
4128
|
const terminal = finishReviewRound(round, "failed", now, { kind: "execution", message: summary });
|
|
3737
4129
|
tx.saveReviewRound(task.id, terminal);
|
|
3738
|
-
recordTaskEvent(tx, task.id, "
|
|
3739
|
-
|
|
4130
|
+
recordTaskEvent(tx, task.id, "run.review-stale-settled", {
|
|
4131
|
+
runId: run.id,
|
|
3740
4132
|
reviewRoundId: round.id,
|
|
3741
4133
|
previousTaskCandidate: JSON.stringify(round.taskCandidate),
|
|
3742
4134
|
currentTaskCandidate: JSON.stringify(currentTaskCandidate),
|
|
3743
4135
|
settledBy: actor,
|
|
3744
4136
|
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
|
|
3745
4137
|
}, now);
|
|
3746
|
-
return {
|
|
4138
|
+
return { run: run, round: terminal, changed: true };
|
|
3747
4139
|
});
|
|
3748
4140
|
return output(result.changed
|
|
3749
|
-
? `Settled obsolete final Review ${result.round.id} from failed
|
|
3750
|
-
: `Obsolete final Review already settled: ${result.round.id}/${result.
|
|
4141
|
+
? `Settled obsolete final Review ${result.round.id} from failed AgentRun ${result.run.id}\n`
|
|
4142
|
+
: `Obsolete final Review already settled: ${result.round.id}/${result.run.id}\n`, { reviewRound: result.round, reviewRun: result.run });
|
|
3751
4143
|
}
|
|
3752
|
-
function
|
|
3753
|
-
exactPositionals(args, 1, "Task turn retry usage: yui task
|
|
4144
|
+
function retryRun(args, store, options) {
|
|
4145
|
+
exactPositionals(args, 1, "Task turn retry usage: yui task run retry <task>/<run>.");
|
|
3754
4146
|
const now = clock(options);
|
|
3755
|
-
const previous = store.transaction((tx) =>
|
|
4147
|
+
const previous = store.transaction((tx) => requireRun(tx, args[0], options));
|
|
3756
4148
|
if (previous.purpose === "review") {
|
|
3757
4149
|
return retryFailedReviewRun(previous, store, options, now);
|
|
3758
4150
|
}
|
|
3759
4151
|
const retried = store.transaction((tx) => {
|
|
3760
4152
|
if (previous.status !== "failed") {
|
|
3761
|
-
throw usageError(`
|
|
4153
|
+
throw usageError(`AgentRun ${previous.id} is not retryable from ${previous.status}.`);
|
|
3762
4154
|
}
|
|
3763
4155
|
const task = requireTask(tx, previous.taskId);
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
4156
|
+
assertTaskExecutionEnabled(task, "retrying a AgentRun");
|
|
4157
|
+
if (!runPurposeAdmitsTaskState(previous.purpose, task)) {
|
|
4158
|
+
throw usageError(`Task does not admit ${previous.purpose} retry: ${task.id}/${task.status}.`);
|
|
4159
|
+
}
|
|
3767
4160
|
const role = requireRole(tx, task.id, previous.roleName);
|
|
3768
|
-
if (tx.
|
|
3769
|
-
throw usageError(`${task.id}/${role.name} already has an active
|
|
4161
|
+
if (tx.getActiveRun(task.id, role.name) !== null) {
|
|
4162
|
+
throw usageError(`${task.id}/${role.name} already has an active run.`);
|
|
3770
4163
|
}
|
|
3771
4164
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
3772
4165
|
const retryItem = previous.workItemId === undefined
|
|
3773
4166
|
? null
|
|
3774
4167
|
: tx.getWorkItem(task.id, previous.workItemId);
|
|
3775
4168
|
if (previous.workItemId !== undefined && retryItem === null) {
|
|
3776
|
-
throw dataError(`Work item not found for
|
|
4169
|
+
throw dataError(`Work item not found for AgentRun ${previous.id}: ${previous.workItemId}.`);
|
|
3777
4170
|
}
|
|
3778
4171
|
const retriesSynthesisMain = previous.sourceExecutionGroupId !== undefined;
|
|
3779
4172
|
if (retriesSynthesisMain
|
|
3780
4173
|
&& (previous.executionGroupId !== undefined || previous.executionLaneId !== undefined)) {
|
|
3781
|
-
throw dataError(`
|
|
4174
|
+
throw dataError(`AgentRun ${previous.id} has conflicting execution lineage.`);
|
|
3782
4175
|
}
|
|
3783
4176
|
const retryLaneBefore = previous.executionLaneId === undefined
|
|
3784
4177
|
? undefined
|
|
@@ -3796,57 +4189,54 @@ function retryTurn(args, store, options) {
|
|
|
3796
4189
|
&& retryLaneBefore !== undefined
|
|
3797
4190
|
&& retryLaneBefore.id === previous.executionLaneId
|
|
3798
4191
|
&& retryLaneBefore.disposition === "open"
|
|
3799
|
-
&& retryLaneBefore.
|
|
4192
|
+
&& retryLaneBefore.currentRunId === previous.id);
|
|
3800
4193
|
if (!exactCurrentLane) {
|
|
3801
|
-
throw usageError(`
|
|
4194
|
+
throw usageError(`AgentRun ${previous.id} no longer owns the current failed Execution Lane.`);
|
|
3802
4195
|
}
|
|
3803
4196
|
const sourceGroup = !retriesSynthesisMain || retryItem === null
|
|
3804
4197
|
? undefined
|
|
3805
4198
|
: workItemExecutionGroupById(retryItem, previous.sourceExecutionGroupId);
|
|
3806
|
-
const
|
|
3807
|
-
?
|
|
3808
|
-
&&
|
|
3809
|
-
&&
|
|
4199
|
+
const sourceMainRuns = retriesSynthesisMain
|
|
4200
|
+
? chronologicalRuns(tx.listRuns(task.id).filter((run) => (run.purpose === "execution"
|
|
4201
|
+
&& run.workItemId === previous.workItemId
|
|
4202
|
+
&& run.sourceExecutionGroupId === previous.sourceExecutionGroupId)))
|
|
3810
4203
|
: [];
|
|
3811
4204
|
const exactSourceMain = !retriesSynthesisMain || (retryItem !== null
|
|
3812
|
-
&& retryItem.status === "
|
|
4205
|
+
&& retryItem.status === "open"
|
|
3813
4206
|
&& retryItem.assignee === previous.roleName
|
|
3814
4207
|
&& sourceGroup !== undefined
|
|
3815
4208
|
&& currentRetryGroup?.id === sourceGroup.id
|
|
3816
|
-
&&
|
|
3817
|
-
&&
|
|
3818
|
-
>= MINIMUM_WORK_ITEM_SYNTHESIS_RESULTS
|
|
3819
|
-
&& sourceMainTurns.at(-1)?.id === previous.id);
|
|
4209
|
+
&& selectedWorkItemSynthesisProducers(tx, retryItem, sourceGroup, synthesisSourceRunIds(tx, previous)).length > 0
|
|
4210
|
+
&& sourceMainRuns.at(-1)?.id === previous.id);
|
|
3820
4211
|
if (!exactSourceMain) {
|
|
3821
|
-
throw usageError(`
|
|
4212
|
+
throw usageError(`AgentRun ${previous.id} no longer owns the current WorkItem main synthesis.`);
|
|
3822
4213
|
}
|
|
3823
|
-
const
|
|
4214
|
+
const directMainRuns = retryItem === null
|
|
3824
4215
|
? []
|
|
3825
|
-
:
|
|
3826
|
-
&&
|
|
3827
|
-
&&
|
|
3828
|
-
&&
|
|
3829
|
-
&&
|
|
4216
|
+
: chronologicalRuns(tx.listRuns(task.id).filter((run) => (run.purpose === "execution"
|
|
4217
|
+
&& run.workItemId === retryItem.id
|
|
4218
|
+
&& run.executionGroupId === undefined
|
|
4219
|
+
&& run.executionLaneId === undefined
|
|
4220
|
+
&& run.sourceExecutionGroupId === undefined)));
|
|
3830
4221
|
const retriesDirectMain = retryItem !== null
|
|
3831
4222
|
&& !retriesExecutionLane
|
|
3832
4223
|
&& !retriesSynthesisMain;
|
|
3833
|
-
const exactDirectMain = !retriesDirectMain || (
|
|
4224
|
+
const exactDirectMain = !retriesDirectMain || (retryItem.status === "open"
|
|
3834
4225
|
&& retryItem.assignee === previous.roleName
|
|
3835
|
-
&&
|
|
4226
|
+
&& directMainRuns.at(-1)?.id === previous.id);
|
|
3836
4227
|
if (!exactDirectMain) {
|
|
3837
|
-
throw usageError(`
|
|
4228
|
+
throw usageError(`AgentRun ${previous.id} no longer owns the current direct WorkItem execution.`);
|
|
3838
4229
|
}
|
|
3839
|
-
const groupedRunningRetry = retryItem?.status === "
|
|
4230
|
+
const groupedRunningRetry = retryItem?.status === "open"
|
|
3840
4231
|
&& retriesExecutionLane
|
|
3841
4232
|
&& exactCurrentLane;
|
|
3842
|
-
const synthesisRunningRetry = retryItem?.status === "
|
|
4233
|
+
const synthesisRunningRetry = retryItem?.status === "open"
|
|
3843
4234
|
&& retriesSynthesisMain
|
|
3844
4235
|
&& exactSourceMain;
|
|
3845
|
-
const directRunningRetry = retryItem?.status === "
|
|
4236
|
+
const directRunningRetry = retryItem?.status === "open"
|
|
3846
4237
|
&& retriesDirectMain
|
|
3847
4238
|
&& exactDirectMain;
|
|
3848
4239
|
if (retryItem !== null
|
|
3849
|
-
&& retryItem.status !== "failed"
|
|
3850
4240
|
&& !groupedRunningRetry
|
|
3851
4241
|
&& !synthesisRunningRetry
|
|
3852
4242
|
&& !directRunningRetry) {
|
|
@@ -3873,7 +4263,7 @@ function retryTurn(args, store, options) {
|
|
|
3873
4263
|
&& (retryGroup === undefined
|
|
3874
4264
|
|| retryGroup.id !== previous.executionGroupId
|
|
3875
4265
|
|| retryLane === undefined))) {
|
|
3876
|
-
throw dataError(`
|
|
4266
|
+
throw dataError(`AgentRun ${previous.id} execution lineage no longer matches its Work Item.`);
|
|
3877
4267
|
}
|
|
3878
4268
|
const retryManagedWorkspace = retryLane === undefined ? runWorkspace : previous.workspace;
|
|
3879
4269
|
if (retryLane !== undefined) {
|
|
@@ -3893,7 +4283,7 @@ function retryTurn(args, store, options) {
|
|
|
3893
4283
|
|| !isDeepStrictEqual(writableProjectIds, [...retryLane.workspace.writableProjectIds].sort())
|
|
3894
4284
|
|| storedLaneWorkspace === null
|
|
3895
4285
|
|| !isDeepStrictEqual(storedLaneWorkspace, previous.workspace)) {
|
|
3896
|
-
throw dataError(`
|
|
4286
|
+
throw dataError(`AgentRun ${previous.id} Lane workspace is missing or has drifted.`);
|
|
3897
4287
|
}
|
|
3898
4288
|
}
|
|
3899
4289
|
if (retriesSynthesisMain) {
|
|
@@ -3910,7 +4300,7 @@ function retryTurn(args, store, options) {
|
|
|
3910
4300
|
|| currentMainWorkspace === null
|
|
3911
4301
|
|| !isDeepStrictEqual(storedMainWorkspace, previous.workspace)
|
|
3912
4302
|
|| !isDeepStrictEqual(currentMainWorkspace, previous.workspace)) {
|
|
3913
|
-
throw dataError(`
|
|
4303
|
+
throw dataError(`AgentRun ${previous.id} WorkItem main workspace is missing or has drifted.`);
|
|
3914
4304
|
}
|
|
3915
4305
|
}
|
|
3916
4306
|
if (retriesDirectMain) {
|
|
@@ -3927,20 +4317,20 @@ function retryTurn(args, store, options) {
|
|
|
3927
4317
|
|| currentDirectWorkspace === null
|
|
3928
4318
|
|| !isDeepStrictEqual(storedDirectWorkspace, previous.workspace)
|
|
3929
4319
|
|| !isDeepStrictEqual(currentDirectWorkspace, previous.workspace)) {
|
|
3930
|
-
throw dataError(`
|
|
4320
|
+
throw dataError(`AgentRun ${previous.id} direct WorkItem workspace is missing or has drifted.`);
|
|
3931
4321
|
}
|
|
3932
4322
|
}
|
|
3933
4323
|
const effective = retryLane?.effective ?? resolveEffectiveLaunch({
|
|
3934
4324
|
role,
|
|
3935
|
-
purpose:
|
|
4325
|
+
purpose: previous.purpose,
|
|
3936
4326
|
...(retryManagedWorkspace === undefined ? {} : { workspace: retryManagedWorkspace }),
|
|
3937
4327
|
...(retryItem === null ? {} : { workItemWriteProjectIds: retryItem.writeProjectIds })
|
|
3938
4328
|
});
|
|
3939
|
-
const
|
|
4329
|
+
const runId = tx.nextRunId(task.id);
|
|
3940
4330
|
const runningGroup = retryGroup === undefined || retryLane === undefined
|
|
3941
4331
|
? undefined
|
|
3942
4332
|
: updateWorkItemExecutionLane(retryGroup, retryLane.id, {
|
|
3943
|
-
|
|
4333
|
+
currentRunId: runId
|
|
3944
4334
|
}, now);
|
|
3945
4335
|
// Restart the bound lane and reopen the failed WorkItem as two ordered
|
|
3946
4336
|
// single-step record revisions, matching the dispatch path. Folding both
|
|
@@ -3949,11 +4339,7 @@ function retryTurn(args, store, options) {
|
|
|
3949
4339
|
const laneRestartedItem = retryItem === null || runningGroup === undefined
|
|
3950
4340
|
? retryItem
|
|
3951
4341
|
: updateWorkItemExecutionGroup(retryItem, runningGroup, now);
|
|
3952
|
-
const retriedItemWithGroup = laneRestartedItem
|
|
3953
|
-
? null
|
|
3954
|
-
: laneRestartedItem.status === "failed"
|
|
3955
|
-
? retryFailedWorkItem(laneRestartedItem, now)
|
|
3956
|
-
: laneRestartedItem;
|
|
4342
|
+
const retriedItemWithGroup = laneRestartedItem;
|
|
3957
4343
|
if (retriedItemWithGroup !== null) {
|
|
3958
4344
|
if (laneRestartedItem !== null && laneRestartedItem !== retryItem) {
|
|
3959
4345
|
tx.saveWorkItem(task.id, laneRestartedItem);
|
|
@@ -3965,13 +4351,13 @@ function retryTurn(args, store, options) {
|
|
|
3965
4351
|
const input = retriesSynthesisMain
|
|
3966
4352
|
? previous.inputs[0].input
|
|
3967
4353
|
: (() => {
|
|
3968
|
-
const retrySnapshot =
|
|
4354
|
+
const retrySnapshot = freezeRunContextSnapshot(tx, {
|
|
3969
4355
|
taskId: task.id,
|
|
3970
4356
|
roleName: role.name,
|
|
3971
|
-
purpose:
|
|
4357
|
+
purpose: previous.purpose,
|
|
3972
4358
|
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId })
|
|
3973
4359
|
}, now, "controller", retryGroup?.assignment.contextSnapshotRef);
|
|
3974
|
-
return
|
|
4360
|
+
return createRunInput({
|
|
3975
4361
|
source: {
|
|
3976
4362
|
type: "yui",
|
|
3977
4363
|
channel: previous.workItemId === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
@@ -3983,7 +4369,8 @@ function retryTurn(args, store, options) {
|
|
|
3983
4369
|
deltaRefIds: contextSnapshotDeltaRefIds(tx, retrySnapshot)
|
|
3984
4370
|
});
|
|
3985
4371
|
})();
|
|
3986
|
-
const created =
|
|
4372
|
+
const created = createRun(runId, task.id, role.name, roleAgentSessionResumeMode(sessions, effective.agentId, effective), input, now, {
|
|
4373
|
+
purpose: previous.purpose,
|
|
3987
4374
|
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId }),
|
|
3988
4375
|
...(runningGroup === undefined ? {} : {
|
|
3989
4376
|
executionGroupId: runningGroup.id,
|
|
@@ -3995,8 +4382,8 @@ function retryTurn(args, store, options) {
|
|
|
3995
4382
|
...(retryManagedWorkspace === undefined ? {} : { workspace: retryManagedWorkspace }),
|
|
3996
4383
|
effective
|
|
3997
4384
|
});
|
|
3998
|
-
tx.
|
|
3999
|
-
tx.
|
|
4385
|
+
tx.saveRun(created);
|
|
4386
|
+
tx.saveActiveRun(created);
|
|
4000
4387
|
if (previous.workItemId !== undefined && retriedItemWithGroup !== null) {
|
|
4001
4388
|
const item = retriedItemWithGroup;
|
|
4002
4389
|
const workspace = tx.getWorkItemWorkspace(task.id, item.id);
|
|
@@ -4006,21 +4393,21 @@ function retryTurn(args, store, options) {
|
|
|
4006
4393
|
+ `cannot retry ${item.id}.`);
|
|
4007
4394
|
}
|
|
4008
4395
|
}
|
|
4009
|
-
|
|
4396
|
+
enqueueRoleRunDispatch(tx, {
|
|
4010
4397
|
taskId: task.id,
|
|
4011
4398
|
roleName: role.name,
|
|
4012
|
-
|
|
4399
|
+
runId: created.id,
|
|
4013
4400
|
reason: "turn-retried",
|
|
4014
4401
|
occurredAt: now
|
|
4015
4402
|
});
|
|
4016
|
-
recordTaskEvent(tx, task.id, "
|
|
4017
|
-
...
|
|
4018
|
-
|
|
4403
|
+
recordTaskEvent(tx, task.id, "run.retried", {
|
|
4404
|
+
...runLaunchEventPayload(created),
|
|
4405
|
+
previousRunId: previous.id
|
|
4019
4406
|
}, now);
|
|
4020
|
-
return { kind: "
|
|
4407
|
+
return { kind: "run", run: created };
|
|
4021
4408
|
});
|
|
4022
|
-
notifyMailbox(options.runtime, roleMailbox(retried.
|
|
4023
|
-
return output(`Retry queued as ${retried.
|
|
4409
|
+
notifyMailbox(options.runtime, roleMailbox(retried.run.taskId, retried.run.roleName), retried.run.taskId);
|
|
4410
|
+
return output(`Retry queued as ${retried.run.id} for ${retried.run.taskId}/${retried.run.roleName}\n`);
|
|
4024
4411
|
}
|
|
4025
4412
|
function actualTaskReviewCandidateForMutation(store, task, options) {
|
|
4026
4413
|
if (options.actualTaskReviewCandidate === undefined) {
|
|
@@ -4063,7 +4450,7 @@ function taskReviewProducerCollision(provenance, reviewerRoleName) {
|
|
|
4063
4450
|
*
|
|
4064
4451
|
* When `expected` is supplied this is also the final dispatch compare-and-swap
|
|
4065
4452
|
* fence: every bound Project must still point at the exact frozen physical
|
|
4066
|
-
* head. Drift fails closed before a Reviewer
|
|
4453
|
+
* head. Drift fails closed before a Reviewer AgentRun is created.
|
|
4067
4454
|
*/
|
|
4068
4455
|
function taskReviewProvenance(store, task, options, expected) {
|
|
4069
4456
|
const candidate = actualTaskReviewCandidateForMutation(store, task, options);
|
|
@@ -4118,13 +4505,13 @@ function taskReviewProvenance(store, task, options, expected) {
|
|
|
4118
4505
|
recordProducer(LEADER_ROLE, item.id);
|
|
4119
4506
|
continue;
|
|
4120
4507
|
}
|
|
4121
|
-
const sourceRun = store.
|
|
4508
|
+
const sourceRun = store.getRun(task.id, sourceCandidate.source.runId);
|
|
4122
4509
|
if (sourceRun === null
|
|
4123
4510
|
|| sourceRun.workItemId !== item.id
|
|
4124
4511
|
|| sourceRun.purpose !== "execution"
|
|
4125
4512
|
|| sourceRun.status !== "completed") {
|
|
4126
|
-
throw dataError(`Committed producer Candidate
|
|
4127
|
-
+ `${item.id}/${sourceCandidate.source.
|
|
4513
|
+
throw dataError(`Committed producer Candidate AgentRun is unavailable: `
|
|
4514
|
+
+ `${item.id}/${sourceCandidate.source.runId}.`);
|
|
4128
4515
|
}
|
|
4129
4516
|
recordProducer(sourceRun.roleName, item.id);
|
|
4130
4517
|
if (sourceRun.sourceExecutionGroupId !== undefined) {
|
|
@@ -4133,7 +4520,7 @@ function taskReviewProvenance(store, task, options, expected) {
|
|
|
4133
4520
|
throw dataError(`Committed producer ExecutionGroup is unavailable: `
|
|
4134
4521
|
+ `${item.id}/${sourceRun.sourceExecutionGroupId}.`);
|
|
4135
4522
|
}
|
|
4136
|
-
for (const producer of
|
|
4523
|
+
for (const producer of selectedWorkItemSynthesisProducers(store, item, executionGroup, synthesisSourceRunIds(store, sourceRun))) {
|
|
4137
4524
|
recordProducer(producer.roleName, item.id);
|
|
4138
4525
|
}
|
|
4139
4526
|
}
|
|
@@ -4182,7 +4569,7 @@ function queueTaskReviewRound(store, task, config, taskCandidate, options, now,
|
|
|
4182
4569
|
const availability = projectReviewerAvailability(store, task.id, config.roleName);
|
|
4183
4570
|
if (availability.kind === "busy") {
|
|
4184
4571
|
throw usageError(`Reviewer ${config.roleName} is busy (${availability.phase}`
|
|
4185
|
-
+ `${availability.
|
|
4572
|
+
+ `${availability.activeRunId === undefined ? "" : `; AgentRun ${availability.activeRunId}`}); `
|
|
4186
4573
|
+ `retry after ${availability.retryAfterSeconds}s.`);
|
|
4187
4574
|
}
|
|
4188
4575
|
let reviewer = store.getRole(task.id, config.roleName);
|
|
@@ -4251,16 +4638,16 @@ function resumablePendingFinalTaskReview(store, task, round, config, taskCandida
|
|
|
4251
4638
|
|| round.reviewerRoleName !== taskFinalContract.reviewerRoleName) {
|
|
4252
4639
|
throw usageError(`Pending final ReviewRound Reviewer identity changed: ${round.id}.`);
|
|
4253
4640
|
}
|
|
4254
|
-
if (round.
|
|
4255
|
-
throw usageError(`Pending final ReviewRound already records Reviewer
|
|
4641
|
+
if (round.reviewerRunId !== undefined) {
|
|
4642
|
+
throw usageError(`Pending final ReviewRound already records Reviewer AgentRun ${round.reviewerRunId}: ${round.id}.`);
|
|
4256
4643
|
}
|
|
4257
4644
|
assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id), round.id, round.reviewerRoleName);
|
|
4258
4645
|
const reviewer = store.getRole(task.id, round.reviewerRoleName);
|
|
4259
4646
|
if (reviewer === null || reviewer.name !== round.reviewerRoleName) {
|
|
4260
4647
|
throw usageError(`Pending final ReviewRound Reviewer identity changed: ${round.id}.`);
|
|
4261
4648
|
}
|
|
4262
|
-
if (store.
|
|
4263
|
-
throw usageError(`Reviewer Role already has an active
|
|
4649
|
+
if (store.getActiveRun(task.id, reviewer.name) !== null) {
|
|
4650
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
4264
4651
|
}
|
|
4265
4652
|
assertPendingFinalReviewWorkspaceEvidence(store, task, round);
|
|
4266
4653
|
return round;
|
|
@@ -4296,7 +4683,7 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
4296
4683
|
}
|
|
4297
4684
|
}
|
|
4298
4685
|
/**
|
|
4299
|
-
* Task-control retry of an exact failed review
|
|
4686
|
+
* Task-control retry of an exact failed review AgentRun. The old failed AgentRun
|
|
4300
4687
|
* remains the attempt trail, while the ReviewRound is reset to pending under
|
|
4301
4688
|
* its existing identity. Every identity and frozen-head fence is
|
|
4302
4689
|
* checked inside one transaction so a partial fail-old-without-reset state can
|
|
@@ -4304,12 +4691,12 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
4304
4691
|
*/
|
|
4305
4692
|
function retryFailedReviewRun(previous, store, options, now) {
|
|
4306
4693
|
const result = store.transaction((tx) => {
|
|
4307
|
-
const run = tx.
|
|
4694
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
4308
4695
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
4309
|
-
throw usageError(`
|
|
4696
|
+
throw usageError(`AgentRun ${previous.id} is not a failed review AgentRun.`);
|
|
4310
4697
|
}
|
|
4311
4698
|
if (run.reviewRoundId === undefined) {
|
|
4312
|
-
throw usageError(`Review
|
|
4699
|
+
throw usageError(`Review AgentRun ${run.id} has no ReviewRound.`);
|
|
4313
4700
|
}
|
|
4314
4701
|
const task = requireTask(tx, run.taskId);
|
|
4315
4702
|
if (task.status !== "active")
|
|
@@ -4325,16 +4712,16 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4325
4712
|
: round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
|
|
4326
4713
|
if (run.executionGroupId !== undefined && (round.executionGroup?.id !== run.executionGroupId
|
|
4327
4714
|
|| retryLane === undefined
|
|
4328
|
-
|| retryLane.
|
|
4715
|
+
|| retryLane.currentRunId !== run.id
|
|
4329
4716
|
|| retryLane.roleName !== run.roleName)) {
|
|
4330
|
-
throw usageError(`Review
|
|
4717
|
+
throw usageError(`Review AgentRun ${run.id} no longer owns its exact Review Lane attempt.`);
|
|
4331
4718
|
}
|
|
4332
4719
|
const panelGroup = round.executionGroup !== undefined;
|
|
4333
4720
|
const runningPanelLaneRetry = round.status === "running"
|
|
4334
4721
|
&& panelGroup
|
|
4335
4722
|
&& retryLane?.disposition === "open";
|
|
4336
4723
|
if (round.status === "running" && panelGroup && !runningPanelLaneRetry) {
|
|
4337
|
-
throw usageError(`Review
|
|
4724
|
+
throw usageError(`Review AgentRun ${run.id} is not the current failed Lane attempt in running Round ${round.id}.`);
|
|
4338
4725
|
}
|
|
4339
4726
|
const retryReviewerRoleName = retryLane?.roleName ?? round.reviewerRoleName;
|
|
4340
4727
|
if (round.status !== "failed"
|
|
@@ -4370,10 +4757,10 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4370
4757
|
throw dataError(`WorkItem ReviewRound has no Candidate anchor: ${round.id}.`);
|
|
4371
4758
|
}
|
|
4372
4759
|
if (run.workItemId !== round.workItemId) {
|
|
4373
|
-
throw usageError(`Review
|
|
4760
|
+
throw usageError(`Review AgentRun ${run.id} does not match WorkItem ${round.workItemId}.`);
|
|
4374
4761
|
}
|
|
4375
4762
|
const item = tx.getWorkItem(task.id, round.workItemId);
|
|
4376
|
-
if (item === null || item.status !== "
|
|
4763
|
+
if (item === null || (item.status !== "open" || item.candidates.length === 0)) {
|
|
4377
4764
|
throw usageError(`WorkItem ReviewRound ${round.id} no longer has an awaiting Candidate.`);
|
|
4378
4765
|
}
|
|
4379
4766
|
const candidate = currentWorkItemCandidate(item);
|
|
@@ -4412,20 +4799,20 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4412
4799
|
throw usageError(`Reviewer already has an active review round for this candidate: ${activeRound.id}.`);
|
|
4413
4800
|
}
|
|
4414
4801
|
const reviewer = requireRole(tx, task.id, retryReviewerRoleName);
|
|
4415
|
-
const activePointer = tx.
|
|
4802
|
+
const activePointer = tx.getActiveRun(task.id, reviewer.name);
|
|
4416
4803
|
// Issue 06: a completed same-Round retry is a no-write idempotent result.
|
|
4417
4804
|
if (round.status === "completed") {
|
|
4418
4805
|
assertReviewerAvailable(tx, task.id, reviewer.name);
|
|
4419
4806
|
return { round, previousRun: run, created: false };
|
|
4420
4807
|
}
|
|
4421
4808
|
// Issue 06: a running same Round is reusable only with its exact active
|
|
4422
|
-
//
|
|
4809
|
+
// AgentRun. A stranded AgentRun (no active pointer) falls through and resets the
|
|
4423
4810
|
// Round after the identity fences below.
|
|
4424
4811
|
if (round.status === "running" && !runningPanelLaneRetry) {
|
|
4425
|
-
const
|
|
4426
|
-
const activeMatches =
|
|
4812
|
+
const reviewerRunId = round.reviewerRunId;
|
|
4813
|
+
const activeMatches = reviewerRunId !== undefined
|
|
4427
4814
|
&& activePointer !== null
|
|
4428
|
-
&& activePointer.id ===
|
|
4815
|
+
&& activePointer.id === reviewerRunId
|
|
4429
4816
|
&& activePointer.status === "active";
|
|
4430
4817
|
if (activePointer !== null && !activeMatches) {
|
|
4431
4818
|
throw usageError(`Existing running ReviewRound ${round.id} lacks its exact active Reviewer execution.`);
|
|
@@ -4438,24 +4825,65 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4438
4825
|
// Issue 06: an already-pending Round is the idempotent retry result.
|
|
4439
4826
|
if (round.status === "pending") {
|
|
4440
4827
|
if (activePointer !== null) {
|
|
4441
|
-
throw usageError(`Reviewer Role already has an active
|
|
4828
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
4442
4829
|
}
|
|
4443
4830
|
assertReviewerAvailable(tx, task.id, reviewer.name, round);
|
|
4444
4831
|
return { round, previousRun: run, created: false };
|
|
4445
4832
|
}
|
|
4446
4833
|
if (activePointer !== null) {
|
|
4447
|
-
throw usageError(`Reviewer Role already has an active
|
|
4834
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
4448
4835
|
}
|
|
4449
4836
|
assertReviewerAvailable(tx, task.id, reviewer.name, round);
|
|
4450
|
-
const validation =
|
|
4837
|
+
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
4451
4838
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
4452
|
-
throw usageError(`Review
|
|
4839
|
+
throw usageError(`Review AgentRun ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
4840
|
+
}
|
|
4841
|
+
if (run.sourceExecutionGroupId !== undefined) {
|
|
4842
|
+
assertTaskExecutionEnabled(task, "retrying Review synthesis");
|
|
4843
|
+
const group = round.executionGroup;
|
|
4844
|
+
if (round.status !== "failed" || round.reviewerRunId !== run.id
|
|
4845
|
+
|| group?.id !== run.sourceExecutionGroupId
|
|
4846
|
+
|| run.workspace === undefined) {
|
|
4847
|
+
throw usageError(`Review AgentRun ${run.id} no longer owns the current main synthesis.`);
|
|
4848
|
+
}
|
|
4849
|
+
selectedReviewSynthesisProducers(tx, round, group, synthesisSourceRunIds(tx, run));
|
|
4850
|
+
const effective = resolveEffectiveLaunch({
|
|
4851
|
+
role: reviewer,
|
|
4852
|
+
purpose: "review",
|
|
4853
|
+
workspace: run.workspace,
|
|
4854
|
+
reviewRoundId: round.id,
|
|
4855
|
+
reviewBaseCommit: round.reviewBaseCommit
|
|
4856
|
+
});
|
|
4857
|
+
const created = createRun(tx.nextRunId(task.id), task.id, reviewer.name, roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(task.id, reviewer.name), effective.agentId, effective), run.inputs[0].input, now, {
|
|
4858
|
+
purpose: "review",
|
|
4859
|
+
...(run.workItemId === undefined ? {} : { workItemId: run.workItemId }),
|
|
4860
|
+
reviewRoundId: round.id,
|
|
4861
|
+
sourceExecutionGroupId: group.id,
|
|
4862
|
+
workspace: run.workspace,
|
|
4863
|
+
effective
|
|
4864
|
+
});
|
|
4865
|
+
const restarted = startReviewRound(retryReviewRound(round, requestedBy, now), created.id);
|
|
4866
|
+
tx.saveReviewRound(task.id, restarted);
|
|
4867
|
+
tx.saveRun(created);
|
|
4868
|
+
tx.saveActiveRun(created);
|
|
4869
|
+
enqueueRoleRunDispatch(tx, {
|
|
4870
|
+
taskId: task.id,
|
|
4871
|
+
roleName: reviewer.name,
|
|
4872
|
+
runId: created.id,
|
|
4873
|
+
reason: "turn-retried",
|
|
4874
|
+
occurredAt: now
|
|
4875
|
+
});
|
|
4876
|
+
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
4877
|
+
...runLaunchEventPayload(created),
|
|
4878
|
+
previousRunId: run.id
|
|
4879
|
+
}, now);
|
|
4880
|
+
return { round: restarted, previousRun: run, created: true, run: created };
|
|
4453
4881
|
}
|
|
4454
4882
|
if (runningPanelLaneRetry) {
|
|
4455
4883
|
const resetRound = retryRunningReviewExecutionLane(round, retryLane.id, run.id);
|
|
4456
4884
|
tx.saveReviewRound(task.id, resetRound);
|
|
4457
|
-
recordTaskEvent(tx, task.id, "
|
|
4458
|
-
|
|
4885
|
+
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
4886
|
+
runId: run.id,
|
|
4459
4887
|
reviewRoundId: round.id,
|
|
4460
4888
|
executionLaneId: retryLane.id
|
|
4461
4889
|
}, now);
|
|
@@ -4465,7 +4893,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4465
4893
|
// fence has passed. The outer transaction rolls back if Round creation fails.
|
|
4466
4894
|
let roundToReset = round;
|
|
4467
4895
|
if (round.status !== "failed") {
|
|
4468
|
-
const summary = `Review
|
|
4896
|
+
const summary = `Review AgentRun ${run.id} failed before delivery.`;
|
|
4469
4897
|
roundToReset = finishReviewRound(round, "failed", now, { kind: "execution", message: summary });
|
|
4470
4898
|
tx.saveReviewRound(task.id, roundToReset);
|
|
4471
4899
|
}
|
|
@@ -4474,41 +4902,44 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4474
4902
|
// stable across execution-attempt failures.
|
|
4475
4903
|
const resetRound = retryReviewRound(roundToReset, requestedBy, now);
|
|
4476
4904
|
tx.saveReviewRound(task.id, resetRound);
|
|
4477
|
-
recordTaskEvent(tx, task.id, "
|
|
4478
|
-
|
|
4905
|
+
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
4906
|
+
runId: run.id,
|
|
4479
4907
|
reviewRoundId: round.id
|
|
4480
4908
|
}, now);
|
|
4481
4909
|
return { round: resetRound, previousRun: run, created: true };
|
|
4482
4910
|
});
|
|
4911
|
+
if ("run" in result && result.run !== undefined) {
|
|
4912
|
+
notifyMailbox(options.runtime, roleMailbox(result.run.taskId, result.run.roleName), result.run.taskId);
|
|
4913
|
+
}
|
|
4483
4914
|
return output(result.created
|
|
4484
4915
|
? `Review retry requested as ${result.round.id}\n`
|
|
4485
|
-
: `Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round });
|
|
4916
|
+
: `Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round, ...("run" in result ? { run: result.run } : {}) });
|
|
4486
4917
|
}
|
|
4487
|
-
/**
|
|
4488
|
-
function
|
|
4489
|
-
const usage = "Task turn show usage: yui task
|
|
4918
|
+
/** AgentRun details are retained audit evidence; continuation uses durable Task state. */
|
|
4919
|
+
function showRun(args, store, options) {
|
|
4920
|
+
const usage = "Task turn show usage: yui task run show <task>/<run> [--json].";
|
|
4490
4921
|
const asJson = args.includes("--json");
|
|
4491
4922
|
const positionals = args.filter((arg) => arg !== "--json");
|
|
4492
4923
|
exactPositionals(positionals, 1, usage);
|
|
4493
4924
|
const data = store.transaction((tx) => {
|
|
4494
|
-
const run =
|
|
4925
|
+
const run = requireRun(tx, positionals[0], options);
|
|
4495
4926
|
const retirement = tx.listEvents(run.taskId)
|
|
4496
4927
|
.map(taskRecordRetirement)
|
|
4497
|
-
.find((entry) => entry?.recordKind === "
|
|
4498
|
-
return {
|
|
4928
|
+
.find((entry) => entry?.recordKind === "run" && entry.recordId === run.id) ?? null;
|
|
4929
|
+
return { run: run, retirement, execution: runExecutionObservation(run, tx.getTaskRoleSessionSet(run.taskId, run.roleName)?.providerBinding, tx.listEvents(run.taskId)) };
|
|
4499
4930
|
});
|
|
4500
4931
|
if (asJson) {
|
|
4501
4932
|
return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
|
|
4502
4933
|
}
|
|
4503
4934
|
return {
|
|
4504
4935
|
kind: "output",
|
|
4505
|
-
output:
|
|
4936
|
+
output: `Delivery observation: ${data.execution.delivery}\n` + renderRunShow(data.run, data.retirement),
|
|
4506
4937
|
data
|
|
4507
4938
|
};
|
|
4508
4939
|
}
|
|
4509
|
-
function
|
|
4940
|
+
function renderRunShow(run, retirement) {
|
|
4510
4941
|
const lines = [
|
|
4511
|
-
`
|
|
4942
|
+
`AgentRun: ${run.id}`,
|
|
4512
4943
|
`Task: ${run.taskId}`,
|
|
4513
4944
|
`Role: ${run.roleName}`,
|
|
4514
4945
|
`Purpose: ${run.purpose}`,
|
|
@@ -4531,40 +4962,40 @@ function renderTurnShow(run, retirement) {
|
|
|
4531
4962
|
return `${lines.join("\n")}\n`;
|
|
4532
4963
|
}
|
|
4533
4964
|
/**
|
|
4534
|
-
* Records a structured progress checkpoint for an active
|
|
4535
|
-
*
|
|
4536
|
-
* a healthy but long-running
|
|
4537
|
-
* collaboration-narrative noise. It never completes, mutates the
|
|
4965
|
+
* Records a structured progress checkpoint for an active AgentRun. This is a durable
|
|
4966
|
+
* AgentRun fact, not a Task Message: it advances the AgentRun's durable-progress clock so
|
|
4967
|
+
* a healthy but long-running AgentRun keeps proving it is alive without adding
|
|
4968
|
+
* collaboration-narrative noise. It never completes, mutates the AgentRun, or wakes the
|
|
4538
4969
|
* Leader.
|
|
4539
4970
|
*/
|
|
4540
|
-
function
|
|
4541
|
-
const usage = "Task turn checkpoint usage: yui task
|
|
4971
|
+
function checkpointRun(args, store, options) {
|
|
4972
|
+
const usage = "Task turn checkpoint usage: yui task run checkpoint <run> (--note <text>|--note-file <path|->).";
|
|
4542
4973
|
const parsed = parseTail(args, new Set(["--note", "--note-file"]), usage);
|
|
4543
4974
|
exactPositionals(parsed.positionals, 1, usage);
|
|
4544
4975
|
const note = readCommandText(parsed.options.get("--note"), parsed.options.get("--note-file"), "--note", usage);
|
|
4545
4976
|
const now = clock(options);
|
|
4546
4977
|
const event = store.transaction((tx) => {
|
|
4547
|
-
const run =
|
|
4978
|
+
const run = requireRun(tx, parsed.positionals[0], options);
|
|
4548
4979
|
if (run.status !== "active") {
|
|
4549
|
-
throw usageError(`
|
|
4980
|
+
throw usageError(`AgentRun ${run.id} is already terminal: ${run.status}.`);
|
|
4550
4981
|
}
|
|
4551
4982
|
const task = requireTask(tx, run.taskId);
|
|
4552
4983
|
if (task.status !== "active")
|
|
4553
|
-
throw usageError(inactiveTaskMessage(task, "checkpointing a
|
|
4554
|
-
const pointer =
|
|
4984
|
+
throw usageError(inactiveTaskMessage(task, "checkpointing a AgentRun"));
|
|
4985
|
+
const pointer = activeRunPointer(tx, run);
|
|
4555
4986
|
if (pointer?.id !== run.id) {
|
|
4556
|
-
throw usageError(`
|
|
4987
|
+
throw usageError(`AgentRun is not active for ${task.id}/${run.roleName}: ${run.id}.`);
|
|
4557
4988
|
}
|
|
4558
4989
|
const events = tx.listEvents(task.id);
|
|
4559
|
-
const recovered =
|
|
4560
|
-
const progress = recordTaskEventRecord(tx, task.id,
|
|
4561
|
-
|
|
4990
|
+
const recovered = isRoleRunStalled(events, run.id);
|
|
4991
|
+
const progress = recordTaskEventRecord(tx, task.id, RUN_PROGRESS_EVENT, {
|
|
4992
|
+
runId: run.id,
|
|
4562
4993
|
note: truncateEventNote(note),
|
|
4563
4994
|
...(run.workItemId === undefined ? {} : { workItemId: run.workItemId })
|
|
4564
4995
|
}, now);
|
|
4565
4996
|
if (recovered) {
|
|
4566
|
-
recordTaskEventRecord(tx, task.id,
|
|
4567
|
-
|
|
4997
|
+
recordTaskEventRecord(tx, task.id, RUN_RECOVERED_EVENT, {
|
|
4998
|
+
runId: run.id,
|
|
4568
4999
|
roleName: run.roleName,
|
|
4569
5000
|
progressAt: now.toISOString(),
|
|
4570
5001
|
kind: "checkpoint"
|
|
@@ -4612,12 +5043,12 @@ export function queueReviewRound(store, item, config, requestedBy, now, requeste
|
|
|
4612
5043
|
reviewer = createTaskRole(store, task, roleName, undefined, now, roleName);
|
|
4613
5044
|
store.saveRole(task.id, reviewer);
|
|
4614
5045
|
}
|
|
4615
|
-
if (store.
|
|
5046
|
+
if (store.getActiveRun(item.taskId, reviewer.name) !== null) {
|
|
4616
5047
|
const pending = createPending();
|
|
4617
5048
|
store.saveReviewRound(item.taskId, pending);
|
|
4618
5049
|
const failed = finishReviewRound(pending, "failed", now, {
|
|
4619
5050
|
kind: "dispatch",
|
|
4620
|
-
message: `Reviewer Role already has an active
|
|
5051
|
+
message: `Reviewer Role already has an active AgentRun: ${reviewer.name}.`
|
|
4621
5052
|
});
|
|
4622
5053
|
store.saveReviewRound(item.taskId, failed);
|
|
4623
5054
|
return { round: failed };
|
|
@@ -4757,8 +5188,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4757
5188
|
&& round.reviewerRoleName !== taskFinalContract.reviewerRoleName) {
|
|
4758
5189
|
throw new TaskFinalReviewDispatchDriftError(`Task final-review Reviewer identity does not match ReviewRound ${round.id}.`);
|
|
4759
5190
|
}
|
|
4760
|
-
if (round.status === "pending" && round.
|
|
4761
|
-
throw new TaskFinalReviewDispatchDriftError(`Pending final ReviewRound already records Reviewer
|
|
5191
|
+
if (round.status === "pending" && round.reviewerRunId !== undefined) {
|
|
5192
|
+
throw new TaskFinalReviewDispatchDriftError(`Pending final ReviewRound already records Reviewer AgentRun ${round.reviewerRunId}: `
|
|
4762
5193
|
+ `${round.id}.`);
|
|
4763
5194
|
}
|
|
4764
5195
|
let currentTaskCandidate;
|
|
@@ -4810,8 +5241,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4810
5241
|
&& (requestedReviewers.has(entry.reviewerRoleName)
|
|
4811
5242
|
|| entry.executionGroup?.lanes.some(({ roleName }) => (requestedReviewers.has(roleName))) === true)
|
|
4812
5243
|
&& !(entry.status === "running"
|
|
4813
|
-
&& entry.
|
|
4814
|
-
&& tx.
|
|
5244
|
+
&& entry.reviewerRunId !== undefined
|
|
5245
|
+
&& tx.getRun(task.id, entry.reviewerRunId)?.status === "failed")));
|
|
4815
5246
|
if (conflicting !== undefined) {
|
|
4816
5247
|
throw new TaskFinalReviewDispatchDriftError(`Another active Task-final ReviewRound already exists: ${conflicting.id}/${conflicting.reviewerRoleName}.`);
|
|
4817
5248
|
}
|
|
@@ -4838,8 +5269,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4838
5269
|
}
|
|
4839
5270
|
const candidateLabel = taskScope
|
|
4840
5271
|
? "frozen Task candidate"
|
|
4841
|
-
: candidate.source.type === "
|
|
4842
|
-
? `candidate
|
|
5272
|
+
: candidate.source.type === "run"
|
|
5273
|
+
? `candidate AgentRun ${candidate.source.runId}`
|
|
4843
5274
|
: `revision ${candidate.workItemRevision}`;
|
|
4844
5275
|
const frozenHeads = taskScope
|
|
4845
5276
|
? round.taskCandidate.projects
|
|
@@ -4854,7 +5285,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4854
5285
|
}
|
|
4855
5286
|
const diffByProject = options.deltaRecheckDiff;
|
|
4856
5287
|
if (diffByProject === undefined) {
|
|
4857
|
-
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck diff is missing for ${round.id}; the CLI preflight did not
|
|
5288
|
+
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck diff is missing for ${round.id}; the CLI preflight did not run.`);
|
|
4858
5289
|
}
|
|
4859
5290
|
try {
|
|
4860
5291
|
verifyDeltaRecheckDiff(round.deltaRecheck, diffByProject);
|
|
@@ -4898,17 +5329,17 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4898
5329
|
"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.",
|
|
4899
5330
|
"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.",
|
|
4900
5331
|
"End the Provider turn with one complete original result in clear Markdown or JSON. Recommended sections are conclusion, material findings, checks actually run, uncertainty, and next actions. Yui preserves the text verbatim and does not parse or validate those sections.",
|
|
4901
|
-
"Report reviewBaseCommit, exact checks/results, material findings, and uncertainty. This
|
|
5332
|
+
"Report reviewBaseCommit, exact checks/results, material findings, and uncertainty. This AgentRun result completes only the Round and creates no Candidate or ChangeSet.",
|
|
4902
5333
|
"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."
|
|
4903
5334
|
].join("\n");
|
|
4904
|
-
const
|
|
5335
|
+
const createdRuns = [];
|
|
4905
5336
|
if (round.executionGroup === undefined) {
|
|
4906
5337
|
if (round.status !== "pending")
|
|
4907
|
-
return
|
|
4908
|
-
if (tx.
|
|
4909
|
-
throw usageError(`Reviewer Role already has an active
|
|
5338
|
+
return createdRuns;
|
|
5339
|
+
if (tx.getActiveRun(taskId, reviewer.name) !== null) {
|
|
5340
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
4910
5341
|
}
|
|
4911
|
-
const
|
|
5342
|
+
const runId = tx.nextRunId(taskId);
|
|
4912
5343
|
const effective = resolveEffectiveLaunch({
|
|
4913
5344
|
role: reviewer,
|
|
4914
5345
|
purpose: "review",
|
|
@@ -4916,14 +5347,14 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4916
5347
|
reviewRoundId: round.id,
|
|
4917
5348
|
reviewBaseCommit: round.reviewBaseCommit
|
|
4918
5349
|
});
|
|
4919
|
-
const snapshot =
|
|
5350
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
4920
5351
|
taskId,
|
|
4921
5352
|
roleName: reviewer.name,
|
|
4922
5353
|
purpose: "review",
|
|
4923
5354
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
4924
5355
|
reviewRoundId: round.id
|
|
4925
5356
|
}, now, "controller");
|
|
4926
|
-
const created =
|
|
5357
|
+
const created = createRun(runId, taskId, reviewer.name, roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(taskId, reviewer.name), effective.agentId, effective), createRunInput({
|
|
4927
5358
|
source: {
|
|
4928
5359
|
type: "yui",
|
|
4929
5360
|
channel: item === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
@@ -4938,27 +5369,27 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4938
5369
|
workspace: round.workspace,
|
|
4939
5370
|
effective
|
|
4940
5371
|
});
|
|
4941
|
-
tx.
|
|
5372
|
+
tx.saveRun(created);
|
|
4942
5373
|
tx.saveReviewRound(taskId, startReviewRound(round, created.id));
|
|
4943
|
-
tx.
|
|
4944
|
-
|
|
5374
|
+
tx.saveActiveRun(created);
|
|
5375
|
+
enqueueRoleRunDispatch(tx, {
|
|
4945
5376
|
taskId,
|
|
4946
5377
|
roleName: reviewer.name,
|
|
4947
|
-
|
|
5378
|
+
runId: created.id,
|
|
4948
5379
|
reason: "review-requested",
|
|
4949
5380
|
occurredAt: now
|
|
4950
5381
|
});
|
|
4951
|
-
recordTaskEvent(tx, taskId, "
|
|
4952
|
-
|
|
4953
|
-
return
|
|
5382
|
+
recordTaskEvent(tx, taskId, "run.review-dispatched", runLaunchEventPayload(created), now);
|
|
5383
|
+
createdRuns.push(created);
|
|
5384
|
+
return createdRuns;
|
|
4954
5385
|
}
|
|
4955
5386
|
let runningGroup = round.executionGroup;
|
|
4956
5387
|
const dispatchLanes = runningGroup.lanes.filter((lane) => {
|
|
4957
5388
|
if (lane.disposition !== "open")
|
|
4958
5389
|
return false;
|
|
4959
|
-
if (lane.
|
|
5390
|
+
if (lane.currentRunId === undefined)
|
|
4960
5391
|
return true;
|
|
4961
|
-
return tx.
|
|
5392
|
+
return tx.getRun(taskId, lane.currentRunId)?.status === "failed";
|
|
4962
5393
|
});
|
|
4963
5394
|
const preparedLaneWorkspaces = requireIsolatedReviewLaneWorkspaces(tx, round, dispatchLanes, options.executionLaneWorkspaces);
|
|
4964
5395
|
for (const lane of dispatchLanes) {
|
|
@@ -4966,8 +5397,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4966
5397
|
if (laneReviewer === null) {
|
|
4967
5398
|
throw usageError(`Review Producer Role not found: ${taskId}/${lane.roleName}.`);
|
|
4968
5399
|
}
|
|
4969
|
-
if (tx.
|
|
4970
|
-
throw usageError(`Review Producer Role already has an active
|
|
5400
|
+
if (tx.getActiveRun(taskId, lane.roleName) !== null) {
|
|
5401
|
+
throw usageError(`Review Producer Role already has an active AgentRun: ${lane.roleName}.`);
|
|
4971
5402
|
}
|
|
4972
5403
|
const laneManagedWorkspace = preparedLaneWorkspaces.get(lane.id);
|
|
4973
5404
|
const effective = lane.effective ?? resolveEffectiveLaunch({
|
|
@@ -4977,8 +5408,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4977
5408
|
reviewRoundId: round.id,
|
|
4978
5409
|
reviewBaseCommit: round.reviewBaseCommit
|
|
4979
5410
|
});
|
|
4980
|
-
const
|
|
4981
|
-
const input =
|
|
5411
|
+
const runId = tx.nextRunId(taskId);
|
|
5412
|
+
const input = createRunInput({
|
|
4982
5413
|
source: {
|
|
4983
5414
|
type: "yui",
|
|
4984
5415
|
channel: item === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
@@ -5000,7 +5431,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5000
5431
|
deltaRefIds: []
|
|
5001
5432
|
});
|
|
5002
5433
|
runningGroup = updateUnifiedExecutionLane(runningGroup, lane.id, {
|
|
5003
|
-
|
|
5434
|
+
currentRunId: runId,
|
|
5004
5435
|
effective,
|
|
5005
5436
|
workspace: {
|
|
5006
5437
|
root: laneManagedWorkspace.root,
|
|
@@ -5009,7 +5440,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5009
5440
|
.map(({ projectId }) => projectId)
|
|
5010
5441
|
}
|
|
5011
5442
|
}, now);
|
|
5012
|
-
|
|
5443
|
+
createdRuns.push(createRun(runId, taskId, laneReviewer.name, roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(taskId, laneReviewer.name), effective.agentId, effective), input, now, {
|
|
5013
5444
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5014
5445
|
purpose: "review",
|
|
5015
5446
|
reviewRoundId: round.id,
|
|
@@ -5031,32 +5462,30 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5031
5462
|
tx.saveManagedWorkspace(prepared);
|
|
5032
5463
|
}
|
|
5033
5464
|
}
|
|
5034
|
-
for (let index = 0; index <
|
|
5035
|
-
const
|
|
5036
|
-
const snapshot =
|
|
5465
|
+
for (let index = 0; index < createdRuns.length; index += 1) {
|
|
5466
|
+
const unboundRun = createdRuns[index];
|
|
5467
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
5037
5468
|
taskId,
|
|
5038
|
-
roleName:
|
|
5469
|
+
roleName: unboundRun.roleName,
|
|
5039
5470
|
purpose: "review",
|
|
5040
5471
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5041
5472
|
reviewRoundId: round.id
|
|
5042
5473
|
}, now, "controller", runningGroup.assignment.contextSnapshotRef);
|
|
5043
|
-
const created =
|
|
5044
|
-
|
|
5045
|
-
const laneReviewer = requireRole(tx, taskId,
|
|
5046
|
-
tx.
|
|
5047
|
-
tx.
|
|
5048
|
-
|
|
5474
|
+
const created = withRunContextSnapshot(unboundRun, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
5475
|
+
createdRuns[index] = created;
|
|
5476
|
+
const laneReviewer = requireRole(tx, taskId, unboundRun.roleName);
|
|
5477
|
+
tx.saveRun(created);
|
|
5478
|
+
tx.saveActiveRun(created);
|
|
5479
|
+
enqueueRoleRunDispatch(tx, {
|
|
5049
5480
|
taskId,
|
|
5050
5481
|
roleName: laneReviewer.name,
|
|
5051
|
-
|
|
5482
|
+
runId: created.id,
|
|
5052
5483
|
reason: "review-requested",
|
|
5053
5484
|
occurredAt: now
|
|
5054
5485
|
});
|
|
5055
|
-
recordTaskEvent(tx, taskId, "
|
|
5486
|
+
recordTaskEvent(tx, taskId, "run.review-dispatched", runLaunchEventPayload(created), now);
|
|
5056
5487
|
}
|
|
5057
|
-
|
|
5058
|
-
createdTurns.push(...reconciliation.createdTurns);
|
|
5059
|
-
return createdTurns;
|
|
5488
|
+
return createdRuns;
|
|
5060
5489
|
});
|
|
5061
5490
|
for (const run of runs) {
|
|
5062
5491
|
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
@@ -5106,6 +5535,16 @@ function requireWorkItemCandidate(item) {
|
|
|
5106
5535
|
}
|
|
5107
5536
|
return candidate;
|
|
5108
5537
|
}
|
|
5538
|
+
function fixedArtifactRefs(store, taskId, ids) {
|
|
5539
|
+
if (new Set(ids).size !== ids.length)
|
|
5540
|
+
throw usageError("Artifact references must be unique.");
|
|
5541
|
+
try {
|
|
5542
|
+
return createProjectResources(store).resultRefs(taskId, ids);
|
|
5543
|
+
}
|
|
5544
|
+
catch (error) {
|
|
5545
|
+
throw usageError(`Result Artifact is unavailable: ${messageOf(error)}`);
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5109
5548
|
/** ReviewRound ids are the durable Task-local creation order; wall time is not causal. */
|
|
5110
5549
|
function reviewRoundsByIdentity(rounds) {
|
|
5111
5550
|
return [...rounds].sort((left, right) => (left.id.localeCompare(right.id, undefined, { numeric: true })));
|
|
@@ -5118,7 +5557,9 @@ function activeReviewRoundForCandidate(store, item, candidate) {
|
|
|
5118
5557
|
.at(-1);
|
|
5119
5558
|
}
|
|
5120
5559
|
function createTaskRole(store, task, roleName, explicitAgentId, now, sourceGlobalRoleName) {
|
|
5121
|
-
const workspace = task.
|
|
5560
|
+
const workspace = task.status === "draft"
|
|
5561
|
+
? join(`${store.rootDirectory()}.task-runtimes`, "planning", task.id)
|
|
5562
|
+
: task.cwd ?? store.getConfig().defaultWorkspace ?? process.cwd();
|
|
5122
5563
|
if (explicitAgentId === undefined) {
|
|
5123
5564
|
const sourceRoleName = sourceGlobalRoleName
|
|
5124
5565
|
?? (roleName === LEADER_ROLE ? LEADER_ROLE : "worker");
|
|
@@ -5136,7 +5577,7 @@ function createTaskRole(store, task, roleName, explicitAgentId, now, sourceGloba
|
|
|
5136
5577
|
throw dataError(`No Agent is configured for Task role: ${roleName}.`);
|
|
5137
5578
|
}
|
|
5138
5579
|
const agent = requireAgent(store, agentId);
|
|
5139
|
-
const binding = createRoleAgentBinding(
|
|
5580
|
+
const binding = createRoleAgentBinding(agent);
|
|
5140
5581
|
return createRole(task.id, roleName, [binding], agent.id, workspace, now);
|
|
5141
5582
|
}
|
|
5142
5583
|
function requireAgentProfile(store, id) {
|
|
@@ -5146,7 +5587,9 @@ function requireAgentProfile(store, id) {
|
|
|
5146
5587
|
return profile;
|
|
5147
5588
|
}
|
|
5148
5589
|
function createTaskRoleFromAgentBinding(store, task, roleName, binding, now) {
|
|
5149
|
-
const workspace = task.
|
|
5590
|
+
const workspace = task.status === "draft"
|
|
5591
|
+
? join(`${store.rootDirectory()}.task-runtimes`, "planning", task.id)
|
|
5592
|
+
: task.cwd ?? store.getConfig().defaultWorkspace ?? process.cwd();
|
|
5150
5593
|
return createRole(task.id, roleName, [binding], binding.agentId, workspace, now);
|
|
5151
5594
|
}
|
|
5152
5595
|
function resolvedAgentProfileRuntime(profile, store) {
|
|
@@ -5174,7 +5617,7 @@ function resolveTaskRoleAgentBindingAdd(parsed, profile, store) {
|
|
|
5174
5617
|
if (explicitAgentId === undefined)
|
|
5175
5618
|
return undefined;
|
|
5176
5619
|
const agent = requireAgent(store, explicitAgentId);
|
|
5177
|
-
binding = createRoleAgentBinding(
|
|
5620
|
+
binding = createRoleAgentBinding(agent);
|
|
5178
5621
|
}
|
|
5179
5622
|
if (hasAgentConfigOptions(parsed)) {
|
|
5180
5623
|
binding = patchRoleAgentBinding(binding, parsed);
|
|
@@ -5185,11 +5628,6 @@ function resolveTaskRoleAgentBindingUpdate(parsed, role, profile, store) {
|
|
|
5185
5628
|
const changesAgentConfig = hasAgentConfigOptions(parsed);
|
|
5186
5629
|
const explicitAgentId = parsed.one("--agent")?.trim();
|
|
5187
5630
|
const targetAgentId = explicitAgentId || role.activeAgentId;
|
|
5188
|
-
if (profile?.runtime.source === "global-worker"
|
|
5189
|
-
&& explicitAgentId === undefined
|
|
5190
|
-
&& !changesAgentConfig) {
|
|
5191
|
-
return undefined;
|
|
5192
|
-
}
|
|
5193
5631
|
let binding;
|
|
5194
5632
|
if (profile !== undefined) {
|
|
5195
5633
|
const profileRuntime = resolvedAgentProfileRuntime(profile, store);
|
|
@@ -5208,7 +5646,7 @@ function resolveTaskRoleAgentBindingUpdate(parsed, role, profile, store) {
|
|
|
5208
5646
|
return undefined;
|
|
5209
5647
|
const agent = requireAgent(store, targetAgentId);
|
|
5210
5648
|
binding = role.agentBindings[targetAgentId]
|
|
5211
|
-
?? createRoleAgentBinding(
|
|
5649
|
+
?? createRoleAgentBinding(agent);
|
|
5212
5650
|
}
|
|
5213
5651
|
if (changesAgentConfig) {
|
|
5214
5652
|
binding = patchRoleAgentBinding(binding, parsed);
|
|
@@ -5245,7 +5683,7 @@ function appendMessage(store, taskId, body, kind, author, now, context = {}) {
|
|
|
5245
5683
|
recordTaskEvent(store, taskId, "message.sent", {
|
|
5246
5684
|
messageId: message.id,
|
|
5247
5685
|
kind: message.kind,
|
|
5248
|
-
...(message.
|
|
5686
|
+
...(message.runId === undefined ? {} : { runId: message.runId })
|
|
5249
5687
|
}, now);
|
|
5250
5688
|
return message;
|
|
5251
5689
|
}
|
|
@@ -5253,43 +5691,29 @@ function recordTaskEvent(store, taskId, type, payload, now) {
|
|
|
5253
5691
|
return recordTaskEventRecord(store, taskId, type, payload, now);
|
|
5254
5692
|
}
|
|
5255
5693
|
function leaderActionEventPayload(store, taskId, options) {
|
|
5256
|
-
const
|
|
5257
|
-
|
|
5694
|
+
const caller = currentManagedRuntime(store, options.environment, taskId, "leader");
|
|
5695
|
+
// A long-lived Session may be handling direct input while another request
|
|
5696
|
+
// awaits admission. The Role's active pointer cannot prove command origin.
|
|
5697
|
+
return caller === undefined ? {} : { leaderNativeSessionId: caller.nativeSessionId };
|
|
5258
5698
|
}
|
|
5259
5699
|
function recordTaskEventRecord(store, taskId, type, payload, now) {
|
|
5260
5700
|
const event = createTaskEvent(store.nextEventId(taskId), taskId, type, payload, now);
|
|
5261
5701
|
store.saveEvent(taskId, event);
|
|
5262
5702
|
return event;
|
|
5263
5703
|
}
|
|
5264
|
-
/** Keeps a free-text
|
|
5704
|
+
/** Keeps a free-text AgentRun-fact note bounded so an event payload stays compact. */
|
|
5265
5705
|
function truncateEventNote(note) {
|
|
5266
5706
|
const normalized = note.trim();
|
|
5267
5707
|
return normalized.length <= 280 ? normalized : `${normalized.slice(0, 279)}…`;
|
|
5268
5708
|
}
|
|
5269
|
-
function
|
|
5270
|
-
const effective = sessions?.sessions[sessions.activeAgentId]?.effective;
|
|
5271
|
-
return {
|
|
5272
|
-
desiredRevision: String(role.launchRevision),
|
|
5273
|
-
defaultAccess: role.defaultAccess,
|
|
5274
|
-
effectiveRevision: effective === undefined
|
|
5275
|
-
? "none"
|
|
5276
|
-
: String(effective.sourceDesiredRevision),
|
|
5277
|
-
profileAccess: effective?.profileAccess ?? "none",
|
|
5278
|
-
effectivePermission: effective?.permission.strategy ?? "none",
|
|
5279
|
-
desiredDrift: effective === undefined
|
|
5280
|
-
? "not-started"
|
|
5281
|
-
: effective.sourceDesiredRevision === role.launchRevision
|
|
5282
|
-
? "none"
|
|
5283
|
-
: "pending-next-launch"
|
|
5284
|
-
};
|
|
5285
|
-
}
|
|
5286
|
-
function turnLaunchEventPayload(run) {
|
|
5709
|
+
function runLaunchEventPayload(run) {
|
|
5287
5710
|
return {
|
|
5288
|
-
|
|
5711
|
+
runId: run.id,
|
|
5289
5712
|
role: run.roleName,
|
|
5290
5713
|
purpose: run.purpose,
|
|
5291
5714
|
mode: run.mode,
|
|
5292
5715
|
agent: `${run.effective.agentId}/${run.effective.adapterId}`,
|
|
5716
|
+
component: run.effective.component,
|
|
5293
5717
|
effectiveRevision: String(run.effective.sourceDesiredRevision),
|
|
5294
5718
|
profileAccess: run.effective.profileAccess,
|
|
5295
5719
|
effectivePermission: run.effective.permission.strategy,
|
|
@@ -5346,18 +5770,18 @@ function requireWorkItem(store, workItemId, options) {
|
|
|
5346
5770
|
}
|
|
5347
5771
|
return item;
|
|
5348
5772
|
}
|
|
5349
|
-
function
|
|
5350
|
-
const reference = taskRecordReference(
|
|
5351
|
-
const run = store.
|
|
5773
|
+
function requireRun(store, runId, options) {
|
|
5774
|
+
const reference = taskRecordReference(runId, "run", "AgentRun reference", options);
|
|
5775
|
+
const run = store.getRun(reference.taskId, reference.localId);
|
|
5352
5776
|
if (run === null) {
|
|
5353
|
-
throw usageError(`
|
|
5777
|
+
throw usageError(`AgentRun not found: ${reference.taskId}/${reference.localId}.`);
|
|
5354
5778
|
}
|
|
5355
5779
|
return run;
|
|
5356
5780
|
}
|
|
5357
|
-
function
|
|
5781
|
+
function activeRunPointer(store, run) {
|
|
5358
5782
|
return run.executionGroupId !== undefined && run.executionLaneId !== undefined
|
|
5359
|
-
? store.
|
|
5360
|
-
: store.
|
|
5783
|
+
? store.getActiveExecutionLaneRun(run.taskId, run.executionGroupId, run.executionLaneId)
|
|
5784
|
+
: store.getActiveRun(run.taskId, run.roleName);
|
|
5361
5785
|
}
|
|
5362
5786
|
function requireReviewRound(store, reviewRoundId, options) {
|
|
5363
5787
|
const reference = taskRecordReference(reviewRoundId, "reviewRound", "ReviewRound reference", options);
|
|
@@ -5392,12 +5816,12 @@ export function assertWorkItemDependenciesCompletedForCommand(store, item) {
|
|
|
5392
5816
|
throw error;
|
|
5393
5817
|
}
|
|
5394
5818
|
}
|
|
5395
|
-
function
|
|
5396
|
-
return [...
|
|
5819
|
+
function chronologicalRuns(runs) {
|
|
5820
|
+
return [...runs].sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
5397
5821
|
|| left.id.localeCompare(right.id)));
|
|
5398
5822
|
}
|
|
5399
5823
|
function isTerminalWorkItemStatus(status) {
|
|
5400
|
-
return ["
|
|
5824
|
+
return ["accepted", "retired"].includes(status);
|
|
5401
5825
|
}
|
|
5402
5826
|
function assertTaskOpen(task) {
|
|
5403
5827
|
if (task.status === "completed") {
|
|
@@ -5405,7 +5829,7 @@ function assertTaskOpen(task) {
|
|
|
5405
5829
|
}
|
|
5406
5830
|
if (task.status === "archived")
|
|
5407
5831
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
5408
|
-
if (task.status === "
|
|
5832
|
+
if (task.status === "cancelled")
|
|
5409
5833
|
throw usageError(`Task is retired: ${task.id}.`);
|
|
5410
5834
|
}
|
|
5411
5835
|
function assertTaskExecutionEnabled(task, action) {
|
|
@@ -5423,7 +5847,7 @@ function inactiveTaskMessage(task, action) {
|
|
|
5423
5847
|
if (task.status === "completed") {
|
|
5424
5848
|
return `Task ${task.id} is completed; reopen it before ${action}.`;
|
|
5425
5849
|
}
|
|
5426
|
-
if (task.status === "
|
|
5850
|
+
if (task.status === "cancelled")
|
|
5427
5851
|
return `Task ${task.id} is retired; it cannot resume ${action}.`;
|
|
5428
5852
|
return `Task is archived: ${task.id}.`;
|
|
5429
5853
|
}
|
|
@@ -5444,10 +5868,6 @@ function parseWorkStatus(value) {
|
|
|
5444
5868
|
throw usageError(`Invalid work item status: ${value}.`);
|
|
5445
5869
|
}
|
|
5446
5870
|
function presentWorkStatus(status) {
|
|
5447
|
-
if (status === "pending")
|
|
5448
|
-
return "todo";
|
|
5449
|
-
if (status === "completed")
|
|
5450
|
-
return "done";
|
|
5451
5871
|
return status;
|
|
5452
5872
|
}
|
|
5453
5873
|
function parseWorkCreateArgs(args, usage) {
|
|
@@ -5622,7 +6042,10 @@ function taskBriefCommand(args, store, options) {
|
|
|
5622
6042
|
...(hasSummary ? { leaderSummary: parsed.options.get("--leader-summary") } : {})
|
|
5623
6043
|
}, updatedBy, now);
|
|
5624
6044
|
tx.saveTaskBrief(task.id, brief);
|
|
5625
|
-
recordTaskEvent(tx, task.id, "brief.updated", {
|
|
6045
|
+
recordTaskEvent(tx, task.id, "brief.updated", {
|
|
6046
|
+
updatedBy,
|
|
6047
|
+
previous: JSON.stringify(existing), current: JSON.stringify(brief)
|
|
6048
|
+
}, now);
|
|
5626
6049
|
enqueueWork(tx, taskMailbox(task.id), "brief-updated", now, [taskRef(task.id)]);
|
|
5627
6050
|
if (task.status === "active" && updatedBy !== "leader") {
|
|
5628
6051
|
enqueueWork(tx, leaderMailbox(task.id), "brief-updated", now, [taskRef(task.id)]);
|
|
@@ -5633,12 +6056,18 @@ function taskBriefCommand(args, store, options) {
|
|
|
5633
6056
|
if (result.task.status === "active") {
|
|
5634
6057
|
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
5635
6058
|
}
|
|
5636
|
-
return output(`Updated brief for ${result.task.id}\n
|
|
6059
|
+
return output(`Updated brief for ${result.task.id}\n`, { taskId: result.task.id, brief: result.brief });
|
|
5637
6060
|
}
|
|
5638
6061
|
throw usageError(command === undefined
|
|
5639
6062
|
? "Task brief command is required."
|
|
5640
6063
|
: `Unknown command: task brief ${command}`);
|
|
5641
6064
|
}
|
|
6065
|
+
/** Persist only edited fields, not another copy of aggregate execution history.
|
|
6066
|
+
* Null records an absent optional field so clearing it remains observable. */
|
|
6067
|
+
function editedFieldValues(record, fields) {
|
|
6068
|
+
const values = record;
|
|
6069
|
+
return JSON.stringify(Object.fromEntries(fields.map((field) => [field, values[field] ?? null])));
|
|
6070
|
+
}
|
|
5642
6071
|
function taskDecisionCommand(args, store, options) {
|
|
5643
6072
|
const [command, ...rest] = args;
|
|
5644
6073
|
if (command === "record") {
|
|
@@ -5889,14 +6318,11 @@ function taskContinuationCommand(args, store) {
|
|
|
5889
6318
|
const report = [...continuation.reports].reverse()[0];
|
|
5890
6319
|
const reportEvent = report === undefined
|
|
5891
6320
|
? undefined
|
|
5892
|
-
: reportEvents.find((entry) => (entry.continuationId === identity.continuationId
|
|
5893
|
-
&& entry.continuationGeneration === identity.generation
|
|
5894
|
-
&& entry.reportId === report.reportId));
|
|
6321
|
+
: reportEvents.find((entry) => (entry.continuationId === identity.continuationId && entry.reportId === report.reportId));
|
|
5895
6322
|
return Object.freeze({
|
|
5896
6323
|
continuationId: identity.continuationId,
|
|
5897
|
-
generation: identity.generation,
|
|
5898
6324
|
driver: identity.providerNamespace,
|
|
5899
|
-
|
|
6325
|
+
runId: continuation.runId,
|
|
5900
6326
|
execution: continuation.execution,
|
|
5901
6327
|
outcome: continuation.outcome,
|
|
5902
6328
|
attachment: continuation.attachment,
|
|
@@ -5942,12 +6368,9 @@ function continuationReportEvents(events) {
|
|
|
5942
6368
|
const observation = runtimeObservationFromTaskEvent(event);
|
|
5943
6369
|
if (observation !== null && observation.kind === "continuation.reported") {
|
|
5944
6370
|
const continuationId = observation.fence.continuationId;
|
|
5945
|
-
const continuationGeneration = observation.fence.continuationGeneration;
|
|
5946
6371
|
const reportId = observation.payload?.reportId;
|
|
5947
|
-
if (continuationId !== undefined
|
|
5948
|
-
|
|
5949
|
-
&& reportId !== undefined) {
|
|
5950
|
-
result.push({ event, continuationId, continuationGeneration, reportId });
|
|
6372
|
+
if (continuationId !== undefined && reportId !== undefined) {
|
|
6373
|
+
result.push({ event, continuationId, reportId });
|
|
5951
6374
|
}
|
|
5952
6375
|
}
|
|
5953
6376
|
}
|
|
@@ -5961,6 +6384,48 @@ function continuationReportEvents(events) {
|
|
|
5961
6384
|
*/
|
|
5962
6385
|
function taskWakeDispatch(args, store, options) {
|
|
5963
6386
|
const [subcommand] = args;
|
|
6387
|
+
if (subcommand === "resolve") {
|
|
6388
|
+
const usage = "Task wake resolve usage: yui task wake resolve <task> <wake> --reason <quiescence-evidence>.";
|
|
6389
|
+
const parsed = parseTail(args.slice(1), new Set(["--reason"]), usage);
|
|
6390
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
6391
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
6392
|
+
const now = clock(options);
|
|
6393
|
+
const result = store.transaction((tx) => {
|
|
6394
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
6395
|
+
taskActor(tx, options, task.id);
|
|
6396
|
+
const wakeId = parsed.positionals[1];
|
|
6397
|
+
const wake = tx.getTaskWake(task.id, wakeId);
|
|
6398
|
+
if (wake === null)
|
|
6399
|
+
throw usageError("Wake is unavailable.");
|
|
6400
|
+
const target = leaderMailbox(task.id);
|
|
6401
|
+
const mailbox = tx.getWorkMailbox(target);
|
|
6402
|
+
const claim = mailbox?.processing;
|
|
6403
|
+
if (claim === undefined || claim === null || claim.owner !== `leader-notification:${wake.id}`) {
|
|
6404
|
+
throw usageError("This wake has no unresolved notification claim.");
|
|
6405
|
+
}
|
|
6406
|
+
const previous = tx.listEvents(task.id).filter((event) => event.type === "notification.delivery" && event.payload.attemptId === claim.batchId).at(-1);
|
|
6407
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, "leader");
|
|
6408
|
+
const provider = sessions?.providerBinding;
|
|
6409
|
+
if (previous?.payload.outcome !== "unknown")
|
|
6410
|
+
throw usageError("Only an unknown notification can be resolved.");
|
|
6411
|
+
if (tx.getActiveRun(task.id, "leader") !== null
|
|
6412
|
+
|| provider?.authority.owner === "human" || provider?.authority.owner === "unknown"
|
|
6413
|
+
|| (provider?.run !== undefined && provider.run !== null
|
|
6414
|
+
&& ["submitting", "accepted", "delivery-unknown"].includes(provider.run.status))) {
|
|
6415
|
+
throw usageError("Shared native execution is not proven quiescent; resolve that exact runtime boundary first.");
|
|
6416
|
+
}
|
|
6417
|
+
// Preserve the original unknown outcome and the unconsumed fixed wake.
|
|
6418
|
+
// This releases only the scheduling claim; it never replays, claims
|
|
6419
|
+
// acceptance, or asserts that Message requirements were implemented.
|
|
6420
|
+
recordTaskEvent(tx, task.id, "notification.resolved", {
|
|
6421
|
+
wakeId: wake.id, attemptId: claim.batchId, reason, outcome: "released-without-replay"
|
|
6422
|
+
}, now);
|
|
6423
|
+
tx.saveWorkMailbox(completeProcessing(mailbox, claim.batchId));
|
|
6424
|
+
return { taskId: task.id, wakeId: wake.id, outcome: "released-without-replay" };
|
|
6425
|
+
});
|
|
6426
|
+
notifyMailbox(options.runtime, leaderMailbox(result.taskId), result.taskId);
|
|
6427
|
+
return output(`Released ${result.wakeId} without replay; original acceptance remains unknown.\n`, result);
|
|
6428
|
+
}
|
|
5964
6429
|
if (subcommand === "list" || subcommand === "show") {
|
|
5965
6430
|
return taskWakeInspectionCommand(args, store);
|
|
5966
6431
|
}
|
|
@@ -5987,13 +6452,13 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
5987
6452
|
{ header: "Wake", minWidth: 8, maxWidth: 18 },
|
|
5988
6453
|
{ header: "Status", minWidth: 8, maxWidth: 12 },
|
|
5989
6454
|
{ header: "Reasons", minWidth: 10, maxWidth: 40 },
|
|
5990
|
-
{ header: "
|
|
6455
|
+
{ header: "AgentRun", minWidth: 10, maxWidth: 20 },
|
|
5991
6456
|
{ header: "Dispatched", minWidth: 10, maxWidth: 28 }
|
|
5992
6457
|
], wakes.map((wake) => [
|
|
5993
6458
|
wake.id,
|
|
5994
6459
|
wake.status,
|
|
5995
6460
|
wake.reasons.map(renderWakeReason).join(", "),
|
|
5996
|
-
wake.
|
|
6461
|
+
wake.runId ?? "-",
|
|
5997
6462
|
presentTime(wake.createdAt, timeZone)
|
|
5998
6463
|
]), defaultTableWidth())}\n`, { taskId: task.id, wakes });
|
|
5999
6464
|
}
|
|
@@ -6012,18 +6477,30 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
6012
6477
|
return ms > fromMs && ms <= toMs;
|
|
6013
6478
|
};
|
|
6014
6479
|
const allEvents = store.listEvents(task.id);
|
|
6015
|
-
const
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
const
|
|
6019
|
-
|
|
6480
|
+
const deliveryEvents = allEvents.filter((event) => (event.type === "notification.delivery" || event.type === "notification.resolved")
|
|
6481
|
+
&& (event.payload.wakeId === wake.id
|
|
6482
|
+
|| event.payload.attemptId?.startsWith(`notification:${task.id}/${wake.id}/`)));
|
|
6483
|
+
const referenced = (type, id) => wake.refs?.some(ref => ref.type === type && ref.id === id
|
|
6484
|
+
&& (!("taskId" in ref) || ref.taskId === task.id)) === true;
|
|
6485
|
+
const events = allEvents.filter((e) => inWindow(e.createdAt) || referenced("event", e.id));
|
|
6486
|
+
const messages = store.listMessages(task.id).filter((m) => inWindow(m.createdAt) || referenced("message", m.id));
|
|
6487
|
+
const allRuns = store.listRuns(task.id);
|
|
6488
|
+
const referencedRunIds = new Set(referencedWakeRunIds(allRuns, allEvents, events));
|
|
6489
|
+
for (const run of allRuns)
|
|
6490
|
+
if (referenced("run", run.id))
|
|
6491
|
+
referencedRunIds.add(run.id);
|
|
6492
|
+
for (const message of messages)
|
|
6493
|
+
if (message.resultRef?.type === "agent-run-result")
|
|
6494
|
+
referencedRunIds.add(message.resultRef.runId);
|
|
6495
|
+
const runs = operationalTaskRecords(allRuns, allEvents, "run").filter((run) => (inWindow(run.createdAt) || referencedRunIds.has(run.id)));
|
|
6020
6496
|
const lines = [
|
|
6021
6497
|
`Wake: ${wake.id}`,
|
|
6022
6498
|
`Task: ${task.id}`,
|
|
6023
6499
|
`Status: ${wake.status}`,
|
|
6500
|
+
`Notification: ${deliveryEvents.at(-1)?.payload.outcome ?? "unobserved"}`,
|
|
6024
6501
|
`Reasons: ${wake.reasons.map(renderWakeReason).join(", ")}`,
|
|
6025
6502
|
`Delta window: ${wake.fromCursor} → ${wake.toCursor}`,
|
|
6026
|
-
...(wake.
|
|
6503
|
+
...(wake.runId === undefined ? [] : [`AgentRun: ${wake.runId}`]),
|
|
6027
6504
|
`Dispatched: ${presentTime(wake.createdAt, timeZone)}`,
|
|
6028
6505
|
...(wake.consumedAt === undefined
|
|
6029
6506
|
? []
|
|
@@ -6032,19 +6509,20 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
6032
6509
|
...events.map((e) => ` ${e.id} ${e.type} ${presentTime(e.createdAt, timeZone)}`),
|
|
6033
6510
|
`Messages (${messages.length}):`,
|
|
6034
6511
|
...messages.map((m) => ` ${m.id} [${taskMessageAuthorLabel(m.author)}] ${presentTime(m.createdAt, timeZone)}`),
|
|
6035
|
-
`
|
|
6036
|
-
...
|
|
6037
|
-
+ `${presentTime(
|
|
6038
|
-
+ `${
|
|
6039
|
-
? ` → yui task
|
|
6512
|
+
`AgentRuns (${runs.length}):`,
|
|
6513
|
+
...runs.map((run) => (` ${run.id} [${run.status}/${run.purpose}] ${run.roleName} `
|
|
6514
|
+
+ `${presentTime(run.createdAt, timeZone)}`
|
|
6515
|
+
+ `${referencedRunIds.has(run.id)
|
|
6516
|
+
? ` → yui task run show ${task.id}/${run.id}`
|
|
6040
6517
|
: ""}`))
|
|
6041
6518
|
];
|
|
6042
6519
|
return output(lines.join("\n").concat("\n"), {
|
|
6043
6520
|
taskId: task.id,
|
|
6521
|
+
deliveryEvents,
|
|
6044
6522
|
wake,
|
|
6045
6523
|
events,
|
|
6046
6524
|
messages,
|
|
6047
|
-
|
|
6525
|
+
runs
|
|
6048
6526
|
});
|
|
6049
6527
|
}
|
|
6050
6528
|
throw usageError(command === undefined
|
|
@@ -6175,8 +6653,8 @@ function leaderMailbox(taskId) {
|
|
|
6175
6653
|
function taskRef(id) {
|
|
6176
6654
|
return { type: "task", id };
|
|
6177
6655
|
}
|
|
6178
|
-
function
|
|
6179
|
-
return { type: "
|
|
6656
|
+
function runRef(taskId, id) {
|
|
6657
|
+
return { type: "run", taskId, id };
|
|
6180
6658
|
}
|
|
6181
6659
|
function workItemRef(taskId, id) {
|
|
6182
6660
|
return { type: "work-item", taskId, id };
|