@zq-silk/yui 0.15.6 → 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 +167 -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 +188 -95
- package/dist/commands/agentCommands.js +72 -14
- package/dist/commands/capabilityCommands.js +57 -0
- 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 +1216 -730
- 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 +77 -0
- package/dist/controller/clientRuntime.js +65 -92
- package/dist/controller/controller.js +127 -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 +530 -476
- 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 +423 -0
- package/dist/kernel/capabilityRegistry.js +314 -0
- package/dist/kernel/capabilitySchema.js +91 -0
- package/dist/kernel/instanceHost.js +18 -1
- package/dist/kernel/kernelPorts.js +4 -1
- 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 +5 -5
- 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
|
}
|
|
@@ -471,40 +535,50 @@ function updateTaskCommand(args, store, options) {
|
|
|
471
535
|
const tags = parsed.options.has("--tags")
|
|
472
536
|
? parseTaskTags(requiredOption(parsed.options, "--tags"))
|
|
473
537
|
: undefined;
|
|
538
|
+
const result = updateTaskMetadataCommand(store, parsed.positionals[0], {
|
|
539
|
+
...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
|
|
540
|
+
...(parsed.options.has("--type")
|
|
541
|
+
? { type: requiredOption(parsed.options, "--type") }
|
|
542
|
+
: parsed.options.has("--clear-type") ? { type: null } : {}),
|
|
543
|
+
...(parsed.options.has("--description")
|
|
544
|
+
? { description: requiredOption(parsed.options, "--description") }
|
|
545
|
+
: parsed.options.has("--clear-description") ? { description: null } : {}),
|
|
546
|
+
...(priority === undefined
|
|
547
|
+
? parsed.options.has("--clear-priority") ? { priority: null } : {}
|
|
548
|
+
: { priority }),
|
|
549
|
+
...(tags === undefined
|
|
550
|
+
? parsed.options.has("--clear-tags") ? { tags: null } : {}
|
|
551
|
+
: { tags }),
|
|
552
|
+
...(dueAt === undefined
|
|
553
|
+
? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
|
|
554
|
+
: { dueAt })
|
|
555
|
+
}, options);
|
|
556
|
+
return `Updated task ${result.id}\n`;
|
|
557
|
+
}
|
|
558
|
+
/** Shared domain transaction for structured capabilities and the legacy CLI.
|
|
559
|
+
* Parsing text flags must not become an alternate business write path. */
|
|
560
|
+
export function updateTaskMetadataCommand(store, taskId, patch, options = {}) {
|
|
561
|
+
if (Object.keys(patch).length === 0)
|
|
562
|
+
throw usageError("At least one Task metadata field is required.");
|
|
474
563
|
const now = clock(options);
|
|
475
564
|
const result = store.transaction((tx) => {
|
|
476
|
-
const current = requireTask(tx,
|
|
565
|
+
const current = requireTask(tx, taskId);
|
|
477
566
|
if (current.status === "archived")
|
|
478
567
|
throw usageError(`Task is archived: ${current.id}.`);
|
|
479
568
|
taskActor(tx, options, current.id);
|
|
480
|
-
const updated = updateTaskMetadata(current,
|
|
481
|
-
...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
|
|
482
|
-
...(parsed.options.has("--type")
|
|
483
|
-
? { type: requiredOption(parsed.options, "--type") }
|
|
484
|
-
: parsed.options.has("--clear-type") ? { type: null } : {}),
|
|
485
|
-
...(parsed.options.has("--description")
|
|
486
|
-
? { description: requiredOption(parsed.options, "--description") }
|
|
487
|
-
: parsed.options.has("--clear-description") ? { description: null } : {}),
|
|
488
|
-
...(priority === undefined
|
|
489
|
-
? parsed.options.has("--clear-priority") ? { priority: null } : {}
|
|
490
|
-
: { priority }),
|
|
491
|
-
...(tags === undefined
|
|
492
|
-
? parsed.options.has("--clear-tags") ? { tags: null } : {}
|
|
493
|
-
: { tags }),
|
|
494
|
-
...(dueAt === undefined
|
|
495
|
-
? parsed.options.has("--clear-due-at") ? { dueAt: null } : {}
|
|
496
|
-
: { dueAt })
|
|
497
|
-
}, now);
|
|
569
|
+
const updated = updateTaskMetadata(current, patch, now);
|
|
498
570
|
tx.saveTask(updated);
|
|
499
571
|
recordTaskEvent(tx, updated.id, "task.updated", {
|
|
500
572
|
status: updated.status,
|
|
573
|
+
previous: editedFieldValues(current, Object.keys(patch)),
|
|
574
|
+
current: editedFieldValues(updated, Object.keys(patch)),
|
|
501
575
|
...(updated.type === undefined ? {} : { taskType: updated.type })
|
|
502
576
|
}, now);
|
|
503
577
|
enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
|
|
504
578
|
return updated;
|
|
505
579
|
});
|
|
506
580
|
notifyMailbox(options.runtime, taskMailbox(result.id), result.id);
|
|
507
|
-
return
|
|
581
|
+
return result;
|
|
508
582
|
}
|
|
509
583
|
export function submitOperatorMessage(body, taskId, store, options = {}) {
|
|
510
584
|
const now = clock(options);
|
|
@@ -513,16 +587,17 @@ export function submitOperatorMessage(body, taskId, store, options = {}) {
|
|
|
513
587
|
const task = requireTask(tx, taskId);
|
|
514
588
|
assertTaskOpen(task);
|
|
515
589
|
const message = appendMessage(tx, task.id, body, "operator", { type: "operator" }, now);
|
|
516
|
-
if (task.status
|
|
590
|
+
if (leaderWakingTaskStatus(task.status)) {
|
|
517
591
|
enqueueWork(tx, leaderMailbox(task.id), "operator-input", now, [messageRef(task.id, message.id)]);
|
|
518
592
|
}
|
|
519
593
|
return { task, message, created: false };
|
|
520
594
|
}
|
|
521
595
|
const created = createTaskAggregate(tx, titleFrom(body), {}, now);
|
|
522
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)]);
|
|
523
598
|
return { ...created, message, created: true };
|
|
524
599
|
});
|
|
525
|
-
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);
|
|
526
601
|
return result.created
|
|
527
602
|
? `Created Draft task ${result.task.id}: ${result.task.title}\nSubmitted message ${result.message.id}\n`
|
|
528
603
|
: `Submitted message ${result.message.id} to ${result.task.id}\n`;
|
|
@@ -649,7 +724,7 @@ function showTaskCommand(args, store, currentTaskCandidate) {
|
|
|
649
724
|
events: events.length,
|
|
650
725
|
workItems: work.length,
|
|
651
726
|
currentWorkItems: currentWorkItemCount,
|
|
652
|
-
|
|
727
|
+
runs: store.listRuns(task.id).length,
|
|
653
728
|
changeSets: changeSets.length,
|
|
654
729
|
integrations: integrations.length,
|
|
655
730
|
publications: publications.length,
|
|
@@ -690,7 +765,7 @@ function showTaskCommand(args, store, currentTaskCandidate) {
|
|
|
690
765
|
`Events: ${counts.events}`,
|
|
691
766
|
`Work items: ${counts.workItems}`,
|
|
692
767
|
`Current work items: ${currentWorkItemCount}`,
|
|
693
|
-
`
|
|
768
|
+
`AgentRuns: ${counts.runs}`,
|
|
694
769
|
`ChangeSets: ${counts.changeSets}`,
|
|
695
770
|
`Integration Attempts: ${counts.integrations}`,
|
|
696
771
|
`Publication references: ${counts.publications} (${verifiedMergedPublications} verified merged)`,
|
|
@@ -713,7 +788,7 @@ function activateTaskCommand(args, store, options) {
|
|
|
713
788
|
const task = requireTask(tx, args[0]);
|
|
714
789
|
if (task.status === "archived")
|
|
715
790
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
716
|
-
if (task.status === "
|
|
791
|
+
if (task.status === "cancelled")
|
|
717
792
|
throw usageError(`Task is retired: ${task.id}.`);
|
|
718
793
|
if (task.status === "completed") {
|
|
719
794
|
throw usageError(`Task ${task.id} is completed; use task reopen before activating it.`);
|
|
@@ -726,13 +801,26 @@ function activateTaskCommand(args, store, options) {
|
|
|
726
801
|
|| activation.task.id !== task.id
|
|
727
802
|
|| activation.task.status !== "active"
|
|
728
803
|
|| !isDeepStrictEqual(activation.task.workspaceIdentity, task.workspaceIdentity)
|
|
729
|
-
|| activation.path !== task.cwd
|
|
730
|
-
|| workspace === null
|
|
731
|
-
|| workspace.owner.type !== "task"
|
|
732
|
-
|| workspace.owner.taskId !== task.id
|
|
733
|
-
|| workspace.root !== task.cwd) {
|
|
804
|
+
|| activation.path !== task.cwd) {
|
|
734
805
|
throw usageError(`Task workspace activation proof does not match ${task.id}.`);
|
|
735
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
|
+
}
|
|
736
824
|
return { task, changed: activation.changed };
|
|
737
825
|
}
|
|
738
826
|
throw usageError(`Task ${task.id} activation requires atomic workspace adoption through the CLI preflight.`);
|
|
@@ -782,7 +870,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
782
870
|
};
|
|
783
871
|
}
|
|
784
872
|
// Completion is a Task decision only. The current Provider Turn remains
|
|
785
|
-
// responsible for closing its own
|
|
873
|
+
// responsible for closing its own AgentRun, and the reusable Session keeps
|
|
786
874
|
// its independent lifecycle.
|
|
787
875
|
// Issue 06: re-validate the full completion readiness inside the
|
|
788
876
|
// transaction (the CAS fence) after final-review preparation. This is the
|
|
@@ -795,7 +883,21 @@ function completeTaskCommand(args, store, options) {
|
|
|
795
883
|
if (!readiness.ready) {
|
|
796
884
|
throw usageError(formatCompletionBlockers(task.id, readiness.blockers));
|
|
797
885
|
}
|
|
798
|
-
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 });
|
|
799
901
|
tx.saveTask(completed);
|
|
800
902
|
tx.clearPendingWakeup(task.id);
|
|
801
903
|
tx.clearLeaderFailure(task.id);
|
|
@@ -826,6 +928,10 @@ function completeTaskCommand(args, store, options) {
|
|
|
826
928
|
const terminalEvent = recordTaskEvent(tx, task.id, "task.completed", {
|
|
827
929
|
by: actor,
|
|
828
930
|
summary,
|
|
931
|
+
...(completed.completionArtifactRefs === undefined ? {} : {
|
|
932
|
+
artifactRefs: JSON.stringify(completed.completionArtifactRefs)
|
|
933
|
+
}),
|
|
934
|
+
dispatchHistory: JSON.stringify(taskDispatchMailboxes(tx, task.id)),
|
|
829
935
|
...(completedProjectHeads === undefined
|
|
830
936
|
? {}
|
|
831
937
|
: { projectHeads: completedProjectHeads }),
|
|
@@ -842,7 +948,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
842
948
|
// discarded at this lifecycle boundary.
|
|
843
949
|
tx.removeWorkMailbox(taskMailbox(task.id));
|
|
844
950
|
// Role mailboxes are also derived wake state. A Worker result or runtime
|
|
845
|
-
// signal queued while the final
|
|
951
|
+
// signal queued while the final AgentRun was being settled must not survive a
|
|
846
952
|
// completed Task and become actionable after a later explicit reopen.
|
|
847
953
|
for (const role of roles) {
|
|
848
954
|
tx.removeWorkMailbox(roleMailbox(task.id, role.name));
|
|
@@ -887,19 +993,39 @@ function reopenTaskCommand(args, store, options) {
|
|
|
887
993
|
return { task, changed: false };
|
|
888
994
|
if (task.status === "archived")
|
|
889
995
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
890
|
-
if (task.status !== "completed")
|
|
891
|
-
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.
|
|
892
1007
|
const active = reopenTask(task, now);
|
|
893
1008
|
tx.saveTask(active);
|
|
894
1009
|
const reopenedReason = wakeReason("task-reopened");
|
|
895
|
-
|
|
1010
|
+
if (actor !== "leader") {
|
|
1011
|
+
enqueueWork(tx, leaderMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
1012
|
+
}
|
|
896
1013
|
enqueueWork(tx, taskMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
897
|
-
recordTaskEvent(tx, task.id, "task.reopened", {
|
|
898
|
-
|
|
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" };
|
|
899
1023
|
});
|
|
900
1024
|
if (result.changed) {
|
|
901
1025
|
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
902
|
-
|
|
1026
|
+
if (result.wakeLeader) {
|
|
1027
|
+
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
1028
|
+
}
|
|
903
1029
|
}
|
|
904
1030
|
return result.changed
|
|
905
1031
|
? `Reopened task ${result.task.id}\n`
|
|
@@ -914,13 +1040,17 @@ function archiveTaskCommand(args, store, options) {
|
|
|
914
1040
|
if (task.status === "archived")
|
|
915
1041
|
return { task, changed: false };
|
|
916
1042
|
if (task.status !== "completed"
|
|
917
|
-
&& task.status !== "
|
|
1043
|
+
&& task.status !== "cancelled") {
|
|
918
1044
|
throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
|
|
919
1045
|
}
|
|
920
1046
|
const remoteDelivery = request.disposition === "integrated"
|
|
921
1047
|
? assertTaskRemoteDeliveryProof(tx, task, options.archiveRemoteDeliveryProof, { forceUnverified: request.forceUnverified })
|
|
922
1048
|
: undefined;
|
|
923
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
|
+
}
|
|
924
1054
|
const unresolvedIntegration = tx.listIntegrationAttempts(task.id).find((integration) => (integration.status === "running"
|
|
925
1055
|
|| integration.status === "blocked"
|
|
926
1056
|
|| integration.status === "validating"));
|
|
@@ -937,9 +1067,9 @@ function archiveTaskCommand(args, store, options) {
|
|
|
937
1067
|
throw usageError(`Task ${task.id} still has managed worktrees; clean them before archiving.`);
|
|
938
1068
|
}
|
|
939
1069
|
const activeRole = tx.listRoles(task.id)
|
|
940
|
-
.find((role) => tx.
|
|
1070
|
+
.find((role) => tx.getActiveRun(task.id, role.name) !== null);
|
|
941
1071
|
if (activeRole !== undefined) {
|
|
942
|
-
throw usageError(`Task ${task.id} still has an active
|
|
1072
|
+
throw usageError(`Task ${task.id} still has an active AgentRun for Role ${activeRole.name}; `
|
|
943
1073
|
+ "stop its runtime before archiving.");
|
|
944
1074
|
}
|
|
945
1075
|
const liveSessionRole = tx.listRoles(task.id).find((role) => {
|
|
@@ -995,6 +1125,41 @@ function archiveTaskCommand(args, store, options) {
|
|
|
995
1125
|
? `Archived task ${result.task.id}\n`
|
|
996
1126
|
: `Task ${result.task.id} is already archived\n`;
|
|
997
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
|
+
}
|
|
998
1163
|
function retireTaskCommand(args, store, options) {
|
|
999
1164
|
const usage = "Task retire usage: yui task retire <task> (--summary <text>|--summary-file <path|->) [--replacement <task>].";
|
|
1000
1165
|
const parsed = parseTail(args, new Set(["--summary", "--summary-file", "--replacement"]), usage);
|
|
@@ -1006,7 +1171,7 @@ function retireTaskCommand(args, store, options) {
|
|
|
1006
1171
|
const result = store.transaction((tx) => {
|
|
1007
1172
|
const task = requireTask(tx, taskId);
|
|
1008
1173
|
const actor = taskActor(tx, options, task.id);
|
|
1009
|
-
if (task.status === "
|
|
1174
|
+
if (task.status === "cancelled") {
|
|
1010
1175
|
const same = task.retirementSummary === summary
|
|
1011
1176
|
&& task.replacementTaskId === replacementTaskId;
|
|
1012
1177
|
if (!same)
|
|
@@ -1035,12 +1200,12 @@ function retireTaskCommand(args, store, options) {
|
|
|
1035
1200
|
throw usageError(`Task ${task.id} has an active DurableJob: ${activeRetireJob.id}/${activeRetireJob.status}.`);
|
|
1036
1201
|
}
|
|
1037
1202
|
assertTaskRetirementProof(tx, task, options.taskRetirementProof);
|
|
1038
|
-
for (const run of tx.
|
|
1039
|
-
const terminal =
|
|
1203
|
+
for (const run of tx.listRuns(task.id).filter(({ status: runStatus }) => (runStatus === "active"))) {
|
|
1204
|
+
const terminal = terminalizeExactTaskRun(tx, {
|
|
1040
1205
|
taskId: task.id,
|
|
1041
1206
|
roleName: run.roleName,
|
|
1042
1207
|
agentId: run.effective.agentId,
|
|
1043
|
-
|
|
1208
|
+
runId: run.id,
|
|
1044
1209
|
mailboxDisposition: "discard",
|
|
1045
1210
|
outcome: {
|
|
1046
1211
|
status: "failed",
|
|
@@ -1049,11 +1214,11 @@ function retireTaskCommand(args, store, options) {
|
|
|
1049
1214
|
}
|
|
1050
1215
|
}, now);
|
|
1051
1216
|
if (terminal.disposition !== "applied") {
|
|
1052
|
-
throw usageError(`Task
|
|
1217
|
+
throw usageError(`Task AgentRun changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
1053
1218
|
}
|
|
1054
1219
|
}
|
|
1055
1220
|
for (const item of tx.listWorkItems(task.id)) {
|
|
1056
|
-
if (item.status === "
|
|
1221
|
+
if (item.status === "accepted" || item.status === "retired")
|
|
1057
1222
|
continue;
|
|
1058
1223
|
tx.saveWorkItem(task.id, updateWorkItemStatus(item, "retired", now, `Task retired: ${summary}`));
|
|
1059
1224
|
}
|
|
@@ -1074,12 +1239,14 @@ function retireTaskCommand(args, store, options) {
|
|
|
1074
1239
|
const retired = retireTask(task, {
|
|
1075
1240
|
by: actor,
|
|
1076
1241
|
summary,
|
|
1242
|
+
isolated: true,
|
|
1077
1243
|
...(replacementTaskId === undefined ? {} : { replacementTaskId })
|
|
1078
1244
|
}, now);
|
|
1079
1245
|
tx.saveTask(retired);
|
|
1080
1246
|
const terminalEvent = recordTaskEvent(tx, task.id, "task.retired", {
|
|
1081
1247
|
by: actor,
|
|
1082
1248
|
summary,
|
|
1249
|
+
isolationEstablished: "true",
|
|
1083
1250
|
...(replacementTaskId === undefined ? {} : { replacementTaskId })
|
|
1084
1251
|
}, now);
|
|
1085
1252
|
enqueueOperatorEvent(tx, terminalEvent, "task-terminal", now);
|
|
@@ -1145,10 +1312,13 @@ function formatProjectCommits(projects) {
|
|
|
1145
1312
|
export function validateTaskArchiveRequest(args, store, options = {}) {
|
|
1146
1313
|
const request = parseTaskArchiveArguments(args);
|
|
1147
1314
|
const task = requireTask(store, request.taskId);
|
|
1148
|
-
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
|
+
}
|
|
1149
1319
|
if (task.status !== "archived"
|
|
1150
1320
|
&& task.status !== "completed"
|
|
1151
|
-
&& task.status !== "
|
|
1321
|
+
&& task.status !== "cancelled") {
|
|
1152
1322
|
throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
|
|
1153
1323
|
}
|
|
1154
1324
|
if (task.status !== "archived") {
|
|
@@ -1171,9 +1341,55 @@ function reconcileTaskCommand(args, store, options) {
|
|
|
1171
1341
|
}
|
|
1172
1342
|
function taskMessageCommand(args, store, options) {
|
|
1173
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
|
+
}
|
|
1174
1390
|
if (command === "send") {
|
|
1175
|
-
const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->) [--wake-policy leader|none].";
|
|
1176
|
-
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);
|
|
1177
1393
|
if (parsed.positionals.length < 1 || parsed.positionals.length > 2)
|
|
1178
1394
|
throw usageError(usage);
|
|
1179
1395
|
const body = readCommandText(parsed.positionals[1], parsed.options.get("--body-file"), "--body", usage);
|
|
@@ -1188,42 +1404,24 @@ function taskMessageCommand(args, store, options) {
|
|
|
1188
1404
|
else {
|
|
1189
1405
|
throw usageError(`--wake-policy must be 'leader' or 'none': ${wakePolicyRaw}.`);
|
|
1190
1406
|
}
|
|
1191
|
-
const
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
// compatibility) enqueues Leader work. `wakePolicy=none` persists the
|
|
1203
|
-
// message as context without waking the Leader.
|
|
1204
|
-
if (task.status === "active"
|
|
1205
|
-
&& actor !== "leader"
|
|
1206
|
-
&& wakePolicy !== "none") {
|
|
1207
|
-
enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [messageRef(task.id, message.id)], {
|
|
1208
|
-
source: actor,
|
|
1209
|
-
dedupeKey: `message:${task.id}:${message.id}`
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
return { task, message, actor };
|
|
1213
|
-
});
|
|
1214
|
-
if (result.actor !== "leader") {
|
|
1215
|
-
notifyMailbox(options.runtime, result.task.status === "active"
|
|
1216
|
-
? leaderMailbox(result.task.id)
|
|
1217
|
-
: taskMailbox(result.task.id), result.task.id);
|
|
1218
|
-
}
|
|
1219
|
-
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 });
|
|
1220
1418
|
}
|
|
1221
1419
|
if (command === "list") {
|
|
1222
1420
|
const messageListUsage = "Task message list usage: yui task message list <id> [--after <timestamp>] [--limit <n>].";
|
|
1223
1421
|
const parsed = parseTail(rest, new Set(["--after", "--limit"]), messageListUsage);
|
|
1224
1422
|
exactPositionals(parsed.positionals, 1, messageListUsage);
|
|
1225
1423
|
const task = requireTask(store, parsed.positionals[0]);
|
|
1226
|
-
let messages = store
|
|
1424
|
+
let messages = listContextMessages(store, task.id, options.environment);
|
|
1227
1425
|
const after = optionalNonEmptyOption(parsed.options, "--after");
|
|
1228
1426
|
if (after !== undefined) {
|
|
1229
1427
|
const afterMs = Date.parse(after);
|
|
@@ -1271,6 +1469,100 @@ function taskMessageCommand(args, store, options) {
|
|
|
1271
1469
|
? "Task message command is required."
|
|
1272
1470
|
: `Unknown command: task message ${command}`);
|
|
1273
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
|
+
}
|
|
1274
1566
|
function updateMessage(args, store, options) {
|
|
1275
1567
|
const usage = "Task message update usage: yui task message update <task>/<message> (<body>|--body-file <path|->) [--wake-policy leader|none].";
|
|
1276
1568
|
const parsed = parseTail(args, new Set(["--body-file", "--wake-policy"]), usage);
|
|
@@ -1314,9 +1606,14 @@ function updateMessage(args, store, options) {
|
|
|
1314
1606
|
updatedBy: actor,
|
|
1315
1607
|
...(wakePolicy === undefined ? {} : { wakePolicy })
|
|
1316
1608
|
}, now);
|
|
1317
|
-
|
|
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 };
|
|
1318
1614
|
});
|
|
1319
|
-
options.runtime
|
|
1615
|
+
notifyMailbox(options.runtime, result.queuedForLeader
|
|
1616
|
+
? leaderMailbox(result.task.id) : taskMailbox(result.task.id), result.task.id);
|
|
1320
1617
|
return output(`Updated Task Message ${result.task.id}/${result.message.id}\n`, {
|
|
1321
1618
|
message: result.message
|
|
1322
1619
|
});
|
|
@@ -1345,7 +1642,7 @@ function retireMessage(args, store, options) {
|
|
|
1345
1642
|
// Remove an isolated pending wake for this exact directive. A merged batch
|
|
1346
1643
|
// is retained because its other signals remain actionable; context and
|
|
1347
1644
|
// actionability projections still filter the retired message below.
|
|
1348
|
-
if (task.status
|
|
1645
|
+
if (leaderWakingTaskStatus(task.status)) {
|
|
1349
1646
|
try {
|
|
1350
1647
|
settleExactWorkExecution(tx, leaderMailbox(task.id), messageRef(task.id, message.id));
|
|
1351
1648
|
}
|
|
@@ -1400,9 +1697,9 @@ function taskRoleCommand(args, store, options) {
|
|
|
1400
1697
|
if (command === "status")
|
|
1401
1698
|
return taskRoleStatus(rest, store, options);
|
|
1402
1699
|
if (command === "show")
|
|
1403
|
-
return
|
|
1700
|
+
return showTaskRole(rest, store);
|
|
1404
1701
|
if (command === "update")
|
|
1405
|
-
return
|
|
1702
|
+
return updateTaskRole(rest, store, options);
|
|
1406
1703
|
if (command === "remove")
|
|
1407
1704
|
return output(removeTaskRole(rest, store, options));
|
|
1408
1705
|
if (command === "bind")
|
|
@@ -1427,20 +1724,76 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1427
1724
|
exactPositionals(rest, 2, "Task Role Session inspect usage: yui task role session inspect <task> <role>.");
|
|
1428
1725
|
const task = requireTask(store, rest[0]);
|
|
1429
1726
|
const role = requireRole(store, task.id, rest[1]);
|
|
1430
|
-
|
|
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
|
+
}
|
|
1431
1733
|
const sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
1432
1734
|
const active = sessions?.sessions[sessions.activeAgentId] ?? null;
|
|
1433
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);
|
|
1434
1743
|
return output(active === null
|
|
1435
|
-
? `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`)
|
|
1436
1746
|
: [
|
|
1437
1747
|
`Session ${task.id}/${role.name}`,
|
|
1438
|
-
|
|
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)`,
|
|
1439
1752
|
`Native id: ${active.nativeSessionId}`,
|
|
1440
|
-
`Host activation: ${active.runtimeGenerationId ?? "none"}`,
|
|
1441
1753
|
`Session: ${active.status}${active.endReason === undefined ? "" : `/${active.endReason}`}`,
|
|
1442
|
-
`
|
|
1443
|
-
|
|
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);
|
|
1444
1797
|
}
|
|
1445
1798
|
if (command === "stop") {
|
|
1446
1799
|
const usage = "Task Role Session stop usage: yui task role session stop <task> <role> --reason <text>.";
|
|
@@ -1450,36 +1803,26 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1450
1803
|
const now = clock(options);
|
|
1451
1804
|
const request = store.transaction((tx) => {
|
|
1452
1805
|
const task = requireTask(tx, parsed.positionals[0]);
|
|
1453
|
-
if (
|
|
1454
|
-
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);
|
|
1455
1808
|
}
|
|
1456
1809
|
const actor = taskActor(tx, options, task.id);
|
|
1457
1810
|
const role = requireRole(tx, task.id, parsed.positionals[1]);
|
|
1458
1811
|
if (actor === "leader" && role.name === LEADER_ROLE) {
|
|
1459
|
-
throw usageError("
|
|
1460
|
-
}
|
|
1461
|
-
if (tx.getActiveTurn(task.id, role.name) !== null) {
|
|
1462
|
-
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);
|
|
1463
1813
|
}
|
|
1464
1814
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1465
|
-
const session = sessions
|
|
1466
|
-
if (session === undefined || session.
|
|
1815
|
+
const session = taskRoleControlTarget(sessions);
|
|
1816
|
+
if (session === undefined || session.adapterId === undefined) {
|
|
1467
1817
|
throw usageError(`Task Role has no active Session: ${task.id}/${role.name}.`, usage);
|
|
1468
1818
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
taskId: task.id,
|
|
1472
|
-
roleName: role.name
|
|
1473
|
-
}));
|
|
1474
|
-
if (lifecycle !== null && workMailboxHasWork(lifecycle)) {
|
|
1475
|
-
throw usageError(`Task Role Session lifecycle is busy: ${task.id}/${role.name}.`, usage);
|
|
1476
|
-
}
|
|
1819
|
+
// A repeated stop is a recovery request, not contention with its own
|
|
1820
|
+
// earlier cleanup obligation.
|
|
1477
1821
|
recordTaskEvent(tx, task.id, "runtime.session-stop-requested", {
|
|
1478
1822
|
roleName: role.name,
|
|
1479
1823
|
agentId: session.agentId,
|
|
1480
1824
|
adapterId: session.adapterId,
|
|
1481
1825
|
nativeSessionId: session.nativeSessionId,
|
|
1482
|
-
runtimeGenerationId: session.runtimeGenerationId ?? "",
|
|
1483
1826
|
reason,
|
|
1484
1827
|
requestedBy: actor
|
|
1485
1828
|
}, now);
|
|
@@ -1489,7 +1832,6 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1489
1832
|
agentId: session.agentId,
|
|
1490
1833
|
adapterId: session.adapterId,
|
|
1491
1834
|
nativeSessionId: session.nativeSessionId,
|
|
1492
|
-
...(session.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: session.runtimeGenerationId }),
|
|
1493
1835
|
sessionUpdatedAt: session.updatedAt
|
|
1494
1836
|
};
|
|
1495
1837
|
});
|
|
@@ -1607,7 +1949,7 @@ function listTaskRoles(args, store, options) {
|
|
|
1607
1949
|
status.health,
|
|
1608
1950
|
taskRoleOpenInputLabel(status),
|
|
1609
1951
|
taskRoleActiveWorkLabel(status),
|
|
1610
|
-
|
|
1952
|
+
taskRoleLastRunLabel(status),
|
|
1611
1953
|
taskRoleNativeSessionLabel(status),
|
|
1612
1954
|
taskRoleTmuxLabel(status)
|
|
1613
1955
|
]), defaultTableWidth())}\n`, { roles: statuses });
|
|
@@ -1625,9 +1967,15 @@ function showTaskRole(args, store) {
|
|
|
1625
1967
|
exactPositionals(args, 2, "Task role show usage: yui task role show <task> <role>.");
|
|
1626
1968
|
const task = requireTask(store, args[0]);
|
|
1627
1969
|
const role = requireRole(store, task.id, args[1]);
|
|
1628
|
-
|
|
1970
|
+
const sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
1971
|
+
return output(renderRoleDetails(`Task Role: ${role.name}`, role, {
|
|
1629
1972
|
kind: "task",
|
|
1630
|
-
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 }))
|
|
1631
1979
|
});
|
|
1632
1980
|
}
|
|
1633
1981
|
function updateTaskRole(args, store, options) {
|
|
@@ -1638,8 +1986,11 @@ function updateTaskRole(args, store, options) {
|
|
|
1638
1986
|
}
|
|
1639
1987
|
const parsed = parseRoleOptions(tail, new Map([
|
|
1640
1988
|
...roleOptionSpecs({ update: true, includeAgent: true }),
|
|
1641
|
-
["--profile", "value"]
|
|
1989
|
+
["--profile", "value"],
|
|
1990
|
+
["--environment", "value"],
|
|
1991
|
+
["--managed-environment", "flag"]
|
|
1642
1992
|
]), usage);
|
|
1993
|
+
validateTaskRoleEnvironmentOptions(parsed, usage);
|
|
1643
1994
|
if (parsed.has("--agent") && (parsed.one("--agent")?.trim().length ?? 0) === 0) {
|
|
1644
1995
|
throw usageError("--agent is required.", usage);
|
|
1645
1996
|
}
|
|
@@ -1652,7 +2003,9 @@ function updateTaskRole(args, store, options) {
|
|
|
1652
2003
|
assertTaskOpen(task);
|
|
1653
2004
|
taskActor(tx, options, task.id);
|
|
1654
2005
|
const role = requireRole(tx, task.id, roleName);
|
|
1655
|
-
const
|
|
2006
|
+
const changesEnvironment = parsed.has("--environment") || parsed.has("--managed-environment");
|
|
2007
|
+
const changesLaunchContext = hasRoleLaunchContextOptions(parsed) || parsed.has("--profile")
|
|
2008
|
+
|| changesEnvironment;
|
|
1656
2009
|
const changesAgentConfig = hasAgentConfigOptions(parsed);
|
|
1657
2010
|
if (changesLaunchContext || changesAgentConfig) {
|
|
1658
2011
|
assertRoleRuntimeMutationAllowed(tx, {
|
|
@@ -1660,13 +2013,6 @@ function updateTaskRole(args, store, options) {
|
|
|
1660
2013
|
taskId: task.id,
|
|
1661
2014
|
roleName: role.name
|
|
1662
2015
|
}, "desired launch configuration update");
|
|
1663
|
-
assertLiveRoleSessionAcknowledged({
|
|
1664
|
-
sessions: tx.getTaskRoleSessionSet(task.id, role.name),
|
|
1665
|
-
roleName: role.name,
|
|
1666
|
-
desiredRevision: role.launchRevision,
|
|
1667
|
-
acknowledged: parsed.has(LIVE_SESSION_ACKNOWLEDGEMENT_OPTION),
|
|
1668
|
-
stopCommand: `yui task role session stop ${task.id} ${role.name} --reason "<decision>"`
|
|
1669
|
-
});
|
|
1670
2016
|
}
|
|
1671
2017
|
const profileId = parsed.one("--profile");
|
|
1672
2018
|
const agentProfile = profileId === undefined
|
|
@@ -1684,7 +2030,7 @@ function updateTaskRole(args, store, options) {
|
|
|
1684
2030
|
[bindingUpdate.agentId]: bindingUpdate.binding
|
|
1685
2031
|
}
|
|
1686
2032
|
}, now);
|
|
1687
|
-
|
|
2033
|
+
let next = updateRole(withBinding, {
|
|
1688
2034
|
...roleProfilePatch(parsed)
|
|
1689
2035
|
}, now);
|
|
1690
2036
|
if (bindingUpdate !== undefined) {
|
|
@@ -1693,16 +2039,32 @@ function updateTaskRole(args, store, options) {
|
|
|
1693
2039
|
if (changesLaunchContext) {
|
|
1694
2040
|
validateConfiguredRoleSkills(options.yuiHome, next.skills ?? []);
|
|
1695
2041
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
+
});
|
|
1699
2052
|
return next;
|
|
1700
2053
|
});
|
|
1701
2054
|
notifyMailbox(options.runtime, taskMailbox(updated.taskId), updated.taskId);
|
|
1702
|
-
|
|
2055
|
+
const sessions = store.getTaskRoleSessionSet(updated.taskId, updated.name);
|
|
2056
|
+
return output(renderRoleDetails(`Updated Task Role: ${updated.name}`, updated, {
|
|
1703
2057
|
kind: "task",
|
|
1704
|
-
sessions
|
|
1705
|
-
});
|
|
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
|
+
}
|
|
1706
2068
|
}
|
|
1707
2069
|
function removeTaskRole(args, store, options) {
|
|
1708
2070
|
exactPositionals(args, 2, "Task role remove usage: yui task role remove <task> <role>.");
|
|
@@ -1719,8 +2081,8 @@ function removeTaskRole(args, store, options) {
|
|
|
1719
2081
|
taskId: task.id,
|
|
1720
2082
|
roleName: role.name
|
|
1721
2083
|
}, "removal");
|
|
1722
|
-
if (tx.
|
|
1723
|
-
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}.`);
|
|
1724
2086
|
}
|
|
1725
2087
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1726
2088
|
if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status === "active")) {
|
|
@@ -1749,7 +2111,7 @@ function bindTaskRole(args, store, options) {
|
|
|
1749
2111
|
}, "desired Agent binding update");
|
|
1750
2112
|
const agent = requireAgent(tx, args[2]);
|
|
1751
2113
|
const binding = role.agentBindings[agent.id]
|
|
1752
|
-
?? createRoleAgentBinding(
|
|
2114
|
+
?? createRoleAgentBinding(agent);
|
|
1753
2115
|
const bound = updateRole(role, {
|
|
1754
2116
|
agentBindings: { ...role.agentBindings, [agent.id]: binding }
|
|
1755
2117
|
}, now);
|
|
@@ -1769,7 +2131,7 @@ function bindTaskRole(args, store, options) {
|
|
|
1769
2131
|
const switched = (() => {
|
|
1770
2132
|
try {
|
|
1771
2133
|
return switchActiveRoleAgent(bound, existing, agent.id, {
|
|
1772
|
-
|
|
2134
|
+
activeRun: tx.getActiveRun(task.id, role.name) !== null,
|
|
1773
2135
|
nativeProcessRunning: currentSession !== undefined
|
|
1774
2136
|
&& currentSession.status === "active"
|
|
1775
2137
|
}, now);
|
|
@@ -1782,6 +2144,10 @@ function bindTaskRole(args, store, options) {
|
|
|
1782
2144
|
recordTaskEvent(tx, task.id, "role.agent-bound", {
|
|
1783
2145
|
role: switched.role.name,
|
|
1784
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 } : {}),
|
|
1785
2151
|
...roleLaunchEventPayload(switched.role, switched.sessions)
|
|
1786
2152
|
}, now);
|
|
1787
2153
|
return { role: switched.role, mode: switched.mode };
|
|
@@ -1851,20 +2217,13 @@ function transferTaskRoleAuthority(args, store, options, action) {
|
|
|
1851
2217
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1852
2218
|
const session = sessions?.sessions[role.activeAgentId];
|
|
1853
2219
|
const binding = sessions?.providerBinding;
|
|
1854
|
-
if (sessions === null || sessions === undefined || session === undefined
|
|
1855
|
-
|| binding === null || binding === undefined
|
|
1856
|
-
|| session.runtimeGenerationId === undefined
|
|
1857
|
-
|| session.status === "ended") {
|
|
2220
|
+
if (sessions === null || sessions === undefined || session === undefined || binding === null || binding === undefined || session.status === "ended") {
|
|
1858
2221
|
throw new Error(`Task Role has no live managed Provider: ${task.id}/${role.name}.`);
|
|
1859
2222
|
}
|
|
1860
|
-
const activation = currentProviderActivation(binding);
|
|
1861
|
-
if (activation === null) {
|
|
1862
|
-
throw new Error(`Provider Activation is not live: ${task.id}/${role.name}.`);
|
|
1863
|
-
}
|
|
1864
2223
|
if (action === "takeover") {
|
|
1865
|
-
const
|
|
1866
|
-
if (
|
|
1867
|
-
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}.`);
|
|
1868
2227
|
}
|
|
1869
2228
|
}
|
|
1870
2229
|
if (action === "takeover"
|
|
@@ -1885,7 +2244,7 @@ function transferTaskRoleAuthority(args, store, options, action) {
|
|
|
1885
2244
|
expectedEpoch: binding.authority.epoch,
|
|
1886
2245
|
expectedOwner: binding.authority.owner,
|
|
1887
2246
|
owner: desiredOwner,
|
|
1888
|
-
holderId: action === "takeover" ? `human:${randomUUID()}` :
|
|
2247
|
+
holderId: action === "takeover" ? `human:${randomUUID()}` : "controller",
|
|
1889
2248
|
changedAt: now.toISOString()
|
|
1890
2249
|
});
|
|
1891
2250
|
const authority = updatedBinding.authority;
|
|
@@ -1906,7 +2265,6 @@ function transferTaskRoleAuthority(args, store, options, action) {
|
|
|
1906
2265
|
action,
|
|
1907
2266
|
taskId: task.id,
|
|
1908
2267
|
roleName: role.name,
|
|
1909
|
-
runtimeGenerationId: session.runtimeGenerationId,
|
|
1910
2268
|
nativeSessionId: session.nativeSessionId,
|
|
1911
2269
|
authority: {
|
|
1912
2270
|
epoch: authority.epoch,
|
|
@@ -1939,6 +2297,8 @@ function taskWorkCommand(args, store, options) {
|
|
|
1939
2297
|
return output(updateWorkScope(rest, store, options));
|
|
1940
2298
|
if (command === "dispatch")
|
|
1941
2299
|
return output(dispatchWork(rest, store, options));
|
|
2300
|
+
if (command === "synthesize")
|
|
2301
|
+
return synthesizeRuns(rest, "workItem", store, options);
|
|
1942
2302
|
if (command === "review") {
|
|
1943
2303
|
return rest[0] === "retry"
|
|
1944
2304
|
? retryFailedTaskReviewRound(rest.slice(1), store, options)
|
|
@@ -1978,14 +2338,10 @@ function editWork(args, store, options) {
|
|
|
1978
2338
|
const result = store.transaction((tx) => {
|
|
1979
2339
|
const item = requireWorkItem(tx, parsed.positionals[0], options);
|
|
1980
2340
|
const task = requireTask(tx, item.taskId);
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
assertDraftTaskExecutionFree(tx, task);
|
|
2341
|
+
assertTaskOpen(task);
|
|
2342
|
+
if (task.status === "draft")
|
|
2343
|
+
assertDraftTaskExecutionFree(tx, task);
|
|
1985
2344
|
const actor = taskActor(tx, options, task.id);
|
|
1986
|
-
if (actor !== "user" && actor !== "operator") {
|
|
1987
|
-
throw usageError("Only the user or Operator may edit a Draft Work Item.");
|
|
1988
|
-
}
|
|
1989
2345
|
if (item.status === "retired") {
|
|
1990
2346
|
throw usageError(`Work Item is retired: ${item.id}.`);
|
|
1991
2347
|
}
|
|
@@ -2006,7 +2362,7 @@ function editWork(args, store, options) {
|
|
|
2006
2362
|
: parsed.options.has("--clear-role") ? null : undefined;
|
|
2007
2363
|
if (typeof assignee === "string")
|
|
2008
2364
|
requireRole(tx, task.id, assignee);
|
|
2009
|
-
const updated =
|
|
2365
|
+
const updated = editWorkItemDefinition(item, {
|
|
2010
2366
|
...(parsed.options.has("--title")
|
|
2011
2367
|
? { title: requiredOption(parsed.options, "--title") }
|
|
2012
2368
|
: {}),
|
|
@@ -2023,7 +2379,21 @@ function editWork(args, store, options) {
|
|
|
2023
2379
|
...(baseRefs === undefined ? {} : { baseRefs }),
|
|
2024
2380
|
...(assignee === undefined ? {} : { assignee })
|
|
2025
2381
|
}, now);
|
|
2026
|
-
|
|
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
|
+
}
|
|
2027
2397
|
tx.saveWorkItem(task.id, updated);
|
|
2028
2398
|
const changedFields = [
|
|
2029
2399
|
parsed.options.has("--title") ? "title" : undefined,
|
|
@@ -2043,8 +2413,13 @@ function editWork(args, store, options) {
|
|
|
2043
2413
|
workItemId: updated.id,
|
|
2044
2414
|
revision: String(updated.revision),
|
|
2045
2415
|
fields: changedFields.join(","),
|
|
2416
|
+
previous: editedFieldValues(item, changedFields),
|
|
2417
|
+
current: editedFieldValues(updated, changedFields),
|
|
2046
2418
|
editedBy: actor
|
|
2047
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
|
+
}
|
|
2048
2423
|
return { task, item: updated };
|
|
2049
2424
|
});
|
|
2050
2425
|
options.runtime?.notifyStateChanged(result.task.id);
|
|
@@ -2121,8 +2496,8 @@ function updateWorkScope(args, store, options) {
|
|
|
2121
2496
|
const item = requireWorkItem(tx, parsed.positionals[0], options);
|
|
2122
2497
|
const task = requireTask(tx, item.taskId);
|
|
2123
2498
|
taskActor(tx, options, task.id);
|
|
2124
|
-
if (tx.
|
|
2125
|
-
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}.`);
|
|
2126
2501
|
}
|
|
2127
2502
|
const requestedProjectIds = (parsed.multiOptions.get("--project") ?? []).map((reference) => {
|
|
2128
2503
|
const project = resolveProject(task.projectBindings.map(({ projectId }) => requireProject(tx, projectId)), reference);
|
|
@@ -2152,12 +2527,16 @@ function updateWorkScope(args, store, options) {
|
|
|
2152
2527
|
return `${updated.changed ? "Updated" : "Unchanged"} Work Item Project scope ${updated.item.id}: ${updated.item.writeProjectIds.join(", ") || "read-only"}\n`;
|
|
2153
2528
|
}
|
|
2154
2529
|
function updateWork(args, store, options) {
|
|
2155
|
-
const usage = "Task work update usage: yui task work update <task>/<work> <todo|running|done|failed> [--summary <text>].";
|
|
2156
|
-
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);
|
|
2157
2532
|
exactPositionals(parsed.positionals, 2, usage);
|
|
2158
2533
|
const requested = parsed.positionals[1];
|
|
2159
2534
|
const status = parseWorkStatus(requested);
|
|
2160
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
|
+
}
|
|
2161
2540
|
if (["completed", "failed"].includes(status)
|
|
2162
2541
|
&& summary === undefined) {
|
|
2163
2542
|
throw usageError(`--summary is required when work becomes ${requested}.`);
|
|
@@ -2167,19 +2546,16 @@ function updateWork(args, store, options) {
|
|
|
2167
2546
|
const current = requireWorkItem(tx, parsed.positionals[0], options);
|
|
2168
2547
|
const task = requireTask(tx, current.taskId);
|
|
2169
2548
|
assertTaskOpen(task);
|
|
2549
|
+
const artifactRefs = artifactIds.length === 0 ? undefined : fixedArtifactRefs(tx, task.id, artifactIds);
|
|
2170
2550
|
if (current.assignee === undefined) {
|
|
2171
2551
|
taskActor(tx, options, task.id);
|
|
2172
2552
|
if (status === "running") {
|
|
2173
2553
|
assertWorkItemDependenciesCompletedForCommand(tx, current);
|
|
2174
2554
|
}
|
|
2175
|
-
if (status === "completed" && current.status === "awaiting_acceptance") {
|
|
2176
|
-
throw usageError(`Work Item ${current.id} is awaiting acceptance; use task work accept `
|
|
2177
|
-
+ "after the required ReviewRound and Integration evidence.");
|
|
2178
|
-
}
|
|
2179
2555
|
const configuredReview = status === "completed" && !isTerminalWorkItemStatus(current.status)
|
|
2180
2556
|
? tx.getReviewConfig()
|
|
2181
2557
|
: null;
|
|
2182
|
-
const taskFinalContract = status === "completed" && current.status === "
|
|
2558
|
+
const taskFinalContract = status === "completed" && current.status === "open"
|
|
2183
2559
|
? taskFinalReviewContractForMutation(tx, task.id, options)
|
|
2184
2560
|
: undefined;
|
|
2185
2561
|
const candidatePolicy = taskFinalContract === undefined
|
|
@@ -2189,9 +2565,7 @@ function updateWork(args, store, options) {
|
|
|
2189
2565
|
&& current.writeProjectIds.length > 0;
|
|
2190
2566
|
const metadataOnlyTaskFinalDelivery = taskFinalContract !== undefined
|
|
2191
2567
|
&& current.assignee === undefined;
|
|
2192
|
-
const candidateRequired = status === "completed"
|
|
2193
|
-
&& current.status === "running"
|
|
2194
|
-
&& (projectDelivery || candidatePolicy !== null);
|
|
2568
|
+
const candidateRequired = status === "completed" && current.status === "open";
|
|
2195
2569
|
const developWorkspace = tx.getWorkItemWorkspace(task.id, current.id);
|
|
2196
2570
|
if (status === "completed"
|
|
2197
2571
|
&& projectDelivery
|
|
@@ -2203,6 +2577,7 @@ function updateWork(args, store, options) {
|
|
|
2203
2577
|
? submitWorkItemCandidate(current, {
|
|
2204
2578
|
summary: summary,
|
|
2205
2579
|
source: { type: "direct" },
|
|
2580
|
+
...(artifactRefs === undefined ? {} : { artifactRefs }),
|
|
2206
2581
|
...(candidatePolicy === null ? {} : { reviewPolicy: candidatePolicy }),
|
|
2207
2582
|
...(taskFinalContract === undefined
|
|
2208
2583
|
? {}
|
|
@@ -2217,9 +2592,7 @@ function updateWork(args, store, options) {
|
|
|
2217
2592
|
? {}
|
|
2218
2593
|
: { taskMainSnapshot: options.directTaskMainSnapshot })
|
|
2219
2594
|
}, now)
|
|
2220
|
-
: current
|
|
2221
|
-
? retryFailedWorkItem(current, now)
|
|
2222
|
-
: updateWorkItemStatus(current, status, now, isTerminalWorkItemStatus(status) ? summary : undefined);
|
|
2595
|
+
: updateWorkItemStatus(current, "open", now);
|
|
2223
2596
|
tx.saveWorkItem(task.id, updated);
|
|
2224
2597
|
if (summary !== undefined) {
|
|
2225
2598
|
recordTaskEvent(tx, task.id, "work.updated", {
|
|
@@ -2242,29 +2615,29 @@ function updateWork(args, store, options) {
|
|
|
2242
2615
|
};
|
|
2243
2616
|
}
|
|
2244
2617
|
taskActor(tx, options, task.id);
|
|
2245
|
-
if (status !== "completed" || current.status !== "
|
|
2246
|
-
throw usageError(`Assigned Work Item ${current.id} can only submit a completed direct
|
|
2247
|
-
+ "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.");
|
|
2248
2621
|
}
|
|
2249
|
-
if (tx.
|
|
2250
|
-
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}.`);
|
|
2251
2624
|
}
|
|
2252
2625
|
const sourceGroup = currentWorkItemExecutionGroup(current);
|
|
2253
|
-
const
|
|
2254
|
-
&&
|
|
2255
|
-
&&
|
|
2256
|
-
&&
|
|
2257
|
-
&&
|
|
2258
|
-
&&
|
|
2259
|
-
&&
|
|
2260
|
-
&&
|
|
2261
|
-
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) {
|
|
2262
2635
|
throw usageError(sourceGroup === undefined
|
|
2263
|
-
? `Work Item ${current.id} has no completed direct main
|
|
2264
|
-
: `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}.`);
|
|
2265
2638
|
}
|
|
2266
|
-
if (
|
|
2267
|
-
throw dataError(`Completed WorkItem main
|
|
2639
|
+
if (mainRun.result === undefined) {
|
|
2640
|
+
throw dataError(`Completed WorkItem main AgentRun has no result: ${mainRun.id}.`);
|
|
2268
2641
|
}
|
|
2269
2642
|
const configuredReview = tx.getReviewConfig();
|
|
2270
2643
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
@@ -2280,8 +2653,9 @@ function updateWork(args, store, options) {
|
|
|
2280
2653
|
throw usageError(`Project-backed Work Item ${current.id} must be isolated before Candidate submission.`);
|
|
2281
2654
|
}
|
|
2282
2655
|
const updated = submitWorkItemCandidate(current, {
|
|
2283
|
-
summary: `Result from
|
|
2284
|
-
source: { type: "
|
|
2656
|
+
summary: `Result from AgentRun ${mainRun.id}.`,
|
|
2657
|
+
source: { type: "run", runId: mainRun.id },
|
|
2658
|
+
...(artifactRefs === undefined ? {} : { artifactRefs }),
|
|
2285
2659
|
...(candidatePolicy === null ? {} : { reviewPolicy: candidatePolicy }),
|
|
2286
2660
|
...(taskFinalContract === undefined
|
|
2287
2661
|
? {}
|
|
@@ -2299,12 +2673,12 @@ function updateWork(args, store, options) {
|
|
|
2299
2673
|
workItemId: updated.id,
|
|
2300
2674
|
status: updated.status,
|
|
2301
2675
|
summary: summary,
|
|
2302
|
-
|
|
2676
|
+
runId: mainRun.id,
|
|
2303
2677
|
...leaderActionEventPayload(tx, task.id, options)
|
|
2304
2678
|
}, now);
|
|
2305
2679
|
enqueueWork(tx, taskMailbox(task.id), "work-updated", now, [
|
|
2306
2680
|
workItemRef(task.id, updated.id),
|
|
2307
|
-
|
|
2681
|
+
runRef(task.id, mainRun.id)
|
|
2308
2682
|
]);
|
|
2309
2683
|
const reviewDispatch = candidatePolicy?.trigger === "always"
|
|
2310
2684
|
? queueReviewRound(tx, updated, candidatePolicy, "policy", now)
|
|
@@ -2316,7 +2690,7 @@ function updateWork(args, store, options) {
|
|
|
2316
2690
|
};
|
|
2317
2691
|
});
|
|
2318
2692
|
notifyMailbox(options.runtime, taskMailbox(result.item.taskId), result.item.taskId);
|
|
2319
|
-
if (result.item.status === "
|
|
2693
|
+
if ((result.item.status === "open" && result.item.candidates.length > 0) && status === "completed") {
|
|
2320
2694
|
const failure = result.reviewDispatch?.round.status === "failed"
|
|
2321
2695
|
? `Review could not start: ${result.reviewDispatch.round.failure?.message ?? result.reviewDispatch.round.id}\n`
|
|
2322
2696
|
: "";
|
|
@@ -2351,13 +2725,13 @@ function dispatchWork(args, store, options) {
|
|
|
2351
2725
|
throw usageError(`Work Item has no Task Role assignee: ${item.id}. `
|
|
2352
2726
|
+ `The Task Leader must run "yui task work update ${item.id} running" and execute it directly.`);
|
|
2353
2727
|
}
|
|
2354
|
-
const lanePlan = planReplicatedWorkItemLanes(item.assignee, requestedLaneRoles, `execution-group-${tx.
|
|
2728
|
+
const lanePlan = planReplicatedWorkItemLanes(item.assignee, requestedLaneRoles, `execution-group-${tx.peekNextRunId(task.id)}`);
|
|
2355
2729
|
const currentGroup = currentWorkItemExecutionGroup(item);
|
|
2356
|
-
if (item.status !== "
|
|
2730
|
+
if (item.status !== "open") {
|
|
2357
2731
|
throw usageError(`Work item ${item.id} cannot be dispatched from ${item.status}.`);
|
|
2358
2732
|
}
|
|
2359
2733
|
if (currentGroup !== undefined && !workItemExecutionGroupSettled(currentGroup)) {
|
|
2360
|
-
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.`);
|
|
2361
2735
|
}
|
|
2362
2736
|
assertWorkItemDependenciesCompletedForCommand(tx, item);
|
|
2363
2737
|
const leaderOwned = item.assignee === "leader";
|
|
@@ -2381,18 +2755,16 @@ function dispatchWork(args, store, options) {
|
|
|
2381
2755
|
}
|
|
2382
2756
|
const roles = lanePlan.roles.map((name) => requireRole(tx, task.id, name));
|
|
2383
2757
|
for (const role of roles) {
|
|
2384
|
-
if (tx.
|
|
2385
|
-
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.`);
|
|
2386
2760
|
}
|
|
2387
2761
|
}
|
|
2388
2762
|
const rawInput = trimmed(parsed.options.get("--input")) ?? item.objective;
|
|
2389
|
-
let workItemForDispatch = item
|
|
2390
|
-
? retryFailedWorkItem(item, now)
|
|
2391
|
-
: item;
|
|
2763
|
+
let workItemForDispatch = prepareWorkItemDispatch(item, now);
|
|
2392
2764
|
if (lanePlan.roles.length === 0) {
|
|
2393
2765
|
const role = requireRole(tx, task.id, item.assignee);
|
|
2394
|
-
if (tx.
|
|
2395
|
-
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.`);
|
|
2396
2768
|
}
|
|
2397
2769
|
const effective = resolveEffectiveLaunch({
|
|
2398
2770
|
role,
|
|
@@ -2400,8 +2772,8 @@ function dispatchWork(args, store, options) {
|
|
|
2400
2772
|
workspace,
|
|
2401
2773
|
workItemWriteProjectIds: item.writeProjectIds
|
|
2402
2774
|
});
|
|
2403
|
-
const
|
|
2404
|
-
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({
|
|
2405
2777
|
source: { type: "yui", channel: "workitem-dispatch" },
|
|
2406
2778
|
directive: rawInput,
|
|
2407
2779
|
deltaRefIds: []
|
|
@@ -2410,30 +2782,30 @@ function dispatchWork(args, store, options) {
|
|
|
2410
2782
|
workspace,
|
|
2411
2783
|
effective
|
|
2412
2784
|
});
|
|
2413
|
-
const snapshot =
|
|
2785
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
2414
2786
|
taskId: task.id,
|
|
2415
|
-
roleName:
|
|
2787
|
+
roleName: run.roleName,
|
|
2416
2788
|
purpose: "execution",
|
|
2417
2789
|
workItemId: item.id
|
|
2418
2790
|
}, now, "controller");
|
|
2419
|
-
const withContext =
|
|
2420
|
-
if (workItemForDispatch.status !== "
|
|
2421
|
-
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "
|
|
2791
|
+
const withContext = withRunContextSnapshot(run, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
2792
|
+
if (workItemForDispatch.status !== "open") {
|
|
2793
|
+
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "open", now);
|
|
2422
2794
|
}
|
|
2423
2795
|
tx.saveWorkItem(task.id, workItemForDispatch);
|
|
2424
|
-
tx.
|
|
2425
|
-
tx.
|
|
2426
|
-
|
|
2796
|
+
tx.saveRun(withContext);
|
|
2797
|
+
tx.saveActiveRun(withContext);
|
|
2798
|
+
enqueueRoleRunDispatch(tx, {
|
|
2427
2799
|
taskId: task.id,
|
|
2428
2800
|
roleName: role.name,
|
|
2429
|
-
|
|
2801
|
+
runId: withContext.id,
|
|
2430
2802
|
reason: "turn-dispatched",
|
|
2431
2803
|
occurredAt: now
|
|
2432
2804
|
});
|
|
2433
|
-
recordTaskEvent(tx, task.id, "
|
|
2434
|
-
return { kind: "direct",
|
|
2805
|
+
recordTaskEvent(tx, task.id, "run.dispatched", runLaunchEventPayload(withContext), now);
|
|
2806
|
+
return { kind: "direct", runs: [withContext] };
|
|
2435
2807
|
}
|
|
2436
|
-
const groupId = `execution-group-${tx.
|
|
2808
|
+
const groupId = `execution-group-${tx.peekNextRunId(task.id)}`;
|
|
2437
2809
|
if (workItemForDispatch !== item) {
|
|
2438
2810
|
// Freeze the Assignment against the retried WorkItem revision. The
|
|
2439
2811
|
// aggregate transaction rolls this back if a later precondition fails.
|
|
@@ -2462,7 +2834,7 @@ function dispatchWork(args, store, options) {
|
|
|
2462
2834
|
laneId,
|
|
2463
2835
|
managedWorkspace: laneWorkspace,
|
|
2464
2836
|
effective,
|
|
2465
|
-
|
|
2837
|
+
runId: tx.nextRunId(task.id),
|
|
2466
2838
|
dispatchMode: roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(task.id, role.name), effective.agentId, effective)
|
|
2467
2839
|
};
|
|
2468
2840
|
});
|
|
@@ -2495,15 +2867,15 @@ function dispatchWork(args, store, options) {
|
|
|
2495
2867
|
root: plan.managedWorkspace.root,
|
|
2496
2868
|
writableProjectIds: [...item.writeProjectIds]
|
|
2497
2869
|
},
|
|
2498
|
-
|
|
2870
|
+
currentRunId: plan.runId
|
|
2499
2871
|
})), now);
|
|
2500
2872
|
workItemForDispatch = attachWorkItemExecutionGroup(workItemForDispatch, group, now);
|
|
2501
|
-
if (workItemForDispatch.status !== "
|
|
2502
|
-
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "
|
|
2873
|
+
if (workItemForDispatch.status !== "open") {
|
|
2874
|
+
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "open", now);
|
|
2503
2875
|
}
|
|
2504
2876
|
tx.saveWorkItem(task.id, workItemForDispatch);
|
|
2505
|
-
const
|
|
2506
|
-
const
|
|
2877
|
+
const runs = plans.map((plan, index) => {
|
|
2878
|
+
const run = createRun(plan.runId, task.id, plan.role.name, plan.dispatchMode, createRunInput({
|
|
2507
2879
|
source: { type: "yui", channel: "workitem-dispatch" },
|
|
2508
2880
|
directive: assignment.input,
|
|
2509
2881
|
deltaRefIds: []
|
|
@@ -2514,37 +2886,37 @@ function dispatchWork(args, store, options) {
|
|
|
2514
2886
|
workspace: plan.managedWorkspace,
|
|
2515
2887
|
effective: plan.effective
|
|
2516
2888
|
});
|
|
2517
|
-
const snapshot =
|
|
2889
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
2518
2890
|
taskId: task.id,
|
|
2519
|
-
roleName:
|
|
2891
|
+
roleName: run.roleName,
|
|
2520
2892
|
purpose: "execution",
|
|
2521
2893
|
workItemId: item.id
|
|
2522
2894
|
}, now, "controller", assignment.contextSnapshotRef);
|
|
2523
|
-
const withContext =
|
|
2895
|
+
const withContext = withRunContextSnapshot(run, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
2524
2896
|
const prepared = options.executionLaneWorkspaces?.get(group.lanes[index].id);
|
|
2525
2897
|
if (prepared !== undefined && tx.getManagedWorkspace(prepared.owner) === null) {
|
|
2526
2898
|
tx.saveManagedWorkspace(prepared);
|
|
2527
2899
|
}
|
|
2528
|
-
tx.
|
|
2529
|
-
tx.
|
|
2530
|
-
|
|
2900
|
+
tx.saveRun(withContext);
|
|
2901
|
+
tx.saveActiveRun(withContext);
|
|
2902
|
+
enqueueRoleRunDispatch(tx, {
|
|
2531
2903
|
taskId: task.id,
|
|
2532
2904
|
roleName: plan.role.name,
|
|
2533
|
-
|
|
2905
|
+
runId: withContext.id,
|
|
2534
2906
|
reason: "turn-dispatched",
|
|
2535
2907
|
occurredAt: now
|
|
2536
2908
|
});
|
|
2537
|
-
recordTaskEvent(tx, task.id, "
|
|
2909
|
+
recordTaskEvent(tx, task.id, "run.dispatched", runLaunchEventPayload(withContext), now);
|
|
2538
2910
|
return withContext;
|
|
2539
2911
|
});
|
|
2540
|
-
return { kind: "replicated",
|
|
2912
|
+
return { kind: "replicated", runs };
|
|
2541
2913
|
});
|
|
2542
|
-
for (const
|
|
2543
|
-
notifyMailbox(options.runtime, roleMailbox(
|
|
2914
|
+
for (const run of dispatch.runs) {
|
|
2915
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
2544
2916
|
}
|
|
2545
2917
|
return dispatch.kind === "direct"
|
|
2546
|
-
? `Direct WorkItem
|
|
2547
|
-
: `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`;
|
|
2548
2920
|
}
|
|
2549
2921
|
function replicatedProducerAssignmentInput(input, requiresCodeRef) {
|
|
2550
2922
|
return [
|
|
@@ -2560,7 +2932,7 @@ function replicatedProducerAssignmentInput(input, requiresCodeRef) {
|
|
|
2560
2932
|
}
|
|
2561
2933
|
function acceptWork(args, store, options) {
|
|
2562
2934
|
const usage = "Task work accept usage: yui task work accept <task>/<work> --summary <text>.";
|
|
2563
|
-
const parsed = parseTail(args, new Set(["--summary"]), usage);
|
|
2935
|
+
const parsed = parseTail(args, new Set(["--summary", "--candidate"]), usage);
|
|
2564
2936
|
exactPositionals(parsed.positionals, 1, usage);
|
|
2565
2937
|
const summary = requiredOption(parsed.options, "--summary");
|
|
2566
2938
|
const now = clock(options);
|
|
@@ -2571,17 +2943,20 @@ function acceptWork(args, store, options) {
|
|
|
2571
2943
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2572
2944
|
}
|
|
2573
2945
|
const actor = taskActor(tx, options, task.id);
|
|
2574
|
-
if (item.status !== "
|
|
2946
|
+
if ((item.status !== "open" || item.candidates.length === 0)) {
|
|
2575
2947
|
throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
|
|
2576
2948
|
}
|
|
2577
2949
|
if (options.workItemIntegrationProof?.workspace.owner.type === "review-round") {
|
|
2578
2950
|
throw usageError("A ReviewRound-owned workspace cannot be used for WorkItem acceptance.");
|
|
2579
2951
|
}
|
|
2580
|
-
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.");
|
|
2581
2959
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
2582
|
-
if (!sameTaskFinalReviewContract(candidate.taskFinalReviewContract, taskFinalContract)) {
|
|
2583
|
-
throw usageError(`Task final-review contract does not match Candidate ${candidate.id}.`);
|
|
2584
|
-
}
|
|
2585
2960
|
const latestReview = reviewRoundsByIdentity(tx.listReviewRounds(item.taskId)
|
|
2586
2961
|
.filter((round) => round.workItemId === item.id
|
|
2587
2962
|
&& round.candidateId === candidate.id)).at(-1);
|
|
@@ -2609,17 +2984,25 @@ function acceptWork(args, store, options) {
|
|
|
2609
2984
|
&& isolatedWorkspace.owner.workItemId === item.id
|
|
2610
2985
|
&& isolatedWorkspace.entries.some(({ access }) => access === "write")) {
|
|
2611
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
|
+
}
|
|
2612
2994
|
}
|
|
2613
|
-
const completed = updateWorkItemStatus(item, "
|
|
2995
|
+
const completed = updateWorkItemStatus(item, "accepted", now, summary, candidate.id);
|
|
2614
2996
|
tx.saveWorkItem(item.taskId, completed);
|
|
2615
2997
|
recordTaskEvent(tx, item.taskId, "work.accepted", {
|
|
2616
2998
|
workItemId: item.id,
|
|
2617
2999
|
candidateId: candidate.id,
|
|
2618
|
-
...(candidate.source.type === "
|
|
2619
|
-
? {
|
|
3000
|
+
...(candidate.source.type === "run"
|
|
3001
|
+
? { runId: candidate.source.runId }
|
|
2620
3002
|
: { workItemRevision: String(candidate.workItemRevision) }),
|
|
2621
3003
|
acceptedBy: actor,
|
|
2622
3004
|
summary,
|
|
3005
|
+
...(latestReview === undefined ? {} : { reviewRoundId: latestReview.id }),
|
|
2623
3006
|
...(actor === "leader" ? leaderActionEventPayload(tx, item.taskId, options) : {})
|
|
2624
3007
|
}, now);
|
|
2625
3008
|
return completed;
|
|
@@ -2665,7 +3048,7 @@ function rejectWork(args, store, options) {
|
|
|
2665
3048
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2666
3049
|
}
|
|
2667
3050
|
const actor = taskActor(tx, options, task.id);
|
|
2668
|
-
if (item.status
|
|
3051
|
+
if (item.status === "retired" || item.candidates.length === 0) {
|
|
2669
3052
|
throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
|
|
2670
3053
|
}
|
|
2671
3054
|
const candidate = requireWorkItemCandidate(item);
|
|
@@ -2673,7 +3056,7 @@ function rejectWork(args, store, options) {
|
|
|
2673
3056
|
if (activeReview !== undefined) {
|
|
2674
3057
|
throw usageError(`ReviewRound is still active: ${activeReview.id}/${activeReview.status}.`);
|
|
2675
3058
|
}
|
|
2676
|
-
const failed = updateWorkItemStatus(item, "
|
|
3059
|
+
const { currentCandidateId: _declinedCandidate, ...failed } = updateWorkItemStatus(item, "open", now);
|
|
2677
3060
|
tx.saveWorkItem(item.taskId, failed);
|
|
2678
3061
|
recordTaskEvent(tx, item.taskId, "work.rejected", {
|
|
2679
3062
|
workItemId: item.id,
|
|
@@ -2727,12 +3110,12 @@ function retireWork(args, store, options) {
|
|
|
2727
3110
|
+ `${activeWorkItemJob.id}/${activeWorkItemJob.status}. `
|
|
2728
3111
|
+ "Cancel or acknowledge it before retiring.");
|
|
2729
3112
|
}
|
|
2730
|
-
for (const run of tx.
|
|
2731
|
-
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, {
|
|
2732
3115
|
taskId: task.id,
|
|
2733
3116
|
roleName: run.roleName,
|
|
2734
3117
|
agentId: run.effective.agentId,
|
|
2735
|
-
|
|
3118
|
+
runId: run.id,
|
|
2736
3119
|
outcome: {
|
|
2737
3120
|
status: "failed",
|
|
2738
3121
|
diagnostic: `Work Item retired: ${summary}`,
|
|
@@ -2740,7 +3123,7 @@ function retireWork(args, store, options) {
|
|
|
2740
3123
|
}
|
|
2741
3124
|
}, now);
|
|
2742
3125
|
if (terminal.disposition !== "applied") {
|
|
2743
|
-
throw usageError(`Work Item
|
|
3126
|
+
throw usageError(`Work Item AgentRun changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
2744
3127
|
}
|
|
2745
3128
|
}
|
|
2746
3129
|
}
|
|
@@ -2777,9 +3160,9 @@ function listWork(args, store) {
|
|
|
2777
3160
|
exactPositionals(args, 1, "Task work list usage: yui task work list <task>.");
|
|
2778
3161
|
const task = requireTask(store, args[0]);
|
|
2779
3162
|
const items = store.listWorkItems(task.id);
|
|
2780
|
-
const
|
|
3163
|
+
const runs = store.listRuns(task.id);
|
|
2781
3164
|
const sessionSets = store.listRoleSessionSets(task.id);
|
|
2782
|
-
const executions = items.map((item) => projectWorkItemExecution(item,
|
|
3165
|
+
const executions = items.map((item) => projectWorkItemExecution(item, runs, sessionSets, store));
|
|
2783
3166
|
const rendered = items.length === 0
|
|
2784
3167
|
? "No work items found.\n"
|
|
2785
3168
|
: `${renderTable(`Task work: ${task.id}`, [
|
|
@@ -2806,7 +3189,7 @@ function listWork(args, store) {
|
|
|
2806
3189
|
function showWork(args, store, options) {
|
|
2807
3190
|
exactPositionals(args, 1, "Task work show usage: yui task work show <work>.");
|
|
2808
3191
|
const item = requireWorkItem(store, args[0], options);
|
|
2809
|
-
const execution = projectWorkItemExecution(item, store.
|
|
3192
|
+
const execution = projectWorkItemExecution(item, store.listRuns(item.taskId), store.listRoleSessionSets(item.taskId), store);
|
|
2810
3193
|
const replacement = item.disposition?.replacementWorkItemId;
|
|
2811
3194
|
const rendered = [
|
|
2812
3195
|
`Work Item: ${item.id}`,
|
|
@@ -2827,7 +3210,7 @@ function showWork(args, store, options) {
|
|
|
2827
3210
|
}
|
|
2828
3211
|
function compactWorkItemExecution(projection) {
|
|
2829
3212
|
if (projection.shape === "direct")
|
|
2830
|
-
return `main=${projection.
|
|
3213
|
+
return `main=${projection.mainRun.status}`;
|
|
2831
3214
|
const counts = projection.laneCounts;
|
|
2832
3215
|
return `lanes ${counts.running}/${counts.succeeded}/${counts.needsAttention}/${counts.failed}/${counts.unknown}; ${projection.synthesis.status}`;
|
|
2833
3216
|
}
|
|
@@ -2840,16 +3223,16 @@ function renderWorkItemExecutionProjection(projection) {
|
|
|
2840
3223
|
: [
|
|
2841
3224
|
`Lanes: running=${projection.laneCounts.running}, succeeded=${projection.laneCounts.succeeded}, needs-attention=${projection.laneCounts.needsAttention}, failed=${projection.laneCounts.failed}, unknown=${projection.laneCounts.unknown}`,
|
|
2842
3225
|
...projection.lanes.map((lane) => (` ${lane.laneId} (#${lane.ordinal}, ${lane.roleName}): ${lane.status}; `
|
|
2843
|
-
+ `
|
|
2844
|
-
+ `retry=${lane.
|
|
3226
|
+
+ `run=${lane.currentRunId ?? "unknown"}; session=${lane.session}; `
|
|
3227
|
+
+ `retry=${lane.retryRunId ?? "none"}; settle=${lane.settleRunId ?? "none"}`))
|
|
2845
3228
|
]),
|
|
2846
|
-
`Synthesis: ${projection.synthesis.status}; successful=${projection.synthesis.successfulLaneCount}
|
|
2847
|
-
`Main
|
|
2848
|
-
`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"}`,
|
|
2849
3232
|
...(projection.candidate.sourceExecutionGroupId === undefined
|
|
2850
3233
|
? []
|
|
2851
3234
|
: [
|
|
2852
|
-
`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"}`
|
|
2853
3236
|
]),
|
|
2854
3237
|
`Next Action: ${projection.nextAction.kind}; owner=${projection.nextAction.owners.join(", ") || "none"}; target=${projection.nextAction.targetIds.join(", ") || "none"}`
|
|
2855
3238
|
];
|
|
@@ -2874,7 +3257,7 @@ function reviewWork(args, store, options) {
|
|
|
2874
3257
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2875
3258
|
}
|
|
2876
3259
|
const requestedBy = taskActor(tx, options, task.id);
|
|
2877
|
-
if (item.status !== "
|
|
3260
|
+
if ((item.status !== "open" || item.candidates.length === 0)) {
|
|
2878
3261
|
throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
|
|
2879
3262
|
}
|
|
2880
3263
|
const candidate = requireWorkItemCandidate(item);
|
|
@@ -2892,9 +3275,9 @@ function reviewWork(args, store, options) {
|
|
|
2892
3275
|
&& (round.status === "pending" || round.status === "running")))).at(-1);
|
|
2893
3276
|
if (activeRound !== undefined) {
|
|
2894
3277
|
const producerDispatchPending = activeRound.executionGroup?.lanes.some((lane) => (lane.disposition === "open"
|
|
2895
|
-
&& (lane.
|
|
2896
|
-
|| tx.
|
|
2897
|
-
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)
|
|
2898
3281
|
|| (activeRound.status === "running" && producerDispatchPending)) {
|
|
2899
3282
|
const persistedRoles = activeRound.executionGroup?.lanes
|
|
2900
3283
|
.map(({ roleName }) => roleName) ?? [];
|
|
@@ -2927,7 +3310,7 @@ function reviewWork(args, store, options) {
|
|
|
2927
3310
|
if (result.kind === "busy") {
|
|
2928
3311
|
const busy = result.availability;
|
|
2929
3312
|
return output(`Reviewer ${busy.reviewerRoleName} is busy (${busy.phase}`
|
|
2930
|
-
+ `${busy.
|
|
3313
|
+
+ `${busy.activeRunId === undefined ? "" : `; AgentRun ${busy.activeRunId}`}); `
|
|
2931
3314
|
+ `${busy.activeReviewRoundId === undefined
|
|
2932
3315
|
? ""
|
|
2933
3316
|
: `active ReviewRound ${busy.activeReviewRoundId}; `}`
|
|
@@ -2942,17 +3325,49 @@ function reviewWork(args, store, options) {
|
|
|
2942
3325
|
}
|
|
2943
3326
|
/**
|
|
2944
3327
|
* Task-control recovery for a failed Task-final ReviewRound that never
|
|
2945
|
-
* created a Reviewer
|
|
2946
|
-
* 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
|
|
2947
3330
|
* path for a failed provider execution. Here the old terminal Round is an
|
|
2948
3331
|
* immutable anchor and one fresh Round is created only after the same frozen
|
|
2949
3332
|
* committed Integration/ChangeSet provenance and Reviewer independence fences
|
|
2950
3333
|
* pass again.
|
|
2951
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
|
+
}
|
|
2952
3365
|
function taskReviewCommand(args, store, options) {
|
|
2953
3366
|
const [command, ...rest] = args;
|
|
2954
3367
|
if (command === "request")
|
|
2955
3368
|
return requestTaskReviewRound(rest, store, options);
|
|
3369
|
+
if (command === "synthesize")
|
|
3370
|
+
return synthesizeRuns(rest, "reviewRound", store, options);
|
|
2956
3371
|
if (command === "retry")
|
|
2957
3372
|
return retryFailedTaskReviewRound(rest, store, options);
|
|
2958
3373
|
throw usageError(command === undefined
|
|
@@ -2960,7 +3375,7 @@ function taskReviewCommand(args, store, options) {
|
|
|
2960
3375
|
: `Unknown command: task review ${command}`);
|
|
2961
3376
|
}
|
|
2962
3377
|
function requestTaskReviewRound(args, store, options) {
|
|
2963
|
-
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> "
|
|
2964
3379
|
+ "[--lane-role <producer-role> ...] [--delta-recheck].";
|
|
2965
3380
|
const parsed = parseMultiValueTail(args, new Set(["--role"]), new Set(["--lane-role"]), usage, new Set(["--delta-recheck"]));
|
|
2966
3381
|
exactPositionals(parsed.positionals, 1, usage);
|
|
@@ -2985,8 +3400,8 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
2985
3400
|
throw usageError("Delta-recheck is not supported with a Task-final review contract.");
|
|
2986
3401
|
}
|
|
2987
3402
|
}
|
|
2988
|
-
if (tx.getGlobalRole(reviewerRoleName) === null) {
|
|
2989
|
-
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}.`);
|
|
2990
3405
|
}
|
|
2991
3406
|
const provenance = taskReviewProvenance(tx, task, options);
|
|
2992
3407
|
const producerCollision = taskReviewProducerCollision(provenance, reviewerRoleName);
|
|
@@ -3087,7 +3502,7 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3087
3502
|
});
|
|
3088
3503
|
if ("kind" in round && round.kind === "busy") {
|
|
3089
3504
|
return output(`Reviewer ${round.reviewerRoleName} is busy (${round.phase}`
|
|
3090
|
-
+ `${round.
|
|
3505
|
+
+ `${round.activeRunId === undefined ? "" : `; AgentRun ${round.activeRunId}`}); `
|
|
3091
3506
|
+ `${round.activeReviewRoundId === undefined
|
|
3092
3507
|
? ""
|
|
3093
3508
|
: `active ReviewRound ${round.activeReviewRoundId}; `}`
|
|
@@ -3126,26 +3541,26 @@ function validateDeltaRecheckRequest(store, taskId, candidate, preflight) {
|
|
|
3126
3541
|
return preflight.record;
|
|
3127
3542
|
}
|
|
3128
3543
|
function assertTaskReviewRequestLane(store, taskId, reviewerRoleName, reusableRound) {
|
|
3129
|
-
const activePointer = store.
|
|
3544
|
+
const activePointer = store.getActiveRun(taskId, reviewerRoleName);
|
|
3130
3545
|
if (reusableRound === undefined || reusableRound.status === "completed") {
|
|
3131
3546
|
assertReviewerAvailable(store, taskId, reviewerRoleName);
|
|
3132
3547
|
return;
|
|
3133
3548
|
}
|
|
3134
3549
|
if (reusableRound.status === "pending") {
|
|
3135
3550
|
if (activePointer !== null) {
|
|
3136
|
-
throw usageError(`Reviewer Role already has an active
|
|
3551
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewerRoleName}.`);
|
|
3137
3552
|
}
|
|
3138
3553
|
assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound);
|
|
3139
3554
|
return;
|
|
3140
3555
|
}
|
|
3141
|
-
const
|
|
3142
|
-
if (
|
|
3556
|
+
const reviewerRunId = reusableRound.reviewerRunId;
|
|
3557
|
+
if (reviewerRunId === undefined
|
|
3143
3558
|
&& reusableRound.executionGroup?.lanes.some(({ disposition }) => disposition === "open")) {
|
|
3144
3559
|
assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound);
|
|
3145
3560
|
return;
|
|
3146
3561
|
}
|
|
3147
|
-
const activeMatches =
|
|
3148
|
-
&& activePointer?.id ===
|
|
3562
|
+
const activeMatches = reviewerRunId !== undefined
|
|
3563
|
+
&& activePointer?.id === reviewerRunId
|
|
3149
3564
|
&& activePointer.status === "active";
|
|
3150
3565
|
if (!activeMatches) {
|
|
3151
3566
|
throw usageError(`Existing Task-final ReviewRound ${reusableRound.id} is running without its exact Reviewer execution.`);
|
|
@@ -3160,7 +3575,7 @@ function assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound)
|
|
|
3160
3575
|
return;
|
|
3161
3576
|
}
|
|
3162
3577
|
throw usageError(`Reviewer ${reviewerRoleName} is busy (${availability.phase}`
|
|
3163
|
-
+ `${availability.
|
|
3578
|
+
+ `${availability.activeRunId === undefined ? "" : `; AgentRun ${availability.activeRunId}`}`
|
|
3164
3579
|
+ `${availability.activeReviewRoundId === undefined
|
|
3165
3580
|
? ""
|
|
3166
3581
|
: `; ReviewRound ${availability.activeReviewRoundId}`}).`);
|
|
@@ -3170,10 +3585,10 @@ function reviewerBusyBelongsToRound(busy, round) {
|
|
|
3170
3585
|
return false;
|
|
3171
3586
|
if (busy.phase === "review-slot")
|
|
3172
3587
|
return true;
|
|
3173
|
-
if (busy.phase !== "active-turn" || busy.
|
|
3588
|
+
if (busy.phase !== "active-turn" || busy.activeRunId === undefined)
|
|
3174
3589
|
return false;
|
|
3175
|
-
return round.
|
|
3176
|
-
|| round.executionGroup?.lanes.some(({
|
|
3590
|
+
return round.reviewerRunId === busy.activeRunId
|
|
3591
|
+
|| round.executionGroup?.lanes.some(({ currentRunId }) => (currentRunId === busy.activeRunId)) === true;
|
|
3177
3592
|
}
|
|
3178
3593
|
function retryFailedTaskReviewRound(args, store, options) {
|
|
3179
3594
|
exactPositionals(args, 1, "Task review retry usage: yui task review retry <task>/<review-round>.");
|
|
@@ -3191,11 +3606,11 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3191
3606
|
if ((round.scope ?? "work-item") !== "task") {
|
|
3192
3607
|
throw usageError(`ReviewRound ${round.id} is not a failed Task-final ReviewRound.`);
|
|
3193
3608
|
}
|
|
3194
|
-
if (round.
|
|
3609
|
+
if (round.reviewerRunId !== undefined) {
|
|
3195
3610
|
if (round.status === "completed") {
|
|
3196
3611
|
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
3197
3612
|
}
|
|
3198
|
-
throw usageError(`ReviewRound ${round.id} has Reviewer
|
|
3613
|
+
throw usageError(`ReviewRound ${round.id} has Reviewer AgentRun ${round.reviewerRunId}; use task run retry instead.`);
|
|
3199
3614
|
}
|
|
3200
3615
|
if (round.status !== "failed" && round.status !== "pending") {
|
|
3201
3616
|
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
@@ -3235,9 +3650,9 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3235
3650
|
reviewer = createTaskRole(tx, task, round.reviewerRoleName, undefined, now, round.reviewerRoleName);
|
|
3236
3651
|
tx.saveRole(task.id, reviewer);
|
|
3237
3652
|
}
|
|
3238
|
-
const activePointer = tx.
|
|
3653
|
+
const activePointer = tx.getActiveRun(task.id, reviewer.name);
|
|
3239
3654
|
if (activePointer !== null) {
|
|
3240
|
-
throw usageError(`Reviewer Role already has an active
|
|
3655
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
3241
3656
|
}
|
|
3242
3657
|
assertReviewerAvailable(tx, task.id, reviewer.name, round);
|
|
3243
3658
|
// Issue 06: an already-pending Round is the idempotent retry result.
|
|
@@ -3258,50 +3673,50 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3258
3673
|
? `Task-final Review retry requested as ${result.round.id}\n`
|
|
3259
3674
|
: `Task-final Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round });
|
|
3260
3675
|
}
|
|
3261
|
-
function
|
|
3676
|
+
function taskRunCommand(args, store, options) {
|
|
3262
3677
|
const [command, ...rest] = args;
|
|
3263
3678
|
if (command === "list")
|
|
3264
|
-
return output(
|
|
3679
|
+
return output(listRuns(rest, store, options));
|
|
3265
3680
|
if (command === "show")
|
|
3266
|
-
return
|
|
3681
|
+
return showRun(rest, store, options);
|
|
3267
3682
|
if (command === "context")
|
|
3268
|
-
return
|
|
3683
|
+
return runContextCommand(rest, store, options);
|
|
3269
3684
|
if (command === "retry")
|
|
3270
|
-
return
|
|
3685
|
+
return retryRun(rest, store, options);
|
|
3271
3686
|
if (command === "settle")
|
|
3272
|
-
return
|
|
3687
|
+
return settleRun(rest, store, options);
|
|
3273
3688
|
if (command === "checkpoint")
|
|
3274
|
-
return output(
|
|
3689
|
+
return output(checkpointRun(rest, store, options));
|
|
3275
3690
|
if (command === "retire")
|
|
3276
|
-
return
|
|
3691
|
+
return retireRun(rest, store, options);
|
|
3277
3692
|
throw usageError(command === undefined
|
|
3278
3693
|
? "Task turn command is required."
|
|
3279
|
-
: `Unknown command: task
|
|
3694
|
+
: `Unknown command: task run ${command}`);
|
|
3280
3695
|
}
|
|
3281
|
-
function
|
|
3282
|
-
exactPositionals(args, 1, "Task turn settle usage: yui task
|
|
3283
|
-
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));
|
|
3284
3699
|
if (previous.purpose === "review") {
|
|
3285
3700
|
if (previous.executionGroupId !== undefined
|
|
3286
3701
|
&& previous.executionLaneId !== undefined) {
|
|
3287
|
-
return
|
|
3702
|
+
return settleFailedReviewExecutionLaneRun(previous, store, options);
|
|
3288
3703
|
}
|
|
3289
|
-
return
|
|
3704
|
+
return settleStaleFinalReviewRun(args, store, options);
|
|
3290
3705
|
}
|
|
3291
|
-
return
|
|
3706
|
+
return settleFailedExecutionLaneRun(previous, store, options);
|
|
3292
3707
|
}
|
|
3293
|
-
function
|
|
3708
|
+
function settleFailedReviewExecutionLaneRun(previous, store, options) {
|
|
3294
3709
|
const now = clock(options);
|
|
3295
3710
|
const result = store.transaction((tx) => {
|
|
3296
|
-
const run = tx.
|
|
3711
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
3297
3712
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
3298
|
-
throw usageError(`
|
|
3713
|
+
throw usageError(`AgentRun ${previous.id} is not a failed review AgentRun.`);
|
|
3299
3714
|
}
|
|
3300
3715
|
if (run.reviewRoundId === undefined
|
|
3301
3716
|
|| run.executionGroupId === undefined
|
|
3302
3717
|
|| run.executionLaneId === undefined
|
|
3303
3718
|
|| run.sourceExecutionGroupId !== undefined) {
|
|
3304
|
-
throw usageError(`
|
|
3719
|
+
throw usageError(`AgentRun ${run.id} is not a failed Review Producer Lane AgentRun.`);
|
|
3305
3720
|
}
|
|
3306
3721
|
const task = requireTask(tx, run.taskId);
|
|
3307
3722
|
if (task.status !== "active")
|
|
@@ -3309,80 +3724,79 @@ function settleFailedReviewExecutionLaneTurn(previous, store, options) {
|
|
|
3309
3724
|
const actor = taskActor(tx, options, task.id);
|
|
3310
3725
|
const round = tx.getReviewRound(task.id, run.reviewRoundId);
|
|
3311
3726
|
if (round === null) {
|
|
3312
|
-
throw dataError(`ReviewRound not found for
|
|
3727
|
+
throw dataError(`ReviewRound not found for AgentRun ${run.id}: ${run.reviewRoundId}.`);
|
|
3313
3728
|
}
|
|
3314
3729
|
const group = round.executionGroup;
|
|
3315
3730
|
if (group === undefined || group.id !== run.executionGroupId) {
|
|
3316
|
-
throw usageError(`
|
|
3731
|
+
throw usageError(`AgentRun ${run.id} no longer belongs to the Review ExecutionGroup.`);
|
|
3317
3732
|
}
|
|
3318
3733
|
const lane = group.lanes.find(({ id }) => id === run.executionLaneId);
|
|
3319
|
-
if (lane === undefined || lane.
|
|
3320
|
-
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.`);
|
|
3321
3736
|
}
|
|
3322
3737
|
if (lane.disposition === "failed") {
|
|
3323
3738
|
return {
|
|
3324
|
-
|
|
3739
|
+
run: run,
|
|
3325
3740
|
reviewRound: round,
|
|
3326
3741
|
changed: false,
|
|
3327
|
-
|
|
3742
|
+
mainRuns: []
|
|
3328
3743
|
};
|
|
3329
3744
|
}
|
|
3330
3745
|
if (round.status !== "running" || lane.disposition !== "open") {
|
|
3331
|
-
throw usageError(`
|
|
3746
|
+
throw usageError(`AgentRun ${run.id} cannot settle ${round.id}/${group.id}/${lane.id} from `
|
|
3332
3747
|
+ `${round.status}/${lane.disposition}.`);
|
|
3333
3748
|
}
|
|
3334
|
-
const validation =
|
|
3749
|
+
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
3335
3750
|
if (validation.disposition !== "applied") {
|
|
3336
|
-
throw usageError(`Review
|
|
3751
|
+
throw usageError(`Review AgentRun ${run.id} no longer matches its frozen Review Lane: `
|
|
3337
3752
|
+ `${validation.reason ?? "mismatch"}.`);
|
|
3338
3753
|
}
|
|
3339
|
-
if (tx.
|
|
3340
|
-
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}.`);
|
|
3341
3756
|
}
|
|
3342
3757
|
const settledGroup = updateUnifiedExecutionLane(group, lane.id, {
|
|
3343
|
-
|
|
3758
|
+
currentRunId: run.id,
|
|
3344
3759
|
disposition: "failed"
|
|
3345
3760
|
}, now);
|
|
3346
3761
|
tx.saveReviewRound(task.id, updateReviewExecutionGroup(round, settledGroup));
|
|
3347
|
-
recordTaskEvent(tx, task.id, "
|
|
3348
|
-
|
|
3762
|
+
recordTaskEvent(tx, task.id, "run.review-settled", {
|
|
3763
|
+
runId: run.id,
|
|
3349
3764
|
reviewRoundId: round.id,
|
|
3350
3765
|
executionGroupId: group.id,
|
|
3351
3766
|
executionLaneId: lane.id,
|
|
3352
3767
|
settledBy: actor,
|
|
3353
3768
|
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
|
|
3354
3769
|
}, now);
|
|
3355
|
-
const reconciliation = reconcileReviewMainTurns(tx, task.id, now);
|
|
3356
3770
|
return {
|
|
3357
|
-
|
|
3771
|
+
run: run,
|
|
3358
3772
|
reviewRound: tx.getReviewRound(task.id, round.id),
|
|
3359
3773
|
changed: true,
|
|
3360
|
-
|
|
3774
|
+
mainRuns: []
|
|
3361
3775
|
};
|
|
3362
3776
|
});
|
|
3363
|
-
for (const
|
|
3364
|
-
notifyMailbox(options.runtime, roleMailbox(
|
|
3777
|
+
for (const run of result.mainRuns) {
|
|
3778
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
3365
3779
|
}
|
|
3366
3780
|
return output(result.changed
|
|
3367
|
-
? `Settled failed Review Producer Lane from
|
|
3368
|
-
: `Failed Review Producer Lane already settled from
|
|
3369
|
-
|
|
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,
|
|
3370
3784
|
reviewRound: result.reviewRound,
|
|
3371
|
-
...(result.
|
|
3785
|
+
...(result.mainRuns.length === 0 ? {} : { mainRuns: result.mainRuns })
|
|
3372
3786
|
});
|
|
3373
3787
|
}
|
|
3374
|
-
function
|
|
3788
|
+
function settleFailedExecutionLaneRun(previous, store, options) {
|
|
3375
3789
|
const now = clock(options);
|
|
3376
3790
|
const result = store.transaction((tx) => {
|
|
3377
|
-
const run = tx.
|
|
3791
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
3378
3792
|
if (run === null || run.status !== "failed" || run.purpose !== "execution") {
|
|
3379
|
-
throw usageError(`
|
|
3793
|
+
throw usageError(`AgentRun ${previous.id} is not a failed execution AgentRun.`);
|
|
3380
3794
|
}
|
|
3381
3795
|
if (run.workItemId === undefined
|
|
3382
3796
|
|| run.executionGroupId === undefined
|
|
3383
3797
|
|| run.executionLaneId === undefined
|
|
3384
3798
|
|| run.sourceExecutionGroupId !== undefined) {
|
|
3385
|
-
throw usageError(`
|
|
3799
|
+
throw usageError(`AgentRun ${run.id} is not a failed WorkItem Execution Lane AgentRun.`);
|
|
3386
3800
|
}
|
|
3387
3801
|
const task = requireTask(tx, run.taskId);
|
|
3388
3802
|
if (task.status !== "active")
|
|
@@ -3390,94 +3804,88 @@ function settleFailedExecutionLaneTurn(previous, store, options) {
|
|
|
3390
3804
|
const actor = taskActor(tx, options, task.id);
|
|
3391
3805
|
const item = tx.getWorkItem(task.id, run.workItemId);
|
|
3392
3806
|
if (item === null)
|
|
3393
|
-
throw dataError(`Work item not found for
|
|
3807
|
+
throw dataError(`Work item not found for AgentRun ${run.id}: ${run.workItemId}.`);
|
|
3394
3808
|
const group = currentWorkItemExecutionGroup(item);
|
|
3395
3809
|
if (group === undefined || group.id !== run.executionGroupId) {
|
|
3396
|
-
throw usageError(`
|
|
3810
|
+
throw usageError(`AgentRun ${run.id} no longer belongs to the current ExecutionGroup.`);
|
|
3397
3811
|
}
|
|
3398
3812
|
const lane = group.lanes.find(({ id }) => id === run.executionLaneId);
|
|
3399
|
-
if (lane === undefined || lane.
|
|
3400
|
-
throw usageError(`
|
|
3813
|
+
if (lane === undefined || lane.currentRunId !== run.id) {
|
|
3814
|
+
throw usageError(`AgentRun ${run.id} no longer owns its Execution Lane.`);
|
|
3401
3815
|
}
|
|
3402
3816
|
if (lane.disposition === "failed") {
|
|
3403
3817
|
return {
|
|
3404
|
-
|
|
3818
|
+
run: run,
|
|
3405
3819
|
workItem: item,
|
|
3406
3820
|
changed: false,
|
|
3407
|
-
|
|
3821
|
+
mainRuns: []
|
|
3408
3822
|
};
|
|
3409
3823
|
}
|
|
3410
|
-
if (item.status !== "
|
|
3411
|
-
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 `
|
|
3412
3826
|
+ `${item.status}/${lane.disposition}.`);
|
|
3413
3827
|
}
|
|
3414
|
-
if (tx.
|
|
3415
|
-
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}.`);
|
|
3416
3830
|
}
|
|
3417
3831
|
const settledGroup = updateWorkItemExecutionLane(group, lane.id, {
|
|
3418
|
-
|
|
3832
|
+
currentRunId: run.id,
|
|
3419
3833
|
disposition: "failed"
|
|
3420
3834
|
}, now);
|
|
3421
3835
|
const settledItem = updateWorkItemExecutionGroup(item, settledGroup, now);
|
|
3422
3836
|
tx.saveWorkItem(task.id, settledItem);
|
|
3423
|
-
recordTaskEvent(tx, task.id, "
|
|
3424
|
-
|
|
3837
|
+
recordTaskEvent(tx, task.id, "run.execution-settled", {
|
|
3838
|
+
runId: run.id,
|
|
3425
3839
|
workItemId: item.id,
|
|
3426
3840
|
executionGroupId: group.id,
|
|
3427
3841
|
executionLaneId: lane.id,
|
|
3428
3842
|
settledBy: actor,
|
|
3429
3843
|
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
|
|
3430
3844
|
}, now);
|
|
3431
|
-
const reconciliation = reconcileWorkItemMainTurns(tx, task.id, now);
|
|
3432
3845
|
return {
|
|
3433
|
-
|
|
3846
|
+
run: run,
|
|
3434
3847
|
workItem: tx.getWorkItem(task.id, item.id) ?? settledItem,
|
|
3435
3848
|
changed: true,
|
|
3436
|
-
|
|
3849
|
+
mainRuns: []
|
|
3437
3850
|
};
|
|
3438
3851
|
});
|
|
3439
|
-
for (const
|
|
3440
|
-
notifyMailbox(options.runtime, roleMailbox(
|
|
3852
|
+
for (const run of result.mainRuns) {
|
|
3853
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
3441
3854
|
}
|
|
3442
3855
|
return output(result.changed
|
|
3443
|
-
? `Settled failed Execution Lane from
|
|
3444
|
-
: `Failed Execution Lane already settled from
|
|
3445
|
-
|
|
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,
|
|
3446
3859
|
workItem: result.workItem,
|
|
3447
|
-
...(result.
|
|
3860
|
+
...(result.mainRuns.length === 0 ? {} : { mainRuns: result.mainRuns })
|
|
3448
3861
|
});
|
|
3449
3862
|
}
|
|
3450
|
-
function
|
|
3451
|
-
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>].";
|
|
3452
3865
|
const parsed = parseTail(args, new Set([
|
|
3453
3866
|
"--reason",
|
|
3454
3867
|
"--expected-progress-at",
|
|
3455
3868
|
"--progress-at",
|
|
3456
3869
|
"--agent-id",
|
|
3457
3870
|
"--adapter-id",
|
|
3458
|
-
"--native-session-id"
|
|
3459
|
-
"--launch-id"
|
|
3871
|
+
"--native-session-id"
|
|
3460
3872
|
]), usage);
|
|
3461
3873
|
exactPositionals(parsed.positionals, 1, usage);
|
|
3462
3874
|
const reason = requiredOption(parsed.options, "--reason");
|
|
3463
|
-
const reference = taskRecordReference(parsed.positionals[0], "
|
|
3875
|
+
const reference = taskRecordReference(parsed.positionals[0], "run", "AgentRun reference", options);
|
|
3464
3876
|
const now = clock(options);
|
|
3465
3877
|
const result = store.transaction((tx) => {
|
|
3466
3878
|
const task = requireTask(tx, reference.taskId);
|
|
3467
3879
|
assertTaskOpen(task);
|
|
3468
3880
|
const actor = taskActor(tx, options, task.id);
|
|
3469
|
-
let run = tx.
|
|
3881
|
+
let run = tx.getRun(task.id, reference.localId);
|
|
3470
3882
|
if (run === null)
|
|
3471
|
-
throw dataError(`
|
|
3883
|
+
throw dataError(`AgentRun not found: ${task.id}/${reference.localId}.`);
|
|
3472
3884
|
const events = tx.listEvents(task.id);
|
|
3473
|
-
if (isTaskRecordRetired(events, "
|
|
3474
|
-
return { task,
|
|
3885
|
+
if (isTaskRecordRetired(events, "run", run.id)) {
|
|
3886
|
+
return { task, run: run, changed: false };
|
|
3475
3887
|
}
|
|
3476
3888
|
if (run.status === "active") {
|
|
3477
|
-
if (actor === "leader"
|
|
3478
|
-
&& taskLeaderActionTurnId(tx, task.id, options.environment) === run.id) {
|
|
3479
|
-
throw usageError("A Task Leader cannot retire its own current authority Turn.", usage);
|
|
3480
|
-
}
|
|
3481
3889
|
const expectedProgressAt = requiredOption(parsed.options, parsed.options.has("--expected-progress-at")
|
|
3482
3890
|
? "--expected-progress-at"
|
|
3483
3891
|
: "--progress-at");
|
|
@@ -3487,35 +3895,30 @@ function retireTurn(args, store, options) {
|
|
|
3487
3895
|
const agentId = requiredOption(parsed.options, "--agent-id");
|
|
3488
3896
|
const adapterId = requiredOption(parsed.options, "--adapter-id");
|
|
3489
3897
|
const nativeSessionId = parsed.options.get("--native-session-id");
|
|
3490
|
-
const runtimeGenerationId = parsed.options.get("--launch-id");
|
|
3491
3898
|
const sessions = tx.getTaskRoleSessionSet(task.id, run.roleName);
|
|
3492
3899
|
const session = sessions?.sessions[run.effective.agentId];
|
|
3493
3900
|
if (session?.nativeSessionId !== undefined && nativeSessionId === undefined) {
|
|
3494
|
-
throw usageError("--native-session-id is required for this active
|
|
3901
|
+
throw usageError("--native-session-id is required for this active AgentRun.", usage);
|
|
3495
3902
|
}
|
|
3496
|
-
|
|
3497
|
-
throw usageError("--launch-id is required for an opaque active Turn.", usage);
|
|
3498
|
-
}
|
|
3499
|
-
const terminal = retireExactActiveTurn(tx, {
|
|
3903
|
+
const terminal = retireExactActiveRun(tx, {
|
|
3500
3904
|
taskId: task.id,
|
|
3501
3905
|
roleName: run.roleName,
|
|
3502
|
-
|
|
3906
|
+
runId: run.id,
|
|
3503
3907
|
agentId,
|
|
3504
3908
|
adapterId,
|
|
3505
3909
|
...(nativeSessionId === undefined ? {} : { nativeSessionId }),
|
|
3506
|
-
...(runtimeGenerationId === undefined ? {} : { runtimeGenerationId }),
|
|
3507
3910
|
expectedProgressAt,
|
|
3508
|
-
reason: `
|
|
3911
|
+
reason: `AgentRun retired: ${reason}`
|
|
3509
3912
|
}, now);
|
|
3510
|
-
if (terminal.disposition !== "applied" || terminal.
|
|
3913
|
+
if (terminal.disposition !== "applied" || terminal.run === null) {
|
|
3511
3914
|
throw usageError(terminal.disposition === "blocked"
|
|
3512
|
-
? `
|
|
3513
|
-
: `
|
|
3915
|
+
? `AgentRun retirement is blocked: ${run.id}/${terminal.reason ?? "unsafe"}.`
|
|
3916
|
+
: `AgentRun changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
3514
3917
|
}
|
|
3515
|
-
run = terminal.
|
|
3918
|
+
run = terminal.run;
|
|
3516
3919
|
}
|
|
3517
|
-
recordTaskEvent(tx, task.id, "
|
|
3518
|
-
|
|
3920
|
+
recordTaskEvent(tx, task.id, "run.retired", {
|
|
3921
|
+
runId: run.id,
|
|
3519
3922
|
reason,
|
|
3520
3923
|
...(parsed.options.get("--expected-progress-at") === undefined
|
|
3521
3924
|
&& parsed.options.get("--progress-at") === undefined
|
|
@@ -3527,9 +3930,6 @@ function retireTurn(args, store, options) {
|
|
|
3527
3930
|
...(parsed.options.get("--native-session-id") === undefined
|
|
3528
3931
|
? {}
|
|
3529
3932
|
: { nativeSessionId: parsed.options.get("--native-session-id") }),
|
|
3530
|
-
...(parsed.options.get("--launch-id") === undefined
|
|
3531
|
-
? {}
|
|
3532
|
-
: { runtimeGenerationId: parsed.options.get("--launch-id") }),
|
|
3533
3933
|
...(actor === "leader"
|
|
3534
3934
|
? leaderActionEventPayload(tx, task.id, options)
|
|
3535
3935
|
: { retiredBy: actor })
|
|
@@ -3537,98 +3937,98 @@ function retireTurn(args, store, options) {
|
|
|
3537
3937
|
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
3538
3938
|
eventId: tx.nextEventId(task.id),
|
|
3539
3939
|
taskId: task.id,
|
|
3540
|
-
recordKind: "
|
|
3940
|
+
recordKind: "run",
|
|
3541
3941
|
recordId: run.id,
|
|
3542
3942
|
reason,
|
|
3543
3943
|
retiredBy: actor
|
|
3544
3944
|
}, now));
|
|
3545
|
-
return { task,
|
|
3945
|
+
return { task, run: run, changed: true };
|
|
3546
3946
|
});
|
|
3547
3947
|
if (result.changed)
|
|
3548
3948
|
options.runtime?.notifyStateChanged(result.task.id);
|
|
3549
|
-
return output(`Retired
|
|
3550
|
-
|
|
3949
|
+
return output(`Retired AgentRun ${result.task.id}/${result.run.id}\n`, {
|
|
3950
|
+
run: result.run,
|
|
3551
3951
|
retired: true
|
|
3552
3952
|
});
|
|
3553
3953
|
}
|
|
3554
|
-
function
|
|
3954
|
+
function runContextCommand(args, store, options) {
|
|
3555
3955
|
const [first, ...rest] = args;
|
|
3556
3956
|
if (first === "expand") {
|
|
3557
|
-
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].";
|
|
3558
3958
|
const parsed = parseTail(rest, new Set(["--store", "--mode"]), usage);
|
|
3559
3959
|
exactPositionals(parsed.positionals, 2, usage);
|
|
3560
3960
|
const mode = parsed.options.get("--mode");
|
|
3561
3961
|
if (mode !== undefined && mode !== "full") {
|
|
3562
|
-
throw usageError("
|
|
3962
|
+
throw usageError("AgentRun Context expansion mode must be full.", usage);
|
|
3563
3963
|
}
|
|
3564
|
-
const { taskId,
|
|
3565
|
-
|
|
3566
|
-
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")));
|
|
3567
3967
|
return output(`${JSON.stringify(expanded, null, 2)}\n`, { context: expanded });
|
|
3568
3968
|
}
|
|
3569
3969
|
if (first === "delta") {
|
|
3570
3970
|
if (rest.length !== 3 || rest[1] !== "--after") {
|
|
3571
|
-
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>.");
|
|
3572
3972
|
}
|
|
3573
|
-
const { taskId,
|
|
3574
|
-
|
|
3575
|
-
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])));
|
|
3576
3976
|
return output(`${JSON.stringify(delta, null, 2)}\n`, { contextDelta: delta });
|
|
3577
3977
|
}
|
|
3578
3978
|
if (first === undefined || rest.length !== 0) {
|
|
3579
|
-
throw usageError("Task turn context usage: yui task
|
|
3979
|
+
throw usageError("Task turn context usage: yui task run context <task>/<run>.");
|
|
3580
3980
|
}
|
|
3581
|
-
const { taskId,
|
|
3582
|
-
|
|
3583
|
-
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));
|
|
3584
3984
|
return output(`${JSON.stringify(pack, null, 2)}\n`, { context: pack });
|
|
3585
3985
|
}
|
|
3586
|
-
function
|
|
3587
|
-
const [taskId,
|
|
3588
|
-
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
|
|
3589
3989
|
|| extra !== undefined) {
|
|
3590
|
-
throw usageError(`
|
|
3990
|
+
throw usageError(`AgentRun context reference is invalid: ${value}.`);
|
|
3591
3991
|
}
|
|
3592
|
-
return { taskId,
|
|
3992
|
+
return { taskId, runId };
|
|
3593
3993
|
}
|
|
3594
3994
|
/**
|
|
3595
|
-
* A
|
|
3995
|
+
* A AgentRun Context Pack is information the Role's own runtime reads in order to
|
|
3596
3996
|
* work. Authorization is therefore scope-shaped, not schedule-shaped: the
|
|
3597
|
-
* 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,
|
|
3598
3998
|
* proven by its per-Session caller key against durable state.
|
|
3599
3999
|
*
|
|
3600
|
-
* It deliberately does not require the
|
|
3601
|
-
* 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
|
|
3602
4002
|
* given; losing read access to its own Role's history is what forces an
|
|
3603
4003
|
* otherwise healthy Agent to stop and escalate to a human.
|
|
3604
4004
|
*/
|
|
3605
|
-
function
|
|
4005
|
+
function authorizeRunContext(store, taskId, runId, environment) {
|
|
3606
4006
|
const managed = environment?.YUI_SESSION_SCOPE !== undefined
|
|
3607
4007
|
|| environment?.YUI_TASK_ID !== undefined
|
|
3608
4008
|
|| environment?.YUI_ROLE !== undefined;
|
|
3609
4009
|
if (!managed)
|
|
3610
4010
|
return;
|
|
3611
|
-
const run = store.
|
|
4011
|
+
const run = store.getRun(taskId, runId);
|
|
3612
4012
|
if (run === null) {
|
|
3613
|
-
throw usageError(`
|
|
4013
|
+
throw usageError(`AgentRun Context access is not authorized: ${taskId}/${runId}.`);
|
|
3614
4014
|
}
|
|
3615
4015
|
if (currentManagedRuntime(store, environment, taskId, run.roleName) === undefined) {
|
|
3616
|
-
throw usageError(`
|
|
4016
|
+
throw usageError(`AgentRun Context access requires the current runtime of ${taskId}/${run.roleName}.`);
|
|
3617
4017
|
}
|
|
3618
4018
|
}
|
|
3619
|
-
function
|
|
3620
|
-
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>.";
|
|
3621
4021
|
exactPositionals(args, 1, usage);
|
|
3622
4022
|
const reference = args[0];
|
|
3623
4023
|
const task = store.getTask(reference);
|
|
3624
4024
|
const item = task === null ? requireWorkItem(store, reference, options) : null;
|
|
3625
4025
|
const taskId = task?.id ?? item.taskId;
|
|
3626
|
-
const
|
|
3627
|
-
if (
|
|
3628
|
-
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";
|
|
3629
4029
|
const events = store.listEvents(taskId);
|
|
3630
|
-
return `${renderTable(`
|
|
3631
|
-
{ header: "
|
|
4030
|
+
return `${renderTable(`AgentRuns: ${item?.id ?? taskId}`, [
|
|
4031
|
+
{ header: "AgentRun", minWidth: 6, maxWidth: 20 },
|
|
3632
4032
|
{ header: "Role", minWidth: 4, maxWidth: 22 },
|
|
3633
4033
|
{ header: "Subject", minWidth: 7, maxWidth: 24 },
|
|
3634
4034
|
{ header: "Purpose", minWidth: 6, maxWidth: 10 },
|
|
@@ -3639,7 +4039,7 @@ function listTurns(args, store, options) {
|
|
|
3639
4039
|
{ header: "Status", minWidth: 6, maxWidth: 12 },
|
|
3640
4040
|
{ header: "History", minWidth: 7, maxWidth: 9 },
|
|
3641
4041
|
{ header: "Summary", minWidth: 8, maxWidth: 58 }
|
|
3642
|
-
],
|
|
4042
|
+
], runs.map((run) => [
|
|
3643
4043
|
run.id,
|
|
3644
4044
|
run.roleName,
|
|
3645
4045
|
run.workItemId ?? (run.reviewRoundId === undefined ? "task" : `review:${run.reviewRoundId}`),
|
|
@@ -3649,29 +4049,29 @@ function listTurns(args, store, options) {
|
|
|
3649
4049
|
run.effective.profileAccess,
|
|
3650
4050
|
run.effective.permission.strategy,
|
|
3651
4051
|
run.status,
|
|
3652
|
-
isTaskRecordRetired(events, "
|
|
4052
|
+
isTaskRecordRetired(events, "run", run.id) ? "retired" : "active",
|
|
3653
4053
|
run.result?.output ?? run.result?.diagnostic ?? "-"
|
|
3654
4054
|
]), defaultTableWidth())}\n`;
|
|
3655
4055
|
}
|
|
3656
4056
|
/**
|
|
3657
|
-
* Settles only the known bootstrap split where a failed Task-final
|
|
4057
|
+
* Settles only the known bootstrap split where a failed Task-final AgentRun
|
|
3658
4058
|
* still owns a running ReviewRound, but the committed Task heads have moved
|
|
3659
4059
|
* on. This is deliberately narrower than retry: it cannot manufacture a
|
|
3660
4060
|
* review or fail an arbitrary Round, and every identity/mailbox fence is
|
|
3661
4061
|
* checked before the old Round changes. The next normal Task completion then
|
|
3662
4062
|
* creates one fresh Round over the newer frozen Task heads.
|
|
3663
4063
|
*/
|
|
3664
|
-
function
|
|
3665
|
-
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>.");
|
|
3666
4066
|
const now = clock(options);
|
|
3667
|
-
const previous = store.transaction((tx) =>
|
|
4067
|
+
const previous = store.transaction((tx) => requireRun(tx, args[0], options));
|
|
3668
4068
|
const result = store.transaction((tx) => {
|
|
3669
|
-
const run = tx.
|
|
4069
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
3670
4070
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
3671
|
-
throw usageError(`
|
|
4071
|
+
throw usageError(`AgentRun ${previous.id} is not a failed review AgentRun.`);
|
|
3672
4072
|
}
|
|
3673
4073
|
if (run.reviewRoundId === undefined) {
|
|
3674
|
-
throw usageError(`Review
|
|
4074
|
+
throw usageError(`Review AgentRun ${run.id} has no ReviewRound.`);
|
|
3675
4075
|
}
|
|
3676
4076
|
const task = requireTask(tx, run.taskId);
|
|
3677
4077
|
if (task.status !== "active")
|
|
@@ -3679,26 +4079,26 @@ function settleStaleFinalReviewTurn(args, store, options) {
|
|
|
3679
4079
|
const actor = taskActor(tx, options, task.id);
|
|
3680
4080
|
const round = tx.getReviewRound(task.id, run.reviewRoundId);
|
|
3681
4081
|
if (round === null) {
|
|
3682
|
-
throw dataError(`ReviewRound not found for
|
|
4082
|
+
throw dataError(`ReviewRound not found for AgentRun ${run.id}: ${run.reviewRoundId}.`);
|
|
3683
4083
|
}
|
|
3684
4084
|
if ((round.scope ?? "work-item") !== "task") {
|
|
3685
|
-
throw usageError(`Review
|
|
4085
|
+
throw usageError(`Review AgentRun ${run.id} is not a Task-final review; request a new WorkItem review `
|
|
3686
4086
|
+ "for a new Candidate.");
|
|
3687
4087
|
}
|
|
3688
4088
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
3689
4089
|
if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)) {
|
|
3690
4090
|
throw usageError(`Task final-review contract does not match ReviewRound ${round.id}.`);
|
|
3691
4091
|
}
|
|
3692
|
-
// This read-only compare-and-swap fence covers the exact
|
|
4092
|
+
// This read-only compare-and-swap fence covers the exact AgentRun/Round,
|
|
3693
4093
|
// Candidate, stored Review workspace, frozen Project scope, and frozen
|
|
3694
4094
|
// Project heads before any mailbox or Round write.
|
|
3695
|
-
const validation =
|
|
4095
|
+
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
3696
4096
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
3697
|
-
throw usageError(`Review
|
|
4097
|
+
throw usageError(`Review AgentRun ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
3698
4098
|
}
|
|
3699
|
-
const
|
|
3700
|
-
if (
|
|
3701
|
-
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}.`);
|
|
3702
4102
|
}
|
|
3703
4103
|
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
|
|
3704
4104
|
.filter((entry) => (entry.scope ?? "work-item") === "task"));
|
|
@@ -3716,7 +4116,7 @@ function settleStaleFinalReviewTurn(args, store, options) {
|
|
|
3716
4116
|
}
|
|
3717
4117
|
if (round.status === "failed") {
|
|
3718
4118
|
if (round.failure?.kind === "execution") {
|
|
3719
|
-
return {
|
|
4119
|
+
return { run: run, round, changed: false };
|
|
3720
4120
|
}
|
|
3721
4121
|
throw usageError(`Final ReviewRound is already terminal: ${round.id}/${round.status}.`);
|
|
3722
4122
|
}
|
|
@@ -3724,53 +4124,54 @@ function settleStaleFinalReviewTurn(args, store, options) {
|
|
|
3724
4124
|
throw usageError(`Final ReviewRound is not stranded running: ${round.id}/${round.status}.`);
|
|
3725
4125
|
}
|
|
3726
4126
|
assertReviewerAvailable(tx, task.id, round.reviewerRoleName, round);
|
|
3727
|
-
const summary = `Review
|
|
4127
|
+
const summary = `Review AgentRun ${run.id} failed before delivery; committed Task heads changed.`;
|
|
3728
4128
|
const terminal = finishReviewRound(round, "failed", now, { kind: "execution", message: summary });
|
|
3729
4129
|
tx.saveReviewRound(task.id, terminal);
|
|
3730
|
-
recordTaskEvent(tx, task.id, "
|
|
3731
|
-
|
|
4130
|
+
recordTaskEvent(tx, task.id, "run.review-stale-settled", {
|
|
4131
|
+
runId: run.id,
|
|
3732
4132
|
reviewRoundId: round.id,
|
|
3733
4133
|
previousTaskCandidate: JSON.stringify(round.taskCandidate),
|
|
3734
4134
|
currentTaskCandidate: JSON.stringify(currentTaskCandidate),
|
|
3735
4135
|
settledBy: actor,
|
|
3736
4136
|
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
|
|
3737
4137
|
}, now);
|
|
3738
|
-
return {
|
|
4138
|
+
return { run: run, round: terminal, changed: true };
|
|
3739
4139
|
});
|
|
3740
4140
|
return output(result.changed
|
|
3741
|
-
? `Settled obsolete final Review ${result.round.id} from failed
|
|
3742
|
-
: `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 });
|
|
3743
4143
|
}
|
|
3744
|
-
function
|
|
3745
|
-
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>.");
|
|
3746
4146
|
const now = clock(options);
|
|
3747
|
-
const previous = store.transaction((tx) =>
|
|
4147
|
+
const previous = store.transaction((tx) => requireRun(tx, args[0], options));
|
|
3748
4148
|
if (previous.purpose === "review") {
|
|
3749
4149
|
return retryFailedReviewRun(previous, store, options, now);
|
|
3750
4150
|
}
|
|
3751
4151
|
const retried = store.transaction((tx) => {
|
|
3752
4152
|
if (previous.status !== "failed") {
|
|
3753
|
-
throw usageError(`
|
|
4153
|
+
throw usageError(`AgentRun ${previous.id} is not retryable from ${previous.status}.`);
|
|
3754
4154
|
}
|
|
3755
4155
|
const task = requireTask(tx, previous.taskId);
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
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
|
+
}
|
|
3759
4160
|
const role = requireRole(tx, task.id, previous.roleName);
|
|
3760
|
-
if (tx.
|
|
3761
|
-
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.`);
|
|
3762
4163
|
}
|
|
3763
4164
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
3764
4165
|
const retryItem = previous.workItemId === undefined
|
|
3765
4166
|
? null
|
|
3766
4167
|
: tx.getWorkItem(task.id, previous.workItemId);
|
|
3767
4168
|
if (previous.workItemId !== undefined && retryItem === null) {
|
|
3768
|
-
throw dataError(`Work item not found for
|
|
4169
|
+
throw dataError(`Work item not found for AgentRun ${previous.id}: ${previous.workItemId}.`);
|
|
3769
4170
|
}
|
|
3770
4171
|
const retriesSynthesisMain = previous.sourceExecutionGroupId !== undefined;
|
|
3771
4172
|
if (retriesSynthesisMain
|
|
3772
4173
|
&& (previous.executionGroupId !== undefined || previous.executionLaneId !== undefined)) {
|
|
3773
|
-
throw dataError(`
|
|
4174
|
+
throw dataError(`AgentRun ${previous.id} has conflicting execution lineage.`);
|
|
3774
4175
|
}
|
|
3775
4176
|
const retryLaneBefore = previous.executionLaneId === undefined
|
|
3776
4177
|
? undefined
|
|
@@ -3788,57 +4189,54 @@ function retryTurn(args, store, options) {
|
|
|
3788
4189
|
&& retryLaneBefore !== undefined
|
|
3789
4190
|
&& retryLaneBefore.id === previous.executionLaneId
|
|
3790
4191
|
&& retryLaneBefore.disposition === "open"
|
|
3791
|
-
&& retryLaneBefore.
|
|
4192
|
+
&& retryLaneBefore.currentRunId === previous.id);
|
|
3792
4193
|
if (!exactCurrentLane) {
|
|
3793
|
-
throw usageError(`
|
|
4194
|
+
throw usageError(`AgentRun ${previous.id} no longer owns the current failed Execution Lane.`);
|
|
3794
4195
|
}
|
|
3795
4196
|
const sourceGroup = !retriesSynthesisMain || retryItem === null
|
|
3796
4197
|
? undefined
|
|
3797
4198
|
: workItemExecutionGroupById(retryItem, previous.sourceExecutionGroupId);
|
|
3798
|
-
const
|
|
3799
|
-
?
|
|
3800
|
-
&&
|
|
3801
|
-
&&
|
|
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)))
|
|
3802
4203
|
: [];
|
|
3803
4204
|
const exactSourceMain = !retriesSynthesisMain || (retryItem !== null
|
|
3804
|
-
&& retryItem.status === "
|
|
4205
|
+
&& retryItem.status === "open"
|
|
3805
4206
|
&& retryItem.assignee === previous.roleName
|
|
3806
4207
|
&& sourceGroup !== undefined
|
|
3807
4208
|
&& currentRetryGroup?.id === sourceGroup.id
|
|
3808
|
-
&&
|
|
3809
|
-
&&
|
|
3810
|
-
>= MINIMUM_WORK_ITEM_SYNTHESIS_RESULTS
|
|
3811
|
-
&& sourceMainTurns.at(-1)?.id === previous.id);
|
|
4209
|
+
&& selectedWorkItemSynthesisProducers(tx, retryItem, sourceGroup, synthesisSourceRunIds(tx, previous)).length > 0
|
|
4210
|
+
&& sourceMainRuns.at(-1)?.id === previous.id);
|
|
3812
4211
|
if (!exactSourceMain) {
|
|
3813
|
-
throw usageError(`
|
|
4212
|
+
throw usageError(`AgentRun ${previous.id} no longer owns the current WorkItem main synthesis.`);
|
|
3814
4213
|
}
|
|
3815
|
-
const
|
|
4214
|
+
const directMainRuns = retryItem === null
|
|
3816
4215
|
? []
|
|
3817
|
-
:
|
|
3818
|
-
&&
|
|
3819
|
-
&&
|
|
3820
|
-
&&
|
|
3821
|
-
&&
|
|
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)));
|
|
3822
4221
|
const retriesDirectMain = retryItem !== null
|
|
3823
4222
|
&& !retriesExecutionLane
|
|
3824
4223
|
&& !retriesSynthesisMain;
|
|
3825
|
-
const exactDirectMain = !retriesDirectMain || (
|
|
4224
|
+
const exactDirectMain = !retriesDirectMain || (retryItem.status === "open"
|
|
3826
4225
|
&& retryItem.assignee === previous.roleName
|
|
3827
|
-
&&
|
|
4226
|
+
&& directMainRuns.at(-1)?.id === previous.id);
|
|
3828
4227
|
if (!exactDirectMain) {
|
|
3829
|
-
throw usageError(`
|
|
4228
|
+
throw usageError(`AgentRun ${previous.id} no longer owns the current direct WorkItem execution.`);
|
|
3830
4229
|
}
|
|
3831
|
-
const groupedRunningRetry = retryItem?.status === "
|
|
4230
|
+
const groupedRunningRetry = retryItem?.status === "open"
|
|
3832
4231
|
&& retriesExecutionLane
|
|
3833
4232
|
&& exactCurrentLane;
|
|
3834
|
-
const synthesisRunningRetry = retryItem?.status === "
|
|
4233
|
+
const synthesisRunningRetry = retryItem?.status === "open"
|
|
3835
4234
|
&& retriesSynthesisMain
|
|
3836
4235
|
&& exactSourceMain;
|
|
3837
|
-
const directRunningRetry = retryItem?.status === "
|
|
4236
|
+
const directRunningRetry = retryItem?.status === "open"
|
|
3838
4237
|
&& retriesDirectMain
|
|
3839
4238
|
&& exactDirectMain;
|
|
3840
4239
|
if (retryItem !== null
|
|
3841
|
-
&& retryItem.status !== "failed"
|
|
3842
4240
|
&& !groupedRunningRetry
|
|
3843
4241
|
&& !synthesisRunningRetry
|
|
3844
4242
|
&& !directRunningRetry) {
|
|
@@ -3865,7 +4263,7 @@ function retryTurn(args, store, options) {
|
|
|
3865
4263
|
&& (retryGroup === undefined
|
|
3866
4264
|
|| retryGroup.id !== previous.executionGroupId
|
|
3867
4265
|
|| retryLane === undefined))) {
|
|
3868
|
-
throw dataError(`
|
|
4266
|
+
throw dataError(`AgentRun ${previous.id} execution lineage no longer matches its Work Item.`);
|
|
3869
4267
|
}
|
|
3870
4268
|
const retryManagedWorkspace = retryLane === undefined ? runWorkspace : previous.workspace;
|
|
3871
4269
|
if (retryLane !== undefined) {
|
|
@@ -3885,7 +4283,7 @@ function retryTurn(args, store, options) {
|
|
|
3885
4283
|
|| !isDeepStrictEqual(writableProjectIds, [...retryLane.workspace.writableProjectIds].sort())
|
|
3886
4284
|
|| storedLaneWorkspace === null
|
|
3887
4285
|
|| !isDeepStrictEqual(storedLaneWorkspace, previous.workspace)) {
|
|
3888
|
-
throw dataError(`
|
|
4286
|
+
throw dataError(`AgentRun ${previous.id} Lane workspace is missing or has drifted.`);
|
|
3889
4287
|
}
|
|
3890
4288
|
}
|
|
3891
4289
|
if (retriesSynthesisMain) {
|
|
@@ -3902,7 +4300,7 @@ function retryTurn(args, store, options) {
|
|
|
3902
4300
|
|| currentMainWorkspace === null
|
|
3903
4301
|
|| !isDeepStrictEqual(storedMainWorkspace, previous.workspace)
|
|
3904
4302
|
|| !isDeepStrictEqual(currentMainWorkspace, previous.workspace)) {
|
|
3905
|
-
throw dataError(`
|
|
4303
|
+
throw dataError(`AgentRun ${previous.id} WorkItem main workspace is missing or has drifted.`);
|
|
3906
4304
|
}
|
|
3907
4305
|
}
|
|
3908
4306
|
if (retriesDirectMain) {
|
|
@@ -3919,20 +4317,20 @@ function retryTurn(args, store, options) {
|
|
|
3919
4317
|
|| currentDirectWorkspace === null
|
|
3920
4318
|
|| !isDeepStrictEqual(storedDirectWorkspace, previous.workspace)
|
|
3921
4319
|
|| !isDeepStrictEqual(currentDirectWorkspace, previous.workspace)) {
|
|
3922
|
-
throw dataError(`
|
|
4320
|
+
throw dataError(`AgentRun ${previous.id} direct WorkItem workspace is missing or has drifted.`);
|
|
3923
4321
|
}
|
|
3924
4322
|
}
|
|
3925
4323
|
const effective = retryLane?.effective ?? resolveEffectiveLaunch({
|
|
3926
4324
|
role,
|
|
3927
|
-
purpose:
|
|
4325
|
+
purpose: previous.purpose,
|
|
3928
4326
|
...(retryManagedWorkspace === undefined ? {} : { workspace: retryManagedWorkspace }),
|
|
3929
4327
|
...(retryItem === null ? {} : { workItemWriteProjectIds: retryItem.writeProjectIds })
|
|
3930
4328
|
});
|
|
3931
|
-
const
|
|
4329
|
+
const runId = tx.nextRunId(task.id);
|
|
3932
4330
|
const runningGroup = retryGroup === undefined || retryLane === undefined
|
|
3933
4331
|
? undefined
|
|
3934
4332
|
: updateWorkItemExecutionLane(retryGroup, retryLane.id, {
|
|
3935
|
-
|
|
4333
|
+
currentRunId: runId
|
|
3936
4334
|
}, now);
|
|
3937
4335
|
// Restart the bound lane and reopen the failed WorkItem as two ordered
|
|
3938
4336
|
// single-step record revisions, matching the dispatch path. Folding both
|
|
@@ -3941,11 +4339,7 @@ function retryTurn(args, store, options) {
|
|
|
3941
4339
|
const laneRestartedItem = retryItem === null || runningGroup === undefined
|
|
3942
4340
|
? retryItem
|
|
3943
4341
|
: updateWorkItemExecutionGroup(retryItem, runningGroup, now);
|
|
3944
|
-
const retriedItemWithGroup = laneRestartedItem
|
|
3945
|
-
? null
|
|
3946
|
-
: laneRestartedItem.status === "failed"
|
|
3947
|
-
? retryFailedWorkItem(laneRestartedItem, now)
|
|
3948
|
-
: laneRestartedItem;
|
|
4342
|
+
const retriedItemWithGroup = laneRestartedItem;
|
|
3949
4343
|
if (retriedItemWithGroup !== null) {
|
|
3950
4344
|
if (laneRestartedItem !== null && laneRestartedItem !== retryItem) {
|
|
3951
4345
|
tx.saveWorkItem(task.id, laneRestartedItem);
|
|
@@ -3957,13 +4351,13 @@ function retryTurn(args, store, options) {
|
|
|
3957
4351
|
const input = retriesSynthesisMain
|
|
3958
4352
|
? previous.inputs[0].input
|
|
3959
4353
|
: (() => {
|
|
3960
|
-
const retrySnapshot =
|
|
4354
|
+
const retrySnapshot = freezeRunContextSnapshot(tx, {
|
|
3961
4355
|
taskId: task.id,
|
|
3962
4356
|
roleName: role.name,
|
|
3963
|
-
purpose:
|
|
4357
|
+
purpose: previous.purpose,
|
|
3964
4358
|
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId })
|
|
3965
4359
|
}, now, "controller", retryGroup?.assignment.contextSnapshotRef);
|
|
3966
|
-
return
|
|
4360
|
+
return createRunInput({
|
|
3967
4361
|
source: {
|
|
3968
4362
|
type: "yui",
|
|
3969
4363
|
channel: previous.workItemId === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
@@ -3975,7 +4369,8 @@ function retryTurn(args, store, options) {
|
|
|
3975
4369
|
deltaRefIds: contextSnapshotDeltaRefIds(tx, retrySnapshot)
|
|
3976
4370
|
});
|
|
3977
4371
|
})();
|
|
3978
|
-
const created =
|
|
4372
|
+
const created = createRun(runId, task.id, role.name, roleAgentSessionResumeMode(sessions, effective.agentId, effective), input, now, {
|
|
4373
|
+
purpose: previous.purpose,
|
|
3979
4374
|
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId }),
|
|
3980
4375
|
...(runningGroup === undefined ? {} : {
|
|
3981
4376
|
executionGroupId: runningGroup.id,
|
|
@@ -3987,8 +4382,8 @@ function retryTurn(args, store, options) {
|
|
|
3987
4382
|
...(retryManagedWorkspace === undefined ? {} : { workspace: retryManagedWorkspace }),
|
|
3988
4383
|
effective
|
|
3989
4384
|
});
|
|
3990
|
-
tx.
|
|
3991
|
-
tx.
|
|
4385
|
+
tx.saveRun(created);
|
|
4386
|
+
tx.saveActiveRun(created);
|
|
3992
4387
|
if (previous.workItemId !== undefined && retriedItemWithGroup !== null) {
|
|
3993
4388
|
const item = retriedItemWithGroup;
|
|
3994
4389
|
const workspace = tx.getWorkItemWorkspace(task.id, item.id);
|
|
@@ -3998,21 +4393,21 @@ function retryTurn(args, store, options) {
|
|
|
3998
4393
|
+ `cannot retry ${item.id}.`);
|
|
3999
4394
|
}
|
|
4000
4395
|
}
|
|
4001
|
-
|
|
4396
|
+
enqueueRoleRunDispatch(tx, {
|
|
4002
4397
|
taskId: task.id,
|
|
4003
4398
|
roleName: role.name,
|
|
4004
|
-
|
|
4399
|
+
runId: created.id,
|
|
4005
4400
|
reason: "turn-retried",
|
|
4006
4401
|
occurredAt: now
|
|
4007
4402
|
});
|
|
4008
|
-
recordTaskEvent(tx, task.id, "
|
|
4009
|
-
...
|
|
4010
|
-
|
|
4403
|
+
recordTaskEvent(tx, task.id, "run.retried", {
|
|
4404
|
+
...runLaunchEventPayload(created),
|
|
4405
|
+
previousRunId: previous.id
|
|
4011
4406
|
}, now);
|
|
4012
|
-
return { kind: "
|
|
4407
|
+
return { kind: "run", run: created };
|
|
4013
4408
|
});
|
|
4014
|
-
notifyMailbox(options.runtime, roleMailbox(retried.
|
|
4015
|
-
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`);
|
|
4016
4411
|
}
|
|
4017
4412
|
function actualTaskReviewCandidateForMutation(store, task, options) {
|
|
4018
4413
|
if (options.actualTaskReviewCandidate === undefined) {
|
|
@@ -4055,7 +4450,7 @@ function taskReviewProducerCollision(provenance, reviewerRoleName) {
|
|
|
4055
4450
|
*
|
|
4056
4451
|
* When `expected` is supplied this is also the final dispatch compare-and-swap
|
|
4057
4452
|
* fence: every bound Project must still point at the exact frozen physical
|
|
4058
|
-
* head. Drift fails closed before a Reviewer
|
|
4453
|
+
* head. Drift fails closed before a Reviewer AgentRun is created.
|
|
4059
4454
|
*/
|
|
4060
4455
|
function taskReviewProvenance(store, task, options, expected) {
|
|
4061
4456
|
const candidate = actualTaskReviewCandidateForMutation(store, task, options);
|
|
@@ -4110,13 +4505,13 @@ function taskReviewProvenance(store, task, options, expected) {
|
|
|
4110
4505
|
recordProducer(LEADER_ROLE, item.id);
|
|
4111
4506
|
continue;
|
|
4112
4507
|
}
|
|
4113
|
-
const sourceRun = store.
|
|
4508
|
+
const sourceRun = store.getRun(task.id, sourceCandidate.source.runId);
|
|
4114
4509
|
if (sourceRun === null
|
|
4115
4510
|
|| sourceRun.workItemId !== item.id
|
|
4116
4511
|
|| sourceRun.purpose !== "execution"
|
|
4117
4512
|
|| sourceRun.status !== "completed") {
|
|
4118
|
-
throw dataError(`Committed producer Candidate
|
|
4119
|
-
+ `${item.id}/${sourceCandidate.source.
|
|
4513
|
+
throw dataError(`Committed producer Candidate AgentRun is unavailable: `
|
|
4514
|
+
+ `${item.id}/${sourceCandidate.source.runId}.`);
|
|
4120
4515
|
}
|
|
4121
4516
|
recordProducer(sourceRun.roleName, item.id);
|
|
4122
4517
|
if (sourceRun.sourceExecutionGroupId !== undefined) {
|
|
@@ -4125,7 +4520,7 @@ function taskReviewProvenance(store, task, options, expected) {
|
|
|
4125
4520
|
throw dataError(`Committed producer ExecutionGroup is unavailable: `
|
|
4126
4521
|
+ `${item.id}/${sourceRun.sourceExecutionGroupId}.`);
|
|
4127
4522
|
}
|
|
4128
|
-
for (const producer of
|
|
4523
|
+
for (const producer of selectedWorkItemSynthesisProducers(store, item, executionGroup, synthesisSourceRunIds(store, sourceRun))) {
|
|
4129
4524
|
recordProducer(producer.roleName, item.id);
|
|
4130
4525
|
}
|
|
4131
4526
|
}
|
|
@@ -4174,7 +4569,7 @@ function queueTaskReviewRound(store, task, config, taskCandidate, options, now,
|
|
|
4174
4569
|
const availability = projectReviewerAvailability(store, task.id, config.roleName);
|
|
4175
4570
|
if (availability.kind === "busy") {
|
|
4176
4571
|
throw usageError(`Reviewer ${config.roleName} is busy (${availability.phase}`
|
|
4177
|
-
+ `${availability.
|
|
4572
|
+
+ `${availability.activeRunId === undefined ? "" : `; AgentRun ${availability.activeRunId}`}); `
|
|
4178
4573
|
+ `retry after ${availability.retryAfterSeconds}s.`);
|
|
4179
4574
|
}
|
|
4180
4575
|
let reviewer = store.getRole(task.id, config.roleName);
|
|
@@ -4243,16 +4638,16 @@ function resumablePendingFinalTaskReview(store, task, round, config, taskCandida
|
|
|
4243
4638
|
|| round.reviewerRoleName !== taskFinalContract.reviewerRoleName) {
|
|
4244
4639
|
throw usageError(`Pending final ReviewRound Reviewer identity changed: ${round.id}.`);
|
|
4245
4640
|
}
|
|
4246
|
-
if (round.
|
|
4247
|
-
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}.`);
|
|
4248
4643
|
}
|
|
4249
4644
|
assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id), round.id, round.reviewerRoleName);
|
|
4250
4645
|
const reviewer = store.getRole(task.id, round.reviewerRoleName);
|
|
4251
4646
|
if (reviewer === null || reviewer.name !== round.reviewerRoleName) {
|
|
4252
4647
|
throw usageError(`Pending final ReviewRound Reviewer identity changed: ${round.id}.`);
|
|
4253
4648
|
}
|
|
4254
|
-
if (store.
|
|
4255
|
-
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}.`);
|
|
4256
4651
|
}
|
|
4257
4652
|
assertPendingFinalReviewWorkspaceEvidence(store, task, round);
|
|
4258
4653
|
return round;
|
|
@@ -4288,7 +4683,7 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
4288
4683
|
}
|
|
4289
4684
|
}
|
|
4290
4685
|
/**
|
|
4291
|
-
* Task-control retry of an exact failed review
|
|
4686
|
+
* Task-control retry of an exact failed review AgentRun. The old failed AgentRun
|
|
4292
4687
|
* remains the attempt trail, while the ReviewRound is reset to pending under
|
|
4293
4688
|
* its existing identity. Every identity and frozen-head fence is
|
|
4294
4689
|
* checked inside one transaction so a partial fail-old-without-reset state can
|
|
@@ -4296,12 +4691,12 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
4296
4691
|
*/
|
|
4297
4692
|
function retryFailedReviewRun(previous, store, options, now) {
|
|
4298
4693
|
const result = store.transaction((tx) => {
|
|
4299
|
-
const run = tx.
|
|
4694
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
4300
4695
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
4301
|
-
throw usageError(`
|
|
4696
|
+
throw usageError(`AgentRun ${previous.id} is not a failed review AgentRun.`);
|
|
4302
4697
|
}
|
|
4303
4698
|
if (run.reviewRoundId === undefined) {
|
|
4304
|
-
throw usageError(`Review
|
|
4699
|
+
throw usageError(`Review AgentRun ${run.id} has no ReviewRound.`);
|
|
4305
4700
|
}
|
|
4306
4701
|
const task = requireTask(tx, run.taskId);
|
|
4307
4702
|
if (task.status !== "active")
|
|
@@ -4317,16 +4712,16 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4317
4712
|
: round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
|
|
4318
4713
|
if (run.executionGroupId !== undefined && (round.executionGroup?.id !== run.executionGroupId
|
|
4319
4714
|
|| retryLane === undefined
|
|
4320
|
-
|| retryLane.
|
|
4715
|
+
|| retryLane.currentRunId !== run.id
|
|
4321
4716
|
|| retryLane.roleName !== run.roleName)) {
|
|
4322
|
-
throw usageError(`Review
|
|
4717
|
+
throw usageError(`Review AgentRun ${run.id} no longer owns its exact Review Lane attempt.`);
|
|
4323
4718
|
}
|
|
4324
4719
|
const panelGroup = round.executionGroup !== undefined;
|
|
4325
4720
|
const runningPanelLaneRetry = round.status === "running"
|
|
4326
4721
|
&& panelGroup
|
|
4327
4722
|
&& retryLane?.disposition === "open";
|
|
4328
4723
|
if (round.status === "running" && panelGroup && !runningPanelLaneRetry) {
|
|
4329
|
-
throw usageError(`Review
|
|
4724
|
+
throw usageError(`Review AgentRun ${run.id} is not the current failed Lane attempt in running Round ${round.id}.`);
|
|
4330
4725
|
}
|
|
4331
4726
|
const retryReviewerRoleName = retryLane?.roleName ?? round.reviewerRoleName;
|
|
4332
4727
|
if (round.status !== "failed"
|
|
@@ -4362,10 +4757,10 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4362
4757
|
throw dataError(`WorkItem ReviewRound has no Candidate anchor: ${round.id}.`);
|
|
4363
4758
|
}
|
|
4364
4759
|
if (run.workItemId !== round.workItemId) {
|
|
4365
|
-
throw usageError(`Review
|
|
4760
|
+
throw usageError(`Review AgentRun ${run.id} does not match WorkItem ${round.workItemId}.`);
|
|
4366
4761
|
}
|
|
4367
4762
|
const item = tx.getWorkItem(task.id, round.workItemId);
|
|
4368
|
-
if (item === null || item.status !== "
|
|
4763
|
+
if (item === null || (item.status !== "open" || item.candidates.length === 0)) {
|
|
4369
4764
|
throw usageError(`WorkItem ReviewRound ${round.id} no longer has an awaiting Candidate.`);
|
|
4370
4765
|
}
|
|
4371
4766
|
const candidate = currentWorkItemCandidate(item);
|
|
@@ -4404,20 +4799,20 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4404
4799
|
throw usageError(`Reviewer already has an active review round for this candidate: ${activeRound.id}.`);
|
|
4405
4800
|
}
|
|
4406
4801
|
const reviewer = requireRole(tx, task.id, retryReviewerRoleName);
|
|
4407
|
-
const activePointer = tx.
|
|
4802
|
+
const activePointer = tx.getActiveRun(task.id, reviewer.name);
|
|
4408
4803
|
// Issue 06: a completed same-Round retry is a no-write idempotent result.
|
|
4409
4804
|
if (round.status === "completed") {
|
|
4410
4805
|
assertReviewerAvailable(tx, task.id, reviewer.name);
|
|
4411
4806
|
return { round, previousRun: run, created: false };
|
|
4412
4807
|
}
|
|
4413
4808
|
// Issue 06: a running same Round is reusable only with its exact active
|
|
4414
|
-
//
|
|
4809
|
+
// AgentRun. A stranded AgentRun (no active pointer) falls through and resets the
|
|
4415
4810
|
// Round after the identity fences below.
|
|
4416
4811
|
if (round.status === "running" && !runningPanelLaneRetry) {
|
|
4417
|
-
const
|
|
4418
|
-
const activeMatches =
|
|
4812
|
+
const reviewerRunId = round.reviewerRunId;
|
|
4813
|
+
const activeMatches = reviewerRunId !== undefined
|
|
4419
4814
|
&& activePointer !== null
|
|
4420
|
-
&& activePointer.id ===
|
|
4815
|
+
&& activePointer.id === reviewerRunId
|
|
4421
4816
|
&& activePointer.status === "active";
|
|
4422
4817
|
if (activePointer !== null && !activeMatches) {
|
|
4423
4818
|
throw usageError(`Existing running ReviewRound ${round.id} lacks its exact active Reviewer execution.`);
|
|
@@ -4430,24 +4825,65 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4430
4825
|
// Issue 06: an already-pending Round is the idempotent retry result.
|
|
4431
4826
|
if (round.status === "pending") {
|
|
4432
4827
|
if (activePointer !== null) {
|
|
4433
|
-
throw usageError(`Reviewer Role already has an active
|
|
4828
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
4434
4829
|
}
|
|
4435
4830
|
assertReviewerAvailable(tx, task.id, reviewer.name, round);
|
|
4436
4831
|
return { round, previousRun: run, created: false };
|
|
4437
4832
|
}
|
|
4438
4833
|
if (activePointer !== null) {
|
|
4439
|
-
throw usageError(`Reviewer Role already has an active
|
|
4834
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
4440
4835
|
}
|
|
4441
4836
|
assertReviewerAvailable(tx, task.id, reviewer.name, round);
|
|
4442
|
-
const validation =
|
|
4837
|
+
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
4443
4838
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
4444
|
-
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 };
|
|
4445
4881
|
}
|
|
4446
4882
|
if (runningPanelLaneRetry) {
|
|
4447
4883
|
const resetRound = retryRunningReviewExecutionLane(round, retryLane.id, run.id);
|
|
4448
4884
|
tx.saveReviewRound(task.id, resetRound);
|
|
4449
|
-
recordTaskEvent(tx, task.id, "
|
|
4450
|
-
|
|
4885
|
+
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
4886
|
+
runId: run.id,
|
|
4451
4887
|
reviewRoundId: round.id,
|
|
4452
4888
|
executionLaneId: retryLane.id
|
|
4453
4889
|
}, now);
|
|
@@ -4457,7 +4893,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4457
4893
|
// fence has passed. The outer transaction rolls back if Round creation fails.
|
|
4458
4894
|
let roundToReset = round;
|
|
4459
4895
|
if (round.status !== "failed") {
|
|
4460
|
-
const summary = `Review
|
|
4896
|
+
const summary = `Review AgentRun ${run.id} failed before delivery.`;
|
|
4461
4897
|
roundToReset = finishReviewRound(round, "failed", now, { kind: "execution", message: summary });
|
|
4462
4898
|
tx.saveReviewRound(task.id, roundToReset);
|
|
4463
4899
|
}
|
|
@@ -4466,41 +4902,44 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4466
4902
|
// stable across execution-attempt failures.
|
|
4467
4903
|
const resetRound = retryReviewRound(roundToReset, requestedBy, now);
|
|
4468
4904
|
tx.saveReviewRound(task.id, resetRound);
|
|
4469
|
-
recordTaskEvent(tx, task.id, "
|
|
4470
|
-
|
|
4905
|
+
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
4906
|
+
runId: run.id,
|
|
4471
4907
|
reviewRoundId: round.id
|
|
4472
4908
|
}, now);
|
|
4473
4909
|
return { round: resetRound, previousRun: run, created: true };
|
|
4474
4910
|
});
|
|
4911
|
+
if ("run" in result && result.run !== undefined) {
|
|
4912
|
+
notifyMailbox(options.runtime, roleMailbox(result.run.taskId, result.run.roleName), result.run.taskId);
|
|
4913
|
+
}
|
|
4475
4914
|
return output(result.created
|
|
4476
4915
|
? `Review retry requested as ${result.round.id}\n`
|
|
4477
|
-
: `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 } : {}) });
|
|
4478
4917
|
}
|
|
4479
|
-
/**
|
|
4480
|
-
function
|
|
4481
|
-
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].";
|
|
4482
4921
|
const asJson = args.includes("--json");
|
|
4483
4922
|
const positionals = args.filter((arg) => arg !== "--json");
|
|
4484
4923
|
exactPositionals(positionals, 1, usage);
|
|
4485
4924
|
const data = store.transaction((tx) => {
|
|
4486
|
-
const run =
|
|
4925
|
+
const run = requireRun(tx, positionals[0], options);
|
|
4487
4926
|
const retirement = tx.listEvents(run.taskId)
|
|
4488
4927
|
.map(taskRecordRetirement)
|
|
4489
|
-
.find((entry) => entry?.recordKind === "
|
|
4490
|
-
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)) };
|
|
4491
4930
|
});
|
|
4492
4931
|
if (asJson) {
|
|
4493
4932
|
return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
|
|
4494
4933
|
}
|
|
4495
4934
|
return {
|
|
4496
4935
|
kind: "output",
|
|
4497
|
-
output:
|
|
4936
|
+
output: `Delivery observation: ${data.execution.delivery}\n` + renderRunShow(data.run, data.retirement),
|
|
4498
4937
|
data
|
|
4499
4938
|
};
|
|
4500
4939
|
}
|
|
4501
|
-
function
|
|
4940
|
+
function renderRunShow(run, retirement) {
|
|
4502
4941
|
const lines = [
|
|
4503
|
-
`
|
|
4942
|
+
`AgentRun: ${run.id}`,
|
|
4504
4943
|
`Task: ${run.taskId}`,
|
|
4505
4944
|
`Role: ${run.roleName}`,
|
|
4506
4945
|
`Purpose: ${run.purpose}`,
|
|
@@ -4523,40 +4962,40 @@ function renderTurnShow(run, retirement) {
|
|
|
4523
4962
|
return `${lines.join("\n")}\n`;
|
|
4524
4963
|
}
|
|
4525
4964
|
/**
|
|
4526
|
-
* Records a structured progress checkpoint for an active
|
|
4527
|
-
*
|
|
4528
|
-
* a healthy but long-running
|
|
4529
|
-
* 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
|
|
4530
4969
|
* Leader.
|
|
4531
4970
|
*/
|
|
4532
|
-
function
|
|
4533
|
-
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|->).";
|
|
4534
4973
|
const parsed = parseTail(args, new Set(["--note", "--note-file"]), usage);
|
|
4535
4974
|
exactPositionals(parsed.positionals, 1, usage);
|
|
4536
4975
|
const note = readCommandText(parsed.options.get("--note"), parsed.options.get("--note-file"), "--note", usage);
|
|
4537
4976
|
const now = clock(options);
|
|
4538
4977
|
const event = store.transaction((tx) => {
|
|
4539
|
-
const run =
|
|
4978
|
+
const run = requireRun(tx, parsed.positionals[0], options);
|
|
4540
4979
|
if (run.status !== "active") {
|
|
4541
|
-
throw usageError(`
|
|
4980
|
+
throw usageError(`AgentRun ${run.id} is already terminal: ${run.status}.`);
|
|
4542
4981
|
}
|
|
4543
4982
|
const task = requireTask(tx, run.taskId);
|
|
4544
4983
|
if (task.status !== "active")
|
|
4545
|
-
throw usageError(inactiveTaskMessage(task, "checkpointing a
|
|
4546
|
-
const pointer =
|
|
4984
|
+
throw usageError(inactiveTaskMessage(task, "checkpointing a AgentRun"));
|
|
4985
|
+
const pointer = activeRunPointer(tx, run);
|
|
4547
4986
|
if (pointer?.id !== run.id) {
|
|
4548
|
-
throw usageError(`
|
|
4987
|
+
throw usageError(`AgentRun is not active for ${task.id}/${run.roleName}: ${run.id}.`);
|
|
4549
4988
|
}
|
|
4550
4989
|
const events = tx.listEvents(task.id);
|
|
4551
|
-
const recovered =
|
|
4552
|
-
const progress = recordTaskEventRecord(tx, task.id,
|
|
4553
|
-
|
|
4990
|
+
const recovered = isRoleRunStalled(events, run.id);
|
|
4991
|
+
const progress = recordTaskEventRecord(tx, task.id, RUN_PROGRESS_EVENT, {
|
|
4992
|
+
runId: run.id,
|
|
4554
4993
|
note: truncateEventNote(note),
|
|
4555
4994
|
...(run.workItemId === undefined ? {} : { workItemId: run.workItemId })
|
|
4556
4995
|
}, now);
|
|
4557
4996
|
if (recovered) {
|
|
4558
|
-
recordTaskEventRecord(tx, task.id,
|
|
4559
|
-
|
|
4997
|
+
recordTaskEventRecord(tx, task.id, RUN_RECOVERED_EVENT, {
|
|
4998
|
+
runId: run.id,
|
|
4560
4999
|
roleName: run.roleName,
|
|
4561
5000
|
progressAt: now.toISOString(),
|
|
4562
5001
|
kind: "checkpoint"
|
|
@@ -4604,12 +5043,12 @@ export function queueReviewRound(store, item, config, requestedBy, now, requeste
|
|
|
4604
5043
|
reviewer = createTaskRole(store, task, roleName, undefined, now, roleName);
|
|
4605
5044
|
store.saveRole(task.id, reviewer);
|
|
4606
5045
|
}
|
|
4607
|
-
if (store.
|
|
5046
|
+
if (store.getActiveRun(item.taskId, reviewer.name) !== null) {
|
|
4608
5047
|
const pending = createPending();
|
|
4609
5048
|
store.saveReviewRound(item.taskId, pending);
|
|
4610
5049
|
const failed = finishReviewRound(pending, "failed", now, {
|
|
4611
5050
|
kind: "dispatch",
|
|
4612
|
-
message: `Reviewer Role already has an active
|
|
5051
|
+
message: `Reviewer Role already has an active AgentRun: ${reviewer.name}.`
|
|
4613
5052
|
});
|
|
4614
5053
|
store.saveReviewRound(item.taskId, failed);
|
|
4615
5054
|
return { round: failed };
|
|
@@ -4749,8 +5188,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4749
5188
|
&& round.reviewerRoleName !== taskFinalContract.reviewerRoleName) {
|
|
4750
5189
|
throw new TaskFinalReviewDispatchDriftError(`Task final-review Reviewer identity does not match ReviewRound ${round.id}.`);
|
|
4751
5190
|
}
|
|
4752
|
-
if (round.status === "pending" && round.
|
|
4753
|
-
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}: `
|
|
4754
5193
|
+ `${round.id}.`);
|
|
4755
5194
|
}
|
|
4756
5195
|
let currentTaskCandidate;
|
|
@@ -4802,8 +5241,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4802
5241
|
&& (requestedReviewers.has(entry.reviewerRoleName)
|
|
4803
5242
|
|| entry.executionGroup?.lanes.some(({ roleName }) => (requestedReviewers.has(roleName))) === true)
|
|
4804
5243
|
&& !(entry.status === "running"
|
|
4805
|
-
&& entry.
|
|
4806
|
-
&& tx.
|
|
5244
|
+
&& entry.reviewerRunId !== undefined
|
|
5245
|
+
&& tx.getRun(task.id, entry.reviewerRunId)?.status === "failed")));
|
|
4807
5246
|
if (conflicting !== undefined) {
|
|
4808
5247
|
throw new TaskFinalReviewDispatchDriftError(`Another active Task-final ReviewRound already exists: ${conflicting.id}/${conflicting.reviewerRoleName}.`);
|
|
4809
5248
|
}
|
|
@@ -4830,8 +5269,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4830
5269
|
}
|
|
4831
5270
|
const candidateLabel = taskScope
|
|
4832
5271
|
? "frozen Task candidate"
|
|
4833
|
-
: candidate.source.type === "
|
|
4834
|
-
? `candidate
|
|
5272
|
+
: candidate.source.type === "run"
|
|
5273
|
+
? `candidate AgentRun ${candidate.source.runId}`
|
|
4835
5274
|
: `revision ${candidate.workItemRevision}`;
|
|
4836
5275
|
const frozenHeads = taskScope
|
|
4837
5276
|
? round.taskCandidate.projects
|
|
@@ -4846,7 +5285,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4846
5285
|
}
|
|
4847
5286
|
const diffByProject = options.deltaRecheckDiff;
|
|
4848
5287
|
if (diffByProject === undefined) {
|
|
4849
|
-
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.`);
|
|
4850
5289
|
}
|
|
4851
5290
|
try {
|
|
4852
5291
|
verifyDeltaRecheckDiff(round.deltaRecheck, diffByProject);
|
|
@@ -4890,17 +5329,17 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4890
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.",
|
|
4891
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.",
|
|
4892
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.",
|
|
4893
|
-
"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.",
|
|
4894
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."
|
|
4895
5334
|
].join("\n");
|
|
4896
|
-
const
|
|
5335
|
+
const createdRuns = [];
|
|
4897
5336
|
if (round.executionGroup === undefined) {
|
|
4898
5337
|
if (round.status !== "pending")
|
|
4899
|
-
return
|
|
4900
|
-
if (tx.
|
|
4901
|
-
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}.`);
|
|
4902
5341
|
}
|
|
4903
|
-
const
|
|
5342
|
+
const runId = tx.nextRunId(taskId);
|
|
4904
5343
|
const effective = resolveEffectiveLaunch({
|
|
4905
5344
|
role: reviewer,
|
|
4906
5345
|
purpose: "review",
|
|
@@ -4908,14 +5347,14 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4908
5347
|
reviewRoundId: round.id,
|
|
4909
5348
|
reviewBaseCommit: round.reviewBaseCommit
|
|
4910
5349
|
});
|
|
4911
|
-
const snapshot =
|
|
5350
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
4912
5351
|
taskId,
|
|
4913
5352
|
roleName: reviewer.name,
|
|
4914
5353
|
purpose: "review",
|
|
4915
5354
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
4916
5355
|
reviewRoundId: round.id
|
|
4917
5356
|
}, now, "controller");
|
|
4918
|
-
const created =
|
|
5357
|
+
const created = createRun(runId, taskId, reviewer.name, roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(taskId, reviewer.name), effective.agentId, effective), createRunInput({
|
|
4919
5358
|
source: {
|
|
4920
5359
|
type: "yui",
|
|
4921
5360
|
channel: item === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
@@ -4930,27 +5369,27 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4930
5369
|
workspace: round.workspace,
|
|
4931
5370
|
effective
|
|
4932
5371
|
});
|
|
4933
|
-
tx.
|
|
5372
|
+
tx.saveRun(created);
|
|
4934
5373
|
tx.saveReviewRound(taskId, startReviewRound(round, created.id));
|
|
4935
|
-
tx.
|
|
4936
|
-
|
|
5374
|
+
tx.saveActiveRun(created);
|
|
5375
|
+
enqueueRoleRunDispatch(tx, {
|
|
4937
5376
|
taskId,
|
|
4938
5377
|
roleName: reviewer.name,
|
|
4939
|
-
|
|
5378
|
+
runId: created.id,
|
|
4940
5379
|
reason: "review-requested",
|
|
4941
5380
|
occurredAt: now
|
|
4942
5381
|
});
|
|
4943
|
-
recordTaskEvent(tx, taskId, "
|
|
4944
|
-
|
|
4945
|
-
return
|
|
5382
|
+
recordTaskEvent(tx, taskId, "run.review-dispatched", runLaunchEventPayload(created), now);
|
|
5383
|
+
createdRuns.push(created);
|
|
5384
|
+
return createdRuns;
|
|
4946
5385
|
}
|
|
4947
5386
|
let runningGroup = round.executionGroup;
|
|
4948
5387
|
const dispatchLanes = runningGroup.lanes.filter((lane) => {
|
|
4949
5388
|
if (lane.disposition !== "open")
|
|
4950
5389
|
return false;
|
|
4951
|
-
if (lane.
|
|
5390
|
+
if (lane.currentRunId === undefined)
|
|
4952
5391
|
return true;
|
|
4953
|
-
return tx.
|
|
5392
|
+
return tx.getRun(taskId, lane.currentRunId)?.status === "failed";
|
|
4954
5393
|
});
|
|
4955
5394
|
const preparedLaneWorkspaces = requireIsolatedReviewLaneWorkspaces(tx, round, dispatchLanes, options.executionLaneWorkspaces);
|
|
4956
5395
|
for (const lane of dispatchLanes) {
|
|
@@ -4958,8 +5397,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4958
5397
|
if (laneReviewer === null) {
|
|
4959
5398
|
throw usageError(`Review Producer Role not found: ${taskId}/${lane.roleName}.`);
|
|
4960
5399
|
}
|
|
4961
|
-
if (tx.
|
|
4962
|
-
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}.`);
|
|
4963
5402
|
}
|
|
4964
5403
|
const laneManagedWorkspace = preparedLaneWorkspaces.get(lane.id);
|
|
4965
5404
|
const effective = lane.effective ?? resolveEffectiveLaunch({
|
|
@@ -4969,8 +5408,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4969
5408
|
reviewRoundId: round.id,
|
|
4970
5409
|
reviewBaseCommit: round.reviewBaseCommit
|
|
4971
5410
|
});
|
|
4972
|
-
const
|
|
4973
|
-
const input =
|
|
5411
|
+
const runId = tx.nextRunId(taskId);
|
|
5412
|
+
const input = createRunInput({
|
|
4974
5413
|
source: {
|
|
4975
5414
|
type: "yui",
|
|
4976
5415
|
channel: item === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
@@ -4992,7 +5431,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4992
5431
|
deltaRefIds: []
|
|
4993
5432
|
});
|
|
4994
5433
|
runningGroup = updateUnifiedExecutionLane(runningGroup, lane.id, {
|
|
4995
|
-
|
|
5434
|
+
currentRunId: runId,
|
|
4996
5435
|
effective,
|
|
4997
5436
|
workspace: {
|
|
4998
5437
|
root: laneManagedWorkspace.root,
|
|
@@ -5001,7 +5440,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5001
5440
|
.map(({ projectId }) => projectId)
|
|
5002
5441
|
}
|
|
5003
5442
|
}, now);
|
|
5004
|
-
|
|
5443
|
+
createdRuns.push(createRun(runId, taskId, laneReviewer.name, roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(taskId, laneReviewer.name), effective.agentId, effective), input, now, {
|
|
5005
5444
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5006
5445
|
purpose: "review",
|
|
5007
5446
|
reviewRoundId: round.id,
|
|
@@ -5023,32 +5462,30 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5023
5462
|
tx.saveManagedWorkspace(prepared);
|
|
5024
5463
|
}
|
|
5025
5464
|
}
|
|
5026
|
-
for (let index = 0; index <
|
|
5027
|
-
const
|
|
5028
|
-
const snapshot =
|
|
5465
|
+
for (let index = 0; index < createdRuns.length; index += 1) {
|
|
5466
|
+
const unboundRun = createdRuns[index];
|
|
5467
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
5029
5468
|
taskId,
|
|
5030
|
-
roleName:
|
|
5469
|
+
roleName: unboundRun.roleName,
|
|
5031
5470
|
purpose: "review",
|
|
5032
5471
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5033
5472
|
reviewRoundId: round.id
|
|
5034
5473
|
}, now, "controller", runningGroup.assignment.contextSnapshotRef);
|
|
5035
|
-
const created =
|
|
5036
|
-
|
|
5037
|
-
const laneReviewer = requireRole(tx, taskId,
|
|
5038
|
-
tx.
|
|
5039
|
-
tx.
|
|
5040
|
-
|
|
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, {
|
|
5041
5480
|
taskId,
|
|
5042
5481
|
roleName: laneReviewer.name,
|
|
5043
|
-
|
|
5482
|
+
runId: created.id,
|
|
5044
5483
|
reason: "review-requested",
|
|
5045
5484
|
occurredAt: now
|
|
5046
5485
|
});
|
|
5047
|
-
recordTaskEvent(tx, taskId, "
|
|
5486
|
+
recordTaskEvent(tx, taskId, "run.review-dispatched", runLaunchEventPayload(created), now);
|
|
5048
5487
|
}
|
|
5049
|
-
|
|
5050
|
-
createdTurns.push(...reconciliation.createdTurns);
|
|
5051
|
-
return createdTurns;
|
|
5488
|
+
return createdRuns;
|
|
5052
5489
|
});
|
|
5053
5490
|
for (const run of runs) {
|
|
5054
5491
|
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
@@ -5098,6 +5535,16 @@ function requireWorkItemCandidate(item) {
|
|
|
5098
5535
|
}
|
|
5099
5536
|
return candidate;
|
|
5100
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
|
+
}
|
|
5101
5548
|
/** ReviewRound ids are the durable Task-local creation order; wall time is not causal. */
|
|
5102
5549
|
function reviewRoundsByIdentity(rounds) {
|
|
5103
5550
|
return [...rounds].sort((left, right) => (left.id.localeCompare(right.id, undefined, { numeric: true })));
|
|
@@ -5110,7 +5557,9 @@ function activeReviewRoundForCandidate(store, item, candidate) {
|
|
|
5110
5557
|
.at(-1);
|
|
5111
5558
|
}
|
|
5112
5559
|
function createTaskRole(store, task, roleName, explicitAgentId, now, sourceGlobalRoleName) {
|
|
5113
|
-
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();
|
|
5114
5563
|
if (explicitAgentId === undefined) {
|
|
5115
5564
|
const sourceRoleName = sourceGlobalRoleName
|
|
5116
5565
|
?? (roleName === LEADER_ROLE ? LEADER_ROLE : "worker");
|
|
@@ -5128,7 +5577,7 @@ function createTaskRole(store, task, roleName, explicitAgentId, now, sourceGloba
|
|
|
5128
5577
|
throw dataError(`No Agent is configured for Task role: ${roleName}.`);
|
|
5129
5578
|
}
|
|
5130
5579
|
const agent = requireAgent(store, agentId);
|
|
5131
|
-
const binding = createRoleAgentBinding(
|
|
5580
|
+
const binding = createRoleAgentBinding(agent);
|
|
5132
5581
|
return createRole(task.id, roleName, [binding], agent.id, workspace, now);
|
|
5133
5582
|
}
|
|
5134
5583
|
function requireAgentProfile(store, id) {
|
|
@@ -5138,7 +5587,9 @@ function requireAgentProfile(store, id) {
|
|
|
5138
5587
|
return profile;
|
|
5139
5588
|
}
|
|
5140
5589
|
function createTaskRoleFromAgentBinding(store, task, roleName, binding, now) {
|
|
5141
|
-
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();
|
|
5142
5593
|
return createRole(task.id, roleName, [binding], binding.agentId, workspace, now);
|
|
5143
5594
|
}
|
|
5144
5595
|
function resolvedAgentProfileRuntime(profile, store) {
|
|
@@ -5166,7 +5617,7 @@ function resolveTaskRoleAgentBindingAdd(parsed, profile, store) {
|
|
|
5166
5617
|
if (explicitAgentId === undefined)
|
|
5167
5618
|
return undefined;
|
|
5168
5619
|
const agent = requireAgent(store, explicitAgentId);
|
|
5169
|
-
binding = createRoleAgentBinding(
|
|
5620
|
+
binding = createRoleAgentBinding(agent);
|
|
5170
5621
|
}
|
|
5171
5622
|
if (hasAgentConfigOptions(parsed)) {
|
|
5172
5623
|
binding = patchRoleAgentBinding(binding, parsed);
|
|
@@ -5177,11 +5628,6 @@ function resolveTaskRoleAgentBindingUpdate(parsed, role, profile, store) {
|
|
|
5177
5628
|
const changesAgentConfig = hasAgentConfigOptions(parsed);
|
|
5178
5629
|
const explicitAgentId = parsed.one("--agent")?.trim();
|
|
5179
5630
|
const targetAgentId = explicitAgentId || role.activeAgentId;
|
|
5180
|
-
if (profile?.runtime.source === "global-worker"
|
|
5181
|
-
&& explicitAgentId === undefined
|
|
5182
|
-
&& !changesAgentConfig) {
|
|
5183
|
-
return undefined;
|
|
5184
|
-
}
|
|
5185
5631
|
let binding;
|
|
5186
5632
|
if (profile !== undefined) {
|
|
5187
5633
|
const profileRuntime = resolvedAgentProfileRuntime(profile, store);
|
|
@@ -5200,7 +5646,7 @@ function resolveTaskRoleAgentBindingUpdate(parsed, role, profile, store) {
|
|
|
5200
5646
|
return undefined;
|
|
5201
5647
|
const agent = requireAgent(store, targetAgentId);
|
|
5202
5648
|
binding = role.agentBindings[targetAgentId]
|
|
5203
|
-
?? createRoleAgentBinding(
|
|
5649
|
+
?? createRoleAgentBinding(agent);
|
|
5204
5650
|
}
|
|
5205
5651
|
if (changesAgentConfig) {
|
|
5206
5652
|
binding = patchRoleAgentBinding(binding, parsed);
|
|
@@ -5237,7 +5683,7 @@ function appendMessage(store, taskId, body, kind, author, now, context = {}) {
|
|
|
5237
5683
|
recordTaskEvent(store, taskId, "message.sent", {
|
|
5238
5684
|
messageId: message.id,
|
|
5239
5685
|
kind: message.kind,
|
|
5240
|
-
...(message.
|
|
5686
|
+
...(message.runId === undefined ? {} : { runId: message.runId })
|
|
5241
5687
|
}, now);
|
|
5242
5688
|
return message;
|
|
5243
5689
|
}
|
|
@@ -5245,43 +5691,29 @@ function recordTaskEvent(store, taskId, type, payload, now) {
|
|
|
5245
5691
|
return recordTaskEventRecord(store, taskId, type, payload, now);
|
|
5246
5692
|
}
|
|
5247
5693
|
function leaderActionEventPayload(store, taskId, options) {
|
|
5248
|
-
const
|
|
5249
|
-
|
|
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 };
|
|
5250
5698
|
}
|
|
5251
5699
|
function recordTaskEventRecord(store, taskId, type, payload, now) {
|
|
5252
5700
|
const event = createTaskEvent(store.nextEventId(taskId), taskId, type, payload, now);
|
|
5253
5701
|
store.saveEvent(taskId, event);
|
|
5254
5702
|
return event;
|
|
5255
5703
|
}
|
|
5256
|
-
/** Keeps a free-text
|
|
5704
|
+
/** Keeps a free-text AgentRun-fact note bounded so an event payload stays compact. */
|
|
5257
5705
|
function truncateEventNote(note) {
|
|
5258
5706
|
const normalized = note.trim();
|
|
5259
5707
|
return normalized.length <= 280 ? normalized : `${normalized.slice(0, 279)}…`;
|
|
5260
5708
|
}
|
|
5261
|
-
function
|
|
5262
|
-
const effective = sessions?.sessions[sessions.activeAgentId]?.effective;
|
|
5263
|
-
return {
|
|
5264
|
-
desiredRevision: String(role.launchRevision),
|
|
5265
|
-
defaultAccess: role.defaultAccess,
|
|
5266
|
-
effectiveRevision: effective === undefined
|
|
5267
|
-
? "none"
|
|
5268
|
-
: String(effective.sourceDesiredRevision),
|
|
5269
|
-
profileAccess: effective?.profileAccess ?? "none",
|
|
5270
|
-
effectivePermission: effective?.permission.strategy ?? "none",
|
|
5271
|
-
desiredDrift: effective === undefined
|
|
5272
|
-
? "not-started"
|
|
5273
|
-
: effective.sourceDesiredRevision === role.launchRevision
|
|
5274
|
-
? "none"
|
|
5275
|
-
: "pending-next-launch"
|
|
5276
|
-
};
|
|
5277
|
-
}
|
|
5278
|
-
function turnLaunchEventPayload(run) {
|
|
5709
|
+
function runLaunchEventPayload(run) {
|
|
5279
5710
|
return {
|
|
5280
|
-
|
|
5711
|
+
runId: run.id,
|
|
5281
5712
|
role: run.roleName,
|
|
5282
5713
|
purpose: run.purpose,
|
|
5283
5714
|
mode: run.mode,
|
|
5284
5715
|
agent: `${run.effective.agentId}/${run.effective.adapterId}`,
|
|
5716
|
+
component: run.effective.component,
|
|
5285
5717
|
effectiveRevision: String(run.effective.sourceDesiredRevision),
|
|
5286
5718
|
profileAccess: run.effective.profileAccess,
|
|
5287
5719
|
effectivePermission: run.effective.permission.strategy,
|
|
@@ -5338,18 +5770,18 @@ function requireWorkItem(store, workItemId, options) {
|
|
|
5338
5770
|
}
|
|
5339
5771
|
return item;
|
|
5340
5772
|
}
|
|
5341
|
-
function
|
|
5342
|
-
const reference = taskRecordReference(
|
|
5343
|
-
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);
|
|
5344
5776
|
if (run === null) {
|
|
5345
|
-
throw usageError(`
|
|
5777
|
+
throw usageError(`AgentRun not found: ${reference.taskId}/${reference.localId}.`);
|
|
5346
5778
|
}
|
|
5347
5779
|
return run;
|
|
5348
5780
|
}
|
|
5349
|
-
function
|
|
5781
|
+
function activeRunPointer(store, run) {
|
|
5350
5782
|
return run.executionGroupId !== undefined && run.executionLaneId !== undefined
|
|
5351
|
-
? store.
|
|
5352
|
-
: store.
|
|
5783
|
+
? store.getActiveExecutionLaneRun(run.taskId, run.executionGroupId, run.executionLaneId)
|
|
5784
|
+
: store.getActiveRun(run.taskId, run.roleName);
|
|
5353
5785
|
}
|
|
5354
5786
|
function requireReviewRound(store, reviewRoundId, options) {
|
|
5355
5787
|
const reference = taskRecordReference(reviewRoundId, "reviewRound", "ReviewRound reference", options);
|
|
@@ -5384,12 +5816,12 @@ export function assertWorkItemDependenciesCompletedForCommand(store, item) {
|
|
|
5384
5816
|
throw error;
|
|
5385
5817
|
}
|
|
5386
5818
|
}
|
|
5387
|
-
function
|
|
5388
|
-
return [...
|
|
5819
|
+
function chronologicalRuns(runs) {
|
|
5820
|
+
return [...runs].sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
5389
5821
|
|| left.id.localeCompare(right.id)));
|
|
5390
5822
|
}
|
|
5391
5823
|
function isTerminalWorkItemStatus(status) {
|
|
5392
|
-
return ["
|
|
5824
|
+
return ["accepted", "retired"].includes(status);
|
|
5393
5825
|
}
|
|
5394
5826
|
function assertTaskOpen(task) {
|
|
5395
5827
|
if (task.status === "completed") {
|
|
@@ -5397,7 +5829,7 @@ function assertTaskOpen(task) {
|
|
|
5397
5829
|
}
|
|
5398
5830
|
if (task.status === "archived")
|
|
5399
5831
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
5400
|
-
if (task.status === "
|
|
5832
|
+
if (task.status === "cancelled")
|
|
5401
5833
|
throw usageError(`Task is retired: ${task.id}.`);
|
|
5402
5834
|
}
|
|
5403
5835
|
function assertTaskExecutionEnabled(task, action) {
|
|
@@ -5415,7 +5847,7 @@ function inactiveTaskMessage(task, action) {
|
|
|
5415
5847
|
if (task.status === "completed") {
|
|
5416
5848
|
return `Task ${task.id} is completed; reopen it before ${action}.`;
|
|
5417
5849
|
}
|
|
5418
|
-
if (task.status === "
|
|
5850
|
+
if (task.status === "cancelled")
|
|
5419
5851
|
return `Task ${task.id} is retired; it cannot resume ${action}.`;
|
|
5420
5852
|
return `Task is archived: ${task.id}.`;
|
|
5421
5853
|
}
|
|
@@ -5436,10 +5868,6 @@ function parseWorkStatus(value) {
|
|
|
5436
5868
|
throw usageError(`Invalid work item status: ${value}.`);
|
|
5437
5869
|
}
|
|
5438
5870
|
function presentWorkStatus(status) {
|
|
5439
|
-
if (status === "pending")
|
|
5440
|
-
return "todo";
|
|
5441
|
-
if (status === "completed")
|
|
5442
|
-
return "done";
|
|
5443
5871
|
return status;
|
|
5444
5872
|
}
|
|
5445
5873
|
function parseWorkCreateArgs(args, usage) {
|
|
@@ -5614,7 +6042,10 @@ function taskBriefCommand(args, store, options) {
|
|
|
5614
6042
|
...(hasSummary ? { leaderSummary: parsed.options.get("--leader-summary") } : {})
|
|
5615
6043
|
}, updatedBy, now);
|
|
5616
6044
|
tx.saveTaskBrief(task.id, brief);
|
|
5617
|
-
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);
|
|
5618
6049
|
enqueueWork(tx, taskMailbox(task.id), "brief-updated", now, [taskRef(task.id)]);
|
|
5619
6050
|
if (task.status === "active" && updatedBy !== "leader") {
|
|
5620
6051
|
enqueueWork(tx, leaderMailbox(task.id), "brief-updated", now, [taskRef(task.id)]);
|
|
@@ -5625,12 +6056,18 @@ function taskBriefCommand(args, store, options) {
|
|
|
5625
6056
|
if (result.task.status === "active") {
|
|
5626
6057
|
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
5627
6058
|
}
|
|
5628
|
-
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 });
|
|
5629
6060
|
}
|
|
5630
6061
|
throw usageError(command === undefined
|
|
5631
6062
|
? "Task brief command is required."
|
|
5632
6063
|
: `Unknown command: task brief ${command}`);
|
|
5633
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
|
+
}
|
|
5634
6071
|
function taskDecisionCommand(args, store, options) {
|
|
5635
6072
|
const [command, ...rest] = args;
|
|
5636
6073
|
if (command === "record") {
|
|
@@ -5881,14 +6318,11 @@ function taskContinuationCommand(args, store) {
|
|
|
5881
6318
|
const report = [...continuation.reports].reverse()[0];
|
|
5882
6319
|
const reportEvent = report === undefined
|
|
5883
6320
|
? undefined
|
|
5884
|
-
: reportEvents.find((entry) => (entry.continuationId === identity.continuationId
|
|
5885
|
-
&& entry.continuationGeneration === identity.generation
|
|
5886
|
-
&& entry.reportId === report.reportId));
|
|
6321
|
+
: reportEvents.find((entry) => (entry.continuationId === identity.continuationId && entry.reportId === report.reportId));
|
|
5887
6322
|
return Object.freeze({
|
|
5888
6323
|
continuationId: identity.continuationId,
|
|
5889
|
-
generation: identity.generation,
|
|
5890
6324
|
driver: identity.providerNamespace,
|
|
5891
|
-
|
|
6325
|
+
runId: continuation.runId,
|
|
5892
6326
|
execution: continuation.execution,
|
|
5893
6327
|
outcome: continuation.outcome,
|
|
5894
6328
|
attachment: continuation.attachment,
|
|
@@ -5934,12 +6368,9 @@ function continuationReportEvents(events) {
|
|
|
5934
6368
|
const observation = runtimeObservationFromTaskEvent(event);
|
|
5935
6369
|
if (observation !== null && observation.kind === "continuation.reported") {
|
|
5936
6370
|
const continuationId = observation.fence.continuationId;
|
|
5937
|
-
const continuationGeneration = observation.fence.continuationGeneration;
|
|
5938
6371
|
const reportId = observation.payload?.reportId;
|
|
5939
|
-
if (continuationId !== undefined
|
|
5940
|
-
|
|
5941
|
-
&& reportId !== undefined) {
|
|
5942
|
-
result.push({ event, continuationId, continuationGeneration, reportId });
|
|
6372
|
+
if (continuationId !== undefined && reportId !== undefined) {
|
|
6373
|
+
result.push({ event, continuationId, reportId });
|
|
5943
6374
|
}
|
|
5944
6375
|
}
|
|
5945
6376
|
}
|
|
@@ -5953,6 +6384,48 @@ function continuationReportEvents(events) {
|
|
|
5953
6384
|
*/
|
|
5954
6385
|
function taskWakeDispatch(args, store, options) {
|
|
5955
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
|
+
}
|
|
5956
6429
|
if (subcommand === "list" || subcommand === "show") {
|
|
5957
6430
|
return taskWakeInspectionCommand(args, store);
|
|
5958
6431
|
}
|
|
@@ -5979,13 +6452,13 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
5979
6452
|
{ header: "Wake", minWidth: 8, maxWidth: 18 },
|
|
5980
6453
|
{ header: "Status", minWidth: 8, maxWidth: 12 },
|
|
5981
6454
|
{ header: "Reasons", minWidth: 10, maxWidth: 40 },
|
|
5982
|
-
{ header: "
|
|
6455
|
+
{ header: "AgentRun", minWidth: 10, maxWidth: 20 },
|
|
5983
6456
|
{ header: "Dispatched", minWidth: 10, maxWidth: 28 }
|
|
5984
6457
|
], wakes.map((wake) => [
|
|
5985
6458
|
wake.id,
|
|
5986
6459
|
wake.status,
|
|
5987
6460
|
wake.reasons.map(renderWakeReason).join(", "),
|
|
5988
|
-
wake.
|
|
6461
|
+
wake.runId ?? "-",
|
|
5989
6462
|
presentTime(wake.createdAt, timeZone)
|
|
5990
6463
|
]), defaultTableWidth())}\n`, { taskId: task.id, wakes });
|
|
5991
6464
|
}
|
|
@@ -6004,18 +6477,30 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
6004
6477
|
return ms > fromMs && ms <= toMs;
|
|
6005
6478
|
};
|
|
6006
6479
|
const allEvents = store.listEvents(task.id);
|
|
6007
|
-
const
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
const
|
|
6011
|
-
|
|
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)));
|
|
6012
6496
|
const lines = [
|
|
6013
6497
|
`Wake: ${wake.id}`,
|
|
6014
6498
|
`Task: ${task.id}`,
|
|
6015
6499
|
`Status: ${wake.status}`,
|
|
6500
|
+
`Notification: ${deliveryEvents.at(-1)?.payload.outcome ?? "unobserved"}`,
|
|
6016
6501
|
`Reasons: ${wake.reasons.map(renderWakeReason).join(", ")}`,
|
|
6017
6502
|
`Delta window: ${wake.fromCursor} → ${wake.toCursor}`,
|
|
6018
|
-
...(wake.
|
|
6503
|
+
...(wake.runId === undefined ? [] : [`AgentRun: ${wake.runId}`]),
|
|
6019
6504
|
`Dispatched: ${presentTime(wake.createdAt, timeZone)}`,
|
|
6020
6505
|
...(wake.consumedAt === undefined
|
|
6021
6506
|
? []
|
|
@@ -6024,19 +6509,20 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
6024
6509
|
...events.map((e) => ` ${e.id} ${e.type} ${presentTime(e.createdAt, timeZone)}`),
|
|
6025
6510
|
`Messages (${messages.length}):`,
|
|
6026
6511
|
...messages.map((m) => ` ${m.id} [${taskMessageAuthorLabel(m.author)}] ${presentTime(m.createdAt, timeZone)}`),
|
|
6027
|
-
`
|
|
6028
|
-
...
|
|
6029
|
-
+ `${presentTime(
|
|
6030
|
-
+ `${
|
|
6031
|
-
? ` → 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}`
|
|
6032
6517
|
: ""}`))
|
|
6033
6518
|
];
|
|
6034
6519
|
return output(lines.join("\n").concat("\n"), {
|
|
6035
6520
|
taskId: task.id,
|
|
6521
|
+
deliveryEvents,
|
|
6036
6522
|
wake,
|
|
6037
6523
|
events,
|
|
6038
6524
|
messages,
|
|
6039
|
-
|
|
6525
|
+
runs
|
|
6040
6526
|
});
|
|
6041
6527
|
}
|
|
6042
6528
|
throw usageError(command === undefined
|
|
@@ -6167,8 +6653,8 @@ function leaderMailbox(taskId) {
|
|
|
6167
6653
|
function taskRef(id) {
|
|
6168
6654
|
return { type: "task", id };
|
|
6169
6655
|
}
|
|
6170
|
-
function
|
|
6171
|
-
return { type: "
|
|
6656
|
+
function runRef(taskId, id) {
|
|
6657
|
+
return { type: "run", taskId, id };
|
|
6172
6658
|
}
|
|
6173
6659
|
function workItemRef(taskId, id) {
|
|
6174
6660
|
return { type: "work-item", taskId, id };
|