@zq-silk/yui 0.13.9 → 0.14.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 +69 -428
- package/README.md +108 -298
- package/dist/agent/managedRuntimeEnvironment.js +2 -2
- package/dist/cli/commandCatalog.js +39 -77
- package/dist/cli/interactionCandidates.js +4 -4
- package/dist/cli/interactionPolicy.js +7 -10
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/updateCommand.js +6 -27
- package/dist/cli/updateOrchestrator.js +63 -484
- package/dist/cli/updatePorts.js +15 -333
- package/dist/cli/upgradeCommand.js +8 -65
- package/dist/cli.js +57 -242
- package/dist/commands/configCommands.js +11 -62
- package/dist/commands/executionAuditCommands.js +23 -23
- package/dist/commands/globalRoleCommands.js +2 -7
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/projectCommands.js +5 -5
- package/dist/commands/releaseCommands.js +3 -10
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/sessionCommands.js +2 -2
- package/dist/commands/taskActor.js +31 -39
- package/dist/commands/taskChangeSetCommands.js +5 -12
- package/dist/commands/taskCommands.js +464 -1249
- package/dist/commands/taskContextCommand.js +35 -36
- package/dist/commands/taskExecutionCommands.js +33 -46
- package/dist/commands/taskInputCommands.js +20 -42
- package/dist/commands/taskNextActionCommand.js +9 -9
- package/dist/commands/taskOverviewCommand.js +56 -95
- package/dist/commands/taskRoleRuntimeStatus.js +77 -107
- package/dist/commands/taskUpstreamCommands.js +3 -3
- package/dist/commands/taskWorkspaceCommands.js +7 -79
- package/dist/commands/telemetryCommands.js +32 -124
- package/dist/commands/workflowCommands.js +3 -3
- package/dist/completion/fileCompletionManager.js +1 -1
- package/dist/config/configCatalog.js +4 -6
- package/dist/config/yuiConfig.js +6 -43
- package/dist/context/dispatchContext.js +14 -30
- package/dist/context/roleSessionContext.js +2 -2
- package/dist/context/sessionBootstrapManifest.js +2 -2
- package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
- package/dist/context/turnInputContract.js +204 -0
- package/dist/context/wakeNotification.js +2 -2
- package/dist/controller/agentRuntimeObserver.js +18 -18
- package/dist/controller/clientRuntime.js +13 -20
- package/dist/controller/controller.js +89 -116
- package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
- package/dist/controller/jobControl.js +17 -17
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +38 -59
- package/dist/controller/resourceInventoryRpc.js +3 -3
- package/dist/controller/resourceInventoryWorker.js +1 -1
- package/dist/controller/runtime.js +46 -53
- package/dist/controller/runtimeEventInbox.js +23 -31
- package/dist/controller/runtimeEventProcessor.js +15 -15
- package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
- package/dist/controller/runtimeLaunchCoordinator.js +3 -3
- package/dist/controller/runtimeObservationHook.js +11 -8
- package/dist/controller/sessionNotify.js +11 -11
- package/dist/controller/structuredProviderObservation.js +89 -16
- package/dist/coordination/workMailbox.js +309 -579
- package/dist/coordination/workMailboxQueue.js +5 -6
- package/dist/core/controllerClient.js +1 -91
- package/dist/core/controllerServer.js +3 -6
- package/dist/doctor/doctor.js +51 -314
- package/dist/execution/candidateConvergence.js +3 -3
- package/dist/execution/executionGroup.js +32 -33
- package/dist/execution/executionHealth.js +36 -38
- package/dist/execution/resourceBroker.js +12 -12
- package/dist/executor/agentExecutor.js +30 -265
- package/dist/executor/effectiveLaunch.js +9 -19
- package/dist/executor/executorRegistry.js +43 -17
- package/dist/executor/fileRoleLaunchPlanner.js +56 -56
- package/dist/executor/launchPlan.js +3 -3
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +20 -25
- package/dist/input/inputRequest.js +12 -12
- package/dist/integration/changeSet.js +9 -13
- package/dist/integration/changeSetManifest.js +1 -1
- package/dist/integration/gitIntegrationService.js +2 -2
- package/dist/integration/integrationQueueService.js +5 -21
- package/dist/integration/overlapDiagnostics.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/job/jobRunner.js +1 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
- package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
- package/dist/message/message.js +6 -6
- package/dist/observability/executionAudit.js +74 -86
- package/dist/observability/faultClassification.js +7 -24
- package/dist/observability/orchestrationMetrics.js +5 -5
- package/dist/observability/runtimeIdentity.js +34 -101
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +3 -3
- package/dist/release/releaseWorkflowEngine.js +6 -10
- package/dist/release/releaseWorkflowPorts.js +2 -2
- package/dist/release/workflowFileLock.js +1 -1
- package/dist/repository/checkoutSwap.js +1 -1
- package/dist/repository/gitWorkspace.js +0 -13
- package/dist/repository/project.js +1 -1
- package/dist/repository/projectMaintenanceLock.js +1 -2
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +21 -25
- package/dist/repository/taskWorkspaceIdentity.js +8 -21
- package/dist/repository/taskWorkspacePreparer.js +63 -620
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewFindingLedger.js +1 -1
- package/dist/review/reviewOutcomeClassifier.js +35 -50
- package/dist/review/reviewRound.js +14 -25
- package/dist/review/reviewerAvailability.js +4 -4
- package/dist/review/taskFinalReviewContractResolution.js +41 -0
- package/dist/role/role.js +6 -3
- package/dist/runtime/agentDriver.js +4 -0
- package/dist/runtime/agentHost.js +151 -36
- package/dist/runtime/builtinAgentDrivers.js +79 -2
- package/dist/runtime/codexAppServerRuntime.js +122 -0
- package/dist/runtime/continuationManager.js +3 -5
- package/dist/runtime/exactControlPlane.js +17 -42
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +4 -4
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/processExitOutbox.js +0 -32
- package/dist/runtime/promptEnvelope.js +9 -9
- package/dist/runtime/providerContinuation.js +2 -2
- package/dist/runtime/providerContinuationReconciliationService.js +2 -2
- package/dist/runtime/providerRuntimeIdentity.js +76 -24
- package/dist/runtime/recentTurnIds.js +38 -0
- package/dist/runtime/runtimeContinuationProjection.js +2 -2
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +58 -21
- package/dist/runtime/runtimeProjection.js +19 -27
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/structuredProviderHost.js +177 -12
- package/dist/runtime/tmuxAdapters.js +46 -11
- package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
- package/dist/scheduler/leaderWakeupProcessor.js +147 -445
- package/dist/scheduler/operatorEvent.js +4 -6
- package/dist/scheduler/resourceQueueProjection.js +71 -0
- package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
- package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
- package/dist/scheduler/taskExecutionProjection.js +99 -99
- package/dist/scheduler/taskObservabilityProjection.js +10 -10
- package/dist/scheduler/taskWake.js +4 -4
- package/dist/scheduler/wakeReason.js +6 -10
- package/dist/scheduler/wakeupQueue.js +0 -9
- package/dist/setup/setupCommand.js +2 -2
- package/dist/storage/currentTaskStore.js +46 -0
- package/dist/storage/persistenceWorker.js +1 -1
- package/dist/storage/sqliteSchema.js +81 -485
- package/dist/storage/sqliteStore.js +206 -365
- package/dist/storage/storageSchema.js +1 -15
- package/dist/storage/storageVersions.js +8 -9
- package/dist/storage/storeRpc.js +10 -34
- package/dist/storage/taskStore.js +63 -3415
- package/dist/storage/upgrade/recordVersions.js +55 -149
- package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
- package/dist/task/completionReadiness.js +5 -5
- package/dist/task/deliveryGuard.js +10 -10
- package/dist/task/nextAction.js +74 -77
- package/dist/task/task.js +2 -4
- package/dist/task/taskRecordReference.js +3 -3
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +53 -53
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +8 -14
- package/dist/turn/turn.js +356 -0
- package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
- package/dist/verification/gateArtifactStore.js +2 -2
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +29 -54
- package/dist/web/assets/client/i18n.js +17 -37
- package/dist/web/assets/client/view.js +37 -37
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +5 -5
- package/dist/web/assets/styles/widgets.js +1 -2
- package/dist/web/webSnapshot.js +65 -60
- package/dist/workItem/workItem.js +11 -11
- package/dist/workspace/workItemChangeSetManager.js +1 -1
- package/docs/task-local-identity.md +5 -5
- package/i18n/README.zh-CN.md +41 -54
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +85 -83
- package/skills/yui-operator/SKILL.md +16 -16
- package/skills/yui-reviewer/SKILL.md +10 -10
- package/skills/yui-runtime/SKILL.md +17 -18
- package/skills/yui-worker/SKILL.md +16 -18
- package/dist/context/runContextContract.js +0 -162
- package/dist/executor/turnCompletion.js +0 -126
- package/dist/review/taskFinalReviewContractEvent.js +0 -1
- package/dist/review/taskFinalReviewContractRebind.js +0 -367
- package/dist/run/agentRun.js +0 -356
- package/dist/run/rejectedYieldAttempt.js +0 -221
- package/dist/run/runControlRequest.js +0 -50
- package/dist/run/yieldReceipt.js +0 -65
- package/dist/runtime/turnCompletion.js +0 -3
- package/dist/scheduler/actionability.js +0 -323
- package/dist/scheduler/activeRoleRunDelivery.js +0 -868
- package/dist/scheduler/activeTaskProgress.js +0 -126
- package/dist/storage/compatibleTaskStore.js +0 -248
- package/dist/storage/migration/baseline.js +0 -78
- package/dist/storage/migration/classifier.js +0 -51
- package/dist/storage/migration/compatibleCodec.js +0 -53
- package/dist/storage/migration/engine.js +0 -147
- package/dist/storage/migration/index.js +0 -33
- package/dist/storage/migration/planner.js +0 -154
- package/dist/storage/migration/productionRegistry.js +0 -3931
- package/dist/storage/migration/registry.js +0 -169
- package/dist/storage/migration/report.js +0 -54
- package/dist/storage/migration/types.js +0 -31
- package/dist/storage/upgrade/homeClassification.js +0 -353
- package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
- package/dist/storage/upgrade/migrationReceipt.js +0 -67
- package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
- package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
- package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
- package/dist/storage/upgrade/recordVersionScan.js +0 -176
- package/dist/storage/upgrade/sqliteFileSet.js +0 -50
- package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
- package/dist/storage/upgrade/switchProgress.js +0 -80
- package/dist/storage/upgrade/upgradeReceipt.js +0 -161
- package/dist/storage/upgradeCoordination.js +0 -187
- package/dist/storage/upgradeFence.js +0 -366
- package/dist/telemetry/telemetryCompaction.js +0 -252
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime,
|
|
4
|
-
import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation,
|
|
5
|
-
import { hasRecentTurnId } from "../
|
|
3
|
+
import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, createRoleSessionSet, recordRoleAgentSession, replaceTaskRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, detachRoleAgentSessionHost, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
4
|
+
import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, managedProviderTurnId, clearProviderGoal, providerGoalContinues, settleProviderTurnSubmission, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
|
|
5
|
+
import { hasRecentTurnId } from "../runtime/recentTurnIds.js";
|
|
6
6
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
7
7
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
8
8
|
import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
|
|
@@ -11,37 +11,38 @@ import { answerInputRequest } from "../input/inputRequest.js";
|
|
|
11
11
|
import { activeRoleAgentBinding } from "../role/role.js";
|
|
12
12
|
import { effectiveLaunchWithTaskMainWorkspace, effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
13
13
|
import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { appendTurnInput, createTurn } from "../turn/turn.js";
|
|
15
|
+
import { createTurnInput } from "../context/turnInputContract.js";
|
|
16
|
+
import { parseReviewResultReport } from "../review/reviewRound.js";
|
|
16
17
|
import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
|
|
17
|
-
import {
|
|
18
|
+
import { terminalizeExactTaskTurn } from "../lifecycle/exactTurnTerminalization.js";
|
|
18
19
|
import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
|
|
19
20
|
import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
|
|
20
21
|
import { recordLeaderAttentionRequired, routeRoleEvent } from "../scheduler/operatorEvent.js";
|
|
21
22
|
import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
|
|
22
23
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
23
24
|
import { wakeReason } from "../scheduler/wakeReason.js";
|
|
24
|
-
import {
|
|
25
|
+
import { foldTurnProgressFacts, latestTurnDurableProgressAt, latestTurnEventTime, latestStallEvidenceKey, isRoleTurnStalled, TURN_PROGRESS_EVENT, TURN_DIAGNOSTIC_FINISHED_EVENT, TURN_RECOVERED_EVENT, TURN_STALLED_EVENT } from "../scheduler/roleTurnStall.js";
|
|
26
|
+
import { pendingWakeupProjection } from "../storage/taskStore.js";
|
|
25
27
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
26
28
|
import { providerContinuationKey } from "../runtime/providerContinuation.js";
|
|
27
|
-
import {
|
|
28
|
-
import { bindExecution, claimPending,
|
|
29
|
+
import { formatTurnReceiptId } from "../task/taskRecordReference.js";
|
|
30
|
+
import { bindExecution, claimPending, completeProcessing, consumePendingBatch, mailboxHasPending, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
|
|
29
31
|
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
30
32
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LAUNCH_RESERVED_REASON, RUNTIME_LIFECYCLE_OWNER, RuntimeLifecycleBusyError, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, isRuntimeLaunchReservation, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
31
33
|
import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
|
|
32
34
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
33
35
|
import { standardAgentError } from "../runtime/agentError.js";
|
|
34
|
-
import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, isRuntimeTokenEvidence, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent,
|
|
35
|
-
import { projectRuntimeTaskEvents } from "../runtime/runtimeProjection.js";
|
|
36
|
+
import { RUNTIME_OBSERVATION_TASK_EVENT, createRuntimeObservation, isRuntimeTokenEvidence, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent, runtimeObservationTurnFenceMatches, runtimeObservationTaskEventPayload } from "../runtime/runtimeObservation.js";
|
|
36
37
|
import { contextSnapshotRef } from "../context/contextSnapshot.js";
|
|
37
|
-
import { contextSnapshotDeltaRefIds,
|
|
38
|
+
import { contextSnapshotDeltaRefIds, freezeTurnContextSnapshot } from "../context/turnContextPack.js";
|
|
38
39
|
export class AgentHostProviderTurnFenceError extends Error {
|
|
39
40
|
constructor(message) {
|
|
40
41
|
super(message);
|
|
41
42
|
this.name = "AgentHostProviderTurnFenceError";
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
/** Maps
|
|
45
|
+
/** Maps authoritative TaskStore records to the scheduler's narrow port. */
|
|
45
46
|
export class FileSchedulerStoreAdapter {
|
|
46
47
|
store;
|
|
47
48
|
telemetry;
|
|
@@ -54,7 +55,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
54
55
|
}
|
|
55
56
|
freezeLeaderContextSnapshot(taskId, roleName, now) {
|
|
56
57
|
return this.store.transaction((tx) => {
|
|
57
|
-
const snapshot =
|
|
58
|
+
const snapshot = freezeTurnContextSnapshot(tx, {
|
|
58
59
|
taskId,
|
|
59
60
|
roleName,
|
|
60
61
|
purpose: "execution"
|
|
@@ -68,7 +69,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
68
69
|
/**
|
|
69
70
|
* Sole provider-independent ingress for structured runtime state. Driver
|
|
70
71
|
* mapping has already happened before this boundary; this method validates
|
|
71
|
-
* the exact durable
|
|
72
|
+
* the exact durable Turn/session fence, applies the small workflow-relevant
|
|
72
73
|
* transitions, and retains the canonical observation for status projection.
|
|
73
74
|
*/
|
|
74
75
|
observeRuntimeObservation(raw, now = new Date()) {
|
|
@@ -81,7 +82,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
81
82
|
}
|
|
82
83
|
const adapterId = this.adapterForRuntimeObservation(input);
|
|
83
84
|
if (adapterId === null) {
|
|
84
|
-
return this.recordObsoleteCanonicalObservation(input, "driver-or-
|
|
85
|
+
return this.recordObsoleteCanonicalObservation(input, "driver-or-turn-mismatch", now);
|
|
85
86
|
}
|
|
86
87
|
if (hasPersistedRuntimeObservation(this.store.listEvents(taskId), input))
|
|
87
88
|
return "applied";
|
|
@@ -103,26 +104,19 @@ export class FileSchedulerStoreAdapter {
|
|
|
103
104
|
adapterId,
|
|
104
105
|
launchId: input.fence.launchId,
|
|
105
106
|
nativeSessionId: input.fence.nativeSessionId,
|
|
106
|
-
|
|
107
|
+
nativeTurnId: input.fence.nativeTurnId,
|
|
107
108
|
attemptId: input.fence.receiptId,
|
|
108
|
-
|
|
109
|
+
turnId: input.fence.turnId
|
|
109
110
|
});
|
|
110
111
|
if (classification !== "apply")
|
|
111
112
|
return classification;
|
|
112
|
-
if (this.runtimeTurnHasActiveNativeSubagents(input)) {
|
|
113
|
-
// This is an intermediate provider Turn boundary, not the end of the
|
|
114
|
-
// durable Yui Run. The provider will deliver child completion
|
|
115
|
-
// notifications as later native Turns in the same Run generation.
|
|
116
|
-
outcome = this.validateCanonicalRunObservation(input, now);
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
113
|
outcome = this.foldProviderTurnBoundary(input, adapterId, "completed", input.payload.summary ?? "Agent turn completed without a workflow outcome.", now);
|
|
120
114
|
break;
|
|
121
115
|
}
|
|
122
116
|
case "turn.failed": {
|
|
123
117
|
const failureEvidence = input.payload.failure;
|
|
124
|
-
// A Provider Turn is an activation boundary, not a
|
|
125
|
-
// Keep the
|
|
118
|
+
// A Provider Turn is an activation boundary, not a Task outcome.
|
|
119
|
+
// Keep the Turn and Session identity available for Agent-directed
|
|
126
120
|
// restore + submit, even when a provider marks its Turn terminal.
|
|
127
121
|
outcome = this.foldProviderTurnBoundary(input, adapterId, "failed", input.payload.summary ?? `Agent Turn failed: ${failureEvidence.error.message}`, now);
|
|
128
122
|
break;
|
|
@@ -138,7 +132,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
138
132
|
case "continuation.reported":
|
|
139
133
|
case "continuation.settled":
|
|
140
134
|
case "input.delivery-unknown":
|
|
141
|
-
outcome = this.
|
|
135
|
+
outcome = this.validateCanonicalTurnObservation(input, now);
|
|
142
136
|
break;
|
|
143
137
|
case "turn.cancelled": {
|
|
144
138
|
outcome = this.foldProviderTurnBoundary(input, adapterId, "cancelled", input.payload.summary ?? "Provider Turn was cancelled.", now);
|
|
@@ -150,6 +144,10 @@ export class FileSchedulerStoreAdapter {
|
|
|
150
144
|
case "activation.failed":
|
|
151
145
|
outcome = this.observeProviderRuntimeIdentity(input, now);
|
|
152
146
|
break;
|
|
147
|
+
case "goal.updated":
|
|
148
|
+
case "goal.cleared":
|
|
149
|
+
outcome = this.observeProviderGoal(input, now);
|
|
150
|
+
break;
|
|
153
151
|
case "session.ended":
|
|
154
152
|
case "session.failed":
|
|
155
153
|
outcome = this.validateCanonicalSessionObservation(input, now);
|
|
@@ -166,10 +164,26 @@ export class FileSchedulerStoreAdapter {
|
|
|
166
164
|
}
|
|
167
165
|
adapterForRuntimeObservation(input) {
|
|
168
166
|
const taskId = input.fence.taskId;
|
|
169
|
-
const
|
|
170
|
-
if (taskId === undefined
|
|
167
|
+
const turnId = input.fence.turnId;
|
|
168
|
+
if (taskId === undefined)
|
|
171
169
|
return null;
|
|
172
|
-
|
|
170
|
+
if (turnId === undefined) {
|
|
171
|
+
const role = this.store.getRole(taskId, input.fence.roleName);
|
|
172
|
+
const sessions = this.store.getTaskRoleSessionSet(taskId, input.fence.roleName);
|
|
173
|
+
const session = sessions?.sessions[input.fence.agentId];
|
|
174
|
+
if (role?.activeAgentId !== input.fence.agentId
|
|
175
|
+
|| session?.adapterId === undefined)
|
|
176
|
+
return null;
|
|
177
|
+
try {
|
|
178
|
+
return this.drivers.requireByAdapterId(session.adapterId).id === input.fence.driverId
|
|
179
|
+
? session.adapterId
|
|
180
|
+
: null;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const run = this.store.getTurn(taskId, turnId);
|
|
173
187
|
if (run === null
|
|
174
188
|
|| run.roleName !== input.fence.roleName
|
|
175
189
|
|| run.effective.agentId !== input.fence.agentId)
|
|
@@ -183,17 +197,17 @@ export class FileSchedulerStoreAdapter {
|
|
|
183
197
|
return null;
|
|
184
198
|
}
|
|
185
199
|
}
|
|
186
|
-
|
|
200
|
+
validateCanonicalTurnObservation(input, now) {
|
|
187
201
|
return this.store.transaction((store) => {
|
|
188
202
|
if (hasPersistedRuntimeObservation(store.listEvents(input.fence.taskId), input)) {
|
|
189
203
|
return "applied";
|
|
190
204
|
}
|
|
191
|
-
const run = store.
|
|
192
|
-
const active = store.
|
|
205
|
+
const run = store.getTurn(input.fence.taskId, input.fence.turnId);
|
|
206
|
+
const active = store.getActiveTurn(input.fence.taskId, input.fence.roleName);
|
|
193
207
|
const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
|
|
194
208
|
const session = sessions?.sessions[input.fence.agentId];
|
|
195
209
|
const knownContinuation = input.kind.startsWith("continuation.")
|
|
196
|
-
&& projectProviderContinuations(store.listEvents(input.fence.taskId)).some((entry) => (entry.
|
|
210
|
+
&& projectProviderContinuations(store.listEvents(input.fence.taskId)).some((entry) => (entry.turnId === input.fence.turnId
|
|
197
211
|
&& entry.identity.providerNamespace === input.fence.driverId
|
|
198
212
|
&& entry.identity.accountScope === input.fence.agentId
|
|
199
213
|
&& entry.identity.conversationId === input.fence.conversationId
|
|
@@ -209,8 +223,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
209
223
|
&& (knownContinuation || (session?.launchId === input.fence.launchId
|
|
210
224
|
&& session.nativeSessionId === input.fence.nativeSessionId
|
|
211
225
|
&& input.fence.sessionGenerationId === input.fence.launchId))
|
|
212
|
-
&&
|
|
213
|
-
&& (run.deliveredAt !== undefined || input.kind === "turn.cancelled");
|
|
226
|
+
&& runtimeReceiptBelongsToTurn(store, input);
|
|
214
227
|
if (!valid) {
|
|
215
228
|
recordCanonicalObservationObsolete(store, input, "runtime-fence-not-current", now);
|
|
216
229
|
return "obsolete";
|
|
@@ -218,14 +231,6 @@ export class FileSchedulerStoreAdapter {
|
|
|
218
231
|
return "applied";
|
|
219
232
|
});
|
|
220
233
|
}
|
|
221
|
-
runtimeTurnHasActiveNativeSubagents(input) {
|
|
222
|
-
const taskId = input.fence.taskId;
|
|
223
|
-
const run = this.store.getAgentRun(taskId, input.fence.runId);
|
|
224
|
-
if (run === null)
|
|
225
|
-
return false;
|
|
226
|
-
const projection = projectRuntimeTaskEvents(input.fence, run.createdAt, this.store.listEvents(taskId));
|
|
227
|
-
return Object.values(projection.operations).some(({ kind }) => kind === "subagent");
|
|
228
|
-
}
|
|
229
234
|
foldProviderTurnBoundary(input, adapterId, providerStatus, summary, now) {
|
|
230
235
|
const completed = {
|
|
231
236
|
taskId: input.fence.taskId,
|
|
@@ -234,9 +239,10 @@ export class FileSchedulerStoreAdapter {
|
|
|
234
239
|
adapterId,
|
|
235
240
|
launchId: input.fence.launchId,
|
|
236
241
|
nativeSessionId: input.fence.nativeSessionId,
|
|
237
|
-
|
|
242
|
+
nativeTurnId: input.fence.nativeTurnId,
|
|
238
243
|
attemptId: input.fence.receiptId,
|
|
239
|
-
|
|
244
|
+
turnId: input.fence.turnId,
|
|
245
|
+
...(input.payload.input === undefined ? {} : { input: input.payload.input }),
|
|
240
246
|
summary,
|
|
241
247
|
providerStatus
|
|
242
248
|
};
|
|
@@ -248,7 +254,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
248
254
|
}
|
|
249
255
|
validateCanonicalSessionObservation(input, now) {
|
|
250
256
|
return this.store.transaction((store) => {
|
|
251
|
-
const run = store.
|
|
257
|
+
const run = store.getTurn(input.fence.taskId, input.fence.turnId);
|
|
252
258
|
const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
|
|
253
259
|
const session = sessions?.sessions[input.fence.agentId];
|
|
254
260
|
if (run === null
|
|
@@ -273,7 +279,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
273
279
|
}
|
|
274
280
|
store.saveTaskRoleSessionSet(updatedSessions);
|
|
275
281
|
for (const continuation of projectProviderContinuations(store.listEvents(input.fence.taskId))) {
|
|
276
|
-
if (continuation.
|
|
282
|
+
if (continuation.turnId !== input.fence.turnId
|
|
277
283
|
|| continuation.identity.conversationId
|
|
278
284
|
!== (input.fence.conversationId ?? input.fence.nativeSessionId)
|
|
279
285
|
|| continuation.identity.activationId
|
|
@@ -322,29 +328,26 @@ export class FileSchedulerStoreAdapter {
|
|
|
322
328
|
{ type: "event", taskId: input.fence.taskId, id: eventId }
|
|
323
329
|
], {
|
|
324
330
|
source: input.fence.driverId,
|
|
325
|
-
dedupeKey: detached.semanticKey
|
|
326
|
-
deliveryMode: "followup",
|
|
327
|
-
lane: "normal"
|
|
331
|
+
dedupeKey: detached.semanticKey
|
|
328
332
|
});
|
|
329
333
|
}
|
|
330
|
-
const active = store.
|
|
334
|
+
const active = store.getActiveTurn(input.fence.taskId, input.fence.roleName);
|
|
331
335
|
const role = store.getRole(input.fence.taskId, input.fence.roleName);
|
|
332
336
|
if ((input.kind === "session.ended" || input.kind === "session.failed")
|
|
333
337
|
&& active !== null
|
|
334
|
-
&& active.id === input.fence.
|
|
338
|
+
&& active.id === input.fence.turnId
|
|
335
339
|
&& active.status === "active"
|
|
336
|
-
&& active.deliveredAt !== undefined
|
|
337
340
|
&& role !== null) {
|
|
338
341
|
if (role.name === "leader") {
|
|
339
342
|
const message = [
|
|
340
|
-
`Leader native Session became unavailable while
|
|
341
|
-
"Yui preserved the
|
|
342
|
-
"Retire the disposable
|
|
343
|
+
`Leader native Session became unavailable while Turn ${active.id} remains active.`,
|
|
344
|
+
"Yui preserved the Turn because Session/host termination is not a Task outcome.",
|
|
345
|
+
"Retire the disposable Turn, or use Task execution stop/start if the current runtime cannot be settled normally."
|
|
343
346
|
].join(" ");
|
|
344
347
|
recordLeaderAttentionRequired(store, {
|
|
345
348
|
taskId: input.fence.taskId,
|
|
346
349
|
reason: "leader-runtime-detached",
|
|
347
|
-
payload: { message,
|
|
350
|
+
payload: { message, turnId: active.id },
|
|
348
351
|
now
|
|
349
352
|
});
|
|
350
353
|
}
|
|
@@ -354,7 +357,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
354
357
|
taskId: input.fence.taskId,
|
|
355
358
|
roleName: "leader"
|
|
356
359
|
}, "role-runtime-detached", now, [
|
|
357
|
-
{ type: "
|
|
360
|
+
{ type: "turn", taskId: input.fence.taskId, id: active.id }
|
|
358
361
|
]);
|
|
359
362
|
}
|
|
360
363
|
}
|
|
@@ -379,7 +382,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
379
382
|
&& input.payload.snapshotComplete === true
|
|
380
383
|
&& input.payload.observationQuality === "exact") {
|
|
381
384
|
for (const continuation of projectProviderContinuations(events)) {
|
|
382
|
-
if (continuation.
|
|
385
|
+
if (continuation.turnId !== input.fence.turnId
|
|
383
386
|
|| continuation.identity.conversationId !== input.fence.conversationId
|
|
384
387
|
|| continuation.identity.activationId !== input.fence.activationId
|
|
385
388
|
|| continuation.execution === "quiescent")
|
|
@@ -419,9 +422,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
419
422
|
store.saveEvent(taskId, createTaskEvent(settledEventId, taskId, RUNTIME_OBSERVATION_TASK_EVENT, runtimeObservationTaskEventPayload(settled), now));
|
|
420
423
|
enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, "provider-continuation-settled", now, [{ type: "event", taskId, id: settledEventId }], {
|
|
421
424
|
source: input.fence.driverId,
|
|
422
|
-
dedupeKey: settled.semanticKey
|
|
423
|
-
deliveryMode: "followup",
|
|
424
|
-
lane: "normal"
|
|
425
|
+
dedupeKey: settled.semanticKey
|
|
425
426
|
});
|
|
426
427
|
}
|
|
427
428
|
}
|
|
@@ -431,21 +432,26 @@ export class FileSchedulerStoreAdapter {
|
|
|
431
432
|
? "provider-continuation-report"
|
|
432
433
|
: "provider-continuation-settled", now, [{ type: "event", taskId, id: observationEventId }], {
|
|
433
434
|
source: input.fence.driverId,
|
|
434
|
-
dedupeKey: input.semanticKey
|
|
435
|
-
deliveryMode: "followup",
|
|
436
|
-
lane: "normal"
|
|
435
|
+
dedupeKey: input.semanticKey
|
|
437
436
|
});
|
|
438
437
|
}
|
|
438
|
+
if ((input.kind === "goal.cleared"
|
|
439
|
+
|| (input.kind === "goal.updated" && input.payload.goalStatus !== "active"))
|
|
440
|
+
&& input.fence.roleName !== "leader") {
|
|
441
|
+
routeRoleEvent(store, observationEvent, input.fence.roleName, input.kind === "goal.cleared"
|
|
442
|
+
? "provider-goal-cleared"
|
|
443
|
+
: `provider-goal-${input.payload.goalStatus}`, now);
|
|
444
|
+
}
|
|
439
445
|
if (input.kind === "turn.failed" && input.payload.failure !== undefined) {
|
|
440
446
|
const failure = input.payload.failure;
|
|
441
447
|
const error = failure.error;
|
|
442
|
-
const run = input.fence.
|
|
448
|
+
const run = input.fence.turnId === undefined
|
|
443
449
|
? null
|
|
444
|
-
: store.
|
|
450
|
+
: store.getTurn(taskId, input.fence.turnId);
|
|
445
451
|
const errorEvent = createTaskEvent(store.nextEventId(taskId), taskId, "runtime.agent-error", {
|
|
446
452
|
sourceEventId: input.eventId,
|
|
447
453
|
observationEventId,
|
|
448
|
-
|
|
454
|
+
turnId: input.fence.turnId ?? "",
|
|
449
455
|
roleName: input.fence.roleName,
|
|
450
456
|
agentId: input.fence.agentId,
|
|
451
457
|
adapterId: run?.effective.adapterId ?? "unknown",
|
|
@@ -469,17 +475,13 @@ export class FileSchedulerStoreAdapter {
|
|
|
469
475
|
store.saveEvent(taskId, errorEvent);
|
|
470
476
|
enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, wakeReason("agent-error", errorEvent.id), now, [{ type: "event", taskId, id: errorEvent.id }], {
|
|
471
477
|
source: input.fence.driverId,
|
|
472
|
-
dedupeKey: `agent-error:${taskId}:${input.eventId}
|
|
473
|
-
deliveryMode: "followup",
|
|
474
|
-
lane: "normal"
|
|
478
|
+
dedupeKey: `agent-error:${taskId}:${input.eventId}`
|
|
475
479
|
});
|
|
476
480
|
if (input.fence.roleName === "leader"
|
|
477
481
|
&& error.sessionDisposition === "unrecoverable") {
|
|
478
482
|
enqueueWork(store, { kind: "operator" }, "leader-session-unrecoverable", now, [{ type: "event", taskId, id: errorEvent.id }], {
|
|
479
483
|
source: input.fence.driverId,
|
|
480
|
-
dedupeKey: `leader-session-unrecoverable:${taskId}:${input.eventId}
|
|
481
|
-
deliveryMode: "followup",
|
|
482
|
-
lane: "normal"
|
|
484
|
+
dedupeKey: `leader-session-unrecoverable:${taskId}:${input.eventId}`
|
|
483
485
|
});
|
|
484
486
|
}
|
|
485
487
|
}
|
|
@@ -495,14 +497,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
495
497
|
}
|
|
496
498
|
}
|
|
497
499
|
});
|
|
498
|
-
if (this.telemetry !== null && input.fence.
|
|
500
|
+
if (this.telemetry !== null && input.fence.turnId !== undefined) {
|
|
499
501
|
try {
|
|
500
502
|
const entry = runtimeObservationTelemetryEntry(input);
|
|
501
503
|
this.telemetry.sink.observe(entry);
|
|
502
|
-
const run = this.store.
|
|
504
|
+
const run = this.store.getTurn(entry.taskId, input.fence.turnId);
|
|
503
505
|
if (run !== null && run.status !== "active") {
|
|
504
506
|
void this.telemetry.retention.flush().then(() => {
|
|
505
|
-
this.telemetry?.retention.pruneGeneration(entry.taskId, entry.roleName, entry.
|
|
507
|
+
this.telemetry?.retention.pruneGeneration(entry.taskId, entry.roleName, entry.turnId, entry.generation);
|
|
506
508
|
}).catch(() => undefined);
|
|
507
509
|
}
|
|
508
510
|
}
|
|
@@ -539,8 +541,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
539
541
|
const events = this.store.listEvents(taskId);
|
|
540
542
|
task = {
|
|
541
543
|
events,
|
|
542
|
-
|
|
543
|
-
|
|
544
|
+
turnFacts: foldTurnProgressFacts(events),
|
|
545
|
+
turns: this.store.listTurns(taskId),
|
|
544
546
|
workItems: this.store.listWorkItems(taskId),
|
|
545
547
|
reviewRounds: this.store.listReviewRounds(taskId),
|
|
546
548
|
changeSets: this.store.listChangeSets(taskId),
|
|
@@ -576,7 +578,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
576
578
|
const latest = latestTaskWake(reader.listTaskWakes(taskId));
|
|
577
579
|
const fromCursor = latest?.toCursor ?? fallbackWakeCursor({
|
|
578
580
|
taskCreatedAt: task.createdAt,
|
|
579
|
-
|
|
581
|
+
leaderTurnCreatedAt: operationalTaskRecords(reader.listTurns(taskId), reader.listEvents(taskId), "turn")
|
|
580
582
|
.filter((run) => run.roleName === "leader")
|
|
581
583
|
.at(-1)?.createdAt
|
|
582
584
|
});
|
|
@@ -595,8 +597,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
595
597
|
const role = this.store.getRole(taskId, roleName);
|
|
596
598
|
return role === null ? null : mapRole(this.store, role);
|
|
597
599
|
}
|
|
598
|
-
|
|
599
|
-
return this.store.
|
|
600
|
+
getActiveTurn(taskId, roleName) {
|
|
601
|
+
return this.store.getActiveTurn(taskId, roleName);
|
|
600
602
|
}
|
|
601
603
|
hasOpenInputRequest(taskId) {
|
|
602
604
|
return this.store.listOpenInputRequests([taskId]).length > 0;
|
|
@@ -660,8 +662,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
660
662
|
listEvents(taskId) {
|
|
661
663
|
return this.#taskReadProjection(taskId).events;
|
|
662
664
|
}
|
|
663
|
-
|
|
664
|
-
return this.#taskReadProjection(taskId).
|
|
665
|
+
listTurns(taskId) {
|
|
666
|
+
return this.#taskReadProjection(taskId).turns;
|
|
665
667
|
}
|
|
666
668
|
listWorkItems(taskId) {
|
|
667
669
|
return this.#taskReadProjection(taskId).workItems;
|
|
@@ -681,17 +683,17 @@ export class FileSchedulerStoreAdapter {
|
|
|
681
683
|
listMessages(taskId) {
|
|
682
684
|
return this.#taskReadProjection(taskId).messages;
|
|
683
685
|
}
|
|
684
|
-
|
|
685
|
-
return this.#taskReadProjection(taskId).
|
|
686
|
+
getTurnProgressFacts(taskId, turnId) {
|
|
687
|
+
return this.#taskReadProjection(taskId).turnFacts.get(turnId);
|
|
686
688
|
}
|
|
687
|
-
|
|
689
|
+
getTurnDurableProgress(taskId, roleName, turnId) {
|
|
688
690
|
const projected = this.#taskReadProjection(taskId);
|
|
689
691
|
// Serve the related-record fold from the same revision's projection: the
|
|
690
692
|
// event history and the WorkItem/Review/ChangeSet/Integration/Input lists
|
|
691
|
-
// are read once per Task per revision, and the per-
|
|
693
|
+
// are read once per Task per revision, and the per-Turn checkpoint/activity
|
|
692
694
|
// facts come from the one-pass fold instead of per-candidate scans.
|
|
693
695
|
const view = {
|
|
694
|
-
|
|
696
|
+
getTurn: (id, turnId) => this.store.getTurn(id, turnId),
|
|
695
697
|
listEvents: () => projected.events,
|
|
696
698
|
getWorkItem: (id, workItemId) => this.store.getWorkItem(id, workItemId),
|
|
697
699
|
listReviewRounds: () => projected.reviewRounds,
|
|
@@ -700,24 +702,24 @@ export class FileSchedulerStoreAdapter {
|
|
|
700
702
|
listInputRequests: () => projected.inputRequests
|
|
701
703
|
};
|
|
702
704
|
// A missing fold entry is an authoritative empty fold, not a signal to
|
|
703
|
-
// re-scan the whole history. Pass {} so
|
|
705
|
+
// re-scan the whole history. Pass {} so latestTurnDurableProgressAt treats
|
|
704
706
|
// the fold as present and skips the per-candidate fallback scans.
|
|
705
|
-
return
|
|
707
|
+
return latestTurnDurableProgressAt(view, taskId, roleName, turnId, projected.turnFacts.get(turnId) ?? {});
|
|
706
708
|
}
|
|
707
|
-
|
|
709
|
+
recordRoleTurnDiagnostic(input) {
|
|
708
710
|
return this.store.transaction((store) => {
|
|
709
711
|
const task = store.getTask(input.taskId);
|
|
710
|
-
const run = store.
|
|
712
|
+
const run = store.getActiveTurn(input.taskId, input.roleName);
|
|
711
713
|
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled"
|
|
712
|
-
|| run === null || run.id !== input.
|
|
714
|
+
|| run === null || run.id !== input.turnId || run.status !== "active") {
|
|
713
715
|
return "state-changed";
|
|
714
716
|
}
|
|
715
|
-
const latest =
|
|
717
|
+
const latest = latestTurnEventTime(store.listEvents(input.taskId), TURN_DIAGNOSTIC_FINISHED_EVENT, input.turnId);
|
|
716
718
|
if (latest !== undefined && Date.parse(latest) >= Date.parse(input.startedAt)) {
|
|
717
719
|
return "already-recorded";
|
|
718
720
|
}
|
|
719
|
-
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId,
|
|
720
|
-
|
|
721
|
+
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, TURN_DIAGNOSTIC_FINISHED_EVENT, {
|
|
722
|
+
turnId: input.turnId,
|
|
721
723
|
roleName: input.roleName,
|
|
722
724
|
outcome: input.outcome,
|
|
723
725
|
startedAt: input.startedAt
|
|
@@ -725,22 +727,22 @@ export class FileSchedulerStoreAdapter {
|
|
|
725
727
|
return "recorded";
|
|
726
728
|
});
|
|
727
729
|
}
|
|
728
|
-
|
|
730
|
+
recordRoleTurnStall(input) {
|
|
729
731
|
return this.store.transaction((store) => {
|
|
730
732
|
const task = store.getTask(input.taskId);
|
|
731
733
|
const role = store.getRole(input.taskId, input.roleName);
|
|
732
|
-
const run = store.
|
|
734
|
+
const run = store.getActiveTurn(input.taskId, input.roleName);
|
|
733
735
|
if (task === null
|
|
734
736
|
|| task.status !== "active"
|
|
735
737
|
|| task.executionGate.state !== "enabled"
|
|
736
738
|
|| role === null
|
|
737
739
|
|| run === null
|
|
738
|
-
|| run.id !== input.
|
|
740
|
+
|| run.id !== input.turnId
|
|
739
741
|
|| run.status !== "active"
|
|
740
742
|
|| run.effective.agentId !== input.agentId
|
|
741
743
|
|| run.effective.adapterId !== input.adapterId)
|
|
742
744
|
return "state-changed";
|
|
743
|
-
const progress =
|
|
745
|
+
const progress = latestTurnDurableProgressAt(store, input.taskId, input.roleName, input.turnId);
|
|
744
746
|
if (progress?.progressAt !== input.progressAt)
|
|
745
747
|
return "state-changed";
|
|
746
748
|
const session = store.getRoleSession(input.taskId, input.roleName);
|
|
@@ -749,8 +751,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
749
751
|
const existing = latestStallEvidenceKey(store.listEvents(task.id), run.id);
|
|
750
752
|
if (existing?.progressAt === input.progressAt)
|
|
751
753
|
return "already-raised";
|
|
752
|
-
const event = createTaskEvent(store.nextEventId(task.id), task.id,
|
|
753
|
-
|
|
754
|
+
const event = createTaskEvent(store.nextEventId(task.id), task.id, TURN_STALLED_EVENT, {
|
|
755
|
+
turnId: run.id,
|
|
754
756
|
roleName: role.name,
|
|
755
757
|
kind: input.kind,
|
|
756
758
|
classification: input.classification,
|
|
@@ -763,15 +765,15 @@ export class FileSchedulerStoreAdapter {
|
|
|
763
765
|
return "raised";
|
|
764
766
|
});
|
|
765
767
|
}
|
|
766
|
-
|
|
768
|
+
recordRoleTurnProgress(input) {
|
|
767
769
|
return this.store.transaction((store) => {
|
|
768
770
|
const task = store.getTask(input.taskId);
|
|
769
|
-
const run = store.
|
|
771
|
+
const run = store.getActiveTurn(input.taskId, input.roleName);
|
|
770
772
|
if (task === null
|
|
771
773
|
|| task.status !== "active"
|
|
772
774
|
|| task.executionGate.state !== "enabled"
|
|
773
775
|
|| run === null
|
|
774
|
-
|| run.id !== input.
|
|
776
|
+
|| run.id !== input.turnId
|
|
775
777
|
|| run.status !== "active")
|
|
776
778
|
return "state-changed";
|
|
777
779
|
const events = store.listEvents(task.id);
|
|
@@ -782,18 +784,18 @@ export class FileSchedulerStoreAdapter {
|
|
|
782
784
|
&& Date.parse(input.progressAt) <= Date.parse(previousStall.progressAt)) {
|
|
783
785
|
return "already-recorded";
|
|
784
786
|
}
|
|
785
|
-
const existing = events.some((event) => (event.type ===
|
|
786
|
-
&& event.payload.
|
|
787
|
+
const existing = events.some((event) => (event.type === TURN_PROGRESS_EVENT
|
|
788
|
+
&& event.payload.turnId === run.id
|
|
787
789
|
&& (event.payload.progressAt === input.progressAt
|
|
788
790
|
|| (typeof event.payload.progressAt === "string"
|
|
789
791
|
&& Number.isFinite(Date.parse(event.payload.progressAt))
|
|
790
792
|
&& Date.parse(event.payload.progressAt) >= Date.parse(input.progressAt)))));
|
|
791
793
|
// Advisory 30-minute diagnostics are intentionally not lifecycle
|
|
792
|
-
// episodes and therefore must never synthesize
|
|
793
|
-
const recovered =
|
|
794
|
+
// episodes and therefore must never synthesize turn.recovered.
|
|
795
|
+
const recovered = isRoleTurnStalled(events, run.id);
|
|
794
796
|
if (!existing) {
|
|
795
|
-
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id,
|
|
796
|
-
|
|
797
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, TURN_PROGRESS_EVENT, {
|
|
798
|
+
turnId: run.id,
|
|
797
799
|
roleName: input.roleName,
|
|
798
800
|
kind: "durable-fold",
|
|
799
801
|
progressAt: input.progressAt,
|
|
@@ -801,8 +803,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
801
803
|
}, input.now));
|
|
802
804
|
}
|
|
803
805
|
if (recovered) {
|
|
804
|
-
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id,
|
|
805
|
-
|
|
806
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, TURN_RECOVERED_EVENT, {
|
|
807
|
+
turnId: run.id,
|
|
806
808
|
roleName: input.roleName,
|
|
807
809
|
progressAt: input.progressAt,
|
|
808
810
|
kind: "durable-progress"
|
|
@@ -816,34 +818,32 @@ export class FileSchedulerStoreAdapter {
|
|
|
816
818
|
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
817
819
|
const session = sessions?.sessions[input.agentId];
|
|
818
820
|
const binding = sessions?.providerBinding;
|
|
819
|
-
const
|
|
820
|
-
kind: "role",
|
|
821
|
-
taskId: input.taskId,
|
|
822
|
-
roleName: input.roleName
|
|
823
|
-
});
|
|
821
|
+
const active = store.getActiveTurn(input.taskId, input.roleName);
|
|
824
822
|
if (sessions === null || sessions === undefined
|
|
825
823
|
|| binding === null || binding === undefined
|
|
826
|
-
||
|
|
824
|
+
|| (input.turnId !== undefined
|
|
825
|
+
&& (active?.id !== input.turnId || active.effective.agentId !== input.agentId))
|
|
827
826
|
|| session?.launchId !== input.launchId
|
|
828
827
|
|| session.nativeSessionId !== input.nativeSessionId
|
|
829
828
|
|| currentProviderConversation(binding).conversationId !== input.nativeSessionId
|
|
830
829
|
|| binding.authority.owner !== input.authorityOwner
|
|
831
830
|
|| binding.authority.epoch !== input.authorityEpoch
|
|
832
|
-
|| binding.authority.holderId !== input.holderId
|
|
833
|
-
|| !processingOwnsExactInitialDelivery(mailbox, input.taskId, input.runId, input.attemptId)) {
|
|
831
|
+
|| binding.authority.holderId !== input.holderId) {
|
|
834
832
|
throw new AgentHostProviderTurnFenceError("Agent Host Provider Turn carries a stale durable writer fence. Release and reacquire Provider authority before retrying input.");
|
|
835
833
|
}
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
&&
|
|
839
|
-
&&
|
|
834
|
+
const currentTurn = binding.turn;
|
|
835
|
+
const exactReplay = currentTurn !== null
|
|
836
|
+
&& currentTurn.turnId === input.turnId
|
|
837
|
+
&& currentTurn.attemptId === input.attemptId
|
|
838
|
+
&& currentTurn.authorityEpoch === input.authorityEpoch
|
|
839
|
+
&& currentTurn.status === "submitting";
|
|
840
840
|
if (!exactReplay && binding.turn !== null
|
|
841
|
-
&& ["submitting", "accepted", "
|
|
841
|
+
&& ["submitting", "accepted", "delivery-unknown"]
|
|
842
842
|
.includes(binding.turn.status)) {
|
|
843
843
|
throw new AgentHostProviderTurnFenceError("Provider Conversation already has an unsettled Turn.");
|
|
844
844
|
}
|
|
845
845
|
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
|
|
846
|
-
|
|
846
|
+
...(input.turnId === undefined ? {} : { turnId: input.turnId }),
|
|
847
847
|
attemptId: input.attemptId,
|
|
848
848
|
authorityEpoch: input.authorityEpoch,
|
|
849
849
|
submittedAt: input.now.toISOString()
|
|
@@ -856,7 +856,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
856
856
|
const binding = sessions?.providerBinding;
|
|
857
857
|
if (sessions === null || sessions === undefined
|
|
858
858
|
|| binding === null || binding === undefined
|
|
859
|
-
|| binding.turn?.
|
|
859
|
+
|| binding.turn?.turnId !== input.turnId
|
|
860
860
|
|| binding.turn?.attemptId !== input.attemptId) {
|
|
861
861
|
throw new Error("Agent Host Provider Turn submission is no longer current.");
|
|
862
862
|
}
|
|
@@ -867,7 +867,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
867
867
|
resolvedAt: input.now.toISOString()
|
|
868
868
|
});
|
|
869
869
|
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, input.now));
|
|
870
|
-
|
|
870
|
+
if (input.turnId === undefined)
|
|
871
|
+
return;
|
|
872
|
+
const run = store.getTurn(input.taskId, input.turnId);
|
|
871
873
|
const session = sessions.sessions[sessions.activeAgentId];
|
|
872
874
|
const driver = run === null
|
|
873
875
|
? null
|
|
@@ -885,7 +887,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
885
887
|
});
|
|
886
888
|
const errorEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.agent-error", {
|
|
887
889
|
sourceEventId: input.attemptId,
|
|
888
|
-
|
|
890
|
+
turnId: input.turnId,
|
|
889
891
|
roleName: input.roleName,
|
|
890
892
|
agentId: run?.effective.agentId ?? sessions.activeAgentId,
|
|
891
893
|
adapterId: run?.effective.adapterId ?? session?.adapterId ?? "unknown",
|
|
@@ -910,9 +912,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
910
912
|
? "leader-turn-submission-error"
|
|
911
913
|
: wakeReason("agent-error", errorEvent.id), input.now, [{ type: "event", taskId: input.taskId, id: errorEvent.id }], {
|
|
912
914
|
source: driver?.id ?? "agent-host",
|
|
913
|
-
dedupeKey: `agent-error:${input.taskId}:${input.attemptId}
|
|
914
|
-
deliveryMode: "followup",
|
|
915
|
-
lane: "normal"
|
|
915
|
+
dedupeKey: `agent-error:${input.taskId}:${input.attemptId}`
|
|
916
916
|
});
|
|
917
917
|
});
|
|
918
918
|
}
|
|
@@ -934,8 +934,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
934
934
|
...binding.authority
|
|
935
935
|
};
|
|
936
936
|
}
|
|
937
|
-
|
|
938
|
-
return this.store.
|
|
937
|
+
peekNextTurnId(taskId) {
|
|
938
|
+
return this.store.peekNextTurnId(taskId);
|
|
939
939
|
}
|
|
940
940
|
getWorkMailbox(target) { return this.store.getWorkMailbox(target); }
|
|
941
941
|
listWorkMailboxes() { return this.store.listWorkMailboxes(); }
|
|
@@ -948,17 +948,23 @@ export class FileSchedulerStoreAdapter {
|
|
|
948
948
|
});
|
|
949
949
|
}
|
|
950
950
|
enqueueLeaderWakeup(taskId, reason, now) {
|
|
951
|
-
return this.store.transaction((store) =>
|
|
951
|
+
return this.store.transaction((store) => {
|
|
952
|
+
const mailbox = enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, reason, now);
|
|
953
|
+
const wakeup = pendingWakeupProjection(mailbox);
|
|
954
|
+
if (wakeup === null)
|
|
955
|
+
throw new Error(`Leader wakeup was not persisted: ${taskId}.`);
|
|
956
|
+
return wakeup;
|
|
957
|
+
});
|
|
952
958
|
}
|
|
953
959
|
recordAgentError(input, now) {
|
|
954
960
|
return this.store.transaction((store) => {
|
|
955
961
|
const duplicate = [...store.listEvents(input.taskId)].reverse().find((event) => (event.type === "runtime.agent-error"
|
|
956
|
-
&& event.payload.
|
|
962
|
+
&& event.payload.turnId === input.turnId
|
|
957
963
|
&& event.payload.phase === input.phase
|
|
958
964
|
&& event.payload.raw === input.raw));
|
|
959
965
|
if (duplicate !== undefined)
|
|
960
966
|
return duplicate.id;
|
|
961
|
-
const run = store.
|
|
967
|
+
const run = store.getTurn(input.taskId, input.turnId);
|
|
962
968
|
const sessionSet = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
963
969
|
const sessionAgentId = run?.effective.agentId ?? sessionSet?.activeAgentId;
|
|
964
970
|
const session = sessionAgentId === undefined
|
|
@@ -984,8 +990,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
984
990
|
: { sessionDisposition: input.sessionDisposition })
|
|
985
991
|
});
|
|
986
992
|
const event = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.agent-error", {
|
|
987
|
-
sourceEventId: `${input.
|
|
988
|
-
|
|
993
|
+
sourceEventId: `${input.turnId}:${input.phase}`,
|
|
994
|
+
turnId: input.turnId,
|
|
989
995
|
roleName: input.roleName,
|
|
990
996
|
agentId: run?.effective.agentId ?? session?.agentId ?? "unknown",
|
|
991
997
|
adapterId: run?.effective.adapterId ?? session?.adapterId ?? "unknown",
|
|
@@ -1008,17 +1014,13 @@ export class FileSchedulerStoreAdapter {
|
|
|
1008
1014
|
if (!leaderCannotReceive) {
|
|
1009
1015
|
enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, wakeReason("agent-error", event.id), now, [{ type: "event", taskId: input.taskId, id: event.id }], {
|
|
1010
1016
|
source: driver?.id ?? input.source,
|
|
1011
|
-
dedupeKey: `agent-error:${input.taskId}:${input.
|
|
1012
|
-
deliveryMode: "followup",
|
|
1013
|
-
lane: "normal"
|
|
1017
|
+
dedupeKey: `agent-error:${input.taskId}:${input.turnId}:${input.phase}:${event.id}`
|
|
1014
1018
|
});
|
|
1015
1019
|
}
|
|
1016
1020
|
if (leaderCannotReceive) {
|
|
1017
1021
|
enqueueWork(store, { kind: "operator" }, "leader-agent-error", now, [{ type: "event", taskId: input.taskId, id: event.id }], {
|
|
1018
1022
|
source: driver?.id ?? input.source,
|
|
1019
|
-
dedupeKey: `leader-agent-error:${input.taskId}:${input.
|
|
1020
|
-
deliveryMode: "followup",
|
|
1021
|
-
lane: "normal"
|
|
1023
|
+
dedupeKey: `leader-agent-error:${input.taskId}:${input.turnId}:${event.id}`
|
|
1022
1024
|
});
|
|
1023
1025
|
}
|
|
1024
1026
|
return event.id;
|
|
@@ -1028,7 +1030,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1028
1030
|
* Records that a Leader wake was suppressed by scheduler single-flight
|
|
1029
1031
|
* (the Role runtime lifecycle lane was busy). The wake stays durable and
|
|
1030
1032
|
* is retried after the lane settles; this event is the audit trail that
|
|
1031
|
-
* separates scheduler backpressure from real
|
|
1033
|
+
* separates scheduler backpressure from real Turn failures.
|
|
1032
1034
|
*/
|
|
1033
1035
|
recordWakeSuppression(taskId, reason, now) {
|
|
1034
1036
|
this.store.transaction((store) => {
|
|
@@ -1048,7 +1050,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1048
1050
|
*
|
|
1049
1051
|
* f6: The wakeup targets the Leader role mailbox (not the Task mailbox)
|
|
1050
1052
|
* and also queues a pending wakeup so processLeaderWakeups dispatches
|
|
1051
|
-
* the Leader
|
|
1053
|
+
* the Leader Turn with the exact job-finished reason.
|
|
1052
1054
|
*/
|
|
1053
1055
|
transitionDurableJob(taskId, jobId, transition, now, wakeup) {
|
|
1054
1056
|
return this.store.transaction((store) => {
|
|
@@ -1096,128 +1098,15 @@ export class FileSchedulerStoreAdapter {
|
|
|
1096
1098
|
return { status: "claimed", processing: claimed.processing };
|
|
1097
1099
|
});
|
|
1098
1100
|
}
|
|
1099
|
-
|
|
1100
|
-
return this.store.transaction((store) => {
|
|
1101
|
-
const mailbox = store.getWorkMailbox(input.target);
|
|
1102
|
-
if (mailbox === null)
|
|
1103
|
-
return { status: "empty" };
|
|
1104
|
-
if (mailbox.inputDelivery !== null) {
|
|
1105
|
-
return { status: "delivery", delivery: mailbox.inputDelivery };
|
|
1106
|
-
}
|
|
1107
|
-
const pending = pendingLane(mailbox, input.lane);
|
|
1108
|
-
if (pending === null)
|
|
1109
|
-
return { status: "empty" };
|
|
1110
|
-
if (input.target.kind === "role"
|
|
1111
|
-
&& input.executionRef.type === "run"
|
|
1112
|
-
&& input.executionRef.taskId === input.target.taskId) {
|
|
1113
|
-
const sessions = store.getTaskRoleSessionSet(input.target.taskId, input.target.roleName);
|
|
1114
|
-
const binding = sessions?.providerBinding;
|
|
1115
|
-
if (sessions === null || sessions === undefined
|
|
1116
|
-
|| binding === null || binding === undefined
|
|
1117
|
-
|| binding.authority.owner !== "controller") {
|
|
1118
|
-
throw new Error("Controller does not own the Provider writer authority.");
|
|
1119
|
-
}
|
|
1120
|
-
if (binding.turn !== null
|
|
1121
|
-
&& ["submitting", "accepted", "running", "delivery-unknown"]
|
|
1122
|
-
.includes(binding.turn.status)) {
|
|
1123
|
-
return { status: "busy" };
|
|
1124
|
-
}
|
|
1125
|
-
const conversation = currentProviderConversation(binding);
|
|
1126
|
-
const activation = currentProviderActivation(binding);
|
|
1127
|
-
if (input.providerFence === undefined
|
|
1128
|
-
|| conversation.conversationId !== input.providerFence.conversationId
|
|
1129
|
-
|| activation?.activationId !== input.providerFence.activationId) {
|
|
1130
|
-
throw new Error("Provider input claim carries a stale Conversation/Activation fence.");
|
|
1131
|
-
}
|
|
1132
|
-
const next = beginProviderTurn(binding, {
|
|
1133
|
-
runId: input.executionRef.id,
|
|
1134
|
-
attemptId: input.attemptId,
|
|
1135
|
-
authorityEpoch: binding.authority.epoch,
|
|
1136
|
-
submittedAt: input.now.toISOString()
|
|
1137
|
-
});
|
|
1138
|
-
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, next, input.now));
|
|
1139
|
-
}
|
|
1140
|
-
const claimed = claimMailboxInputDelivery(mailbox, {
|
|
1141
|
-
attemptId: input.attemptId,
|
|
1142
|
-
lane: input.lane,
|
|
1143
|
-
mode: input.mode,
|
|
1144
|
-
owner: input.owner,
|
|
1145
|
-
startedAt: input.now.toISOString(),
|
|
1146
|
-
executionRef: input.executionRef,
|
|
1147
|
-
providerFence: input.providerFence
|
|
1148
|
-
});
|
|
1149
|
-
store.saveWorkMailbox(claimed);
|
|
1150
|
-
return { status: "claimed", delivery: claimed.inputDelivery };
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
markInputDeliveryPushed(target, attemptId, now) {
|
|
1154
|
-
return this.store.transaction((store) => {
|
|
1155
|
-
const mailbox = store.getWorkMailbox(target);
|
|
1156
|
-
if (mailbox?.inputDelivery?.attemptId !== attemptId)
|
|
1157
|
-
return false;
|
|
1158
|
-
store.saveWorkMailbox(markMailboxInputDeliveryPushed(mailbox, attemptId, now));
|
|
1159
|
-
return true;
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
completeInputDelivery(target, attemptId, now) {
|
|
1101
|
+
consumeWorkMailbox(target, expected) {
|
|
1163
1102
|
return this.store.transaction((store) => {
|
|
1164
1103
|
const mailbox = store.getWorkMailbox(target);
|
|
1165
|
-
if (mailbox?.
|
|
1104
|
+
if (mailbox?.pending === null || mailbox?.pending === undefined
|
|
1105
|
+
|| mailbox.pending.fromSequence !== expected.fromSequence
|
|
1106
|
+
|| mailbox.pending.toSequence < expected.toSequence) {
|
|
1166
1107
|
return false;
|
|
1167
|
-
const delivery = mailbox.inputDelivery;
|
|
1168
|
-
if (target.kind === "role"
|
|
1169
|
-
&& delivery.executionRef.type === "run"
|
|
1170
|
-
&& delivery.executionRef.taskId === target.taskId) {
|
|
1171
|
-
store.saveEvent(target.taskId, createTaskEvent(store.nextEventId(target.taskId), target.taskId, "run.input-delivered", {
|
|
1172
|
-
attemptId: delivery.attemptId,
|
|
1173
|
-
runId: delivery.executionRef.id,
|
|
1174
|
-
...(delivery.providerFence === undefined ? {} : {
|
|
1175
|
-
conversationId: delivery.providerFence.conversationId,
|
|
1176
|
-
activationId: delivery.providerFence.activationId,
|
|
1177
|
-
...(delivery.providerFence.nativeTurnId === undefined ? {} : {
|
|
1178
|
-
nativeTurnId: delivery.providerFence.nativeTurnId
|
|
1179
|
-
})
|
|
1180
|
-
})
|
|
1181
|
-
}, now));
|
|
1182
|
-
}
|
|
1183
|
-
store.saveWorkMailbox(completeMailboxInputDelivery(mailbox, attemptId, now));
|
|
1184
|
-
return true;
|
|
1185
|
-
});
|
|
1186
|
-
}
|
|
1187
|
-
releaseInputDelivery(target, attemptId, now) {
|
|
1188
|
-
return this.store.transaction((store) => {
|
|
1189
|
-
const mailbox = store.getWorkMailbox(target);
|
|
1190
|
-
if (mailbox?.inputDelivery?.attemptId !== attemptId)
|
|
1191
|
-
return false;
|
|
1192
|
-
rejectSubmittingProviderTurn(store, target, mailbox.inputDelivery, now, "Provider write was not attempted.");
|
|
1193
|
-
store.saveWorkMailbox(releaseMailboxInputDelivery(mailbox, attemptId));
|
|
1194
|
-
return true;
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
resolveInputDeliveryNotAccepted(target, attemptId, now) {
|
|
1198
|
-
return this.store.transaction((store) => {
|
|
1199
|
-
const mailbox = store.getWorkMailbox(target);
|
|
1200
|
-
if (mailbox?.inputDelivery?.attemptId !== attemptId)
|
|
1201
|
-
return false;
|
|
1202
|
-
rejectSubmittingProviderTurn(store, target, mailbox.inputDelivery, now, "Provider returned an exact negative acknowledgement.");
|
|
1203
|
-
store.saveWorkMailbox(resolveMailboxInputDeliveryNotAccepted(mailbox, attemptId));
|
|
1204
|
-
return true;
|
|
1205
|
-
});
|
|
1206
|
-
}
|
|
1207
|
-
markInputDeliveryUnknown(target, attemptId, reason, now) {
|
|
1208
|
-
return this.store.transaction((store) => {
|
|
1209
|
-
const mailbox = store.getWorkMailbox(target);
|
|
1210
|
-
if (mailbox?.inputDelivery?.attemptId !== attemptId)
|
|
1211
|
-
return false;
|
|
1212
|
-
const unknown = markMailboxInputDeliveryUnknown(mailbox, attemptId, reason, now);
|
|
1213
|
-
markSubmittingProviderTurnUnknown(store, target, mailbox.inputDelivery, now, reason);
|
|
1214
|
-
store.saveWorkMailbox(unknown);
|
|
1215
|
-
if (target.kind === "role") {
|
|
1216
|
-
const taskId = target.taskId;
|
|
1217
|
-
const event = createTaskEvent(store.nextEventId(taskId), taskId, "run.input-delivery-unknown", { attemptId, reason }, now);
|
|
1218
|
-
store.saveEvent(taskId, event);
|
|
1219
|
-
routeRoleEvent(store, event, target.roleName, "run-input-delivery-unknown", now);
|
|
1220
1108
|
}
|
|
1109
|
+
store.saveWorkMailbox(consumePendingBatch(mailbox, expected));
|
|
1221
1110
|
return true;
|
|
1222
1111
|
});
|
|
1223
1112
|
}
|
|
@@ -1259,7 +1148,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1259
1148
|
}
|
|
1260
1149
|
mailbox = completeProcessing(mailbox, mailbox.processing.batchId);
|
|
1261
1150
|
}
|
|
1262
|
-
const pending =
|
|
1151
|
+
const pending = mailbox.pending;
|
|
1263
1152
|
if (pending !== null) {
|
|
1264
1153
|
if (pending.reasons.length === 0
|
|
1265
1154
|
|| !pending.reasons.every(isRuntimeCleanupReason)) {
|
|
@@ -1298,7 +1187,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1298
1187
|
return this.listRuntimeSessionCandidates().flatMap((candidate) => {
|
|
1299
1188
|
if (hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget(candidate.owner)))
|
|
1300
1189
|
|| (candidate.owner.scope === "task"
|
|
1301
|
-
&& this.store.
|
|
1190
|
+
&& this.store.getActiveTurn(candidate.owner.taskId, candidate.owner.roleName) !== null)) {
|
|
1302
1191
|
return [];
|
|
1303
1192
|
}
|
|
1304
1193
|
return [{
|
|
@@ -1366,12 +1255,12 @@ export class FileSchedulerStoreAdapter {
|
|
|
1366
1255
|
|| role.workspace !== input.role.workspace) {
|
|
1367
1256
|
return "state-changed";
|
|
1368
1257
|
}
|
|
1369
|
-
if (!isDeepStrictEqual(input.
|
|
1258
|
+
if (!isDeepStrictEqual(input.turn.effective, input.role.effective)) {
|
|
1370
1259
|
return "state-changed";
|
|
1371
1260
|
}
|
|
1372
|
-
if (store.
|
|
1261
|
+
if (store.getTurn(input.task.id, input.turn.id) !== null)
|
|
1373
1262
|
return "state-changed";
|
|
1374
|
-
if (store.
|
|
1263
|
+
if (store.getActiveTurn(input.task.id, input.role.name) !== null)
|
|
1375
1264
|
return "busy";
|
|
1376
1265
|
const pending = store.getPendingWakeup(input.task.id);
|
|
1377
1266
|
if (pending === null || !pendingWakeupsMatch(pending, input.wakeup)) {
|
|
@@ -1381,29 +1270,20 @@ export class FileSchedulerStoreAdapter {
|
|
|
1381
1270
|
const mailbox = store.getWorkMailbox(target);
|
|
1382
1271
|
if (mailbox === null || !mailboxHasPending(mailbox))
|
|
1383
1272
|
return "state-changed";
|
|
1384
|
-
if (
|
|
1385
|
-
return "busy";
|
|
1386
|
-
if (store.peekNextAgentRunId(input.task.id) !== input.run.id) {
|
|
1273
|
+
if (store.peekNextTurnId(input.task.id) !== input.turn.id) {
|
|
1387
1274
|
return "state-changed";
|
|
1388
1275
|
}
|
|
1389
|
-
const
|
|
1390
|
-
if (
|
|
1391
|
-
throw new Error(`Leader
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
// FileTaskStore happens to persist both from saveActiveAgentRun. Keep
|
|
1401
|
-
// the adapter contract backend-neutral and make the two writes atomic.
|
|
1402
|
-
store.saveAgentRun(input.run);
|
|
1403
|
-
store.saveActiveAgentRun(input.run);
|
|
1404
|
-
store.saveWorkMailbox(claimed);
|
|
1405
|
-
bindTaskRoleRunInFlight(store, role, input.run, input.now);
|
|
1406
|
-
store.saveEvent(input.task.id, createTaskEvent(store.nextEventId(input.task.id), input.task.id, "run.dispatched", runLaunchEventPayload(input.run), input.now));
|
|
1276
|
+
const allocatedTurnId = store.nextTurnId(input.task.id);
|
|
1277
|
+
if (allocatedTurnId !== input.turn.id) {
|
|
1278
|
+
throw new Error(`Leader Turn allocation changed unexpectedly: ${input.task.id}.`);
|
|
1279
|
+
}
|
|
1280
|
+
// The durable Turn row and its active pointer are separate projections;
|
|
1281
|
+
// keep both writes in the same storage transaction.
|
|
1282
|
+
store.saveTurn(input.turn);
|
|
1283
|
+
store.saveActiveTurn(input.turn);
|
|
1284
|
+
store.saveWorkMailbox(consumePendingBatch(mailbox));
|
|
1285
|
+
store.clearPendingWakeup(input.task.id);
|
|
1286
|
+
store.saveEvent(input.task.id, createTaskEvent(store.nextEventId(input.task.id), input.task.id, "turn.dispatched", turnLaunchEventPayload(input.turn), input.now));
|
|
1407
1287
|
if (input.wakeId !== undefined && input.wakeFromCursor !== undefined) {
|
|
1408
1288
|
if (store.peekNextTaskWakeId(input.task.id) !== input.wakeId) {
|
|
1409
1289
|
return "state-changed";
|
|
@@ -1418,11 +1298,11 @@ export class FileSchedulerStoreAdapter {
|
|
|
1418
1298
|
reasons: input.wakeup.reasons,
|
|
1419
1299
|
fromCursor: input.wakeFromCursor,
|
|
1420
1300
|
toCursor: input.now.toISOString(),
|
|
1421
|
-
|
|
1301
|
+
turnId: input.turn.id,
|
|
1422
1302
|
now: input.now
|
|
1423
1303
|
}));
|
|
1424
1304
|
}
|
|
1425
|
-
if (input.
|
|
1305
|
+
if (input.turn.mode !== "new"
|
|
1426
1306
|
&& input.session !== null
|
|
1427
1307
|
&& input.session.nativeSessionId !== undefined) {
|
|
1428
1308
|
saveTaskSession(store, role, {
|
|
@@ -1434,73 +1314,48 @@ export class FileSchedulerStoreAdapter {
|
|
|
1434
1314
|
return "claimed";
|
|
1435
1315
|
});
|
|
1436
1316
|
}
|
|
1437
|
-
|
|
1438
|
-
this.store.transaction((store) => {
|
|
1439
|
-
const task = store.getTask(input.
|
|
1317
|
+
saveLeaderSteer(input) {
|
|
1318
|
+
return this.store.transaction((store) => {
|
|
1319
|
+
const task = store.getTask(input.taskId);
|
|
1320
|
+
const active = store.getActiveTurn(input.taskId, "leader");
|
|
1440
1321
|
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1441
|
-
|
|
1442
|
-
}
|
|
1443
|
-
const role = requireRole(store, input.task.id, input.role.name);
|
|
1444
|
-
const active = store.getActiveAgentRun(input.task.id, input.role.name);
|
|
1445
|
-
if (active === null || active.id !== input.run.id) {
|
|
1446
|
-
throw new Error(`Active Agent run changed before delivery was persisted: ${input.run.id}.`);
|
|
1447
|
-
}
|
|
1448
|
-
let deliveryRun = active;
|
|
1449
|
-
if (active.controlRequest?.state === "dispatching") {
|
|
1450
|
-
deliveryRun = {
|
|
1451
|
-
...active,
|
|
1452
|
-
controlRequest: markWorkflowOutcomeRequestDispatched(active.controlRequest)
|
|
1453
|
-
};
|
|
1454
|
-
store.saveAgentRun(deliveryRun);
|
|
1455
|
-
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, "runtime.workflow-outcome-request-dispatched", {
|
|
1456
|
-
runId: active.id,
|
|
1457
|
-
roleName: active.roleName,
|
|
1458
|
-
requestId: active.controlRequest.requestId,
|
|
1459
|
-
receiptId: active.controlRequest.receiptId,
|
|
1460
|
-
nativeTurnId: active.controlRequest.nativeTurnId
|
|
1461
|
-
}, input.now));
|
|
1462
|
-
}
|
|
1463
|
-
// Transport success records prompt-pushed ONLY. Acceptance (deliveredAt)
|
|
1464
|
-
// is written exclusively by an exact provider-accepted fold, never by the
|
|
1465
|
-
// transport receipt — that removes the transport-to-delivered false path.
|
|
1466
|
-
if (deliveryRun.pushedAt === undefined) {
|
|
1467
|
-
const pushed = markAgentRunPushed(deliveryRun, input.now);
|
|
1468
|
-
store.saveAgentRun(pushed);
|
|
1469
|
-
markTaskRoleRunPushedInFlight(store, role, pushed, input.now);
|
|
1470
|
-
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, "run.pushed", runLaunchEventPayload(pushed), input.now));
|
|
1471
|
-
}
|
|
1472
|
-
else {
|
|
1473
|
-
const sessions = store.getTaskRoleSessionSet(input.task.id, input.role.name);
|
|
1474
|
-
if (sessions?.inFlight?.runId === input.run.id
|
|
1475
|
-
&& sessions.inFlight.pushedAt === undefined) {
|
|
1476
|
-
markTaskRoleRunPushedInFlight(store, role, deliveryRun, input.now);
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
if (input.session !== null && input.session.nativeSessionId !== undefined) {
|
|
1480
|
-
const existing = store.getRoleSession(input.task.id, input.role.name);
|
|
1481
|
-
if (existing?.agentId !== input.session.agentId
|
|
1482
|
-
|| existing.adapterId !== input.session.adapterId
|
|
1483
|
-
|| existing.nativeSessionId !== input.session.nativeSessionId
|
|
1484
|
-
|| (input.launchId !== undefined && existing.launchId !== input.launchId)
|
|
1485
|
-
|| existing.status !== "active") {
|
|
1486
|
-
saveTaskSession(store, role, {
|
|
1487
|
-
...input.session,
|
|
1488
|
-
nativeSessionId: input.session.nativeSessionId
|
|
1489
|
-
}, "active", input.now, input.launchId);
|
|
1490
|
-
}
|
|
1322
|
+
return "unavailable";
|
|
1491
1323
|
}
|
|
1324
|
+
if (active === null || active.id !== input.turnId || active.status !== "active")
|
|
1325
|
+
return "busy";
|
|
1326
|
+
const target = { kind: "role", taskId: input.taskId, roleName: "leader" };
|
|
1327
|
+
const mailbox = store.getWorkMailbox(target);
|
|
1328
|
+
const processing = mailbox?.processing;
|
|
1329
|
+
if (mailbox === null
|
|
1330
|
+
|| processing?.batchId !== input.batchId
|
|
1331
|
+
|| processing.owner !== `leader-steer:${input.turnId}`)
|
|
1332
|
+
return "state-changed";
|
|
1333
|
+
const updated = appendTurnInput(active, input.input, input.now);
|
|
1334
|
+
store.saveTurn(updated);
|
|
1335
|
+
store.saveActiveTurn(updated);
|
|
1336
|
+
store.saveWorkMailbox(completeProcessing(mailbox, input.batchId));
|
|
1337
|
+
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "turn.input-submitted", {
|
|
1338
|
+
turnId: active.id,
|
|
1339
|
+
sequence: String(updated.inputs.length),
|
|
1340
|
+
source: `${input.input.source.type}/${input.input.source.channel}`,
|
|
1341
|
+
reasons: processing.batch.reasons.join(",")
|
|
1342
|
+
}, input.now));
|
|
1343
|
+
return "claimed";
|
|
1492
1344
|
});
|
|
1493
1345
|
}
|
|
1494
|
-
|
|
1346
|
+
saveRoleTurnPrepared(input) {
|
|
1495
1347
|
this.store.transaction((store) => {
|
|
1496
1348
|
const task = store.getTask(input.task.id);
|
|
1497
1349
|
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1498
1350
|
throw new Error(`Task is not active: ${input.task.id}.`);
|
|
1499
1351
|
}
|
|
1500
1352
|
const role = requireRole(store, input.task.id, input.role.name);
|
|
1501
|
-
const active = store.
|
|
1502
|
-
if (active === null || active.id !== input.
|
|
1503
|
-
throw new Error(`Active
|
|
1353
|
+
const active = store.getActiveTurn(input.task.id, input.role.name);
|
|
1354
|
+
if (active === null || active.id !== input.turn.id) {
|
|
1355
|
+
throw new Error(`Active Turn changed before preparation was persisted: ${input.turn.id}.`);
|
|
1356
|
+
}
|
|
1357
|
+
if (store.getTaskRoleSessionSet(input.task.id, input.role.name) === null) {
|
|
1358
|
+
store.saveTaskRoleSessionSet(createRoleSessionSet({ scope: "task", taskId: input.task.id, roleName: input.role.name }, input.turn.effective.agentId, input.now));
|
|
1504
1359
|
}
|
|
1505
1360
|
if (input.session !== null && input.session.nativeSessionId !== undefined) {
|
|
1506
1361
|
const existing = store.getRoleSession(input.task.id, input.role.name);
|
|
@@ -1522,71 +1377,51 @@ export class FileSchedulerStoreAdapter {
|
|
|
1522
1377
|
}, "active", input.now, input.launchId);
|
|
1523
1378
|
}
|
|
1524
1379
|
}
|
|
1525
|
-
bindTaskRoleRunInFlight(store, role, input.run, input.now);
|
|
1526
1380
|
});
|
|
1527
1381
|
}
|
|
1528
|
-
|
|
1382
|
+
saveRoleTurnDeliveryFailure(input) {
|
|
1529
1383
|
return this.store.transaction((store) => {
|
|
1530
1384
|
const task = store.getTask(input.taskId);
|
|
1531
1385
|
const role = store.getRole(input.taskId, input.roleName);
|
|
1532
|
-
const active = store.
|
|
1386
|
+
const active = store.getActiveTurn(input.taskId, input.roleName);
|
|
1533
1387
|
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
1534
1388
|
const session = sessions?.sessions[input.agentId];
|
|
1535
|
-
const mailbox = store.getWorkMailbox({
|
|
1536
|
-
kind: "role",
|
|
1537
|
-
taskId: input.taskId,
|
|
1538
|
-
roleName: input.roleName
|
|
1539
|
-
});
|
|
1540
1389
|
if (task === null
|
|
1541
1390
|
|| task.status !== "active"
|
|
1542
1391
|
|| task.executionGate.state !== "enabled"
|
|
1543
1392
|
|| role === null
|
|
1544
1393
|
|| active === null
|
|
1545
|
-
|| active.id !== input.
|
|
1394
|
+
|| active.id !== input.turnId
|
|
1546
1395
|
|| active.status !== "active"
|
|
1547
|
-
|| active.pushedAt !== undefined
|
|
1548
1396
|
|| active.effective.agentId !== input.agentId
|
|
1549
|
-
|| active.effective.adapterId !== input.adapterId
|
|
1550
|
-
|| mailbox?.processing?.batchId !== input.mailboxBatchId
|
|
1551
|
-
|| mailbox.processing.executionRef?.type !== "run"
|
|
1552
|
-
|| mailbox.processing.executionRef.taskId !== input.taskId
|
|
1553
|
-
|| mailbox.processing.executionRef.id !== input.runId
|
|
1554
|
-
|| !preparedDeliveryFailureSessionFenceMatches(sessions, session, input, agentRunDeliveryReceiptId(active))) {
|
|
1397
|
+
|| active.effective.adapterId !== input.adapterId) {
|
|
1555
1398
|
return "state-changed";
|
|
1556
1399
|
}
|
|
1557
1400
|
const summary = input.summary
|
|
1558
|
-
?? `Role delivery failed conclusively before exact
|
|
1559
|
-
const result =
|
|
1401
|
+
?? `Role delivery failed conclusively before exact Turn input acceptance: ${input.turnId}.`;
|
|
1402
|
+
const result = terminalizeExactTaskTurn(store, {
|
|
1560
1403
|
taskId: input.taskId,
|
|
1561
1404
|
roleName: input.roleName,
|
|
1562
1405
|
agentId: input.agentId,
|
|
1563
|
-
|
|
1564
|
-
receiptId: agentRunDeliveryReceiptId(active),
|
|
1406
|
+
turnId: input.turnId,
|
|
1565
1407
|
...(session?.nativeSessionId === undefined
|
|
1566
1408
|
? {}
|
|
1567
1409
|
: { nativeSessionId: session.nativeSessionId }),
|
|
1568
1410
|
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
1569
|
-
outcome: { status: "failed", summary }
|
|
1411
|
+
outcome: { status: "failed", summary, failureReason: input.failureReason }
|
|
1570
1412
|
}, input.now);
|
|
1571
|
-
if (result.disposition !== "applied" || result.
|
|
1413
|
+
if (result.disposition !== "applied" || result.turn === null) {
|
|
1572
1414
|
return "state-changed";
|
|
1573
1415
|
}
|
|
1574
|
-
|
|
1575
|
-
enqueueWork(store, runtimeLifecycleTarget({
|
|
1576
|
-
scope: "task",
|
|
1577
|
-
taskId: input.taskId,
|
|
1578
|
-
roleName: input.roleName
|
|
1579
|
-
}), RUNTIME_HOST_DETACH_REQUIRED_REASON, input.now, [{ type: "task", id: input.taskId }]);
|
|
1580
|
-
}
|
|
1581
|
-
const terminal = result.run;
|
|
1416
|
+
const terminal = result.turn;
|
|
1582
1417
|
const deliveryFailureEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.role-delivery-failed", {
|
|
1583
|
-
|
|
1418
|
+
turnId: terminal.id,
|
|
1584
1419
|
roleName: input.roleName,
|
|
1585
1420
|
outcome: terminal.status
|
|
1586
1421
|
}, input.now);
|
|
1587
1422
|
store.saveEvent(input.taskId, deliveryFailureEvent);
|
|
1588
1423
|
if (input.roleName !== "leader") {
|
|
1589
|
-
routeRoleEvent(store, deliveryFailureEvent, input.roleName, terminal.purpose === "review" ? "review-failed" : "role-
|
|
1424
|
+
routeRoleEvent(store, deliveryFailureEvent, input.roleName, terminal.purpose === "review" ? "review-failed" : "role-turn-failed", input.now);
|
|
1590
1425
|
}
|
|
1591
1426
|
else {
|
|
1592
1427
|
store.saveLeaderFailure(recordLeaderFailure(input.taskId, session?.nativeSessionId ?? "(unregistered)", summary, input.now, store.getLeaderFailure(input.taskId)));
|
|
@@ -1620,7 +1455,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1620
1455
|
: []);
|
|
1621
1456
|
const queued = store.getWorkMailbox(target);
|
|
1622
1457
|
if (queued === null
|
|
1623
|
-
||
|
|
1458
|
+
|| queued.pending === null
|
|
1624
1459
|
|| queued.processing !== null) {
|
|
1625
1460
|
throw new Error("Runtime launch reservation could not be queued.");
|
|
1626
1461
|
}
|
|
@@ -1704,7 +1539,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1704
1539
|
* The exact reservation is authoritative. If its matching Hook won the
|
|
1705
1540
|
* transaction race and already cleared that reservation, the session
|
|
1706
1541
|
* fallback is permitted only while the same Agent/native identity remains
|
|
1707
|
-
* current and no later lifecycle work or Task
|
|
1542
|
+
* current and no later lifecycle work or Task Turn exists.
|
|
1708
1543
|
*/
|
|
1709
1544
|
settleStoppedRuntimeLaunch(input, now = new Date()) {
|
|
1710
1545
|
return this.store.transaction((store) => {
|
|
@@ -1722,7 +1557,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1722
1557
|
if (hasRuntimeLifecycleWork(mailbox))
|
|
1723
1558
|
return false;
|
|
1724
1559
|
if (input.owner.scope === "task"
|
|
1725
|
-
&& store.
|
|
1560
|
+
&& store.getActiveTurn(input.owner.taskId, input.owner.roleName) !== null) {
|
|
1726
1561
|
return false;
|
|
1727
1562
|
}
|
|
1728
1563
|
const sessions = runtimeOwnerSessionSet(store, input.owner);
|
|
@@ -1769,57 +1604,43 @@ export class FileSchedulerStoreAdapter {
|
|
|
1769
1604
|
}
|
|
1770
1605
|
const providerTurn = sessions?.providerBinding?.turn;
|
|
1771
1606
|
const canonicalTurnId = sessions === null || sessions === undefined
|
|
1772
|
-
? input.
|
|
1773
|
-
: canonicalStructuredProviderTurnId(sessions, input.
|
|
1774
|
-
const recordedProviderTurn =
|
|
1775
|
-
&& providerTurn
|
|
1776
|
-
&& providerTurn.
|
|
1607
|
+
? input.nativeTurnId
|
|
1608
|
+
: canonicalStructuredProviderTurnId(sessions, input.nativeTurnId, input.attemptId);
|
|
1609
|
+
const recordedProviderTurn = providerTurn !== null
|
|
1610
|
+
&& providerTurn !== undefined
|
|
1611
|
+
&& providerTurn.nativeTurnId === input.nativeTurnId
|
|
1612
|
+
&& providerTurn.nativeTurnId === canonicalTurnId;
|
|
1777
1613
|
if (recordedProviderTurn) {
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
}
|
|
1783
|
-
// A normal explicit CLI yield may precede its native Hook; that Hook still
|
|
1784
|
-
// owns the turn fact. A forced cleanup boundary or stopped process instead
|
|
1785
|
-
// makes the old generation obsolete.
|
|
1786
|
-
if (sessions?.inFlight === null || sessions === null) {
|
|
1787
|
-
const cleanup = hasRuntimeCleanupObligation(this.store.getWorkMailbox(runtimeLifecycleTarget({
|
|
1788
|
-
scope: "task",
|
|
1789
|
-
taskId: input.taskId,
|
|
1790
|
-
roleName: input.roleName
|
|
1791
|
-
})));
|
|
1792
|
-
return cleanup || isObsoleteTerminalRuntimeRun(this.store, input)
|
|
1793
|
-
? "obsolete"
|
|
1794
|
-
: "apply";
|
|
1795
|
-
}
|
|
1796
|
-
if (input.runId === undefined || sessions.inFlight.runId !== input.runId) {
|
|
1797
|
-
return "obsolete";
|
|
1614
|
+
if (input.turnId === undefined)
|
|
1615
|
+
return "apply";
|
|
1616
|
+
const run = this.store.getTurn(input.taskId, input.turnId);
|
|
1617
|
+
return run === null ? "obsolete" : "apply";
|
|
1798
1618
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
return
|
|
1619
|
+
const active = this.store.getActiveTurn(input.taskId, input.roleName);
|
|
1620
|
+
if (input.turnId === undefined)
|
|
1621
|
+
return active === null ? "apply" : "obsolete";
|
|
1622
|
+
return active?.id === input.turnId ? "apply" : "obsolete";
|
|
1803
1623
|
}
|
|
1804
1624
|
observeRuntimeTurnCompleted(input, now = new Date()) {
|
|
1805
1625
|
return this.store.transaction((store) => {
|
|
1806
1626
|
const task = store.getTask(input.taskId);
|
|
1807
1627
|
if (task === null)
|
|
1808
1628
|
throw new Error(`Task not found: ${input.taskId}.`);
|
|
1809
|
-
if (task.status !== "active"
|
|
1810
|
-
throw new Error(`Cannot complete a runtime turn for
|
|
1629
|
+
if (task.status !== "active" && task.status !== "completed") {
|
|
1630
|
+
throw new Error(`Cannot complete a runtime turn for unavailable Task: ${input.taskId}.`);
|
|
1811
1631
|
}
|
|
1812
1632
|
const role = requireRole(store, input.taskId, input.roleName);
|
|
1813
1633
|
let sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName)
|
|
1814
1634
|
?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
|
|
1815
|
-
const canonicalTurnId = canonicalStructuredProviderTurnId(sessions, input.
|
|
1635
|
+
const canonicalTurnId = canonicalStructuredProviderTurnId(sessions, input.nativeTurnId, input.attemptId);
|
|
1816
1636
|
const providerTurn = sessions.providerBinding?.turn;
|
|
1817
|
-
const recordedProviderTurn =
|
|
1818
|
-
&& providerTurn
|
|
1819
|
-
&& providerTurn.
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1637
|
+
const recordedProviderTurn = providerTurn !== null
|
|
1638
|
+
&& providerTurn !== undefined
|
|
1639
|
+
&& providerTurn.nativeTurnId === input.nativeTurnId
|
|
1640
|
+
&& providerTurn.nativeTurnId === canonicalTurnId;
|
|
1641
|
+
const observedTurn = input.turnId === undefined
|
|
1642
|
+
? store.listTurns(input.taskId).find((turn) => (turn.result?.provider?.nativeTurnId === canonicalTurnId)) ?? null
|
|
1643
|
+
: store.getTurn(input.taskId, input.turnId);
|
|
1823
1644
|
const existing = sessions.sessions[input.agentId];
|
|
1824
1645
|
const owner = {
|
|
1825
1646
|
scope: "task",
|
|
@@ -1837,30 +1658,21 @@ export class FileSchedulerStoreAdapter {
|
|
|
1837
1658
|
}
|
|
1838
1659
|
if (effectiveExisting !== undefined
|
|
1839
1660
|
&& hasRecentTurnId(effectiveExisting.recentCompletedTurnIds, canonicalTurnId)) {
|
|
1840
|
-
return {
|
|
1661
|
+
return {
|
|
1662
|
+
session: effectiveExisting,
|
|
1663
|
+
duplicate: true,
|
|
1664
|
+
...(observedTurn === null ? {} : { turn: observedTurn })
|
|
1665
|
+
};
|
|
1841
1666
|
}
|
|
1842
1667
|
if (!recordedProviderTurn
|
|
1843
|
-
&& sessions.inFlight === null
|
|
1844
1668
|
&& existing !== undefined
|
|
1845
1669
|
&& (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))
|
|
1846
|
-
||
|
|
1670
|
+
|| isObsoleteTerminalRuntimeTurn(store, input))) {
|
|
1847
1671
|
return { session: existing, duplicate: false, disposition: "obsolete" };
|
|
1848
1672
|
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
&& sessions.inFlight !== null
|
|
1853
|
-
&& (input.runId === undefined
|
|
1854
|
-
|| sessions.inFlight.runId !== input.runId)) {
|
|
1855
|
-
if (effectiveExisting === undefined) {
|
|
1856
|
-
throw new Error("Runtime turn completion no longer matches the in-flight Run.");
|
|
1857
|
-
}
|
|
1858
|
-
return { session: effectiveExisting, duplicate: false };
|
|
1859
|
-
}
|
|
1860
|
-
if (recordedProviderTurn
|
|
1861
|
-
? observedRun?.pushedAt === undefined
|
|
1862
|
-
: sessions.inFlight !== null && sessions.inFlight.pushedAt === undefined) {
|
|
1863
|
-
throw new Error("Runtime turn completion requires an independently committed transport receipt.");
|
|
1673
|
+
if (!recordedProviderTurn && input.turnId !== undefined
|
|
1674
|
+
&& observedTurn?.id !== input.turnId) {
|
|
1675
|
+
return { session: effectiveExisting, duplicate: false, disposition: "obsolete" };
|
|
1864
1676
|
}
|
|
1865
1677
|
const sessionStatus = effectiveExisting?.status ?? "active";
|
|
1866
1678
|
sessions = recordRoleAgentSession(sessions, {
|
|
@@ -1876,16 +1688,6 @@ export class FileSchedulerStoreAdapter {
|
|
|
1876
1688
|
effective
|
|
1877
1689
|
}, now);
|
|
1878
1690
|
sessions = settleStructuredProviderTurn(sessions, canonicalTurnId, input.providerStatus ?? "completed", now);
|
|
1879
|
-
if (sessions.inFlight === null) {
|
|
1880
|
-
sessions = rememberRoleAgentCompletedTurn(sessions, input.agentId, input.nativeSessionId, canonicalTurnId, now);
|
|
1881
|
-
store.saveTaskRoleSessionSet(sessions);
|
|
1882
|
-
completeRuntimeHookReservation(store, owner, input.launchId);
|
|
1883
|
-
return { session: sessions.sessions[input.agentId], duplicate: false };
|
|
1884
|
-
}
|
|
1885
|
-
// A provider Turn is an activation boundary, not a Yui Run outcome.
|
|
1886
|
-
// Preserve the Run fence and make the fixed native Session available for
|
|
1887
|
-
// a later mailbox batch. Provider-owned background subagents may finish
|
|
1888
|
-
// after this point and Yui must not invent a failure while waiting.
|
|
1889
1691
|
sessions = recordTaskRoleTurnBoundary(sessions, {
|
|
1890
1692
|
agentId: input.agentId,
|
|
1891
1693
|
nativeSessionId: input.nativeSessionId,
|
|
@@ -1893,9 +1695,89 @@ export class FileSchedulerStoreAdapter {
|
|
|
1893
1695
|
}, now);
|
|
1894
1696
|
store.saveTaskRoleSessionSet(sessions);
|
|
1895
1697
|
completeRuntimeHookReservation(store, owner, input.launchId);
|
|
1698
|
+
let terminalTurn;
|
|
1699
|
+
if (recordedProviderTurn && observedTurn?.status === "active") {
|
|
1700
|
+
const binding = sessions.providerBinding;
|
|
1701
|
+
const conversation = currentProviderConversation(binding);
|
|
1702
|
+
const activation = binding.activations.find((entry) => (entry.activationId === input.launchId
|
|
1703
|
+
|| (entry.conversationId === conversation.conversationId && entry.status === "active")));
|
|
1704
|
+
if (activation === undefined) {
|
|
1705
|
+
throw new Error("Provider Turn result has no matching Activation.");
|
|
1706
|
+
}
|
|
1707
|
+
const providerStatus = input.providerStatus ?? "completed";
|
|
1708
|
+
let reviewResult;
|
|
1709
|
+
if (observedTurn.purpose === "review" || observedTurn.executionGroupId !== undefined) {
|
|
1710
|
+
try {
|
|
1711
|
+
const parsed = parseReviewResultReport(input.summary);
|
|
1712
|
+
reviewResult = {
|
|
1713
|
+
report: parsed.report,
|
|
1714
|
+
checks: parsed.checks,
|
|
1715
|
+
...(parsed.findings === undefined ? {} : { findings: parsed.findings }),
|
|
1716
|
+
...(parsed.evidence === undefined ? {} : { evidence: parsed.evidence }),
|
|
1717
|
+
...(parsed.evidenceCommit === undefined
|
|
1718
|
+
? {}
|
|
1719
|
+
: { evidenceCommit: parsed.evidenceCommit }),
|
|
1720
|
+
...(parsed.deltaDisposition === undefined
|
|
1721
|
+
? {}
|
|
1722
|
+
: { deltaDisposition: parsed.deltaDisposition }),
|
|
1723
|
+
...(parsed.deltaReasoning === undefined
|
|
1724
|
+
? {}
|
|
1725
|
+
: { deltaReasoning: parsed.deltaReasoning })
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
catch {
|
|
1729
|
+
reviewResult = { report: input.summary, checks: [] };
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
const terminalized = terminalizeExactTaskTurn(store, {
|
|
1733
|
+
taskId: input.taskId,
|
|
1734
|
+
roleName: input.roleName,
|
|
1735
|
+
agentId: input.agentId,
|
|
1736
|
+
turnId: observedTurn.id,
|
|
1737
|
+
nativeSessionId: input.nativeSessionId,
|
|
1738
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
1739
|
+
outcome: {
|
|
1740
|
+
status: providerStatus === "completed" ? "completed" : "failed",
|
|
1741
|
+
summary: input.summary,
|
|
1742
|
+
...(providerStatus === "completed"
|
|
1743
|
+
? {}
|
|
1744
|
+
: {
|
|
1745
|
+
failureReason: providerStatus === "cancelled"
|
|
1746
|
+
? "cancelled"
|
|
1747
|
+
: "runtime-failed"
|
|
1748
|
+
}),
|
|
1749
|
+
provider: {
|
|
1750
|
+
providerNamespace: binding.providerNamespace,
|
|
1751
|
+
accountScope: binding.accountScope,
|
|
1752
|
+
conversationId: conversation.conversationId,
|
|
1753
|
+
activationId: activation.activationId,
|
|
1754
|
+
nativeTurnId: canonicalTurnId,
|
|
1755
|
+
status: providerStatus
|
|
1756
|
+
}
|
|
1757
|
+
},
|
|
1758
|
+
...(reviewResult === undefined ? {} : { reviewResult })
|
|
1759
|
+
}, now);
|
|
1760
|
+
if (terminalized.disposition !== "applied" || terminalized.turn === null) {
|
|
1761
|
+
throw new Error(`Provider Turn terminal could not complete its exact Turn: ${terminalized.reason ?? "obsolete"}.`);
|
|
1762
|
+
}
|
|
1763
|
+
terminalTurn = terminalized.turn;
|
|
1764
|
+
const event = createTaskEvent(store.nextEventId(input.taskId), input.taskId, terminalTurn.status === "completed" ? "turn.completed" : "turn.failed", {
|
|
1765
|
+
turnId: terminalTurn.id,
|
|
1766
|
+
roleName: terminalTurn.roleName,
|
|
1767
|
+
providerTurnId: canonicalTurnId,
|
|
1768
|
+
providerStatus,
|
|
1769
|
+
summary: input.summary
|
|
1770
|
+
}, now);
|
|
1771
|
+
store.saveEvent(input.taskId, event);
|
|
1772
|
+
if (terminalTurn.roleName !== "leader"
|
|
1773
|
+
&& (providerStatus !== "completed" || !providerGoalContinues(binding.goal))) {
|
|
1774
|
+
routeRoleEvent(store, event, terminalTurn.roleName, terminalTurn.purpose === "review" ? "review-result" : "role-turn-result", now);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1896
1777
|
return {
|
|
1897
1778
|
session: sessions.sessions[input.agentId],
|
|
1898
|
-
duplicate: false
|
|
1779
|
+
duplicate: false,
|
|
1780
|
+
...(terminalTurn === undefined ? {} : { turn: terminalTurn })
|
|
1899
1781
|
};
|
|
1900
1782
|
});
|
|
1901
1783
|
}
|
|
@@ -1904,7 +1786,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1904
1786
|
const identity = [
|
|
1905
1787
|
input.taskId,
|
|
1906
1788
|
input.roleName,
|
|
1907
|
-
input.
|
|
1789
|
+
input.turnId,
|
|
1908
1790
|
input.launchId ?? "unknown-launch",
|
|
1909
1791
|
String(input.deadStatus ?? "unknown-status")
|
|
1910
1792
|
].join("\0");
|
|
@@ -1925,8 +1807,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
1925
1807
|
hostInstanceId: `tmux-${input.launchId ?? input.roleName}`,
|
|
1926
1808
|
taskId: input.taskId,
|
|
1927
1809
|
roleName: input.roleName,
|
|
1928
|
-
|
|
1929
|
-
launchId: input.launchId ?? `unknown-${input.
|
|
1810
|
+
turnId: input.turnId,
|
|
1811
|
+
launchId: input.launchId ?? `unknown-${input.turnId}`,
|
|
1930
1812
|
...(input.nativeSessionId === undefined
|
|
1931
1813
|
? {}
|
|
1932
1814
|
: { nativeSessionId: input.nativeSessionId }),
|
|
@@ -1948,7 +1830,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1948
1830
|
});
|
|
1949
1831
|
}
|
|
1950
1832
|
/**
|
|
1951
|
-
* Issue 04: reopens each due retry on its original Native Session. A
|
|
1833
|
+
* Issue 04: reopens each due retry on its original Native Session. A Turn
|
|
1952
1834
|
* whose Session is proven dead terminalizes with an exact replacement
|
|
1953
1835
|
* blocker; a live Session is reopened for the existing delivery path, which
|
|
1954
1836
|
* re-pushes the exact same input in the same pass.
|
|
@@ -1957,8 +1839,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
1957
1839
|
return this.store.transaction((store) => {
|
|
1958
1840
|
const taskId = input.fence.taskId;
|
|
1959
1841
|
const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
|
|
1960
|
-
|
|
1961
|
-
if (sessions === null || sessions.providerBinding === null || run === null
|
|
1842
|
+
if (sessions === null || sessions.providerBinding === null
|
|
1962
1843
|
|| input.fence.conversationId === undefined) {
|
|
1963
1844
|
recordCanonicalObservationObsolete(store, input, "provider-binding-missing", now);
|
|
1964
1845
|
return "obsolete";
|
|
@@ -1997,6 +1878,33 @@ export class FileSchedulerStoreAdapter {
|
|
|
1997
1878
|
return "applied";
|
|
1998
1879
|
});
|
|
1999
1880
|
}
|
|
1881
|
+
observeProviderGoal(input, now) {
|
|
1882
|
+
return this.store.transaction((store) => {
|
|
1883
|
+
const taskId = input.fence.taskId;
|
|
1884
|
+
const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
|
|
1885
|
+
const binding = sessions?.providerBinding;
|
|
1886
|
+
if (sessions === null || sessions === undefined || binding === null || binding === undefined
|
|
1887
|
+
|| input.fence.conversationId !== currentProviderConversation(binding).conversationId) {
|
|
1888
|
+
recordCanonicalObservationObsolete(store, input, "provider-goal-session-mismatch", now);
|
|
1889
|
+
return "obsolete";
|
|
1890
|
+
}
|
|
1891
|
+
const updated = input.kind === "goal.cleared"
|
|
1892
|
+
? clearProviderGoal(binding)
|
|
1893
|
+
: updateProviderGoal(binding, {
|
|
1894
|
+
status: input.payload.goalStatus,
|
|
1895
|
+
objective: input.payload.goalObjective,
|
|
1896
|
+
updatedAt: input.payload.goalUpdatedAt,
|
|
1897
|
+
...(input.payload.goalNativeTurnId === undefined
|
|
1898
|
+
? {}
|
|
1899
|
+
: { nativeTurnId: input.payload.goalNativeTurnId }),
|
|
1900
|
+
...(input.payload.goalTokenBudget === undefined
|
|
1901
|
+
? {}
|
|
1902
|
+
: { tokenBudget: input.payload.goalTokenBudget })
|
|
1903
|
+
});
|
|
1904
|
+
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, now));
|
|
1905
|
+
return "applied";
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
2000
1908
|
observeRuntimeSession(input, adapterId, now) {
|
|
2001
1909
|
return this.store.transaction((store) => {
|
|
2002
1910
|
const event = createCanonicalLifecycleEvent({
|
|
@@ -2032,7 +1940,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
2032
1940
|
const taskId = input.fence.taskId;
|
|
2033
1941
|
const role = store.getRole(taskId, input.fence.roleName);
|
|
2034
1942
|
const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
|
|
2035
|
-
const run = store.
|
|
1943
|
+
const run = store.getTurn(taskId, input.fence.turnId);
|
|
2036
1944
|
if (role === null || sessions === null || run === null) {
|
|
2037
1945
|
recordCanonicalObservationObsolete(store, input, "bind-state-missing", now);
|
|
2038
1946
|
return "obsolete";
|
|
@@ -2069,20 +1977,11 @@ export class FileSchedulerStoreAdapter {
|
|
|
2069
1977
|
: replaceTaskRoleAgentSession(sessions, sessionInput, now);
|
|
2070
1978
|
const withProvider = bindOrSupersedeProviderRuntime(bound, input, now, replacementBasis ?? undefined);
|
|
2071
1979
|
store.saveTaskRoleSessionSet(withProvider);
|
|
2072
|
-
const driver = this.drivers.require(input.fence.driverId);
|
|
2073
|
-
if (driver.capabilities.observation.sessionBootstrap === "discovered"
|
|
2074
|
-
&& run.pushedAt === undefined) {
|
|
2075
|
-
const pushed = markAgentRunPushed(run, now);
|
|
2076
|
-
store.saveAgentRun(pushed);
|
|
2077
|
-
markTaskRoleRunPushedInFlight(store, role, pushed, now);
|
|
2078
|
-
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "run.pushed", runLaunchEventPayload(pushed), now));
|
|
2079
|
-
}
|
|
2080
1980
|
completeRuntimeHookReservation(store, { scope: "task", taskId, roleName: input.fence.roleName }, input.fence.launchId);
|
|
2081
1981
|
}
|
|
2082
1982
|
const current = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
|
|
2083
1983
|
const currentSession = current?.sessions[input.fence.agentId];
|
|
2084
1984
|
if (current !== null && current !== undefined
|
|
2085
|
-
&& current.inFlight !== null
|
|
2086
1985
|
&& currentSession?.nativeSessionId === input.fence.nativeSessionId) {
|
|
2087
1986
|
store.saveTaskRoleSessionSet(bindOrSupersedeProviderRuntime(current, input, now));
|
|
2088
1987
|
}
|
|
@@ -2094,72 +1993,104 @@ export class FileSchedulerStoreAdapter {
|
|
|
2094
1993
|
/** Provider acceptance is canonical before storage and remains receipt-fenced. */
|
|
2095
1994
|
observeRuntimePromptAccepted(input, adapterId, now) {
|
|
2096
1995
|
return this.store.transaction((store) => {
|
|
2097
|
-
const
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
const processing = mailbox?.processing;
|
|
2104
|
-
const inputDelivery = mailbox?.inputDelivery;
|
|
2105
|
-
const continuation = inputDelivery !== null
|
|
2106
|
-
&& inputDelivery !== undefined
|
|
2107
|
-
&& inputDelivery.attemptId === input.fence.receiptId
|
|
2108
|
-
&& inputDelivery.executionRef.type === "run"
|
|
2109
|
-
&& inputDelivery.executionRef.taskId === input.fence.taskId
|
|
2110
|
-
&& inputDelivery.executionRef.id === input.fence.runId
|
|
2111
|
-
&& inputDelivery.attemptId !== formatAgentRunReceiptId(input.fence.taskId, input.fence.runId);
|
|
2112
|
-
const humanAttemptId = input.fence.receiptId;
|
|
2113
|
-
if (humanAttemptId !== undefined && humanAttemptId.startsWith("human:")) {
|
|
2114
|
-
const active = store.getActiveAgentRun(input.fence.taskId, input.fence.roleName);
|
|
2115
|
-
const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
|
|
1996
|
+
const continuation = input.fence.receiptId?.startsWith("turn-input:") === true;
|
|
1997
|
+
const ordinaryAttemptId = input.fence.receiptId;
|
|
1998
|
+
if (input.fence.turnId === undefined
|
|
1999
|
+
&& ordinaryAttemptId?.startsWith("direct:") === true) {
|
|
2000
|
+
const taskId = input.fence.taskId;
|
|
2001
|
+
const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
|
|
2116
2002
|
const session = sessions?.sessions[input.fence.agentId];
|
|
2117
2003
|
const binding = sessions?.providerBinding;
|
|
2118
|
-
|
|
2119
|
-
|
|
2004
|
+
const nativeTurnId = input.fence.nativeTurnId;
|
|
2005
|
+
const active = store.getActiveTurn(taskId, input.fence.roleName);
|
|
2006
|
+
if (sessions === null || sessions === undefined
|
|
2120
2007
|
|| binding === null || binding === undefined
|
|
2121
|
-
|| binding.authority.owner !== "human"
|
|
2122
|
-
|| !humanAttemptId.startsWith(`human:${binding.authority.holderId}:`)
|
|
2123
|
-
|| binding.turn?.attemptId !== humanAttemptId
|
|
2124
2008
|
|| session?.launchId !== input.fence.launchId
|
|
2125
|
-
|| session.nativeSessionId !== input.fence.nativeSessionId
|
|
2126
|
-
|
|
2009
|
+
|| session.nativeSessionId !== input.fence.nativeSessionId
|
|
2010
|
+
|| currentProviderConversation(binding).conversationId !== input.fence.nativeSessionId) {
|
|
2011
|
+
recordCanonicalObservationObsolete(store, input, "direct-turn-session-mismatch", now);
|
|
2127
2012
|
return "obsolete";
|
|
2128
2013
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2014
|
+
if (binding.turn?.attemptId === ordinaryAttemptId
|
|
2015
|
+
&& binding.turn.nativeTurnId === nativeTurnId
|
|
2016
|
+
&& binding.turn.turnId !== undefined
|
|
2017
|
+
&& active?.id === binding.turn.turnId) {
|
|
2018
|
+
return "applied";
|
|
2019
|
+
}
|
|
2020
|
+
if (active !== null) {
|
|
2021
|
+
recordCanonicalObservationObsolete(store, input, "direct-turn-conflicts-with-active-turn", now);
|
|
2022
|
+
return "obsolete";
|
|
2023
|
+
}
|
|
2024
|
+
const turnId = store.nextTurnId(taskId);
|
|
2025
|
+
const goalContinuation = input.payload.input === undefined
|
|
2026
|
+
&& providerGoalContinues(binding.goal);
|
|
2027
|
+
const direct = createTurn(turnId, taskId, input.fence.roleName, "resume", createTurnInput({
|
|
2028
|
+
source: goalContinuation
|
|
2029
|
+
? { type: "provider", channel: "goal-continuation" }
|
|
2030
|
+
: { type: "user", channel: "direct" },
|
|
2031
|
+
...(input.payload.input === undefined
|
|
2032
|
+
? {}
|
|
2033
|
+
: { directive: input.payload.input }),
|
|
2034
|
+
deltaRefIds: []
|
|
2035
|
+
}), now, { effective: session.effective });
|
|
2036
|
+
const submittedAt = input.observedAt ?? input.receivedAt;
|
|
2037
|
+
const accepted = acceptProviderTurn(beginProviderTurn(binding, {
|
|
2038
|
+
turnId,
|
|
2039
|
+
attemptId: ordinaryAttemptId,
|
|
2040
|
+
authorityEpoch: binding.authority.epoch,
|
|
2041
|
+
submittedAt
|
|
2042
|
+
}), {
|
|
2043
|
+
attemptId: ordinaryAttemptId,
|
|
2044
|
+
nativeTurnId,
|
|
2045
|
+
acceptedAt: submittedAt
|
|
2046
|
+
});
|
|
2047
|
+
store.saveTurn(direct);
|
|
2048
|
+
store.saveActiveTurn(direct);
|
|
2049
|
+
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, accepted, now));
|
|
2050
|
+
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "turn.dispatched", turnLaunchEventPayload(direct), now));
|
|
2136
2051
|
return "applied";
|
|
2137
2052
|
}
|
|
2053
|
+
if (ordinaryAttemptId !== undefined) {
|
|
2054
|
+
const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
|
|
2055
|
+
const session = sessions?.sessions[input.fence.agentId];
|
|
2056
|
+
const binding = sessions?.providerBinding;
|
|
2057
|
+
if (sessions !== null && sessions !== undefined
|
|
2058
|
+
&& binding !== null && binding !== undefined
|
|
2059
|
+
&& binding.turn?.turnId === undefined
|
|
2060
|
+
&& binding.turn?.attemptId === ordinaryAttemptId) {
|
|
2061
|
+
if (session?.launchId !== input.fence.launchId
|
|
2062
|
+
|| session.nativeSessionId !== input.fence.nativeSessionId) {
|
|
2063
|
+
recordCanonicalObservationObsolete(store, input, "ordinary-turn-session-mismatch", now);
|
|
2064
|
+
return "obsolete";
|
|
2065
|
+
}
|
|
2066
|
+
store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(sessions, input, now));
|
|
2067
|
+
return "applied";
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2138
2070
|
if (continuation) {
|
|
2139
|
-
const active = store.
|
|
2071
|
+
const active = store.getActiveTurn(input.fence.taskId, input.fence.roleName);
|
|
2140
2072
|
const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
|
|
2141
2073
|
const session = sessions?.sessions[input.fence.agentId];
|
|
2142
2074
|
if (active === null
|
|
2143
|
-
|| active.id !== input.fence.
|
|
2075
|
+
|| active.id !== input.fence.turnId
|
|
2144
2076
|
|| active.status !== "active"
|
|
2145
|
-
||
|
|
2146
|
-
|| sessions
|
|
2077
|
+
|| sessions === null
|
|
2078
|
+
|| sessions === undefined
|
|
2147
2079
|
|| session?.launchId !== input.fence.launchId
|
|
2148
2080
|
|| session.nativeSessionId !== input.fence.nativeSessionId) {
|
|
2149
2081
|
recordCanonicalObservationObsolete(store, input, "continuation-fence-mismatch", now);
|
|
2150
2082
|
return "obsolete";
|
|
2151
2083
|
}
|
|
2152
2084
|
store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(sessions, input, now));
|
|
2153
|
-
store.saveEvent(input.fence.taskId, createTaskEvent(store.nextEventId(input.fence.taskId), input.fence.taskId, "
|
|
2154
|
-
attemptId:
|
|
2155
|
-
|
|
2085
|
+
store.saveEvent(input.fence.taskId, createTaskEvent(store.nextEventId(input.fence.taskId), input.fence.taskId, "turn.input-delivered", {
|
|
2086
|
+
attemptId: input.fence.receiptId,
|
|
2087
|
+
turnId: active.id,
|
|
2156
2088
|
conversationId: input.fence.conversationId ?? input.fence.nativeSessionId,
|
|
2157
2089
|
activationId: input.fence.activationId ?? input.fence.launchId,
|
|
2158
2090
|
...(input.fence.nativeTurnId === undefined
|
|
2159
2091
|
? {}
|
|
2160
2092
|
: { nativeTurnId: input.fence.nativeTurnId })
|
|
2161
2093
|
}, now));
|
|
2162
|
-
store.saveWorkMailbox(completeMailboxInputDelivery(mailbox, inputDelivery.attemptId, now));
|
|
2163
2094
|
return "applied";
|
|
2164
2095
|
}
|
|
2165
2096
|
const event = createCanonicalLifecycleEvent({
|
|
@@ -2177,44 +2108,26 @@ export class FileSchedulerStoreAdapter {
|
|
|
2177
2108
|
return "deferred";
|
|
2178
2109
|
if (decision.kind === "idempotent")
|
|
2179
2110
|
return "applied";
|
|
2180
|
-
const active = store.
|
|
2111
|
+
const active = store.getActiveTurn(input.fence.taskId, input.fence.roleName);
|
|
2181
2112
|
if (active === null
|
|
2182
|
-
|| active.id !== input.fence.
|
|
2113
|
+
|| active.id !== input.fence.turnId
|
|
2183
2114
|
|| active.status !== "active") {
|
|
2184
|
-
recordCanonicalObservationObsolete(store, input, "
|
|
2185
|
-
return "obsolete";
|
|
2186
|
-
}
|
|
2187
|
-
const role = store.getRole(input.fence.taskId, input.fence.roleName);
|
|
2188
|
-
if (role === null) {
|
|
2189
|
-
recordCanonicalObservationObsolete(store, input, "role-missing", now);
|
|
2115
|
+
recordCanonicalObservationObsolete(store, input, "turn-not-active", now);
|
|
2190
2116
|
return "obsolete";
|
|
2191
2117
|
}
|
|
2192
2118
|
const sessions = store.getTaskRoleSessionSet(input.fence.taskId, input.fence.roleName);
|
|
2193
2119
|
if (sessions !== null) {
|
|
2194
2120
|
store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(sessions, input, now));
|
|
2195
2121
|
}
|
|
2196
|
-
if (active.deliveredAt === undefined) {
|
|
2197
|
-
const delivered = markAgentRunDelivered(active, now);
|
|
2198
|
-
store.saveAgentRun(delivered);
|
|
2199
|
-
markTaskRoleRunDeliveredInFlight(store, role, delivered, now);
|
|
2200
|
-
store.saveEvent(input.fence.taskId, createTaskEvent(store.nextEventId(input.fence.taskId), input.fence.taskId, "run.delivered", runLaunchEventPayload(delivered), now));
|
|
2201
|
-
}
|
|
2202
|
-
if (processing !== null && processing !== undefined
|
|
2203
|
-
&& processing.batchId === input.fence.receiptId
|
|
2204
|
-
&& processing.executionRef?.type === "run"
|
|
2205
|
-
&& processing.executionRef.taskId === input.fence.taskId
|
|
2206
|
-
&& processing.executionRef.id === input.fence.runId) {
|
|
2207
|
-
store.saveWorkMailbox(completeProcessing(mailbox, processing.batchId));
|
|
2208
|
-
}
|
|
2209
2122
|
return "applied";
|
|
2210
2123
|
});
|
|
2211
2124
|
}
|
|
2212
2125
|
foldRuntimeLifecycleEvent(store, event, observation) {
|
|
2213
|
-
const expectation = this.
|
|
2126
|
+
const expectation = this.projectTurnExpectation(store, event.fence, observation.fence.turnId);
|
|
2214
2127
|
if (expectation === null) {
|
|
2215
2128
|
return preallocatedRuntimeReadyAwaitingProjection(store, observation, this.drivers)
|
|
2216
2129
|
? { kind: "apply", outcome: { outcome: "mark-ready", preInputReady: true } }
|
|
2217
|
-
: { kind: "obsolete", reason: "
|
|
2130
|
+
: { kind: "obsolete", reason: "turn-or-role-missing" };
|
|
2218
2131
|
}
|
|
2219
2132
|
const outcome = foldCanonicalLifecycleEvent(event, expectation);
|
|
2220
2133
|
switch (outcome.outcome) {
|
|
@@ -2230,14 +2143,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
2230
2143
|
return { kind: "apply", outcome };
|
|
2231
2144
|
}
|
|
2232
2145
|
}
|
|
2233
|
-
/** Reads durable
|
|
2234
|
-
|
|
2146
|
+
/** Reads durable Turn + Session state into the pure fold's expectation shape. */
|
|
2147
|
+
projectTurnExpectation(store, fence, turnId) {
|
|
2235
2148
|
const role = store.getRole(fence.taskId, fence.roleName);
|
|
2236
2149
|
if (role === null)
|
|
2237
2150
|
return null;
|
|
2238
2151
|
const sessionSet = store.getTaskRoleSessionSet(fence.taskId, fence.roleName);
|
|
2239
2152
|
const session = sessionSet?.sessions[fence.agentId] ?? null;
|
|
2240
|
-
if (
|
|
2153
|
+
if (turnId === undefined) {
|
|
2241
2154
|
return {
|
|
2242
2155
|
fence,
|
|
2243
2156
|
sessionStarted: false,
|
|
@@ -2250,13 +2163,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
2250
2163
|
: { boundNativeSessionId: session.nativeSessionId })
|
|
2251
2164
|
};
|
|
2252
2165
|
}
|
|
2253
|
-
const run = store.
|
|
2254
|
-
|
|
2255
|
-
if (run === null
|
|
2256
|
-
|| inFlight === null
|
|
2257
|
-
|| inFlight === undefined
|
|
2258
|
-
|| inFlight.runId !== run.id
|
|
2259
|
-
|| inFlight.agentId !== run.effective.agentId)
|
|
2166
|
+
const run = store.getTurn(fence.taskId, turnId);
|
|
2167
|
+
if (run === null || run.status !== "active")
|
|
2260
2168
|
return null;
|
|
2261
2169
|
const owner = { scope: "task", taskId: fence.taskId, roleName: fence.roleName };
|
|
2262
2170
|
const freshConversationLaunch = run.mode === "new"
|
|
@@ -2270,14 +2178,18 @@ export class FileSchedulerStoreAdapter {
|
|
|
2270
2178
|
?? (runtimeHookMatchesReservation(store, owner, fence.launchId) ? fence.launchId : undefined);
|
|
2271
2179
|
if (launchId === undefined)
|
|
2272
2180
|
return null;
|
|
2181
|
+
const providerTurn = sessionSet?.providerBinding?.turn;
|
|
2182
|
+
const managedTurnMatches = managedProviderTurnId(providerTurn) === run.id;
|
|
2273
2183
|
const expectedFence = {
|
|
2274
2184
|
taskId: fence.taskId,
|
|
2275
2185
|
roleName: fence.roleName,
|
|
2276
2186
|
agentId: run.effective.agentId,
|
|
2277
2187
|
adapterId: run.effective.adapterId,
|
|
2278
|
-
|
|
2188
|
+
turnId: run.id,
|
|
2279
2189
|
launchId,
|
|
2280
|
-
receiptId:
|
|
2190
|
+
receiptId: managedTurnMatches && providerTurn !== null && providerTurn !== undefined
|
|
2191
|
+
? providerTurn.attemptId
|
|
2192
|
+
: formatTurnReceiptId(run.taskId, run.id),
|
|
2281
2193
|
...(boundNativeSessionId === undefined ? {} : { nativeSessionId: boundNativeSessionId })
|
|
2282
2194
|
};
|
|
2283
2195
|
const driverId = this.drivers.requireByAdapterId(run.effective.adapterId).id;
|
|
@@ -2297,8 +2209,10 @@ export class FileSchedulerStoreAdapter {
|
|
|
2297
2209
|
fence: expectedFence,
|
|
2298
2210
|
sessionStarted: lifecycleEvents.length > 0,
|
|
2299
2211
|
ready: lifecycleEvents.some((event) => event.kind === "session.ready"),
|
|
2300
|
-
pushed:
|
|
2301
|
-
accepted:
|
|
2212
|
+
pushed: managedTurnMatches,
|
|
2213
|
+
accepted: managedTurnMatches && providerTurn !== null && providerTurn !== undefined
|
|
2214
|
+
&& ["accepted", "completed", "failed", "cancelled"]
|
|
2215
|
+
.includes(providerTurn.status),
|
|
2302
2216
|
terminal: run.status !== "active",
|
|
2303
2217
|
...(boundNativeSessionId === undefined ? {} : { boundNativeSessionId })
|
|
2304
2218
|
};
|
|
@@ -2350,7 +2264,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
2350
2264
|
: { endReason: effectiveExisting.endReason }),
|
|
2351
2265
|
effective
|
|
2352
2266
|
}, now);
|
|
2353
|
-
current = rememberRoleAgentCompletedTurn(current, input.agentId, nativeSessionId, input.
|
|
2267
|
+
current = rememberRoleAgentCompletedTurn(current, input.agentId, nativeSessionId, input.nativeTurnId, now);
|
|
2354
2268
|
store.saveGlobalRoleSessionSet(current);
|
|
2355
2269
|
completeRuntimeHookReservation(store, owner, input.launchId);
|
|
2356
2270
|
if (input.roleName === SYSTEM_OPERATOR_ROLE
|
|
@@ -2402,26 +2316,26 @@ function runtimeObservationLifecycleFence(input, adapterId) {
|
|
|
2402
2316
|
agentId: input.fence.agentId,
|
|
2403
2317
|
adapterId,
|
|
2404
2318
|
launchId: input.fence.launchId,
|
|
2405
|
-
...(input.fence.
|
|
2319
|
+
...(input.fence.turnId === undefined ? {} : { turnId: input.fence.turnId }),
|
|
2406
2320
|
...(input.fence.nativeSessionId === undefined
|
|
2407
2321
|
? {}
|
|
2408
2322
|
: { nativeSessionId: input.fence.nativeSessionId }),
|
|
2409
2323
|
...(input.fence.receiptId === undefined ? {} : { receiptId: input.fence.receiptId })
|
|
2410
2324
|
};
|
|
2411
2325
|
}
|
|
2412
|
-
/** Accepts the original
|
|
2413
|
-
function
|
|
2326
|
+
/** Accepts the original Turn receipt or a later mailbox activation receipt. */
|
|
2327
|
+
function runtimeReceiptBelongsToTurn(store, input) {
|
|
2414
2328
|
const taskId = input.fence.taskId;
|
|
2415
|
-
const
|
|
2329
|
+
const turnId = input.fence.turnId;
|
|
2416
2330
|
const receiptId = input.fence.receiptId;
|
|
2417
|
-
if (receiptId ===
|
|
2331
|
+
if (receiptId === formatTurnReceiptId(taskId, turnId))
|
|
2418
2332
|
return true;
|
|
2419
2333
|
if (receiptId === undefined)
|
|
2420
2334
|
return false;
|
|
2421
2335
|
return store.listEvents(taskId).some((event) => {
|
|
2422
2336
|
const accepted = runtimeObservationFromTaskEvent(event);
|
|
2423
2337
|
return accepted?.kind === "turn.accepted"
|
|
2424
|
-
&& accepted.fence.
|
|
2338
|
+
&& accepted.fence.turnId === turnId
|
|
2425
2339
|
&& accepted.fence.roleName === input.fence.roleName
|
|
2426
2340
|
&& accepted.fence.agentId === input.fence.agentId
|
|
2427
2341
|
&& accepted.fence.launchId === input.fence.launchId
|
|
@@ -2440,7 +2354,7 @@ function recordCanonicalObservationObsolete(store, input, reason, now) {
|
|
|
2440
2354
|
taskId,
|
|
2441
2355
|
roleName: input.fence.roleName,
|
|
2442
2356
|
agentId: input.fence.agentId,
|
|
2443
|
-
...(input.fence.
|
|
2357
|
+
...(input.fence.turnId === undefined ? {} : { turnId: input.fence.turnId }),
|
|
2444
2358
|
launchId: input.fence.launchId,
|
|
2445
2359
|
nativeSessionId: input.fence.nativeSessionId ?? input.fence.sessionGenerationId
|
|
2446
2360
|
}, reason, now);
|
|
@@ -2456,15 +2370,15 @@ function recordObsoleteRuntimeEvent(store, input, reason, now) {
|
|
|
2456
2370
|
roleName: input.roleName,
|
|
2457
2371
|
agentId: input.agentId,
|
|
2458
2372
|
nativeSessionId: input.nativeSessionId,
|
|
2459
|
-
...(input.
|
|
2373
|
+
...(input.turnId === undefined ? {} : { turnId: input.turnId }),
|
|
2460
2374
|
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
2461
2375
|
reason
|
|
2462
2376
|
}, now));
|
|
2463
2377
|
}
|
|
2464
|
-
function
|
|
2465
|
-
if (input.
|
|
2378
|
+
function isLeaderDisposedWorkItemTurn(store, input) {
|
|
2379
|
+
if (input.turnId === undefined)
|
|
2466
2380
|
return false;
|
|
2467
|
-
const run = store.
|
|
2381
|
+
const run = store.getTurn(input.taskId, input.turnId);
|
|
2468
2382
|
if (run === null
|
|
2469
2383
|
|| run.status !== "failed"
|
|
2470
2384
|
|| run.roleName !== input.roleName
|
|
@@ -2474,12 +2388,12 @@ function isLeaderDisposedWorkItemRun(store, input) {
|
|
|
2474
2388
|
}
|
|
2475
2389
|
return store.getWorkItem(input.taskId, run.workItemId)?.disposition !== undefined;
|
|
2476
2390
|
}
|
|
2477
|
-
function
|
|
2478
|
-
if (
|
|
2391
|
+
function isObsoleteTerminalRuntimeTurn(store, input) {
|
|
2392
|
+
if (isLeaderDisposedWorkItemTurn(store, input))
|
|
2479
2393
|
return true;
|
|
2480
|
-
if (input.
|
|
2394
|
+
if (input.turnId === undefined)
|
|
2481
2395
|
return false;
|
|
2482
|
-
const run = store.
|
|
2396
|
+
const run = store.getTurn(input.taskId, input.turnId);
|
|
2483
2397
|
if (run === null
|
|
2484
2398
|
|| run.status !== "failed"
|
|
2485
2399
|
|| run.roleName !== input.roleName
|
|
@@ -2487,7 +2401,7 @@ function isObsoleteTerminalRuntimeRun(store, input) {
|
|
|
2487
2401
|
return false;
|
|
2488
2402
|
}
|
|
2489
2403
|
return store.listEvents(input.taskId).some((event) => (event.type === "runtime.role-delivery-failed"
|
|
2490
|
-
&& event.payload.
|
|
2404
|
+
&& event.payload.turnId === input.turnId));
|
|
2491
2405
|
}
|
|
2492
2406
|
function sessionPreview(value) {
|
|
2493
2407
|
const normalized = value.trim().replaceAll(/\s+/g, " ");
|
|
@@ -2513,28 +2427,27 @@ function runtimeHookMatchesReservation(store, owner, launchId) {
|
|
|
2513
2427
|
}
|
|
2514
2428
|
function preallocatedRuntimeReadyAwaitingProjection(store, observation, drivers) {
|
|
2515
2429
|
const taskId = observation.fence.taskId;
|
|
2516
|
-
const
|
|
2430
|
+
const turnId = observation.fence.turnId;
|
|
2517
2431
|
const nativeSessionId = observation.fence.nativeSessionId;
|
|
2518
2432
|
const driver = drivers.find(observation.fence.driverId);
|
|
2519
2433
|
if (observation.kind !== "session.ready"
|
|
2520
2434
|
|| driver?.capabilities.observation.sessionBootstrap !== "preallocated"
|
|
2521
2435
|
|| taskId === undefined
|
|
2522
|
-
||
|
|
2436
|
+
|| turnId === undefined
|
|
2523
2437
|
|| nativeSessionId === undefined)
|
|
2524
2438
|
return false;
|
|
2525
2439
|
const task = store.getTask(taskId);
|
|
2526
2440
|
const role = store.getRole(taskId, observation.fence.roleName);
|
|
2527
|
-
const run = store.
|
|
2441
|
+
const run = store.getActiveTurn(taskId, observation.fence.roleName);
|
|
2528
2442
|
const sessions = store.getTaskRoleSessionSet(taskId, observation.fence.roleName);
|
|
2529
2443
|
if (task?.status !== "active"
|
|
2530
2444
|
|| task.executionGate.state !== "enabled"
|
|
2531
2445
|
|| role?.activeAgentId !== observation.fence.agentId
|
|
2532
|
-
|| run?.id !==
|
|
2446
|
+
|| run?.id !== turnId
|
|
2533
2447
|
|| run.status !== "active"
|
|
2534
2448
|
|| run.effective.agentId !== observation.fence.agentId
|
|
2535
2449
|
|| run.effective.adapterId !== driver.adapterId
|
|
2536
|
-
|| observation.fence.receiptId !==
|
|
2537
|
-
|| (sessions?.inFlight !== null && sessions?.inFlight !== undefined)
|
|
2450
|
+
|| observation.fence.receiptId !== formatTurnReceiptId(run.taskId, run.id)
|
|
2538
2451
|
|| sessions?.sessions[observation.fence.agentId] !== undefined)
|
|
2539
2452
|
return false;
|
|
2540
2453
|
const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
|
|
@@ -2586,7 +2499,7 @@ function dormantRuntimeCandidateIsCurrent(store, candidate) {
|
|
|
2586
2499
|
return false;
|
|
2587
2500
|
}
|
|
2588
2501
|
if (owner.scope === "task"
|
|
2589
|
-
&& store.
|
|
2502
|
+
&& store.getActiveTurn(owner.taskId, owner.roleName) !== null) {
|
|
2590
2503
|
return false;
|
|
2591
2504
|
}
|
|
2592
2505
|
const sessions = runtimeOwnerSessionSet(store, owner);
|
|
@@ -2666,34 +2579,6 @@ function runtimeSessionMatchesSettledLaunch(session, input) {
|
|
|
2666
2579
|
&& (input.nativeSessionId === undefined
|
|
2667
2580
|
|| session.nativeSessionId === input.nativeSessionId);
|
|
2668
2581
|
}
|
|
2669
|
-
function preparedSessionMatches(session, agentId, adapterId, nativeSessionId, launchId) {
|
|
2670
|
-
if (session === undefined)
|
|
2671
|
-
return nativeSessionId === undefined;
|
|
2672
|
-
if (session.agentId !== agentId
|
|
2673
|
-
|| session.adapterId !== adapterId
|
|
2674
|
-
|| session.status === "ended"
|
|
2675
|
-
|| (nativeSessionId !== undefined
|
|
2676
|
-
&& session.nativeSessionId !== nativeSessionId)) {
|
|
2677
|
-
return false;
|
|
2678
|
-
}
|
|
2679
|
-
return launchId === undefined
|
|
2680
|
-
? session.launchId === undefined
|
|
2681
|
-
: session.launchId === launchId;
|
|
2682
|
-
}
|
|
2683
|
-
function preparedDeliveryFailureSessionFenceMatches(sessions, session, input, expectedReceiptId) {
|
|
2684
|
-
if (sessions === null)
|
|
2685
|
-
return input.nativeSessionId === undefined;
|
|
2686
|
-
if (sessions.activeAgentId !== input.agentId)
|
|
2687
|
-
return false;
|
|
2688
|
-
if (sessions.inFlight === null) {
|
|
2689
|
-
return preparedSessionMatches(session, input.agentId, input.adapterId, input.nativeSessionId, input.launchId);
|
|
2690
|
-
}
|
|
2691
|
-
return sessions.inFlight.agentId === input.agentId
|
|
2692
|
-
&& sessions.inFlight.runId === input.runId
|
|
2693
|
-
&& sessions.inFlight.receiptId === expectedReceiptId
|
|
2694
|
-
&& sessions.inFlight.pushedAt === undefined
|
|
2695
|
-
&& preparedSessionMatches(session, input.agentId, input.adapterId, input.nativeSessionId, input.launchId);
|
|
2696
|
-
}
|
|
2697
2582
|
function recordTaskRuntimeNativeSession(store, input, now) {
|
|
2698
2583
|
const task = store.getTask(input.taskId);
|
|
2699
2584
|
if (task === null)
|
|
@@ -2826,14 +2711,14 @@ function mapRole(store, role) {
|
|
|
2826
2711
|
};
|
|
2827
2712
|
}
|
|
2828
2713
|
function taskSessionEffective(store, taskId, roleName, agentId, existing) {
|
|
2829
|
-
const active = store.
|
|
2714
|
+
const active = store.getActiveTurn(taskId, roleName);
|
|
2830
2715
|
if (active !== null) {
|
|
2831
2716
|
if (active.effective.agentId !== agentId) {
|
|
2832
|
-
throw new Error(`Native Session registration does not match the effective
|
|
2717
|
+
throw new Error(`Native Session registration does not match the effective Turn Agent: ${taskId}/${roleName}.`);
|
|
2833
2718
|
}
|
|
2834
2719
|
if (existing !== undefined) {
|
|
2835
2720
|
if (!effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, active.effective)) {
|
|
2836
|
-
throw new Error(`Native Session effective launch does not match the active
|
|
2721
|
+
throw new Error(`Native Session effective launch does not match the active Turn: ${taskId}/${roleName}.`);
|
|
2837
2722
|
}
|
|
2838
2723
|
return existing.effective;
|
|
2839
2724
|
}
|
|
@@ -2896,82 +2781,6 @@ function saveTaskSession(store, role, session, status, now, launchId) {
|
|
|
2896
2781
|
}, now);
|
|
2897
2782
|
store.saveRoleSessionSet(updated);
|
|
2898
2783
|
}
|
|
2899
|
-
function bindTaskRoleRunInFlight(store, role, run, now) {
|
|
2900
|
-
const agentId = run.effective.agentId;
|
|
2901
|
-
let current = store.getRoleSessionSet(role.taskId, role.name)
|
|
2902
|
-
?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name }, agentId, now);
|
|
2903
|
-
if (current.activeAgentId !== agentId) {
|
|
2904
|
-
if (current.inFlight !== null || current.providerBinding !== null) {
|
|
2905
|
-
throw new Error("Task Role Session identity cannot change with an unsettled Run fence.");
|
|
2906
|
-
}
|
|
2907
|
-
const liveConflict = Object.values(current.sessions).find((session) => (session.agentId !== agentId
|
|
2908
|
-
&& session.status === "active"));
|
|
2909
|
-
if (liveConflict !== undefined) {
|
|
2910
|
-
throw new Error(`Task Role Session identity cannot change while ${liveConflict.agentId} is live.`);
|
|
2911
|
-
}
|
|
2912
|
-
current = {
|
|
2913
|
-
...current,
|
|
2914
|
-
activeAgentId: agentId,
|
|
2915
|
-
updatedAt: now.toISOString()
|
|
2916
|
-
};
|
|
2917
|
-
}
|
|
2918
|
-
const updated = bindTaskRoleRun(current, {
|
|
2919
|
-
agentId,
|
|
2920
|
-
runId: run.id,
|
|
2921
|
-
receiptId: agentRunDeliveryReceiptId(run)
|
|
2922
|
-
}, now, run.mode);
|
|
2923
|
-
store.saveRoleSessionSet(updated);
|
|
2924
|
-
}
|
|
2925
|
-
function markTaskRoleRunPushedInFlight(store, role, run, now) {
|
|
2926
|
-
const current = store.getRoleSessionSet(role.taskId, role.name);
|
|
2927
|
-
if (current === null) {
|
|
2928
|
-
throw new Error(`Task Role session set is missing for pushed Run: ${run.id}.`);
|
|
2929
|
-
}
|
|
2930
|
-
const updated = markTaskRoleRunPushed(current, {
|
|
2931
|
-
agentId: run.effective.agentId,
|
|
2932
|
-
runId: run.id,
|
|
2933
|
-
receiptId: agentRunDeliveryReceiptId(run)
|
|
2934
|
-
}, now);
|
|
2935
|
-
store.saveRoleSessionSet(updated);
|
|
2936
|
-
}
|
|
2937
|
-
function markTaskRoleRunDeliveredInFlight(store, role, run, now) {
|
|
2938
|
-
const current = store.getRoleSessionSet(role.taskId, role.name);
|
|
2939
|
-
if (current === null) {
|
|
2940
|
-
throw new Error(`Task Role session set is missing for delivered Run: ${run.id}.`);
|
|
2941
|
-
}
|
|
2942
|
-
const updated = markTaskRoleRunDelivered(current, {
|
|
2943
|
-
agentId: run.effective.agentId,
|
|
2944
|
-
runId: run.id,
|
|
2945
|
-
receiptId: agentRunDeliveryReceiptId(run)
|
|
2946
|
-
}, now);
|
|
2947
|
-
store.saveRoleSessionSet(updated);
|
|
2948
|
-
}
|
|
2949
|
-
function clearTaskRoleRunInFlight(store, role, run, now) {
|
|
2950
|
-
const current = store.getRoleSessionSet(role.taskId, role.name);
|
|
2951
|
-
if (current === null)
|
|
2952
|
-
return;
|
|
2953
|
-
if (current.inFlight?.runId !== run.id)
|
|
2954
|
-
return;
|
|
2955
|
-
const updated = clearTaskRoleRun(current, {
|
|
2956
|
-
agentId: run.effective.agentId,
|
|
2957
|
-
runId: run.id,
|
|
2958
|
-
receiptId: agentRunDeliveryReceiptId(run)
|
|
2959
|
-
}, now);
|
|
2960
|
-
store.saveRoleSessionSet(updated);
|
|
2961
|
-
}
|
|
2962
|
-
/**
|
|
2963
|
-
* The durable Run/mailbox claim is the pre-Host delivery intent. The Host may
|
|
2964
|
-
* create a ProviderTurn only while that exact reservation still owns the Run;
|
|
2965
|
-
* this closes the gap where a Session could start without any user Turn.
|
|
2966
|
-
*/
|
|
2967
|
-
function processingOwnsExactInitialDelivery(mailbox, taskId, runId, attemptId) {
|
|
2968
|
-
const processing = mailbox?.processing;
|
|
2969
|
-
return processing?.batchId === attemptId
|
|
2970
|
-
&& processing.owner === "controller"
|
|
2971
|
-
&& processing.executionRef?.type === "run"
|
|
2972
|
-
&& processing.executionRef.taskId === taskId
|
|
2973
|
-
&& processing.executionRef.id === runId;
|
|
2974
|
-
}
|
|
2975
2784
|
function matchesStallSessionFence(current, expected) {
|
|
2976
2785
|
if (current === null || expected === null)
|
|
2977
2786
|
return current === expected;
|
|
@@ -2987,24 +2796,24 @@ function requireRole(store, taskId, roleName) {
|
|
|
2987
2796
|
throw new Error(`Role not found: ${taskId}/${roleName}.`);
|
|
2988
2797
|
return role;
|
|
2989
2798
|
}
|
|
2990
|
-
function
|
|
2799
|
+
function turnLaunchEventPayload(turn) {
|
|
2991
2800
|
return {
|
|
2992
|
-
|
|
2993
|
-
role:
|
|
2994
|
-
purpose:
|
|
2995
|
-
mode:
|
|
2996
|
-
agent: `${
|
|
2997
|
-
effectiveRevision: String(
|
|
2998
|
-
profileAccess:
|
|
2999
|
-
effectivePermission:
|
|
3000
|
-
writeProjectIds:
|
|
2801
|
+
turnId: turn.id,
|
|
2802
|
+
role: turn.roleName,
|
|
2803
|
+
purpose: turn.purpose,
|
|
2804
|
+
mode: turn.mode,
|
|
2805
|
+
agent: `${turn.effective.agentId}/${turn.effective.adapterId}`,
|
|
2806
|
+
effectiveRevision: String(turn.effective.sourceDesiredRevision),
|
|
2807
|
+
profileAccess: turn.effective.profileAccess,
|
|
2808
|
+
effectivePermission: turn.effective.permission.strategy,
|
|
2809
|
+
writeProjectIds: turn.effective.writeProjectIds.join(",") || "none"
|
|
3001
2810
|
};
|
|
3002
2811
|
}
|
|
3003
2812
|
function runtimeObservationTelemetryEntry(input) {
|
|
3004
2813
|
return {
|
|
3005
2814
|
taskId: input.fence.taskId,
|
|
3006
2815
|
roleName: input.fence.roleName,
|
|
3007
|
-
|
|
2816
|
+
turnId: input.fence.turnId,
|
|
3008
2817
|
generation: input.fence.sessionGenerationId,
|
|
3009
2818
|
progressId: [
|
|
3010
2819
|
input.kind,
|
|
@@ -3043,7 +2852,7 @@ function compactedRuntimeObservationIds(events, incoming) {
|
|
|
3043
2852
|
const observation = runtimeObservationFromTaskEvent(event);
|
|
3044
2853
|
const matches = incoming.kind.startsWith("operation.")
|
|
3045
2854
|
? observation !== null
|
|
3046
|
-
&&
|
|
2855
|
+
&& runtimeObservationTurnFenceMatches(observation.fence, incoming.fence)
|
|
3047
2856
|
: observation !== null
|
|
3048
2857
|
&& runtimeObservationFenceMatches(observation.fence, incoming.fence);
|
|
3049
2858
|
return observation !== null
|
|
@@ -3098,14 +2907,15 @@ function recordStructuredProviderAcceptance(sessions, input, now) {
|
|
|
3098
2907
|
const turnId = input.fence.nativeTurnId;
|
|
3099
2908
|
if (current === null || attemptId === undefined || turnId === undefined)
|
|
3100
2909
|
return sessions;
|
|
3101
|
-
if (current.turn === null
|
|
2910
|
+
if (current.turn === null
|
|
2911
|
+
|| current.turn.turnId !== input.fence.turnId)
|
|
3102
2912
|
return sessions;
|
|
3103
2913
|
if (current.turn.attemptId === attemptId
|
|
3104
|
-
&& ["accepted", "
|
|
2914
|
+
&& ["accepted", "completed", "failed", "cancelled"].includes(current.turn.status))
|
|
3105
2915
|
return sessions;
|
|
3106
2916
|
const binding = acceptProviderTurn(current, {
|
|
3107
2917
|
attemptId,
|
|
3108
|
-
turnId,
|
|
2918
|
+
nativeTurnId: turnId,
|
|
3109
2919
|
acceptedAt: input.observedAt ?? input.receivedAt
|
|
3110
2920
|
});
|
|
3111
2921
|
return updateTaskRoleProviderRuntime(sessions, binding, now);
|
|
@@ -3120,13 +2930,11 @@ function canonicalStructuredProviderTurnId(sessions, observedTurnId, attemptId)
|
|
|
3120
2930
|
const turn = sessions.providerBinding?.turn;
|
|
3121
2931
|
return attemptId !== undefined
|
|
3122
2932
|
&& turn?.attemptId === attemptId
|
|
3123
|
-
&& turn.
|
|
3124
|
-
? turn.
|
|
2933
|
+
&& turn.nativeTurnId !== undefined
|
|
2934
|
+
? turn.nativeTurnId
|
|
3125
2935
|
: observedTurnId;
|
|
3126
2936
|
}
|
|
3127
2937
|
function bindOrSupersedeProviderRuntime(sessions, input, now, replacementBasis) {
|
|
3128
|
-
if (sessions.inFlight === null)
|
|
3129
|
-
return sessions;
|
|
3130
2938
|
const conversationId = input.fence.conversationId ?? input.fence.nativeSessionId;
|
|
3131
2939
|
const activationId = input.fence.activationId ?? input.fence.launchId;
|
|
3132
2940
|
if (sessions.providerBinding === null) {
|
|
@@ -3135,7 +2943,7 @@ function bindOrSupersedeProviderRuntime(sessions, input, now, replacementBasis)
|
|
|
3135
2943
|
accountScope: input.fence.agentId,
|
|
3136
2944
|
conversationId,
|
|
3137
2945
|
activationId,
|
|
3138
|
-
startedAt:
|
|
2946
|
+
startedAt: input.observedAt ?? input.receivedAt
|
|
3139
2947
|
}), now);
|
|
3140
2948
|
}
|
|
3141
2949
|
const current = currentProviderConversation(sessions.providerBinding);
|
|
@@ -3161,54 +2969,15 @@ function bindOrSupersedeProviderRuntime(sessions, input, now, replacementBasis)
|
|
|
3161
2969
|
basis: replacementBasis
|
|
3162
2970
|
}), now);
|
|
3163
2971
|
}
|
|
3164
|
-
function rejectSubmittingProviderTurn(store, target, delivery, now, reason) {
|
|
3165
|
-
if (target.kind !== "role"
|
|
3166
|
-
|| delivery.executionRef.type !== "run"
|
|
3167
|
-
|| delivery.executionRef.taskId !== target.taskId)
|
|
3168
|
-
return;
|
|
3169
|
-
const sessions = store.getTaskRoleSessionSet(target.taskId, target.roleName);
|
|
3170
|
-
const binding = sessions?.providerBinding;
|
|
3171
|
-
if (sessions === null || sessions === undefined
|
|
3172
|
-
|| binding === null || binding === undefined
|
|
3173
|
-
|| binding.turn?.runId !== delivery.executionRef.id
|
|
3174
|
-
|| binding.turn?.attemptId !== delivery.attemptId
|
|
3175
|
-
|| (binding.turn.status !== "submitting"
|
|
3176
|
-
&& binding.turn.status !== "delivery-unknown"))
|
|
3177
|
-
return;
|
|
3178
|
-
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, rejectProviderTurn(binding, {
|
|
3179
|
-
attemptId: delivery.attemptId,
|
|
3180
|
-
rejectedAt: now.toISOString(),
|
|
3181
|
-
reason
|
|
3182
|
-
}), now));
|
|
3183
|
-
}
|
|
3184
|
-
function markSubmittingProviderTurnUnknown(store, target, delivery, now, reason) {
|
|
3185
|
-
if (target.kind !== "role"
|
|
3186
|
-
|| delivery.executionRef.type !== "run"
|
|
3187
|
-
|| delivery.executionRef.taskId !== target.taskId)
|
|
3188
|
-
return;
|
|
3189
|
-
const sessions = store.getTaskRoleSessionSet(target.taskId, target.roleName);
|
|
3190
|
-
const binding = sessions?.providerBinding;
|
|
3191
|
-
if (sessions === null || sessions === undefined
|
|
3192
|
-
|| binding === null || binding === undefined
|
|
3193
|
-
|| binding.turn?.runId !== delivery.executionRef.id
|
|
3194
|
-
|| binding.turn?.attemptId !== delivery.attemptId
|
|
3195
|
-
|| binding.turn.status !== "submitting")
|
|
3196
|
-
return;
|
|
3197
|
-
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, markProviderTurnDeliveryUnknown(binding, {
|
|
3198
|
-
attemptId: delivery.attemptId,
|
|
3199
|
-
observedAt: now.toISOString(),
|
|
3200
|
-
reason
|
|
3201
|
-
}), now));
|
|
3202
|
-
}
|
|
3203
2972
|
function settleStructuredProviderTurn(sessions, turnId, status, now) {
|
|
3204
2973
|
const binding = sessions.providerBinding;
|
|
3205
|
-
if (binding === null || binding.turn === null || binding.turn.
|
|
2974
|
+
if (binding === null || binding.turn === null || binding.turn.nativeTurnId !== turnId) {
|
|
3206
2975
|
return sessions;
|
|
3207
2976
|
}
|
|
3208
2977
|
if (["completed", "failed", "cancelled"].includes(binding.turn.status))
|
|
3209
2978
|
return sessions;
|
|
3210
2979
|
return updateTaskRoleProviderRuntime(sessions, settleProviderTurn(binding, {
|
|
3211
|
-
turnId,
|
|
2980
|
+
nativeTurnId: turnId,
|
|
3212
2981
|
status,
|
|
3213
2982
|
settledAt: now.toISOString()
|
|
3214
2983
|
}), now);
|