@zq-silk/yui 0.15.7 → 0.15.9
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 +194 -399
- package/ARCHITECTURE.zh-CN.md +151 -0
- package/README.md +306 -1131
- 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/artifacts/artifactCapability.js +74 -0
- package/dist/artifacts/artifactCommitLock.js +249 -0
- package/dist/artifacts/artifactPaths.js +151 -0
- package/dist/artifacts/gitArtifactRef.js +146 -0
- package/dist/artifacts/managedGit.js +332 -0
- package/dist/artifacts/taskArtifactRepository.js +277 -0
- package/dist/brief/taskBrief.js +12 -0
- package/dist/cli/agentConfigurationPicker.js +13 -0
- package/dist/cli/commandCatalog.js +165 -74
- 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 +245 -95
- package/dist/commands/agentCommands.js +72 -14
- package/dist/commands/capabilityCommands.js +9 -6
- package/dist/commands/configCommands.js +20 -20
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +24 -24
- package/dist/commands/globalRoleCommands.js +1 -1
- package/dist/commands/grantCommands.js +4 -4
- package/dist/commands/operatorCommands.js +34 -9
- 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 +281 -0
- package/dist/commands/taskActor.js +28 -49
- package/dist/commands/taskCommands.js +1461 -720
- 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} +158 -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 +481 -0
- package/dist/context/wakeNotification.js +27 -27
- package/dist/controller/agentRuntimeObserver.js +21 -24
- package/dist/controller/capabilityBridge.js +17 -6
- package/dist/controller/clientRuntime.js +65 -92
- package/dist/controller/controller.js +120 -188
- package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
- package/dist/controller/jobControl.js +54 -85
- package/dist/controller/resourceInventory.js +8 -27
- package/dist/controller/resourceInventoryLinux.js +12 -13
- package/dist/controller/runtime.js +529 -479
- package/dist/controller/runtimeEventInbox.js +55 -25
- package/dist/controller/runtimeEventProcessor.js +22 -31
- package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
- package/dist/controller/runtimeLaunchCoordinator.js +80 -426
- package/dist/controller/runtimeObservationHook.js +14 -18
- package/dist/controller/sessionNotify.js +16 -24
- package/dist/controller/sessionOwnerReconciliation.js +168 -50
- package/dist/controller/structuredProviderObservation.js +138 -99
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/coordination/workMailboxQueue.js +36 -33
- package/dist/core/boundedRpc.js +8 -1
- package/dist/core/controllerClient.js +20 -1
- package/dist/core/controllerServer.js +4 -4
- package/dist/doctor/doctor.js +13 -2
- package/dist/domain/agentResultTransport.js +9 -9
- package/dist/execution/codexThreadNaming.js +2 -8
- package/dist/execution/executionHealth.js +51 -63
- package/dist/execution/reviewMainRun.js +137 -0
- package/dist/execution/workItemExecution.js +28 -29
- package/dist/execution/workItemExecutionProjection.js +99 -107
- package/dist/execution/workItemMainRun.js +141 -0
- package/dist/executor/agentAdapter.js +227 -20
- package/dist/executor/agentConfigurationCatalog.js +126 -4
- package/dist/executor/agentConfigurationProbe.js +162 -4
- package/dist/executor/agentExecutor.js +79 -78
- package/dist/executor/effectiveLaunch.js +105 -18
- package/dist/executor/executorRegistry.js +29 -44
- package/dist/executor/fileRoleLaunchPlanner.js +229 -154
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +6 -3
- package/dist/input/inputRequest.js +12 -10
- package/dist/integration/gitIntegrationService.js +4 -11
- package/dist/integration/integrationQueueService.js +4 -4
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/kernel/builtinCapabilities.js +255 -12
- package/dist/kernel/capabilityRegistry.js +64 -18
- package/dist/kernel/instanceHost.js +12 -1
- package/dist/kernel/kernelPorts.js +2 -2
- package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
- package/dist/lifecycle/exactRunTerminalization.js +449 -0
- package/dist/message/message.js +118 -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 +388 -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 +75 -0
- package/dist/resources/projectResourceService.js +343 -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 +3 -7
- package/dist/storage/migrations/agentRunContract.js +159 -0
- package/dist/storage/migrations/artifactsToGit.js +338 -0
- package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
- package/dist/storage/migrations/submitIntent.js +126 -0
- package/dist/storage/sqliteSchema.js +467 -7
- package/dist/storage/sqliteStore.js +355 -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 +327 -0
- package/dist/task/taskActivationService.js +408 -0
- package/dist/task/taskRecordReference.js +5 -4
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/task/taskSubmission.js +236 -0
- 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 +121 -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 +442 -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 +133 -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 +84 -48
- package/dist/workspace/workItemChangeSetManager.js +16 -9
- package/docs/agent-result-consumption.md +96 -0
- package/docs/agent-result-consumption.zh-CN.md +81 -0
- package/docs/agent-runtime-drivers.md +93 -0
- package/docs/agent-runtime-drivers.zh-CN.md +77 -0
- package/docs/architecture/README.md +50 -0
- package/docs/architecture/README.zh-CN.md +43 -0
- package/docs/architecture/capabilities-and-resources.md +118 -0
- package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
- package/docs/managed-turn-and-session-runtime.md +224 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
- package/docs/observability/README.md +83 -0
- package/docs/observability/README.zh-CN.md +71 -0
- package/docs/plugin-sdk.md +393 -0
- package/docs/plugin-sdk.zh-CN.md +293 -0
- package/docs/provider-runtime.md +165 -0
- package/docs/provider-runtime.zh-CN.md +132 -0
- package/docs/release-workflow.md +305 -0
- package/docs/release-workflow.zh-CN.md +237 -0
- package/docs/roles-and-configuration.md +115 -0
- package/docs/roles-and-configuration.zh-CN.md +96 -0
- package/docs/sqlite-control-plane-design.md +78 -0
- package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
- package/docs/task-dag-semantics.md +80 -0
- package/docs/task-dag-semantics.zh-CN.md +59 -0
- package/docs/task-delivery.md +105 -0
- package/docs/task-delivery.zh-CN.md +82 -0
- package/docs/task-local-identity.md +8 -6
- package/docs/task-local-identity.zh-CN.md +58 -0
- package/docs/testing/verification-levels.md +88 -0
- package/docs/testing/verification-levels.zh-CN.md +69 -0
- package/i18n/README.zh-CN.md +270 -722
- package/package.json +3 -2
- package/skills/yui-leader/SKILL.md +88 -304
- package/skills/yui-leader/references/execution.md +303 -0
- package/skills/yui-leader/references/integration.md +39 -0
- package/skills/yui-leader/references/planning.md +109 -0
- package/skills/yui-leader/references/replicated-execution.md +42 -0
- package/skills/yui-leader/references/task-plugins.md +37 -0
- package/skills/yui-operator/SKILL.md +46 -62
- 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,33 @@
|
|
|
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, withSubmissionReceipt, TASK_SUBMISSION_INTENTS } from "../message/message.js";
|
|
20
24
|
import { assertDraftTaskExecutionFree, validateDraftWorkItemEdit } from "../task/draftPlan.js";
|
|
25
|
+
import { TASK_PLANNING_ENTERED_EVENT, decideSubmissionRouting, describeSubmissionFeedback, draftActivationState, draftHasEnteredPlanning, normalizeSubmissionIntent, sameSubmissionTarget } from "../task/taskSubmission.js";
|
|
26
|
+
import { recordTaskActivationRequestInTransaction, taskActivationOperationRef } from "../task/taskActivationService.js";
|
|
21
27
|
import { cancelInputRequest } from "../input/inputRequest.js";
|
|
22
|
-
import {
|
|
28
|
+
import { retireExactActiveRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
|
|
23
29
|
import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole } from "../role/role.js";
|
|
24
|
-
import {
|
|
30
|
+
import { createRun, runPurposeAdmitsTaskState, runExecutionObservation, withRunContextSnapshot } from "../agentRun/agentRun.js";
|
|
25
31
|
import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, finishReviewRound, recordReviewWorkspaceDisposition, retryReviewRound, retryRunningReviewExecutionLane, retryTaskReviewRound, startReplicatedReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
|
|
26
32
|
import { buildDeltaRecheckDispatchContext, verifyDeltaRecheckDiff } from "../review/deltaRecheck.js";
|
|
27
33
|
import { isCompletedTaskReviewEvidence } from "../review/reviewAcceptance.js";
|
|
@@ -30,38 +36,45 @@ import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
|
|
|
30
36
|
import { createDecision, supersedeDecision } from "../decision/decision.js";
|
|
31
37
|
import { createMilestone } from "../milestone/milestone.js";
|
|
32
38
|
import { runPublicationCommand } from "./taskPublicationCommands.js";
|
|
39
|
+
import { runTaskActivationCommand, nonLeaderActivationIdentity } from "./taskActivationCommands.js";
|
|
33
40
|
import { assertTaskRemoteDeliveryProof, projectTaskRemoteDeliveryFromStore, renderTaskRemoteDelivery, runTaskRemoteDeliveryCommand } from "./taskRemoteDeliveryCommand.js";
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
41
|
+
import { enqueueRoleRunDispatch, enqueueWork, settleExactWorkExecution } from "../coordination/workMailboxQueue.js";
|
|
42
|
+
import { completeProcessing } from "../coordination/workMailbox.js";
|
|
43
|
+
import { runtimeLifecycleTarget, RUNTIME_SESSION_REPLACE_REQUIRED_REASON } from "../runtime/lifecycleReservation.js";
|
|
37
44
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
38
45
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
39
|
-
import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
|
|
46
|
+
import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, taskOwnsManagedWorkspace, updateTaskMetadata } from "../task/task.js";
|
|
40
47
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
41
48
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
49
|
+
import { StorageConflictError } from "../storage/taskStore.js";
|
|
42
50
|
import { requireResolvedAgentProfileRuntime } from "../profile/agentProfileRuntime.js";
|
|
43
51
|
import { assertProjectActive, resolveProject } from "../repository/project.js";
|
|
44
|
-
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem,
|
|
52
|
+
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, editWorkItemDefinition, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, prepareWorkItemDispatch, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
45
53
|
import { assertWorkItemDependenciesCompleted as assertWorkItemDependencyGate, WorkItemDependencyGateError } from "../workItem/dependencyGate.js";
|
|
46
|
-
import { createExecutionGroup, createReviewExecutionAssignment, createWorkItemExecutionAssignment, createWorkItemExecutionGroup,
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
54
|
+
import { createExecutionGroup, createReviewExecutionAssignment, createWorkItemExecutionAssignment, createWorkItemExecutionGroup, updateExecutionLane as updateUnifiedExecutionLane, updateWorkItemExecutionLane, workItemExecutionGroupSettled } from "../execution/workItemExecution.js";
|
|
55
|
+
import { dispatchWorkItemSynthesis, selectedWorkItemSynthesisProducers } from "../execution/workItemMainRun.js";
|
|
56
|
+
import { dispatchReviewSynthesis, selectedReviewSynthesisProducers } from "../execution/reviewMainRun.js";
|
|
57
|
+
import { synthesisSourceRunIds } from "../context/runContextPack.js";
|
|
49
58
|
import { projectWorkItemExecution } from "../execution/workItemExecutionProjection.js";
|
|
50
59
|
import { sameTaskFinalReviewContract, taskFinalReviewConfig, validateTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
|
|
51
60
|
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractResolution.js";
|
|
52
61
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
53
62
|
import { hasAgentConfigOptions, hasNoRoleMutation, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfilePatch } from "./roleConfiguration.js";
|
|
54
63
|
import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
|
|
55
|
-
import {
|
|
64
|
+
import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
|
|
56
65
|
import { runTaskContextCommand } from "./taskContextCommand.js";
|
|
66
|
+
import { listContextMessages } from "../context/taskContext.js";
|
|
67
|
+
import { createProjectResources } from "../resources/projectResourceService.js";
|
|
68
|
+
import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArtifactRef.js";
|
|
57
69
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
58
70
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
59
|
-
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel,
|
|
71
|
+
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
60
72
|
import { assertNoOpenInputRequests, openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
61
73
|
import { runGrantCommand } from "./grantCommands.js";
|
|
62
74
|
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
63
|
-
import { taskLocalActor as resolveTaskLocalActor,
|
|
64
|
-
import { currentManagedRuntime } from "../runtime/managedCaller.js";
|
|
75
|
+
import { taskLocalActor as resolveTaskLocalActor, assertTaskDeliveryAuthority } from "./taskActor.js";
|
|
76
|
+
import { currentManagedRuntime, resolveManagedTaskReader } from "../runtime/managedCaller.js";
|
|
77
|
+
import { resolveMessageRecipient, messageContinuationBlocker } from "../message/messageContinuation.js";
|
|
65
78
|
import { enqueueOperatorEvent } from "../scheduler/operatorEvent.js";
|
|
66
79
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
67
80
|
import { renderWakeReason, wakeReason } from "../scheduler/wakeReason.js";
|
|
@@ -181,8 +194,11 @@ export function previewTaskRoleAgentConfigurationMutation(args, store) {
|
|
|
181
194
|
}
|
|
182
195
|
const parsed = parseRoleOptions(tail, new Map([
|
|
183
196
|
...roleOptionSpecs({ update: true, includeAgent: true }),
|
|
184
|
-
["--profile", "value"]
|
|
197
|
+
["--profile", "value"],
|
|
198
|
+
["--environment", "value"],
|
|
199
|
+
["--managed-environment", "flag"]
|
|
185
200
|
]), usage);
|
|
201
|
+
validateTaskRoleEnvironmentOptions(parsed, usage);
|
|
186
202
|
if (parsed.has("--agent") && (parsed.one("--agent")?.trim().length ?? 0) === 0) {
|
|
187
203
|
throw usageError("--agent is required.", usage);
|
|
188
204
|
}
|
|
@@ -201,7 +217,7 @@ export function previewTaskRoleAgentConfigurationMutation(args, store) {
|
|
|
201
217
|
}
|
|
202
218
|
export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
203
219
|
const usage = "Task complete usage: yui task complete <id> (--summary <text>|--summary-file <path|->) [--refresh-remote] [--accept-published-tree <publication-id>].";
|
|
204
|
-
const parsed =
|
|
220
|
+
const parsed = parseMultiValueTail(args, new Set(["--summary", "--summary-file", "--accept-published-tree"]), new Set(["--artifact-ref"]), usage, new Set(["--refresh-remote"]));
|
|
205
221
|
exactPositionals(parsed.positionals, 1, usage);
|
|
206
222
|
const inlineSummary = parsed.options.get("--summary");
|
|
207
223
|
const summaryFile = parsed.options.get("--summary-file");
|
|
@@ -213,6 +229,7 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
|
213
229
|
return {
|
|
214
230
|
taskId: parsed.positionals[0],
|
|
215
231
|
summary,
|
|
232
|
+
artifactRefs: parsed.multiOptions.get("--artifact-ref") ?? [],
|
|
216
233
|
...(acceptedPublishedTreePublicationId === undefined
|
|
217
234
|
? {}
|
|
218
235
|
: { acceptedPublishedTreePublicationId })
|
|
@@ -233,6 +250,7 @@ export function parseTaskCompletionRequest(args, summaryOverride) {
|
|
|
233
250
|
export function preflightTaskCompletion(taskId, store, options = {}, request = {}) {
|
|
234
251
|
const task = requireTask(store, taskId);
|
|
235
252
|
const actor = taskActor(store, options, task.id);
|
|
253
|
+
assertTaskDeliveryAuthority(store, options.environment, task.id);
|
|
236
254
|
if (task.status === "completed") {
|
|
237
255
|
return { task, actor, completed: true, activeTaskReview: false };
|
|
238
256
|
}
|
|
@@ -279,19 +297,29 @@ function formatCompletionBlockers(taskId, blockers) {
|
|
|
279
297
|
}
|
|
280
298
|
export function runTaskCommand(args, store, options = {}) {
|
|
281
299
|
const [command, ...rest] = args;
|
|
300
|
+
const delivery = command === "complete"
|
|
301
|
+
|| (command === "work" && ["dispatch", "synthesize", "review", "accept"].includes(rest[0] ?? ""))
|
|
302
|
+
|| (command === "review" && !["list", "show"].includes(rest[0] ?? ""))
|
|
303
|
+
|| ((command === "run" || command === "run") && rest[0] === "retry");
|
|
304
|
+
if (delivery && options.environment?.YUI_SESSION_SCOPE === "task"
|
|
305
|
+
&& options.environment.YUI_TASK_ID !== undefined) {
|
|
306
|
+
assertTaskDeliveryAuthority(store, options.environment, options.environment.YUI_TASK_ID);
|
|
307
|
+
}
|
|
282
308
|
switch (command) {
|
|
283
309
|
case "create": return createTaskCommand(rest, store, options);
|
|
284
310
|
case "update": return output(updateTaskCommand(rest, store, options));
|
|
285
311
|
case "list": return listTaskCommand(rest, store);
|
|
286
312
|
case "show": return showTaskCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
287
|
-
case "context": return runTaskContextCommand(rest, store, options.
|
|
313
|
+
case "context": return runTaskContextCommand(rest, store, options.environment);
|
|
288
314
|
case "next-action": return runTaskNextActionCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
289
315
|
case "remote-delivery": return runTaskRemoteDeliveryCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
290
316
|
case "activate": return output(activateTaskCommand(rest, store, options));
|
|
317
|
+
case "activation": return runTaskActivationCommand(rest, store, options);
|
|
291
318
|
case "complete": return completeTaskCommand(rest, store, options);
|
|
292
319
|
case "reopen": return output(reopenTaskCommand(rest, store, options));
|
|
293
320
|
case "archive": return output(archiveTaskCommand(rest, store, options));
|
|
294
321
|
case "retire": return retireTaskCommand(rest, store, options);
|
|
322
|
+
case "cancel": return cancelTaskCommand(rest, store, options);
|
|
295
323
|
case "reconcile": return output(reconcileTaskCommand(rest, store, options));
|
|
296
324
|
case "message": {
|
|
297
325
|
const execution = taskMessageCommand(rest, store, options);
|
|
@@ -306,7 +334,8 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
306
334
|
case "role": return taskRoleCommand(rest, store, options);
|
|
307
335
|
case "work": return taskWorkCommand(rest, store, options);
|
|
308
336
|
case "review": return taskReviewCommand(rest, store, options);
|
|
309
|
-
case "
|
|
337
|
+
case "run":
|
|
338
|
+
case "run": return taskRunCommand(rest, store, options);
|
|
310
339
|
case "brief": return taskBriefCommand(rest, store, options);
|
|
311
340
|
case "decision": return taskDecisionCommand(rest, store, options);
|
|
312
341
|
case "milestone": return taskMilestoneCommand(rest, store, options);
|
|
@@ -356,21 +385,21 @@ function workItemCandidateProducerRoles(store, item, candidate) {
|
|
|
356
385
|
roles.add(LEADER_ROLE);
|
|
357
386
|
return roles;
|
|
358
387
|
}
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
361
|
-
||
|
|
362
|
-
||
|
|
363
|
-
||
|
|
364
|
-
throw dataError(`WorkItem Candidate producer
|
|
388
|
+
const sourceRun = store.getRun(item.taskId, candidate.source.runId);
|
|
389
|
+
if (sourceRun === null
|
|
390
|
+
|| sourceRun.workItemId !== item.id
|
|
391
|
+
|| sourceRun.purpose !== "execution"
|
|
392
|
+
|| sourceRun.status !== "completed") {
|
|
393
|
+
throw dataError(`WorkItem Candidate producer AgentRun is unavailable: ${item.id}/${candidate.source.runId}.`);
|
|
365
394
|
}
|
|
366
|
-
roles.add(
|
|
367
|
-
if (
|
|
368
|
-
const group = workItemExecutionGroupById(item,
|
|
395
|
+
roles.add(sourceRun.roleName);
|
|
396
|
+
if (sourceRun.sourceExecutionGroupId !== undefined) {
|
|
397
|
+
const group = workItemExecutionGroupById(item, sourceRun.sourceExecutionGroupId);
|
|
369
398
|
if (group === undefined) {
|
|
370
399
|
throw dataError(`WorkItem Candidate ExecutionGroup is unavailable: `
|
|
371
|
-
+ `${item.id}/${
|
|
400
|
+
+ `${item.id}/${sourceRun.sourceExecutionGroupId}.`);
|
|
372
401
|
}
|
|
373
|
-
for (const producer of
|
|
402
|
+
for (const producer of selectedWorkItemSynthesisProducers(store, item, group, synthesisSourceRunIds(store, sourceRun))) {
|
|
374
403
|
roles.add(producer.roleName);
|
|
375
404
|
}
|
|
376
405
|
}
|
|
@@ -506,6 +535,8 @@ export function updateTaskMetadataCommand(store, taskId, patch, options = {}) {
|
|
|
506
535
|
tx.saveTask(updated);
|
|
507
536
|
recordTaskEvent(tx, updated.id, "task.updated", {
|
|
508
537
|
status: updated.status,
|
|
538
|
+
previous: editedFieldValues(current, Object.keys(patch)),
|
|
539
|
+
current: editedFieldValues(updated, Object.keys(patch)),
|
|
509
540
|
...(updated.type === undefined ? {} : { taskType: updated.type })
|
|
510
541
|
}, now);
|
|
511
542
|
enqueueWork(tx, taskMailbox(updated.id), "task-updated", now, [taskRef(updated.id)]);
|
|
@@ -514,26 +545,273 @@ export function updateTaskMetadataCommand(store, taskId, patch, options = {}) {
|
|
|
514
545
|
notifyMailbox(options.runtime, taskMailbox(result.id), result.id);
|
|
515
546
|
return result;
|
|
516
547
|
}
|
|
517
|
-
|
|
548
|
+
function routeUserSubmission(tx, task, actor, body, intent, now, submissionKey, target) {
|
|
549
|
+
const kind = actor;
|
|
550
|
+
const author = actor === "operator"
|
|
551
|
+
? { type: "operator" }
|
|
552
|
+
: { type: "user" };
|
|
553
|
+
// §2.3 keyed idempotency: a retry is detected by reading an existing keyed
|
|
554
|
+
// Message, and its original disposition is reproduced from the receipt that
|
|
555
|
+
// Message carries — never recomputed from current state. A matching key with
|
|
556
|
+
// matching input and target replays; any mismatch conflicts.
|
|
557
|
+
if (submissionKey !== undefined) {
|
|
558
|
+
const prior = tx.listMessages(task.id).find((message) => message.submissionKey === submissionKey);
|
|
559
|
+
if (prior !== undefined) {
|
|
560
|
+
return replayKeyedSubmission(task, prior, kind, body, intent, target);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// Save first, recording intent and key, so the "saved" facet holds regardless of
|
|
564
|
+
// how routing then resolves (§2.5). Intent is stored, never re-read from the body.
|
|
565
|
+
const message = appendMessage(tx, task.id, body, kind, author, now, {
|
|
566
|
+
intent,
|
|
567
|
+
...(submissionKey === undefined ? {} : { submissionKey })
|
|
568
|
+
});
|
|
569
|
+
// Re-read phase and activation inside this same transaction: this is the point
|
|
570
|
+
// the race is decided at (§2.3).
|
|
571
|
+
const enteredPlanning = draftHasEnteredPlanning(tx, task);
|
|
572
|
+
const activation = draftActivationState(task.activationRequest);
|
|
573
|
+
const routing = decideSubmissionRouting({
|
|
574
|
+
intent,
|
|
575
|
+
status: task.status,
|
|
576
|
+
enteredPlanning,
|
|
577
|
+
activation,
|
|
578
|
+
executionEnabled: task.executionGate.state === "enabled",
|
|
579
|
+
// develop adopts no extra resource by guessing: the workspace is still built
|
|
580
|
+
// from the Task's own Project bindings when it activates (§2.3).
|
|
581
|
+
developEnvironmentPlan: { kind: "empty" }
|
|
582
|
+
});
|
|
583
|
+
let queuedForLeader = false;
|
|
584
|
+
let activationRef;
|
|
585
|
+
let activationFailure;
|
|
586
|
+
switch (routing.kind) {
|
|
587
|
+
case "record":
|
|
588
|
+
case "planned-needs-manual-activation":
|
|
589
|
+
case "activation-blocked-execution-stopped":
|
|
590
|
+
// Save only. No Leader wake, no planning, no activation.
|
|
591
|
+
break;
|
|
592
|
+
case "await-activation":
|
|
593
|
+
// The submission is a post-activation input or a report against an existing
|
|
594
|
+
// request; it acts on nothing itself but surfaces the exact reference.
|
|
595
|
+
activationRef = task.activationRequest === undefined
|
|
596
|
+
? undefined
|
|
597
|
+
: taskActivationOperationRef(task.id, task.activationRequest.operation.requestId);
|
|
598
|
+
if (routing.state === "failed") {
|
|
599
|
+
activationFailure = task.activationRequest?.outcome;
|
|
600
|
+
}
|
|
601
|
+
break;
|
|
602
|
+
case "enter-planning":
|
|
603
|
+
// The one place the shared service itself writes the planning-entered fact,
|
|
604
|
+
// in the same transaction as the message (§2.2 derivation source (a)).
|
|
605
|
+
recordTaskEvent(tx, task.id, TASK_PLANNING_ENTERED_EVENT, {
|
|
606
|
+
messageId: message.id,
|
|
607
|
+
intent
|
|
608
|
+
}, now);
|
|
609
|
+
enqueueWork(tx, leaderMailbox(task.id), submissionEnqueueReason(actor), now, [messageRef(task.id, message.id)], { source: actor, dedupeKey: `message:${task.id}:${message.id}` });
|
|
610
|
+
queuedForLeader = true;
|
|
611
|
+
break;
|
|
612
|
+
case "continue-planning":
|
|
613
|
+
case "active-context":
|
|
614
|
+
enqueueWork(tx, leaderMailbox(task.id), submissionEnqueueReason(actor), now, [messageRef(task.id, message.id)], { source: actor, dedupeKey: `message:${task.id}:${message.id}` });
|
|
615
|
+
queuedForLeader = true;
|
|
616
|
+
break;
|
|
617
|
+
case "activate": {
|
|
618
|
+
// Record the activation in-transaction, then queue only activation
|
|
619
|
+
// processing. The requestId is derived from the message so a retried
|
|
620
|
+
// submission cannot mint a second request.
|
|
621
|
+
const identity = nonLeaderActivationIdentity(tx, actor);
|
|
622
|
+
recordTaskActivationRequestInTransaction(tx, {
|
|
623
|
+
taskId: task.id,
|
|
624
|
+
requestId: `submit-${message.id}`,
|
|
625
|
+
actorId: identity.actorId,
|
|
626
|
+
authorityRef: identity.authorityRef,
|
|
627
|
+
environmentPlan: routing.environmentPlan,
|
|
628
|
+
origin: "submit-develop"
|
|
629
|
+
}, now);
|
|
630
|
+
activationRef = taskActivationOperationRef(task.id, `submit-${message.id}`);
|
|
631
|
+
enqueueWork(tx, taskMailbox(task.id), "activation-requested", now, [taskRef(task.id)]);
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
default: {
|
|
635
|
+
const exhaustive = routing;
|
|
636
|
+
throw new Error(`Unhandled submission routing: ${JSON.stringify(exhaustive)}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const feedback = describeSubmissionFeedback({
|
|
640
|
+
taskId: task.id,
|
|
641
|
+
messageId: message.id,
|
|
642
|
+
status: task.status,
|
|
643
|
+
enteredPlanning,
|
|
644
|
+
activationState: activation,
|
|
645
|
+
routing,
|
|
646
|
+
...(activationRef === undefined ? {} : { activationRef }),
|
|
647
|
+
...(activationFailure === undefined ? {} : { activationFailure })
|
|
648
|
+
});
|
|
649
|
+
// §2.3 receipt: a keyed submission records the disposition it actually received
|
|
650
|
+
// (routing + feedback) and the target its key bound to, in this same
|
|
651
|
+
// transaction, so a later retry reproduces exactly this outcome rather than
|
|
652
|
+
// re-deriving one from a phase or activation that has since changed.
|
|
653
|
+
if (submissionKey !== undefined && target !== undefined) {
|
|
654
|
+
const receipted = withSubmissionReceipt(message, { target, routing, feedback });
|
|
655
|
+
tx.updateMessage(task.id, receipted);
|
|
656
|
+
return { task, message: receipted, routing, feedback, queuedForLeader };
|
|
657
|
+
}
|
|
658
|
+
return { task, message, routing, feedback, queuedForLeader };
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Reproduce a keyed submission's original outcome from the receipt it recorded
|
|
662
|
+
* (task-32 §2.3), never by recomputing from the Task's current state. The prior
|
|
663
|
+
* Message's frozen {@link SubmissionReceipt} names the routing it received and the
|
|
664
|
+
* §2.5 feedback it returned, so a gate enabled or an activation cancelled after
|
|
665
|
+
* the fact can no longer fabricate a different disposition. The replay writes
|
|
666
|
+
* nothing — any Leader wake or activation request the original made already
|
|
667
|
+
* exists — and preserves the original Task/Message/routing/request references
|
|
668
|
+
* exactly.
|
|
669
|
+
*
|
|
670
|
+
* Same key with a different normalized input (kind, body or intent) or a
|
|
671
|
+
* different target is a conflict, never a silent overwrite or a cross-target
|
|
672
|
+
* replay. A prior keyed Message that carries no receipt (an old client, before
|
|
673
|
+
* receipts existed) also conflicts rather than being replayed from a rebuilt
|
|
674
|
+
* disposition.
|
|
675
|
+
*/
|
|
676
|
+
function replayKeyedSubmission(task, prior, kind, body, intent, target) {
|
|
677
|
+
const receipt = prior.submissionReceipt;
|
|
678
|
+
if (prior.kind !== kind
|
|
679
|
+
|| prior.body !== body
|
|
680
|
+
|| normalizeSubmissionIntent(prior.intent) !== intent
|
|
681
|
+
|| receipt === undefined
|
|
682
|
+
|| (target !== undefined && !sameSubmissionTarget(receipt.target, target))) {
|
|
683
|
+
throw new StorageConflictError(`Submission key ${prior.submissionKey} was already used for a different submission on ${task.id}/${prior.id}.`);
|
|
684
|
+
}
|
|
685
|
+
// Reproduce the recorded disposition verbatim; a retry acts on nothing itself.
|
|
686
|
+
return {
|
|
687
|
+
task,
|
|
688
|
+
message: prior,
|
|
689
|
+
routing: receipt.routing,
|
|
690
|
+
feedback: receipt.feedback,
|
|
691
|
+
queuedForLeader: false
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/** The mailbox reason a submission wake carries, preserving the existing
|
|
695
|
+
* operator-input / user-message vocabulary the Controller already understands. */
|
|
696
|
+
function submissionEnqueueReason(actor) {
|
|
697
|
+
return actor === "operator" ? "operator-input" : "user-message";
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Render the §2.5 feedback as CLI text, one facet per line, so the user always
|
|
701
|
+
* sees the save, the phase, planning, activation, delivery and next step as
|
|
702
|
+
* separate statements — never a single "started". Web and capability callers
|
|
703
|
+
* return {@link SubmissionFeedback} structurally instead of this text.
|
|
704
|
+
*/
|
|
705
|
+
function renderSubmissionFeedback(feedback) {
|
|
706
|
+
const lines = [
|
|
707
|
+
`Saved message ${feedback.saved.messageId} to ${feedback.saved.taskId}.`,
|
|
708
|
+
`Phase: ${SUBMISSION_PHASE_LABEL[feedback.phase]}.`
|
|
709
|
+
];
|
|
710
|
+
if (feedback.planning !== "none") {
|
|
711
|
+
lines.push(`Planning: ${feedback.planning === "entered"
|
|
712
|
+
? "started; the Leader is queued to plan"
|
|
713
|
+
: "continued; the Leader is queued"}.`);
|
|
714
|
+
}
|
|
715
|
+
if (feedback.activation !== "none") {
|
|
716
|
+
lines.push(`Activation: ${SUBMISSION_ACTIVATION_LABEL[feedback.activation]}.`);
|
|
717
|
+
}
|
|
718
|
+
if (feedback.delivery === "queued" && feedback.planning === "none") {
|
|
719
|
+
lines.push("Delivery: queued to the Leader.");
|
|
720
|
+
}
|
|
721
|
+
const nextStep = feedback.nextStep;
|
|
722
|
+
if (nextStep !== undefined)
|
|
723
|
+
lines.push(`Next: ${renderSubmissionNextStep(nextStep)}`);
|
|
724
|
+
return `${lines.join("\n")}\n`;
|
|
725
|
+
}
|
|
726
|
+
const SUBMISSION_PHASE_LABEL = {
|
|
727
|
+
active: "active",
|
|
728
|
+
"draft-planning": "Draft, in planning",
|
|
729
|
+
"draft-unplanned": "Draft, not yet in planning"
|
|
730
|
+
};
|
|
731
|
+
const SUBMISSION_ACTIVATION_LABEL = {
|
|
732
|
+
none: "none",
|
|
733
|
+
requested: "requested; activation is queued",
|
|
734
|
+
pending: "already pending; this input waits for it",
|
|
735
|
+
failed: "the previous request failed",
|
|
736
|
+
"manual-required": "already planned; activate explicitly to develop",
|
|
737
|
+
"execution-stopped": "not requested; execution is stopped"
|
|
738
|
+
};
|
|
739
|
+
function renderSubmissionNextStep(step) {
|
|
740
|
+
switch (step.kind) {
|
|
741
|
+
case "activate-manually":
|
|
742
|
+
return `activate it explicitly with "yui task activate ${step.taskId}".`;
|
|
743
|
+
case "start-execution":
|
|
744
|
+
return `start execution with "yui task execution start ${step.taskId}", then submit develop again.`;
|
|
745
|
+
case "await-pending-activation":
|
|
746
|
+
return `wait for the pending activation ${step.activationRef}.`;
|
|
747
|
+
case "resolve-failed-activation":
|
|
748
|
+
return `retry with a new activation request or cancel ${step.activationRef} (failure: ${step.failure}).`;
|
|
749
|
+
default: {
|
|
750
|
+
const exhaustive = step;
|
|
751
|
+
throw new Error(`Unhandled submission next step: ${JSON.stringify(exhaustive)}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
/** Parse and validate the CLI `--intent` option. Absent leaves it undefined so
|
|
756
|
+
* the shared service applies the discuss default (task-32 §2.5). */
|
|
757
|
+
function parseSubmissionIntentOption(raw, usage) {
|
|
758
|
+
if (raw === undefined)
|
|
759
|
+
return undefined;
|
|
760
|
+
if (TASK_SUBMISSION_INTENTS.includes(raw)) {
|
|
761
|
+
return raw;
|
|
762
|
+
}
|
|
763
|
+
throw usageError(`--intent must be one of ${TASK_SUBMISSION_INTENTS.join(", ")}: ${raw}.`, usage);
|
|
764
|
+
}
|
|
765
|
+
export function submitOperatorMessage(body, taskId, store, options = {}, intent, submissionKey) {
|
|
518
766
|
const now = clock(options);
|
|
767
|
+
const effectiveIntent = normalizeSubmissionIntent(intent);
|
|
519
768
|
const result = store.transaction((tx) => {
|
|
520
769
|
if (taskId !== undefined) {
|
|
770
|
+
// Addressed submit: the key is scoped to this Task and dedups within it, so
|
|
771
|
+
// no other Task is read (§2.3). Its target is this Task.
|
|
521
772
|
const task = requireTask(tx, taskId);
|
|
522
773
|
assertTaskOpen(task);
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
774
|
+
const routed = routeUserSubmission(tx, task, "operator", body, effectiveIntent, now, submissionKey, { kind: "task", taskId: task.id });
|
|
775
|
+
return { ...routed, created: false };
|
|
776
|
+
}
|
|
777
|
+
// Task-less submit: the key is scoped to "create a new Task". A retry must
|
|
778
|
+
// locate the Draft the original create produced — not any same-key Message on
|
|
779
|
+
// an addressed Task — and a key already bound to a specific Task cannot be
|
|
780
|
+
// reused to create (§2.3 "同 key 不同目标冲突").
|
|
781
|
+
if (submissionKey !== undefined) {
|
|
782
|
+
const lookup = findSubmissionKeyCreate(tx, submissionKey);
|
|
783
|
+
if (lookup.kind === "create") {
|
|
784
|
+
const task = requireTask(tx, lookup.taskId);
|
|
785
|
+
const routed = routeUserSubmission(tx, task, "operator", body, effectiveIntent, now, submissionKey, { kind: "create" });
|
|
786
|
+
return { ...routed, created: false };
|
|
787
|
+
}
|
|
788
|
+
if (lookup.kind === "other-target") {
|
|
789
|
+
throw new StorageConflictError(`Submission key ${submissionKey} is already bound to a specific Task; a task-less create cannot reuse it.`);
|
|
526
790
|
}
|
|
527
|
-
return { task, message, created: false };
|
|
528
791
|
}
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
return { ...
|
|
792
|
+
const createdAgg = createTaskAggregate(tx, titleFrom(body), {}, now);
|
|
793
|
+
const routed = routeUserSubmission(tx, createdAgg.task, "operator", body, effectiveIntent, now, submissionKey, { kind: "create" });
|
|
794
|
+
return { ...routed, task: createdAgg.task, created: true };
|
|
532
795
|
});
|
|
533
|
-
notifyMailbox(options.runtime, result.
|
|
534
|
-
|
|
535
|
-
? `Created Draft task ${result.task.id}: ${result.task.title}\
|
|
536
|
-
:
|
|
796
|
+
notifyMailbox(options.runtime, result.queuedForLeader ? leaderMailbox(result.task.id) : taskMailbox(result.task.id), result.task.id);
|
|
797
|
+
const header = result.created
|
|
798
|
+
? `Created Draft task ${result.task.id}: ${result.task.title}\n`
|
|
799
|
+
: "";
|
|
800
|
+
return `${header}${renderSubmissionFeedback(result.feedback)}`;
|
|
801
|
+
}
|
|
802
|
+
function findSubmissionKeyCreate(store, submissionKey) {
|
|
803
|
+
let otherTarget = false;
|
|
804
|
+
for (const task of store.listTasks()) {
|
|
805
|
+
for (const message of store.listMessages(task.id)) {
|
|
806
|
+
if (message.submissionKey !== submissionKey)
|
|
807
|
+
continue;
|
|
808
|
+
if (message.submissionReceipt?.target.kind === "create") {
|
|
809
|
+
return { kind: "create", taskId: task.id };
|
|
810
|
+
}
|
|
811
|
+
otherTarget = true;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return otherTarget ? { kind: "other-target" } : { kind: "unused" };
|
|
537
815
|
}
|
|
538
816
|
function createTaskCommand(args, store, options) {
|
|
539
817
|
const parsed = parseTaskCreation(args, store);
|
|
@@ -657,7 +935,7 @@ function showTaskCommand(args, store, currentTaskCandidate) {
|
|
|
657
935
|
events: events.length,
|
|
658
936
|
workItems: work.length,
|
|
659
937
|
currentWorkItems: currentWorkItemCount,
|
|
660
|
-
|
|
938
|
+
runs: store.listRuns(task.id).length,
|
|
661
939
|
changeSets: changeSets.length,
|
|
662
940
|
integrations: integrations.length,
|
|
663
941
|
publications: publications.length,
|
|
@@ -698,7 +976,7 @@ function showTaskCommand(args, store, currentTaskCandidate) {
|
|
|
698
976
|
`Events: ${counts.events}`,
|
|
699
977
|
`Work items: ${counts.workItems}`,
|
|
700
978
|
`Current work items: ${currentWorkItemCount}`,
|
|
701
|
-
`
|
|
979
|
+
`AgentRuns: ${counts.runs}`,
|
|
702
980
|
`ChangeSets: ${counts.changeSets}`,
|
|
703
981
|
`Integration Attempts: ${counts.integrations}`,
|
|
704
982
|
`Publication references: ${counts.publications} (${verifiedMergedPublications} verified merged)`,
|
|
@@ -721,7 +999,7 @@ function activateTaskCommand(args, store, options) {
|
|
|
721
999
|
const task = requireTask(tx, args[0]);
|
|
722
1000
|
if (task.status === "archived")
|
|
723
1001
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
724
|
-
if (task.status === "
|
|
1002
|
+
if (task.status === "cancelled")
|
|
725
1003
|
throw usageError(`Task is retired: ${task.id}.`);
|
|
726
1004
|
if (task.status === "completed") {
|
|
727
1005
|
throw usageError(`Task ${task.id} is completed; use task reopen before activating it.`);
|
|
@@ -734,13 +1012,26 @@ function activateTaskCommand(args, store, options) {
|
|
|
734
1012
|
|| activation.task.id !== task.id
|
|
735
1013
|
|| activation.task.status !== "active"
|
|
736
1014
|
|| !isDeepStrictEqual(activation.task.workspaceIdentity, task.workspaceIdentity)
|
|
737
|
-
|| activation.path !== task.cwd
|
|
738
|
-
|| workspace === null
|
|
739
|
-
|| workspace.owner.type !== "task"
|
|
740
|
-
|| workspace.owner.taskId !== task.id
|
|
741
|
-
|| workspace.root !== task.cwd) {
|
|
1015
|
+
|| activation.path !== task.cwd) {
|
|
742
1016
|
throw usageError(`Task workspace activation proof does not match ${task.id}.`);
|
|
743
1017
|
}
|
|
1018
|
+
// An empty environment plan over no bound Project is a legal Task shape,
|
|
1019
|
+
// so the proof of adoption is the *absence* of a workspace rather than a
|
|
1020
|
+
// ManagedWorkspace record. Demanding one here would have forced every
|
|
1021
|
+
// such activation to create a worktree purely to satisfy this check.
|
|
1022
|
+
if (taskOwnsManagedWorkspace(task)) {
|
|
1023
|
+
if (workspace === null
|
|
1024
|
+
|| workspace.owner.type !== "task"
|
|
1025
|
+
|| workspace.owner.taskId !== task.id
|
|
1026
|
+
|| workspace.root !== task.cwd) {
|
|
1027
|
+
throw usageError(`Task workspace activation proof does not match ${task.id}.`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
else if (workspace !== null
|
|
1031
|
+
|| task.workspaceIdentity !== undefined
|
|
1032
|
+
|| activation.path !== undefined) {
|
|
1033
|
+
throw usageError(`Workspace-free Task activation proof claims a workspace: ${task.id}.`);
|
|
1034
|
+
}
|
|
744
1035
|
return { task, changed: activation.changed };
|
|
745
1036
|
}
|
|
746
1037
|
throw usageError(`Task ${task.id} activation requires atomic workspace adoption through the CLI preflight.`);
|
|
@@ -790,7 +1081,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
790
1081
|
};
|
|
791
1082
|
}
|
|
792
1083
|
// Completion is a Task decision only. The current Provider Turn remains
|
|
793
|
-
// responsible for closing its own
|
|
1084
|
+
// responsible for closing its own AgentRun, and the reusable Session keeps
|
|
794
1085
|
// its independent lifecycle.
|
|
795
1086
|
// Issue 06: re-validate the full completion readiness inside the
|
|
796
1087
|
// transaction (the CAS fence) after final-review preparation. This is the
|
|
@@ -803,7 +1094,21 @@ function completeTaskCommand(args, store, options) {
|
|
|
803
1094
|
if (!readiness.ready) {
|
|
804
1095
|
throw usageError(formatCompletionBlockers(task.id, readiness.blockers));
|
|
805
1096
|
}
|
|
806
|
-
const
|
|
1097
|
+
for (const ref of request.artifactRefs) {
|
|
1098
|
+
if (isGitArtifactRefString(ref)) {
|
|
1099
|
+
fixedArtifactRefs(task.id, [ref]);
|
|
1100
|
+
}
|
|
1101
|
+
else if (ref.startsWith("turn:")) {
|
|
1102
|
+
const run = tx.getRun(task.id, ref.slice("turn:".length));
|
|
1103
|
+
if (run === null || run.result === undefined) {
|
|
1104
|
+
throw usageError(`Task completion result ref is not readable: ${ref}.`);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
else if (!/^https?:\/\/[^\s]+$/u.test(ref)) {
|
|
1108
|
+
throw usageError("Completion --artifact-ref must be a commit-pinned git:<commit>:<relativePath> reference, turn:<local-turn-id>, or an explicit HTTP(S) reference URL.");
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const completed = completeTask(task, now, { by: actor, summary, artifactRefs: request.artifactRefs });
|
|
807
1112
|
tx.saveTask(completed);
|
|
808
1113
|
tx.clearPendingWakeup(task.id);
|
|
809
1114
|
tx.clearLeaderFailure(task.id);
|
|
@@ -834,6 +1139,10 @@ function completeTaskCommand(args, store, options) {
|
|
|
834
1139
|
const terminalEvent = recordTaskEvent(tx, task.id, "task.completed", {
|
|
835
1140
|
by: actor,
|
|
836
1141
|
summary,
|
|
1142
|
+
...(completed.completionArtifactRefs === undefined ? {} : {
|
|
1143
|
+
artifactRefs: JSON.stringify(completed.completionArtifactRefs)
|
|
1144
|
+
}),
|
|
1145
|
+
dispatchHistory: JSON.stringify(taskDispatchMailboxes(tx, task.id)),
|
|
837
1146
|
...(completedProjectHeads === undefined
|
|
838
1147
|
? {}
|
|
839
1148
|
: { projectHeads: completedProjectHeads }),
|
|
@@ -850,7 +1159,7 @@ function completeTaskCommand(args, store, options) {
|
|
|
850
1159
|
// discarded at this lifecycle boundary.
|
|
851
1160
|
tx.removeWorkMailbox(taskMailbox(task.id));
|
|
852
1161
|
// Role mailboxes are also derived wake state. A Worker result or runtime
|
|
853
|
-
// signal queued while the final
|
|
1162
|
+
// signal queued while the final AgentRun was being settled must not survive a
|
|
854
1163
|
// completed Task and become actionable after a later explicit reopen.
|
|
855
1164
|
for (const role of roles) {
|
|
856
1165
|
tx.removeWorkMailbox(roleMailbox(task.id, role.name));
|
|
@@ -895,19 +1204,39 @@ function reopenTaskCommand(args, store, options) {
|
|
|
895
1204
|
return { task, changed: false };
|
|
896
1205
|
if (task.status === "archived")
|
|
897
1206
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
898
|
-
if (task.status !== "completed")
|
|
899
|
-
throw usageError(`Task is not completed: ${task.id}.`);
|
|
1207
|
+
if (task.status !== "completed" && task.status !== "cancelled") {
|
|
1208
|
+
throw usageError(`Task is not completed or cancelled: ${task.id}.`);
|
|
1209
|
+
}
|
|
1210
|
+
const actor = taskActor(tx, options, task.id);
|
|
1211
|
+
if (task.status === "cancelled" && actor === "leader") {
|
|
1212
|
+
throw usageError("Restoring a cancelled Task requires user or Operator authority.");
|
|
1213
|
+
}
|
|
1214
|
+
const dispatchHistory = JSON.stringify(taskDispatchMailboxes(tx, task.id));
|
|
1215
|
+
// Reopening changes Task intent, not the disposition of existing inputs.
|
|
1216
|
+
// In particular, external messages and unknown deliveries remain owned by
|
|
1217
|
+
// their existing mailbox fences.
|
|
900
1218
|
const active = reopenTask(task, now);
|
|
901
1219
|
tx.saveTask(active);
|
|
902
1220
|
const reopenedReason = wakeReason("task-reopened");
|
|
903
|
-
|
|
1221
|
+
if (actor !== "leader") {
|
|
1222
|
+
enqueueWork(tx, leaderMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
1223
|
+
}
|
|
904
1224
|
enqueueWork(tx, taskMailbox(task.id), reopenedReason, now, [taskRef(task.id)]);
|
|
905
|
-
recordTaskEvent(tx, task.id, "task.reopened", {
|
|
906
|
-
|
|
1225
|
+
recordTaskEvent(tx, task.id, "task.reopened", {
|
|
1226
|
+
status: active.status, by: actor, historicalInputs: "preserved", dispatchHistory,
|
|
1227
|
+
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {}),
|
|
1228
|
+
previous: editedFieldValues(task, [
|
|
1229
|
+
"status", "completedAt", "completedBy", "completionSummary", "completionArtifactRefs",
|
|
1230
|
+
"retiredAt", "retiredBy", "retirementSummary", "replacementTaskId", "retirementIsolation"
|
|
1231
|
+
])
|
|
1232
|
+
}, now);
|
|
1233
|
+
return { task: active, changed: true, wakeLeader: actor !== "leader" };
|
|
907
1234
|
});
|
|
908
1235
|
if (result.changed) {
|
|
909
1236
|
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
910
|
-
|
|
1237
|
+
if (result.wakeLeader) {
|
|
1238
|
+
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
1239
|
+
}
|
|
911
1240
|
}
|
|
912
1241
|
return result.changed
|
|
913
1242
|
? `Reopened task ${result.task.id}\n`
|
|
@@ -922,13 +1251,17 @@ function archiveTaskCommand(args, store, options) {
|
|
|
922
1251
|
if (task.status === "archived")
|
|
923
1252
|
return { task, changed: false };
|
|
924
1253
|
if (task.status !== "completed"
|
|
925
|
-
&& task.status !== "
|
|
1254
|
+
&& task.status !== "cancelled") {
|
|
926
1255
|
throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
|
|
927
1256
|
}
|
|
928
1257
|
const remoteDelivery = request.disposition === "integrated"
|
|
929
1258
|
? assertTaskRemoteDeliveryProof(tx, task, options.archiveRemoteDeliveryProof, { forceUnverified: request.forceUnverified })
|
|
930
1259
|
: undefined;
|
|
931
1260
|
assertNoOpenInputRequests(tx, task.id, "archiving the Task");
|
|
1261
|
+
const unsettledWork = tx.listWorkItems(task.id).find((item) => item.status === "open");
|
|
1262
|
+
if (unsettledWork !== undefined) {
|
|
1263
|
+
throw usageError(`Work Item ${unsettledWork.id} must be accepted or explicitly retired before archive.`);
|
|
1264
|
+
}
|
|
932
1265
|
const unresolvedIntegration = tx.listIntegrationAttempts(task.id).find((integration) => (integration.status === "running"
|
|
933
1266
|
|| integration.status === "blocked"
|
|
934
1267
|
|| integration.status === "validating"));
|
|
@@ -945,9 +1278,9 @@ function archiveTaskCommand(args, store, options) {
|
|
|
945
1278
|
throw usageError(`Task ${task.id} still has managed worktrees; clean them before archiving.`);
|
|
946
1279
|
}
|
|
947
1280
|
const activeRole = tx.listRoles(task.id)
|
|
948
|
-
.find((role) => tx.
|
|
1281
|
+
.find((role) => tx.getActiveRun(task.id, role.name) !== null);
|
|
949
1282
|
if (activeRole !== undefined) {
|
|
950
|
-
throw usageError(`Task ${task.id} still has an active
|
|
1283
|
+
throw usageError(`Task ${task.id} still has an active AgentRun for Role ${activeRole.name}; `
|
|
951
1284
|
+ "stop its runtime before archiving.");
|
|
952
1285
|
}
|
|
953
1286
|
const liveSessionRole = tx.listRoles(task.id).find((role) => {
|
|
@@ -1003,6 +1336,41 @@ function archiveTaskCommand(args, store, options) {
|
|
|
1003
1336
|
? `Archived task ${result.task.id}\n`
|
|
1004
1337
|
: `Task ${result.task.id} is already archived\n`;
|
|
1005
1338
|
}
|
|
1339
|
+
function cancelTaskCommand(args, store, options) {
|
|
1340
|
+
const usage = "Task cancel usage: yui task cancel <task> (--summary <text>|--summary-file <path|->).";
|
|
1341
|
+
const parsed = parseTail(args, new Set(["--summary", "--summary-file"]), usage);
|
|
1342
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1343
|
+
const summary = readCommandText(parsed.options.get("--summary"), parsed.options.get("--summary-file"), "--summary", usage);
|
|
1344
|
+
const now = clock(options);
|
|
1345
|
+
const result = store.transaction((tx) => {
|
|
1346
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
1347
|
+
const actor = taskActor(tx, options, task.id);
|
|
1348
|
+
const cancelled = retireTask(task, { by: actor, summary }, now);
|
|
1349
|
+
if (cancelled === task)
|
|
1350
|
+
return task;
|
|
1351
|
+
const dispatchHistory = JSON.stringify(taskDispatchMailboxes(tx, task.id));
|
|
1352
|
+
tx.saveTask(cancelled);
|
|
1353
|
+
tx.clearPendingWakeup(task.id);
|
|
1354
|
+
tx.clearLeaderFailure(task.id);
|
|
1355
|
+
for (const mailbox of tx.listWorkMailboxes()) {
|
|
1356
|
+
if ((mailbox.target.kind === "task" || mailbox.target.kind === "role")
|
|
1357
|
+
&& mailbox.target.taskId === task.id)
|
|
1358
|
+
tx.removeWorkMailbox(mailbox.target);
|
|
1359
|
+
}
|
|
1360
|
+
const event = recordTaskEvent(tx, task.id, "task.cancelled", { by: actor, summary, dispatchHistory }, now);
|
|
1361
|
+
enqueueOperatorEvent(tx, event, "task-terminal", now);
|
|
1362
|
+
// Cancellation is intent, not fabricated proof that old processes stopped.
|
|
1363
|
+
// AgentRuns, inputs, WorkItems and their results remain independently readable.
|
|
1364
|
+
return cancelled;
|
|
1365
|
+
});
|
|
1366
|
+
options.runtime?.notifyStateChanged(result.id);
|
|
1367
|
+
notifyMailbox(options.runtime, { kind: "operator" }, result.id);
|
|
1368
|
+
return output(`Cancelled task ${result.id}\n`, { task: result });
|
|
1369
|
+
}
|
|
1370
|
+
/** Historical evidence only; these snapshots are never dispatch input. */
|
|
1371
|
+
function taskDispatchMailboxes(store, taskId) {
|
|
1372
|
+
return store.listWorkMailboxes().filter(({ target }) => (target.kind === "task" || target.kind === "role") && target.taskId === taskId);
|
|
1373
|
+
}
|
|
1006
1374
|
function retireTaskCommand(args, store, options) {
|
|
1007
1375
|
const usage = "Task retire usage: yui task retire <task> (--summary <text>|--summary-file <path|->) [--replacement <task>].";
|
|
1008
1376
|
const parsed = parseTail(args, new Set(["--summary", "--summary-file", "--replacement"]), usage);
|
|
@@ -1014,7 +1382,7 @@ function retireTaskCommand(args, store, options) {
|
|
|
1014
1382
|
const result = store.transaction((tx) => {
|
|
1015
1383
|
const task = requireTask(tx, taskId);
|
|
1016
1384
|
const actor = taskActor(tx, options, task.id);
|
|
1017
|
-
if (task.status === "
|
|
1385
|
+
if (task.status === "cancelled") {
|
|
1018
1386
|
const same = task.retirementSummary === summary
|
|
1019
1387
|
&& task.replacementTaskId === replacementTaskId;
|
|
1020
1388
|
if (!same)
|
|
@@ -1043,12 +1411,12 @@ function retireTaskCommand(args, store, options) {
|
|
|
1043
1411
|
throw usageError(`Task ${task.id} has an active DurableJob: ${activeRetireJob.id}/${activeRetireJob.status}.`);
|
|
1044
1412
|
}
|
|
1045
1413
|
assertTaskRetirementProof(tx, task, options.taskRetirementProof);
|
|
1046
|
-
for (const run of tx.
|
|
1047
|
-
const terminal =
|
|
1414
|
+
for (const run of tx.listRuns(task.id).filter(({ status: runStatus }) => (runStatus === "active"))) {
|
|
1415
|
+
const terminal = terminalizeExactTaskRun(tx, {
|
|
1048
1416
|
taskId: task.id,
|
|
1049
1417
|
roleName: run.roleName,
|
|
1050
1418
|
agentId: run.effective.agentId,
|
|
1051
|
-
|
|
1419
|
+
runId: run.id,
|
|
1052
1420
|
mailboxDisposition: "discard",
|
|
1053
1421
|
outcome: {
|
|
1054
1422
|
status: "failed",
|
|
@@ -1057,11 +1425,11 @@ function retireTaskCommand(args, store, options) {
|
|
|
1057
1425
|
}
|
|
1058
1426
|
}, now);
|
|
1059
1427
|
if (terminal.disposition !== "applied") {
|
|
1060
|
-
throw usageError(`Task
|
|
1428
|
+
throw usageError(`Task AgentRun changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
1061
1429
|
}
|
|
1062
1430
|
}
|
|
1063
1431
|
for (const item of tx.listWorkItems(task.id)) {
|
|
1064
|
-
if (item.status === "
|
|
1432
|
+
if (item.status === "accepted" || item.status === "retired")
|
|
1065
1433
|
continue;
|
|
1066
1434
|
tx.saveWorkItem(task.id, updateWorkItemStatus(item, "retired", now, `Task retired: ${summary}`));
|
|
1067
1435
|
}
|
|
@@ -1082,12 +1450,14 @@ function retireTaskCommand(args, store, options) {
|
|
|
1082
1450
|
const retired = retireTask(task, {
|
|
1083
1451
|
by: actor,
|
|
1084
1452
|
summary,
|
|
1453
|
+
isolated: true,
|
|
1085
1454
|
...(replacementTaskId === undefined ? {} : { replacementTaskId })
|
|
1086
1455
|
}, now);
|
|
1087
1456
|
tx.saveTask(retired);
|
|
1088
1457
|
const terminalEvent = recordTaskEvent(tx, task.id, "task.retired", {
|
|
1089
1458
|
by: actor,
|
|
1090
1459
|
summary,
|
|
1460
|
+
isolationEstablished: "true",
|
|
1091
1461
|
...(replacementTaskId === undefined ? {} : { replacementTaskId })
|
|
1092
1462
|
}, now);
|
|
1093
1463
|
enqueueOperatorEvent(tx, terminalEvent, "task-terminal", now);
|
|
@@ -1153,10 +1523,13 @@ function formatProjectCommits(projects) {
|
|
|
1153
1523
|
export function validateTaskArchiveRequest(args, store, options = {}) {
|
|
1154
1524
|
const request = parseTaskArchiveArguments(args);
|
|
1155
1525
|
const task = requireTask(store, request.taskId);
|
|
1156
|
-
taskActor(store, options, task.id);
|
|
1526
|
+
const archiveActor = taskActor(store, options, task.id);
|
|
1527
|
+
if (archiveActor === "leader") {
|
|
1528
|
+
throw usageError("Task archive requires independent user or Operator authorization.");
|
|
1529
|
+
}
|
|
1157
1530
|
if (task.status !== "archived"
|
|
1158
1531
|
&& task.status !== "completed"
|
|
1159
|
-
&& task.status !== "
|
|
1532
|
+
&& task.status !== "cancelled") {
|
|
1160
1533
|
throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
|
|
1161
1534
|
}
|
|
1162
1535
|
if (task.status !== "archived") {
|
|
@@ -1179,9 +1552,55 @@ function reconcileTaskCommand(args, store, options) {
|
|
|
1179
1552
|
}
|
|
1180
1553
|
function taskMessageCommand(args, store, options) {
|
|
1181
1554
|
const [command, ...rest] = args;
|
|
1555
|
+
if (command === "handoff") {
|
|
1556
|
+
const usage = "Task message handoff usage: yui task message handoff <task/message> --to <role>.";
|
|
1557
|
+
const parsed = parseTail(rest, new Set(["--to"]), usage);
|
|
1558
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1559
|
+
const ref = taskRecordReference(parsed.positionals[0], "message", "Message reference", options);
|
|
1560
|
+
const now = clock(options);
|
|
1561
|
+
const message = store.transaction((tx) => {
|
|
1562
|
+
assertTaskDeliveryAuthority(tx, options.environment, ref.taskId);
|
|
1563
|
+
const current = tx.listMessages(ref.taskId).find((entry) => entry.id === ref.localId);
|
|
1564
|
+
if (current?.recipient?.ownerRunId === undefined || current.continuation?.runId !== undefined) {
|
|
1565
|
+
throw usageError("Only an unassigned pending Message can be explicitly handed off.");
|
|
1566
|
+
}
|
|
1567
|
+
const recipient = resolveMessageRecipient(tx, ref.taskId, requiredOption(parsed.options, "--to"), {
|
|
1568
|
+
...(current.recipient.workItemId === undefined ? {} : { workItemId: current.recipient.workItemId }),
|
|
1569
|
+
...(current.recipient.reviewRoundId === undefined ? {} : { reviewRoundId: current.recipient.reviewRoundId })
|
|
1570
|
+
});
|
|
1571
|
+
const updated = { ...current, recipient, continuation: {},
|
|
1572
|
+
handovers: [...(current.handovers ?? []), { from: current.recipient, at: now.toISOString() }] };
|
|
1573
|
+
const blocker = messageContinuationBlocker(tx, updated);
|
|
1574
|
+
if (blocker !== undefined)
|
|
1575
|
+
throw usageError(`Message handoff cannot proceed: ${blocker}.`);
|
|
1576
|
+
tx.updateMessage(ref.taskId, updated);
|
|
1577
|
+
recordTaskEvent(tx, ref.taskId, "message.handed-off", {
|
|
1578
|
+
messageId: current.id, fromRole: current.recipient.roleName, toRole: recipient.roleName,
|
|
1579
|
+
ownerRunId: recipient.ownerRunId
|
|
1580
|
+
}, now);
|
|
1581
|
+
enqueueWork(tx, taskMailbox(ref.taskId), "message-continuation", now, [messageRef(ref.taskId, current.id)]);
|
|
1582
|
+
return updated;
|
|
1583
|
+
});
|
|
1584
|
+
notifyMailbox(options.runtime, taskMailbox(ref.taskId), ref.taskId);
|
|
1585
|
+
return output(`Handed off Message ${ref.localId} to ${message.recipient.roleName}.\n`, message);
|
|
1586
|
+
}
|
|
1587
|
+
if (command === "show") {
|
|
1588
|
+
const usage = "Task message show usage: yui task message show <task/message>.";
|
|
1589
|
+
exactPositionals(rest, 1, usage);
|
|
1590
|
+
const ref = taskRecordReference(rest[0], "message", "Message reference", options);
|
|
1591
|
+
const message = listContextMessages(store, ref.taskId, options.environment)
|
|
1592
|
+
.find((entry) => entry.id === ref.localId);
|
|
1593
|
+
if (message === undefined)
|
|
1594
|
+
throw dataError("Message is unavailable in the caller's scope.");
|
|
1595
|
+
const continuationRun = message.continuation?.runId === undefined
|
|
1596
|
+
? null : store.getRun(ref.taskId, message.continuation.runId);
|
|
1597
|
+
const expanded = { ...expandTaskMessageResult(message, (task, run) => store.getRun(task, run)),
|
|
1598
|
+
...(continuationRun === null ? {} : { execution: runExecutionObservation(continuationRun, store.getTaskRoleSessionSet(ref.taskId, continuationRun.roleName)?.providerBinding, store.listEvents(ref.taskId)) }) };
|
|
1599
|
+
return { kind: "output", output: `${JSON.stringify(expanded, null, 2)}\n`, data: expanded };
|
|
1600
|
+
}
|
|
1182
1601
|
if (command === "send") {
|
|
1183
|
-
const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->) [--wake-policy leader|none].";
|
|
1184
|
-
const parsed = parseTail(rest, new Set(["--body-file", "--wake-policy"]), usage);
|
|
1602
|
+
const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->) [--intent record|discuss|develop] [--request-id <key>] [--wake-policy leader|none] [--to <role> --work-item <id>|--review-round <id>].";
|
|
1603
|
+
const parsed = parseTail(rest, new Set(["--body-file", "--intent", "--request-id", "--wake-policy", "--to", "--work-item", "--review-round"]), usage);
|
|
1185
1604
|
if (parsed.positionals.length < 1 || parsed.positionals.length > 2)
|
|
1186
1605
|
throw usageError(usage);
|
|
1187
1606
|
const body = readCommandText(parsed.positionals[1], parsed.options.get("--body-file"), "--body", usage);
|
|
@@ -1196,42 +1615,34 @@ function taskMessageCommand(args, store, options) {
|
|
|
1196
1615
|
else {
|
|
1197
1616
|
throw usageError(`--wake-policy must be 'leader' or 'none': ${wakePolicyRaw}.`);
|
|
1198
1617
|
}
|
|
1199
|
-
const
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
return { task, message, actor };
|
|
1221
|
-
});
|
|
1222
|
-
if (result.actor !== "leader") {
|
|
1223
|
-
notifyMailbox(options.runtime, result.task.status === "active"
|
|
1224
|
-
? leaderMailbox(result.task.id)
|
|
1225
|
-
: taskMailbox(result.task.id), result.task.id);
|
|
1226
|
-
}
|
|
1227
|
-
return `Sent message ${result.message.id} to ${result.task.id}\n`;
|
|
1618
|
+
const intent = parseSubmissionIntentOption(parsed.options.get("--intent"), usage);
|
|
1619
|
+
const submissionKey = parsed.options.get("--request-id");
|
|
1620
|
+
if (submissionKey !== undefined && submissionKey.trim().length === 0) {
|
|
1621
|
+
throw usageError("--request-id is required.", usage);
|
|
1622
|
+
}
|
|
1623
|
+
const recipientRole = parsed.options.get("--to");
|
|
1624
|
+
const workItemId = parsed.options.get("--work-item");
|
|
1625
|
+
const reviewRoundId = parsed.options.get("--review-round");
|
|
1626
|
+
if (recipientRole === undefined && (workItemId !== undefined || reviewRoundId !== undefined))
|
|
1627
|
+
throw usageError("--to is required for scoped Message delivery.");
|
|
1628
|
+
const result = sendTaskMessageCommand(store, parsed.positionals[0], body, wakePolicy, options, recipientRole === undefined ? undefined : { roleName: recipientRole, workItemId, reviewRoundId }, intent, submissionKey);
|
|
1629
|
+
// A user/operator submission returns the unified §2.5 feedback; render each
|
|
1630
|
+
// facet on its own line and expose the structure to non-text callers.
|
|
1631
|
+
if (result.feedback !== undefined) {
|
|
1632
|
+
return output(renderSubmissionFeedback(result.feedback), { taskId: result.task.id, message: result.message, submission: result.feedback });
|
|
1633
|
+
}
|
|
1634
|
+
const reason = result.message.continuation?.notDeliveredReason;
|
|
1635
|
+
const delivery = reason !== undefined ? { state: "not-delivered", reason }
|
|
1636
|
+
: recipientRole !== undefined || result.queuedForLeader
|
|
1637
|
+
? { state: "queued" } : { state: "saved" };
|
|
1638
|
+
return output(`Saved message ${result.message.id} to ${result.task.id} (${delivery.state}${reason === undefined ? "" : `: ${reason}`}).\n`, { taskId: result.task.id, message: result.message, delivery });
|
|
1228
1639
|
}
|
|
1229
1640
|
if (command === "list") {
|
|
1230
1641
|
const messageListUsage = "Task message list usage: yui task message list <id> [--after <timestamp>] [--limit <n>].";
|
|
1231
1642
|
const parsed = parseTail(rest, new Set(["--after", "--limit"]), messageListUsage);
|
|
1232
1643
|
exactPositionals(parsed.positionals, 1, messageListUsage);
|
|
1233
1644
|
const task = requireTask(store, parsed.positionals[0]);
|
|
1234
|
-
let messages = store
|
|
1645
|
+
let messages = listContextMessages(store, task.id, options.environment);
|
|
1235
1646
|
const after = optionalNonEmptyOption(parsed.options, "--after");
|
|
1236
1647
|
if (after !== undefined) {
|
|
1237
1648
|
const afterMs = Date.parse(after);
|
|
@@ -1279,6 +1690,127 @@ function taskMessageCommand(args, store, options) {
|
|
|
1279
1690
|
? "Task message command is required."
|
|
1280
1691
|
: `Unknown command: task message ${command}`);
|
|
1281
1692
|
}
|
|
1693
|
+
/** CLI and authenticated user Surface share the same message and mailbox
|
|
1694
|
+
* transaction. Talking to Leader does not impersonate Leader authority. */
|
|
1695
|
+
/**
|
|
1696
|
+
* The one transaction that turns an inbound Task Message into durable facts and,
|
|
1697
|
+
* when the Task's own lifecycle calls for it, Leader work.
|
|
1698
|
+
*
|
|
1699
|
+
* CLI `task message send` and the Web Task surface both call this, so neither
|
|
1700
|
+
* owns a private notion of what "sending a message" means. The Draft case is the
|
|
1701
|
+
* reason that matters here: a Draft's Leader conversation is its planning Turn,
|
|
1702
|
+
* so a Draft must wake its Leader exactly like an active Task does. Gating the
|
|
1703
|
+
* wake on `active` would leave the Draft entry point saving text that no Leader
|
|
1704
|
+
* ever reads — the message would be persisted and silently go nowhere, which is
|
|
1705
|
+
* indistinguishable to the user from a Provider that never answered.
|
|
1706
|
+
*
|
|
1707
|
+
* The wake carries intent only. Whether the resulting Turn is planning or
|
|
1708
|
+
* execution is decided by the Controller from the Task's own status, so this
|
|
1709
|
+
* function never names a purpose and no second planning path exists.
|
|
1710
|
+
*/
|
|
1711
|
+
export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options = {}, recipient, intent, submissionKey) {
|
|
1712
|
+
if (!body.trim())
|
|
1713
|
+
throw usageError("Message body is required.");
|
|
1714
|
+
if (recipient !== undefined && wakePolicy !== undefined) {
|
|
1715
|
+
throw usageError("--wake-policy applies only to unaddressed Leader Messages; an owner-directed Message uses its exact continuation boundary.");
|
|
1716
|
+
}
|
|
1717
|
+
if (recipient !== undefined && intent !== undefined) {
|
|
1718
|
+
throw usageError("A submission intent applies only to unaddressed Leader Messages; an owner-directed Message uses its exact continuation boundary.");
|
|
1719
|
+
}
|
|
1720
|
+
if (recipient !== undefined && submissionKey !== undefined) {
|
|
1721
|
+
throw usageError("A submission key applies only to unaddressed Leader Messages; an owner-directed Message uses its exact continuation boundary.");
|
|
1722
|
+
}
|
|
1723
|
+
const now = clock(options);
|
|
1724
|
+
const result = store.transaction((tx) => {
|
|
1725
|
+
const task = requireTask(tx, taskId);
|
|
1726
|
+
if (recipient === undefined)
|
|
1727
|
+
assertTaskOpen(task);
|
|
1728
|
+
const caller = currentManagedRuntime(tx, options.environment, task.id);
|
|
1729
|
+
const roleCaller = caller !== undefined && caller.roleName !== "leader" ? caller : undefined;
|
|
1730
|
+
if (roleCaller !== undefined) {
|
|
1731
|
+
const run = roleCaller.currentRunId === undefined ? null : tx.getRun(task.id, roleCaller.currentRunId);
|
|
1732
|
+
if (run === null || recipient?.roleName !== "leader"
|
|
1733
|
+
|| recipient.workItemId !== run.workItemId || recipient.reviewRoundId !== run.reviewRoundId) {
|
|
1734
|
+
throw usageError("A Worker or Reviewer may send only to Leader within its current Assignment.");
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const actor = roleCaller === undefined ? taskActor(tx, options, task.id) : "role";
|
|
1738
|
+
// A user/operator Message with no explicit recipient is the single path that
|
|
1739
|
+
// carries submission intent (record | discuss | develop). It routes through
|
|
1740
|
+
// the one shared submission service so intent means exactly the same thing
|
|
1741
|
+
// here as on every other surface. Internal role/leader result messages and
|
|
1742
|
+
// owner-directed continuations keep their exact existing boundary and never
|
|
1743
|
+
// gain develop authority (task-32 §2.5).
|
|
1744
|
+
if (recipient === undefined && (actor === "user" || actor === "operator")) {
|
|
1745
|
+
const effectiveIntent = normalizeSubmissionIntent(intent, wakePolicy);
|
|
1746
|
+
const routed = routeUserSubmission(tx, task, actor, body, effectiveIntent, now, submissionKey, { kind: "task", taskId: task.id });
|
|
1747
|
+
return {
|
|
1748
|
+
task: routed.task, message: routed.message, actor,
|
|
1749
|
+
queuedForLeader: routed.queuedForLeader, feedback: routed.feedback
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
if (intent !== undefined) {
|
|
1753
|
+
throw usageError("Submission intent applies only to an unaddressed user or Operator Message.");
|
|
1754
|
+
}
|
|
1755
|
+
if (recipient !== undefined && actor === "leader")
|
|
1756
|
+
assertTaskDeliveryAuthority(tx, options.environment, task.id);
|
|
1757
|
+
const target = recipient === undefined ? undefined
|
|
1758
|
+
: recipient.roleName === "leader" && roleCaller !== undefined ? {
|
|
1759
|
+
roleName: "leader", ...(recipient.workItemId === undefined ? {} : { workItemId: recipient.workItemId }),
|
|
1760
|
+
...(recipient.reviewRoundId === undefined ? {} : { reviewRoundId: recipient.reviewRoundId })
|
|
1761
|
+
} : resolveMessageRecipient(tx, task.id, recipient.roleName, {
|
|
1762
|
+
...(recipient.workItemId === undefined ? {} : { workItemId: recipient.workItemId }),
|
|
1763
|
+
...(recipient.reviewRoundId === undefined ? {} : { reviewRoundId: recipient.reviewRoundId })
|
|
1764
|
+
});
|
|
1765
|
+
const context = {
|
|
1766
|
+
...(target === undefined ? {} : { recipient: target }),
|
|
1767
|
+
...(recipient?.workItemId === undefined ? {} : { workItemId: recipient.workItemId })
|
|
1768
|
+
};
|
|
1769
|
+
const message = actor === "leader"
|
|
1770
|
+
? appendMessage(tx, task.id, body, "role-result", { type: "role", roleName: LEADER_ROLE }, now, context)
|
|
1771
|
+
: actor === "role"
|
|
1772
|
+
? appendMessage(tx, task.id, body, "role-result", { type: "role", roleName: roleCaller.roleName }, now, context)
|
|
1773
|
+
: actor === "operator"
|
|
1774
|
+
? appendMessage(tx, task.id, body, "operator", { type: "operator" }, now, context)
|
|
1775
|
+
: appendMessage(tx, task.id, body, "user", { type: "user" }, now, context);
|
|
1776
|
+
// Reached only by addressed Messages and by an unaddressed Leader result now:
|
|
1777
|
+
// unaddressed user/operator submissions are handled above by the shared
|
|
1778
|
+
// service. A Worker/Reviewer message addressed to its Leader still wakes the
|
|
1779
|
+
// Leader exactly as before; an owner-directed continuation posts to the Task
|
|
1780
|
+
// mailbox for its exact owner Run.
|
|
1781
|
+
const queuedForLeader = target?.ownerRunId === undefined
|
|
1782
|
+
&& leaderWakingTaskStatus(task.status) && actor !== "leader";
|
|
1783
|
+
if (target?.ownerRunId !== undefined) {
|
|
1784
|
+
const reason = messageContinuationBlocker(tx, message);
|
|
1785
|
+
if (reason !== undefined) {
|
|
1786
|
+
message.continuation = { notDeliveredReason: reason };
|
|
1787
|
+
tx.updateMessage(task.id, message);
|
|
1788
|
+
}
|
|
1789
|
+
enqueueWork(tx, taskMailbox(task.id), "message-continuation", now, [messageRef(task.id, message.id)], { dedupeKey: `message:${task.id}:${message.id}` });
|
|
1790
|
+
}
|
|
1791
|
+
else if (queuedForLeader) {
|
|
1792
|
+
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}` });
|
|
1793
|
+
}
|
|
1794
|
+
return { task, message, actor, queuedForLeader, feedback: undefined };
|
|
1795
|
+
});
|
|
1796
|
+
if (recipient !== undefined) {
|
|
1797
|
+
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
1798
|
+
}
|
|
1799
|
+
else if (result.actor !== "leader") {
|
|
1800
|
+
notifyMailbox(options.runtime, result.queuedForLeader
|
|
1801
|
+
? leaderMailbox(result.task.id) : taskMailbox(result.task.id), result.task.id);
|
|
1802
|
+
}
|
|
1803
|
+
return result;
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Whether an inbound Message on a Task in this status is delivered to its
|
|
1807
|
+
* Leader. Draft qualifies because planning is a Leader conversation that
|
|
1808
|
+
* deliberately precedes any delivery environment or Activation (S01); a
|
|
1809
|
+
* terminal Task has no Leader lane and keeps the message as context only.
|
|
1810
|
+
*/
|
|
1811
|
+
export function leaderWakingTaskStatus(status) {
|
|
1812
|
+
return status === "active" || status === "draft";
|
|
1813
|
+
}
|
|
1282
1814
|
function updateMessage(args, store, options) {
|
|
1283
1815
|
const usage = "Task message update usage: yui task message update <task>/<message> (<body>|--body-file <path|->) [--wake-policy leader|none].";
|
|
1284
1816
|
const parsed = parseTail(args, new Set(["--body-file", "--wake-policy"]), usage);
|
|
@@ -1322,9 +1854,14 @@ function updateMessage(args, store, options) {
|
|
|
1322
1854
|
updatedBy: actor,
|
|
1323
1855
|
...(wakePolicy === undefined ? {} : { wakePolicy })
|
|
1324
1856
|
}, now);
|
|
1325
|
-
|
|
1857
|
+
const queuedForLeader = updated.wakePolicy !== "none";
|
|
1858
|
+
if (queuedForLeader) {
|
|
1859
|
+
enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [messageRef(task.id, updated.id)], { source: actor });
|
|
1860
|
+
}
|
|
1861
|
+
return { task, message: updated, queuedForLeader };
|
|
1326
1862
|
});
|
|
1327
|
-
options.runtime
|
|
1863
|
+
notifyMailbox(options.runtime, result.queuedForLeader
|
|
1864
|
+
? leaderMailbox(result.task.id) : taskMailbox(result.task.id), result.task.id);
|
|
1328
1865
|
return output(`Updated Task Message ${result.task.id}/${result.message.id}\n`, {
|
|
1329
1866
|
message: result.message
|
|
1330
1867
|
});
|
|
@@ -1353,7 +1890,7 @@ function retireMessage(args, store, options) {
|
|
|
1353
1890
|
// Remove an isolated pending wake for this exact directive. A merged batch
|
|
1354
1891
|
// is retained because its other signals remain actionable; context and
|
|
1355
1892
|
// actionability projections still filter the retired message below.
|
|
1356
|
-
if (task.status
|
|
1893
|
+
if (leaderWakingTaskStatus(task.status)) {
|
|
1357
1894
|
try {
|
|
1358
1895
|
settleExactWorkExecution(tx, leaderMailbox(task.id), messageRef(task.id, message.id));
|
|
1359
1896
|
}
|
|
@@ -1408,9 +1945,9 @@ function taskRoleCommand(args, store, options) {
|
|
|
1408
1945
|
if (command === "status")
|
|
1409
1946
|
return taskRoleStatus(rest, store, options);
|
|
1410
1947
|
if (command === "show")
|
|
1411
|
-
return
|
|
1948
|
+
return showTaskRole(rest, store);
|
|
1412
1949
|
if (command === "update")
|
|
1413
|
-
return
|
|
1950
|
+
return updateTaskRole(rest, store, options);
|
|
1414
1951
|
if (command === "remove")
|
|
1415
1952
|
return output(removeTaskRole(rest, store, options));
|
|
1416
1953
|
if (command === "bind")
|
|
@@ -1435,20 +1972,76 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1435
1972
|
exactPositionals(rest, 2, "Task Role Session inspect usage: yui task role session inspect <task> <role>.");
|
|
1436
1973
|
const task = requireTask(store, rest[0]);
|
|
1437
1974
|
const role = requireRole(store, task.id, rest[1]);
|
|
1438
|
-
|
|
1975
|
+
const reader = resolveManagedTaskReader(store, options.environment);
|
|
1976
|
+
if (reader === undefined)
|
|
1977
|
+
taskActor(store, options, task.id);
|
|
1978
|
+
else if (reader.taskId !== task.id || (reader.roleName !== "leader" && reader.roleName !== role.name)) {
|
|
1979
|
+
throw usageError("Session inspection is outside the caller's Task/Role.");
|
|
1980
|
+
}
|
|
1439
1981
|
const sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
1440
1982
|
const active = sessions?.sessions[sessions.activeAgentId] ?? null;
|
|
1441
1983
|
const binding = sessions?.providerBinding ?? null;
|
|
1984
|
+
// The live reading, when the CLI took one. Rendered under the persisted
|
|
1985
|
+
// facts rather than merged into them: the Session's own record is what Yui
|
|
1986
|
+
// launched, and this is what the Agent says about it now. An empty render
|
|
1987
|
+
// means there was nothing an Agent reported, and the one-line label above
|
|
1988
|
+
// still states which of the "no value" cases applies.
|
|
1989
|
+
const runConfiguration = active === null ? undefined : options.liveRunConfiguration;
|
|
1990
|
+
const runConfigurationDetail = renderAgentRunConfiguration(runConfiguration);
|
|
1442
1991
|
return output(active === null
|
|
1443
|
-
? `No Session exists for ${task.id}/${role.name}.\n`
|
|
1992
|
+
? `No attached Session exists for ${task.id}/${role.name}.\n`
|
|
1993
|
+
+ (binding === null ? "" : `Retained native execution: ${currentProviderConversation(binding).conversationId}; input=${binding.run?.status ?? "none"}. Use session new to replace from durable context.\n`)
|
|
1444
1994
|
: [
|
|
1445
1995
|
`Session ${task.id}/${role.name}`,
|
|
1446
|
-
|
|
1996
|
+
// The component, not just the connection plan: several products are
|
|
1997
|
+
// reached over `acp`, and the plan alone cannot say which one ran.
|
|
1998
|
+
`Agent: ${active.agentId}/${agentExecutionComponentLabel(active.effective.component)}`
|
|
1999
|
+
+ ` (${active.adapterId} plan)`,
|
|
1447
2000
|
`Native id: ${active.nativeSessionId}`,
|
|
1448
|
-
`Host activation: ${active.runtimeGenerationId ?? "none"}`,
|
|
1449
2001
|
`Session: ${active.status}${active.endReason === undefined ? "" : `/${active.endReason}`}`,
|
|
1450
|
-
`
|
|
1451
|
-
|
|
2002
|
+
`AgentRun: ${binding?.run?.status ?? "none"}`,
|
|
2003
|
+
`Run configuration: ${agentRunConfigurationLabel(runConfiguration)}`
|
|
2004
|
+
].join("\n") + "\n"
|
|
2005
|
+
+ `\n${renderRoleLaunchComparison(role, active.effective)}\n`
|
|
2006
|
+
+ (runConfigurationDetail === "" ? "" : `\n${runConfigurationDetail}\n`), {
|
|
2007
|
+
task,
|
|
2008
|
+
role,
|
|
2009
|
+
session: active,
|
|
2010
|
+
providerBinding: binding,
|
|
2011
|
+
...(runConfiguration === undefined ? {} : { runConfiguration })
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
if (command === "new") {
|
|
2015
|
+
const usage = "Task Role Session new usage: yui task role session new <task> <role> --reason <text>.";
|
|
2016
|
+
const parsed = parseTail(rest, new Set(["--reason"]), usage);
|
|
2017
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
2018
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
2019
|
+
const now = clock(options);
|
|
2020
|
+
const result = store.transaction((tx) => {
|
|
2021
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
2022
|
+
if (task.status === "archived")
|
|
2023
|
+
throw usageError("An archived Task cannot select a new Session.", usage);
|
|
2024
|
+
const actor = taskActor(tx, options, task.id);
|
|
2025
|
+
const role = requireRole(tx, task.id, parsed.positionals[1]);
|
|
2026
|
+
const current = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
2027
|
+
const previous = current?.sessions[current.activeAgentId];
|
|
2028
|
+
const target = runtimeLifecycleTarget({ scope: "task", taskId: task.id, roleName: role.name });
|
|
2029
|
+
const mailbox = tx.getWorkMailbox(target);
|
|
2030
|
+
if ([...(mailbox?.pending?.reasons ?? []), ...(mailbox?.processing?.batch.reasons ?? [])]
|
|
2031
|
+
.includes(RUNTIME_SESSION_REPLACE_REQUIRED_REASON))
|
|
2032
|
+
return { taskId: task.id, roleName: role.name, target, alreadyRequested: true };
|
|
2033
|
+
recordTaskEvent(tx, task.id, "runtime.session-replacement-requested", {
|
|
2034
|
+
roleName: role.name, agentId: current?.activeAgentId ?? role.activeAgentId,
|
|
2035
|
+
...(previous === undefined ? {} : { nativeSessionId: previous.nativeSessionId }),
|
|
2036
|
+
reason, requestedBy: actor
|
|
2037
|
+
}, now);
|
|
2038
|
+
enqueueWork(tx, target, RUNTIME_SESSION_REPLACE_REQUIRED_REASON, now, [{ type: "task", id: task.id }]);
|
|
2039
|
+
return { taskId: task.id, roleName: role.name, target, alreadyRequested: false };
|
|
2040
|
+
});
|
|
2041
|
+
notifyMailbox(options.runtime, result.target, result.taskId);
|
|
2042
|
+
return output(`Requested Session replacement for ${result.taskId}/${result.roleName}. `
|
|
2043
|
+
+ "Yui will stop the old execution, retain its history and workspaces, and select a fresh Session. "
|
|
2044
|
+
+ "If replacing your own Session, end this turn; the successor reads durable Task context.\n", result);
|
|
1452
2045
|
}
|
|
1453
2046
|
if (command === "stop") {
|
|
1454
2047
|
const usage = "Task Role Session stop usage: yui task role session stop <task> <role> --reason <text>.";
|
|
@@ -1458,36 +2051,26 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1458
2051
|
const now = clock(options);
|
|
1459
2052
|
const request = store.transaction((tx) => {
|
|
1460
2053
|
const task = requireTask(tx, parsed.positionals[0]);
|
|
1461
|
-
if (
|
|
1462
|
-
throw usageError(`Task Role Session stop
|
|
2054
|
+
if (!["draft", "active", "completed", "cancelled"].includes(task.status)) {
|
|
2055
|
+
throw usageError(`Task Role Session stop is unavailable for an archived Task: ${task.id}.`, usage);
|
|
1463
2056
|
}
|
|
1464
2057
|
const actor = taskActor(tx, options, task.id);
|
|
1465
2058
|
const role = requireRole(tx, task.id, parsed.positionals[1]);
|
|
1466
2059
|
if (actor === "leader" && role.name === LEADER_ROLE) {
|
|
1467
|
-
throw usageError("
|
|
1468
|
-
}
|
|
1469
|
-
if (tx.getActiveTurn(task.id, role.name) !== null) {
|
|
1470
|
-
throw usageError(`Task Role has an active Turn; settle or retire it before stopping the Session: ${task.id}/${role.name}.`, usage);
|
|
2060
|
+
throw usageError("Use task role session new to request your own replacement and end this turn; synchronous self-stop cannot return.", usage);
|
|
1471
2061
|
}
|
|
1472
2062
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1473
|
-
const session = sessions
|
|
1474
|
-
if (session === undefined || session.
|
|
2063
|
+
const session = taskRoleControlTarget(sessions);
|
|
2064
|
+
if (session === undefined || session.adapterId === undefined) {
|
|
1475
2065
|
throw usageError(`Task Role has no active Session: ${task.id}/${role.name}.`, usage);
|
|
1476
2066
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
taskId: task.id,
|
|
1480
|
-
roleName: role.name
|
|
1481
|
-
}));
|
|
1482
|
-
if (lifecycle !== null && workMailboxHasWork(lifecycle)) {
|
|
1483
|
-
throw usageError(`Task Role Session lifecycle is busy: ${task.id}/${role.name}.`, usage);
|
|
1484
|
-
}
|
|
2067
|
+
// A repeated stop is a recovery request, not contention with its own
|
|
2068
|
+
// earlier cleanup obligation.
|
|
1485
2069
|
recordTaskEvent(tx, task.id, "runtime.session-stop-requested", {
|
|
1486
2070
|
roleName: role.name,
|
|
1487
2071
|
agentId: session.agentId,
|
|
1488
2072
|
adapterId: session.adapterId,
|
|
1489
2073
|
nativeSessionId: session.nativeSessionId,
|
|
1490
|
-
runtimeGenerationId: session.runtimeGenerationId ?? "",
|
|
1491
2074
|
reason,
|
|
1492
2075
|
requestedBy: actor
|
|
1493
2076
|
}, now);
|
|
@@ -1497,7 +2080,6 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1497
2080
|
agentId: session.agentId,
|
|
1498
2081
|
adapterId: session.adapterId,
|
|
1499
2082
|
nativeSessionId: session.nativeSessionId,
|
|
1500
|
-
...(session.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: session.runtimeGenerationId }),
|
|
1501
2083
|
sessionUpdatedAt: session.updatedAt
|
|
1502
2084
|
};
|
|
1503
2085
|
});
|
|
@@ -1615,7 +2197,7 @@ function listTaskRoles(args, store, options) {
|
|
|
1615
2197
|
status.health,
|
|
1616
2198
|
taskRoleOpenInputLabel(status),
|
|
1617
2199
|
taskRoleActiveWorkLabel(status),
|
|
1618
|
-
|
|
2200
|
+
taskRoleLastRunLabel(status),
|
|
1619
2201
|
taskRoleNativeSessionLabel(status),
|
|
1620
2202
|
taskRoleTmuxLabel(status)
|
|
1621
2203
|
]), defaultTableWidth())}\n`, { roles: statuses });
|
|
@@ -1633,9 +2215,15 @@ function showTaskRole(args, store) {
|
|
|
1633
2215
|
exactPositionals(args, 2, "Task role show usage: yui task role show <task> <role>.");
|
|
1634
2216
|
const task = requireTask(store, args[0]);
|
|
1635
2217
|
const role = requireRole(store, task.id, args[1]);
|
|
1636
|
-
|
|
2218
|
+
const sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
2219
|
+
return output(renderRoleDetails(`Task Role: ${role.name}`, role, {
|
|
1637
2220
|
kind: "task",
|
|
1638
|
-
sessions
|
|
2221
|
+
sessions
|
|
2222
|
+
}), {
|
|
2223
|
+
role,
|
|
2224
|
+
sessions,
|
|
2225
|
+
runs: store.listRuns(task.id).filter((run) => run.roleName === role.name)
|
|
2226
|
+
.map((run) => ({ id: run.id, status: run.status, effective: run.effective }))
|
|
1639
2227
|
});
|
|
1640
2228
|
}
|
|
1641
2229
|
function updateTaskRole(args, store, options) {
|
|
@@ -1646,8 +2234,11 @@ function updateTaskRole(args, store, options) {
|
|
|
1646
2234
|
}
|
|
1647
2235
|
const parsed = parseRoleOptions(tail, new Map([
|
|
1648
2236
|
...roleOptionSpecs({ update: true, includeAgent: true }),
|
|
1649
|
-
["--profile", "value"]
|
|
2237
|
+
["--profile", "value"],
|
|
2238
|
+
["--environment", "value"],
|
|
2239
|
+
["--managed-environment", "flag"]
|
|
1650
2240
|
]), usage);
|
|
2241
|
+
validateTaskRoleEnvironmentOptions(parsed, usage);
|
|
1651
2242
|
if (parsed.has("--agent") && (parsed.one("--agent")?.trim().length ?? 0) === 0) {
|
|
1652
2243
|
throw usageError("--agent is required.", usage);
|
|
1653
2244
|
}
|
|
@@ -1660,7 +2251,9 @@ function updateTaskRole(args, store, options) {
|
|
|
1660
2251
|
assertTaskOpen(task);
|
|
1661
2252
|
taskActor(tx, options, task.id);
|
|
1662
2253
|
const role = requireRole(tx, task.id, roleName);
|
|
1663
|
-
const
|
|
2254
|
+
const changesEnvironment = parsed.has("--environment") || parsed.has("--managed-environment");
|
|
2255
|
+
const changesLaunchContext = hasRoleLaunchContextOptions(parsed) || parsed.has("--profile")
|
|
2256
|
+
|| changesEnvironment;
|
|
1664
2257
|
const changesAgentConfig = hasAgentConfigOptions(parsed);
|
|
1665
2258
|
if (changesLaunchContext || changesAgentConfig) {
|
|
1666
2259
|
assertRoleRuntimeMutationAllowed(tx, {
|
|
@@ -1668,13 +2261,6 @@ function updateTaskRole(args, store, options) {
|
|
|
1668
2261
|
taskId: task.id,
|
|
1669
2262
|
roleName: role.name
|
|
1670
2263
|
}, "desired launch configuration update");
|
|
1671
|
-
assertLiveRoleSessionAcknowledged({
|
|
1672
|
-
sessions: tx.getTaskRoleSessionSet(task.id, role.name),
|
|
1673
|
-
roleName: role.name,
|
|
1674
|
-
desiredRevision: role.launchRevision,
|
|
1675
|
-
acknowledged: parsed.has(LIVE_SESSION_ACKNOWLEDGEMENT_OPTION),
|
|
1676
|
-
stopCommand: `yui task role session stop ${task.id} ${role.name} --reason "<decision>"`
|
|
1677
|
-
});
|
|
1678
2264
|
}
|
|
1679
2265
|
const profileId = parsed.one("--profile");
|
|
1680
2266
|
const agentProfile = profileId === undefined
|
|
@@ -1692,7 +2278,7 @@ function updateTaskRole(args, store, options) {
|
|
|
1692
2278
|
[bindingUpdate.agentId]: bindingUpdate.binding
|
|
1693
2279
|
}
|
|
1694
2280
|
}, now);
|
|
1695
|
-
|
|
2281
|
+
let next = updateRole(withBinding, {
|
|
1696
2282
|
...roleProfilePatch(parsed)
|
|
1697
2283
|
}, now);
|
|
1698
2284
|
if (bindingUpdate !== undefined) {
|
|
@@ -1701,16 +2287,32 @@ function updateTaskRole(args, store, options) {
|
|
|
1701
2287
|
if (changesLaunchContext) {
|
|
1702
2288
|
validateConfiguredRoleSkills(options.yuiHome, next.skills ?? []);
|
|
1703
2289
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2290
|
+
if (changesEnvironment) {
|
|
2291
|
+
next = updateRole(next, {
|
|
2292
|
+
executionEnvironment: parsed.has("--managed-environment")
|
|
2293
|
+
? null
|
|
2294
|
+
: createProjectResources(tx, () => now).resolveExecutionEnvironment(task.id, parsed.one("--environment"))
|
|
2295
|
+
}, now);
|
|
2296
|
+
}
|
|
2297
|
+
saveTaskRoleUpdate(tx, role, next, now, {
|
|
2298
|
+
source: "task.role.update", actor: taskActor(tx, options, task.id)
|
|
2299
|
+
});
|
|
1707
2300
|
return next;
|
|
1708
2301
|
});
|
|
1709
2302
|
notifyMailbox(options.runtime, taskMailbox(updated.taskId), updated.taskId);
|
|
1710
|
-
|
|
2303
|
+
const sessions = store.getTaskRoleSessionSet(updated.taskId, updated.name);
|
|
2304
|
+
return output(renderRoleDetails(`Updated Task Role: ${updated.name}`, updated, {
|
|
1711
2305
|
kind: "task",
|
|
1712
|
-
sessions
|
|
1713
|
-
});
|
|
2306
|
+
sessions
|
|
2307
|
+
}), { role: updated, sessions });
|
|
2308
|
+
}
|
|
2309
|
+
function validateTaskRoleEnvironmentOptions(parsed, usage) {
|
|
2310
|
+
if (parsed.has("--environment") && parsed.has("--managed-environment")) {
|
|
2311
|
+
throw usageError("--environment and --managed-environment are mutually exclusive.", usage);
|
|
2312
|
+
}
|
|
2313
|
+
if (parsed.has("--environment") && (parsed.one("--environment")?.trim().length ?? 0) === 0) {
|
|
2314
|
+
throw usageError("--environment requires an adopted preparation id.", usage);
|
|
2315
|
+
}
|
|
1714
2316
|
}
|
|
1715
2317
|
function removeTaskRole(args, store, options) {
|
|
1716
2318
|
exactPositionals(args, 2, "Task role remove usage: yui task role remove <task> <role>.");
|
|
@@ -1727,8 +2329,8 @@ function removeTaskRole(args, store, options) {
|
|
|
1727
2329
|
taskId: task.id,
|
|
1728
2330
|
roleName: role.name
|
|
1729
2331
|
}, "removal");
|
|
1730
|
-
if (tx.
|
|
1731
|
-
throw usageError(`Task Role has an active
|
|
2332
|
+
if (tx.getActiveRun(task.id, role.name) !== null) {
|
|
2333
|
+
throw usageError(`Task Role has an active AgentRun and cannot be removed: ${task.id}/${role.name}.`);
|
|
1732
2334
|
}
|
|
1733
2335
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1734
2336
|
if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status === "active")) {
|
|
@@ -1757,7 +2359,7 @@ function bindTaskRole(args, store, options) {
|
|
|
1757
2359
|
}, "desired Agent binding update");
|
|
1758
2360
|
const agent = requireAgent(tx, args[2]);
|
|
1759
2361
|
const binding = role.agentBindings[agent.id]
|
|
1760
|
-
?? createRoleAgentBinding(
|
|
2362
|
+
?? createRoleAgentBinding(agent);
|
|
1761
2363
|
const bound = updateRole(role, {
|
|
1762
2364
|
agentBindings: { ...role.agentBindings, [agent.id]: binding }
|
|
1763
2365
|
}, now);
|
|
@@ -1777,7 +2379,7 @@ function bindTaskRole(args, store, options) {
|
|
|
1777
2379
|
const switched = (() => {
|
|
1778
2380
|
try {
|
|
1779
2381
|
return switchActiveRoleAgent(bound, existing, agent.id, {
|
|
1780
|
-
|
|
2382
|
+
activeRun: tx.getActiveRun(task.id, role.name) !== null,
|
|
1781
2383
|
nativeProcessRunning: currentSession !== undefined
|
|
1782
2384
|
&& currentSession.status === "active"
|
|
1783
2385
|
}, now);
|
|
@@ -1790,6 +2392,10 @@ function bindTaskRole(args, store, options) {
|
|
|
1790
2392
|
recordTaskEvent(tx, task.id, "role.agent-bound", {
|
|
1791
2393
|
role: switched.role.name,
|
|
1792
2394
|
agentId: agent.id,
|
|
2395
|
+
// Re-selecting an Agent must not revive its old management entrance.
|
|
2396
|
+
// This revokes authority, not the Session's independent execution fact.
|
|
2397
|
+
...(role.name === LEADER_ROLE && currentSession?.nativeSessionId !== undefined
|
|
2398
|
+
? { revokedNativeSessionId: currentSession.nativeSessionId } : {}),
|
|
1793
2399
|
...roleLaunchEventPayload(switched.role, switched.sessions)
|
|
1794
2400
|
}, now);
|
|
1795
2401
|
return { role: switched.role, mode: switched.mode };
|
|
@@ -1859,20 +2465,13 @@ function transferTaskRoleAuthority(args, store, options, action) {
|
|
|
1859
2465
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1860
2466
|
const session = sessions?.sessions[role.activeAgentId];
|
|
1861
2467
|
const binding = sessions?.providerBinding;
|
|
1862
|
-
if (sessions === null || sessions === undefined || session === undefined
|
|
1863
|
-
|| binding === null || binding === undefined
|
|
1864
|
-
|| session.runtimeGenerationId === undefined
|
|
1865
|
-
|| session.status === "ended") {
|
|
2468
|
+
if (sessions === null || sessions === undefined || session === undefined || binding === null || binding === undefined || session.status === "ended") {
|
|
1866
2469
|
throw new Error(`Task Role has no live managed Provider: ${task.id}/${role.name}.`);
|
|
1867
2470
|
}
|
|
1868
|
-
const activation = currentProviderActivation(binding);
|
|
1869
|
-
if (activation === null) {
|
|
1870
|
-
throw new Error(`Provider Activation is not live: ${task.id}/${role.name}.`);
|
|
1871
|
-
}
|
|
1872
2471
|
if (action === "takeover") {
|
|
1873
|
-
const
|
|
1874
|
-
if (
|
|
1875
|
-
throw new Error(`Task Role has no active managed
|
|
2472
|
+
const activeRun = tx.getActiveRun(task.id, role.name);
|
|
2473
|
+
if (activeRun === null) {
|
|
2474
|
+
throw new Error(`Task Role has no active managed AgentRun for takeover: ${task.id}/${role.name}.`);
|
|
1876
2475
|
}
|
|
1877
2476
|
}
|
|
1878
2477
|
if (action === "takeover"
|
|
@@ -1893,7 +2492,7 @@ function transferTaskRoleAuthority(args, store, options, action) {
|
|
|
1893
2492
|
expectedEpoch: binding.authority.epoch,
|
|
1894
2493
|
expectedOwner: binding.authority.owner,
|
|
1895
2494
|
owner: desiredOwner,
|
|
1896
|
-
holderId: action === "takeover" ? `human:${randomUUID()}` :
|
|
2495
|
+
holderId: action === "takeover" ? `human:${randomUUID()}` : "controller",
|
|
1897
2496
|
changedAt: now.toISOString()
|
|
1898
2497
|
});
|
|
1899
2498
|
const authority = updatedBinding.authority;
|
|
@@ -1914,7 +2513,6 @@ function transferTaskRoleAuthority(args, store, options, action) {
|
|
|
1914
2513
|
action,
|
|
1915
2514
|
taskId: task.id,
|
|
1916
2515
|
roleName: role.name,
|
|
1917
|
-
runtimeGenerationId: session.runtimeGenerationId,
|
|
1918
2516
|
nativeSessionId: session.nativeSessionId,
|
|
1919
2517
|
authority: {
|
|
1920
2518
|
epoch: authority.epoch,
|
|
@@ -1947,6 +2545,8 @@ function taskWorkCommand(args, store, options) {
|
|
|
1947
2545
|
return output(updateWorkScope(rest, store, options));
|
|
1948
2546
|
if (command === "dispatch")
|
|
1949
2547
|
return output(dispatchWork(rest, store, options));
|
|
2548
|
+
if (command === "synthesize")
|
|
2549
|
+
return synthesizeRuns(rest, "workItem", store, options);
|
|
1950
2550
|
if (command === "review") {
|
|
1951
2551
|
return rest[0] === "retry"
|
|
1952
2552
|
? retryFailedTaskReviewRound(rest.slice(1), store, options)
|
|
@@ -1986,14 +2586,10 @@ function editWork(args, store, options) {
|
|
|
1986
2586
|
const result = store.transaction((tx) => {
|
|
1987
2587
|
const item = requireWorkItem(tx, parsed.positionals[0], options);
|
|
1988
2588
|
const task = requireTask(tx, item.taskId);
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
assertDraftTaskExecutionFree(tx, task);
|
|
2589
|
+
assertTaskOpen(task);
|
|
2590
|
+
if (task.status === "draft")
|
|
2591
|
+
assertDraftTaskExecutionFree(tx, task);
|
|
1993
2592
|
const actor = taskActor(tx, options, task.id);
|
|
1994
|
-
if (actor !== "user" && actor !== "operator") {
|
|
1995
|
-
throw usageError("Only the user or Operator may edit a Draft Work Item.");
|
|
1996
|
-
}
|
|
1997
2593
|
if (item.status === "retired") {
|
|
1998
2594
|
throw usageError(`Work Item is retired: ${item.id}.`);
|
|
1999
2595
|
}
|
|
@@ -2014,7 +2610,7 @@ function editWork(args, store, options) {
|
|
|
2014
2610
|
: parsed.options.has("--clear-role") ? null : undefined;
|
|
2015
2611
|
if (typeof assignee === "string")
|
|
2016
2612
|
requireRole(tx, task.id, assignee);
|
|
2017
|
-
const updated =
|
|
2613
|
+
const updated = editWorkItemDefinition(item, {
|
|
2018
2614
|
...(parsed.options.has("--title")
|
|
2019
2615
|
? { title: requiredOption(parsed.options, "--title") }
|
|
2020
2616
|
: {}),
|
|
@@ -2031,7 +2627,21 @@ function editWork(args, store, options) {
|
|
|
2031
2627
|
...(baseRefs === undefined ? {} : { baseRefs }),
|
|
2032
2628
|
...(assignee === undefined ? {} : { assignee })
|
|
2033
2629
|
}, now);
|
|
2034
|
-
|
|
2630
|
+
if (task.status === "draft")
|
|
2631
|
+
validateDraftWorkItemEdit(tx, task, updated);
|
|
2632
|
+
else {
|
|
2633
|
+
// Requirements can evolve while a frozen Assignment continues. Resource
|
|
2634
|
+
// scope and ownership changes still require an explicit idle boundary.
|
|
2635
|
+
if ((projects !== undefined || baseRefs !== undefined || assignee !== undefined)
|
|
2636
|
+
&& tx.listRuns(task.id).some((run) => run.workItemId === item.id && run.status === "active")) {
|
|
2637
|
+
throw usageError(`Stop the active Work Item AgentRun before changing ownership or workspace scope: ${item.id}.`);
|
|
2638
|
+
}
|
|
2639
|
+
for (const dependencyId of updated.dependsOn) {
|
|
2640
|
+
if (tx.getWorkItem(task.id, dependencyId) === null) {
|
|
2641
|
+
throw usageError(`Work Item dependency not found: ${dependencyId}.`);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2035
2645
|
tx.saveWorkItem(task.id, updated);
|
|
2036
2646
|
const changedFields = [
|
|
2037
2647
|
parsed.options.has("--title") ? "title" : undefined,
|
|
@@ -2051,8 +2661,13 @@ function editWork(args, store, options) {
|
|
|
2051
2661
|
workItemId: updated.id,
|
|
2052
2662
|
revision: String(updated.revision),
|
|
2053
2663
|
fields: changedFields.join(","),
|
|
2664
|
+
previous: editedFieldValues(item, changedFields),
|
|
2665
|
+
current: editedFieldValues(updated, changedFields),
|
|
2054
2666
|
editedBy: actor
|
|
2055
2667
|
}, now);
|
|
2668
|
+
if (actor !== "leader") {
|
|
2669
|
+
enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [workItemRef(task.id, updated.id)], { source: actor });
|
|
2670
|
+
}
|
|
2056
2671
|
return { task, item: updated };
|
|
2057
2672
|
});
|
|
2058
2673
|
options.runtime?.notifyStateChanged(result.task.id);
|
|
@@ -2129,8 +2744,8 @@ function updateWorkScope(args, store, options) {
|
|
|
2129
2744
|
const item = requireWorkItem(tx, parsed.positionals[0], options);
|
|
2130
2745
|
const task = requireTask(tx, item.taskId);
|
|
2131
2746
|
taskActor(tx, options, task.id);
|
|
2132
|
-
if (tx.
|
|
2133
|
-
throw usageError(`Stop the active Work Item
|
|
2747
|
+
if (tx.getActiveRun(task.id, item.assignee ?? "") !== null) {
|
|
2748
|
+
throw usageError(`Stop the active Work Item AgentRun before changing scope: ${item.id}.`);
|
|
2134
2749
|
}
|
|
2135
2750
|
const requestedProjectIds = (parsed.multiOptions.get("--project") ?? []).map((reference) => {
|
|
2136
2751
|
const project = resolveProject(task.projectBindings.map(({ projectId }) => requireProject(tx, projectId)), reference);
|
|
@@ -2160,12 +2775,16 @@ function updateWorkScope(args, store, options) {
|
|
|
2160
2775
|
return `${updated.changed ? "Updated" : "Unchanged"} Work Item Project scope ${updated.item.id}: ${updated.item.writeProjectIds.join(", ") || "read-only"}\n`;
|
|
2161
2776
|
}
|
|
2162
2777
|
function updateWork(args, store, options) {
|
|
2163
|
-
const usage = "Task work update usage: yui task work update <task>/<work> <todo|running|done|failed> [--summary <text>].";
|
|
2164
|
-
const parsed =
|
|
2778
|
+
const usage = "Task work update usage: yui task work update <task>/<work> <todo|running|done|failed> [--summary <text>] [--artifact-ref git:<commit>:<relative-path> ...].";
|
|
2779
|
+
const parsed = parseMultiValueTail(args, new Set(["--summary"]), new Set(["--artifact-ref"]), usage);
|
|
2165
2780
|
exactPositionals(parsed.positionals, 2, usage);
|
|
2166
2781
|
const requested = parsed.positionals[1];
|
|
2167
2782
|
const status = parseWorkStatus(requested);
|
|
2168
2783
|
const summary = trimmed(parsed.options.get("--summary"));
|
|
2784
|
+
const artifactIds = parsed.multiOptions.get("--artifact-ref") ?? [];
|
|
2785
|
+
if (artifactIds.length > 0 && status !== "completed") {
|
|
2786
|
+
throw usageError("--artifact-ref is only valid when submitting a done Candidate.");
|
|
2787
|
+
}
|
|
2169
2788
|
if (["completed", "failed"].includes(status)
|
|
2170
2789
|
&& summary === undefined) {
|
|
2171
2790
|
throw usageError(`--summary is required when work becomes ${requested}.`);
|
|
@@ -2175,19 +2794,16 @@ function updateWork(args, store, options) {
|
|
|
2175
2794
|
const current = requireWorkItem(tx, parsed.positionals[0], options);
|
|
2176
2795
|
const task = requireTask(tx, current.taskId);
|
|
2177
2796
|
assertTaskOpen(task);
|
|
2797
|
+
const artifactRefs = artifactIds.length === 0 ? undefined : fixedArtifactRefs(task.id, artifactIds);
|
|
2178
2798
|
if (current.assignee === undefined) {
|
|
2179
2799
|
taskActor(tx, options, task.id);
|
|
2180
2800
|
if (status === "running") {
|
|
2181
2801
|
assertWorkItemDependenciesCompletedForCommand(tx, current);
|
|
2182
2802
|
}
|
|
2183
|
-
if (status === "completed" && current.status === "awaiting_acceptance") {
|
|
2184
|
-
throw usageError(`Work Item ${current.id} is awaiting acceptance; use task work accept `
|
|
2185
|
-
+ "after the required ReviewRound and Integration evidence.");
|
|
2186
|
-
}
|
|
2187
2803
|
const configuredReview = status === "completed" && !isTerminalWorkItemStatus(current.status)
|
|
2188
2804
|
? tx.getReviewConfig()
|
|
2189
2805
|
: null;
|
|
2190
|
-
const taskFinalContract = status === "completed" && current.status === "
|
|
2806
|
+
const taskFinalContract = status === "completed" && current.status === "open"
|
|
2191
2807
|
? taskFinalReviewContractForMutation(tx, task.id, options)
|
|
2192
2808
|
: undefined;
|
|
2193
2809
|
const candidatePolicy = taskFinalContract === undefined
|
|
@@ -2197,9 +2813,7 @@ function updateWork(args, store, options) {
|
|
|
2197
2813
|
&& current.writeProjectIds.length > 0;
|
|
2198
2814
|
const metadataOnlyTaskFinalDelivery = taskFinalContract !== undefined
|
|
2199
2815
|
&& current.assignee === undefined;
|
|
2200
|
-
const candidateRequired = status === "completed"
|
|
2201
|
-
&& current.status === "running"
|
|
2202
|
-
&& (projectDelivery || candidatePolicy !== null);
|
|
2816
|
+
const candidateRequired = status === "completed" && current.status === "open";
|
|
2203
2817
|
const developWorkspace = tx.getWorkItemWorkspace(task.id, current.id);
|
|
2204
2818
|
if (status === "completed"
|
|
2205
2819
|
&& projectDelivery
|
|
@@ -2211,6 +2825,7 @@ function updateWork(args, store, options) {
|
|
|
2211
2825
|
? submitWorkItemCandidate(current, {
|
|
2212
2826
|
summary: summary,
|
|
2213
2827
|
source: { type: "direct" },
|
|
2828
|
+
...(artifactRefs === undefined ? {} : { artifactRefs }),
|
|
2214
2829
|
...(candidatePolicy === null ? {} : { reviewPolicy: candidatePolicy }),
|
|
2215
2830
|
...(taskFinalContract === undefined
|
|
2216
2831
|
? {}
|
|
@@ -2225,9 +2840,7 @@ function updateWork(args, store, options) {
|
|
|
2225
2840
|
? {}
|
|
2226
2841
|
: { taskMainSnapshot: options.directTaskMainSnapshot })
|
|
2227
2842
|
}, now)
|
|
2228
|
-
: current
|
|
2229
|
-
? retryFailedWorkItem(current, now)
|
|
2230
|
-
: updateWorkItemStatus(current, status, now, isTerminalWorkItemStatus(status) ? summary : undefined);
|
|
2843
|
+
: updateWorkItemStatus(current, "open", now);
|
|
2231
2844
|
tx.saveWorkItem(task.id, updated);
|
|
2232
2845
|
if (summary !== undefined) {
|
|
2233
2846
|
recordTaskEvent(tx, task.id, "work.updated", {
|
|
@@ -2250,29 +2863,29 @@ function updateWork(args, store, options) {
|
|
|
2250
2863
|
};
|
|
2251
2864
|
}
|
|
2252
2865
|
taskActor(tx, options, task.id);
|
|
2253
|
-
if (status !== "completed" || current.status !== "
|
|
2254
|
-
throw usageError(`Assigned Work Item ${current.id} can only submit a completed direct
|
|
2255
|
-
+ "use dispatch, task
|
|
2866
|
+
if (status !== "completed" || current.status !== "open") {
|
|
2867
|
+
throw usageError(`Assigned Work Item ${current.id} can only submit a completed direct AgentRun from running; `
|
|
2868
|
+
+ "use dispatch, task run retry, or task work retire for other transitions.");
|
|
2256
2869
|
}
|
|
2257
|
-
if (tx.
|
|
2258
|
-
throw usageError(`Work Item main
|
|
2870
|
+
if (tx.getActiveRun(task.id, current.assignee) !== null) {
|
|
2871
|
+
throw usageError(`Work Item main AgentRun is still active: ${current.id}/${current.assignee}.`);
|
|
2259
2872
|
}
|
|
2260
2873
|
const sourceGroup = currentWorkItemExecutionGroup(current);
|
|
2261
|
-
const
|
|
2262
|
-
&&
|
|
2263
|
-
&&
|
|
2264
|
-
&&
|
|
2265
|
-
&&
|
|
2266
|
-
&&
|
|
2267
|
-
&&
|
|
2268
|
-
&&
|
|
2269
|
-
if (
|
|
2874
|
+
const mainRun = tx.listRuns(task.id).filter((run) => (run.purpose === "execution"
|
|
2875
|
+
&& run.workItemId === current.id
|
|
2876
|
+
&& run.roleName === current.assignee
|
|
2877
|
+
&& run.executionGroupId === undefined
|
|
2878
|
+
&& run.executionLaneId === undefined
|
|
2879
|
+
&& run.sourceExecutionGroupId === sourceGroup?.id
|
|
2880
|
+
&& run.status === "completed"
|
|
2881
|
+
&& run.result !== undefined)).at(-1);
|
|
2882
|
+
if (mainRun === undefined) {
|
|
2270
2883
|
throw usageError(sourceGroup === undefined
|
|
2271
|
-
? `Work Item ${current.id} has no completed direct main
|
|
2272
|
-
: `Work Item ${current.id} has no completed main
|
|
2884
|
+
? `Work Item ${current.id} has no completed direct main AgentRun.`
|
|
2885
|
+
: `Work Item ${current.id} has no completed main AgentRun for ExecutionGroup ${sourceGroup.id}.`);
|
|
2273
2886
|
}
|
|
2274
|
-
if (
|
|
2275
|
-
throw dataError(`Completed WorkItem main
|
|
2887
|
+
if (mainRun.result === undefined) {
|
|
2888
|
+
throw dataError(`Completed WorkItem main AgentRun has no result: ${mainRun.id}.`);
|
|
2276
2889
|
}
|
|
2277
2890
|
const configuredReview = tx.getReviewConfig();
|
|
2278
2891
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
@@ -2288,8 +2901,9 @@ function updateWork(args, store, options) {
|
|
|
2288
2901
|
throw usageError(`Project-backed Work Item ${current.id} must be isolated before Candidate submission.`);
|
|
2289
2902
|
}
|
|
2290
2903
|
const updated = submitWorkItemCandidate(current, {
|
|
2291
|
-
summary: `Result from
|
|
2292
|
-
source: { type: "
|
|
2904
|
+
summary: `Result from AgentRun ${mainRun.id}.`,
|
|
2905
|
+
source: { type: "run", runId: mainRun.id },
|
|
2906
|
+
...(artifactRefs === undefined ? {} : { artifactRefs }),
|
|
2293
2907
|
...(candidatePolicy === null ? {} : { reviewPolicy: candidatePolicy }),
|
|
2294
2908
|
...(taskFinalContract === undefined
|
|
2295
2909
|
? {}
|
|
@@ -2307,12 +2921,12 @@ function updateWork(args, store, options) {
|
|
|
2307
2921
|
workItemId: updated.id,
|
|
2308
2922
|
status: updated.status,
|
|
2309
2923
|
summary: summary,
|
|
2310
|
-
|
|
2924
|
+
runId: mainRun.id,
|
|
2311
2925
|
...leaderActionEventPayload(tx, task.id, options)
|
|
2312
2926
|
}, now);
|
|
2313
2927
|
enqueueWork(tx, taskMailbox(task.id), "work-updated", now, [
|
|
2314
2928
|
workItemRef(task.id, updated.id),
|
|
2315
|
-
|
|
2929
|
+
runRef(task.id, mainRun.id)
|
|
2316
2930
|
]);
|
|
2317
2931
|
const reviewDispatch = candidatePolicy?.trigger === "always"
|
|
2318
2932
|
? queueReviewRound(tx, updated, candidatePolicy, "policy", now)
|
|
@@ -2324,7 +2938,7 @@ function updateWork(args, store, options) {
|
|
|
2324
2938
|
};
|
|
2325
2939
|
});
|
|
2326
2940
|
notifyMailbox(options.runtime, taskMailbox(result.item.taskId), result.item.taskId);
|
|
2327
|
-
if (result.item.status === "
|
|
2941
|
+
if ((result.item.status === "open" && result.item.candidates.length > 0) && status === "completed") {
|
|
2328
2942
|
const failure = result.reviewDispatch?.round.status === "failed"
|
|
2329
2943
|
? `Review could not start: ${result.reviewDispatch.round.failure?.message ?? result.reviewDispatch.round.id}\n`
|
|
2330
2944
|
: "";
|
|
@@ -2359,13 +2973,13 @@ function dispatchWork(args, store, options) {
|
|
|
2359
2973
|
throw usageError(`Work Item has no Task Role assignee: ${item.id}. `
|
|
2360
2974
|
+ `The Task Leader must run "yui task work update ${item.id} running" and execute it directly.`);
|
|
2361
2975
|
}
|
|
2362
|
-
const lanePlan = planReplicatedWorkItemLanes(item.assignee, requestedLaneRoles, `execution-group-${tx.
|
|
2976
|
+
const lanePlan = planReplicatedWorkItemLanes(item.assignee, requestedLaneRoles, `execution-group-${tx.peekNextRunId(task.id)}`);
|
|
2363
2977
|
const currentGroup = currentWorkItemExecutionGroup(item);
|
|
2364
|
-
if (item.status !== "
|
|
2978
|
+
if (item.status !== "open") {
|
|
2365
2979
|
throw usageError(`Work item ${item.id} cannot be dispatched from ${item.status}.`);
|
|
2366
2980
|
}
|
|
2367
2981
|
if (currentGroup !== undefined && !workItemExecutionGroupSettled(currentGroup)) {
|
|
2368
|
-
throw usageError(`Work Item ${item.id} retains open ExecutionGroup ${currentGroup.id}; retry or settle its exact Lane
|
|
2982
|
+
throw usageError(`Work Item ${item.id} retains open ExecutionGroup ${currentGroup.id}; retry or settle its exact Lane AgentRuns.`);
|
|
2369
2983
|
}
|
|
2370
2984
|
assertWorkItemDependenciesCompletedForCommand(tx, item);
|
|
2371
2985
|
const leaderOwned = item.assignee === "leader";
|
|
@@ -2389,18 +3003,16 @@ function dispatchWork(args, store, options) {
|
|
|
2389
3003
|
}
|
|
2390
3004
|
const roles = lanePlan.roles.map((name) => requireRole(tx, task.id, name));
|
|
2391
3005
|
for (const role of roles) {
|
|
2392
|
-
if (tx.
|
|
2393
|
-
throw usageError(`${task.id}/${role.name} already has an active
|
|
3006
|
+
if (tx.getActiveRun(task.id, role.name) !== null) {
|
|
3007
|
+
throw usageError(`${task.id}/${role.name} already has an active run.`);
|
|
2394
3008
|
}
|
|
2395
3009
|
}
|
|
2396
3010
|
const rawInput = trimmed(parsed.options.get("--input")) ?? item.objective;
|
|
2397
|
-
let workItemForDispatch = item
|
|
2398
|
-
? retryFailedWorkItem(item, now)
|
|
2399
|
-
: item;
|
|
3011
|
+
let workItemForDispatch = prepareWorkItemDispatch(item, now);
|
|
2400
3012
|
if (lanePlan.roles.length === 0) {
|
|
2401
3013
|
const role = requireRole(tx, task.id, item.assignee);
|
|
2402
|
-
if (tx.
|
|
2403
|
-
throw usageError(`${task.id}/${role.name} already has an active
|
|
3014
|
+
if (tx.getActiveRun(task.id, role.name) !== null) {
|
|
3015
|
+
throw usageError(`${task.id}/${role.name} already has an active run.`);
|
|
2404
3016
|
}
|
|
2405
3017
|
const effective = resolveEffectiveLaunch({
|
|
2406
3018
|
role,
|
|
@@ -2408,8 +3020,8 @@ function dispatchWork(args, store, options) {
|
|
|
2408
3020
|
workspace,
|
|
2409
3021
|
workItemWriteProjectIds: item.writeProjectIds
|
|
2410
3022
|
});
|
|
2411
|
-
const
|
|
2412
|
-
const
|
|
3023
|
+
const runId = tx.nextRunId(task.id);
|
|
3024
|
+
const run = createRun(runId, task.id, role.name, roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(task.id, role.name), effective.agentId, effective), createRunInput({
|
|
2413
3025
|
source: { type: "yui", channel: "workitem-dispatch" },
|
|
2414
3026
|
directive: rawInput,
|
|
2415
3027
|
deltaRefIds: []
|
|
@@ -2418,30 +3030,30 @@ function dispatchWork(args, store, options) {
|
|
|
2418
3030
|
workspace,
|
|
2419
3031
|
effective
|
|
2420
3032
|
});
|
|
2421
|
-
const snapshot =
|
|
3033
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
2422
3034
|
taskId: task.id,
|
|
2423
|
-
roleName:
|
|
3035
|
+
roleName: run.roleName,
|
|
2424
3036
|
purpose: "execution",
|
|
2425
3037
|
workItemId: item.id
|
|
2426
3038
|
}, now, "controller");
|
|
2427
|
-
const withContext =
|
|
2428
|
-
if (workItemForDispatch.status !== "
|
|
2429
|
-
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "
|
|
3039
|
+
const withContext = withRunContextSnapshot(run, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
3040
|
+
if (workItemForDispatch.status !== "open") {
|
|
3041
|
+
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "open", now);
|
|
2430
3042
|
}
|
|
2431
3043
|
tx.saveWorkItem(task.id, workItemForDispatch);
|
|
2432
|
-
tx.
|
|
2433
|
-
tx.
|
|
2434
|
-
|
|
3044
|
+
tx.saveRun(withContext);
|
|
3045
|
+
tx.saveActiveRun(withContext);
|
|
3046
|
+
enqueueRoleRunDispatch(tx, {
|
|
2435
3047
|
taskId: task.id,
|
|
2436
3048
|
roleName: role.name,
|
|
2437
|
-
|
|
3049
|
+
runId: withContext.id,
|
|
2438
3050
|
reason: "turn-dispatched",
|
|
2439
3051
|
occurredAt: now
|
|
2440
3052
|
});
|
|
2441
|
-
recordTaskEvent(tx, task.id, "
|
|
2442
|
-
return { kind: "direct",
|
|
3053
|
+
recordTaskEvent(tx, task.id, "run.dispatched", runLaunchEventPayload(withContext), now);
|
|
3054
|
+
return { kind: "direct", runs: [withContext] };
|
|
2443
3055
|
}
|
|
2444
|
-
const groupId = `execution-group-${tx.
|
|
3056
|
+
const groupId = `execution-group-${tx.peekNextRunId(task.id)}`;
|
|
2445
3057
|
if (workItemForDispatch !== item) {
|
|
2446
3058
|
// Freeze the Assignment against the retried WorkItem revision. The
|
|
2447
3059
|
// aggregate transaction rolls this back if a later precondition fails.
|
|
@@ -2470,7 +3082,7 @@ function dispatchWork(args, store, options) {
|
|
|
2470
3082
|
laneId,
|
|
2471
3083
|
managedWorkspace: laneWorkspace,
|
|
2472
3084
|
effective,
|
|
2473
|
-
|
|
3085
|
+
runId: tx.nextRunId(task.id),
|
|
2474
3086
|
dispatchMode: roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(task.id, role.name), effective.agentId, effective)
|
|
2475
3087
|
};
|
|
2476
3088
|
});
|
|
@@ -2503,15 +3115,15 @@ function dispatchWork(args, store, options) {
|
|
|
2503
3115
|
root: plan.managedWorkspace.root,
|
|
2504
3116
|
writableProjectIds: [...item.writeProjectIds]
|
|
2505
3117
|
},
|
|
2506
|
-
|
|
3118
|
+
currentRunId: plan.runId
|
|
2507
3119
|
})), now);
|
|
2508
3120
|
workItemForDispatch = attachWorkItemExecutionGroup(workItemForDispatch, group, now);
|
|
2509
|
-
if (workItemForDispatch.status !== "
|
|
2510
|
-
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "
|
|
3121
|
+
if (workItemForDispatch.status !== "open") {
|
|
3122
|
+
workItemForDispatch = updateWorkItemStatus(workItemForDispatch, "open", now);
|
|
2511
3123
|
}
|
|
2512
3124
|
tx.saveWorkItem(task.id, workItemForDispatch);
|
|
2513
|
-
const
|
|
2514
|
-
const
|
|
3125
|
+
const runs = plans.map((plan, index) => {
|
|
3126
|
+
const run = createRun(plan.runId, task.id, plan.role.name, plan.dispatchMode, createRunInput({
|
|
2515
3127
|
source: { type: "yui", channel: "workitem-dispatch" },
|
|
2516
3128
|
directive: assignment.input,
|
|
2517
3129
|
deltaRefIds: []
|
|
@@ -2522,37 +3134,37 @@ function dispatchWork(args, store, options) {
|
|
|
2522
3134
|
workspace: plan.managedWorkspace,
|
|
2523
3135
|
effective: plan.effective
|
|
2524
3136
|
});
|
|
2525
|
-
const snapshot =
|
|
3137
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
2526
3138
|
taskId: task.id,
|
|
2527
|
-
roleName:
|
|
3139
|
+
roleName: run.roleName,
|
|
2528
3140
|
purpose: "execution",
|
|
2529
3141
|
workItemId: item.id
|
|
2530
3142
|
}, now, "controller", assignment.contextSnapshotRef);
|
|
2531
|
-
const withContext =
|
|
3143
|
+
const withContext = withRunContextSnapshot(run, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
2532
3144
|
const prepared = options.executionLaneWorkspaces?.get(group.lanes[index].id);
|
|
2533
3145
|
if (prepared !== undefined && tx.getManagedWorkspace(prepared.owner) === null) {
|
|
2534
3146
|
tx.saveManagedWorkspace(prepared);
|
|
2535
3147
|
}
|
|
2536
|
-
tx.
|
|
2537
|
-
tx.
|
|
2538
|
-
|
|
3148
|
+
tx.saveRun(withContext);
|
|
3149
|
+
tx.saveActiveRun(withContext);
|
|
3150
|
+
enqueueRoleRunDispatch(tx, {
|
|
2539
3151
|
taskId: task.id,
|
|
2540
3152
|
roleName: plan.role.name,
|
|
2541
|
-
|
|
3153
|
+
runId: withContext.id,
|
|
2542
3154
|
reason: "turn-dispatched",
|
|
2543
3155
|
occurredAt: now
|
|
2544
3156
|
});
|
|
2545
|
-
recordTaskEvent(tx, task.id, "
|
|
3157
|
+
recordTaskEvent(tx, task.id, "run.dispatched", runLaunchEventPayload(withContext), now);
|
|
2546
3158
|
return withContext;
|
|
2547
3159
|
});
|
|
2548
|
-
return { kind: "replicated",
|
|
3160
|
+
return { kind: "replicated", runs };
|
|
2549
3161
|
});
|
|
2550
|
-
for (const
|
|
2551
|
-
notifyMailbox(options.runtime, roleMailbox(
|
|
3162
|
+
for (const run of dispatch.runs) {
|
|
3163
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
2552
3164
|
}
|
|
2553
3165
|
return dispatch.kind === "direct"
|
|
2554
|
-
? `Direct WorkItem
|
|
2555
|
-
: `Dispatch queued for ${dispatch.
|
|
3166
|
+
? `Direct WorkItem AgentRun queued as ${dispatch.runs[0].id}\n`
|
|
3167
|
+
: `Dispatch queued for ${dispatch.runs.length} replicated Lanes\n`;
|
|
2556
3168
|
}
|
|
2557
3169
|
function replicatedProducerAssignmentInput(input, requiresCodeRef) {
|
|
2558
3170
|
return [
|
|
@@ -2568,7 +3180,7 @@ function replicatedProducerAssignmentInput(input, requiresCodeRef) {
|
|
|
2568
3180
|
}
|
|
2569
3181
|
function acceptWork(args, store, options) {
|
|
2570
3182
|
const usage = "Task work accept usage: yui task work accept <task>/<work> --summary <text>.";
|
|
2571
|
-
const parsed = parseTail(args, new Set(["--summary"]), usage);
|
|
3183
|
+
const parsed = parseTail(args, new Set(["--summary", "--candidate"]), usage);
|
|
2572
3184
|
exactPositionals(parsed.positionals, 1, usage);
|
|
2573
3185
|
const summary = requiredOption(parsed.options, "--summary");
|
|
2574
3186
|
const now = clock(options);
|
|
@@ -2579,17 +3191,23 @@ function acceptWork(args, store, options) {
|
|
|
2579
3191
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2580
3192
|
}
|
|
2581
3193
|
const actor = taskActor(tx, options, task.id);
|
|
2582
|
-
if (item.status !== "
|
|
3194
|
+
if ((item.status !== "open" || item.candidates.length === 0)) {
|
|
2583
3195
|
throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
|
|
2584
3196
|
}
|
|
2585
3197
|
if (options.workItemIntegrationProof?.workspace.owner.type === "review-round") {
|
|
2586
3198
|
throw usageError("A ReviewRound-owned workspace cannot be used for WorkItem acceptance.");
|
|
2587
3199
|
}
|
|
2588
|
-
const
|
|
3200
|
+
const candidateId = parsed.options.get("--candidate");
|
|
3201
|
+
const candidate = candidateId === undefined ? requireWorkItemCandidate(item)
|
|
3202
|
+
: item.candidates.find(({ id }) => id === candidateId);
|
|
3203
|
+
if (candidate === undefined)
|
|
3204
|
+
throw usageError(`Work Item Candidate not found: ${candidateId}.`);
|
|
3205
|
+
// A Candidate's artifact references are commit-pinned (git:<commit>:<path>):
|
|
3206
|
+
// the commit self-certifies the frozen bytes, so they cannot drift and are
|
|
3207
|
+
// re-validated for shape whenever the Candidate is loaded. Their bytes are
|
|
3208
|
+
// resolved lazily on the async read/context path, never re-derived from a DB
|
|
3209
|
+
// mirror here.
|
|
2589
3210
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
2590
|
-
if (!sameTaskFinalReviewContract(candidate.taskFinalReviewContract, taskFinalContract)) {
|
|
2591
|
-
throw usageError(`Task final-review contract does not match Candidate ${candidate.id}.`);
|
|
2592
|
-
}
|
|
2593
3211
|
const latestReview = reviewRoundsByIdentity(tx.listReviewRounds(item.taskId)
|
|
2594
3212
|
.filter((round) => round.workItemId === item.id
|
|
2595
3213
|
&& round.candidateId === candidate.id)).at(-1);
|
|
@@ -2617,17 +3235,25 @@ function acceptWork(args, store, options) {
|
|
|
2617
3235
|
&& isolatedWorkspace.owner.workItemId === item.id
|
|
2618
3236
|
&& isolatedWorkspace.entries.some(({ access }) => access === "write")) {
|
|
2619
3237
|
assertWorkItemIntegrationProof(tx, item.id, item.assignee, isolatedWorkspace, options.workItemIntegrationProof);
|
|
3238
|
+
for (const proof of options.workItemIntegrationProof.projects) {
|
|
3239
|
+
const candidateCommit = candidate.gitSnapshot?.projects
|
|
3240
|
+
.find(({ projectId }) => projectId === proof.projectId)?.commit;
|
|
3241
|
+
if (candidateCommit === undefined || candidateCommit !== proof.headCommit) {
|
|
3242
|
+
throw usageError(`Selected Candidate ${candidate.id} does not match the integrated result for ${proof.projectId}.`);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
2620
3245
|
}
|
|
2621
|
-
const completed = updateWorkItemStatus(item, "
|
|
3246
|
+
const completed = updateWorkItemStatus(item, "accepted", now, summary, candidate.id);
|
|
2622
3247
|
tx.saveWorkItem(item.taskId, completed);
|
|
2623
3248
|
recordTaskEvent(tx, item.taskId, "work.accepted", {
|
|
2624
3249
|
workItemId: item.id,
|
|
2625
3250
|
candidateId: candidate.id,
|
|
2626
|
-
...(candidate.source.type === "
|
|
2627
|
-
? {
|
|
3251
|
+
...(candidate.source.type === "run"
|
|
3252
|
+
? { runId: candidate.source.runId }
|
|
2628
3253
|
: { workItemRevision: String(candidate.workItemRevision) }),
|
|
2629
3254
|
acceptedBy: actor,
|
|
2630
3255
|
summary,
|
|
3256
|
+
...(latestReview === undefined ? {} : { reviewRoundId: latestReview.id }),
|
|
2631
3257
|
...(actor === "leader" ? leaderActionEventPayload(tx, item.taskId, options) : {})
|
|
2632
3258
|
}, now);
|
|
2633
3259
|
return completed;
|
|
@@ -2673,7 +3299,7 @@ function rejectWork(args, store, options) {
|
|
|
2673
3299
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2674
3300
|
}
|
|
2675
3301
|
const actor = taskActor(tx, options, task.id);
|
|
2676
|
-
if (item.status
|
|
3302
|
+
if (item.status === "retired" || item.candidates.length === 0) {
|
|
2677
3303
|
throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
|
|
2678
3304
|
}
|
|
2679
3305
|
const candidate = requireWorkItemCandidate(item);
|
|
@@ -2681,7 +3307,7 @@ function rejectWork(args, store, options) {
|
|
|
2681
3307
|
if (activeReview !== undefined) {
|
|
2682
3308
|
throw usageError(`ReviewRound is still active: ${activeReview.id}/${activeReview.status}.`);
|
|
2683
3309
|
}
|
|
2684
|
-
const failed = updateWorkItemStatus(item, "
|
|
3310
|
+
const { currentCandidateId: _declinedCandidate, ...failed } = updateWorkItemStatus(item, "open", now);
|
|
2685
3311
|
tx.saveWorkItem(item.taskId, failed);
|
|
2686
3312
|
recordTaskEvent(tx, item.taskId, "work.rejected", {
|
|
2687
3313
|
workItemId: item.id,
|
|
@@ -2735,12 +3361,12 @@ function retireWork(args, store, options) {
|
|
|
2735
3361
|
+ `${activeWorkItemJob.id}/${activeWorkItemJob.status}. `
|
|
2736
3362
|
+ "Cancel or acknowledge it before retiring.");
|
|
2737
3363
|
}
|
|
2738
|
-
for (const run of tx.
|
|
2739
|
-
const terminal =
|
|
3364
|
+
for (const run of tx.listRuns(task.id).filter((candidate) => (candidate.status === "active" && candidate.workItemId === item.id))) {
|
|
3365
|
+
const terminal = terminalizeExactTaskRun(tx, {
|
|
2740
3366
|
taskId: task.id,
|
|
2741
3367
|
roleName: run.roleName,
|
|
2742
3368
|
agentId: run.effective.agentId,
|
|
2743
|
-
|
|
3369
|
+
runId: run.id,
|
|
2744
3370
|
outcome: {
|
|
2745
3371
|
status: "failed",
|
|
2746
3372
|
diagnostic: `Work Item retired: ${summary}`,
|
|
@@ -2748,7 +3374,7 @@ function retireWork(args, store, options) {
|
|
|
2748
3374
|
}
|
|
2749
3375
|
}, now);
|
|
2750
3376
|
if (terminal.disposition !== "applied") {
|
|
2751
|
-
throw usageError(`Work Item
|
|
3377
|
+
throw usageError(`Work Item AgentRun changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
2752
3378
|
}
|
|
2753
3379
|
}
|
|
2754
3380
|
}
|
|
@@ -2785,9 +3411,9 @@ function listWork(args, store) {
|
|
|
2785
3411
|
exactPositionals(args, 1, "Task work list usage: yui task work list <task>.");
|
|
2786
3412
|
const task = requireTask(store, args[0]);
|
|
2787
3413
|
const items = store.listWorkItems(task.id);
|
|
2788
|
-
const
|
|
3414
|
+
const runs = store.listRuns(task.id);
|
|
2789
3415
|
const sessionSets = store.listRoleSessionSets(task.id);
|
|
2790
|
-
const executions = items.map((item) => projectWorkItemExecution(item,
|
|
3416
|
+
const executions = items.map((item) => projectWorkItemExecution(item, runs, sessionSets, store));
|
|
2791
3417
|
const rendered = items.length === 0
|
|
2792
3418
|
? "No work items found.\n"
|
|
2793
3419
|
: `${renderTable(`Task work: ${task.id}`, [
|
|
@@ -2814,7 +3440,7 @@ function listWork(args, store) {
|
|
|
2814
3440
|
function showWork(args, store, options) {
|
|
2815
3441
|
exactPositionals(args, 1, "Task work show usage: yui task work show <work>.");
|
|
2816
3442
|
const item = requireWorkItem(store, args[0], options);
|
|
2817
|
-
const execution = projectWorkItemExecution(item, store.
|
|
3443
|
+
const execution = projectWorkItemExecution(item, store.listRuns(item.taskId), store.listRoleSessionSets(item.taskId), store);
|
|
2818
3444
|
const replacement = item.disposition?.replacementWorkItemId;
|
|
2819
3445
|
const rendered = [
|
|
2820
3446
|
`Work Item: ${item.id}`,
|
|
@@ -2835,7 +3461,7 @@ function showWork(args, store, options) {
|
|
|
2835
3461
|
}
|
|
2836
3462
|
function compactWorkItemExecution(projection) {
|
|
2837
3463
|
if (projection.shape === "direct")
|
|
2838
|
-
return `main=${projection.
|
|
3464
|
+
return `main=${projection.mainRun.status}`;
|
|
2839
3465
|
const counts = projection.laneCounts;
|
|
2840
3466
|
return `lanes ${counts.running}/${counts.succeeded}/${counts.needsAttention}/${counts.failed}/${counts.unknown}; ${projection.synthesis.status}`;
|
|
2841
3467
|
}
|
|
@@ -2848,16 +3474,16 @@ function renderWorkItemExecutionProjection(projection) {
|
|
|
2848
3474
|
: [
|
|
2849
3475
|
`Lanes: running=${projection.laneCounts.running}, succeeded=${projection.laneCounts.succeeded}, needs-attention=${projection.laneCounts.needsAttention}, failed=${projection.laneCounts.failed}, unknown=${projection.laneCounts.unknown}`,
|
|
2850
3476
|
...projection.lanes.map((lane) => (` ${lane.laneId} (#${lane.ordinal}, ${lane.roleName}): ${lane.status}; `
|
|
2851
|
-
+ `
|
|
2852
|
-
+ `retry=${lane.
|
|
3477
|
+
+ `run=${lane.currentRunId ?? "unknown"}; session=${lane.session}; `
|
|
3478
|
+
+ `retry=${lane.retryRunId ?? "none"}; settle=${lane.settleRunId ?? "none"}`))
|
|
2853
3479
|
]),
|
|
2854
|
-
`Synthesis: ${projection.synthesis.status}; successful=${projection.synthesis.successfulLaneCount}
|
|
2855
|
-
`Main
|
|
2856
|
-
`Candidate Source: ${projection.candidate.candidateId ?? "none"} [${projection.candidate.status}]; source=${projection.candidate.sourceType ?? "unobserved"}; main=${projection.candidate.
|
|
3480
|
+
`Synthesis: ${projection.synthesis.status}; successful=${projection.synthesis.successfulLaneCount}; sources selected by Leader`,
|
|
3481
|
+
`Main AgentRun: ${projection.mainRun.runId ?? "unobserved"} [${projection.mainRun.status}]; role=${projection.mainRun.roleName ?? "unobserved"}; session=${projection.mainRun.session}; retry=${projection.mainRun.retryRunId ?? "none"}`,
|
|
3482
|
+
`Candidate Source: ${projection.candidate.candidateId ?? "none"} [${projection.candidate.status}]; source=${projection.candidate.sourceType ?? "unobserved"}; main=${projection.candidate.mainRunId ?? "unobserved"}`,
|
|
2857
3483
|
...(projection.candidate.sourceExecutionGroupId === undefined
|
|
2858
3484
|
? []
|
|
2859
3485
|
: [
|
|
2860
|
-
`Candidate Provenance: main ${projection.candidate.
|
|
3486
|
+
`Candidate Provenance: main ${projection.candidate.mainRunId ?? "unobserved"} -> group ${projection.candidate.sourceExecutionGroupId} -> ${projection.candidate.successfulLaneRuns.map(({ laneId, successfulRunId }) => `${laneId} -> ${successfulRunId}`).join(", ") || "unobserved"}`
|
|
2861
3487
|
]),
|
|
2862
3488
|
`Next Action: ${projection.nextAction.kind}; owner=${projection.nextAction.owners.join(", ") || "none"}; target=${projection.nextAction.targetIds.join(", ") || "none"}`
|
|
2863
3489
|
];
|
|
@@ -2882,7 +3508,7 @@ function reviewWork(args, store, options) {
|
|
|
2882
3508
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2883
3509
|
}
|
|
2884
3510
|
const requestedBy = taskActor(tx, options, task.id);
|
|
2885
|
-
if (item.status !== "
|
|
3511
|
+
if ((item.status !== "open" || item.candidates.length === 0)) {
|
|
2886
3512
|
throw usageError(`Work Item is not awaiting acceptance: ${item.id}/${item.status}.`);
|
|
2887
3513
|
}
|
|
2888
3514
|
const candidate = requireWorkItemCandidate(item);
|
|
@@ -2900,9 +3526,9 @@ function reviewWork(args, store, options) {
|
|
|
2900
3526
|
&& (round.status === "pending" || round.status === "running")))).at(-1);
|
|
2901
3527
|
if (activeRound !== undefined) {
|
|
2902
3528
|
const producerDispatchPending = activeRound.executionGroup?.lanes.some((lane) => (lane.disposition === "open"
|
|
2903
|
-
&& (lane.
|
|
2904
|
-
|| tx.
|
|
2905
|
-
if ((activeRound.status === "pending" && activeRound.
|
|
3529
|
+
&& (lane.currentRunId === undefined
|
|
3530
|
+
|| tx.getRun(task.id, lane.currentRunId)?.status === "failed"))) === true;
|
|
3531
|
+
if ((activeRound.status === "pending" && activeRound.reviewerRunId === undefined)
|
|
2906
3532
|
|| (activeRound.status === "running" && producerDispatchPending)) {
|
|
2907
3533
|
const persistedRoles = activeRound.executionGroup?.lanes
|
|
2908
3534
|
.map(({ roleName }) => roleName) ?? [];
|
|
@@ -2935,7 +3561,7 @@ function reviewWork(args, store, options) {
|
|
|
2935
3561
|
if (result.kind === "busy") {
|
|
2936
3562
|
const busy = result.availability;
|
|
2937
3563
|
return output(`Reviewer ${busy.reviewerRoleName} is busy (${busy.phase}`
|
|
2938
|
-
+ `${busy.
|
|
3564
|
+
+ `${busy.activeRunId === undefined ? "" : `; AgentRun ${busy.activeRunId}`}); `
|
|
2939
3565
|
+ `${busy.activeReviewRoundId === undefined
|
|
2940
3566
|
? ""
|
|
2941
3567
|
: `active ReviewRound ${busy.activeReviewRoundId}; `}`
|
|
@@ -2950,17 +3576,49 @@ function reviewWork(args, store, options) {
|
|
|
2950
3576
|
}
|
|
2951
3577
|
/**
|
|
2952
3578
|
* Task-control recovery for a failed Task-final ReviewRound that never
|
|
2953
|
-
* created a Reviewer
|
|
2954
|
-
* that command requires an exact failed
|
|
3579
|
+
* created a Reviewer AgentRun. This is deliberately separate from `task run retry`:
|
|
3580
|
+
* that command requires an exact failed AgentRun and remains the only retry
|
|
2955
3581
|
* path for a failed provider execution. Here the old terminal Round is an
|
|
2956
3582
|
* immutable anchor and one fresh Round is created only after the same frozen
|
|
2957
3583
|
* committed Integration/ChangeSet provenance and Reviewer independence fences
|
|
2958
3584
|
* pass again.
|
|
2959
3585
|
*/
|
|
3586
|
+
function synthesizeRuns(args, kind, store, options) {
|
|
3587
|
+
const subject = kind === "workItem" ? "work" : "review";
|
|
3588
|
+
const usage = `Usage: yui task ${subject} synthesize <task>/<${subject}> --source-run <task>/<run> ...`;
|
|
3589
|
+
const parsed = parseMultiValueTail(args, new Set(), new Set(["--source-run"]), usage);
|
|
3590
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
3591
|
+
const reference = taskRecordReference(parsed.positionals[0], kind, "Synthesis target", options);
|
|
3592
|
+
const sources = parsed.multiOptions.get("--source-run") ?? [];
|
|
3593
|
+
const sourceRunIds = sources.map((value) => {
|
|
3594
|
+
const source = taskRecordReference(value, "run", "Source AgentRun", options);
|
|
3595
|
+
if (source.taskId !== reference.taskId)
|
|
3596
|
+
throw usageError("Synthesis sources must belong to the same Task.");
|
|
3597
|
+
return source.localId;
|
|
3598
|
+
});
|
|
3599
|
+
const now = clock(options);
|
|
3600
|
+
const run = store.transaction((tx) => {
|
|
3601
|
+
const actor = taskActor(tx, options, reference.taskId);
|
|
3602
|
+
const created = kind === "workItem"
|
|
3603
|
+
? dispatchWorkItemSynthesis(tx, reference.taskId, reference.localId, sourceRunIds, now)
|
|
3604
|
+
: dispatchReviewSynthesis(tx, reference.taskId, reference.localId, sourceRunIds, now);
|
|
3605
|
+
recordTaskEvent(tx, reference.taskId, "run.synthesis-requested", {
|
|
3606
|
+
runId: created.id,
|
|
3607
|
+
requestedBy: actor,
|
|
3608
|
+
sourceRunIds: sourceRunIds.join(","),
|
|
3609
|
+
...(actor === "leader" ? leaderActionEventPayload(tx, reference.taskId, options) : {})
|
|
3610
|
+
}, now);
|
|
3611
|
+
return created;
|
|
3612
|
+
});
|
|
3613
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
3614
|
+
return output(`Dispatched synthesis AgentRun ${run.taskId}/${run.id}\n`, { run });
|
|
3615
|
+
}
|
|
2960
3616
|
function taskReviewCommand(args, store, options) {
|
|
2961
3617
|
const [command, ...rest] = args;
|
|
2962
3618
|
if (command === "request")
|
|
2963
3619
|
return requestTaskReviewRound(rest, store, options);
|
|
3620
|
+
if (command === "synthesize")
|
|
3621
|
+
return synthesizeRuns(rest, "reviewRound", store, options);
|
|
2964
3622
|
if (command === "retry")
|
|
2965
3623
|
return retryFailedTaskReviewRound(rest, store, options);
|
|
2966
3624
|
throw usageError(command === undefined
|
|
@@ -2968,7 +3626,7 @@ function taskReviewCommand(args, store, options) {
|
|
|
2968
3626
|
: `Unknown command: task review ${command}`);
|
|
2969
3627
|
}
|
|
2970
3628
|
function requestTaskReviewRound(args, store, options) {
|
|
2971
|
-
const usage = "Task review request usage: yui task review request <task> --role <
|
|
3629
|
+
const usage = "Task review request usage: yui task review request <task> --role <reviewer-role> "
|
|
2972
3630
|
+ "[--lane-role <producer-role> ...] [--delta-recheck].";
|
|
2973
3631
|
const parsed = parseMultiValueTail(args, new Set(["--role"]), new Set(["--lane-role"]), usage, new Set(["--delta-recheck"]));
|
|
2974
3632
|
exactPositionals(parsed.positionals, 1, usage);
|
|
@@ -2993,8 +3651,8 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
2993
3651
|
throw usageError("Delta-recheck is not supported with a Task-final review contract.");
|
|
2994
3652
|
}
|
|
2995
3653
|
}
|
|
2996
|
-
if (tx.getGlobalRole(reviewerRoleName) === null) {
|
|
2997
|
-
throw usageError(`
|
|
3654
|
+
if (tx.getRole(task.id, reviewerRoleName) === null && tx.getGlobalRole(reviewerRoleName) === null) {
|
|
3655
|
+
throw usageError(`Reviewer Role not found in this Task or global templates: ${reviewerRoleName}.`);
|
|
2998
3656
|
}
|
|
2999
3657
|
const provenance = taskReviewProvenance(tx, task, options);
|
|
3000
3658
|
const producerCollision = taskReviewProducerCollision(provenance, reviewerRoleName);
|
|
@@ -3095,7 +3753,7 @@ function requestTaskReviewRound(args, store, options) {
|
|
|
3095
3753
|
});
|
|
3096
3754
|
if ("kind" in round && round.kind === "busy") {
|
|
3097
3755
|
return output(`Reviewer ${round.reviewerRoleName} is busy (${round.phase}`
|
|
3098
|
-
+ `${round.
|
|
3756
|
+
+ `${round.activeRunId === undefined ? "" : `; AgentRun ${round.activeRunId}`}); `
|
|
3099
3757
|
+ `${round.activeReviewRoundId === undefined
|
|
3100
3758
|
? ""
|
|
3101
3759
|
: `active ReviewRound ${round.activeReviewRoundId}; `}`
|
|
@@ -3134,26 +3792,26 @@ function validateDeltaRecheckRequest(store, taskId, candidate, preflight) {
|
|
|
3134
3792
|
return preflight.record;
|
|
3135
3793
|
}
|
|
3136
3794
|
function assertTaskReviewRequestLane(store, taskId, reviewerRoleName, reusableRound) {
|
|
3137
|
-
const activePointer = store.
|
|
3795
|
+
const activePointer = store.getActiveRun(taskId, reviewerRoleName);
|
|
3138
3796
|
if (reusableRound === undefined || reusableRound.status === "completed") {
|
|
3139
3797
|
assertReviewerAvailable(store, taskId, reviewerRoleName);
|
|
3140
3798
|
return;
|
|
3141
3799
|
}
|
|
3142
3800
|
if (reusableRound.status === "pending") {
|
|
3143
3801
|
if (activePointer !== null) {
|
|
3144
|
-
throw usageError(`Reviewer Role already has an active
|
|
3802
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewerRoleName}.`);
|
|
3145
3803
|
}
|
|
3146
3804
|
assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound);
|
|
3147
3805
|
return;
|
|
3148
3806
|
}
|
|
3149
|
-
const
|
|
3150
|
-
if (
|
|
3807
|
+
const reviewerRunId = reusableRound.reviewerRunId;
|
|
3808
|
+
if (reviewerRunId === undefined
|
|
3151
3809
|
&& reusableRound.executionGroup?.lanes.some(({ disposition }) => disposition === "open")) {
|
|
3152
3810
|
assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound);
|
|
3153
3811
|
return;
|
|
3154
3812
|
}
|
|
3155
|
-
const activeMatches =
|
|
3156
|
-
&& activePointer?.id ===
|
|
3813
|
+
const activeMatches = reviewerRunId !== undefined
|
|
3814
|
+
&& activePointer?.id === reviewerRunId
|
|
3157
3815
|
&& activePointer.status === "active";
|
|
3158
3816
|
if (!activeMatches) {
|
|
3159
3817
|
throw usageError(`Existing Task-final ReviewRound ${reusableRound.id} is running without its exact Reviewer execution.`);
|
|
@@ -3168,7 +3826,7 @@ function assertReviewerAvailable(store, taskId, reviewerRoleName, reusableRound)
|
|
|
3168
3826
|
return;
|
|
3169
3827
|
}
|
|
3170
3828
|
throw usageError(`Reviewer ${reviewerRoleName} is busy (${availability.phase}`
|
|
3171
|
-
+ `${availability.
|
|
3829
|
+
+ `${availability.activeRunId === undefined ? "" : `; AgentRun ${availability.activeRunId}`}`
|
|
3172
3830
|
+ `${availability.activeReviewRoundId === undefined
|
|
3173
3831
|
? ""
|
|
3174
3832
|
: `; ReviewRound ${availability.activeReviewRoundId}`}).`);
|
|
@@ -3178,10 +3836,10 @@ function reviewerBusyBelongsToRound(busy, round) {
|
|
|
3178
3836
|
return false;
|
|
3179
3837
|
if (busy.phase === "review-slot")
|
|
3180
3838
|
return true;
|
|
3181
|
-
if (busy.phase !== "active-turn" || busy.
|
|
3839
|
+
if (busy.phase !== "active-turn" || busy.activeRunId === undefined)
|
|
3182
3840
|
return false;
|
|
3183
|
-
return round.
|
|
3184
|
-
|| round.executionGroup?.lanes.some(({
|
|
3841
|
+
return round.reviewerRunId === busy.activeRunId
|
|
3842
|
+
|| round.executionGroup?.lanes.some(({ currentRunId }) => (currentRunId === busy.activeRunId)) === true;
|
|
3185
3843
|
}
|
|
3186
3844
|
function retryFailedTaskReviewRound(args, store, options) {
|
|
3187
3845
|
exactPositionals(args, 1, "Task review retry usage: yui task review retry <task>/<review-round>.");
|
|
@@ -3199,11 +3857,11 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3199
3857
|
if ((round.scope ?? "work-item") !== "task") {
|
|
3200
3858
|
throw usageError(`ReviewRound ${round.id} is not a failed Task-final ReviewRound.`);
|
|
3201
3859
|
}
|
|
3202
|
-
if (round.
|
|
3860
|
+
if (round.reviewerRunId !== undefined) {
|
|
3203
3861
|
if (round.status === "completed") {
|
|
3204
3862
|
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
3205
3863
|
}
|
|
3206
|
-
throw usageError(`ReviewRound ${round.id} has Reviewer
|
|
3864
|
+
throw usageError(`ReviewRound ${round.id} has Reviewer AgentRun ${round.reviewerRunId}; use task run retry instead.`);
|
|
3207
3865
|
}
|
|
3208
3866
|
if (round.status !== "failed" && round.status !== "pending") {
|
|
3209
3867
|
throw usageError(`ReviewRound ${round.id} is not retryable from ${round.status}.`);
|
|
@@ -3243,9 +3901,9 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3243
3901
|
reviewer = createTaskRole(tx, task, round.reviewerRoleName, undefined, now, round.reviewerRoleName);
|
|
3244
3902
|
tx.saveRole(task.id, reviewer);
|
|
3245
3903
|
}
|
|
3246
|
-
const activePointer = tx.
|
|
3904
|
+
const activePointer = tx.getActiveRun(task.id, reviewer.name);
|
|
3247
3905
|
if (activePointer !== null) {
|
|
3248
|
-
throw usageError(`Reviewer Role already has an active
|
|
3906
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
3249
3907
|
}
|
|
3250
3908
|
assertReviewerAvailable(tx, task.id, reviewer.name, round);
|
|
3251
3909
|
// Issue 06: an already-pending Round is the idempotent retry result.
|
|
@@ -3266,50 +3924,50 @@ function retryFailedTaskReviewRound(args, store, options) {
|
|
|
3266
3924
|
? `Task-final Review retry requested as ${result.round.id}\n`
|
|
3267
3925
|
: `Task-final Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round });
|
|
3268
3926
|
}
|
|
3269
|
-
function
|
|
3927
|
+
function taskRunCommand(args, store, options) {
|
|
3270
3928
|
const [command, ...rest] = args;
|
|
3271
3929
|
if (command === "list")
|
|
3272
|
-
return output(
|
|
3930
|
+
return output(listRuns(rest, store, options));
|
|
3273
3931
|
if (command === "show")
|
|
3274
|
-
return
|
|
3932
|
+
return showRun(rest, store, options);
|
|
3275
3933
|
if (command === "context")
|
|
3276
|
-
return
|
|
3934
|
+
return runContextCommand(rest, store, options);
|
|
3277
3935
|
if (command === "retry")
|
|
3278
|
-
return
|
|
3936
|
+
return retryRun(rest, store, options);
|
|
3279
3937
|
if (command === "settle")
|
|
3280
|
-
return
|
|
3938
|
+
return settleRun(rest, store, options);
|
|
3281
3939
|
if (command === "checkpoint")
|
|
3282
|
-
return output(
|
|
3940
|
+
return output(checkpointRun(rest, store, options));
|
|
3283
3941
|
if (command === "retire")
|
|
3284
|
-
return
|
|
3942
|
+
return retireRun(rest, store, options);
|
|
3285
3943
|
throw usageError(command === undefined
|
|
3286
3944
|
? "Task turn command is required."
|
|
3287
|
-
: `Unknown command: task
|
|
3945
|
+
: `Unknown command: task run ${command}`);
|
|
3288
3946
|
}
|
|
3289
|
-
function
|
|
3290
|
-
exactPositionals(args, 1, "Task turn settle usage: yui task
|
|
3291
|
-
const previous = store.transaction((tx) =>
|
|
3947
|
+
function settleRun(args, store, options) {
|
|
3948
|
+
exactPositionals(args, 1, "Task turn settle usage: yui task run settle <task>/<run>.");
|
|
3949
|
+
const previous = store.transaction((tx) => requireRun(tx, args[0], options));
|
|
3292
3950
|
if (previous.purpose === "review") {
|
|
3293
3951
|
if (previous.executionGroupId !== undefined
|
|
3294
3952
|
&& previous.executionLaneId !== undefined) {
|
|
3295
|
-
return
|
|
3953
|
+
return settleFailedReviewExecutionLaneRun(previous, store, options);
|
|
3296
3954
|
}
|
|
3297
|
-
return
|
|
3955
|
+
return settleStaleFinalReviewRun(args, store, options);
|
|
3298
3956
|
}
|
|
3299
|
-
return
|
|
3957
|
+
return settleFailedExecutionLaneRun(previous, store, options);
|
|
3300
3958
|
}
|
|
3301
|
-
function
|
|
3959
|
+
function settleFailedReviewExecutionLaneRun(previous, store, options) {
|
|
3302
3960
|
const now = clock(options);
|
|
3303
3961
|
const result = store.transaction((tx) => {
|
|
3304
|
-
const run = tx.
|
|
3962
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
3305
3963
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
3306
|
-
throw usageError(`
|
|
3964
|
+
throw usageError(`AgentRun ${previous.id} is not a failed review AgentRun.`);
|
|
3307
3965
|
}
|
|
3308
3966
|
if (run.reviewRoundId === undefined
|
|
3309
3967
|
|| run.executionGroupId === undefined
|
|
3310
3968
|
|| run.executionLaneId === undefined
|
|
3311
3969
|
|| run.sourceExecutionGroupId !== undefined) {
|
|
3312
|
-
throw usageError(`
|
|
3970
|
+
throw usageError(`AgentRun ${run.id} is not a failed Review Producer Lane AgentRun.`);
|
|
3313
3971
|
}
|
|
3314
3972
|
const task = requireTask(tx, run.taskId);
|
|
3315
3973
|
if (task.status !== "active")
|
|
@@ -3317,80 +3975,79 @@ function settleFailedReviewExecutionLaneTurn(previous, store, options) {
|
|
|
3317
3975
|
const actor = taskActor(tx, options, task.id);
|
|
3318
3976
|
const round = tx.getReviewRound(task.id, run.reviewRoundId);
|
|
3319
3977
|
if (round === null) {
|
|
3320
|
-
throw dataError(`ReviewRound not found for
|
|
3978
|
+
throw dataError(`ReviewRound not found for AgentRun ${run.id}: ${run.reviewRoundId}.`);
|
|
3321
3979
|
}
|
|
3322
3980
|
const group = round.executionGroup;
|
|
3323
3981
|
if (group === undefined || group.id !== run.executionGroupId) {
|
|
3324
|
-
throw usageError(`
|
|
3982
|
+
throw usageError(`AgentRun ${run.id} no longer belongs to the Review ExecutionGroup.`);
|
|
3325
3983
|
}
|
|
3326
3984
|
const lane = group.lanes.find(({ id }) => id === run.executionLaneId);
|
|
3327
|
-
if (lane === undefined || lane.
|
|
3328
|
-
throw usageError(`
|
|
3985
|
+
if (lane === undefined || lane.currentRunId !== run.id || lane.roleName !== run.roleName) {
|
|
3986
|
+
throw usageError(`AgentRun ${run.id} no longer owns its Review Producer Lane.`);
|
|
3329
3987
|
}
|
|
3330
3988
|
if (lane.disposition === "failed") {
|
|
3331
3989
|
return {
|
|
3332
|
-
|
|
3990
|
+
run: run,
|
|
3333
3991
|
reviewRound: round,
|
|
3334
3992
|
changed: false,
|
|
3335
|
-
|
|
3993
|
+
mainRuns: []
|
|
3336
3994
|
};
|
|
3337
3995
|
}
|
|
3338
3996
|
if (round.status !== "running" || lane.disposition !== "open") {
|
|
3339
|
-
throw usageError(`
|
|
3997
|
+
throw usageError(`AgentRun ${run.id} cannot settle ${round.id}/${group.id}/${lane.id} from `
|
|
3340
3998
|
+ `${round.status}/${lane.disposition}.`);
|
|
3341
3999
|
}
|
|
3342
|
-
const validation =
|
|
4000
|
+
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
3343
4001
|
if (validation.disposition !== "applied") {
|
|
3344
|
-
throw usageError(`Review
|
|
4002
|
+
throw usageError(`Review AgentRun ${run.id} no longer matches its frozen Review Lane: `
|
|
3345
4003
|
+ `${validation.reason ?? "mismatch"}.`);
|
|
3346
4004
|
}
|
|
3347
|
-
if (tx.
|
|
3348
|
-
throw usageError(`Review Producer Lane still has an active
|
|
4005
|
+
if (tx.getActiveExecutionLaneRun(task.id, group.id, lane.id) !== null) {
|
|
4006
|
+
throw usageError(`Review Producer Lane still has an active AgentRun: ${group.id}/${lane.id}.`);
|
|
3349
4007
|
}
|
|
3350
4008
|
const settledGroup = updateUnifiedExecutionLane(group, lane.id, {
|
|
3351
|
-
|
|
4009
|
+
currentRunId: run.id,
|
|
3352
4010
|
disposition: "failed"
|
|
3353
4011
|
}, now);
|
|
3354
4012
|
tx.saveReviewRound(task.id, updateReviewExecutionGroup(round, settledGroup));
|
|
3355
|
-
recordTaskEvent(tx, task.id, "
|
|
3356
|
-
|
|
4013
|
+
recordTaskEvent(tx, task.id, "run.review-settled", {
|
|
4014
|
+
runId: run.id,
|
|
3357
4015
|
reviewRoundId: round.id,
|
|
3358
4016
|
executionGroupId: group.id,
|
|
3359
4017
|
executionLaneId: lane.id,
|
|
3360
4018
|
settledBy: actor,
|
|
3361
4019
|
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
|
|
3362
4020
|
}, now);
|
|
3363
|
-
const reconciliation = reconcileReviewMainTurns(tx, task.id, now);
|
|
3364
4021
|
return {
|
|
3365
|
-
|
|
4022
|
+
run: run,
|
|
3366
4023
|
reviewRound: tx.getReviewRound(task.id, round.id),
|
|
3367
4024
|
changed: true,
|
|
3368
|
-
|
|
4025
|
+
mainRuns: []
|
|
3369
4026
|
};
|
|
3370
4027
|
});
|
|
3371
|
-
for (const
|
|
3372
|
-
notifyMailbox(options.runtime, roleMailbox(
|
|
4028
|
+
for (const run of result.mainRuns) {
|
|
4029
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
3373
4030
|
}
|
|
3374
4031
|
return output(result.changed
|
|
3375
|
-
? `Settled failed Review Producer Lane from
|
|
3376
|
-
: `Failed Review Producer Lane already settled from
|
|
3377
|
-
|
|
4032
|
+
? `Settled failed Review Producer Lane from AgentRun ${result.run.id}\n`
|
|
4033
|
+
: `Failed Review Producer Lane already settled from AgentRun ${result.run.id}\n`, {
|
|
4034
|
+
run: result.run,
|
|
3378
4035
|
reviewRound: result.reviewRound,
|
|
3379
|
-
...(result.
|
|
4036
|
+
...(result.mainRuns.length === 0 ? {} : { mainRuns: result.mainRuns })
|
|
3380
4037
|
});
|
|
3381
4038
|
}
|
|
3382
|
-
function
|
|
4039
|
+
function settleFailedExecutionLaneRun(previous, store, options) {
|
|
3383
4040
|
const now = clock(options);
|
|
3384
4041
|
const result = store.transaction((tx) => {
|
|
3385
|
-
const run = tx.
|
|
4042
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
3386
4043
|
if (run === null || run.status !== "failed" || run.purpose !== "execution") {
|
|
3387
|
-
throw usageError(`
|
|
4044
|
+
throw usageError(`AgentRun ${previous.id} is not a failed execution AgentRun.`);
|
|
3388
4045
|
}
|
|
3389
4046
|
if (run.workItemId === undefined
|
|
3390
4047
|
|| run.executionGroupId === undefined
|
|
3391
4048
|
|| run.executionLaneId === undefined
|
|
3392
4049
|
|| run.sourceExecutionGroupId !== undefined) {
|
|
3393
|
-
throw usageError(`
|
|
4050
|
+
throw usageError(`AgentRun ${run.id} is not a failed WorkItem Execution Lane AgentRun.`);
|
|
3394
4051
|
}
|
|
3395
4052
|
const task = requireTask(tx, run.taskId);
|
|
3396
4053
|
if (task.status !== "active")
|
|
@@ -3398,94 +4055,88 @@ function settleFailedExecutionLaneTurn(previous, store, options) {
|
|
|
3398
4055
|
const actor = taskActor(tx, options, task.id);
|
|
3399
4056
|
const item = tx.getWorkItem(task.id, run.workItemId);
|
|
3400
4057
|
if (item === null)
|
|
3401
|
-
throw dataError(`Work item not found for
|
|
4058
|
+
throw dataError(`Work item not found for AgentRun ${run.id}: ${run.workItemId}.`);
|
|
3402
4059
|
const group = currentWorkItemExecutionGroup(item);
|
|
3403
4060
|
if (group === undefined || group.id !== run.executionGroupId) {
|
|
3404
|
-
throw usageError(`
|
|
4061
|
+
throw usageError(`AgentRun ${run.id} no longer belongs to the current ExecutionGroup.`);
|
|
3405
4062
|
}
|
|
3406
4063
|
const lane = group.lanes.find(({ id }) => id === run.executionLaneId);
|
|
3407
|
-
if (lane === undefined || lane.
|
|
3408
|
-
throw usageError(`
|
|
4064
|
+
if (lane === undefined || lane.currentRunId !== run.id) {
|
|
4065
|
+
throw usageError(`AgentRun ${run.id} no longer owns its Execution Lane.`);
|
|
3409
4066
|
}
|
|
3410
4067
|
if (lane.disposition === "failed") {
|
|
3411
4068
|
return {
|
|
3412
|
-
|
|
4069
|
+
run: run,
|
|
3413
4070
|
workItem: item,
|
|
3414
4071
|
changed: false,
|
|
3415
|
-
|
|
4072
|
+
mainRuns: []
|
|
3416
4073
|
};
|
|
3417
4074
|
}
|
|
3418
|
-
if (item.status !== "
|
|
3419
|
-
throw usageError(`
|
|
4075
|
+
if (item.status !== "open" || lane.disposition !== "open") {
|
|
4076
|
+
throw usageError(`AgentRun ${run.id} cannot settle ${item.id}/${group.id}/${lane.id} from `
|
|
3420
4077
|
+ `${item.status}/${lane.disposition}.`);
|
|
3421
4078
|
}
|
|
3422
|
-
if (tx.
|
|
3423
|
-
throw usageError(`Execution Lane still has an active
|
|
4079
|
+
if (tx.getActiveExecutionLaneRun(task.id, group.id, lane.id) !== null) {
|
|
4080
|
+
throw usageError(`Execution Lane still has an active AgentRun: ${group.id}/${lane.id}.`);
|
|
3424
4081
|
}
|
|
3425
4082
|
const settledGroup = updateWorkItemExecutionLane(group, lane.id, {
|
|
3426
|
-
|
|
4083
|
+
currentRunId: run.id,
|
|
3427
4084
|
disposition: "failed"
|
|
3428
4085
|
}, now);
|
|
3429
4086
|
const settledItem = updateWorkItemExecutionGroup(item, settledGroup, now);
|
|
3430
4087
|
tx.saveWorkItem(task.id, settledItem);
|
|
3431
|
-
recordTaskEvent(tx, task.id, "
|
|
3432
|
-
|
|
4088
|
+
recordTaskEvent(tx, task.id, "run.execution-settled", {
|
|
4089
|
+
runId: run.id,
|
|
3433
4090
|
workItemId: item.id,
|
|
3434
4091
|
executionGroupId: group.id,
|
|
3435
4092
|
executionLaneId: lane.id,
|
|
3436
4093
|
settledBy: actor,
|
|
3437
4094
|
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
|
|
3438
4095
|
}, now);
|
|
3439
|
-
const reconciliation = reconcileWorkItemMainTurns(tx, task.id, now);
|
|
3440
4096
|
return {
|
|
3441
|
-
|
|
4097
|
+
run: run,
|
|
3442
4098
|
workItem: tx.getWorkItem(task.id, item.id) ?? settledItem,
|
|
3443
4099
|
changed: true,
|
|
3444
|
-
|
|
4100
|
+
mainRuns: []
|
|
3445
4101
|
};
|
|
3446
4102
|
});
|
|
3447
|
-
for (const
|
|
3448
|
-
notifyMailbox(options.runtime, roleMailbox(
|
|
4103
|
+
for (const run of result.mainRuns) {
|
|
4104
|
+
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
3449
4105
|
}
|
|
3450
4106
|
return output(result.changed
|
|
3451
|
-
? `Settled failed Execution Lane from
|
|
3452
|
-
: `Failed Execution Lane already settled from
|
|
3453
|
-
|
|
4107
|
+
? `Settled failed Execution Lane from AgentRun ${result.run.id}\n`
|
|
4108
|
+
: `Failed Execution Lane already settled from AgentRun ${result.run.id}\n`, {
|
|
4109
|
+
run: result.run,
|
|
3454
4110
|
workItem: result.workItem,
|
|
3455
|
-
...(result.
|
|
4111
|
+
...(result.mainRuns.length === 0 ? {} : { mainRuns: result.mainRuns })
|
|
3456
4112
|
});
|
|
3457
4113
|
}
|
|
3458
|
-
function
|
|
3459
|
-
const usage = "Task turn retire usage: yui task
|
|
4114
|
+
function retireRun(args, store, options) {
|
|
4115
|
+
const usage = "Task turn retire usage: yui task run retire <task>/<run> --reason <text> [--expected-progress-at <timestamp>] [--agent-id <id>] [--adapter-id <id>] [--native-session-id <id>].";
|
|
3460
4116
|
const parsed = parseTail(args, new Set([
|
|
3461
4117
|
"--reason",
|
|
3462
4118
|
"--expected-progress-at",
|
|
3463
4119
|
"--progress-at",
|
|
3464
4120
|
"--agent-id",
|
|
3465
4121
|
"--adapter-id",
|
|
3466
|
-
"--native-session-id"
|
|
3467
|
-
"--launch-id"
|
|
4122
|
+
"--native-session-id"
|
|
3468
4123
|
]), usage);
|
|
3469
4124
|
exactPositionals(parsed.positionals, 1, usage);
|
|
3470
4125
|
const reason = requiredOption(parsed.options, "--reason");
|
|
3471
|
-
const reference = taskRecordReference(parsed.positionals[0], "
|
|
4126
|
+
const reference = taskRecordReference(parsed.positionals[0], "run", "AgentRun reference", options);
|
|
3472
4127
|
const now = clock(options);
|
|
3473
4128
|
const result = store.transaction((tx) => {
|
|
3474
4129
|
const task = requireTask(tx, reference.taskId);
|
|
3475
4130
|
assertTaskOpen(task);
|
|
3476
4131
|
const actor = taskActor(tx, options, task.id);
|
|
3477
|
-
let run = tx.
|
|
4132
|
+
let run = tx.getRun(task.id, reference.localId);
|
|
3478
4133
|
if (run === null)
|
|
3479
|
-
throw dataError(`
|
|
4134
|
+
throw dataError(`AgentRun not found: ${task.id}/${reference.localId}.`);
|
|
3480
4135
|
const events = tx.listEvents(task.id);
|
|
3481
|
-
if (isTaskRecordRetired(events, "
|
|
3482
|
-
return { task,
|
|
4136
|
+
if (isTaskRecordRetired(events, "run", run.id)) {
|
|
4137
|
+
return { task, run: run, changed: false };
|
|
3483
4138
|
}
|
|
3484
4139
|
if (run.status === "active") {
|
|
3485
|
-
if (actor === "leader"
|
|
3486
|
-
&& taskLeaderActionTurnId(tx, task.id, options.environment) === run.id) {
|
|
3487
|
-
throw usageError("A Task Leader cannot retire its own current authority Turn.", usage);
|
|
3488
|
-
}
|
|
3489
4140
|
const expectedProgressAt = requiredOption(parsed.options, parsed.options.has("--expected-progress-at")
|
|
3490
4141
|
? "--expected-progress-at"
|
|
3491
4142
|
: "--progress-at");
|
|
@@ -3495,35 +4146,30 @@ function retireTurn(args, store, options) {
|
|
|
3495
4146
|
const agentId = requiredOption(parsed.options, "--agent-id");
|
|
3496
4147
|
const adapterId = requiredOption(parsed.options, "--adapter-id");
|
|
3497
4148
|
const nativeSessionId = parsed.options.get("--native-session-id");
|
|
3498
|
-
const runtimeGenerationId = parsed.options.get("--launch-id");
|
|
3499
4149
|
const sessions = tx.getTaskRoleSessionSet(task.id, run.roleName);
|
|
3500
4150
|
const session = sessions?.sessions[run.effective.agentId];
|
|
3501
4151
|
if (session?.nativeSessionId !== undefined && nativeSessionId === undefined) {
|
|
3502
|
-
throw usageError("--native-session-id is required for this active
|
|
4152
|
+
throw usageError("--native-session-id is required for this active AgentRun.", usage);
|
|
3503
4153
|
}
|
|
3504
|
-
|
|
3505
|
-
throw usageError("--launch-id is required for an opaque active Turn.", usage);
|
|
3506
|
-
}
|
|
3507
|
-
const terminal = retireExactActiveTurn(tx, {
|
|
4154
|
+
const terminal = retireExactActiveRun(tx, {
|
|
3508
4155
|
taskId: task.id,
|
|
3509
4156
|
roleName: run.roleName,
|
|
3510
|
-
|
|
4157
|
+
runId: run.id,
|
|
3511
4158
|
agentId,
|
|
3512
4159
|
adapterId,
|
|
3513
4160
|
...(nativeSessionId === undefined ? {} : { nativeSessionId }),
|
|
3514
|
-
...(runtimeGenerationId === undefined ? {} : { runtimeGenerationId }),
|
|
3515
4161
|
expectedProgressAt,
|
|
3516
|
-
reason: `
|
|
4162
|
+
reason: `AgentRun retired: ${reason}`
|
|
3517
4163
|
}, now);
|
|
3518
|
-
if (terminal.disposition !== "applied" || terminal.
|
|
4164
|
+
if (terminal.disposition !== "applied" || terminal.run === null) {
|
|
3519
4165
|
throw usageError(terminal.disposition === "blocked"
|
|
3520
|
-
? `
|
|
3521
|
-
: `
|
|
4166
|
+
? `AgentRun retirement is blocked: ${run.id}/${terminal.reason ?? "unsafe"}.`
|
|
4167
|
+
: `AgentRun changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
3522
4168
|
}
|
|
3523
|
-
run = terminal.
|
|
4169
|
+
run = terminal.run;
|
|
3524
4170
|
}
|
|
3525
|
-
recordTaskEvent(tx, task.id, "
|
|
3526
|
-
|
|
4171
|
+
recordTaskEvent(tx, task.id, "run.retired", {
|
|
4172
|
+
runId: run.id,
|
|
3527
4173
|
reason,
|
|
3528
4174
|
...(parsed.options.get("--expected-progress-at") === undefined
|
|
3529
4175
|
&& parsed.options.get("--progress-at") === undefined
|
|
@@ -3535,9 +4181,6 @@ function retireTurn(args, store, options) {
|
|
|
3535
4181
|
...(parsed.options.get("--native-session-id") === undefined
|
|
3536
4182
|
? {}
|
|
3537
4183
|
: { nativeSessionId: parsed.options.get("--native-session-id") }),
|
|
3538
|
-
...(parsed.options.get("--launch-id") === undefined
|
|
3539
|
-
? {}
|
|
3540
|
-
: { runtimeGenerationId: parsed.options.get("--launch-id") }),
|
|
3541
4184
|
...(actor === "leader"
|
|
3542
4185
|
? leaderActionEventPayload(tx, task.id, options)
|
|
3543
4186
|
: { retiredBy: actor })
|
|
@@ -3545,98 +4188,98 @@ function retireTurn(args, store, options) {
|
|
|
3545
4188
|
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
3546
4189
|
eventId: tx.nextEventId(task.id),
|
|
3547
4190
|
taskId: task.id,
|
|
3548
|
-
recordKind: "
|
|
4191
|
+
recordKind: "run",
|
|
3549
4192
|
recordId: run.id,
|
|
3550
4193
|
reason,
|
|
3551
4194
|
retiredBy: actor
|
|
3552
4195
|
}, now));
|
|
3553
|
-
return { task,
|
|
4196
|
+
return { task, run: run, changed: true };
|
|
3554
4197
|
});
|
|
3555
4198
|
if (result.changed)
|
|
3556
4199
|
options.runtime?.notifyStateChanged(result.task.id);
|
|
3557
|
-
return output(`Retired
|
|
3558
|
-
|
|
4200
|
+
return output(`Retired AgentRun ${result.task.id}/${result.run.id}\n`, {
|
|
4201
|
+
run: result.run,
|
|
3559
4202
|
retired: true
|
|
3560
4203
|
});
|
|
3561
4204
|
}
|
|
3562
|
-
function
|
|
4205
|
+
function runContextCommand(args, store, options) {
|
|
3563
4206
|
const [first, ...rest] = args;
|
|
3564
4207
|
if (first === "expand") {
|
|
3565
|
-
const usage = "Task turn context expand usage: yui task
|
|
4208
|
+
const usage = "Task turn context expand usage: yui task run context expand <task>/<run> <ref-id> [--store <store>] [--mode full].";
|
|
3566
4209
|
const parsed = parseTail(rest, new Set(["--store", "--mode"]), usage);
|
|
3567
4210
|
exactPositionals(parsed.positionals, 2, usage);
|
|
3568
4211
|
const mode = parsed.options.get("--mode");
|
|
3569
4212
|
if (mode !== undefined && mode !== "full") {
|
|
3570
|
-
throw usageError("
|
|
4213
|
+
throw usageError("AgentRun Context expansion mode must be full.", usage);
|
|
3571
4214
|
}
|
|
3572
|
-
const { taskId,
|
|
3573
|
-
|
|
3574
|
-
const expanded = store.transaction((tx) =>
|
|
4215
|
+
const { taskId, runId } = parseRunContextReference(parsed.positionals[0]);
|
|
4216
|
+
authorizeRunContext(store, taskId, runId, options.environment);
|
|
4217
|
+
const expanded = store.transaction((tx) => expandRunContextRef(tx, taskId, runId, parsed.positionals[1], optionalNonEmptyOption(parsed.options, "--store")));
|
|
3575
4218
|
return output(`${JSON.stringify(expanded, null, 2)}\n`, { context: expanded });
|
|
3576
4219
|
}
|
|
3577
4220
|
if (first === "delta") {
|
|
3578
4221
|
if (rest.length !== 3 || rest[1] !== "--after") {
|
|
3579
|
-
throw usageError("Task turn context delta usage: yui task
|
|
4222
|
+
throw usageError("Task turn context delta usage: yui task run context delta <task>/<run> --after <cursor>.");
|
|
3580
4223
|
}
|
|
3581
|
-
const { taskId,
|
|
3582
|
-
|
|
3583
|
-
const delta = store.transaction((tx) => (
|
|
4224
|
+
const { taskId, runId } = parseRunContextReference(rest[0]);
|
|
4225
|
+
authorizeRunContext(store, taskId, runId, options.environment);
|
|
4226
|
+
const delta = store.transaction((tx) => (buildRunContextDelta(tx, taskId, runId, rest[2])));
|
|
3584
4227
|
return output(`${JSON.stringify(delta, null, 2)}\n`, { contextDelta: delta });
|
|
3585
4228
|
}
|
|
3586
4229
|
if (first === undefined || rest.length !== 0) {
|
|
3587
|
-
throw usageError("Task turn context usage: yui task
|
|
4230
|
+
throw usageError("Task turn context usage: yui task run context <task>/<run>.");
|
|
3588
4231
|
}
|
|
3589
|
-
const { taskId,
|
|
3590
|
-
|
|
3591
|
-
const pack = store.transaction((tx) =>
|
|
4232
|
+
const { taskId, runId } = parseRunContextReference(first);
|
|
4233
|
+
authorizeRunContext(store, taskId, runId, options.environment);
|
|
4234
|
+
const pack = store.transaction((tx) => buildRunContextPack(tx, taskId, runId));
|
|
3592
4235
|
return output(`${JSON.stringify(pack, null, 2)}\n`, { context: pack });
|
|
3593
4236
|
}
|
|
3594
|
-
function
|
|
3595
|
-
const [taskId,
|
|
3596
|
-
if (taskId === undefined || taskId.length === 0 ||
|
|
4237
|
+
function parseRunContextReference(value) {
|
|
4238
|
+
const [taskId, runId, extra] = value.split("/");
|
|
4239
|
+
if (taskId === undefined || taskId.length === 0 || runId === undefined || runId.length === 0
|
|
3597
4240
|
|| extra !== undefined) {
|
|
3598
|
-
throw usageError(`
|
|
4241
|
+
throw usageError(`AgentRun context reference is invalid: ${value}.`);
|
|
3599
4242
|
}
|
|
3600
|
-
return { taskId,
|
|
4243
|
+
return { taskId, runId };
|
|
3601
4244
|
}
|
|
3602
4245
|
/**
|
|
3603
|
-
* A
|
|
4246
|
+
* A AgentRun Context Pack is information the Role's own runtime reads in order to
|
|
3604
4247
|
* work. Authorization is therefore scope-shaped, not schedule-shaped: the
|
|
3605
|
-
* caller must be the current runtime of the Task Role that owns the
|
|
4248
|
+
* caller must be the current runtime of the Task Role that owns the AgentRun,
|
|
3606
4249
|
* proven by its per-Session caller key against durable state.
|
|
3607
4250
|
*
|
|
3608
|
-
* It deliberately does not require the
|
|
3609
|
-
* Agent whose
|
|
4251
|
+
* It deliberately does not require the AgentRun to still be the active one. An
|
|
4252
|
+
* Agent whose AgentRun has advanced must still be able to read the context it was
|
|
3610
4253
|
* given; losing read access to its own Role's history is what forces an
|
|
3611
4254
|
* otherwise healthy Agent to stop and escalate to a human.
|
|
3612
4255
|
*/
|
|
3613
|
-
function
|
|
4256
|
+
function authorizeRunContext(store, taskId, runId, environment) {
|
|
3614
4257
|
const managed = environment?.YUI_SESSION_SCOPE !== undefined
|
|
3615
4258
|
|| environment?.YUI_TASK_ID !== undefined
|
|
3616
4259
|
|| environment?.YUI_ROLE !== undefined;
|
|
3617
4260
|
if (!managed)
|
|
3618
4261
|
return;
|
|
3619
|
-
const run = store.
|
|
4262
|
+
const run = store.getRun(taskId, runId);
|
|
3620
4263
|
if (run === null) {
|
|
3621
|
-
throw usageError(`
|
|
4264
|
+
throw usageError(`AgentRun Context access is not authorized: ${taskId}/${runId}.`);
|
|
3622
4265
|
}
|
|
3623
4266
|
if (currentManagedRuntime(store, environment, taskId, run.roleName) === undefined) {
|
|
3624
|
-
throw usageError(`
|
|
4267
|
+
throw usageError(`AgentRun Context access requires the current runtime of ${taskId}/${run.roleName}.`);
|
|
3625
4268
|
}
|
|
3626
4269
|
}
|
|
3627
|
-
function
|
|
3628
|
-
const usage = "Task turn list usage: yui task
|
|
4270
|
+
function listRuns(args, store, options) {
|
|
4271
|
+
const usage = "Task turn list usage: yui task run list <task|task/work>.";
|
|
3629
4272
|
exactPositionals(args, 1, usage);
|
|
3630
4273
|
const reference = args[0];
|
|
3631
4274
|
const task = store.getTask(reference);
|
|
3632
4275
|
const item = task === null ? requireWorkItem(store, reference, options) : null;
|
|
3633
4276
|
const taskId = task?.id ?? item.taskId;
|
|
3634
|
-
const
|
|
3635
|
-
if (
|
|
3636
|
-
return "No
|
|
4277
|
+
const runs = store.listRuns(taskId).filter((run) => (item === null || run.workItemId === item.id));
|
|
4278
|
+
if (runs.length === 0)
|
|
4279
|
+
return "No AgentRuns found.\n";
|
|
3637
4280
|
const events = store.listEvents(taskId);
|
|
3638
|
-
return `${renderTable(`
|
|
3639
|
-
{ header: "
|
|
4281
|
+
return `${renderTable(`AgentRuns: ${item?.id ?? taskId}`, [
|
|
4282
|
+
{ header: "AgentRun", minWidth: 6, maxWidth: 20 },
|
|
3640
4283
|
{ header: "Role", minWidth: 4, maxWidth: 22 },
|
|
3641
4284
|
{ header: "Subject", minWidth: 7, maxWidth: 24 },
|
|
3642
4285
|
{ header: "Purpose", minWidth: 6, maxWidth: 10 },
|
|
@@ -3647,7 +4290,7 @@ function listTurns(args, store, options) {
|
|
|
3647
4290
|
{ header: "Status", minWidth: 6, maxWidth: 12 },
|
|
3648
4291
|
{ header: "History", minWidth: 7, maxWidth: 9 },
|
|
3649
4292
|
{ header: "Summary", minWidth: 8, maxWidth: 58 }
|
|
3650
|
-
],
|
|
4293
|
+
], runs.map((run) => [
|
|
3651
4294
|
run.id,
|
|
3652
4295
|
run.roleName,
|
|
3653
4296
|
run.workItemId ?? (run.reviewRoundId === undefined ? "task" : `review:${run.reviewRoundId}`),
|
|
@@ -3657,29 +4300,29 @@ function listTurns(args, store, options) {
|
|
|
3657
4300
|
run.effective.profileAccess,
|
|
3658
4301
|
run.effective.permission.strategy,
|
|
3659
4302
|
run.status,
|
|
3660
|
-
isTaskRecordRetired(events, "
|
|
4303
|
+
isTaskRecordRetired(events, "run", run.id) ? "retired" : "active",
|
|
3661
4304
|
run.result?.output ?? run.result?.diagnostic ?? "-"
|
|
3662
4305
|
]), defaultTableWidth())}\n`;
|
|
3663
4306
|
}
|
|
3664
4307
|
/**
|
|
3665
|
-
* Settles only the known bootstrap split where a failed Task-final
|
|
4308
|
+
* Settles only the known bootstrap split where a failed Task-final AgentRun
|
|
3666
4309
|
* still owns a running ReviewRound, but the committed Task heads have moved
|
|
3667
4310
|
* on. This is deliberately narrower than retry: it cannot manufacture a
|
|
3668
4311
|
* review or fail an arbitrary Round, and every identity/mailbox fence is
|
|
3669
4312
|
* checked before the old Round changes. The next normal Task completion then
|
|
3670
4313
|
* creates one fresh Round over the newer frozen Task heads.
|
|
3671
4314
|
*/
|
|
3672
|
-
function
|
|
3673
|
-
exactPositionals(args, 1, "Task turn settle usage: yui task
|
|
4315
|
+
function settleStaleFinalReviewRun(args, store, options) {
|
|
4316
|
+
exactPositionals(args, 1, "Task turn settle usage: yui task run settle <task>/<run>.");
|
|
3674
4317
|
const now = clock(options);
|
|
3675
|
-
const previous = store.transaction((tx) =>
|
|
4318
|
+
const previous = store.transaction((tx) => requireRun(tx, args[0], options));
|
|
3676
4319
|
const result = store.transaction((tx) => {
|
|
3677
|
-
const run = tx.
|
|
4320
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
3678
4321
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
3679
|
-
throw usageError(`
|
|
4322
|
+
throw usageError(`AgentRun ${previous.id} is not a failed review AgentRun.`);
|
|
3680
4323
|
}
|
|
3681
4324
|
if (run.reviewRoundId === undefined) {
|
|
3682
|
-
throw usageError(`Review
|
|
4325
|
+
throw usageError(`Review AgentRun ${run.id} has no ReviewRound.`);
|
|
3683
4326
|
}
|
|
3684
4327
|
const task = requireTask(tx, run.taskId);
|
|
3685
4328
|
if (task.status !== "active")
|
|
@@ -3687,26 +4330,26 @@ function settleStaleFinalReviewTurn(args, store, options) {
|
|
|
3687
4330
|
const actor = taskActor(tx, options, task.id);
|
|
3688
4331
|
const round = tx.getReviewRound(task.id, run.reviewRoundId);
|
|
3689
4332
|
if (round === null) {
|
|
3690
|
-
throw dataError(`ReviewRound not found for
|
|
4333
|
+
throw dataError(`ReviewRound not found for AgentRun ${run.id}: ${run.reviewRoundId}.`);
|
|
3691
4334
|
}
|
|
3692
4335
|
if ((round.scope ?? "work-item") !== "task") {
|
|
3693
|
-
throw usageError(`Review
|
|
4336
|
+
throw usageError(`Review AgentRun ${run.id} is not a Task-final review; request a new WorkItem review `
|
|
3694
4337
|
+ "for a new Candidate.");
|
|
3695
4338
|
}
|
|
3696
4339
|
const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
|
|
3697
4340
|
if (!sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)) {
|
|
3698
4341
|
throw usageError(`Task final-review contract does not match ReviewRound ${round.id}.`);
|
|
3699
4342
|
}
|
|
3700
|
-
// This read-only compare-and-swap fence covers the exact
|
|
4343
|
+
// This read-only compare-and-swap fence covers the exact AgentRun/Round,
|
|
3701
4344
|
// Candidate, stored Review workspace, frozen Project scope, and frozen
|
|
3702
4345
|
// Project heads before any mailbox or Round write.
|
|
3703
|
-
const validation =
|
|
4346
|
+
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
3704
4347
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
3705
|
-
throw usageError(`Review
|
|
4348
|
+
throw usageError(`Review AgentRun ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
3706
4349
|
}
|
|
3707
|
-
const
|
|
3708
|
-
if (
|
|
3709
|
-
throw usageError(`${task.id}/${round.reviewerRoleName} already has active
|
|
4350
|
+
const activeRoleRun = tx.getActiveRun(task.id, round.reviewerRoleName);
|
|
4351
|
+
if (activeRoleRun !== null) {
|
|
4352
|
+
throw usageError(`${task.id}/${round.reviewerRoleName} already has active AgentRun ${activeRoleRun.id}.`);
|
|
3710
4353
|
}
|
|
3711
4354
|
const taskRounds = reviewRoundsByIdentity(tx.listReviewRounds(task.id)
|
|
3712
4355
|
.filter((entry) => (entry.scope ?? "work-item") === "task"));
|
|
@@ -3724,7 +4367,7 @@ function settleStaleFinalReviewTurn(args, store, options) {
|
|
|
3724
4367
|
}
|
|
3725
4368
|
if (round.status === "failed") {
|
|
3726
4369
|
if (round.failure?.kind === "execution") {
|
|
3727
|
-
return {
|
|
4370
|
+
return { run: run, round, changed: false };
|
|
3728
4371
|
}
|
|
3729
4372
|
throw usageError(`Final ReviewRound is already terminal: ${round.id}/${round.status}.`);
|
|
3730
4373
|
}
|
|
@@ -3732,53 +4375,54 @@ function settleStaleFinalReviewTurn(args, store, options) {
|
|
|
3732
4375
|
throw usageError(`Final ReviewRound is not stranded running: ${round.id}/${round.status}.`);
|
|
3733
4376
|
}
|
|
3734
4377
|
assertReviewerAvailable(tx, task.id, round.reviewerRoleName, round);
|
|
3735
|
-
const summary = `Review
|
|
4378
|
+
const summary = `Review AgentRun ${run.id} failed before delivery; committed Task heads changed.`;
|
|
3736
4379
|
const terminal = finishReviewRound(round, "failed", now, { kind: "execution", message: summary });
|
|
3737
4380
|
tx.saveReviewRound(task.id, terminal);
|
|
3738
|
-
recordTaskEvent(tx, task.id, "
|
|
3739
|
-
|
|
4381
|
+
recordTaskEvent(tx, task.id, "run.review-stale-settled", {
|
|
4382
|
+
runId: run.id,
|
|
3740
4383
|
reviewRoundId: round.id,
|
|
3741
4384
|
previousTaskCandidate: JSON.stringify(round.taskCandidate),
|
|
3742
4385
|
currentTaskCandidate: JSON.stringify(currentTaskCandidate),
|
|
3743
4386
|
settledBy: actor,
|
|
3744
4387
|
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : {})
|
|
3745
4388
|
}, now);
|
|
3746
|
-
return {
|
|
4389
|
+
return { run: run, round: terminal, changed: true };
|
|
3747
4390
|
});
|
|
3748
4391
|
return output(result.changed
|
|
3749
|
-
? `Settled obsolete final Review ${result.round.id} from failed
|
|
3750
|
-
: `Obsolete final Review already settled: ${result.round.id}/${result.
|
|
4392
|
+
? `Settled obsolete final Review ${result.round.id} from failed AgentRun ${result.run.id}\n`
|
|
4393
|
+
: `Obsolete final Review already settled: ${result.round.id}/${result.run.id}\n`, { reviewRound: result.round, reviewRun: result.run });
|
|
3751
4394
|
}
|
|
3752
|
-
function
|
|
3753
|
-
exactPositionals(args, 1, "Task turn retry usage: yui task
|
|
4395
|
+
function retryRun(args, store, options) {
|
|
4396
|
+
exactPositionals(args, 1, "Task turn retry usage: yui task run retry <task>/<run>.");
|
|
3754
4397
|
const now = clock(options);
|
|
3755
|
-
const previous = store.transaction((tx) =>
|
|
4398
|
+
const previous = store.transaction((tx) => requireRun(tx, args[0], options));
|
|
3756
4399
|
if (previous.purpose === "review") {
|
|
3757
4400
|
return retryFailedReviewRun(previous, store, options, now);
|
|
3758
4401
|
}
|
|
3759
4402
|
const retried = store.transaction((tx) => {
|
|
3760
4403
|
if (previous.status !== "failed") {
|
|
3761
|
-
throw usageError(`
|
|
4404
|
+
throw usageError(`AgentRun ${previous.id} is not retryable from ${previous.status}.`);
|
|
3762
4405
|
}
|
|
3763
4406
|
const task = requireTask(tx, previous.taskId);
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
4407
|
+
assertTaskExecutionEnabled(task, "retrying a AgentRun");
|
|
4408
|
+
if (!runPurposeAdmitsTaskState(previous.purpose, task)) {
|
|
4409
|
+
throw usageError(`Task does not admit ${previous.purpose} retry: ${task.id}/${task.status}.`);
|
|
4410
|
+
}
|
|
3767
4411
|
const role = requireRole(tx, task.id, previous.roleName);
|
|
3768
|
-
if (tx.
|
|
3769
|
-
throw usageError(`${task.id}/${role.name} already has an active
|
|
4412
|
+
if (tx.getActiveRun(task.id, role.name) !== null) {
|
|
4413
|
+
throw usageError(`${task.id}/${role.name} already has an active run.`);
|
|
3770
4414
|
}
|
|
3771
4415
|
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
3772
4416
|
const retryItem = previous.workItemId === undefined
|
|
3773
4417
|
? null
|
|
3774
4418
|
: tx.getWorkItem(task.id, previous.workItemId);
|
|
3775
4419
|
if (previous.workItemId !== undefined && retryItem === null) {
|
|
3776
|
-
throw dataError(`Work item not found for
|
|
4420
|
+
throw dataError(`Work item not found for AgentRun ${previous.id}: ${previous.workItemId}.`);
|
|
3777
4421
|
}
|
|
3778
4422
|
const retriesSynthesisMain = previous.sourceExecutionGroupId !== undefined;
|
|
3779
4423
|
if (retriesSynthesisMain
|
|
3780
4424
|
&& (previous.executionGroupId !== undefined || previous.executionLaneId !== undefined)) {
|
|
3781
|
-
throw dataError(`
|
|
4425
|
+
throw dataError(`AgentRun ${previous.id} has conflicting execution lineage.`);
|
|
3782
4426
|
}
|
|
3783
4427
|
const retryLaneBefore = previous.executionLaneId === undefined
|
|
3784
4428
|
? undefined
|
|
@@ -3796,57 +4440,54 @@ function retryTurn(args, store, options) {
|
|
|
3796
4440
|
&& retryLaneBefore !== undefined
|
|
3797
4441
|
&& retryLaneBefore.id === previous.executionLaneId
|
|
3798
4442
|
&& retryLaneBefore.disposition === "open"
|
|
3799
|
-
&& retryLaneBefore.
|
|
4443
|
+
&& retryLaneBefore.currentRunId === previous.id);
|
|
3800
4444
|
if (!exactCurrentLane) {
|
|
3801
|
-
throw usageError(`
|
|
4445
|
+
throw usageError(`AgentRun ${previous.id} no longer owns the current failed Execution Lane.`);
|
|
3802
4446
|
}
|
|
3803
4447
|
const sourceGroup = !retriesSynthesisMain || retryItem === null
|
|
3804
4448
|
? undefined
|
|
3805
4449
|
: workItemExecutionGroupById(retryItem, previous.sourceExecutionGroupId);
|
|
3806
|
-
const
|
|
3807
|
-
?
|
|
3808
|
-
&&
|
|
3809
|
-
&&
|
|
4450
|
+
const sourceMainRuns = retriesSynthesisMain
|
|
4451
|
+
? chronologicalRuns(tx.listRuns(task.id).filter((run) => (run.purpose === "execution"
|
|
4452
|
+
&& run.workItemId === previous.workItemId
|
|
4453
|
+
&& run.sourceExecutionGroupId === previous.sourceExecutionGroupId)))
|
|
3810
4454
|
: [];
|
|
3811
4455
|
const exactSourceMain = !retriesSynthesisMain || (retryItem !== null
|
|
3812
|
-
&& retryItem.status === "
|
|
4456
|
+
&& retryItem.status === "open"
|
|
3813
4457
|
&& retryItem.assignee === previous.roleName
|
|
3814
4458
|
&& sourceGroup !== undefined
|
|
3815
4459
|
&& currentRetryGroup?.id === sourceGroup.id
|
|
3816
|
-
&&
|
|
3817
|
-
&&
|
|
3818
|
-
>= MINIMUM_WORK_ITEM_SYNTHESIS_RESULTS
|
|
3819
|
-
&& sourceMainTurns.at(-1)?.id === previous.id);
|
|
4460
|
+
&& selectedWorkItemSynthesisProducers(tx, retryItem, sourceGroup, synthesisSourceRunIds(tx, previous)).length > 0
|
|
4461
|
+
&& sourceMainRuns.at(-1)?.id === previous.id);
|
|
3820
4462
|
if (!exactSourceMain) {
|
|
3821
|
-
throw usageError(`
|
|
4463
|
+
throw usageError(`AgentRun ${previous.id} no longer owns the current WorkItem main synthesis.`);
|
|
3822
4464
|
}
|
|
3823
|
-
const
|
|
4465
|
+
const directMainRuns = retryItem === null
|
|
3824
4466
|
? []
|
|
3825
|
-
:
|
|
3826
|
-
&&
|
|
3827
|
-
&&
|
|
3828
|
-
&&
|
|
3829
|
-
&&
|
|
4467
|
+
: chronologicalRuns(tx.listRuns(task.id).filter((run) => (run.purpose === "execution"
|
|
4468
|
+
&& run.workItemId === retryItem.id
|
|
4469
|
+
&& run.executionGroupId === undefined
|
|
4470
|
+
&& run.executionLaneId === undefined
|
|
4471
|
+
&& run.sourceExecutionGroupId === undefined)));
|
|
3830
4472
|
const retriesDirectMain = retryItem !== null
|
|
3831
4473
|
&& !retriesExecutionLane
|
|
3832
4474
|
&& !retriesSynthesisMain;
|
|
3833
|
-
const exactDirectMain = !retriesDirectMain || (
|
|
4475
|
+
const exactDirectMain = !retriesDirectMain || (retryItem.status === "open"
|
|
3834
4476
|
&& retryItem.assignee === previous.roleName
|
|
3835
|
-
&&
|
|
4477
|
+
&& directMainRuns.at(-1)?.id === previous.id);
|
|
3836
4478
|
if (!exactDirectMain) {
|
|
3837
|
-
throw usageError(`
|
|
4479
|
+
throw usageError(`AgentRun ${previous.id} no longer owns the current direct WorkItem execution.`);
|
|
3838
4480
|
}
|
|
3839
|
-
const groupedRunningRetry = retryItem?.status === "
|
|
4481
|
+
const groupedRunningRetry = retryItem?.status === "open"
|
|
3840
4482
|
&& retriesExecutionLane
|
|
3841
4483
|
&& exactCurrentLane;
|
|
3842
|
-
const synthesisRunningRetry = retryItem?.status === "
|
|
4484
|
+
const synthesisRunningRetry = retryItem?.status === "open"
|
|
3843
4485
|
&& retriesSynthesisMain
|
|
3844
4486
|
&& exactSourceMain;
|
|
3845
|
-
const directRunningRetry = retryItem?.status === "
|
|
4487
|
+
const directRunningRetry = retryItem?.status === "open"
|
|
3846
4488
|
&& retriesDirectMain
|
|
3847
4489
|
&& exactDirectMain;
|
|
3848
4490
|
if (retryItem !== null
|
|
3849
|
-
&& retryItem.status !== "failed"
|
|
3850
4491
|
&& !groupedRunningRetry
|
|
3851
4492
|
&& !synthesisRunningRetry
|
|
3852
4493
|
&& !directRunningRetry) {
|
|
@@ -3873,7 +4514,7 @@ function retryTurn(args, store, options) {
|
|
|
3873
4514
|
&& (retryGroup === undefined
|
|
3874
4515
|
|| retryGroup.id !== previous.executionGroupId
|
|
3875
4516
|
|| retryLane === undefined))) {
|
|
3876
|
-
throw dataError(`
|
|
4517
|
+
throw dataError(`AgentRun ${previous.id} execution lineage no longer matches its Work Item.`);
|
|
3877
4518
|
}
|
|
3878
4519
|
const retryManagedWorkspace = retryLane === undefined ? runWorkspace : previous.workspace;
|
|
3879
4520
|
if (retryLane !== undefined) {
|
|
@@ -3893,7 +4534,7 @@ function retryTurn(args, store, options) {
|
|
|
3893
4534
|
|| !isDeepStrictEqual(writableProjectIds, [...retryLane.workspace.writableProjectIds].sort())
|
|
3894
4535
|
|| storedLaneWorkspace === null
|
|
3895
4536
|
|| !isDeepStrictEqual(storedLaneWorkspace, previous.workspace)) {
|
|
3896
|
-
throw dataError(`
|
|
4537
|
+
throw dataError(`AgentRun ${previous.id} Lane workspace is missing or has drifted.`);
|
|
3897
4538
|
}
|
|
3898
4539
|
}
|
|
3899
4540
|
if (retriesSynthesisMain) {
|
|
@@ -3910,7 +4551,7 @@ function retryTurn(args, store, options) {
|
|
|
3910
4551
|
|| currentMainWorkspace === null
|
|
3911
4552
|
|| !isDeepStrictEqual(storedMainWorkspace, previous.workspace)
|
|
3912
4553
|
|| !isDeepStrictEqual(currentMainWorkspace, previous.workspace)) {
|
|
3913
|
-
throw dataError(`
|
|
4554
|
+
throw dataError(`AgentRun ${previous.id} WorkItem main workspace is missing or has drifted.`);
|
|
3914
4555
|
}
|
|
3915
4556
|
}
|
|
3916
4557
|
if (retriesDirectMain) {
|
|
@@ -3927,20 +4568,20 @@ function retryTurn(args, store, options) {
|
|
|
3927
4568
|
|| currentDirectWorkspace === null
|
|
3928
4569
|
|| !isDeepStrictEqual(storedDirectWorkspace, previous.workspace)
|
|
3929
4570
|
|| !isDeepStrictEqual(currentDirectWorkspace, previous.workspace)) {
|
|
3930
|
-
throw dataError(`
|
|
4571
|
+
throw dataError(`AgentRun ${previous.id} direct WorkItem workspace is missing or has drifted.`);
|
|
3931
4572
|
}
|
|
3932
4573
|
}
|
|
3933
4574
|
const effective = retryLane?.effective ?? resolveEffectiveLaunch({
|
|
3934
4575
|
role,
|
|
3935
|
-
purpose:
|
|
4576
|
+
purpose: previous.purpose,
|
|
3936
4577
|
...(retryManagedWorkspace === undefined ? {} : { workspace: retryManagedWorkspace }),
|
|
3937
4578
|
...(retryItem === null ? {} : { workItemWriteProjectIds: retryItem.writeProjectIds })
|
|
3938
4579
|
});
|
|
3939
|
-
const
|
|
4580
|
+
const runId = tx.nextRunId(task.id);
|
|
3940
4581
|
const runningGroup = retryGroup === undefined || retryLane === undefined
|
|
3941
4582
|
? undefined
|
|
3942
4583
|
: updateWorkItemExecutionLane(retryGroup, retryLane.id, {
|
|
3943
|
-
|
|
4584
|
+
currentRunId: runId
|
|
3944
4585
|
}, now);
|
|
3945
4586
|
// Restart the bound lane and reopen the failed WorkItem as two ordered
|
|
3946
4587
|
// single-step record revisions, matching the dispatch path. Folding both
|
|
@@ -3949,11 +4590,7 @@ function retryTurn(args, store, options) {
|
|
|
3949
4590
|
const laneRestartedItem = retryItem === null || runningGroup === undefined
|
|
3950
4591
|
? retryItem
|
|
3951
4592
|
: updateWorkItemExecutionGroup(retryItem, runningGroup, now);
|
|
3952
|
-
const retriedItemWithGroup = laneRestartedItem
|
|
3953
|
-
? null
|
|
3954
|
-
: laneRestartedItem.status === "failed"
|
|
3955
|
-
? retryFailedWorkItem(laneRestartedItem, now)
|
|
3956
|
-
: laneRestartedItem;
|
|
4593
|
+
const retriedItemWithGroup = laneRestartedItem;
|
|
3957
4594
|
if (retriedItemWithGroup !== null) {
|
|
3958
4595
|
if (laneRestartedItem !== null && laneRestartedItem !== retryItem) {
|
|
3959
4596
|
tx.saveWorkItem(task.id, laneRestartedItem);
|
|
@@ -3965,13 +4602,13 @@ function retryTurn(args, store, options) {
|
|
|
3965
4602
|
const input = retriesSynthesisMain
|
|
3966
4603
|
? previous.inputs[0].input
|
|
3967
4604
|
: (() => {
|
|
3968
|
-
const retrySnapshot =
|
|
4605
|
+
const retrySnapshot = freezeRunContextSnapshot(tx, {
|
|
3969
4606
|
taskId: task.id,
|
|
3970
4607
|
roleName: role.name,
|
|
3971
|
-
purpose:
|
|
4608
|
+
purpose: previous.purpose,
|
|
3972
4609
|
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId })
|
|
3973
4610
|
}, now, "controller", retryGroup?.assignment.contextSnapshotRef);
|
|
3974
|
-
return
|
|
4611
|
+
return createRunInput({
|
|
3975
4612
|
source: {
|
|
3976
4613
|
type: "yui",
|
|
3977
4614
|
channel: previous.workItemId === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
@@ -3983,7 +4620,8 @@ function retryTurn(args, store, options) {
|
|
|
3983
4620
|
deltaRefIds: contextSnapshotDeltaRefIds(tx, retrySnapshot)
|
|
3984
4621
|
});
|
|
3985
4622
|
})();
|
|
3986
|
-
const created =
|
|
4623
|
+
const created = createRun(runId, task.id, role.name, roleAgentSessionResumeMode(sessions, effective.agentId, effective), input, now, {
|
|
4624
|
+
purpose: previous.purpose,
|
|
3987
4625
|
...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId }),
|
|
3988
4626
|
...(runningGroup === undefined ? {} : {
|
|
3989
4627
|
executionGroupId: runningGroup.id,
|
|
@@ -3995,8 +4633,8 @@ function retryTurn(args, store, options) {
|
|
|
3995
4633
|
...(retryManagedWorkspace === undefined ? {} : { workspace: retryManagedWorkspace }),
|
|
3996
4634
|
effective
|
|
3997
4635
|
});
|
|
3998
|
-
tx.
|
|
3999
|
-
tx.
|
|
4636
|
+
tx.saveRun(created);
|
|
4637
|
+
tx.saveActiveRun(created);
|
|
4000
4638
|
if (previous.workItemId !== undefined && retriedItemWithGroup !== null) {
|
|
4001
4639
|
const item = retriedItemWithGroup;
|
|
4002
4640
|
const workspace = tx.getWorkItemWorkspace(task.id, item.id);
|
|
@@ -4006,21 +4644,21 @@ function retryTurn(args, store, options) {
|
|
|
4006
4644
|
+ `cannot retry ${item.id}.`);
|
|
4007
4645
|
}
|
|
4008
4646
|
}
|
|
4009
|
-
|
|
4647
|
+
enqueueRoleRunDispatch(tx, {
|
|
4010
4648
|
taskId: task.id,
|
|
4011
4649
|
roleName: role.name,
|
|
4012
|
-
|
|
4650
|
+
runId: created.id,
|
|
4013
4651
|
reason: "turn-retried",
|
|
4014
4652
|
occurredAt: now
|
|
4015
4653
|
});
|
|
4016
|
-
recordTaskEvent(tx, task.id, "
|
|
4017
|
-
...
|
|
4018
|
-
|
|
4654
|
+
recordTaskEvent(tx, task.id, "run.retried", {
|
|
4655
|
+
...runLaunchEventPayload(created),
|
|
4656
|
+
previousRunId: previous.id
|
|
4019
4657
|
}, now);
|
|
4020
|
-
return { kind: "
|
|
4658
|
+
return { kind: "run", run: created };
|
|
4021
4659
|
});
|
|
4022
|
-
notifyMailbox(options.runtime, roleMailbox(retried.
|
|
4023
|
-
return output(`Retry queued as ${retried.
|
|
4660
|
+
notifyMailbox(options.runtime, roleMailbox(retried.run.taskId, retried.run.roleName), retried.run.taskId);
|
|
4661
|
+
return output(`Retry queued as ${retried.run.id} for ${retried.run.taskId}/${retried.run.roleName}\n`);
|
|
4024
4662
|
}
|
|
4025
4663
|
function actualTaskReviewCandidateForMutation(store, task, options) {
|
|
4026
4664
|
if (options.actualTaskReviewCandidate === undefined) {
|
|
@@ -4063,7 +4701,7 @@ function taskReviewProducerCollision(provenance, reviewerRoleName) {
|
|
|
4063
4701
|
*
|
|
4064
4702
|
* When `expected` is supplied this is also the final dispatch compare-and-swap
|
|
4065
4703
|
* fence: every bound Project must still point at the exact frozen physical
|
|
4066
|
-
* head. Drift fails closed before a Reviewer
|
|
4704
|
+
* head. Drift fails closed before a Reviewer AgentRun is created.
|
|
4067
4705
|
*/
|
|
4068
4706
|
function taskReviewProvenance(store, task, options, expected) {
|
|
4069
4707
|
const candidate = actualTaskReviewCandidateForMutation(store, task, options);
|
|
@@ -4118,13 +4756,13 @@ function taskReviewProvenance(store, task, options, expected) {
|
|
|
4118
4756
|
recordProducer(LEADER_ROLE, item.id);
|
|
4119
4757
|
continue;
|
|
4120
4758
|
}
|
|
4121
|
-
const sourceRun = store.
|
|
4759
|
+
const sourceRun = store.getRun(task.id, sourceCandidate.source.runId);
|
|
4122
4760
|
if (sourceRun === null
|
|
4123
4761
|
|| sourceRun.workItemId !== item.id
|
|
4124
4762
|
|| sourceRun.purpose !== "execution"
|
|
4125
4763
|
|| sourceRun.status !== "completed") {
|
|
4126
|
-
throw dataError(`Committed producer Candidate
|
|
4127
|
-
+ `${item.id}/${sourceCandidate.source.
|
|
4764
|
+
throw dataError(`Committed producer Candidate AgentRun is unavailable: `
|
|
4765
|
+
+ `${item.id}/${sourceCandidate.source.runId}.`);
|
|
4128
4766
|
}
|
|
4129
4767
|
recordProducer(sourceRun.roleName, item.id);
|
|
4130
4768
|
if (sourceRun.sourceExecutionGroupId !== undefined) {
|
|
@@ -4133,7 +4771,7 @@ function taskReviewProvenance(store, task, options, expected) {
|
|
|
4133
4771
|
throw dataError(`Committed producer ExecutionGroup is unavailable: `
|
|
4134
4772
|
+ `${item.id}/${sourceRun.sourceExecutionGroupId}.`);
|
|
4135
4773
|
}
|
|
4136
|
-
for (const producer of
|
|
4774
|
+
for (const producer of selectedWorkItemSynthesisProducers(store, item, executionGroup, synthesisSourceRunIds(store, sourceRun))) {
|
|
4137
4775
|
recordProducer(producer.roleName, item.id);
|
|
4138
4776
|
}
|
|
4139
4777
|
}
|
|
@@ -4182,7 +4820,7 @@ function queueTaskReviewRound(store, task, config, taskCandidate, options, now,
|
|
|
4182
4820
|
const availability = projectReviewerAvailability(store, task.id, config.roleName);
|
|
4183
4821
|
if (availability.kind === "busy") {
|
|
4184
4822
|
throw usageError(`Reviewer ${config.roleName} is busy (${availability.phase}`
|
|
4185
|
-
+ `${availability.
|
|
4823
|
+
+ `${availability.activeRunId === undefined ? "" : `; AgentRun ${availability.activeRunId}`}); `
|
|
4186
4824
|
+ `retry after ${availability.retryAfterSeconds}s.`);
|
|
4187
4825
|
}
|
|
4188
4826
|
let reviewer = store.getRole(task.id, config.roleName);
|
|
@@ -4251,16 +4889,16 @@ function resumablePendingFinalTaskReview(store, task, round, config, taskCandida
|
|
|
4251
4889
|
|| round.reviewerRoleName !== taskFinalContract.reviewerRoleName) {
|
|
4252
4890
|
throw usageError(`Pending final ReviewRound Reviewer identity changed: ${round.id}.`);
|
|
4253
4891
|
}
|
|
4254
|
-
if (round.
|
|
4255
|
-
throw usageError(`Pending final ReviewRound already records Reviewer
|
|
4892
|
+
if (round.reviewerRunId !== undefined) {
|
|
4893
|
+
throw usageError(`Pending final ReviewRound already records Reviewer AgentRun ${round.reviewerRunId}: ${round.id}.`);
|
|
4256
4894
|
}
|
|
4257
4895
|
assertNoConflictingTaskReviewRound(store.listReviewRounds(task.id), round.id, round.reviewerRoleName);
|
|
4258
4896
|
const reviewer = store.getRole(task.id, round.reviewerRoleName);
|
|
4259
4897
|
if (reviewer === null || reviewer.name !== round.reviewerRoleName) {
|
|
4260
4898
|
throw usageError(`Pending final ReviewRound Reviewer identity changed: ${round.id}.`);
|
|
4261
4899
|
}
|
|
4262
|
-
if (store.
|
|
4263
|
-
throw usageError(`Reviewer Role already has an active
|
|
4900
|
+
if (store.getActiveRun(task.id, reviewer.name) !== null) {
|
|
4901
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
4264
4902
|
}
|
|
4265
4903
|
assertPendingFinalReviewWorkspaceEvidence(store, task, round);
|
|
4266
4904
|
return round;
|
|
@@ -4296,7 +4934,7 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
4296
4934
|
}
|
|
4297
4935
|
}
|
|
4298
4936
|
/**
|
|
4299
|
-
* Task-control retry of an exact failed review
|
|
4937
|
+
* Task-control retry of an exact failed review AgentRun. The old failed AgentRun
|
|
4300
4938
|
* remains the attempt trail, while the ReviewRound is reset to pending under
|
|
4301
4939
|
* its existing identity. Every identity and frozen-head fence is
|
|
4302
4940
|
* checked inside one transaction so a partial fail-old-without-reset state can
|
|
@@ -4304,12 +4942,12 @@ function assertPendingFinalReviewWorkspaceEvidence(store, task, round) {
|
|
|
4304
4942
|
*/
|
|
4305
4943
|
function retryFailedReviewRun(previous, store, options, now) {
|
|
4306
4944
|
const result = store.transaction((tx) => {
|
|
4307
|
-
const run = tx.
|
|
4945
|
+
const run = tx.getRun(previous.taskId, previous.id);
|
|
4308
4946
|
if (run === null || run.status !== "failed" || run.purpose !== "review") {
|
|
4309
|
-
throw usageError(`
|
|
4947
|
+
throw usageError(`AgentRun ${previous.id} is not a failed review AgentRun.`);
|
|
4310
4948
|
}
|
|
4311
4949
|
if (run.reviewRoundId === undefined) {
|
|
4312
|
-
throw usageError(`Review
|
|
4950
|
+
throw usageError(`Review AgentRun ${run.id} has no ReviewRound.`);
|
|
4313
4951
|
}
|
|
4314
4952
|
const task = requireTask(tx, run.taskId);
|
|
4315
4953
|
if (task.status !== "active")
|
|
@@ -4325,16 +4963,16 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4325
4963
|
: round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
|
|
4326
4964
|
if (run.executionGroupId !== undefined && (round.executionGroup?.id !== run.executionGroupId
|
|
4327
4965
|
|| retryLane === undefined
|
|
4328
|
-
|| retryLane.
|
|
4966
|
+
|| retryLane.currentRunId !== run.id
|
|
4329
4967
|
|| retryLane.roleName !== run.roleName)) {
|
|
4330
|
-
throw usageError(`Review
|
|
4968
|
+
throw usageError(`Review AgentRun ${run.id} no longer owns its exact Review Lane attempt.`);
|
|
4331
4969
|
}
|
|
4332
4970
|
const panelGroup = round.executionGroup !== undefined;
|
|
4333
4971
|
const runningPanelLaneRetry = round.status === "running"
|
|
4334
4972
|
&& panelGroup
|
|
4335
4973
|
&& retryLane?.disposition === "open";
|
|
4336
4974
|
if (round.status === "running" && panelGroup && !runningPanelLaneRetry) {
|
|
4337
|
-
throw usageError(`Review
|
|
4975
|
+
throw usageError(`Review AgentRun ${run.id} is not the current failed Lane attempt in running Round ${round.id}.`);
|
|
4338
4976
|
}
|
|
4339
4977
|
const retryReviewerRoleName = retryLane?.roleName ?? round.reviewerRoleName;
|
|
4340
4978
|
if (round.status !== "failed"
|
|
@@ -4370,10 +5008,10 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4370
5008
|
throw dataError(`WorkItem ReviewRound has no Candidate anchor: ${round.id}.`);
|
|
4371
5009
|
}
|
|
4372
5010
|
if (run.workItemId !== round.workItemId) {
|
|
4373
|
-
throw usageError(`Review
|
|
5011
|
+
throw usageError(`Review AgentRun ${run.id} does not match WorkItem ${round.workItemId}.`);
|
|
4374
5012
|
}
|
|
4375
5013
|
const item = tx.getWorkItem(task.id, round.workItemId);
|
|
4376
|
-
if (item === null || item.status !== "
|
|
5014
|
+
if (item === null || (item.status !== "open" || item.candidates.length === 0)) {
|
|
4377
5015
|
throw usageError(`WorkItem ReviewRound ${round.id} no longer has an awaiting Candidate.`);
|
|
4378
5016
|
}
|
|
4379
5017
|
const candidate = currentWorkItemCandidate(item);
|
|
@@ -4412,20 +5050,20 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4412
5050
|
throw usageError(`Reviewer already has an active review round for this candidate: ${activeRound.id}.`);
|
|
4413
5051
|
}
|
|
4414
5052
|
const reviewer = requireRole(tx, task.id, retryReviewerRoleName);
|
|
4415
|
-
const activePointer = tx.
|
|
5053
|
+
const activePointer = tx.getActiveRun(task.id, reviewer.name);
|
|
4416
5054
|
// Issue 06: a completed same-Round retry is a no-write idempotent result.
|
|
4417
5055
|
if (round.status === "completed") {
|
|
4418
5056
|
assertReviewerAvailable(tx, task.id, reviewer.name);
|
|
4419
5057
|
return { round, previousRun: run, created: false };
|
|
4420
5058
|
}
|
|
4421
5059
|
// Issue 06: a running same Round is reusable only with its exact active
|
|
4422
|
-
//
|
|
5060
|
+
// AgentRun. A stranded AgentRun (no active pointer) falls through and resets the
|
|
4423
5061
|
// Round after the identity fences below.
|
|
4424
5062
|
if (round.status === "running" && !runningPanelLaneRetry) {
|
|
4425
|
-
const
|
|
4426
|
-
const activeMatches =
|
|
5063
|
+
const reviewerRunId = round.reviewerRunId;
|
|
5064
|
+
const activeMatches = reviewerRunId !== undefined
|
|
4427
5065
|
&& activePointer !== null
|
|
4428
|
-
&& activePointer.id ===
|
|
5066
|
+
&& activePointer.id === reviewerRunId
|
|
4429
5067
|
&& activePointer.status === "active";
|
|
4430
5068
|
if (activePointer !== null && !activeMatches) {
|
|
4431
5069
|
throw usageError(`Existing running ReviewRound ${round.id} lacks its exact active Reviewer execution.`);
|
|
@@ -4438,24 +5076,65 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4438
5076
|
// Issue 06: an already-pending Round is the idempotent retry result.
|
|
4439
5077
|
if (round.status === "pending") {
|
|
4440
5078
|
if (activePointer !== null) {
|
|
4441
|
-
throw usageError(`Reviewer Role already has an active
|
|
5079
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
4442
5080
|
}
|
|
4443
5081
|
assertReviewerAvailable(tx, task.id, reviewer.name, round);
|
|
4444
5082
|
return { round, previousRun: run, created: false };
|
|
4445
5083
|
}
|
|
4446
5084
|
if (activePointer !== null) {
|
|
4447
|
-
throw usageError(`Reviewer Role already has an active
|
|
5085
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
4448
5086
|
}
|
|
4449
5087
|
assertReviewerAvailable(tx, task.id, reviewer.name, round);
|
|
4450
|
-
const validation =
|
|
5088
|
+
const validation = validateExactRunReviewRound(tx, run, { allowTerminal: true });
|
|
4451
5089
|
if (validation.disposition !== "applied" || validation.round === null) {
|
|
4452
|
-
throw usageError(`Review
|
|
5090
|
+
throw usageError(`Review AgentRun ${run.id} identity does not match its ReviewRound or frozen Task state changed: ${validation.reason ?? "mismatch"}.`);
|
|
5091
|
+
}
|
|
5092
|
+
if (run.sourceExecutionGroupId !== undefined) {
|
|
5093
|
+
assertTaskExecutionEnabled(task, "retrying Review synthesis");
|
|
5094
|
+
const group = round.executionGroup;
|
|
5095
|
+
if (round.status !== "failed" || round.reviewerRunId !== run.id
|
|
5096
|
+
|| group?.id !== run.sourceExecutionGroupId
|
|
5097
|
+
|| run.workspace === undefined) {
|
|
5098
|
+
throw usageError(`Review AgentRun ${run.id} no longer owns the current main synthesis.`);
|
|
5099
|
+
}
|
|
5100
|
+
selectedReviewSynthesisProducers(tx, round, group, synthesisSourceRunIds(tx, run));
|
|
5101
|
+
const effective = resolveEffectiveLaunch({
|
|
5102
|
+
role: reviewer,
|
|
5103
|
+
purpose: "review",
|
|
5104
|
+
workspace: run.workspace,
|
|
5105
|
+
reviewRoundId: round.id,
|
|
5106
|
+
reviewBaseCommit: round.reviewBaseCommit
|
|
5107
|
+
});
|
|
5108
|
+
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, {
|
|
5109
|
+
purpose: "review",
|
|
5110
|
+
...(run.workItemId === undefined ? {} : { workItemId: run.workItemId }),
|
|
5111
|
+
reviewRoundId: round.id,
|
|
5112
|
+
sourceExecutionGroupId: group.id,
|
|
5113
|
+
workspace: run.workspace,
|
|
5114
|
+
effective
|
|
5115
|
+
});
|
|
5116
|
+
const restarted = startReviewRound(retryReviewRound(round, requestedBy, now), created.id);
|
|
5117
|
+
tx.saveReviewRound(task.id, restarted);
|
|
5118
|
+
tx.saveRun(created);
|
|
5119
|
+
tx.saveActiveRun(created);
|
|
5120
|
+
enqueueRoleRunDispatch(tx, {
|
|
5121
|
+
taskId: task.id,
|
|
5122
|
+
roleName: reviewer.name,
|
|
5123
|
+
runId: created.id,
|
|
5124
|
+
reason: "turn-retried",
|
|
5125
|
+
occurredAt: now
|
|
5126
|
+
});
|
|
5127
|
+
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
5128
|
+
...runLaunchEventPayload(created),
|
|
5129
|
+
previousRunId: run.id
|
|
5130
|
+
}, now);
|
|
5131
|
+
return { round: restarted, previousRun: run, created: true, run: created };
|
|
4453
5132
|
}
|
|
4454
5133
|
if (runningPanelLaneRetry) {
|
|
4455
5134
|
const resetRound = retryRunningReviewExecutionLane(round, retryLane.id, run.id);
|
|
4456
5135
|
tx.saveReviewRound(task.id, resetRound);
|
|
4457
|
-
recordTaskEvent(tx, task.id, "
|
|
4458
|
-
|
|
5136
|
+
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
5137
|
+
runId: run.id,
|
|
4459
5138
|
reviewRoundId: round.id,
|
|
4460
5139
|
executionLaneId: retryLane.id
|
|
4461
5140
|
}, now);
|
|
@@ -4465,7 +5144,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4465
5144
|
// fence has passed. The outer transaction rolls back if Round creation fails.
|
|
4466
5145
|
let roundToReset = round;
|
|
4467
5146
|
if (round.status !== "failed") {
|
|
4468
|
-
const summary = `Review
|
|
5147
|
+
const summary = `Review AgentRun ${run.id} failed before delivery.`;
|
|
4469
5148
|
roundToReset = finishReviewRound(round, "failed", now, { kind: "execution", message: summary });
|
|
4470
5149
|
tx.saveReviewRound(task.id, roundToReset);
|
|
4471
5150
|
}
|
|
@@ -4474,41 +5153,44 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4474
5153
|
// stable across execution-attempt failures.
|
|
4475
5154
|
const resetRound = retryReviewRound(roundToReset, requestedBy, now);
|
|
4476
5155
|
tx.saveReviewRound(task.id, resetRound);
|
|
4477
|
-
recordTaskEvent(tx, task.id, "
|
|
4478
|
-
|
|
5156
|
+
recordTaskEvent(tx, task.id, "run.review-retried", {
|
|
5157
|
+
runId: run.id,
|
|
4479
5158
|
reviewRoundId: round.id
|
|
4480
5159
|
}, now);
|
|
4481
5160
|
return { round: resetRound, previousRun: run, created: true };
|
|
4482
5161
|
});
|
|
5162
|
+
if ("run" in result && result.run !== undefined) {
|
|
5163
|
+
notifyMailbox(options.runtime, roleMailbox(result.run.taskId, result.run.roleName), result.run.taskId);
|
|
5164
|
+
}
|
|
4483
5165
|
return output(result.created
|
|
4484
5166
|
? `Review retry requested as ${result.round.id}\n`
|
|
4485
|
-
: `Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round });
|
|
5167
|
+
: `Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round, ...("run" in result ? { run: result.run } : {}) });
|
|
4486
5168
|
}
|
|
4487
|
-
/**
|
|
4488
|
-
function
|
|
4489
|
-
const usage = "Task turn show usage: yui task
|
|
5169
|
+
/** AgentRun details are retained audit evidence; continuation uses durable Task state. */
|
|
5170
|
+
function showRun(args, store, options) {
|
|
5171
|
+
const usage = "Task turn show usage: yui task run show <task>/<run> [--json].";
|
|
4490
5172
|
const asJson = args.includes("--json");
|
|
4491
5173
|
const positionals = args.filter((arg) => arg !== "--json");
|
|
4492
5174
|
exactPositionals(positionals, 1, usage);
|
|
4493
5175
|
const data = store.transaction((tx) => {
|
|
4494
|
-
const run =
|
|
5176
|
+
const run = requireRun(tx, positionals[0], options);
|
|
4495
5177
|
const retirement = tx.listEvents(run.taskId)
|
|
4496
5178
|
.map(taskRecordRetirement)
|
|
4497
|
-
.find((entry) => entry?.recordKind === "
|
|
4498
|
-
return {
|
|
5179
|
+
.find((entry) => entry?.recordKind === "run" && entry.recordId === run.id) ?? null;
|
|
5180
|
+
return { run: run, retirement, execution: runExecutionObservation(run, tx.getTaskRoleSessionSet(run.taskId, run.roleName)?.providerBinding, tx.listEvents(run.taskId)) };
|
|
4499
5181
|
});
|
|
4500
5182
|
if (asJson) {
|
|
4501
5183
|
return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
|
|
4502
5184
|
}
|
|
4503
5185
|
return {
|
|
4504
5186
|
kind: "output",
|
|
4505
|
-
output:
|
|
5187
|
+
output: `Delivery observation: ${data.execution.delivery}\n` + renderRunShow(data.run, data.retirement),
|
|
4506
5188
|
data
|
|
4507
5189
|
};
|
|
4508
5190
|
}
|
|
4509
|
-
function
|
|
5191
|
+
function renderRunShow(run, retirement) {
|
|
4510
5192
|
const lines = [
|
|
4511
|
-
`
|
|
5193
|
+
`AgentRun: ${run.id}`,
|
|
4512
5194
|
`Task: ${run.taskId}`,
|
|
4513
5195
|
`Role: ${run.roleName}`,
|
|
4514
5196
|
`Purpose: ${run.purpose}`,
|
|
@@ -4531,40 +5213,40 @@ function renderTurnShow(run, retirement) {
|
|
|
4531
5213
|
return `${lines.join("\n")}\n`;
|
|
4532
5214
|
}
|
|
4533
5215
|
/**
|
|
4534
|
-
* Records a structured progress checkpoint for an active
|
|
4535
|
-
*
|
|
4536
|
-
* a healthy but long-running
|
|
4537
|
-
* collaboration-narrative noise. It never completes, mutates the
|
|
5216
|
+
* Records a structured progress checkpoint for an active AgentRun. This is a durable
|
|
5217
|
+
* AgentRun fact, not a Task Message: it advances the AgentRun's durable-progress clock so
|
|
5218
|
+
* a healthy but long-running AgentRun keeps proving it is alive without adding
|
|
5219
|
+
* collaboration-narrative noise. It never completes, mutates the AgentRun, or wakes the
|
|
4538
5220
|
* Leader.
|
|
4539
5221
|
*/
|
|
4540
|
-
function
|
|
4541
|
-
const usage = "Task turn checkpoint usage: yui task
|
|
5222
|
+
function checkpointRun(args, store, options) {
|
|
5223
|
+
const usage = "Task turn checkpoint usage: yui task run checkpoint <run> (--note <text>|--note-file <path|->).";
|
|
4542
5224
|
const parsed = parseTail(args, new Set(["--note", "--note-file"]), usage);
|
|
4543
5225
|
exactPositionals(parsed.positionals, 1, usage);
|
|
4544
5226
|
const note = readCommandText(parsed.options.get("--note"), parsed.options.get("--note-file"), "--note", usage);
|
|
4545
5227
|
const now = clock(options);
|
|
4546
5228
|
const event = store.transaction((tx) => {
|
|
4547
|
-
const run =
|
|
5229
|
+
const run = requireRun(tx, parsed.positionals[0], options);
|
|
4548
5230
|
if (run.status !== "active") {
|
|
4549
|
-
throw usageError(`
|
|
5231
|
+
throw usageError(`AgentRun ${run.id} is already terminal: ${run.status}.`);
|
|
4550
5232
|
}
|
|
4551
5233
|
const task = requireTask(tx, run.taskId);
|
|
4552
5234
|
if (task.status !== "active")
|
|
4553
|
-
throw usageError(inactiveTaskMessage(task, "checkpointing a
|
|
4554
|
-
const pointer =
|
|
5235
|
+
throw usageError(inactiveTaskMessage(task, "checkpointing a AgentRun"));
|
|
5236
|
+
const pointer = activeRunPointer(tx, run);
|
|
4555
5237
|
if (pointer?.id !== run.id) {
|
|
4556
|
-
throw usageError(`
|
|
5238
|
+
throw usageError(`AgentRun is not active for ${task.id}/${run.roleName}: ${run.id}.`);
|
|
4557
5239
|
}
|
|
4558
5240
|
const events = tx.listEvents(task.id);
|
|
4559
|
-
const recovered =
|
|
4560
|
-
const progress = recordTaskEventRecord(tx, task.id,
|
|
4561
|
-
|
|
5241
|
+
const recovered = isRoleRunStalled(events, run.id);
|
|
5242
|
+
const progress = recordTaskEventRecord(tx, task.id, RUN_PROGRESS_EVENT, {
|
|
5243
|
+
runId: run.id,
|
|
4562
5244
|
note: truncateEventNote(note),
|
|
4563
5245
|
...(run.workItemId === undefined ? {} : { workItemId: run.workItemId })
|
|
4564
5246
|
}, now);
|
|
4565
5247
|
if (recovered) {
|
|
4566
|
-
recordTaskEventRecord(tx, task.id,
|
|
4567
|
-
|
|
5248
|
+
recordTaskEventRecord(tx, task.id, RUN_RECOVERED_EVENT, {
|
|
5249
|
+
runId: run.id,
|
|
4568
5250
|
roleName: run.roleName,
|
|
4569
5251
|
progressAt: now.toISOString(),
|
|
4570
5252
|
kind: "checkpoint"
|
|
@@ -4612,12 +5294,12 @@ export function queueReviewRound(store, item, config, requestedBy, now, requeste
|
|
|
4612
5294
|
reviewer = createTaskRole(store, task, roleName, undefined, now, roleName);
|
|
4613
5295
|
store.saveRole(task.id, reviewer);
|
|
4614
5296
|
}
|
|
4615
|
-
if (store.
|
|
5297
|
+
if (store.getActiveRun(item.taskId, reviewer.name) !== null) {
|
|
4616
5298
|
const pending = createPending();
|
|
4617
5299
|
store.saveReviewRound(item.taskId, pending);
|
|
4618
5300
|
const failed = finishReviewRound(pending, "failed", now, {
|
|
4619
5301
|
kind: "dispatch",
|
|
4620
|
-
message: `Reviewer Role already has an active
|
|
5302
|
+
message: `Reviewer Role already has an active AgentRun: ${reviewer.name}.`
|
|
4621
5303
|
});
|
|
4622
5304
|
store.saveReviewRound(item.taskId, failed);
|
|
4623
5305
|
return { round: failed };
|
|
@@ -4757,8 +5439,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4757
5439
|
&& round.reviewerRoleName !== taskFinalContract.reviewerRoleName) {
|
|
4758
5440
|
throw new TaskFinalReviewDispatchDriftError(`Task final-review Reviewer identity does not match ReviewRound ${round.id}.`);
|
|
4759
5441
|
}
|
|
4760
|
-
if (round.status === "pending" && round.
|
|
4761
|
-
throw new TaskFinalReviewDispatchDriftError(`Pending final ReviewRound already records Reviewer
|
|
5442
|
+
if (round.status === "pending" && round.reviewerRunId !== undefined) {
|
|
5443
|
+
throw new TaskFinalReviewDispatchDriftError(`Pending final ReviewRound already records Reviewer AgentRun ${round.reviewerRunId}: `
|
|
4762
5444
|
+ `${round.id}.`);
|
|
4763
5445
|
}
|
|
4764
5446
|
let currentTaskCandidate;
|
|
@@ -4810,8 +5492,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4810
5492
|
&& (requestedReviewers.has(entry.reviewerRoleName)
|
|
4811
5493
|
|| entry.executionGroup?.lanes.some(({ roleName }) => (requestedReviewers.has(roleName))) === true)
|
|
4812
5494
|
&& !(entry.status === "running"
|
|
4813
|
-
&& entry.
|
|
4814
|
-
&& tx.
|
|
5495
|
+
&& entry.reviewerRunId !== undefined
|
|
5496
|
+
&& tx.getRun(task.id, entry.reviewerRunId)?.status === "failed")));
|
|
4815
5497
|
if (conflicting !== undefined) {
|
|
4816
5498
|
throw new TaskFinalReviewDispatchDriftError(`Another active Task-final ReviewRound already exists: ${conflicting.id}/${conflicting.reviewerRoleName}.`);
|
|
4817
5499
|
}
|
|
@@ -4838,8 +5520,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4838
5520
|
}
|
|
4839
5521
|
const candidateLabel = taskScope
|
|
4840
5522
|
? "frozen Task candidate"
|
|
4841
|
-
: candidate.source.type === "
|
|
4842
|
-
? `candidate
|
|
5523
|
+
: candidate.source.type === "run"
|
|
5524
|
+
? `candidate AgentRun ${candidate.source.runId}`
|
|
4843
5525
|
: `revision ${candidate.workItemRevision}`;
|
|
4844
5526
|
const frozenHeads = taskScope
|
|
4845
5527
|
? round.taskCandidate.projects
|
|
@@ -4854,7 +5536,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4854
5536
|
}
|
|
4855
5537
|
const diffByProject = options.deltaRecheckDiff;
|
|
4856
5538
|
if (diffByProject === undefined) {
|
|
4857
|
-
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck diff is missing for ${round.id}; the CLI preflight did not
|
|
5539
|
+
throw new TaskFinalReviewDispatchDriftError(`Delta-recheck diff is missing for ${round.id}; the CLI preflight did not run.`);
|
|
4858
5540
|
}
|
|
4859
5541
|
try {
|
|
4860
5542
|
verifyDeltaRecheckDiff(round.deltaRecheck, diffByProject);
|
|
@@ -4898,17 +5580,17 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4898
5580
|
"You may freely edit source/tests, run local build or test commands, and optionally commit diagnostic evidence only inside this stable Reviewer workspace at the exact ReviewRound snapshot.",
|
|
4899
5581
|
"Do not push, integrate, mutate Task state, touch the Candidate or Worker workspace, another Task/workspace, a stable checkout, or the real Yui control-plane home.",
|
|
4900
5582
|
"End the Provider turn with one complete original result in clear Markdown or JSON. Recommended sections are conclusion, material findings, checks actually run, uncertainty, and next actions. Yui preserves the text verbatim and does not parse or validate those sections.",
|
|
4901
|
-
"Report reviewBaseCommit, exact checks/results, material findings, and uncertainty. This
|
|
5583
|
+
"Report reviewBaseCommit, exact checks/results, material findings, and uncertainty. This AgentRun result completes only the Round and creates no Candidate or ChangeSet.",
|
|
4902
5584
|
"The Leader alone interprets and routes evidence: original Worker when open, a small Repair WorkItem when needed, or Leader/Integration for merge and local fixes; never merge review evidence yourself."
|
|
4903
5585
|
].join("\n");
|
|
4904
|
-
const
|
|
5586
|
+
const createdRuns = [];
|
|
4905
5587
|
if (round.executionGroup === undefined) {
|
|
4906
5588
|
if (round.status !== "pending")
|
|
4907
|
-
return
|
|
4908
|
-
if (tx.
|
|
4909
|
-
throw usageError(`Reviewer Role already has an active
|
|
5589
|
+
return createdRuns;
|
|
5590
|
+
if (tx.getActiveRun(taskId, reviewer.name) !== null) {
|
|
5591
|
+
throw usageError(`Reviewer Role already has an active AgentRun: ${reviewer.name}.`);
|
|
4910
5592
|
}
|
|
4911
|
-
const
|
|
5593
|
+
const runId = tx.nextRunId(taskId);
|
|
4912
5594
|
const effective = resolveEffectiveLaunch({
|
|
4913
5595
|
role: reviewer,
|
|
4914
5596
|
purpose: "review",
|
|
@@ -4916,14 +5598,14 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4916
5598
|
reviewRoundId: round.id,
|
|
4917
5599
|
reviewBaseCommit: round.reviewBaseCommit
|
|
4918
5600
|
});
|
|
4919
|
-
const snapshot =
|
|
5601
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
4920
5602
|
taskId,
|
|
4921
5603
|
roleName: reviewer.name,
|
|
4922
5604
|
purpose: "review",
|
|
4923
5605
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
4924
5606
|
reviewRoundId: round.id
|
|
4925
5607
|
}, now, "controller");
|
|
4926
|
-
const created =
|
|
5608
|
+
const created = createRun(runId, taskId, reviewer.name, roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(taskId, reviewer.name), effective.agentId, effective), createRunInput({
|
|
4927
5609
|
source: {
|
|
4928
5610
|
type: "yui",
|
|
4929
5611
|
channel: item === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
@@ -4938,27 +5620,27 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4938
5620
|
workspace: round.workspace,
|
|
4939
5621
|
effective
|
|
4940
5622
|
});
|
|
4941
|
-
tx.
|
|
5623
|
+
tx.saveRun(created);
|
|
4942
5624
|
tx.saveReviewRound(taskId, startReviewRound(round, created.id));
|
|
4943
|
-
tx.
|
|
4944
|
-
|
|
5625
|
+
tx.saveActiveRun(created);
|
|
5626
|
+
enqueueRoleRunDispatch(tx, {
|
|
4945
5627
|
taskId,
|
|
4946
5628
|
roleName: reviewer.name,
|
|
4947
|
-
|
|
5629
|
+
runId: created.id,
|
|
4948
5630
|
reason: "review-requested",
|
|
4949
5631
|
occurredAt: now
|
|
4950
5632
|
});
|
|
4951
|
-
recordTaskEvent(tx, taskId, "
|
|
4952
|
-
|
|
4953
|
-
return
|
|
5633
|
+
recordTaskEvent(tx, taskId, "run.review-dispatched", runLaunchEventPayload(created), now);
|
|
5634
|
+
createdRuns.push(created);
|
|
5635
|
+
return createdRuns;
|
|
4954
5636
|
}
|
|
4955
5637
|
let runningGroup = round.executionGroup;
|
|
4956
5638
|
const dispatchLanes = runningGroup.lanes.filter((lane) => {
|
|
4957
5639
|
if (lane.disposition !== "open")
|
|
4958
5640
|
return false;
|
|
4959
|
-
if (lane.
|
|
5641
|
+
if (lane.currentRunId === undefined)
|
|
4960
5642
|
return true;
|
|
4961
|
-
return tx.
|
|
5643
|
+
return tx.getRun(taskId, lane.currentRunId)?.status === "failed";
|
|
4962
5644
|
});
|
|
4963
5645
|
const preparedLaneWorkspaces = requireIsolatedReviewLaneWorkspaces(tx, round, dispatchLanes, options.executionLaneWorkspaces);
|
|
4964
5646
|
for (const lane of dispatchLanes) {
|
|
@@ -4966,8 +5648,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4966
5648
|
if (laneReviewer === null) {
|
|
4967
5649
|
throw usageError(`Review Producer Role not found: ${taskId}/${lane.roleName}.`);
|
|
4968
5650
|
}
|
|
4969
|
-
if (tx.
|
|
4970
|
-
throw usageError(`Review Producer Role already has an active
|
|
5651
|
+
if (tx.getActiveRun(taskId, lane.roleName) !== null) {
|
|
5652
|
+
throw usageError(`Review Producer Role already has an active AgentRun: ${lane.roleName}.`);
|
|
4971
5653
|
}
|
|
4972
5654
|
const laneManagedWorkspace = preparedLaneWorkspaces.get(lane.id);
|
|
4973
5655
|
const effective = lane.effective ?? resolveEffectiveLaunch({
|
|
@@ -4977,8 +5659,8 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
4977
5659
|
reviewRoundId: round.id,
|
|
4978
5660
|
reviewBaseCommit: round.reviewBaseCommit
|
|
4979
5661
|
});
|
|
4980
|
-
const
|
|
4981
|
-
const input =
|
|
5662
|
+
const runId = tx.nextRunId(taskId);
|
|
5663
|
+
const input = createRunInput({
|
|
4982
5664
|
source: {
|
|
4983
5665
|
type: "yui",
|
|
4984
5666
|
channel: item === undefined ? "task-dispatch" : "workitem-dispatch"
|
|
@@ -5000,7 +5682,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5000
5682
|
deltaRefIds: []
|
|
5001
5683
|
});
|
|
5002
5684
|
runningGroup = updateUnifiedExecutionLane(runningGroup, lane.id, {
|
|
5003
|
-
|
|
5685
|
+
currentRunId: runId,
|
|
5004
5686
|
effective,
|
|
5005
5687
|
workspace: {
|
|
5006
5688
|
root: laneManagedWorkspace.root,
|
|
@@ -5009,7 +5691,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5009
5691
|
.map(({ projectId }) => projectId)
|
|
5010
5692
|
}
|
|
5011
5693
|
}, now);
|
|
5012
|
-
|
|
5694
|
+
createdRuns.push(createRun(runId, taskId, laneReviewer.name, roleAgentSessionResumeMode(tx.getTaskRoleSessionSet(taskId, laneReviewer.name), effective.agentId, effective), input, now, {
|
|
5013
5695
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5014
5696
|
purpose: "review",
|
|
5015
5697
|
reviewRoundId: round.id,
|
|
@@ -5031,32 +5713,30 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
|
|
|
5031
5713
|
tx.saveManagedWorkspace(prepared);
|
|
5032
5714
|
}
|
|
5033
5715
|
}
|
|
5034
|
-
for (let index = 0; index <
|
|
5035
|
-
const
|
|
5036
|
-
const snapshot =
|
|
5716
|
+
for (let index = 0; index < createdRuns.length; index += 1) {
|
|
5717
|
+
const unboundRun = createdRuns[index];
|
|
5718
|
+
const snapshot = freezeRunContextSnapshot(tx, {
|
|
5037
5719
|
taskId,
|
|
5038
|
-
roleName:
|
|
5720
|
+
roleName: unboundRun.roleName,
|
|
5039
5721
|
purpose: "review",
|
|
5040
5722
|
...(item === undefined ? {} : { workItemId: item.id }),
|
|
5041
5723
|
reviewRoundId: round.id
|
|
5042
5724
|
}, now, "controller", runningGroup.assignment.contextSnapshotRef);
|
|
5043
|
-
const created =
|
|
5044
|
-
|
|
5045
|
-
const laneReviewer = requireRole(tx, taskId,
|
|
5046
|
-
tx.
|
|
5047
|
-
tx.
|
|
5048
|
-
|
|
5725
|
+
const created = withRunContextSnapshot(unboundRun, contextSnapshotRef(snapshot), contextSnapshotDeltaRefIds(tx, snapshot));
|
|
5726
|
+
createdRuns[index] = created;
|
|
5727
|
+
const laneReviewer = requireRole(tx, taskId, unboundRun.roleName);
|
|
5728
|
+
tx.saveRun(created);
|
|
5729
|
+
tx.saveActiveRun(created);
|
|
5730
|
+
enqueueRoleRunDispatch(tx, {
|
|
5049
5731
|
taskId,
|
|
5050
5732
|
roleName: laneReviewer.name,
|
|
5051
|
-
|
|
5733
|
+
runId: created.id,
|
|
5052
5734
|
reason: "review-requested",
|
|
5053
5735
|
occurredAt: now
|
|
5054
5736
|
});
|
|
5055
|
-
recordTaskEvent(tx, taskId, "
|
|
5737
|
+
recordTaskEvent(tx, taskId, "run.review-dispatched", runLaunchEventPayload(created), now);
|
|
5056
5738
|
}
|
|
5057
|
-
|
|
5058
|
-
createdTurns.push(...reconciliation.createdTurns);
|
|
5059
|
-
return createdTurns;
|
|
5739
|
+
return createdRuns;
|
|
5060
5740
|
});
|
|
5061
5741
|
for (const run of runs) {
|
|
5062
5742
|
notifyMailbox(options.runtime, roleMailbox(run.taskId, run.roleName), run.taskId);
|
|
@@ -5106,6 +5786,28 @@ function requireWorkItemCandidate(item) {
|
|
|
5106
5786
|
}
|
|
5107
5787
|
return candidate;
|
|
5108
5788
|
}
|
|
5789
|
+
/**
|
|
5790
|
+
* Parse `--artifact-ref git:<commit>:<relativePath>` selectors into canonical
|
|
5791
|
+
* commit-pinned references for the given Task. This is a PURE shape check with
|
|
5792
|
+
* no Git or DB I/O: the commit self-certifies the frozen bytes, so a valid
|
|
5793
|
+
* pinned reference is complete evidence. The bytes are proven to exist lazily
|
|
5794
|
+
* when they are resolved on the async read/context path.
|
|
5795
|
+
*/
|
|
5796
|
+
function fixedArtifactRefs(taskId, refs) {
|
|
5797
|
+
if (new Set(refs).size !== refs.length)
|
|
5798
|
+
throw usageError("Artifact references must be unique.");
|
|
5799
|
+
return refs.map((ref) => {
|
|
5800
|
+
if (!isGitArtifactRefString(ref)) {
|
|
5801
|
+
throw usageError("Artifact reference must be a commit-pinned git:<commit>:<relativePath> reference.");
|
|
5802
|
+
}
|
|
5803
|
+
try {
|
|
5804
|
+
return parseGitArtifactRef(ref, taskId);
|
|
5805
|
+
}
|
|
5806
|
+
catch (error) {
|
|
5807
|
+
throw usageError(`Artifact reference is invalid: ${messageOf(error)}`);
|
|
5808
|
+
}
|
|
5809
|
+
});
|
|
5810
|
+
}
|
|
5109
5811
|
/** ReviewRound ids are the durable Task-local creation order; wall time is not causal. */
|
|
5110
5812
|
function reviewRoundsByIdentity(rounds) {
|
|
5111
5813
|
return [...rounds].sort((left, right) => (left.id.localeCompare(right.id, undefined, { numeric: true })));
|
|
@@ -5118,7 +5820,9 @@ function activeReviewRoundForCandidate(store, item, candidate) {
|
|
|
5118
5820
|
.at(-1);
|
|
5119
5821
|
}
|
|
5120
5822
|
function createTaskRole(store, task, roleName, explicitAgentId, now, sourceGlobalRoleName) {
|
|
5121
|
-
const workspace = task.
|
|
5823
|
+
const workspace = task.status === "draft"
|
|
5824
|
+
? join(`${store.rootDirectory()}.task-runtimes`, "planning", task.id)
|
|
5825
|
+
: task.cwd ?? store.getConfig().defaultWorkspace ?? process.cwd();
|
|
5122
5826
|
if (explicitAgentId === undefined) {
|
|
5123
5827
|
const sourceRoleName = sourceGlobalRoleName
|
|
5124
5828
|
?? (roleName === LEADER_ROLE ? LEADER_ROLE : "worker");
|
|
@@ -5136,7 +5840,7 @@ function createTaskRole(store, task, roleName, explicitAgentId, now, sourceGloba
|
|
|
5136
5840
|
throw dataError(`No Agent is configured for Task role: ${roleName}.`);
|
|
5137
5841
|
}
|
|
5138
5842
|
const agent = requireAgent(store, agentId);
|
|
5139
|
-
const binding = createRoleAgentBinding(
|
|
5843
|
+
const binding = createRoleAgentBinding(agent);
|
|
5140
5844
|
return createRole(task.id, roleName, [binding], agent.id, workspace, now);
|
|
5141
5845
|
}
|
|
5142
5846
|
function requireAgentProfile(store, id) {
|
|
@@ -5146,7 +5850,9 @@ function requireAgentProfile(store, id) {
|
|
|
5146
5850
|
return profile;
|
|
5147
5851
|
}
|
|
5148
5852
|
function createTaskRoleFromAgentBinding(store, task, roleName, binding, now) {
|
|
5149
|
-
const workspace = task.
|
|
5853
|
+
const workspace = task.status === "draft"
|
|
5854
|
+
? join(`${store.rootDirectory()}.task-runtimes`, "planning", task.id)
|
|
5855
|
+
: task.cwd ?? store.getConfig().defaultWorkspace ?? process.cwd();
|
|
5150
5856
|
return createRole(task.id, roleName, [binding], binding.agentId, workspace, now);
|
|
5151
5857
|
}
|
|
5152
5858
|
function resolvedAgentProfileRuntime(profile, store) {
|
|
@@ -5174,7 +5880,7 @@ function resolveTaskRoleAgentBindingAdd(parsed, profile, store) {
|
|
|
5174
5880
|
if (explicitAgentId === undefined)
|
|
5175
5881
|
return undefined;
|
|
5176
5882
|
const agent = requireAgent(store, explicitAgentId);
|
|
5177
|
-
binding = createRoleAgentBinding(
|
|
5883
|
+
binding = createRoleAgentBinding(agent);
|
|
5178
5884
|
}
|
|
5179
5885
|
if (hasAgentConfigOptions(parsed)) {
|
|
5180
5886
|
binding = patchRoleAgentBinding(binding, parsed);
|
|
@@ -5185,11 +5891,6 @@ function resolveTaskRoleAgentBindingUpdate(parsed, role, profile, store) {
|
|
|
5185
5891
|
const changesAgentConfig = hasAgentConfigOptions(parsed);
|
|
5186
5892
|
const explicitAgentId = parsed.one("--agent")?.trim();
|
|
5187
5893
|
const targetAgentId = explicitAgentId || role.activeAgentId;
|
|
5188
|
-
if (profile?.runtime.source === "global-worker"
|
|
5189
|
-
&& explicitAgentId === undefined
|
|
5190
|
-
&& !changesAgentConfig) {
|
|
5191
|
-
return undefined;
|
|
5192
|
-
}
|
|
5193
5894
|
let binding;
|
|
5194
5895
|
if (profile !== undefined) {
|
|
5195
5896
|
const profileRuntime = resolvedAgentProfileRuntime(profile, store);
|
|
@@ -5208,7 +5909,7 @@ function resolveTaskRoleAgentBindingUpdate(parsed, role, profile, store) {
|
|
|
5208
5909
|
return undefined;
|
|
5209
5910
|
const agent = requireAgent(store, targetAgentId);
|
|
5210
5911
|
binding = role.agentBindings[targetAgentId]
|
|
5211
|
-
?? createRoleAgentBinding(
|
|
5912
|
+
?? createRoleAgentBinding(agent);
|
|
5212
5913
|
}
|
|
5213
5914
|
if (changesAgentConfig) {
|
|
5214
5915
|
binding = patchRoleAgentBinding(binding, parsed);
|
|
@@ -5245,7 +5946,7 @@ function appendMessage(store, taskId, body, kind, author, now, context = {}) {
|
|
|
5245
5946
|
recordTaskEvent(store, taskId, "message.sent", {
|
|
5246
5947
|
messageId: message.id,
|
|
5247
5948
|
kind: message.kind,
|
|
5248
|
-
...(message.
|
|
5949
|
+
...(message.runId === undefined ? {} : { runId: message.runId })
|
|
5249
5950
|
}, now);
|
|
5250
5951
|
return message;
|
|
5251
5952
|
}
|
|
@@ -5253,43 +5954,29 @@ function recordTaskEvent(store, taskId, type, payload, now) {
|
|
|
5253
5954
|
return recordTaskEventRecord(store, taskId, type, payload, now);
|
|
5254
5955
|
}
|
|
5255
5956
|
function leaderActionEventPayload(store, taskId, options) {
|
|
5256
|
-
const
|
|
5257
|
-
|
|
5957
|
+
const caller = currentManagedRuntime(store, options.environment, taskId, "leader");
|
|
5958
|
+
// A long-lived Session may be handling direct input while another request
|
|
5959
|
+
// awaits admission. The Role's active pointer cannot prove command origin.
|
|
5960
|
+
return caller === undefined ? {} : { leaderNativeSessionId: caller.nativeSessionId };
|
|
5258
5961
|
}
|
|
5259
5962
|
function recordTaskEventRecord(store, taskId, type, payload, now) {
|
|
5260
5963
|
const event = createTaskEvent(store.nextEventId(taskId), taskId, type, payload, now);
|
|
5261
5964
|
store.saveEvent(taskId, event);
|
|
5262
5965
|
return event;
|
|
5263
5966
|
}
|
|
5264
|
-
/** Keeps a free-text
|
|
5967
|
+
/** Keeps a free-text AgentRun-fact note bounded so an event payload stays compact. */
|
|
5265
5968
|
function truncateEventNote(note) {
|
|
5266
5969
|
const normalized = note.trim();
|
|
5267
5970
|
return normalized.length <= 280 ? normalized : `${normalized.slice(0, 279)}…`;
|
|
5268
5971
|
}
|
|
5269
|
-
function
|
|
5270
|
-
const effective = sessions?.sessions[sessions.activeAgentId]?.effective;
|
|
5972
|
+
function runLaunchEventPayload(run) {
|
|
5271
5973
|
return {
|
|
5272
|
-
|
|
5273
|
-
defaultAccess: role.defaultAccess,
|
|
5274
|
-
effectiveRevision: effective === undefined
|
|
5275
|
-
? "none"
|
|
5276
|
-
: String(effective.sourceDesiredRevision),
|
|
5277
|
-
profileAccess: effective?.profileAccess ?? "none",
|
|
5278
|
-
effectivePermission: effective?.permission.strategy ?? "none",
|
|
5279
|
-
desiredDrift: effective === undefined
|
|
5280
|
-
? "not-started"
|
|
5281
|
-
: effective.sourceDesiredRevision === role.launchRevision
|
|
5282
|
-
? "none"
|
|
5283
|
-
: "pending-next-launch"
|
|
5284
|
-
};
|
|
5285
|
-
}
|
|
5286
|
-
function turnLaunchEventPayload(run) {
|
|
5287
|
-
return {
|
|
5288
|
-
turnId: run.id,
|
|
5974
|
+
runId: run.id,
|
|
5289
5975
|
role: run.roleName,
|
|
5290
5976
|
purpose: run.purpose,
|
|
5291
5977
|
mode: run.mode,
|
|
5292
5978
|
agent: `${run.effective.agentId}/${run.effective.adapterId}`,
|
|
5979
|
+
component: run.effective.component,
|
|
5293
5980
|
effectiveRevision: String(run.effective.sourceDesiredRevision),
|
|
5294
5981
|
profileAccess: run.effective.profileAccess,
|
|
5295
5982
|
effectivePermission: run.effective.permission.strategy,
|
|
@@ -5346,18 +6033,18 @@ function requireWorkItem(store, workItemId, options) {
|
|
|
5346
6033
|
}
|
|
5347
6034
|
return item;
|
|
5348
6035
|
}
|
|
5349
|
-
function
|
|
5350
|
-
const reference = taskRecordReference(
|
|
5351
|
-
const run = store.
|
|
6036
|
+
function requireRun(store, runId, options) {
|
|
6037
|
+
const reference = taskRecordReference(runId, "run", "AgentRun reference", options);
|
|
6038
|
+
const run = store.getRun(reference.taskId, reference.localId);
|
|
5352
6039
|
if (run === null) {
|
|
5353
|
-
throw usageError(`
|
|
6040
|
+
throw usageError(`AgentRun not found: ${reference.taskId}/${reference.localId}.`);
|
|
5354
6041
|
}
|
|
5355
6042
|
return run;
|
|
5356
6043
|
}
|
|
5357
|
-
function
|
|
6044
|
+
function activeRunPointer(store, run) {
|
|
5358
6045
|
return run.executionGroupId !== undefined && run.executionLaneId !== undefined
|
|
5359
|
-
? store.
|
|
5360
|
-
: store.
|
|
6046
|
+
? store.getActiveExecutionLaneRun(run.taskId, run.executionGroupId, run.executionLaneId)
|
|
6047
|
+
: store.getActiveRun(run.taskId, run.roleName);
|
|
5361
6048
|
}
|
|
5362
6049
|
function requireReviewRound(store, reviewRoundId, options) {
|
|
5363
6050
|
const reference = taskRecordReference(reviewRoundId, "reviewRound", "ReviewRound reference", options);
|
|
@@ -5392,12 +6079,12 @@ export function assertWorkItemDependenciesCompletedForCommand(store, item) {
|
|
|
5392
6079
|
throw error;
|
|
5393
6080
|
}
|
|
5394
6081
|
}
|
|
5395
|
-
function
|
|
5396
|
-
return [...
|
|
6082
|
+
function chronologicalRuns(runs) {
|
|
6083
|
+
return [...runs].sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
5397
6084
|
|| left.id.localeCompare(right.id)));
|
|
5398
6085
|
}
|
|
5399
6086
|
function isTerminalWorkItemStatus(status) {
|
|
5400
|
-
return ["
|
|
6087
|
+
return ["accepted", "retired"].includes(status);
|
|
5401
6088
|
}
|
|
5402
6089
|
function assertTaskOpen(task) {
|
|
5403
6090
|
if (task.status === "completed") {
|
|
@@ -5405,7 +6092,7 @@ function assertTaskOpen(task) {
|
|
|
5405
6092
|
}
|
|
5406
6093
|
if (task.status === "archived")
|
|
5407
6094
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
5408
|
-
if (task.status === "
|
|
6095
|
+
if (task.status === "cancelled")
|
|
5409
6096
|
throw usageError(`Task is retired: ${task.id}.`);
|
|
5410
6097
|
}
|
|
5411
6098
|
function assertTaskExecutionEnabled(task, action) {
|
|
@@ -5423,7 +6110,7 @@ function inactiveTaskMessage(task, action) {
|
|
|
5423
6110
|
if (task.status === "completed") {
|
|
5424
6111
|
return `Task ${task.id} is completed; reopen it before ${action}.`;
|
|
5425
6112
|
}
|
|
5426
|
-
if (task.status === "
|
|
6113
|
+
if (task.status === "cancelled")
|
|
5427
6114
|
return `Task ${task.id} is retired; it cannot resume ${action}.`;
|
|
5428
6115
|
return `Task is archived: ${task.id}.`;
|
|
5429
6116
|
}
|
|
@@ -5444,10 +6131,6 @@ function parseWorkStatus(value) {
|
|
|
5444
6131
|
throw usageError(`Invalid work item status: ${value}.`);
|
|
5445
6132
|
}
|
|
5446
6133
|
function presentWorkStatus(status) {
|
|
5447
|
-
if (status === "pending")
|
|
5448
|
-
return "todo";
|
|
5449
|
-
if (status === "completed")
|
|
5450
|
-
return "done";
|
|
5451
6134
|
return status;
|
|
5452
6135
|
}
|
|
5453
6136
|
function parseWorkCreateArgs(args, usage) {
|
|
@@ -5622,7 +6305,10 @@ function taskBriefCommand(args, store, options) {
|
|
|
5622
6305
|
...(hasSummary ? { leaderSummary: parsed.options.get("--leader-summary") } : {})
|
|
5623
6306
|
}, updatedBy, now);
|
|
5624
6307
|
tx.saveTaskBrief(task.id, brief);
|
|
5625
|
-
recordTaskEvent(tx, task.id, "brief.updated", {
|
|
6308
|
+
recordTaskEvent(tx, task.id, "brief.updated", {
|
|
6309
|
+
updatedBy,
|
|
6310
|
+
previous: JSON.stringify(existing), current: JSON.stringify(brief)
|
|
6311
|
+
}, now);
|
|
5626
6312
|
enqueueWork(tx, taskMailbox(task.id), "brief-updated", now, [taskRef(task.id)]);
|
|
5627
6313
|
if (task.status === "active" && updatedBy !== "leader") {
|
|
5628
6314
|
enqueueWork(tx, leaderMailbox(task.id), "brief-updated", now, [taskRef(task.id)]);
|
|
@@ -5633,12 +6319,18 @@ function taskBriefCommand(args, store, options) {
|
|
|
5633
6319
|
if (result.task.status === "active") {
|
|
5634
6320
|
notifyMailbox(options.runtime, leaderMailbox(result.task.id), result.task.id);
|
|
5635
6321
|
}
|
|
5636
|
-
return output(`Updated brief for ${result.task.id}\n
|
|
6322
|
+
return output(`Updated brief for ${result.task.id}\n`, { taskId: result.task.id, brief: result.brief });
|
|
5637
6323
|
}
|
|
5638
6324
|
throw usageError(command === undefined
|
|
5639
6325
|
? "Task brief command is required."
|
|
5640
6326
|
: `Unknown command: task brief ${command}`);
|
|
5641
6327
|
}
|
|
6328
|
+
/** Persist only edited fields, not another copy of aggregate execution history.
|
|
6329
|
+
* Null records an absent optional field so clearing it remains observable. */
|
|
6330
|
+
function editedFieldValues(record, fields) {
|
|
6331
|
+
const values = record;
|
|
6332
|
+
return JSON.stringify(Object.fromEntries(fields.map((field) => [field, values[field] ?? null])));
|
|
6333
|
+
}
|
|
5642
6334
|
function taskDecisionCommand(args, store, options) {
|
|
5643
6335
|
const [command, ...rest] = args;
|
|
5644
6336
|
if (command === "record") {
|
|
@@ -5889,14 +6581,11 @@ function taskContinuationCommand(args, store) {
|
|
|
5889
6581
|
const report = [...continuation.reports].reverse()[0];
|
|
5890
6582
|
const reportEvent = report === undefined
|
|
5891
6583
|
? undefined
|
|
5892
|
-
: reportEvents.find((entry) => (entry.continuationId === identity.continuationId
|
|
5893
|
-
&& entry.continuationGeneration === identity.generation
|
|
5894
|
-
&& entry.reportId === report.reportId));
|
|
6584
|
+
: reportEvents.find((entry) => (entry.continuationId === identity.continuationId && entry.reportId === report.reportId));
|
|
5895
6585
|
return Object.freeze({
|
|
5896
6586
|
continuationId: identity.continuationId,
|
|
5897
|
-
generation: identity.generation,
|
|
5898
6587
|
driver: identity.providerNamespace,
|
|
5899
|
-
|
|
6588
|
+
runId: continuation.runId,
|
|
5900
6589
|
execution: continuation.execution,
|
|
5901
6590
|
outcome: continuation.outcome,
|
|
5902
6591
|
attachment: continuation.attachment,
|
|
@@ -5942,12 +6631,9 @@ function continuationReportEvents(events) {
|
|
|
5942
6631
|
const observation = runtimeObservationFromTaskEvent(event);
|
|
5943
6632
|
if (observation !== null && observation.kind === "continuation.reported") {
|
|
5944
6633
|
const continuationId = observation.fence.continuationId;
|
|
5945
|
-
const continuationGeneration = observation.fence.continuationGeneration;
|
|
5946
6634
|
const reportId = observation.payload?.reportId;
|
|
5947
|
-
if (continuationId !== undefined
|
|
5948
|
-
|
|
5949
|
-
&& reportId !== undefined) {
|
|
5950
|
-
result.push({ event, continuationId, continuationGeneration, reportId });
|
|
6635
|
+
if (continuationId !== undefined && reportId !== undefined) {
|
|
6636
|
+
result.push({ event, continuationId, reportId });
|
|
5951
6637
|
}
|
|
5952
6638
|
}
|
|
5953
6639
|
}
|
|
@@ -5961,6 +6647,48 @@ function continuationReportEvents(events) {
|
|
|
5961
6647
|
*/
|
|
5962
6648
|
function taskWakeDispatch(args, store, options) {
|
|
5963
6649
|
const [subcommand] = args;
|
|
6650
|
+
if (subcommand === "resolve") {
|
|
6651
|
+
const usage = "Task wake resolve usage: yui task wake resolve <task> <wake> --reason <quiescence-evidence>.";
|
|
6652
|
+
const parsed = parseTail(args.slice(1), new Set(["--reason"]), usage);
|
|
6653
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
6654
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
6655
|
+
const now = clock(options);
|
|
6656
|
+
const result = store.transaction((tx) => {
|
|
6657
|
+
const task = requireTask(tx, parsed.positionals[0]);
|
|
6658
|
+
taskActor(tx, options, task.id);
|
|
6659
|
+
const wakeId = parsed.positionals[1];
|
|
6660
|
+
const wake = tx.getTaskWake(task.id, wakeId);
|
|
6661
|
+
if (wake === null)
|
|
6662
|
+
throw usageError("Wake is unavailable.");
|
|
6663
|
+
const target = leaderMailbox(task.id);
|
|
6664
|
+
const mailbox = tx.getWorkMailbox(target);
|
|
6665
|
+
const claim = mailbox?.processing;
|
|
6666
|
+
if (claim === undefined || claim === null || claim.owner !== `leader-notification:${wake.id}`) {
|
|
6667
|
+
throw usageError("This wake has no unresolved notification claim.");
|
|
6668
|
+
}
|
|
6669
|
+
const previous = tx.listEvents(task.id).filter((event) => event.type === "notification.delivery" && event.payload.attemptId === claim.batchId).at(-1);
|
|
6670
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, "leader");
|
|
6671
|
+
const provider = sessions?.providerBinding;
|
|
6672
|
+
if (previous?.payload.outcome !== "unknown")
|
|
6673
|
+
throw usageError("Only an unknown notification can be resolved.");
|
|
6674
|
+
if (tx.getActiveRun(task.id, "leader") !== null
|
|
6675
|
+
|| provider?.authority.owner === "human" || provider?.authority.owner === "unknown"
|
|
6676
|
+
|| (provider?.run !== undefined && provider.run !== null
|
|
6677
|
+
&& ["submitting", "accepted", "delivery-unknown"].includes(provider.run.status))) {
|
|
6678
|
+
throw usageError("Shared native execution is not proven quiescent; resolve that exact runtime boundary first.");
|
|
6679
|
+
}
|
|
6680
|
+
// Preserve the original unknown outcome and the unconsumed fixed wake.
|
|
6681
|
+
// This releases only the scheduling claim; it never replays, claims
|
|
6682
|
+
// acceptance, or asserts that Message requirements were implemented.
|
|
6683
|
+
recordTaskEvent(tx, task.id, "notification.resolved", {
|
|
6684
|
+
wakeId: wake.id, attemptId: claim.batchId, reason, outcome: "released-without-replay"
|
|
6685
|
+
}, now);
|
|
6686
|
+
tx.saveWorkMailbox(completeProcessing(mailbox, claim.batchId));
|
|
6687
|
+
return { taskId: task.id, wakeId: wake.id, outcome: "released-without-replay" };
|
|
6688
|
+
});
|
|
6689
|
+
notifyMailbox(options.runtime, leaderMailbox(result.taskId), result.taskId);
|
|
6690
|
+
return output(`Released ${result.wakeId} without replay; original acceptance remains unknown.\n`, result);
|
|
6691
|
+
}
|
|
5964
6692
|
if (subcommand === "list" || subcommand === "show") {
|
|
5965
6693
|
return taskWakeInspectionCommand(args, store);
|
|
5966
6694
|
}
|
|
@@ -5987,13 +6715,13 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
5987
6715
|
{ header: "Wake", minWidth: 8, maxWidth: 18 },
|
|
5988
6716
|
{ header: "Status", minWidth: 8, maxWidth: 12 },
|
|
5989
6717
|
{ header: "Reasons", minWidth: 10, maxWidth: 40 },
|
|
5990
|
-
{ header: "
|
|
6718
|
+
{ header: "AgentRun", minWidth: 10, maxWidth: 20 },
|
|
5991
6719
|
{ header: "Dispatched", minWidth: 10, maxWidth: 28 }
|
|
5992
6720
|
], wakes.map((wake) => [
|
|
5993
6721
|
wake.id,
|
|
5994
6722
|
wake.status,
|
|
5995
6723
|
wake.reasons.map(renderWakeReason).join(", "),
|
|
5996
|
-
wake.
|
|
6724
|
+
wake.runId ?? "-",
|
|
5997
6725
|
presentTime(wake.createdAt, timeZone)
|
|
5998
6726
|
]), defaultTableWidth())}\n`, { taskId: task.id, wakes });
|
|
5999
6727
|
}
|
|
@@ -6012,18 +6740,30 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
6012
6740
|
return ms > fromMs && ms <= toMs;
|
|
6013
6741
|
};
|
|
6014
6742
|
const allEvents = store.listEvents(task.id);
|
|
6015
|
-
const
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
const
|
|
6019
|
-
|
|
6743
|
+
const deliveryEvents = allEvents.filter((event) => (event.type === "notification.delivery" || event.type === "notification.resolved")
|
|
6744
|
+
&& (event.payload.wakeId === wake.id
|
|
6745
|
+
|| event.payload.attemptId?.startsWith(`notification:${task.id}/${wake.id}/`)));
|
|
6746
|
+
const referenced = (type, id) => wake.refs?.some(ref => ref.type === type && ref.id === id
|
|
6747
|
+
&& (!("taskId" in ref) || ref.taskId === task.id)) === true;
|
|
6748
|
+
const events = allEvents.filter((e) => inWindow(e.createdAt) || referenced("event", e.id));
|
|
6749
|
+
const messages = store.listMessages(task.id).filter((m) => inWindow(m.createdAt) || referenced("message", m.id));
|
|
6750
|
+
const allRuns = store.listRuns(task.id);
|
|
6751
|
+
const referencedRunIds = new Set(referencedWakeRunIds(allRuns, allEvents, events));
|
|
6752
|
+
for (const run of allRuns)
|
|
6753
|
+
if (referenced("run", run.id))
|
|
6754
|
+
referencedRunIds.add(run.id);
|
|
6755
|
+
for (const message of messages)
|
|
6756
|
+
if (message.resultRef?.type === "agent-run-result")
|
|
6757
|
+
referencedRunIds.add(message.resultRef.runId);
|
|
6758
|
+
const runs = operationalTaskRecords(allRuns, allEvents, "run").filter((run) => (inWindow(run.createdAt) || referencedRunIds.has(run.id)));
|
|
6020
6759
|
const lines = [
|
|
6021
6760
|
`Wake: ${wake.id}`,
|
|
6022
6761
|
`Task: ${task.id}`,
|
|
6023
6762
|
`Status: ${wake.status}`,
|
|
6763
|
+
`Notification: ${deliveryEvents.at(-1)?.payload.outcome ?? "unobserved"}`,
|
|
6024
6764
|
`Reasons: ${wake.reasons.map(renderWakeReason).join(", ")}`,
|
|
6025
6765
|
`Delta window: ${wake.fromCursor} → ${wake.toCursor}`,
|
|
6026
|
-
...(wake.
|
|
6766
|
+
...(wake.runId === undefined ? [] : [`AgentRun: ${wake.runId}`]),
|
|
6027
6767
|
`Dispatched: ${presentTime(wake.createdAt, timeZone)}`,
|
|
6028
6768
|
...(wake.consumedAt === undefined
|
|
6029
6769
|
? []
|
|
@@ -6032,19 +6772,20 @@ function taskWakeInspectionCommand(args, store) {
|
|
|
6032
6772
|
...events.map((e) => ` ${e.id} ${e.type} ${presentTime(e.createdAt, timeZone)}`),
|
|
6033
6773
|
`Messages (${messages.length}):`,
|
|
6034
6774
|
...messages.map((m) => ` ${m.id} [${taskMessageAuthorLabel(m.author)}] ${presentTime(m.createdAt, timeZone)}`),
|
|
6035
|
-
`
|
|
6036
|
-
...
|
|
6037
|
-
+ `${presentTime(
|
|
6038
|
-
+ `${
|
|
6039
|
-
? ` → yui task
|
|
6775
|
+
`AgentRuns (${runs.length}):`,
|
|
6776
|
+
...runs.map((run) => (` ${run.id} [${run.status}/${run.purpose}] ${run.roleName} `
|
|
6777
|
+
+ `${presentTime(run.createdAt, timeZone)}`
|
|
6778
|
+
+ `${referencedRunIds.has(run.id)
|
|
6779
|
+
? ` → yui task run show ${task.id}/${run.id}`
|
|
6040
6780
|
: ""}`))
|
|
6041
6781
|
];
|
|
6042
6782
|
return output(lines.join("\n").concat("\n"), {
|
|
6043
6783
|
taskId: task.id,
|
|
6784
|
+
deliveryEvents,
|
|
6044
6785
|
wake,
|
|
6045
6786
|
events,
|
|
6046
6787
|
messages,
|
|
6047
|
-
|
|
6788
|
+
runs
|
|
6048
6789
|
});
|
|
6049
6790
|
}
|
|
6050
6791
|
throw usageError(command === undefined
|
|
@@ -6175,8 +6916,8 @@ function leaderMailbox(taskId) {
|
|
|
6175
6916
|
function taskRef(id) {
|
|
6176
6917
|
return { type: "task", id };
|
|
6177
6918
|
}
|
|
6178
|
-
function
|
|
6179
|
-
return { type: "
|
|
6919
|
+
function runRef(taskId, id) {
|
|
6920
|
+
return { type: "run", taskId, id };
|
|
6180
6921
|
}
|
|
6181
6922
|
function workItemRef(taskId, id) {
|
|
6182
6923
|
return { type: "work-item", taskId, id };
|