@zq-silk/yui 0.15.9 → 0.15.12
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 +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +64 -16
- package/dist/cli/interactionPolicy.js +7 -3
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli/updateOrchestrator.js +24 -1
- package/dist/cli/updatePorts.js +7 -3
- package/dist/cli/upgradeCommand.js +42 -2
- package/dist/cli.js +381 -107
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +339 -4
- package/dist/commands/projectCommands.js +50 -22
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +586 -96
- package/dist/commands/taskIntegrationCommands.js +19 -39
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +55 -6
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +14 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
- package/dist/controller/globalInputDelivery.js +132 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +117 -7
- package/dist/controller/runtimeEventInbox.js +32 -3
- package/dist/controller/runtimeEventProcessor.js +26 -6
- package/dist/controller/runtimeHookRunFence.js +75 -19
- package/dist/controller/structuredProviderObservation.js +133 -70
- package/dist/coordination/workMailboxQueue.js +5 -0
- package/dist/execution/workItemExecutionProjection.js +1 -1
- package/dist/executor/agentExecutor.js +64 -4
- package/dist/executor/executorRegistry.js +3 -0
- package/dist/executor/fileRoleLaunchPlanner.js +78 -118
- package/dist/integration/deliveryObligation.js +2 -1
- package/dist/integration/gitIntegrationService.js +329 -386
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +367 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +430 -107
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +182 -101
- package/dist/repository/taskWorkspacePreparer.js +205 -72
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +179 -82
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +38 -6
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +26 -8
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +167 -4
- package/dist/storage/sqliteStore.js +57 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +129 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +147 -17
- package/dist/web/assets/client/components.js +56 -13
- package/dist/web/assets/client/i18n.js +78 -4
- package/dist/web/assets/client/taskSurface.js +108 -1
- package/dist/web/assets/client/view.js +39 -8
- package/dist/web/assets/shell.js +29 -0
- package/dist/web/assets/styles/layout.js +8 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +131 -4
- package/dist/web/webSnapshot.js +16 -6
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +111 -35
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/release-workflow.md +39 -0
- package/docs/release-workflow.zh-CN.md +29 -0
- package/docs/sqlite-control-plane-design.md +223 -1
- package/docs/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +40 -0
- package/docs/testing/verification-levels.zh-CN.md +23 -0
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +154 -51
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +19 -3
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +42 -0
- package/skills/yui-runtime/references/publication.md +42 -0
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
|
|
1
2
|
import { roleLaunchEventPayload, saveTaskRoleUpdate } from "../role/taskRoleUpdate.js";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { archiveDeliveryWarnings, archiveRetainedResources, renderArchiveDiagnostics, taskArchiveDiagnostics } from "../task/archiveDiagnostics.js";
|
|
5
|
+
import { archiveSettlementChecks } from "../task/archivePreflight.js";
|
|
6
|
+
import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
|
|
4
7
|
import { isDeepStrictEqual } from "node:util";
|
|
5
8
|
import { createRunInput } from "../context/runInputContract.js";
|
|
6
9
|
import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds, expandRunContextRef, freezeWorkItemExecutionAssignmentContextSnapshot, freezeReviewStageContextSnapshot, freezeRunContextSnapshot } from "../context/runContextPack.js";
|
|
@@ -20,7 +23,7 @@ import { agentExecutionComponentLabel } from "../agent/executionComponents.js";
|
|
|
20
23
|
import { agentRunConfigurationLabel, renderAgentRunConfiguration } from "../output/agentRunConfigurationPresentation.js";
|
|
21
24
|
import { formatTimestamp } from "../output/timePresentation.js";
|
|
22
25
|
import { renderRoleDetails, renderRoleLaunchComparison } from "../output/rolePresentation.js";
|
|
23
|
-
import { createTaskMessage, expandTaskMessageResult, taskMessageAuthorLabel, updateDraftTaskMessage, withSubmissionReceipt, TASK_SUBMISSION_INTENTS } from "../message/message.js";
|
|
26
|
+
import { createTaskMessage, expandTaskMessageResult, recordTaskMessageControlOutcome, taskMessageAuthorLabel, taskMessageInputControlState, updateDraftTaskMessage, withSubmissionReceipt, TASK_SUBMISSION_INTENTS } from "../message/message.js";
|
|
24
27
|
import { assertDraftTaskExecutionFree, validateDraftWorkItemEdit } from "../task/draftPlan.js";
|
|
25
28
|
import { TASK_PLANNING_ENTERED_EVENT, decideSubmissionRouting, describeSubmissionFeedback, draftActivationState, draftHasEnteredPlanning, normalizeSubmissionIntent, sameSubmissionTarget } from "../task/taskSubmission.js";
|
|
26
29
|
import { recordTaskActivationRequestInTransaction, taskActivationOperationRef } from "../task/taskActivationService.js";
|
|
@@ -47,6 +50,7 @@ import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTas
|
|
|
47
50
|
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
48
51
|
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
49
52
|
import { StorageConflictError } from "../storage/taskStore.js";
|
|
53
|
+
import { planningRuntimeCwd } from "../storage/homeLayout.js";
|
|
50
54
|
import { requireResolvedAgentProfileRuntime } from "../profile/agentProfileRuntime.js";
|
|
51
55
|
import { assertProjectActive, resolveProject } from "../repository/project.js";
|
|
52
56
|
import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, editWorkItemDefinition, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, prepareWorkItemDispatch, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
@@ -68,13 +72,16 @@ import { createProjectResources } from "../resources/projectResourceService.js";
|
|
|
68
72
|
import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArtifactRef.js";
|
|
69
73
|
import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
|
|
70
74
|
import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
|
|
71
|
-
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
|
|
72
|
-
import {
|
|
75
|
+
import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel, withTaskRoleHostObservation, taskRoleHostDiagnostic } from "./taskRoleRuntimeStatus.js";
|
|
76
|
+
import { openInputRequestCount, runTaskInputCommand } from "./taskInputCommands.js";
|
|
73
77
|
import { runGrantCommand } from "./grantCommands.js";
|
|
74
78
|
import { runWorkflowCommand } from "./workflowCommands.js";
|
|
75
|
-
import { taskLocalActor as resolveTaskLocalActor, assertTaskDeliveryAuthority } from "./taskActor.js";
|
|
79
|
+
import { taskLocalActor as resolveTaskLocalActor, assertTaskDeliveryAuthority, assertTaskInputControlAuthority } from "./taskActor.js";
|
|
76
80
|
import { currentManagedRuntime, resolveManagedTaskReader } from "../runtime/managedCaller.js";
|
|
81
|
+
import { parseTaskCatalogOptions, readTaskCatalog, renderTaskCatalog, taskCatalogScope } from "../context/taskCatalog.js";
|
|
77
82
|
import { resolveMessageRecipient, messageContinuationBlocker } from "../message/messageContinuation.js";
|
|
83
|
+
import { findTaskInterrupt, reserveTaskInterrupt, taskInterruptReceipt, taskInterruptWasRejected } from "../message/taskInterrupt.js";
|
|
84
|
+
import { resolveTaskInputControl } from "../message/inputControlResolution.js";
|
|
78
85
|
import { enqueueOperatorEvent } from "../scheduler/operatorEvent.js";
|
|
79
86
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
80
87
|
import { renderWakeReason, wakeReason } from "../scheduler/wakeReason.js";
|
|
@@ -308,7 +315,7 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
308
315
|
switch (command) {
|
|
309
316
|
case "create": return createTaskCommand(rest, store, options);
|
|
310
317
|
case "update": return output(updateTaskCommand(rest, store, options));
|
|
311
|
-
case "list": return listTaskCommand(rest, store);
|
|
318
|
+
case "list": return listTaskCommand(rest, store, options.environment);
|
|
312
319
|
case "show": return showTaskCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
313
320
|
case "context": return runTaskContextCommand(rest, store, options.environment);
|
|
314
321
|
case "next-action": return runTaskNextActionCommand(rest, store, options.actualTaskReviewCandidate ?? null);
|
|
@@ -317,7 +324,7 @@ export function runTaskCommand(args, store, options = {}) {
|
|
|
317
324
|
case "activation": return runTaskActivationCommand(rest, store, options);
|
|
318
325
|
case "complete": return completeTaskCommand(rest, store, options);
|
|
319
326
|
case "reopen": return output(reopenTaskCommand(rest, store, options));
|
|
320
|
-
case "archive": return
|
|
327
|
+
case "archive": return archiveTaskCommand(rest, store, options);
|
|
321
328
|
case "retire": return retireTaskCommand(rest, store, options);
|
|
322
329
|
case "cancel": return cancelTaskCommand(rest, store, options);
|
|
323
330
|
case "reconcile": return output(reconcileTaskCommand(rest, store, options));
|
|
@@ -545,7 +552,7 @@ export function updateTaskMetadataCommand(store, taskId, patch, options = {}) {
|
|
|
545
552
|
notifyMailbox(options.runtime, taskMailbox(result.id), result.id);
|
|
546
553
|
return result;
|
|
547
554
|
}
|
|
548
|
-
function routeUserSubmission(tx, task, actor, body, intent, now, submissionKey, target) {
|
|
555
|
+
function routeUserSubmission(tx, task, actor, body, intent, now, submissionKey, target, inputControl) {
|
|
549
556
|
const kind = actor;
|
|
550
557
|
const author = actor === "operator"
|
|
551
558
|
? { type: "operator" }
|
|
@@ -564,7 +571,8 @@ function routeUserSubmission(tx, task, actor, body, intent, now, submissionKey,
|
|
|
564
571
|
// how routing then resolves (§2.5). Intent is stored, never re-read from the body.
|
|
565
572
|
const message = appendMessage(tx, task.id, body, kind, author, now, {
|
|
566
573
|
intent,
|
|
567
|
-
...(submissionKey === undefined ? {} : { submissionKey })
|
|
574
|
+
...(submissionKey === undefined ? {} : { submissionKey }),
|
|
575
|
+
...(inputControl === undefined ? {} : { inputControl })
|
|
568
576
|
});
|
|
569
577
|
// Re-read phase and activation inside this same transaction: this is the point
|
|
570
578
|
// the race is decided at (§2.3).
|
|
@@ -900,10 +908,15 @@ function createTaskAggregate(store, title, metadata, now, defaultProjectIds = []
|
|
|
900
908
|
enqueueWork(store, taskMailbox(task.id), "task-created", now, [taskRef(task.id)]);
|
|
901
909
|
return { task, leader };
|
|
902
910
|
}
|
|
903
|
-
function listTaskCommand(args, store) {
|
|
911
|
+
function listTaskCommand(args, store, environment = {}) {
|
|
912
|
+
if (args.includes("--view")) {
|
|
913
|
+
const result = readTaskCatalog(store, parseTaskCatalogOptions(args), environment);
|
|
914
|
+
return output(renderTaskCatalog(result), result);
|
|
915
|
+
}
|
|
916
|
+
const taskId = taskCatalogScope(store, environment);
|
|
904
917
|
const options = parseTaskListOptions(args);
|
|
905
918
|
const snapshot = store.transaction((reader) => ({
|
|
906
|
-
result: buildTaskOverview(reader, options),
|
|
919
|
+
result: buildTaskOverview(reader, options, new Date(), taskId),
|
|
907
920
|
timeZone: reader.getConfig().timeZone
|
|
908
921
|
}));
|
|
909
922
|
const rendered = renderTaskOverview(snapshot.result, options, snapshot.timeZone, defaultTableWidth());
|
|
@@ -924,6 +937,7 @@ function showTaskCommand(args, store, currentTaskCandidate) {
|
|
|
924
937
|
const integrations = store.listIntegrationAttempts(task.id);
|
|
925
938
|
const publications = store.listPublicationReferences(task.id);
|
|
926
939
|
const remoteDelivery = projectTaskRemoteDeliveryFromStore(store, task, currentTaskCandidate);
|
|
940
|
+
const archive = task.status === "archived" ? taskArchiveDiagnostics(store, task) : undefined;
|
|
927
941
|
const verifiedMergedPublications = publications.filter((reference) => (reference.state === "merged" && reference.verification === "verified")).length;
|
|
928
942
|
const currentMessageCount = operationalTaskRecords(messages, events, "message").length;
|
|
929
943
|
const currentWorkItemCount = work.filter(({ status }) => status !== "retired").length;
|
|
@@ -981,6 +995,7 @@ function showTaskCommand(args, store, currentTaskCandidate) {
|
|
|
981
995
|
`Integration Attempts: ${counts.integrations}`,
|
|
982
996
|
`Publication references: ${counts.publications} (${verifiedMergedPublications} verified merged)`,
|
|
983
997
|
renderTaskRemoteDelivery(remoteDelivery).trimEnd(),
|
|
998
|
+
...(archive === undefined ? [] : [renderArchiveDiagnostics(archive).trimEnd()]),
|
|
984
999
|
`Open inputs: ${counts.openInputs}`,
|
|
985
1000
|
`Created: ${presentTime(task.createdAt, timeZone)}`,
|
|
986
1001
|
`Updated: ${presentTime(task.updatedAt, timeZone)}`
|
|
@@ -989,7 +1004,8 @@ function showTaskCommand(args, store, currentTaskCandidate) {
|
|
|
989
1004
|
task,
|
|
990
1005
|
counts,
|
|
991
1006
|
hasBrief: brief !== null,
|
|
992
|
-
remoteDelivery
|
|
1007
|
+
remoteDelivery,
|
|
1008
|
+
...(archive === undefined ? {} : { archive })
|
|
993
1009
|
});
|
|
994
1010
|
}
|
|
995
1011
|
function activateTaskCommand(args, store, options) {
|
|
@@ -1254,50 +1270,44 @@ function archiveTaskCommand(args, store, options) {
|
|
|
1254
1270
|
&& task.status !== "cancelled") {
|
|
1255
1271
|
throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
|
|
1256
1272
|
}
|
|
1257
|
-
const remoteDelivery = request.
|
|
1258
|
-
?
|
|
1259
|
-
:
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
if (
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
||
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
+ "stop its runtime before archiving.");
|
|
1285
|
-
}
|
|
1286
|
-
const liveSessionRole = tx.listRoles(task.id).find((role) => {
|
|
1287
|
-
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1288
|
-
const session = sessions?.sessions[sessions.activeAgentId];
|
|
1289
|
-
return session !== undefined && session.status !== "ended";
|
|
1290
|
-
});
|
|
1291
|
-
if (liveSessionRole !== undefined) {
|
|
1292
|
-
throw usageError(`Task ${task.id} still has a live Session for Role ${liveSessionRole.name}; `
|
|
1293
|
-
+ "stop that Session before archiving.");
|
|
1273
|
+
const remoteDelivery = request.force
|
|
1274
|
+
? projectTaskRemoteDeliveryFromStore(tx, task)
|
|
1275
|
+
: request.disposition === "integrated"
|
|
1276
|
+
? assertTaskRemoteDeliveryProof(tx, task, options.archiveRemoteDeliveryProof)
|
|
1277
|
+
: undefined;
|
|
1278
|
+
if (!request.force) {
|
|
1279
|
+
const checks = archiveSettlementChecks(tx, task);
|
|
1280
|
+
if (checks.length > 0)
|
|
1281
|
+
throw new CleanupInspectionError(checks);
|
|
1282
|
+
if (task.cwd !== undefined || tx.listManagedWorkspaces(task.id).length > 0) {
|
|
1283
|
+
throw usageError(`Task ${task.id} still has managed worktrees; clean them before archiving.`);
|
|
1284
|
+
}
|
|
1285
|
+
const activeRole = tx.listRoles(task.id)
|
|
1286
|
+
.find((role) => tx.getActiveRun(task.id, role.name) !== null);
|
|
1287
|
+
if (activeRole !== undefined) {
|
|
1288
|
+
throw usageError(`Task ${task.id} still has an active AgentRun for Role ${activeRole.name}; `
|
|
1289
|
+
+ "stop its runtime before archiving.");
|
|
1290
|
+
}
|
|
1291
|
+
const liveSessionRole = tx.listRoles(task.id).find((role) => {
|
|
1292
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
1293
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
1294
|
+
return session !== undefined && session.status !== "ended";
|
|
1295
|
+
});
|
|
1296
|
+
if (liveSessionRole !== undefined) {
|
|
1297
|
+
throw usageError(`Task ${task.id} still has a live Session for Role ${liveSessionRole.name}; `
|
|
1298
|
+
+ "stop that Session before archiving.");
|
|
1299
|
+
}
|
|
1294
1300
|
}
|
|
1295
|
-
const
|
|
1301
|
+
const retainedResources = request.force ? archiveRetainedResources(tx, task) : [];
|
|
1302
|
+
const warnings = request.force && remoteDelivery !== undefined ? archiveDeliveryWarnings(remoteDelivery) : [];
|
|
1303
|
+
const archived = { ...archiveTask(task, now, { by: actor }), executionGate: { state: "stopped" } };
|
|
1296
1304
|
tx.saveTask(archived);
|
|
1297
|
-
tx.clearPendingWakeup(task.id);
|
|
1298
1305
|
tx.clearLeaderFailure(task.id);
|
|
1299
|
-
|
|
1300
|
-
tx.
|
|
1306
|
+
if (!request.force) {
|
|
1307
|
+
tx.clearPendingWakeup(task.id);
|
|
1308
|
+
for (const role of tx.listRoles(task.id)) {
|
|
1309
|
+
tx.removeWorkMailbox(roleMailbox(task.id, role.name));
|
|
1310
|
+
}
|
|
1301
1311
|
}
|
|
1302
1312
|
const remoteProjectHeads = remoteDelivery === undefined
|
|
1303
1313
|
? undefined
|
|
@@ -1314,27 +1324,29 @@ function archiveTaskCommand(args, store, options) {
|
|
|
1314
1324
|
recordTaskEvent(tx, task.id, "task.archived", {
|
|
1315
1325
|
by: actor,
|
|
1316
1326
|
workspaceDisposition: request.disposition,
|
|
1327
|
+
...(request.force ? {
|
|
1328
|
+
force: "true", cleanup: "pending",
|
|
1329
|
+
warnings: JSON.stringify(warnings),
|
|
1330
|
+
retainedResources: JSON.stringify(retainedResources)
|
|
1331
|
+
} : {}),
|
|
1317
1332
|
...(remoteDelivery === undefined
|
|
1318
1333
|
? {}
|
|
1319
1334
|
: {
|
|
1320
1335
|
mergeCoverage: remoteDelivery.status,
|
|
1321
1336
|
allMerged: String(remoteDelivery.allMerged),
|
|
1322
|
-
allVerified: String(remoteDelivery.allVerified)
|
|
1323
|
-
...(request.forceUnverified && !remoteDelivery.allVerified
|
|
1324
|
-
? { verificationOverride: "true" }
|
|
1325
|
-
: {})
|
|
1337
|
+
allVerified: String(remoteDelivery.allVerified)
|
|
1326
1338
|
}),
|
|
1327
1339
|
...(remoteProjectHeads === undefined ? {} : { projectHeads: remoteProjectHeads }),
|
|
1328
1340
|
...(remoteProjectBases === undefined ? {} : { projectBases: remoteProjectBases })
|
|
1329
1341
|
}, now);
|
|
1330
|
-
enqueueWork(tx, taskMailbox(task.id), "task-archived", now, [taskRef(task.id)]);
|
|
1331
1342
|
return { task: archived, changed: true };
|
|
1332
1343
|
});
|
|
1333
|
-
if (result.changed)
|
|
1334
|
-
|
|
1335
|
-
|
|
1344
|
+
if (result.changed && !request.force)
|
|
1345
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
1346
|
+
const diagnostics = taskArchiveDiagnostics(store, result.task);
|
|
1347
|
+
return output((result.changed
|
|
1336
1348
|
? `Archived task ${result.task.id}\n`
|
|
1337
|
-
: `Task ${result.task.id} is already archived\n
|
|
1349
|
+
: `Task ${result.task.id} is already archived\n`) + renderArchiveDiagnostics(diagnostics), { task: result.task, changed: result.changed, ...diagnostics });
|
|
1338
1350
|
}
|
|
1339
1351
|
function cancelTaskCommand(args, store, options) {
|
|
1340
1352
|
const usage = "Task cancel usage: yui task cancel <task> (--summary <text>|--summary-file <path|->).";
|
|
@@ -1400,6 +1412,7 @@ function retireTaskCommand(args, store, options) {
|
|
|
1400
1412
|
}
|
|
1401
1413
|
const unresolvedIntegration = tx.listIntegrationAttempts(task.id).find((integration) => (integration.status === "running"
|
|
1402
1414
|
|| integration.status === "blocked"
|
|
1415
|
+
|| integration.status === "conflicted"
|
|
1403
1416
|
|| integration.status === "validating"));
|
|
1404
1417
|
if (unresolvedIntegration !== undefined) {
|
|
1405
1418
|
throw usageError(`Task ${task.id} has an unresolved Integration Attempt: ${unresolvedIntegration.id}.`);
|
|
@@ -1493,9 +1506,9 @@ function assertTaskRetirementProof(store, task, proof) {
|
|
|
1493
1506
|
throw usageError(`Task workspaces changed after retirement preflight: ${task.id}.`);
|
|
1494
1507
|
}
|
|
1495
1508
|
}
|
|
1496
|
-
export function parseTaskArchiveArguments(args) {
|
|
1509
|
+
export function parseTaskArchiveArguments(args, command = "archive") {
|
|
1497
1510
|
const usage = "Task archive usage: "
|
|
1498
|
-
+
|
|
1511
|
+
+ `yui task ${command} <id> (--integrated|--abandon) [--force].`;
|
|
1499
1512
|
const taskId = args[0]?.trim();
|
|
1500
1513
|
const flags = args.slice(1);
|
|
1501
1514
|
if (taskId === undefined
|
|
@@ -1506,14 +1519,14 @@ export function parseTaskArchiveArguments(args) {
|
|
|
1506
1519
|
}
|
|
1507
1520
|
const integrated = flags.includes("--integrated");
|
|
1508
1521
|
const abandoned = flags.includes("--abandon");
|
|
1509
|
-
const
|
|
1510
|
-
if (integrated === abandoned
|
|
1522
|
+
const force = flags.includes("--force");
|
|
1523
|
+
if (integrated === abandoned) {
|
|
1511
1524
|
throw usageError(usage);
|
|
1512
1525
|
}
|
|
1513
1526
|
return {
|
|
1514
1527
|
taskId,
|
|
1515
1528
|
disposition: integrated ? "integrated" : "abandoned",
|
|
1516
|
-
|
|
1529
|
+
force
|
|
1517
1530
|
};
|
|
1518
1531
|
}
|
|
1519
1532
|
function formatProjectCommits(projects) {
|
|
@@ -1532,14 +1545,10 @@ export function validateTaskArchiveRequest(args, store, options = {}) {
|
|
|
1532
1545
|
&& task.status !== "cancelled") {
|
|
1533
1546
|
throw usageError(`Task ${task.id} must be completed or retired before it can be archived.`);
|
|
1534
1547
|
}
|
|
1535
|
-
if (task.status !== "archived") {
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|| integration.status === "validating"));
|
|
1540
|
-
if (unresolvedIntegration !== undefined) {
|
|
1541
|
-
throw usageError(`Task ${task.id} has an unresolved Integration Attempt: ${unresolvedIntegration.id}.`);
|
|
1542
|
-
}
|
|
1548
|
+
if (task.status !== "archived" && !request.force) {
|
|
1549
|
+
const checks = archiveSettlementChecks(store, task);
|
|
1550
|
+
if (checks.length > 0)
|
|
1551
|
+
throw new CleanupInspectionError(checks);
|
|
1543
1552
|
}
|
|
1544
1553
|
return request;
|
|
1545
1554
|
}
|
|
@@ -1637,6 +1646,10 @@ function taskMessageCommand(args, store, options) {
|
|
|
1637
1646
|
? { state: "queued" } : { state: "saved" };
|
|
1638
1647
|
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 });
|
|
1639
1648
|
}
|
|
1649
|
+
if (command === "queue")
|
|
1650
|
+
return queueTaskMessage(rest, store, options);
|
|
1651
|
+
if (command === "steer")
|
|
1652
|
+
return steerTaskMessage(rest, store, options);
|
|
1640
1653
|
if (command === "list") {
|
|
1641
1654
|
const messageListUsage = "Task message list usage: yui task message list <id> [--after <timestamp>] [--limit <n>].";
|
|
1642
1655
|
const parsed = parseTail(rest, new Set(["--after", "--limit"]), messageListUsage);
|
|
@@ -1690,6 +1703,149 @@ function taskMessageCommand(args, store, options) {
|
|
|
1690
1703
|
? "Task message command is required."
|
|
1691
1704
|
: `Unknown command: task message ${command}`);
|
|
1692
1705
|
}
|
|
1706
|
+
/** CLI and authenticated user Surface share the same message and mailbox
|
|
1707
|
+
* transaction. Talking to Leader does not impersonate Leader authority. */
|
|
1708
|
+
/**
|
|
1709
|
+
* `queue` is the explicit, idempotent form of the existing send: it persists a
|
|
1710
|
+
* Message tagged with the durable `queue` action and a stable requestId, and
|
|
1711
|
+
* relies on the identical continuation path as `send`. Repeating the exact same
|
|
1712
|
+
* (requestId, body) returns the original Message and delivers nothing twice;
|
|
1713
|
+
* reusing the requestId with different content is a conflict, never a second
|
|
1714
|
+
* input (decision-3 §5/§6).
|
|
1715
|
+
*/
|
|
1716
|
+
function queueTaskMessage(args, store, options) {
|
|
1717
|
+
const usage = "Task message queue usage: yui task message queue <id> (<body>|--body-file <path|->) --request-id <id> [--to <role> --work-item <id>|--review-round <id>].";
|
|
1718
|
+
const parsed = parseTail(args, new Set(["--body-file", "--request-id", "--to", "--work-item", "--review-round"]), usage);
|
|
1719
|
+
if (parsed.positionals.length < 1 || parsed.positionals.length > 2)
|
|
1720
|
+
throw usageError(usage);
|
|
1721
|
+
const body = readCommandText(parsed.positionals[1], parsed.options.get("--body-file"), "--body", usage);
|
|
1722
|
+
const requestId = requiredOption(parsed.options, "--request-id");
|
|
1723
|
+
const recipientRole = parsed.options.get("--to");
|
|
1724
|
+
const workItemId = parsed.options.get("--work-item");
|
|
1725
|
+
const reviewRoundId = parsed.options.get("--review-round");
|
|
1726
|
+
if (recipientRole === undefined && (workItemId !== undefined || reviewRoundId !== undefined)) {
|
|
1727
|
+
throw usageError("--to is required for scoped Message delivery.");
|
|
1728
|
+
}
|
|
1729
|
+
const result = sendTaskMessageCommand(store, parsed.positionals[0], body, undefined, options, recipientRole === undefined ? undefined : { roleName: recipientRole, workItemId, reviewRoundId }, undefined, undefined, { action: "queue", requestId });
|
|
1730
|
+
const reason = result.message.continuation?.notDeliveredReason;
|
|
1731
|
+
const delivery = result.idempotentReplay ? { state: "idempotent-replay" }
|
|
1732
|
+
: reason !== undefined ? { state: "not-delivered", reason }
|
|
1733
|
+
: recipientRole !== undefined || result.actor !== "leader" ? { state: "queued" } : { state: "saved" };
|
|
1734
|
+
return output(`Queued message ${result.message.id} to ${result.task.id} (${delivery.state}${reason === undefined ? "" : `: ${reason}`}).\n`, { taskId: result.task.id, message: result.message, delivery });
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* `steer` targets only the exact current native Turn. It always persists the
|
|
1738
|
+
* Message (so an unsupported or missed steer stays visible and re-choosable),
|
|
1739
|
+
* then resolves the live target from durable state with no Provider call. On a
|
|
1740
|
+
* ready resolution it returns a structured intent the CLI performs against the
|
|
1741
|
+
* Agent Host; on any explicit failure it returns the saved Message plus the
|
|
1742
|
+
* exact error code, and never falls back to an interrupt or a queue
|
|
1743
|
+
* (decision-3 §1/§5). The steer Message is excluded from the queued
|
|
1744
|
+
* continuation path, so a failed live attempt cannot silently become a queue.
|
|
1745
|
+
*/
|
|
1746
|
+
function steerTaskMessage(args, store, options) {
|
|
1747
|
+
const usage = "Task message steer usage: yui task message steer <id> (<body>|--body-file <path|->) --request-id <id> --expected-target <turn> [--to <role> --work-item <id>|--review-round <id>].";
|
|
1748
|
+
const parsed = parseTail(args, new Set(["--body-file", "--request-id", "--expected-target", "--to", "--work-item", "--review-round"]), usage);
|
|
1749
|
+
if (parsed.positionals.length < 1 || parsed.positionals.length > 2)
|
|
1750
|
+
throw usageError(usage);
|
|
1751
|
+
const body = readCommandText(parsed.positionals[1], parsed.options.get("--body-file"), "--body", usage);
|
|
1752
|
+
const requestId = requiredOption(parsed.options, "--request-id");
|
|
1753
|
+
const expectedTarget = requiredOption(parsed.options, "--expected-target");
|
|
1754
|
+
const recipientRole = parsed.options.get("--to");
|
|
1755
|
+
const workItemId = parsed.options.get("--work-item");
|
|
1756
|
+
const reviewRoundId = parsed.options.get("--review-round");
|
|
1757
|
+
if (recipientRole === undefined) {
|
|
1758
|
+
throw usageError("steer requires --to <role>; it targets that Role's exact current Turn.");
|
|
1759
|
+
}
|
|
1760
|
+
// decision-3 §9: a Task Leader's current native turn (including a Draft's
|
|
1761
|
+
// planning turn) is a real turn but NOT an implicit AgentRun, so steering it
|
|
1762
|
+
// never establishes a Worker-style Assignment. It is persisted as an ordinary
|
|
1763
|
+
// no-Run Leader notification (recipient undefined, wake "none" so it never
|
|
1764
|
+
// spawns a Leader AgentRun) and its live target is the Leader's own turn. A
|
|
1765
|
+
// Worker/Reviewer steer keeps the exact Assignment recipient it always had.
|
|
1766
|
+
const leaderTarget = recipientRole === "leader";
|
|
1767
|
+
if (leaderTarget && (workItemId !== undefined || reviewRoundId !== undefined)) {
|
|
1768
|
+
throw usageError("Steering the Leader targets its current turn directly; it takes no --work-item/--review-round Assignment.");
|
|
1769
|
+
}
|
|
1770
|
+
const result = leaderTarget
|
|
1771
|
+
? sendTaskMessageCommand(store, parsed.positionals[0], body, "none", options, undefined, undefined, undefined, { action: "steer", requestId, expectedTarget })
|
|
1772
|
+
: sendTaskMessageCommand(store, parsed.positionals[0], body, undefined, options, { roleName: recipientRole, workItemId, reviewRoundId }, undefined, undefined, { action: "steer", requestId, expectedTarget });
|
|
1773
|
+
const roleName = result.message.recipient?.roleName ?? recipientRole;
|
|
1774
|
+
if (result.idempotentReplay) {
|
|
1775
|
+
// A replay is a later reader (another CLI invocation): surface the durable
|
|
1776
|
+
// control state the Host settlement folded onto this exact Message, so an
|
|
1777
|
+
// unknown steer is visibly not-submitted/pending/accepted/rejected/
|
|
1778
|
+
// delivery-unknown rather than silently re-attempted (message-5 gap D).
|
|
1779
|
+
const controlState = taskMessageInputControlState(result.message);
|
|
1780
|
+
return output(`Steer message ${result.message.id} already recorded (idempotent-replay${controlState === undefined ? "" : `; control ${controlState}`}).\n`, { taskId: result.task.id, message: result.message,
|
|
1781
|
+
steer: { state: "idempotent-replay",
|
|
1782
|
+
...(controlState === undefined ? {} : { control: controlState }) } });
|
|
1783
|
+
}
|
|
1784
|
+
// The static resolution is the testable contract: capability gate, exact-turn
|
|
1785
|
+
// match, and writer fence, all from durable state without a live Endpoint.
|
|
1786
|
+
const resolution = resolveTaskInputControl(store, result.task.id, roleName, "steer", expectedTarget);
|
|
1787
|
+
if (resolution.outcome !== "ready") {
|
|
1788
|
+
return output(`Steer message ${result.message.id} saved but not delivered (${resolution.code}: ${resolution.detail}).\n`, { taskId: result.task.id, message: result.message,
|
|
1789
|
+
steer: { state: "not-steered", code: resolution.code, detail: resolution.detail } });
|
|
1790
|
+
}
|
|
1791
|
+
if (!leaderTarget) {
|
|
1792
|
+
const current = store.getActiveRun(result.task.id, roleName);
|
|
1793
|
+
const native = store.getTaskRoleSessionSet(result.task.id, roleName)?.providerBinding?.run;
|
|
1794
|
+
const recipient = result.message.recipient;
|
|
1795
|
+
if (current === null || native?.runId !== current.id
|
|
1796
|
+
|| recipient?.ownerRunId !== current.id
|
|
1797
|
+
|| recipient.workItemId !== current.workItemId
|
|
1798
|
+
|| recipient.reviewRoundId !== current.reviewRoundId
|
|
1799
|
+
|| messageContinuationBlocker(store, result.message) !== undefined) {
|
|
1800
|
+
return output(`Steer message ${result.message.id} saved but not delivered (TARGET_CHANGED: active Assignment differs).\n`, { taskId: result.task.id, message: result.message,
|
|
1801
|
+
steer: { state: "not-steered", code: "TARGET_CHANGED", detail: "The input does not belong to the active Turn's Assignment." } });
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
const receiptId = `steer:${result.task.id}/${result.message.id}`;
|
|
1805
|
+
// Register the one independent control attempt as `pending` before the live
|
|
1806
|
+
// edge runs (decision-3 §3, message-5 gap D). This is the messageRef-associated
|
|
1807
|
+
// control op that makes a dispatched-but-unproven steer visibly distinct from a
|
|
1808
|
+
// steer that was merely saved and never attempted (`not-submitted`). It is
|
|
1809
|
+
// keyed by this exact receiptId and the steer's own requestId, and is monotonic
|
|
1810
|
+
// and idempotent, so the Host's later settlement fold promotes it to a proven
|
|
1811
|
+
// terminal and never rewinds it. An idempotent replay above already returned
|
|
1812
|
+
// the saved Message, so this records the attempt exactly once.
|
|
1813
|
+
store.transaction((tx) => {
|
|
1814
|
+
const saved = tx.listMessages(result.task.id).find((entry) => entry.id === result.message.id);
|
|
1815
|
+
const requestId = saved?.inputControl?.requestId;
|
|
1816
|
+
if (saved === undefined || requestId === undefined)
|
|
1817
|
+
return;
|
|
1818
|
+
tx.updateMessage(result.task.id, recordTaskMessageControlOutcome(saved, {
|
|
1819
|
+
requestId, receiptId, outcome: "pending", observedAt: clock(options)
|
|
1820
|
+
}));
|
|
1821
|
+
});
|
|
1822
|
+
return {
|
|
1823
|
+
kind: "input-steer",
|
|
1824
|
+
taskId: result.task.id,
|
|
1825
|
+
roleName,
|
|
1826
|
+
messageId: result.message.id,
|
|
1827
|
+
target: resolution.target,
|
|
1828
|
+
receiptId,
|
|
1829
|
+
// decision-3 §9: deliver the input as an authorized, reconcilable delta, not
|
|
1830
|
+
// loose body text and not a bare Message id. The Host additionally decorates
|
|
1831
|
+
// this with the Session Manifest read pointer, so the running turn can read
|
|
1832
|
+
// the exact durable Message through its authorized Context path.
|
|
1833
|
+
text: steerInputDelivery(result.task.id, result.message.id, roleName, body),
|
|
1834
|
+
output: `Steering ${result.task.id}/${roleName} at Turn ${resolution.target.nativeTurnId ?? resolution.target.attemptId} with message ${result.message.id}.\n`
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
/** Compose the exact input a steer pushes into the live turn: a bounded header
|
|
1838
|
+
* that names the durable Message and states receipt-is-not-acceptance, then the
|
|
1839
|
+
* verbatim body. The header keeps the input reconcilable with durable state
|
|
1840
|
+
* (decision-3 §9) without copying the body twice or fabricating a Run. */
|
|
1841
|
+
function steerInputDelivery(taskId, messageId, roleName, body) {
|
|
1842
|
+
return [
|
|
1843
|
+
`[Steer ${taskId}/${messageId} → ${roleName}] Additional input to your current turn; receipt is not acceptance of prior work.`,
|
|
1844
|
+
`It is saved durably as Message ${messageId}; reconcile it through your Session Manifest's authorized Context read path.`,
|
|
1845
|
+
"",
|
|
1846
|
+
body
|
|
1847
|
+
].join("\n");
|
|
1848
|
+
}
|
|
1693
1849
|
/** CLI and authenticated user Surface share the same message and mailbox
|
|
1694
1850
|
* transaction. Talking to Leader does not impersonate Leader authority. */
|
|
1695
1851
|
/**
|
|
@@ -1708,7 +1864,7 @@ function taskMessageCommand(args, store, options) {
|
|
|
1708
1864
|
* execution is decided by the Controller from the Task's own status, so this
|
|
1709
1865
|
* function never names a purpose and no second planning path exists.
|
|
1710
1866
|
*/
|
|
1711
|
-
export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options = {}, recipient, intent, submissionKey) {
|
|
1867
|
+
export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options = {}, recipient, intent, submissionKey, inputControl) {
|
|
1712
1868
|
if (!body.trim())
|
|
1713
1869
|
throw usageError("Message body is required.");
|
|
1714
1870
|
if (recipient !== undefined && wakePolicy !== undefined) {
|
|
@@ -1723,6 +1879,32 @@ export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options
|
|
|
1723
1879
|
const now = clock(options);
|
|
1724
1880
|
const result = store.transaction((tx) => {
|
|
1725
1881
|
const task = requireTask(tx, taskId);
|
|
1882
|
+
// A stable requestId makes the whole send idempotent: an exact repeat
|
|
1883
|
+
// returns the original Message, and any different body/action/recipient/
|
|
1884
|
+
// target under the same id is a conflicting reuse, never a silent second
|
|
1885
|
+
// input (decision-3 §6). The prior input is matched by requestId across
|
|
1886
|
+
// both a live inputControl and the reusedInput provenance an interrupt-then
|
|
1887
|
+
// handoff preserved, so re-tagging a steer as a handoff never frees its
|
|
1888
|
+
// requestId to create a second Message.
|
|
1889
|
+
if (inputControl !== undefined) {
|
|
1890
|
+
const existing = tx.listMessages(task.id).find((entry) => entry.inputControl?.requestId === inputControl.requestId
|
|
1891
|
+
|| entry.interruptThen?.reusedInput?.requestId === inputControl.requestId);
|
|
1892
|
+
if (existing !== undefined) {
|
|
1893
|
+
const prior = existing.inputControl ?? existing.interruptThen?.reusedInput;
|
|
1894
|
+
const priorRecipient = existing.recipient === undefined ? undefined
|
|
1895
|
+
: { roleName: existing.recipient.roleName, workItemId: existing.recipient.workItemId,
|
|
1896
|
+
reviewRoundId: existing.recipient.reviewRoundId };
|
|
1897
|
+
const nextRecipient = recipient === undefined ? undefined
|
|
1898
|
+
: { roleName: recipient.roleName, workItemId: recipient.workItemId, reviewRoundId: recipient.reviewRoundId };
|
|
1899
|
+
if (prior?.action !== inputControl.action || existing.body !== body
|
|
1900
|
+
|| prior.expectedTarget !== inputControl.expectedTarget
|
|
1901
|
+
|| !isDeepStrictEqual(priorRecipient, nextRecipient)) {
|
|
1902
|
+
throw usageError(`Input requestId ${inputControl.requestId} was already used with different content or target; use a new requestId for a new input.`);
|
|
1903
|
+
}
|
|
1904
|
+
return { task, message: existing, actor: replayActor(existing), queuedForLeader: false,
|
|
1905
|
+
feedback: existing.submissionReceipt?.feedback, idempotentReplay: true };
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1726
1908
|
if (recipient === undefined)
|
|
1727
1909
|
assertTaskOpen(task);
|
|
1728
1910
|
const caller = currentManagedRuntime(tx, options.environment, task.id);
|
|
@@ -1743,10 +1925,10 @@ export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options
|
|
|
1743
1925
|
// gain develop authority (task-32 §2.5).
|
|
1744
1926
|
if (recipient === undefined && (actor === "user" || actor === "operator")) {
|
|
1745
1927
|
const effectiveIntent = normalizeSubmissionIntent(intent, wakePolicy);
|
|
1746
|
-
const routed = routeUserSubmission(tx, task, actor, body, effectiveIntent, now, submissionKey, { kind: "task", taskId: task.id });
|
|
1928
|
+
const routed = routeUserSubmission(tx, task, actor, body, effectiveIntent, now, submissionKey, { kind: "task", taskId: task.id }, inputControl);
|
|
1747
1929
|
return {
|
|
1748
1930
|
task: routed.task, message: routed.message, actor,
|
|
1749
|
-
queuedForLeader: routed.queuedForLeader, feedback: routed.feedback
|
|
1931
|
+
queuedForLeader: routed.queuedForLeader, feedback: routed.feedback, idempotentReplay: false
|
|
1750
1932
|
};
|
|
1751
1933
|
}
|
|
1752
1934
|
if (intent !== undefined) {
|
|
@@ -1764,7 +1946,8 @@ export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options
|
|
|
1764
1946
|
});
|
|
1765
1947
|
const context = {
|
|
1766
1948
|
...(target === undefined ? {} : { recipient: target }),
|
|
1767
|
-
...(recipient?.workItemId === undefined ? {} : { workItemId: recipient.workItemId })
|
|
1949
|
+
...(recipient?.workItemId === undefined ? {} : { workItemId: recipient.workItemId }),
|
|
1950
|
+
...(inputControl === undefined ? {} : { inputControl })
|
|
1768
1951
|
};
|
|
1769
1952
|
const message = actor === "leader"
|
|
1770
1953
|
? appendMessage(tx, task.id, body, "role-result", { type: "role", roleName: LEADER_ROLE }, now, context)
|
|
@@ -1791,8 +1974,10 @@ export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options
|
|
|
1791
1974
|
else if (queuedForLeader) {
|
|
1792
1975
|
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
1976
|
}
|
|
1794
|
-
return { task, message, actor, queuedForLeader, feedback: undefined };
|
|
1977
|
+
return { task, message, actor, queuedForLeader, feedback: undefined, idempotentReplay: false };
|
|
1795
1978
|
});
|
|
1979
|
+
if (result.idempotentReplay)
|
|
1980
|
+
return result;
|
|
1796
1981
|
if (recipient !== undefined) {
|
|
1797
1982
|
notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
|
|
1798
1983
|
}
|
|
@@ -1802,6 +1987,13 @@ export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options
|
|
|
1802
1987
|
}
|
|
1803
1988
|
return result;
|
|
1804
1989
|
}
|
|
1990
|
+
/** The durable author of an already-persisted Message, for an idempotent replay
|
|
1991
|
+
* that must return the same actor label the original send computed. */
|
|
1992
|
+
function replayActor(message) {
|
|
1993
|
+
if (message.author.type === "role")
|
|
1994
|
+
return message.author.roleName === LEADER_ROLE ? "leader" : "role";
|
|
1995
|
+
return message.author.type === "operator" ? "operator" : "user";
|
|
1996
|
+
}
|
|
1805
1997
|
/**
|
|
1806
1998
|
* Whether an inbound Message on a Task in this status is delivered to its
|
|
1807
1999
|
* Leader. Draft qualifies because planning is a Leader conversation that
|
|
@@ -1956,6 +2148,8 @@ function taskRoleCommand(args, store, options) {
|
|
|
1956
2148
|
return output(unbindTaskRole(rest, store, options));
|
|
1957
2149
|
if (command === "session")
|
|
1958
2150
|
return taskRoleSessionCommand(rest, store, options);
|
|
2151
|
+
if (command === "interrupt")
|
|
2152
|
+
return interruptTaskRole(rest, store, options);
|
|
1959
2153
|
if (command === "view")
|
|
1960
2154
|
return viewTaskRole(rest, store);
|
|
1961
2155
|
if (command === "takeover")
|
|
@@ -1966,8 +2160,254 @@ function taskRoleCommand(args, store, options) {
|
|
|
1966
2160
|
? "Task role command is required."
|
|
1967
2161
|
: `Unknown command: task role ${command}`);
|
|
1968
2162
|
}
|
|
2163
|
+
/**
|
|
2164
|
+
* `interrupt` stops the exact current native Turn through the Provider's own
|
|
2165
|
+
* native cancel, and never through an owned-process kill, restart, or detach
|
|
2166
|
+
* (decision-3 §1/§7). It is a control operation, not a persisted Message, so it
|
|
2167
|
+
* carries no body of its own.
|
|
2168
|
+
*
|
|
2169
|
+
* The optional `--then-message` is the explicit continuation handoff
|
|
2170
|
+
* (decision-3 §4): it names a Message the Leader already saved (typically the
|
|
2171
|
+
* one a failed steer left behind, reusing that Message's original ref) and
|
|
2172
|
+
* claims "deliver this once, in the same Session, after the interrupted Turn
|
|
2173
|
+
* reaches a proven terminal, without the prior queue preempting it." The claim
|
|
2174
|
+
* is registered on the durable Message before the live cancel. Registration is
|
|
2175
|
+
* idempotent per interrupt requestId, and only one continuation may claim a
|
|
2176
|
+
* given target AgentRun; a conflicting second claim is refused, never silently
|
|
2177
|
+
* dropped or duplicated.
|
|
2178
|
+
*/
|
|
2179
|
+
function interruptTaskRole(args, store, options) {
|
|
2180
|
+
const usage = "Task role interrupt usage: yui task role interrupt <task> <role> --expected-target <turn> [--then-message <task/message>] [--request-id <id>].";
|
|
2181
|
+
const parsed = parseTail(args, new Set(["--expected-target", "--then-message", "--request-id"]), usage);
|
|
2182
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
2183
|
+
const expectedTarget = requiredOption(parsed.options, "--expected-target");
|
|
2184
|
+
const thenMessageRef = optionalNonEmptyOption(parsed.options, "--then-message");
|
|
2185
|
+
const requestId = optionalNonEmptyOption(parsed.options, "--request-id");
|
|
2186
|
+
const now = clock(options);
|
|
2187
|
+
const task = requireTask(store, parsed.positionals[0]);
|
|
2188
|
+
const role = requireRole(store, task.id, parsed.positionals[1]);
|
|
2189
|
+
const fingerprint = createHash("sha256").update(JSON.stringify({
|
|
2190
|
+
roleName: role.name, expectedTarget, thenMessageRef: thenMessageRef ?? null
|
|
2191
|
+
})).digest("hex");
|
|
2192
|
+
const operationId = requestId ?? `interrupt-${fingerprint}`;
|
|
2193
|
+
let receiptId = "";
|
|
2194
|
+
const resolved = store.transaction((tx) => {
|
|
2195
|
+
assertTaskOpen(task);
|
|
2196
|
+
assertTaskInputControlAuthority(tx, options.environment, task.id, role.name);
|
|
2197
|
+
const previous = findTaskInterrupt(tx, task.id, operationId);
|
|
2198
|
+
if (previous !== undefined) {
|
|
2199
|
+
if (previous.payload.fingerprint !== fingerprint)
|
|
2200
|
+
throw usageError("Interrupt requestId already names a different target or then input.");
|
|
2201
|
+
return output("Interrupt already recorded; no native request was repeated.\n", {
|
|
2202
|
+
taskId: task.id, roleName: role.name, interrupt: {
|
|
2203
|
+
state: "idempotent-replay", receipt: taskInterruptReceipt(tx, task.id, previous.payload.receiptId)
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
const resolution = resolveTaskInputControl(tx, task.id, role.name, "interrupt", expectedTarget);
|
|
2208
|
+
if (resolution.outcome !== "ready") {
|
|
2209
|
+
return output(`Interrupt not delivered (${resolution.code}: ${resolution.detail}).\n`, { taskId: task.id, roleName: role.name,
|
|
2210
|
+
interrupt: { state: "not-interrupted", code: resolution.code, detail: resolution.detail } });
|
|
2211
|
+
}
|
|
2212
|
+
const priorTarget = tx.listEvents(task.id).find(event => event.type === "input.interrupt-requested"
|
|
2213
|
+
&& event.payload.roleName === role.name && event.payload.nativeSessionId === resolution.target.nativeSessionId
|
|
2214
|
+
&& event.payload.attemptId === resolution.target.attemptId
|
|
2215
|
+
&& !taskInterruptWasRejected(tx, task.id, event.payload.receiptId));
|
|
2216
|
+
if (priorTarget !== undefined) {
|
|
2217
|
+
return output("This exact Turn already has an interrupt request; inspect its original receipt.\n", {
|
|
2218
|
+
taskId: task.id, roleName: role.name, interrupt: {
|
|
2219
|
+
state: "not-interrupted", code: "DELIVERY_UNKNOWN", receiptId: priorTarget.payload.receiptId
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
if (thenMessageRef !== undefined) {
|
|
2224
|
+
const claimResult = registerInterruptThen(tx, task.id, role.name, thenMessageRef, resolution.target, operationId, options, now);
|
|
2225
|
+
if (claimResult !== "claimed") {
|
|
2226
|
+
return output(`Interrupt not delivered (${claimResult.code}: ${claimResult.detail}).\n`, { taskId: task.id, roleName: role.name,
|
|
2227
|
+
interrupt: { state: "not-interrupted", code: claimResult.code, detail: claimResult.detail } });
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
receiptId = reserveTaskInterrupt(tx, task.id, operationId, fingerprint, resolution.target, thenMessageRef, now);
|
|
2231
|
+
return resolution.target;
|
|
2232
|
+
});
|
|
2233
|
+
if ("kind" in resolved)
|
|
2234
|
+
return resolved;
|
|
2235
|
+
return {
|
|
2236
|
+
kind: "input-interrupt",
|
|
2237
|
+
taskId: task.id,
|
|
2238
|
+
roleName: role.name,
|
|
2239
|
+
target: resolved,
|
|
2240
|
+
receiptId,
|
|
2241
|
+
...(thenMessageRef === undefined ? {} : {
|
|
2242
|
+
thenMessageId: taskRecordReference(thenMessageRef, "message", "Then Message reference", options).localId
|
|
2243
|
+
}),
|
|
2244
|
+
output: `Interrupting ${task.id}/${role.name} at Turn ${resolved.nativeTurnId ?? resolved.attemptId}`
|
|
2245
|
+
+ `${thenMessageRef === undefined ? "" : `, then delivering ${thenMessageRef} once after a proven terminal`}.\n`
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Bind a saved Message as the one continuation of a target AgentRun. The target
|
|
2250
|
+
* run is the exact interrupted Turn's durable AgentRun; the claim is refused
|
|
2251
|
+
* when the target has no AgentRun (a native-only Turn cannot prove its terminal
|
|
2252
|
+
* for a later same-Session delivery), when the Message is unknown or not a
|
|
2253
|
+
* legal saved input to reuse, or when another continuation already claims that
|
|
2254
|
+
* run.
|
|
2255
|
+
*
|
|
2256
|
+
* The Message being reused must be a legal handoff for this exact interrupt: it
|
|
2257
|
+
* must belong to this Role's current Assignment, must not already have been
|
|
2258
|
+
* delivered or marked undeliverable, and must not still be a live control op.
|
|
2259
|
+
* Its original input identity is preserved as `interruptThen.reusedInput`
|
|
2260
|
+
* provenance rather than erased, so a replay of that requestId keeps resolving
|
|
2261
|
+
* to this same Message and never creates a second input (decision-3 §3).
|
|
2262
|
+
*/
|
|
2263
|
+
function registerInterruptThen(store, taskId, roleName, thenMessageRef, target, requestId, options, now) {
|
|
2264
|
+
const targetAttemptId = target.attemptId;
|
|
2265
|
+
const ref = taskRecordReference(thenMessageRef, "message", "Then Message reference", options);
|
|
2266
|
+
if (ref.taskId !== taskId) {
|
|
2267
|
+
return { code: "TARGET_CHANGED", detail: "The then-Message belongs to another Task." };
|
|
2268
|
+
}
|
|
2269
|
+
const message = store.listMessages(taskId).find((entry) => entry.id === ref.localId);
|
|
2270
|
+
if (message === undefined) {
|
|
2271
|
+
return { code: "TARGET_CHANGED", detail: `Then-Message ${thenMessageRef} is unknown.` };
|
|
2272
|
+
}
|
|
2273
|
+
// The claim is keyed on the exact interrupted native Turn (targetAttemptId),
|
|
2274
|
+
// which the resolution proved present under the current Session. An AgentRun
|
|
2275
|
+
// owns the Turn for a Worker/Reviewer Assignment; a Leader's own management or
|
|
2276
|
+
// Draft planning Turn is a real native Turn with NO AgentRun (decision-3 §9,
|
|
2277
|
+
// message-5 gap B). The claim never invents a Run to prove a terminal — the
|
|
2278
|
+
// native Turn is the primary proof, and targetRunId is recorded only when a Run
|
|
2279
|
+
// actually owns the Turn.
|
|
2280
|
+
const active = store.getActiveRun(taskId, roleName);
|
|
2281
|
+
// A synthesized fallback id is an opaque idempotency key (compared only for
|
|
2282
|
+
// equality, never parsed), so it must satisfy the same safe-identity rule as a
|
|
2283
|
+
// caller-supplied requestId: no path separators. Use `:` as the field joiner —
|
|
2284
|
+
// the receiptId carries the `/`-shaped human receipt, this key stays slash-free.
|
|
2285
|
+
const claimRequestId = requestId ?? `interrupt:${taskId}:${roleName}:${targetAttemptId}`;
|
|
2286
|
+
// Idempotent per interrupt requestId: an exact repeat of the same claim (same
|
|
2287
|
+
// request, same native Turn) is a no-op that still authorizes the live cancel.
|
|
2288
|
+
if (message.interruptThen !== undefined) {
|
|
2289
|
+
const prior = findTaskInterrupt(store, taskId, message.interruptThen.requestId);
|
|
2290
|
+
const canReferenceClaim = message.interruptThen.requestId === claimRequestId
|
|
2291
|
+
|| (prior !== undefined && taskInterruptWasRejected(store, taskId, prior.payload.receiptId));
|
|
2292
|
+
if (canReferenceClaim && message.interruptThen.notDeliveredReason === undefined
|
|
2293
|
+
&& message.interruptThen.targetAttemptId === targetAttemptId
|
|
2294
|
+
&& message.interruptThen.targetNativeSessionId === target.nativeSessionId
|
|
2295
|
+
&& message.interruptThen.targetAgentId === target.agentId
|
|
2296
|
+
&& message.interruptThen.targetAdapterId === target.adapterId
|
|
2297
|
+
&& message.interruptThen.targetRoleName === roleName
|
|
2298
|
+
&& message.interruptThen.targetAuthorityEpoch === target.authority.epoch
|
|
2299
|
+
&& message.interruptThen.targetAuthorityHolderId === target.authority.holderId)
|
|
2300
|
+
return "claimed";
|
|
2301
|
+
return { code: "TARGET_CHANGED",
|
|
2302
|
+
detail: `Message ${thenMessageRef} already claims a continuation of Turn ${message.interruptThen.targetAttemptId}.` };
|
|
2303
|
+
}
|
|
2304
|
+
const inputState = taskMessageInputControlState(message);
|
|
2305
|
+
if (inputState === "accepted" || inputState === "pending" || inputState === "delivery-unknown") {
|
|
2306
|
+
return {
|
|
2307
|
+
code: inputState === "accepted" ? "TARGET_CHANGED" : "DELIVERY_UNKNOWN",
|
|
2308
|
+
detail: `Message ${thenMessageRef} has steer disposition ${inputState}; it cannot be submitted again.`
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
// The reused Message must be a legal handoff for this exact interrupt. A
|
|
2312
|
+
// Worker/Reviewer handoff must be addressed to the interrupted Assignment; a
|
|
2313
|
+
// no-Run Leader handoff (its own management/Draft turn) reuses a Leader input,
|
|
2314
|
+
// which is never addressed to a Worker Assignment (no ownerRunId).
|
|
2315
|
+
if (roleName !== LEADER_ROLE) {
|
|
2316
|
+
if (active === null || message.recipient?.roleName !== roleName || message.recipient.ownerRunId !== active.id) {
|
|
2317
|
+
return { code: "TARGET_CHANGED",
|
|
2318
|
+
detail: `Message ${thenMessageRef} is not addressed to the current execution Assignment.` };
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
else if (message.recipient?.ownerRunId !== undefined) {
|
|
2322
|
+
return { code: "TARGET_CHANGED",
|
|
2323
|
+
detail: `Message ${thenMessageRef} is addressed to an execution Assignment and cannot be a no-Run ${roleName} handoff.` };
|
|
2324
|
+
}
|
|
2325
|
+
// A Message that already delivered, or was already ruled undeliverable, is not
|
|
2326
|
+
// a fresh input to reuse; reusing it would replay or resurrect a settled fact.
|
|
2327
|
+
if (message.continuation?.runId !== undefined) {
|
|
2328
|
+
return { code: "TARGET_CHANGED", detail: `Message ${thenMessageRef} was already delivered by ${message.continuation.runId}.` };
|
|
2329
|
+
}
|
|
2330
|
+
if (message.continuation?.notDeliveredReason !== undefined) {
|
|
2331
|
+
return { code: "TARGET_CHANGED",
|
|
2332
|
+
detail: `Message ${thenMessageRef} was already settled not-delivered (${message.continuation.notDeliveredReason}).` };
|
|
2333
|
+
}
|
|
2334
|
+
// Only one continuation may claim a given target native Turn.
|
|
2335
|
+
const existing = store.listMessages(taskId).find((entry) => entry.interruptThen?.targetAttemptId === targetAttemptId && entry.id !== message.id);
|
|
2336
|
+
if (existing !== undefined) {
|
|
2337
|
+
return { code: "TARGET_CHANGED",
|
|
2338
|
+
detail: `Turn ${targetAttemptId} is already the terminal target of Message ${existing.id}.` };
|
|
2339
|
+
}
|
|
2340
|
+
// Reference the new op without rewriting the original input facts: keep the
|
|
2341
|
+
// Message's history, move any steer identity into reusedInput provenance (so a
|
|
2342
|
+
// replay of that requestId still resolves here, never a second input), and
|
|
2343
|
+
// clear only the live steer action so the handoff is deliverable by the queued
|
|
2344
|
+
// continuation path after a proven terminal.
|
|
2345
|
+
const { inputControl, ...rest } = message;
|
|
2346
|
+
store.updateMessage(taskId, { ...rest,
|
|
2347
|
+
interruptThen: { requestId: claimRequestId, targetAttemptId,
|
|
2348
|
+
targetRoleName: roleName, targetNativeSessionId: target.nativeSessionId,
|
|
2349
|
+
targetAgentId: target.agentId, targetAdapterId: target.adapterId,
|
|
2350
|
+
targetAuthorityEpoch: target.authority.epoch, targetAuthorityHolderId: target.authority.holderId,
|
|
2351
|
+
...(target.nativeTurnId === undefined ? {} : { targetNativeTurnId: target.nativeTurnId }),
|
|
2352
|
+
...(active === null ? {} : { targetRunId: active.id }),
|
|
2353
|
+
...(inputControl === undefined ? {} : { reusedInput: inputControl }) } });
|
|
2354
|
+
recordTaskEvent(store, taskId, "message.interrupt-then-claimed", {
|
|
2355
|
+
messageId: message.id, roleName, targetAttemptId,
|
|
2356
|
+
...(active === null ? {} : { targetRunId: active.id }),
|
|
2357
|
+
...(inputControl === undefined ? {} : { reusedRequestId: inputControl.requestId })
|
|
2358
|
+
}, now);
|
|
2359
|
+
// Release trigger for a no-Run Leader handoff (decision-3 §4/§9). A
|
|
2360
|
+
// Worker/Reviewer claim (active !== null) is released by the reconcile loop's
|
|
2361
|
+
// unconditional prepareMessageContinuations pass, driven by the owning
|
|
2362
|
+
// AgentRun's terminal — no extra wake is needed. A no-Run Leader turn owns no
|
|
2363
|
+
// AgentRun and is never surfaced by that push path; it is delivered only by
|
|
2364
|
+
// being woken to read its own Context. The interrupt itself (and, when the
|
|
2365
|
+
// handoff reuses a Leader self-steer, wakePolicy "none") enqueues no wake, so
|
|
2366
|
+
// without this the claim would be structurally unreleasable: listPendingWakeups
|
|
2367
|
+
// never selects the Task and the busy-gated leader mailbox is never consulted.
|
|
2368
|
+
// Enqueue that wake now, keyed to the claimed Message so an idempotent repeat of
|
|
2369
|
+
// the same claim does not stack a second one; claimLeaderNotification holds it
|
|
2370
|
+
// until the interrupted native Turn reaches its proven terminal, and only then
|
|
2371
|
+
// does the Leader read the Context that surfaces this handoff.
|
|
2372
|
+
if (roleName === LEADER_ROLE) {
|
|
2373
|
+
enqueueWork(store, leaderMailbox(taskId), "interrupt-then", now, [messageRef(taskId, message.id)], { source: "interrupt-then", dedupeKey: `interrupt-then:${taskId}:${message.id}` });
|
|
2374
|
+
}
|
|
2375
|
+
return "claimed";
|
|
2376
|
+
}
|
|
1969
2377
|
function taskRoleSessionCommand(args, store, options) {
|
|
1970
2378
|
const [command, ...rest] = args;
|
|
2379
|
+
if (command === "retry") {
|
|
2380
|
+
const [taskId, roleName, action = "show", ...extra] = rest;
|
|
2381
|
+
if (extra.length > 0 || !["show", "cancel", "disable", "enable"].includes(action)) {
|
|
2382
|
+
throw usageError("Usage: yui task role session retry <task> <role> [show|cancel|disable|enable]");
|
|
2383
|
+
}
|
|
2384
|
+
return store.transaction(tx => {
|
|
2385
|
+
const task = requireTask(tx, taskId);
|
|
2386
|
+
const role = requireRole(tx, task.id, roleName);
|
|
2387
|
+
if (action !== "show")
|
|
2388
|
+
taskActor(tx, options, task.id);
|
|
2389
|
+
else {
|
|
2390
|
+
const reader = resolveManagedTaskReader(tx, options.environment);
|
|
2391
|
+
if (reader === undefined)
|
|
2392
|
+
taskActor(tx, options, task.id);
|
|
2393
|
+
else if (reader.taskId !== task.id || reader.roleName !== "leader" && reader.roleName !== role.name) {
|
|
2394
|
+
throw usageError("Provider retry read is outside the caller's Task/Role.");
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
|
|
2398
|
+
let binding = sessions?.providerBinding ?? null;
|
|
2399
|
+
if (action !== "show") {
|
|
2400
|
+
if (task.status === "archived" || sessions === null || binding === null)
|
|
2401
|
+
throw usageError("No writable Provider Session.");
|
|
2402
|
+
binding = controlProviderRetry(binding, action, clock(options).getTime());
|
|
2403
|
+
tx.saveTaskRoleSessionSet({ ...sessions, providerBinding: binding });
|
|
2404
|
+
recordTaskEvent(tx, task.id, "provider.retry-control", { roleName: role.name, action }, clock(options));
|
|
2405
|
+
}
|
|
2406
|
+
return output(`${renderProviderRetry(binding)}\n`, {
|
|
2407
|
+
roleName: role.name, retry: providerRetryProjection(binding), disabled: binding?.retryDisabled ?? false
|
|
2408
|
+
});
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
1971
2411
|
if (command === "inspect") {
|
|
1972
2412
|
exactPositionals(rest, 2, "Task Role Session inspect usage: yui task role session inspect <task> <role>.");
|
|
1973
2413
|
const task = requireTask(store, rest[0]);
|
|
@@ -1987,6 +2427,7 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
1987
2427
|
// means there was nothing an Agent reported, and the one-line label above
|
|
1988
2428
|
// still states which of the "no value" cases applies.
|
|
1989
2429
|
const runConfiguration = active === null ? undefined : options.liveRunConfiguration;
|
|
2430
|
+
const host = options.liveHostObservations?.[role.name];
|
|
1990
2431
|
const runConfigurationDetail = renderAgentRunConfiguration(runConfiguration);
|
|
1991
2432
|
return output(active === null
|
|
1992
2433
|
? `No attached Session exists for ${task.id}/${role.name}.\n`
|
|
@@ -2000,6 +2441,8 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
2000
2441
|
`Native id: ${active.nativeSessionId}`,
|
|
2001
2442
|
`Session: ${active.status}${active.endReason === undefined ? "" : `/${active.endReason}`}`,
|
|
2002
2443
|
`AgentRun: ${binding?.run?.status ?? "none"}`,
|
|
2444
|
+
renderProviderRetry(binding),
|
|
2445
|
+
...(host === undefined ? [] : [`Host reporting: ${taskRoleHostDiagnostic(host)}`]),
|
|
2003
2446
|
`Run configuration: ${agentRunConfigurationLabel(runConfiguration)}`
|
|
2004
2447
|
].join("\n") + "\n"
|
|
2005
2448
|
+ `\n${renderRoleLaunchComparison(role, active.effective)}\n`
|
|
@@ -2008,6 +2451,8 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
2008
2451
|
role,
|
|
2009
2452
|
session: active,
|
|
2010
2453
|
providerBinding: binding,
|
|
2454
|
+
retry: providerRetryProjection(binding),
|
|
2455
|
+
...(host === undefined ? {} : { host }),
|
|
2011
2456
|
...(runConfiguration === undefined ? {} : { runConfiguration })
|
|
2012
2457
|
});
|
|
2013
2458
|
}
|
|
@@ -2051,9 +2496,6 @@ function taskRoleSessionCommand(args, store, options) {
|
|
|
2051
2496
|
const now = clock(options);
|
|
2052
2497
|
const request = store.transaction((tx) => {
|
|
2053
2498
|
const task = requireTask(tx, parsed.positionals[0]);
|
|
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);
|
|
2056
|
-
}
|
|
2057
2499
|
const actor = taskActor(tx, options, task.id);
|
|
2058
2500
|
const role = requireRole(tx, task.id, parsed.positionals[1]);
|
|
2059
2501
|
if (actor === "leader" && role.name === LEADER_ROLE) {
|
|
@@ -2172,14 +2614,14 @@ function addTaskRole(args, store, options) {
|
|
|
2172
2614
|
`Runtime source: ${runtimeSource}`,
|
|
2173
2615
|
`Agent: ${result.role.activeAgentId}/${result.binding.adapterId}`,
|
|
2174
2616
|
`Model: ${result.binding.config.model ?? "CLI default"}; effort: ${result.binding.config.effort ?? "CLI default"}; permission: ${result.binding.config.permission.strategy}`,
|
|
2175
|
-
"Next:
|
|
2617
|
+
"Next: dispatch an assigned WorkItem, or request a Task-final Review with this Role; a Review needs no WorkItem."
|
|
2176
2618
|
].join("\n").concat("\n");
|
|
2177
2619
|
}
|
|
2178
2620
|
function listTaskRoles(args, store, options) {
|
|
2179
2621
|
exactPositionals(args, 1, "Task role list usage: yui task role list <task>.");
|
|
2180
2622
|
const task = requireTask(store, args[0]);
|
|
2181
2623
|
const roles = store.listRoles(task.id);
|
|
2182
|
-
const statuses = inspectTaskRoleRuntimeStatuses(task.id, roles, store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? [], options.now?.() ?? new Date());
|
|
2624
|
+
const statuses = inspectTaskRoleRuntimeStatuses(task.id, roles, store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? [], options.now?.() ?? new Date()).map(status => withTaskRoleHostObservation(status, options.liveHostObservations?.[status.roleName]));
|
|
2183
2625
|
if (statuses.length === 0)
|
|
2184
2626
|
return output("No roles assigned.\n", { roles: statuses });
|
|
2185
2627
|
return output(`${renderTable(`Task roles: ${task.id}`, [
|
|
@@ -2209,7 +2651,8 @@ function taskRoleStatus(args, store, options) {
|
|
|
2209
2651
|
const [status] = inspectTaskRoleRuntimeStatuses(task.id, [role], store, options.runtime?.inspectTaskRolePanes?.(task.id) ?? [], options.now?.() ?? new Date());
|
|
2210
2652
|
if (status === undefined)
|
|
2211
2653
|
throw roleNotFound(role.name);
|
|
2212
|
-
|
|
2654
|
+
const observed = withTaskRoleHostObservation(status, options.liveHostObservations?.[role.name]);
|
|
2655
|
+
return output(renderTaskRoleRuntimeStatus(observed), { role: observed });
|
|
2213
2656
|
}
|
|
2214
2657
|
function showTaskRole(args, store) {
|
|
2215
2658
|
exactPositionals(args, 2, "Task role show usage: yui task role show <task> <role>.");
|
|
@@ -2956,6 +3399,17 @@ function updateWork(args, store, options) {
|
|
|
2956
3399
|
workItem: result.item
|
|
2957
3400
|
});
|
|
2958
3401
|
}
|
|
3402
|
+
/** Shared with CLI preflight so a refused dispatch cannot prepare workspaces. */
|
|
3403
|
+
export function requireWorkItemAssignee(item) {
|
|
3404
|
+
if (item.assignee === undefined) {
|
|
3405
|
+
throw usageError(`Work Item has no Task Role assignee: ${item.id}. `
|
|
3406
|
+
+ `The Task Leader can execute it directly without dispatch: `
|
|
3407
|
+
+ `yui task work update ${item.taskId}/${item.id} running. `
|
|
3408
|
+
+ (item.writeProjectIds.length === 0 ? "" :
|
|
3409
|
+
`For code, first use yui task work isolate ${item.taskId}/${item.id}.`));
|
|
3410
|
+
}
|
|
3411
|
+
return item.assignee;
|
|
3412
|
+
}
|
|
2959
3413
|
function dispatchWork(args, store, options) {
|
|
2960
3414
|
const usage = "Task work dispatch usage: yui task work dispatch <task>/<work> [--input <text>] [--lane-role <role> ...].";
|
|
2961
3415
|
const parsed = parseMultiValueTail(args, new Set(["--input"]), new Set(["--lane-role"]), usage);
|
|
@@ -2969,11 +3423,8 @@ function dispatchWork(args, store, options) {
|
|
|
2969
3423
|
if (task.status !== "active")
|
|
2970
3424
|
throw usageError(inactiveTaskMessage(task, "dispatch"));
|
|
2971
3425
|
assertTaskExecutionEnabled(task, "dispatching work");
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
+ `The Task Leader must run "yui task work update ${item.id} running" and execute it directly.`);
|
|
2975
|
-
}
|
|
2976
|
-
const lanePlan = planReplicatedWorkItemLanes(item.assignee, requestedLaneRoles, `execution-group-${tx.peekNextRunId(task.id)}`);
|
|
3426
|
+
const assignee = requireWorkItemAssignee(item);
|
|
3427
|
+
const lanePlan = planReplicatedWorkItemLanes(assignee, requestedLaneRoles, `execution-group-${tx.peekNextRunId(task.id)}`);
|
|
2977
3428
|
const currentGroup = currentWorkItemExecutionGroup(item);
|
|
2978
3429
|
if (item.status !== "open") {
|
|
2979
3430
|
throw usageError(`Work item ${item.id} cannot be dispatched from ${item.status}.`);
|
|
@@ -3010,7 +3461,7 @@ function dispatchWork(args, store, options) {
|
|
|
3010
3461
|
const rawInput = trimmed(parsed.options.get("--input")) ?? item.objective;
|
|
3011
3462
|
let workItemForDispatch = prepareWorkItemDispatch(item, now);
|
|
3012
3463
|
if (lanePlan.roles.length === 0) {
|
|
3013
|
-
const role = requireRole(tx, task.id,
|
|
3464
|
+
const role = requireRole(tx, task.id, assignee);
|
|
3014
3465
|
if (tx.getActiveRun(task.id, role.name) !== null) {
|
|
3015
3466
|
throw usageError(`${task.id}/${role.name} already has an active run.`);
|
|
3016
3467
|
}
|
|
@@ -4393,6 +4844,45 @@ function settleStaleFinalReviewRun(args, store, options) {
|
|
|
4393
4844
|
: `Obsolete final Review already settled: ${result.round.id}/${result.run.id}\n`, { reviewRound: result.round, reviewRun: result.run });
|
|
4394
4845
|
}
|
|
4395
4846
|
function retryRun(args, store, options) {
|
|
4847
|
+
return store.transaction(tx => {
|
|
4848
|
+
const previous = requireRun(tx, args[0], options);
|
|
4849
|
+
const existing = tx.listEvents(previous.taskId).find(event => event.type === "run.retried"
|
|
4850
|
+
&& event.payload.previousRunId === previous.id);
|
|
4851
|
+
if (existing !== undefined) {
|
|
4852
|
+
return output(`Retry already recorded as ${existing.payload.runId}; retry that exact successor if needed.\n`);
|
|
4853
|
+
}
|
|
4854
|
+
const sessions = tx.getTaskRoleSessionSet(previous.taskId, previous.roleName);
|
|
4855
|
+
const retry = sessions?.providerBinding?.retry;
|
|
4856
|
+
if (options.providerRetryChainId !== undefined
|
|
4857
|
+
&& (retry?.chainId !== options.providerRetryChainId || retry.status !== "waiting"
|
|
4858
|
+
|| retry.previousRunId !== previous.id || clock(options).getTime() < Date.parse(retry.nextEligibleAt)
|
|
4859
|
+
|| clock(options).getTime() >= Date.parse(retry.deadline))) {
|
|
4860
|
+
throw usageError("Provider recovery intent is no longer eligible.");
|
|
4861
|
+
}
|
|
4862
|
+
const result = retryRunOperation(args, tx, options);
|
|
4863
|
+
if (retry !== undefined && retry.status !== "recovered" && retry.previousRunId === previous.id && sessions?.providerBinding != null) {
|
|
4864
|
+
const successor = tx.getActiveRun(previous.taskId, previous.roleName);
|
|
4865
|
+
tx.saveTaskRoleSessionSet({
|
|
4866
|
+
...sessions, providerBinding: {
|
|
4867
|
+
...sessions.providerBinding, retry: {
|
|
4868
|
+
...retry, status: "waiting", successorAutomatic: options.providerRetryChainId !== undefined,
|
|
4869
|
+
...(options.providerRetryChainId === undefined ? {
|
|
4870
|
+
intentAt: clock(options).toISOString(),
|
|
4871
|
+
// An explicit manual attempt does not inherit automatic jitter,
|
|
4872
|
+
// but cannot shorten a trusted server minimum.
|
|
4873
|
+
nextEligibleAt: new Date(Math.max(clock(options).getTime(), retry.error.retryAfterMs === undefined ? 0
|
|
4874
|
+
: Date.parse(sessions.providerBinding.run.updatedAt) + retry.error.retryAfterMs + 1)).toISOString()
|
|
4875
|
+
} : {}),
|
|
4876
|
+
...(successor === null ? {} : { successorRunId: successor.id }),
|
|
4877
|
+
...(previous.reviewRoundId === undefined ? {} : { successorReviewRoundId: previous.reviewRoundId })
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
return result;
|
|
4883
|
+
});
|
|
4884
|
+
}
|
|
4885
|
+
function retryRunOperation(args, store, options) {
|
|
4396
4886
|
exactPositionals(args, 1, "Task turn retry usage: yui task run retry <task>/<run>.");
|
|
4397
4887
|
const now = clock(options);
|
|
4398
4888
|
const previous = store.transaction((tx) => requireRun(tx, args[0], options));
|
|
@@ -4952,7 +5442,7 @@ function retryFailedReviewRun(previous, store, options, now) {
|
|
|
4952
5442
|
const task = requireTask(tx, run.taskId);
|
|
4953
5443
|
if (task.status !== "active")
|
|
4954
5444
|
throw usageError(`Task is not active: ${task.id}.`);
|
|
4955
|
-
const requestedBy = taskActor(tx, options, task.id);
|
|
5445
|
+
const requestedBy = options.providerRetryChainId === undefined ? taskActor(tx, options, task.id) : "policy";
|
|
4956
5446
|
const round = tx.getReviewRound(task.id, run.reviewRoundId);
|
|
4957
5447
|
if (round === null) {
|
|
4958
5448
|
throw dataError(`ReviewRound not found for run ${run.id}: ${run.reviewRoundId}.`);
|
|
@@ -5821,7 +6311,7 @@ function activeReviewRoundForCandidate(store, item, candidate) {
|
|
|
5821
6311
|
}
|
|
5822
6312
|
function createTaskRole(store, task, roleName, explicitAgentId, now, sourceGlobalRoleName) {
|
|
5823
6313
|
const workspace = task.status === "draft"
|
|
5824
|
-
?
|
|
6314
|
+
? planningRuntimeCwd(store.rootDirectory(), task.id)
|
|
5825
6315
|
: task.cwd ?? store.getConfig().defaultWorkspace ?? process.cwd();
|
|
5826
6316
|
if (explicitAgentId === undefined) {
|
|
5827
6317
|
const sourceRoleName = sourceGlobalRoleName
|
|
@@ -5851,7 +6341,7 @@ function requireAgentProfile(store, id) {
|
|
|
5851
6341
|
}
|
|
5852
6342
|
function createTaskRoleFromAgentBinding(store, task, roleName, binding, now) {
|
|
5853
6343
|
const workspace = task.status === "draft"
|
|
5854
|
-
?
|
|
6344
|
+
? planningRuntimeCwd(store.rootDirectory(), task.id)
|
|
5855
6345
|
: task.cwd ?? store.getConfig().defaultWorkspace ?? process.cwd();
|
|
5856
6346
|
return createRole(task.id, roleName, [binding], binding.agentId, workspace, now);
|
|
5857
6347
|
}
|