@zq-silk/yui 0.15.11 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +11 -6
- package/ARCHITECTURE.zh-CN.md +8 -4
- package/README.md +13 -5
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/artifacts/artifactCommitLock.js +16 -55
- package/dist/artifacts/taskArtifactRepository.js +3 -3
- package/dist/cli/agentConfigurationPicker.js +1 -1
- package/dist/cli/commandCatalog.js +92 -48
- package/dist/cli/completion.js +18 -18
- package/dist/cli/completionWizard.js +1 -1
- package/dist/cli/dynamicCompletion.js +1 -1
- package/dist/cli/interactionPolicy.js +11 -3
- package/dist/cli/invocationAuthority.js +64 -0
- package/dist/cli/managedDiagnostics.js +2 -2
- package/dist/cli/parseRepeatable.js +35 -0
- package/dist/cli/updatePorts.js +26 -5
- package/dist/cli.js +1303 -1235
- package/dist/commands/agentCommands.js +4 -4
- package/dist/commands/capabilityCommands.js +1 -1
- package/dist/commands/configCommands.js +6 -57
- package/dist/commands/configOverview.js +2 -2
- package/dist/commands/durableJobCommands.js +12 -6
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +78 -29
- package/dist/commands/grantCommands.js +0 -3
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/operatorCommands.js +3 -3
- package/dist/commands/projectCommands.js +45 -25
- package/dist/commands/resourcesCommands.js +10 -40
- package/dist/commands/roleConfiguration.js +2 -6
- package/dist/commands/taskActivationCommands.js +4 -8
- package/dist/commands/taskAgentCapabilities.js +9 -0
- package/dist/commands/taskCommandSupport.js +155 -0
- package/dist/commands/taskCommandTypes.js +1 -0
- package/dist/commands/taskCommands.js +355 -732
- package/dist/commands/taskCompletionGate.js +1 -1
- package/dist/commands/taskExecutionCommands.js +1 -1
- package/dist/commands/taskFactCommands.js +325 -0
- package/dist/commands/taskInputCommands.js +12 -42
- package/dist/commands/taskIntegrationCommands.js +20 -58
- package/dist/commands/taskNextActionCommand.js +7 -6
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +12 -3
- package/dist/commands/taskPublicationVerifyCommand.js +24 -40
- package/dist/commands/taskRemoteDeliveryCommand.js +5 -86
- package/dist/commands/taskUpstreamCommands.js +19 -8
- package/dist/commands/workflowCommands.js +6 -1
- package/dist/completion/completionInstaller.js +26 -26
- package/dist/completion/completionState.js +26 -26
- package/dist/completion/fileCompletionManager.js +6 -11
- package/dist/config/configCatalog.js +0 -2
- package/dist/config/timeZone.js +14 -0
- package/dist/config/yuiConfig.js +0 -29
- package/dist/context/runContextPack.js +5 -3
- package/dist/context/taskCatalog.js +185 -0
- package/dist/context/taskContext.js +152 -151
- package/dist/context/wakeRunReferences.js +11 -0
- package/dist/controller/agentCapabilities.js +58 -0
- package/dist/controller/agentRuntimeObserver.js +6 -11
- package/dist/controller/clientRuntime.js +22 -14
- package/dist/controller/controller.js +20 -24
- package/dist/controller/fileSchedulerStoreAdapter.js +191 -225
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/jobClient.js +4 -4
- package/dist/controller/jobControl.js +60 -25
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +85 -43
- package/dist/controller/runtimeEventProcessor.js +0 -5
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/controller/taskAgentError.js +35 -0
- package/dist/coordination/workMailboxQueue.js +2 -24
- package/dist/core/controllerClient.js +2 -2
- package/dist/core/fileLockOwner.js +74 -0
- package/dist/decision/decision.js +17 -0
- package/dist/doctor/doctor.js +18 -9
- package/dist/event/taskEvent.js +12 -0
- package/dist/execution/workItemExecutionProjection.js +2 -2
- package/dist/executor/agentCapabilityConfig.js +43 -0
- package/dist/executor/agentConfigurationCatalog.js +49 -11
- package/dist/executor/agentExecutor.js +5 -8
- package/dist/executor/fileRoleLaunchPlanner.js +11 -53
- package/dist/executor/taskAgentCapabilities.js +64 -0
- package/dist/integration/deliveryObligation.js +1 -69
- package/dist/integration/gitIntegrationService.js +79 -90
- package/dist/integration/integrationAttempt.js +3 -14
- package/dist/integration/integrationSourceApplication.js +4 -12
- package/dist/integration/manifestTags.js +2 -2
- package/dist/job/durableJob.js +4 -25
- package/dist/job/jobAssignmentScope.js +22 -0
- package/dist/job/jobOperation.js +16 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/stepDirectory.js +15 -0
- package/dist/kernel/builtinCapabilities.js +15 -11
- package/dist/kernel/kernelPorts.js +1 -17
- package/dist/lifecycle/exactRunTerminalization.js +5 -10
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/message/inputControlResolution.js +3 -4
- package/dist/message/message.js +17 -20
- package/dist/message/messageContinuation.js +1 -1
- package/dist/milestone/milestone.js +11 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/observability/orchestrationMetrics.js +24 -1
- package/dist/observability/runtimeIdentity.js +1 -17
- package/dist/output/agentConfigurationPresentation.js +2 -0
- package/dist/output/timePresentation.js +1 -14
- package/dist/plugins/pluginService.js +12 -4
- package/dist/release/releaseWorkflowPorts.js +45 -96
- package/dist/release/runtimeRelease.js +9 -15
- package/dist/repository/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +98 -82
- package/dist/repository/taskBaseFreshness.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +82 -144
- package/dist/repository/taskWorkspacePreparer.js +113 -47
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/autoResourceGc.js +4 -77
- package/dist/resources/liveReferences.js +7 -54
- package/dist/resources/resourceDiscovery.js +5 -118
- package/dist/resources/resourceGc.js +233 -181
- package/dist/resources/resourceRegistrar.js +5 -4
- package/dist/resources/resourceRegistry.js +3 -37
- package/dist/resources/resourceRegistryStore.js +13 -36
- package/dist/resources/sqliteResourceRegistry.js +43 -26
- package/dist/review/deltaRecheck.js +1 -1
- package/dist/review/reviewAcceptance.js +1 -1
- package/dist/review/reviewDecision.js +1 -1
- package/dist/review/reviewRound.js +8 -7
- package/dist/review/taskFinalReviewContractResolution.js +1 -1
- package/dist/runtime/acpProtocol.js +4 -51
- package/dist/runtime/acpSession.js +10 -75
- package/dist/runtime/acpSessionConfiguration.js +1 -8
- package/dist/runtime/agentEndpoint.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentFailureContext.js +43 -0
- package/dist/runtime/agentHost.js +42 -25
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/codexInteractiveHost.js +2 -2
- package/dist/runtime/index.js +0 -1
- package/dist/runtime/managedCaller.js +21 -1
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerErrors.js +38 -0
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/runtimeObservation.js +0 -5
- package/dist/runtime/runtimeSessionCandidate.js +2 -3
- package/dist/runtime/sessionOwnerIdentity.js +1 -1
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +10 -43
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +6 -8
- package/dist/scheduler/activeRoleRunDelivery.js +19 -7
- package/dist/scheduler/leaderWakeupProcessor.js +24 -6
- package/dist/scheduler/operatorEvent.js +12 -20
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +5 -8
- package/dist/scheduler/roleRunLiveness.js +4 -4
- package/dist/scheduler/roleRunStall.js +16 -23
- package/dist/scheduler/taskExecutionProjection.js +49 -36
- package/dist/scheduler/taskObservabilityProjection.js +6 -45
- package/dist/scheduler/taskWake.js +5 -10
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/scheduler/wakeupQueue.js +2 -4
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/contextRecords.js +106 -0
- package/dist/storage/currentTaskStore.js +1 -1
- package/dist/storage/homeLayout.js +13 -17
- package/dist/storage/migrations/agentFailureContext.js +22 -0
- package/dist/storage/migrations/currentInputContract.js +86 -0
- package/dist/storage/migrations/currentRuntimeContract.js +228 -0
- package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
- package/dist/storage/migrations/integrationContinuation.js +5 -4
- package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
- package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
- package/dist/storage/migrations/verificationPlanV1.js +162 -0
- package/dist/storage/migrations/verificationPolicy.js +74 -0
- package/dist/storage/migrations/workItemHistory.js +46 -0
- package/dist/storage/persistenceWorker.js +12 -4
- package/dist/storage/recordValidation.js +87 -0
- package/dist/storage/sqliteSchema.js +173 -1
- package/dist/storage/sqliteStore.js +172 -161
- package/dist/storage/storageSchema.js +2 -14
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +16 -7
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +5 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/completionReadiness.js +5 -24
- package/dist/task/draftPlan.js +0 -53
- package/dist/task/nextAction.js +7 -13
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/task/remoteDeliveryService.js +89 -0
- package/dist/task/taskActivation.js +2 -25
- package/dist/task/taskActivationService.js +0 -2
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/task/taskSubmission.js +3 -14
- package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
- package/dist/telemetry/sqliteTelemetryStore.js +49 -49
- package/dist/telemetry/telemetryWiring.js +4 -3
- package/dist/tmux/tmuxManager.js +29 -20
- package/dist/verification/gateArtifact.js +15 -24
- package/dist/verification/gateArtifactStore.js +14 -7
- package/dist/verification/verificationGateService.js +29 -88
- package/dist/verification/verificationPlan.js +27 -72
- package/dist/web/assets/client/app.js +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +6 -5
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +12 -3
- package/dist/web/webSnapshot.js +14 -85
- package/dist/web/webTaskSurface.js +17 -46
- package/dist/workItem/workItem.js +1 -12
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- 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 +42 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +33 -3
- package/docs/managed-turn-and-session-runtime.md +121 -1
- package/docs/managed-turn-and-session-runtime.zh-CN.md +96 -2
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/plugin-sdk.md +4 -2
- 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/provider-runtime.md +3 -1
- package/docs/provider-runtime.zh-CN.md +4 -2
- package/docs/release-workflow.md +190 -7
- package/docs/release-workflow.zh-CN.md +139 -5
- package/docs/roles-and-configuration.md +29 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/sqlite-control-plane-design.md +16 -3
- package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
- package/docs/task-dag-semantics.md +3 -3
- package/docs/task-dag-semantics.zh-CN.md +2 -2
- package/docs/task-delivery.md +223 -17
- package/docs/task-delivery.zh-CN.md +171 -14
- package/docs/task-discovery.md +101 -0
- package/docs/task-discovery.zh-CN.md +85 -0
- package/docs/testing/verification-levels.md +161 -9
- package/docs/testing/verification-levels.zh-CN.md +116 -9
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -0
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-leader/references/integration.md +9 -5
- package/skills/yui-leader/references/planning.md +9 -3
- package/skills/yui-operator/SKILL.md +24 -8
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +71 -0
- package/dist/agent/agentRegistry.js +0 -10
- package/dist/agentRun/runIdentity.js +0 -22
- package/dist/commands/deliveryGuardPreflight.js +0 -30
- package/dist/commands/taskIntegrationQueueCommands.js +0 -226
- package/dist/commands/taskOverviewCommand.js +0 -376
- package/dist/completion/completionWizard.js +0 -125
- package/dist/context/dispatchContext.js +0 -110
- package/dist/context/wakeNotification.js +0 -144
- package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
- package/dist/integration/integrationQueueEntry.js +0 -191
- package/dist/integration/integrationQueueService.js +0 -810
- package/dist/release/fakeReleasePorts.js +0 -55
- package/dist/runtime/sessionOwnerRegistry.js +0 -137
- package/dist/task/deliveryGuard.js +0 -226
- /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
|
@@ -2,6 +2,8 @@ import { sendAgentHostRunControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime
|
|
|
2
2
|
import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
3
3
|
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
4
4
|
import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
|
|
5
|
+
import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
6
|
+
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
5
7
|
/** Messages are the only queue. Host's atomic begin arbitrates native writers. */
|
|
6
8
|
export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
7
9
|
for (const role of store.listGlobalRoles()) {
|
|
@@ -11,6 +13,15 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
11
13
|
if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
|
|
12
14
|
continue;
|
|
13
15
|
let sessions = store.getGlobalRoleSessionSet(role.name);
|
|
16
|
+
if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
|
|
17
|
+
store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
|
|
18
|
+
pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
|
|
19
|
+
&& (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
|
|
20
|
+
if (pending.length === 0)
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (providerRetryPending(sessions?.providerBinding))
|
|
24
|
+
continue;
|
|
14
25
|
const current = sessions?.providerBinding?.run;
|
|
15
26
|
const obsolete = pending.filter(message => {
|
|
16
27
|
const claim = message.interruptThen;
|
|
@@ -99,6 +110,8 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
99
110
|
if (latest === undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
|
|
100
111
|
return;
|
|
101
112
|
const turn = tx.getGlobalRoleSessionSet(role.name)?.providerBinding?.run;
|
|
113
|
+
if (providerRetryPending(tx.getGlobalRoleSessionSet(role.name)?.providerBinding))
|
|
114
|
+
return;
|
|
102
115
|
// A later canonical acceptance wins over transport diagnostics.
|
|
103
116
|
if (turn?.attemptId === attemptId && ["accepted", "completed", "failed", "cancelled"].includes(turn.status))
|
|
104
117
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveJobCaller } from "../
|
|
1
|
+
import { resolveJobCaller } from "../task/taskAuthority.js";
|
|
2
2
|
import { callFileTaskController } from "./clientRuntime.js";
|
|
3
3
|
export async function startDurableJob(home, params, clientOptions = {}) {
|
|
4
4
|
// rr6/f2: Route through callFileTaskController so an Integration-issued
|
|
@@ -7,8 +7,8 @@ export async function startDurableJob(home, params, clientOptions = {}) {
|
|
|
7
7
|
const result = await callFileTaskController(home, "job.start", params, clientOptions);
|
|
8
8
|
return parseStartResult(result);
|
|
9
9
|
}
|
|
10
|
-
export async function getDurableJob(home, taskId, jobId, clientOptions = {}) {
|
|
11
|
-
const result = await callFileTaskController(home, "job.get", { taskId, jobId }, clientOptions);
|
|
10
|
+
export async function getDurableJob(home, taskId, jobId, caller, clientOptions = {}) {
|
|
11
|
+
const result = await callFileTaskController(home, "job.get", { taskId, jobId, caller }, clientOptions);
|
|
12
12
|
return parseJobResult(result);
|
|
13
13
|
}
|
|
14
14
|
export async function acknowledgeDurableJob(home, taskId, jobId, caller, clientOptions = {}) {
|
|
@@ -89,7 +89,7 @@ export function createControllerIntegrationJobPort(home, clientOptions = {}) {
|
|
|
89
89
|
return job;
|
|
90
90
|
},
|
|
91
91
|
async getJob(taskId, jobId) {
|
|
92
|
-
return getDurableJob(home, taskId, jobId, clientOptions);
|
|
92
|
+
return getDurableJob(home, taskId, jobId, resolveJobCaller(clientOptions.environment, taskId), clientOptions);
|
|
93
93
|
},
|
|
94
94
|
async cancelJob(taskId, jobId) {
|
|
95
95
|
// rr8/rr12: Bind the cancel request to the caller's managed identity.
|
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
* record is created or cancel-requested. The Controller socket layer calls
|
|
4
4
|
* this port for `job.*` requests; nothing else writes queued jobs.
|
|
5
5
|
*
|
|
6
|
-
* Creation is idempotent per
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Creation is idempotent per authenticated actor and explicit request ID.
|
|
7
|
+
* Repeating that request with identical inputs returns the original Job;
|
|
8
|
+
* changing its input is a conflict. Integration owner identity additionally
|
|
9
|
+
* preserves its admitted Job across authorized Session replacement.
|
|
9
10
|
*/
|
|
10
11
|
import { execSync } from "node:child_process";
|
|
11
12
|
import { createHash } from "node:crypto";
|
|
12
13
|
import { resolve, sep } from "node:path";
|
|
13
|
-
import { acknowledgeUnknownDurableJob, createDurableJob, durableJobIdempotencyKey, isDurableJobTerminal, requestDurableJobCancel, retryDurableJobIdempotencyKey } from "../job/durableJob.js";
|
|
14
14
|
import { activeLiveRoleAgentSession } from "../executor/agentExecutor.js";
|
|
15
|
+
import { acknowledgeUnknownDurableJob, createDurableJob, durableJobIdempotencyKey, isDurableJobTerminal, requestDurableJobCancel, retryDurableJobIdempotencyKey } from "../job/durableJob.js";
|
|
15
16
|
import { CallAuthority } from "../kernel/callAuthority.js";
|
|
16
17
|
import { redactLaunchText } from "../runtime/launchDiagnostics.js";
|
|
17
|
-
import { requireManagedTaskCaller } from "../runtime/managedCaller.js";
|
|
18
|
+
import { requireManagedTaskCaller, ManagedRuntimeDriftError } from "../runtime/managedCaller.js";
|
|
19
|
+
import { CliError } from "../errors/cliError.js";
|
|
20
|
+
import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAssignmentScope.js";
|
|
21
|
+
import { assertContextRecordReadable } from "../context/taskContext.js";
|
|
18
22
|
export function createDurableJobControl(store) {
|
|
19
23
|
const authority = createJobCallAuthority(store);
|
|
20
24
|
return {
|
|
@@ -25,6 +29,7 @@ export function createDurableJobControl(store) {
|
|
|
25
29
|
// startJob with the same key create a duplicate job.
|
|
26
30
|
return store.transaction((tx) => {
|
|
27
31
|
const context = authority.authenticate(Object.freeze({ ...params.caller }), params.taskId);
|
|
32
|
+
requireJobAssignment(tx, params, params.caller);
|
|
28
33
|
assertNonSecretJobInput(params);
|
|
29
34
|
const baseKey = durableJobIdempotencyKey({
|
|
30
35
|
owner: params.owner,
|
|
@@ -37,8 +42,7 @@ export function createDurableJobControl(store) {
|
|
|
37
42
|
const inputDigest = params.retryOf === undefined
|
|
38
43
|
? baseKey
|
|
39
44
|
: retryDurableJobIdempotencyKey(baseKey, params.retryOf);
|
|
40
|
-
const requestId = params.requestId
|
|
41
|
-
: requiredId(params.requestId, "job.start requestId");
|
|
45
|
+
const requestId = requiredId(params.requestId, "job.start requestId");
|
|
42
46
|
// An IntegrationAttempt already is a durable operation identity.
|
|
43
47
|
// Recovery by another authorized Role must find its original Job,
|
|
44
48
|
// including the window before Integration persisted the returned id.
|
|
@@ -68,12 +72,6 @@ export function createDurableJobControl(store) {
|
|
|
68
72
|
}
|
|
69
73
|
return { job: existing, created: false };
|
|
70
74
|
}
|
|
71
|
-
// A historical content-addressed request has no attributable caller.
|
|
72
|
-
// Do not silently execute it again under a newly attributed identity.
|
|
73
|
-
const historical = tx.findDurableJobByIdempotencyKey(params.taskId, inputDigest);
|
|
74
|
-
if (params.requestId === undefined && historical !== null) {
|
|
75
|
-
throw jobDomainError(`Historical request already exists: ${historical.id}; inspect it or select an explicit new requestId.`);
|
|
76
|
-
}
|
|
77
75
|
authority.authorize(context, params.taskId);
|
|
78
76
|
validateStartParams(tx, params);
|
|
79
77
|
const id = tx.nextDurableJobId(params.taskId);
|
|
@@ -97,7 +95,21 @@ export function createDurableJobControl(store) {
|
|
|
97
95
|
return { job, created: true };
|
|
98
96
|
});
|
|
99
97
|
},
|
|
100
|
-
getJob(taskId, jobId) {
|
|
98
|
+
getJob(taskId, jobId, caller) {
|
|
99
|
+
if (caller === undefined)
|
|
100
|
+
throw jobControlError("UNAUTHORIZED", "Job reads require an explicit caller.");
|
|
101
|
+
try {
|
|
102
|
+
assertContextRecordReadable(store, taskId, "job", jobId, caller.scope === "user" ? {} : {
|
|
103
|
+
YUI_SESSION_SCOPE: caller.scope, YUI_TASK_ID: caller.taskId, YUI_ROLE: caller.role,
|
|
104
|
+
YUI_AGENT_ID: caller.agentId, YUI_ADAPTER_ID: caller.adapterId, YUI_NATIVE_SESSION_ID: caller.nativeSessionId
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error instanceof ManagedRuntimeDriftError || (error instanceof CliError && error.code === "USAGE_ERROR")) {
|
|
109
|
+
throw jobControlError("UNAUTHORIZED", error.message);
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
101
113
|
return store.getDurableJob(taskId, jobId);
|
|
102
114
|
},
|
|
103
115
|
cancelJob(taskId, jobId, now, caller) {
|
|
@@ -108,6 +120,7 @@ export function createDurableJobControl(store) {
|
|
|
108
120
|
// rr8: Bind the cancel request to the caller's managed identity. The
|
|
109
121
|
// same rules as job.start apply, checked against the job's owner.
|
|
110
122
|
assertCallerAuthorized(tx, caller, taskId);
|
|
123
|
+
requireJobAssignment(tx, current, caller);
|
|
111
124
|
const next = requestDurableJobCancel(current, now);
|
|
112
125
|
if (next !== current)
|
|
113
126
|
tx.saveDurableJob(taskId, next);
|
|
@@ -120,6 +133,7 @@ export function createDurableJobControl(store) {
|
|
|
120
133
|
if (current === null)
|
|
121
134
|
return null;
|
|
122
135
|
assertCallerAuthorized(tx, caller, taskId);
|
|
136
|
+
requireJobAssignment(tx, current, caller);
|
|
123
137
|
const next = acknowledgeUnknownDurableJob(current, now);
|
|
124
138
|
if (next !== current)
|
|
125
139
|
tx.saveDurableJob(taskId, next);
|
|
@@ -152,8 +166,19 @@ export function authorizeJobStart(store, job) {
|
|
|
152
166
|
if (jobAuthorityBinding(store, scope, role, job.taskId) !== job.operation.authorityRef) {
|
|
153
167
|
throw jobDomainError("Job caller binding was revoked; no execution was started.");
|
|
154
168
|
}
|
|
169
|
+
requireJobAssignment(store, job, { scope, role });
|
|
155
170
|
validateJobTarget(store, job);
|
|
156
171
|
}
|
|
172
|
+
function requireJobAssignment(store, target, caller) {
|
|
173
|
+
try {
|
|
174
|
+
assertJobAssignmentScope(store, target, caller);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error instanceof JobAssignmentScopeError)
|
|
178
|
+
throw jobControlError("UNAUTHORIZED", error.message);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
157
182
|
function jobAuthorityBinding(store, scope, roleName, taskId) {
|
|
158
183
|
// A Host detach/reattach preserves the native Session and its queued work.
|
|
159
184
|
// Authenticate the live launch at ingress, but bind accepted Jobs to the
|
|
@@ -328,8 +353,8 @@ function validateJobTarget(store, params) {
|
|
|
328
353
|
* - `task` + mismatched taskId: rejected.
|
|
329
354
|
* - A non-Leader Task caller requires its current active Assignment.
|
|
330
355
|
* - A Leader Task caller requires its current, unrevoked native Session.
|
|
331
|
-
* -
|
|
332
|
-
*
|
|
356
|
+
* - Non-Leader Task callers additionally pass the shared exact Assignment
|
|
357
|
+
* check for every Job operation and before a queued Job starts.
|
|
333
358
|
*/
|
|
334
359
|
/**
|
|
335
360
|
* Verify the caller's current native Session and Task scope. This local Home
|
|
@@ -470,28 +495,26 @@ export function parseDurableJobStartParams(value) {
|
|
|
470
495
|
env,
|
|
471
496
|
steps,
|
|
472
497
|
caller,
|
|
473
|
-
|
|
474
|
-
requestId: requiredId(record.requestId, "job.start requestId")
|
|
475
|
-
}),
|
|
498
|
+
requestId: requiredId(record.requestId, "job.start requestId"),
|
|
476
499
|
...(retryOf === undefined ? {} : { retryOf })
|
|
477
500
|
};
|
|
478
501
|
}
|
|
479
502
|
export function parseDurableJobRefParams(value) {
|
|
480
503
|
if (typeof value !== "object" || value === null || Array.isArray(value)
|
|
481
|
-
|| Object.keys(value).length !==
|
|
504
|
+
|| Object.keys(value).length !== 3) {
|
|
482
505
|
throw jobControlError("INVALID_PARAMS", "DurableJob ref params are invalid.");
|
|
483
506
|
}
|
|
484
507
|
const record = value;
|
|
485
508
|
return {
|
|
486
509
|
taskId: requiredId(record.taskId, "DurableJob taskId"),
|
|
487
|
-
jobId: requiredId(record.jobId, "DurableJob jobId")
|
|
510
|
+
jobId: requiredId(record.jobId, "DurableJob jobId"),
|
|
511
|
+
caller: parseCaller(record.caller)
|
|
488
512
|
};
|
|
489
513
|
}
|
|
490
514
|
/**
|
|
491
515
|
* rr8: `job.cancel` params carry the caller identity so the Controller can
|
|
492
|
-
* bind the cancel request to the caller's managed scope.
|
|
493
|
-
*
|
|
494
|
-
* third `caller` key.
|
|
516
|
+
* bind the cancel request to the caller's managed scope. Reads also carry
|
|
517
|
+
* an explicit caller but use Context's read authority rather than write authority.
|
|
495
518
|
*/
|
|
496
519
|
export function parseDurableJobCancelParams(value) {
|
|
497
520
|
if (typeof value !== "object" || value === null || Array.isArray(value)
|
|
@@ -551,7 +574,7 @@ function parseSteps(value) {
|
|
|
551
574
|
throw jobControlError("INVALID_PARAMS", "job.start steps are invalid.");
|
|
552
575
|
}
|
|
553
576
|
const record = entry;
|
|
554
|
-
const allowed = new Set(["name", "command", "timeoutMs"]);
|
|
577
|
+
const allowed = new Set(["name", "command", "argv", "cwd", "env", "timeoutMs"]);
|
|
555
578
|
for (const key of Object.keys(record)) {
|
|
556
579
|
if (!allowed.has(key)) {
|
|
557
580
|
throw jobControlError("INVALID_PARAMS", "job.start steps are invalid.");
|
|
@@ -559,6 +582,15 @@ function parseSteps(value) {
|
|
|
559
582
|
}
|
|
560
583
|
const name = requiredId(record.name, "job.start step name");
|
|
561
584
|
const command = requiredId(record.command, "job.start step command");
|
|
585
|
+
const argv = record.argv === undefined ? undefined : record.argv;
|
|
586
|
+
if (argv !== undefined && (!Array.isArray(argv) || argv.length === 0
|
|
587
|
+
|| argv.some(value => typeof value !== "string" || value.length === 0 || value.includes("\0")))) {
|
|
588
|
+
throw jobControlError("INVALID_PARAMS", `job.start step argv is invalid: ${name}.`);
|
|
589
|
+
}
|
|
590
|
+
const cwd = record.cwd === undefined ? undefined : requiredId(record.cwd, "job.start step cwd");
|
|
591
|
+
if (cwd !== undefined && !cwd.startsWith("/")) {
|
|
592
|
+
throw jobControlError("INVALID_PARAMS", `job.start step cwd must be absolute: ${name}.`);
|
|
593
|
+
}
|
|
562
594
|
if (names.has(name)) {
|
|
563
595
|
throw jobControlError("INVALID_PARAMS", `job.start step names must be unique: ${name}.`);
|
|
564
596
|
}
|
|
@@ -572,6 +604,9 @@ function parseSteps(value) {
|
|
|
572
604
|
const step = {
|
|
573
605
|
name,
|
|
574
606
|
command,
|
|
607
|
+
...(argv === undefined ? {} : { argv: argv }),
|
|
608
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
609
|
+
...(record.env === undefined ? {} : { env: parseStringMap(record.env, "job.start step env") }),
|
|
575
610
|
...(record.timeoutMs === undefined ? {} : { timeoutMs: record.timeoutMs })
|
|
576
611
|
};
|
|
577
612
|
steps.push(step);
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
* cancelRequestedAt -> write cancel fence + SIGTERM
|
|
13
13
|
*/
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
-
import { chmodSync, existsSync, mkdirSync, openSync,
|
|
15
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
19
18
|
import { cancelQueuedDurableJob, completeDurableJob, isDurableJobTerminal, markDurableJobUnknown, markDurableJobWakeupNotified, rejectQueuedDurableJob, startDurableJob, touchDurableJobHeartbeat } from "../job/durableJob.js";
|
|
20
|
-
import { readLinuxProcessStartIdentity } from "./domainIdentity.js";
|
|
21
|
-
import { wakeReason } from "../scheduler/wakeReason.js";
|
|
22
19
|
import { recordOperationEvidence } from "../kernel/operationFacts.js";
|
|
20
|
+
import { wakeReason } from "../scheduler/wakeReason.js";
|
|
21
|
+
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
22
|
+
import { readLinuxProcessStartIdentity } from "./domainIdentity.js";
|
|
23
23
|
const DEFAULT_STEP_TIMEOUT_MS = 30 * 60_000;
|
|
24
24
|
const HEARTBEAT_STALE_MS = 2 * 60_000;
|
|
25
25
|
const SIGKILL_GRACE_MS = 30_000;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { admitProviderRetry, cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
|
|
2
|
+
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
3
|
+
import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
export function ownedProviderInput(input) {
|
|
6
|
+
if (input.runId !== undefined && input.taskId !== undefined) {
|
|
7
|
+
return { kind: "run", taskId: input.taskId, runId: input.runId };
|
|
8
|
+
}
|
|
9
|
+
const wake = /^notification:([^/]+)\/([^/]+)\//u.exec(input.attemptId);
|
|
10
|
+
if (wake !== null && wake[1] === input.taskId) {
|
|
11
|
+
return { kind: "wake", taskId: wake[1], wakeId: wake[2] };
|
|
12
|
+
}
|
|
13
|
+
const message = /^global-input:([^/]+)\/([^/]+)$/u.exec(input.attemptId);
|
|
14
|
+
if (message !== null && message[1] === input.roleName && input.taskId === undefined) {
|
|
15
|
+
return { kind: "message", roleName: input.roleName, messageId: message[2] };
|
|
16
|
+
}
|
|
17
|
+
return input.boundedText === undefined ? undefined : { kind: "text", text: input.boundedText };
|
|
18
|
+
}
|
|
19
|
+
export function retryIntentBlocker(store, input, retry) {
|
|
20
|
+
if (input.taskId === undefined) {
|
|
21
|
+
const role = store.getGlobalRole(input.roleName);
|
|
22
|
+
const sessions = store.getGlobalRoleSessionSet(input.roleName);
|
|
23
|
+
if (role === null || sessions?.activeAgentId !== role.activeAgentId)
|
|
24
|
+
return "role-changed";
|
|
25
|
+
if (sessions.sessions[sessions.activeAgentId]?.effective.sourceDesiredRevision !== role.launchRevision)
|
|
26
|
+
return "role-configuration-changed";
|
|
27
|
+
if (store.listGlobalRoleMessages(input.roleName).some(message => Date.parse(message.createdAt) > Date.parse(retry.intentAt)
|
|
28
|
+
&& message.delivery === undefined && message.notDelivered === undefined))
|
|
29
|
+
return "new-user-input";
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const task = store.getTask(input.taskId);
|
|
33
|
+
if (task === null || !["active", "draft"].includes(task.status)
|
|
34
|
+
|| task.executionGate.state !== "enabled")
|
|
35
|
+
return "task-execution-stopped";
|
|
36
|
+
const role = store.getRole(input.taskId, input.roleName);
|
|
37
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
38
|
+
if (role === null || sessions?.activeAgentId !== role.activeAgentId)
|
|
39
|
+
return "role-changed";
|
|
40
|
+
if (sessions.sessions[sessions.activeAgentId]?.effective.sourceDesiredRevision !== role.launchRevision)
|
|
41
|
+
return "role-configuration-changed";
|
|
42
|
+
if (store.listMessages(input.taskId).some(message => (message.kind === "operator" || message.kind === "user")
|
|
43
|
+
&& Date.parse(message.createdAt) > Date.parse(retry.intentAt)))
|
|
44
|
+
return "new-user-input";
|
|
45
|
+
if (blockingProviderContinuations(store.listEvents(input.taskId)).some(c => c.roleName === input.roleName && c.identity.conversationId === retry.nativeSessionId)) {
|
|
46
|
+
return "background-work-unsettled";
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Shared by automatic recovery and every ordinary Host submit. The native
|
|
51
|
+
* writer CAS remains the only grant; a scheduled successor is not a grant. */
|
|
52
|
+
export function admitOwnedProviderInput(store, binding, input) {
|
|
53
|
+
const retry = binding.retry;
|
|
54
|
+
const sameReviewSuccessor = retry?.successorReviewRoundId !== undefined && input.taskId !== undefined && input.runId !== undefined
|
|
55
|
+
&& store.getRun(input.taskId, input.runId)?.reviewRoundId === retry.successorReviewRoundId;
|
|
56
|
+
if (retry !== undefined && !providerRetryPending(binding)
|
|
57
|
+
&& (input.attemptId.startsWith("provider-retry:") || input.runId !== undefined && input.runId === retry.successorRunId
|
|
58
|
+
|| sameReviewSuccessor)) {
|
|
59
|
+
throw new Error(`Provider retry is ${retry.status}; this scheduled successor cannot regain admission.`);
|
|
60
|
+
}
|
|
61
|
+
if (!providerRetryPending(binding) || retry === undefined)
|
|
62
|
+
return binding;
|
|
63
|
+
const automatic = input.attemptId === providerRetryAttemptId(retry)
|
|
64
|
+
|| input.runId !== undefined && input.runId === retry.successorRunId
|
|
65
|
+
|| input.runId !== undefined && input.taskId !== undefined && retry.successorReviewRoundId !== undefined
|
|
66
|
+
&& store.getRun(input.taskId, input.runId)?.reviewRoundId === retry.successorReviewRoundId
|
|
67
|
+
|| retry.status === "in-flight" && input.attemptId === retry.currentAttemptId;
|
|
68
|
+
if (!automatic)
|
|
69
|
+
return cancelProviderRetry(binding, "superseded-by-explicit-input", input.now.getTime());
|
|
70
|
+
const blocker = retryIntentBlocker(store, input, retry);
|
|
71
|
+
if (blocker !== undefined)
|
|
72
|
+
throw new Error(`Provider retry admission stopped: ${blocker}.`);
|
|
73
|
+
if (input.retrySupported !== true)
|
|
74
|
+
throw new Error("This Host cannot prove safe same-Session recovery; use explicit recovery.");
|
|
75
|
+
if (binding.goal?.status === "active")
|
|
76
|
+
throw new Error("Provider still owns an autonomous Goal; inspect its exact continuation.");
|
|
77
|
+
if (input.taskId !== undefined && input.runId !== undefined) {
|
|
78
|
+
const run = store.getRun(input.taskId, input.runId);
|
|
79
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
80
|
+
if (run === null || run.status !== "active"
|
|
81
|
+
|| !isDeepStrictEqual(run.effective, sessions?.sessions[input.agentId]?.effective)) {
|
|
82
|
+
throw new Error("Retry execution no longer matches its fixed native Session.");
|
|
83
|
+
}
|
|
84
|
+
if (run.workspace !== undefined && !isDeepStrictEqual(store.getManagedWorkspace(run.workspace.owner), run.workspace)) {
|
|
85
|
+
throw new Error("Retry workspace authority changed.");
|
|
86
|
+
}
|
|
87
|
+
if (run.workItemId !== undefined) {
|
|
88
|
+
const item = store.getWorkItem(input.taskId, run.workItemId);
|
|
89
|
+
if (item?.status !== "open" || run.purpose !== "review" && run.executionLaneId === undefined
|
|
90
|
+
&& item.assignee !== run.roleName)
|
|
91
|
+
throw new Error("Retry WorkItem responsibility changed.");
|
|
92
|
+
}
|
|
93
|
+
if (validateExactRunReviewRound(store, run).disposition !== "applied") {
|
|
94
|
+
throw new Error("Retry Review no longer matches its exact Round and frozen candidate.");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const admitted = admitProviderRetry(binding, { attemptId: input.attemptId, at: input.now.getTime() });
|
|
98
|
+
return input.runId === undefined ? admitted
|
|
99
|
+
: { ...admitted, retry: { ...admitted.retry, successorRunId: input.runId } };
|
|
100
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { runTaskCommand } from "../commands/taskCommands.js";
|
|
3
|
+
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
4
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
5
|
+
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
6
|
+
import { redactAgentErrorText } from "../runtime/agentError.js";
|
|
7
|
+
import { AGENT_HOST_CONTROL_PROTOCOL, agentHostControlSocketPath, inspectAgentHostSocket, sendAgentHostRunControl } from "../runtime/agentHost.js";
|
|
8
|
+
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
9
|
+
import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
|
|
10
|
+
import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
11
|
+
import { retryIntentBlocker } from "./providerRetryAdmission.js";
|
|
12
|
+
/** Hooks in the existing Controller pass and nearest-deadline timer. SQLite's
|
|
13
|
+
* current Provider input is the only pending intent; nothing is queued here. */
|
|
14
|
+
export function createProviderRetryHooks(home, store, ports = {}) {
|
|
15
|
+
const anchors = new Map();
|
|
16
|
+
const inspectedUnknown = new Set();
|
|
17
|
+
const inspect = ports.inspect ?? ((target) => inspectAgentHostSocket(agentHostControlSocketPath(target), 250));
|
|
18
|
+
const now = (retry) => {
|
|
19
|
+
const wall = (ports.now ?? (() => new Date()))().getTime();
|
|
20
|
+
const monotonic = (ports.monotonicNow ?? (() => performance.now()))();
|
|
21
|
+
let anchor = anchors.get(retry.chainId);
|
|
22
|
+
if (anchor === undefined) {
|
|
23
|
+
anchor = { wall: Math.max(wall, Date.parse(retry.updatedAt)), monotonic };
|
|
24
|
+
anchors.set(retry.chainId, anchor);
|
|
25
|
+
}
|
|
26
|
+
return Math.max(wall, anchor.wall + Math.max(0, monotonic - anchor.monotonic));
|
|
27
|
+
};
|
|
28
|
+
const load = (owner) => owner.scope === "task"
|
|
29
|
+
? store.getTaskRoleSessionSet(owner.taskId, owner.roleName) : store.getGlobalRoleSessionSet(owner.roleName);
|
|
30
|
+
const save = (set) => {
|
|
31
|
+
if (set.owner.scope === "task")
|
|
32
|
+
store.saveTaskRoleSessionSet(set);
|
|
33
|
+
else
|
|
34
|
+
store.saveGlobalRoleSessionSet(set);
|
|
35
|
+
};
|
|
36
|
+
const stop = (set, reason, at, exhausted = false) => {
|
|
37
|
+
store.transaction(() => {
|
|
38
|
+
const current = load(set.owner);
|
|
39
|
+
if (current === null || current.providerBinding?.retry?.chainId !== set.providerBinding?.retry?.chainId)
|
|
40
|
+
return;
|
|
41
|
+
const binding = current.providerBinding;
|
|
42
|
+
if (!providerRetryPending(binding))
|
|
43
|
+
return;
|
|
44
|
+
const cancelled = cancelProviderRetry(binding, reason, at);
|
|
45
|
+
save({ ...current, providerBinding: exhausted
|
|
46
|
+
? { ...cancelled, retry: { ...cancelled.retry, status: "exhausted" } } : cancelled });
|
|
47
|
+
const owner = set.owner;
|
|
48
|
+
if (owner.scope === "global")
|
|
49
|
+
settleGlobalRetryInput(store, owner.roleName, load(owner)?.providerBinding, new Date(at));
|
|
50
|
+
if (owner.scope === "task") {
|
|
51
|
+
const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-stopped", {
|
|
52
|
+
roleName: owner.roleName, chainId: binding.retry.chainId, reason
|
|
53
|
+
}, new Date(at));
|
|
54
|
+
store.saveEvent(owner.taskId, event);
|
|
55
|
+
enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
|
|
56
|
+
: { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const key = (set) => set.owner.scope === "task"
|
|
61
|
+
? `role:${encodeURIComponent(set.owner.taskId)}/${encodeURIComponent(set.owner.roleName)}`
|
|
62
|
+
: `global-role:${encodeURIComponent(set.owner.roleName)}`;
|
|
63
|
+
return {
|
|
64
|
+
deadlines() {
|
|
65
|
+
return store.listProviderRetrySessions().flatMap(set => {
|
|
66
|
+
const retry = set.providerBinding.retry;
|
|
67
|
+
if (retry.status !== "waiting")
|
|
68
|
+
return [];
|
|
69
|
+
// Busy or already-queued work awaits the next ordinary pass without a
|
|
70
|
+
// zero-delay timer loop. Waiting never consumes an execution attempt.
|
|
71
|
+
const wall = (ports.now ?? (() => new Date()))().getTime();
|
|
72
|
+
const effective = now(retry);
|
|
73
|
+
return [{ key: key(set), at: wall + Math.max(250, (retry.successorAutomatic === false ? Date.parse(retry.nextEligibleAt)
|
|
74
|
+
: Math.min(Date.parse(retry.nextEligibleAt), Date.parse(retry.deadline))) - effective) }];
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
async reconcile() {
|
|
78
|
+
const current = store.listProviderRetrySessions();
|
|
79
|
+
const activeChains = new Set(current.map(set => set.providerBinding.retry.chainId));
|
|
80
|
+
for (const id of anchors.keys())
|
|
81
|
+
if (!activeChains.has(id))
|
|
82
|
+
anchors.delete(id);
|
|
83
|
+
const pendingAttempts = new Set(current.map(set => set.providerBinding.retry.currentAttemptId));
|
|
84
|
+
for (const id of inspectedUnknown)
|
|
85
|
+
if (!pendingAttempts.has(id))
|
|
86
|
+
inspectedUnknown.delete(id);
|
|
87
|
+
for (const set of current) {
|
|
88
|
+
const owner = set.owner;
|
|
89
|
+
const binding = set.providerBinding;
|
|
90
|
+
const retry = binding.retry;
|
|
91
|
+
const at = now(retry);
|
|
92
|
+
const scoped = { ...(owner.scope === "task" ? { taskId: owner.taskId } : {}), roleName: owner.roleName };
|
|
93
|
+
const session = set.sessions[set.activeAgentId];
|
|
94
|
+
const blocker = retryIntentBlocker(store, scoped, retry);
|
|
95
|
+
if (blocker !== undefined && blocker !== "background-work-unsettled") {
|
|
96
|
+
stop(set, blocker, at);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (binding.retryDisabled && retry.successorAutomatic !== false || binding.authority.owner !== "controller"
|
|
100
|
+
|| binding.authority.epoch !== retry.authorityEpoch
|
|
101
|
+
|| currentProviderConversation(binding).conversationId !== retry.nativeSessionId
|
|
102
|
+
|| session?.status !== "active" || session.nativeSessionId !== retry.nativeSessionId
|
|
103
|
+
|| hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
|
|
104
|
+
stop(set, "session-or-authority-changed", at);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Never replay an admitted attempt. Existing exact runtime queries and
|
|
108
|
+
// terminal ingress resolve it, even across Controller restart.
|
|
109
|
+
if (retry.status === "in-flight") {
|
|
110
|
+
if (binding.run?.status === "delivery-unknown" && retry.currentAttemptId !== undefined
|
|
111
|
+
&& !inspectedUnknown.has(retry.currentAttemptId)) {
|
|
112
|
+
inspectedUnknown.add(retry.currentAttemptId);
|
|
113
|
+
let detail;
|
|
114
|
+
try {
|
|
115
|
+
const host = await inspect({ home, ...scoped, scope: owner.scope });
|
|
116
|
+
detail = host.nativeSessionId === retry.nativeSessionId && host.attemptId === retry.currentAttemptId
|
|
117
|
+
? `Exact Host query: ${host.state}; awaiting original canonical Provider evidence.`
|
|
118
|
+
: "Host query did not prove the original Session/input identity; no replay.";
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
detail = `Original Host query unavailable; no replay: ${redactAgentErrorText(error instanceof Error ? error.message : String(error))}`;
|
|
122
|
+
}
|
|
123
|
+
store.transaction(() => {
|
|
124
|
+
const latest = load(owner);
|
|
125
|
+
if (latest === null || latest.providerBinding == null || latest.providerBinding.run === null
|
|
126
|
+
|| latest.providerBinding.run.attemptId !== retry.currentAttemptId
|
|
127
|
+
|| latest.providerBinding.run.status !== "delivery-unknown")
|
|
128
|
+
return;
|
|
129
|
+
save({ ...latest, providerBinding: { ...latest.providerBinding,
|
|
130
|
+
retry: { ...latest.providerBinding.retry, reason: detail } } });
|
|
131
|
+
if (owner.scope === "task") {
|
|
132
|
+
const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-unconfirmed", {
|
|
133
|
+
roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
|
|
134
|
+
}, new Date(at));
|
|
135
|
+
store.saveEvent(owner.taskId, event);
|
|
136
|
+
enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
|
|
137
|
+
: { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (retry.successorAutomatic !== false && at >= Date.parse(retry.deadline)) {
|
|
144
|
+
stop(set, "deadline-exhausted", at, true);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (at < Date.parse(retry.nextEligibleAt) || blocker !== undefined || binding.goal?.status === "active")
|
|
148
|
+
continue;
|
|
149
|
+
if (binding.run?.attemptId !== retry.failedAttemptId) {
|
|
150
|
+
stop(set, "superseded-input", at);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
if (owner.scope === "task" && retry.previousRunId !== undefined) {
|
|
155
|
+
if (retry.successorRunId !== undefined || retry.successorReviewRoundId !== undefined)
|
|
156
|
+
continue;
|
|
157
|
+
const previous = store.getRun(owner.taskId, retry.previousRunId);
|
|
158
|
+
if (previous?.status === "active")
|
|
159
|
+
continue; // ordinary rejected-delivery terminal fold
|
|
160
|
+
if (previous?.status !== "failed") {
|
|
161
|
+
stop(set, "run-no-longer-failed", at);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const actualTaskReviewCandidate = await ports.snapshotTaskCandidate?.(previous);
|
|
165
|
+
store.transaction(() => {
|
|
166
|
+
const latest = load(owner)?.providerBinding?.retry;
|
|
167
|
+
if (latest?.chainId !== retry.chainId || latest.failedAttemptId !== retry.failedAttemptId
|
|
168
|
+
|| latest.status !== "waiting")
|
|
169
|
+
return;
|
|
170
|
+
const reason = retryIntentBlocker(store, scoped, latest);
|
|
171
|
+
if (reason !== undefined)
|
|
172
|
+
throw new Error(reason);
|
|
173
|
+
runTaskCommand(["run", "retry", `${owner.taskId}/${previous.id}`], store, {
|
|
174
|
+
now: () => new Date(at), environment: {}, providerRetryChainId: retry.chainId,
|
|
175
|
+
...(actualTaskReviewCandidate === undefined ? {} : { actualTaskReviewCandidate })
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const original = retry.input.kind === "text" ? retry.input.text
|
|
181
|
+
: retry.input.kind === "wake"
|
|
182
|
+
? `Yui Task notification: task=${retry.input.taskId} wake=${retry.input.wakeId}.\nRead current Task context and that fixed wake window, including original Messages in full. This is not an execution assignment.`
|
|
183
|
+
: retry.input.kind === "message"
|
|
184
|
+
? `Yui Global Message: role=${retry.input.roleName} message=${retry.input.messageId}.\nRead your Session Context and referenced original Message in full.`
|
|
185
|
+
: undefined;
|
|
186
|
+
if (original === undefined) {
|
|
187
|
+
stop(set, "original-input-unavailable", at);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const result = await (ports.submit ?? sendAgentHostRunControl)({
|
|
191
|
+
home, ...scoped, scope: owner.scope,
|
|
192
|
+
control: {
|
|
193
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn", nativeSessionId: retry.nativeSessionId,
|
|
194
|
+
authority: { epoch: binding.authority.epoch, owner: "controller", holderId: binding.authority.holderId },
|
|
195
|
+
run: { attemptId: providerRetryAttemptId(retry), boundedText: original }
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
// Durable settlement belongs to Host/Controller ingress. In
|
|
199
|
+
// particular a lost response is never a reason to replay.
|
|
200
|
+
if (result.outcome === "rejected" && result.failure?.registrationDisposition === "not-committed"
|
|
201
|
+
&& result.failure.errorName !== "ProviderTurnBusyError") {
|
|
202
|
+
stop(set, result.failure.detail, at);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// A send failure may have applied. A durable submitting/unknown
|
|
208
|
+
// identity fences it; do not manufacture a successor to uncertainty.
|
|
209
|
+
const latest = load(owner);
|
|
210
|
+
if (latest?.providerBinding?.run?.attemptId === retry.failedAttemptId) {
|
|
211
|
+
stop(set, error instanceof Error ? error.message : String(error), at);
|
|
212
|
+
}
|
|
213
|
+
ports.onError?.(error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|