@zq-silk/yui 0.15.9 → 0.15.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +64 -16
- package/dist/cli/interactionPolicy.js +7 -3
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli/updateOrchestrator.js +24 -1
- package/dist/cli/updatePorts.js +7 -3
- package/dist/cli/upgradeCommand.js +42 -2
- package/dist/cli.js +381 -107
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +339 -4
- package/dist/commands/projectCommands.js +50 -22
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +586 -96
- package/dist/commands/taskIntegrationCommands.js +19 -39
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +55 -6
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +14 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
- package/dist/controller/globalInputDelivery.js +132 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +117 -7
- package/dist/controller/runtimeEventInbox.js +32 -3
- package/dist/controller/runtimeEventProcessor.js +26 -6
- package/dist/controller/runtimeHookRunFence.js +75 -19
- package/dist/controller/structuredProviderObservation.js +133 -70
- package/dist/coordination/workMailboxQueue.js +5 -0
- package/dist/execution/workItemExecutionProjection.js +1 -1
- package/dist/executor/agentExecutor.js +64 -4
- package/dist/executor/executorRegistry.js +3 -0
- package/dist/executor/fileRoleLaunchPlanner.js +78 -118
- package/dist/integration/deliveryObligation.js +2 -1
- package/dist/integration/gitIntegrationService.js +329 -386
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +367 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +430 -107
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +182 -101
- package/dist/repository/taskWorkspacePreparer.js +205 -72
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +179 -82
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +38 -6
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +26 -8
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +167 -4
- package/dist/storage/sqliteStore.js +57 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +129 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +147 -17
- package/dist/web/assets/client/components.js +56 -13
- package/dist/web/assets/client/i18n.js +78 -4
- package/dist/web/assets/client/taskSurface.js +108 -1
- package/dist/web/assets/client/view.js +39 -8
- package/dist/web/assets/shell.js +29 -0
- package/dist/web/assets/styles/layout.js +8 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +131 -4
- package/dist/web/webSnapshot.js +16 -6
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +111 -35
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/release-workflow.md +39 -0
- package/docs/release-workflow.zh-CN.md +29 -0
- package/docs/sqlite-control-plane-design.md +223 -1
- package/docs/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +40 -0
- package/docs/testing/verification-levels.zh-CN.md +23 -0
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +154 -51
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +19 -3
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +42 -0
- package/skills/yui-runtime/references/publication.md +42 -0
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { prepareMessageContinuations } from "../message/messageContinuation.js";
|
|
2
|
+
import { prepareMessageContinuations, interruptThenTerminalState } from "../message/messageContinuation.js";
|
|
3
3
|
import { freezeRunContextSnapshot } from "../context/runContextPack.js";
|
|
4
4
|
import { contextSnapshotRef } from "../context/contextSnapshot.js";
|
|
5
5
|
import { isDeepStrictEqual } from "node:util";
|
|
6
6
|
import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveAgentHostObservation, recordAgentHostConnection, AgentHostObservationDeferred } from "./agentHostObservation.js";
|
|
8
|
+
import { RuntimeHookRunFenceError } from "./runtimeHookRunFence.js";
|
|
9
|
+
import { admitOwnedProviderInput, ownedProviderInput } from "./providerRetryAdmission.js";
|
|
10
|
+
import { recordProviderFailure, providerRetryPending, deferProviderRetry } from "../runtime/providerRetry.js";
|
|
11
|
+
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
12
|
+
import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindGlobalRoleProviderRuntime, createRoleSessionSet, recordRoleAgentSession, replaceTaskRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, detachRoleAgentSessionHost, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime, updateGlobalRoleProviderRuntime, selectNewTaskRoleSession, taskRoleControlTarget } from "../executor/agentExecutor.js";
|
|
8
13
|
import { acceptProviderTurn, cancelQuiescentProviderInput, beginProviderTurn, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, clearProviderGoal, settleProviderTurnSubmission, settleProviderTurn, transferProviderAuthority, supersedeProviderConversation, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
|
|
9
14
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
10
15
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
@@ -17,6 +22,7 @@ import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
|
|
|
17
22
|
import { appendRunInput, createRun, withRunContextSnapshot, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
18
23
|
import { transportAgentResult } from "../domain/agentResultTransport.js";
|
|
19
24
|
import { createRunInput } from "../context/runInputContract.js";
|
|
25
|
+
import { recordTaskMessageControlOutcome, markGlobalRoleMessageDelivered, markGlobalRoleMessageNotDelivered } from "../message/message.js";
|
|
20
26
|
import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
|
|
21
27
|
import { terminalizeExactTaskRun, cancelQuiescentRoleRuns } from "../lifecycle/exactRunTerminalization.js";
|
|
22
28
|
import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
|
|
@@ -29,7 +35,7 @@ import { pendingWakeupProjection } from "../storage/taskStore.js";
|
|
|
29
35
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
30
36
|
import { providerContinuationKey } from "../runtime/providerContinuation.js";
|
|
31
37
|
import { formatRunReceiptId } from "../task/taskRecordReference.js";
|
|
32
|
-
import { bindExecution, claimPending, completeProcessing, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
|
|
38
|
+
import { bindExecution, claimPending, completeProcessing, consumePendingBatch, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
|
|
33
39
|
import { enqueueWork, settleRoleRunDispatch as settleRoleRunDispatchMailbox } from "../coordination/workMailboxQueue.js";
|
|
34
40
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
35
41
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
@@ -58,6 +64,57 @@ export class FileSchedulerStoreAdapter {
|
|
|
58
64
|
this.drivers = drivers;
|
|
59
65
|
this.snapshotExecutionLaneWorkspace = snapshotExecutionLaneWorkspace;
|
|
60
66
|
}
|
|
67
|
+
observeAgentHostObservation(event, now = new Date()) {
|
|
68
|
+
// Match canonical ingress: archive and Host fact settlement share the
|
|
69
|
+
// same write boundary, including the original transport envelope.
|
|
70
|
+
return this.store.transaction(() => this.foldAgentHostObservation(event, now));
|
|
71
|
+
}
|
|
72
|
+
foldAgentHostObservation(event, now) {
|
|
73
|
+
const raw = createRuntimeObservation(event.observation);
|
|
74
|
+
const task = raw.fence.taskId === undefined ? null : this.store.getTask(raw.fence.taskId);
|
|
75
|
+
if (raw.fence.taskId !== undefined && task === null)
|
|
76
|
+
return "obsolete";
|
|
77
|
+
if (task?.status === "archived") {
|
|
78
|
+
this.observeObsoleteRuntimeEvent({
|
|
79
|
+
eventId: event.id, eventType: event.type, taskId: task.id,
|
|
80
|
+
roleName: raw.fence.roleName, agentId: raw.fence.agentId,
|
|
81
|
+
nativeSessionId: raw.fence.nativeSessionId ?? "unknown",
|
|
82
|
+
reason: "task-archived", originalEvent: event
|
|
83
|
+
}, now);
|
|
84
|
+
return "obsolete";
|
|
85
|
+
}
|
|
86
|
+
if (Date.parse(raw.receivedAt) > now.getTime()) {
|
|
87
|
+
return this.recordObsoleteCanonicalObservation(raw, "received-at-in-future", now);
|
|
88
|
+
}
|
|
89
|
+
let input;
|
|
90
|
+
try {
|
|
91
|
+
if (event.observation.kind === "host.observed") {
|
|
92
|
+
return this.store.transaction(store => {
|
|
93
|
+
const connection = resolveAgentHostObservation(store, event);
|
|
94
|
+
recordAgentHostConnection(store, event, connection, now);
|
|
95
|
+
if (connection.fence.taskId !== undefined)
|
|
96
|
+
this.persistRuntimeObservation(connection, now);
|
|
97
|
+
return "applied";
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
input = resolveAgentHostObservation(this.store, event);
|
|
101
|
+
if (input.kind === "input.accepted" && input.fence.runId === undefined
|
|
102
|
+
&& input.fence.receiptId?.startsWith("native-input:")) {
|
|
103
|
+
// A valid ordinary conversation has no managed Run input to mirror.
|
|
104
|
+
// Consuming this fact is not rejection, nor assignment acceptance.
|
|
105
|
+
return "applied";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error instanceof AgentHostObservationDeferred)
|
|
110
|
+
return "deferred";
|
|
111
|
+
if (!(error instanceof RuntimeHookRunFenceError))
|
|
112
|
+
throw error;
|
|
113
|
+
// Rejection is durable diagnostic evidence, never business acceptance.
|
|
114
|
+
return this.recordObsoleteCanonicalObservation(event.observation, error.message, now);
|
|
115
|
+
}
|
|
116
|
+
return this.observeRuntimeObservation(input, now);
|
|
117
|
+
}
|
|
61
118
|
/**
|
|
62
119
|
* Sole provider-independent ingress for structured runtime state. Driver
|
|
63
120
|
* mapping has already happened before this boundary; this method validates
|
|
@@ -65,10 +122,19 @@ export class FileSchedulerStoreAdapter {
|
|
|
65
122
|
* transitions, and retains the canonical observation for status projection.
|
|
66
123
|
*/
|
|
67
124
|
observeRuntimeObservation(raw, now = new Date()) {
|
|
125
|
+
// Archive and observation settlement share the same write boundary.
|
|
126
|
+
// An ingress read outside this transaction must never authorize clearing
|
|
127
|
+
// a claim after another CLI has committed archive.
|
|
128
|
+
return this.store.transaction(() => this.foldRuntimeObservation(raw, now));
|
|
129
|
+
}
|
|
130
|
+
foldRuntimeObservation(raw, now) {
|
|
68
131
|
const input = createRuntimeObservation(raw);
|
|
69
132
|
const taskId = input.fence.taskId;
|
|
70
133
|
if (taskId === undefined)
|
|
71
|
-
return
|
|
134
|
+
return this.observeGlobalProviderObservation(input, now);
|
|
135
|
+
if (this.store.getTask(taskId)?.status === "archived") {
|
|
136
|
+
return this.recordObsoleteCanonicalObservation(input, "task-archived", now);
|
|
137
|
+
}
|
|
72
138
|
if (Date.parse(input.receivedAt) > now.getTime()) {
|
|
73
139
|
return this.recordObsoleteCanonicalObservation(input, "received-at-in-future", now);
|
|
74
140
|
}
|
|
@@ -83,12 +149,18 @@ export class FileSchedulerStoreAdapter {
|
|
|
83
149
|
const run = input.fence.runId === undefined ? null : store.getRun(taskId, input.fence.runId);
|
|
84
150
|
const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
|
|
85
151
|
const native = sessions?.providerBinding?.run;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
152
|
+
const isSteer = input.fence.receiptId.startsWith("steer:");
|
|
153
|
+
// A Worker/Reviewer steer's accepted input is folded onto the owning
|
|
154
|
+
// AgentRun. A no-Run Leader steer (decision-3 §9) has no run to append to,
|
|
155
|
+
// but its accepted disposition is still a real fact that gap D records on
|
|
156
|
+
// the Message; only the Worker append requires the exact Run/Turn fence.
|
|
157
|
+
const runFolded = run !== null && run.roleName === input.fence.roleName
|
|
158
|
+
&& run.effective.agentId === input.fence.agentId && native?.runId === run.id
|
|
159
|
+
&& native.nativeTurnId === input.fence.nativeTurnId
|
|
160
|
+
&& sessions?.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId;
|
|
161
|
+
if (!runFolded && !(isSteer && input.fence.runId === undefined))
|
|
90
162
|
return "obsolete";
|
|
91
|
-
if (!hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
|
|
163
|
+
if (runFolded && !hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
|
|
92
164
|
const updated = appendRunInput(run, createRunInput({
|
|
93
165
|
source: input.fence.receiptId.startsWith("native-input:")
|
|
94
166
|
? { type: "provider", channel: "visible-input" }
|
|
@@ -98,6 +170,31 @@ export class FileSchedulerStoreAdapter {
|
|
|
98
170
|
store.saveRun(updated);
|
|
99
171
|
store.saveActiveRun(updated);
|
|
100
172
|
}
|
|
173
|
+
// decision-3 §3/§8, message-5 gap D: promote the Message's independent
|
|
174
|
+
// control op to its proven terminal from the Host's own settlement, keyed
|
|
175
|
+
// by this exact steer receipt. Monotonic and idempotent — never a rewind
|
|
176
|
+
// of an already-proven outcome, and a replayed observation is absorbed.
|
|
177
|
+
if (isSteer)
|
|
178
|
+
this.recordSteerControlOutcome(store, input.fence.receiptId, "accepted", now);
|
|
179
|
+
this.persistRuntimeObservation(input, now);
|
|
180
|
+
return "applied";
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// A steer's rejected/delivery-unknown disposition is the Host's proven
|
|
184
|
+
// terminal for the one live control attempt (message-5 gap D). It carries no
|
|
185
|
+
// AgentRun append — a rejected input changed no execution — so it is recorded
|
|
186
|
+
// only on the Message's control op, for a Worker/Reviewer or a no-Run Leader
|
|
187
|
+
// steer alike, keyed by the exact steer receipt.
|
|
188
|
+
if ((input.kind === "input.rejected" || input.kind === "input.delivery-unknown")
|
|
189
|
+
&& input.fence.receiptId?.startsWith("steer:") === true) {
|
|
190
|
+
return this.store.transaction((store) => {
|
|
191
|
+
if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
|
|
192
|
+
return "applied";
|
|
193
|
+
const recorded = this.recordSteerControlOutcome(store, input.fence.receiptId, input.kind === "input.rejected" ? "rejected" : "delivery-unknown", now);
|
|
194
|
+
if (!recorded) {
|
|
195
|
+
recordCanonicalObservationObsolete(store, input, "steer-message-not-found", now);
|
|
196
|
+
return "obsolete";
|
|
197
|
+
}
|
|
101
198
|
this.persistRuntimeObservation(input, now);
|
|
102
199
|
return "applied";
|
|
103
200
|
});
|
|
@@ -173,6 +270,38 @@ export class FileSchedulerStoreAdapter {
|
|
|
173
270
|
}
|
|
174
271
|
return outcome;
|
|
175
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Promote a steer Message's independent control op to a proven outcome from the
|
|
275
|
+
* Host's own settlement (decision-3 §3/§8, message-5 gap D). The `steer:` fence
|
|
276
|
+
* names the exact `steer:<taskId>/<messageId>` receipt, so the outcome binds to
|
|
277
|
+
* that one Message and never another; the requestId is read from the Message's
|
|
278
|
+
* own steer identity (its live input, or the reused-steer provenance a handoff
|
|
279
|
+
* preserved) so the recorded op matches the same idempotency key the live edge
|
|
280
|
+
* used. Returns false when the receipt names no such steer Message, so the
|
|
281
|
+
* caller can mark the observation obsolete rather than silently drop it. The
|
|
282
|
+
* record is monotonic and idempotent inside {@link recordTaskMessageControlOutcome}.
|
|
283
|
+
*/
|
|
284
|
+
recordSteerControlOutcome(store, receiptId, outcome, now) {
|
|
285
|
+
const parsed = /^steer:([^/]+)\/(.+)$/.exec(receiptId);
|
|
286
|
+
if (parsed === null)
|
|
287
|
+
return false;
|
|
288
|
+
const [, taskId, messageId] = parsed;
|
|
289
|
+
const message = store.listMessages(taskId).find((entry) => entry.id === messageId);
|
|
290
|
+
const requestId = message?.inputControl?.requestId ?? message?.interruptThen?.reusedInput?.requestId;
|
|
291
|
+
const wasSteer = message?.inputControl?.action === "steer"
|
|
292
|
+
|| message?.interruptThen?.reusedInput?.action === "steer";
|
|
293
|
+
if (message === undefined || requestId === undefined || !wasSteer)
|
|
294
|
+
return false;
|
|
295
|
+
const recorded = recordTaskMessageControlOutcome(message, {
|
|
296
|
+
requestId, receiptId, outcome, observedAt: now
|
|
297
|
+
});
|
|
298
|
+
// The monotonic guard returns the same Message when the record is absorbed
|
|
299
|
+
// (an idempotent repeat, or a stale outcome that must not rewind a proven
|
|
300
|
+
// terminal); only persist a real change.
|
|
301
|
+
if (recorded !== message)
|
|
302
|
+
store.updateMessage(taskId, recorded);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
176
305
|
/** A delayed steer receipt settles only its original claimed mailbox batch. */
|
|
177
306
|
observeLeaderSteerReceipt(input, now) {
|
|
178
307
|
return this.store.transaction((store) => {
|
|
@@ -238,6 +367,105 @@ export class FileSchedulerStoreAdapter {
|
|
|
238
367
|
return "applied";
|
|
239
368
|
});
|
|
240
369
|
}
|
|
370
|
+
observeGlobalProviderObservation(input, now) {
|
|
371
|
+
return this.store.transaction((store) => {
|
|
372
|
+
const fence = input.fence;
|
|
373
|
+
let sessions = store.getGlobalRoleSessionSet(fence.roleName);
|
|
374
|
+
const session = sessions?.sessions[fence.agentId];
|
|
375
|
+
let binding = sessions?.providerBinding;
|
|
376
|
+
if (sessions == null || session == null || binding == null || fence.runId !== undefined
|
|
377
|
+
|| sessions.activeAgentId !== fence.agentId
|
|
378
|
+
|| session.nativeSessionId !== fence.nativeSessionId
|
|
379
|
+
|| this.drivers.requireByAdapterId(session.adapterId).id !== fence.driverId
|
|
380
|
+
|| currentProviderConversation(binding).conversationId !== fence.nativeSessionId
|
|
381
|
+
|| Date.parse(input.receivedAt) > now.getTime())
|
|
382
|
+
return "obsolete";
|
|
383
|
+
const at = input.observedAt ?? input.receivedAt;
|
|
384
|
+
const receipt = fence.receiptId;
|
|
385
|
+
const matches = receipt !== undefined && binding.run?.attemptId === receipt
|
|
386
|
+
&& (fence.nativeTurnId === undefined || binding.run.nativeTurnId === undefined
|
|
387
|
+
|| binding.run.nativeTurnId === fence.nativeTurnId);
|
|
388
|
+
if (input.kind === "turn.accepted") {
|
|
389
|
+
if (input.authority === "transport" && matches) {
|
|
390
|
+
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
|
|
391
|
+
if (message !== undefined)
|
|
392
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at), "transport"));
|
|
393
|
+
return "applied";
|
|
394
|
+
}
|
|
395
|
+
if (input.authority !== "provider-structured")
|
|
396
|
+
return "applied";
|
|
397
|
+
if (!matches) {
|
|
398
|
+
if (receipt?.startsWith("direct:") !== true || binding.run != null
|
|
399
|
+
&& ["submitting", "accepted", "delivery-unknown"].includes(binding.run.status))
|
|
400
|
+
return "obsolete";
|
|
401
|
+
binding = beginProviderTurn(binding, { attemptId: receipt,
|
|
402
|
+
authorityEpoch: binding.authority.epoch, submittedAt: at });
|
|
403
|
+
}
|
|
404
|
+
if (binding.run?.status === "submitting" || binding.run?.status === "delivery-unknown") {
|
|
405
|
+
binding = acceptProviderTurn(binding, { attemptId: receipt,
|
|
406
|
+
nativeTurnId: fence.nativeTurnId, acceptedAt: at });
|
|
407
|
+
}
|
|
408
|
+
else if (binding.run?.status !== "accepted")
|
|
409
|
+
return "applied";
|
|
410
|
+
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`
|
|
411
|
+
|| binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
|
|
412
|
+
&& binding.retry.input.messageId === entry.id);
|
|
413
|
+
if (message !== undefined)
|
|
414
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
|
|
415
|
+
}
|
|
416
|
+
else if (["turn.completed", "turn.cancelled", "turn.failed"].includes(input.kind)) {
|
|
417
|
+
if (!matches || input.authority !== "provider-structured")
|
|
418
|
+
return "obsolete";
|
|
419
|
+
// A provider final result proves this exact submitted input ran (Claude
|
|
420
|
+
// may emit its result without an earlier assistant acceptance event).
|
|
421
|
+
// Process exit/failure alone must not turn unconfirmed input into proof.
|
|
422
|
+
if (input.kind === "turn.completed" && binding.run != null
|
|
423
|
+
&& ["submitting", "delivery-unknown"].includes(binding.run.status)) {
|
|
424
|
+
binding = acceptProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId, acceptedAt: at });
|
|
425
|
+
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`
|
|
426
|
+
|| binding?.retry?.currentAttemptId === receipt && binding.retry.input.kind === "message"
|
|
427
|
+
&& binding.retry.input.messageId === entry.id);
|
|
428
|
+
if (message !== undefined)
|
|
429
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
|
|
430
|
+
}
|
|
431
|
+
if (binding.run?.status !== "accepted")
|
|
432
|
+
return binding.run != null
|
|
433
|
+
&& ["completed", "cancelled", "failed"].includes(binding.run.status) ? "applied" : "obsolete";
|
|
434
|
+
binding = settleProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId,
|
|
435
|
+
status: input.kind === "turn.completed" ? "completed" : input.kind === "turn.cancelled" ? "cancelled" : "failed",
|
|
436
|
+
settledAt: at });
|
|
437
|
+
if (fence.nativeTurnId !== undefined)
|
|
438
|
+
sessions = rememberRoleAgentCompletedTurn(sessions, fence.agentId, fence.nativeSessionId, fence.nativeTurnId, new Date(at));
|
|
439
|
+
}
|
|
440
|
+
else if (["input.accepted", "input.rejected", "input.delivery-unknown"].includes(input.kind)) {
|
|
441
|
+
if (input.authority !== "provider-structured" || binding.run?.nativeTurnId !== fence.nativeTurnId
|
|
442
|
+
|| binding.run?.status !== "accepted")
|
|
443
|
+
return "obsolete";
|
|
444
|
+
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `steer:${fence.roleName}/${entry.id}`);
|
|
445
|
+
if (message !== undefined && message.inputControl !== undefined) {
|
|
446
|
+
const outcome = input.kind === "input.accepted" ? "accepted"
|
|
447
|
+
: input.kind === "input.rejected" ? "rejected" : "delivery-unknown";
|
|
448
|
+
if (message.control?.outcome !== "accepted" && message.control?.outcome !== "rejected") {
|
|
449
|
+
const updated = { ...message, control: { requestId: message.inputControl.requestId,
|
|
450
|
+
receiptId: receipt, outcome, observedAt: at } };
|
|
451
|
+
store.updateGlobalRoleMessage(outcome === "accepted"
|
|
452
|
+
? markGlobalRoleMessageDelivered(updated, new Date(at)) : updated);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
else if (input.kind === "conversation.observed" && input.payload.recoverability !== undefined) {
|
|
457
|
+
binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
|
|
458
|
+
}
|
|
459
|
+
if (input.kind === "turn.failed" && matches && input.payload.failure !== undefined) {
|
|
460
|
+
binding = recordProviderFailure(binding, {
|
|
461
|
+
error: input.payload.failure.error, failureRef: input.eventId, at: now.getTime()
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, binding, now));
|
|
465
|
+
settleGlobalRetryInput(store, fence.roleName, binding, now);
|
|
466
|
+
return "applied";
|
|
467
|
+
});
|
|
468
|
+
}
|
|
241
469
|
adapterForRuntimeObservation(input) {
|
|
242
470
|
const taskId = input.fence.taskId;
|
|
243
471
|
const runId = input.fence.runId;
|
|
@@ -536,7 +764,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
536
764
|
...(failure.lastOutput === undefined ? {} : { lastOutput: failure.lastOutput })
|
|
537
765
|
}, now);
|
|
538
766
|
store.saveEvent(taskId, errorEvent);
|
|
539
|
-
|
|
767
|
+
if (!providerRetryPending(store.getTaskRoleSessionSet(taskId, input.fence.roleName)?.providerBinding))
|
|
768
|
+
routeRoleEvent(store, errorEvent, input.fence.roleName, wakeReason("agent-error", errorEvent.id), now);
|
|
540
769
|
}
|
|
541
770
|
if ((input.kind === "operation.completed" || input.kind === "operation.failed")
|
|
542
771
|
&& input.payload.operation === "subagent") {
|
|
@@ -871,6 +1100,72 @@ export class FileSchedulerStoreAdapter {
|
|
|
871
1100
|
});
|
|
872
1101
|
}
|
|
873
1102
|
beginAgentHostProviderTurn(input) {
|
|
1103
|
+
if (input.taskId === undefined) {
|
|
1104
|
+
this.store.transaction((store) => {
|
|
1105
|
+
if (input.runId !== undefined || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: input.roleName })))) {
|
|
1106
|
+
throw new AgentHostProviderTurnFenceError("Global Session admission is stopped.");
|
|
1107
|
+
}
|
|
1108
|
+
const sessions = store.getGlobalRoleSessionSet(input.roleName);
|
|
1109
|
+
let binding = sessions?.providerBinding;
|
|
1110
|
+
const session = sessions?.sessions[input.agentId];
|
|
1111
|
+
if (sessions === null || binding == null || session === undefined
|
|
1112
|
+
|| sessions.activeAgentId !== input.agentId || session.status !== "active"
|
|
1113
|
+
|| session.nativeSessionId !== input.nativeSessionId
|
|
1114
|
+
|| currentProviderConversation(binding).conversationId !== input.nativeSessionId
|
|
1115
|
+
|| binding.authority.owner !== input.authorityOwner
|
|
1116
|
+
|| binding.authority.epoch !== input.authorityEpoch
|
|
1117
|
+
|| binding.authority.holderId !== input.holderId) {
|
|
1118
|
+
throw new AgentHostProviderTurnFenceError("Global input carries a stale Session or writer fence.");
|
|
1119
|
+
}
|
|
1120
|
+
const turn = binding.run;
|
|
1121
|
+
if (turn?.attemptId === input.attemptId && turn.status === "submitting") {
|
|
1122
|
+
admitOwnedProviderInput(store, binding, input);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (turn != null && ["submitting", "accepted", "delivery-unknown"].includes(turn.status)) {
|
|
1126
|
+
throw new AgentHostProviderSessionBusyError("Global Conversation has an unsettled input.");
|
|
1127
|
+
}
|
|
1128
|
+
const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
|
|
1129
|
+
const reserved = store.listGlobalRoleMessages(input.roleName).find(entry => entry.delivery === undefined && entry.notDelivered === undefined
|
|
1130
|
+
&& entry.interruptThen?.targetNativeSessionId === input.nativeSessionId
|
|
1131
|
+
&& entry.interruptThen.targetAgentId === input.agentId
|
|
1132
|
+
&& entry.interruptThen.targetAuthorityEpoch === input.authorityEpoch
|
|
1133
|
+
&& entry.interruptThen.targetAuthorityHolderId === input.holderId);
|
|
1134
|
+
if (reserved !== undefined && reserved.id !== message?.id) {
|
|
1135
|
+
throw new AgentHostProviderSessionBusyError("The next Global input belongs to an explicit then handoff.");
|
|
1136
|
+
}
|
|
1137
|
+
if (message !== undefined) {
|
|
1138
|
+
if (message.delivery !== undefined || message.notDelivered !== undefined
|
|
1139
|
+
|| message.control !== undefined && message.control.outcome !== "rejected"
|
|
1140
|
+
|| message.deliveryTarget?.agentId !== input.agentId
|
|
1141
|
+
|| message.deliveryTarget?.nativeSessionId !== input.nativeSessionId) {
|
|
1142
|
+
throw new AgentHostProviderTurnFenceError("Global Message is settled, unconfirmed, or bound to another Session.");
|
|
1143
|
+
}
|
|
1144
|
+
const claim = message.interruptThen;
|
|
1145
|
+
if (claim !== undefined && (claim.targetAgentId !== input.agentId
|
|
1146
|
+
|| claim.targetNativeSessionId !== input.nativeSessionId
|
|
1147
|
+
|| claim.targetAuthorityEpoch !== input.authorityEpoch
|
|
1148
|
+
|| claim.targetAuthorityHolderId !== input.holderId
|
|
1149
|
+
|| turn?.attemptId !== claim.targetAttemptId
|
|
1150
|
+
|| !["completed", "failed", "cancelled"].includes(turn.status))) {
|
|
1151
|
+
throw new AgentHostProviderTurnFenceError("Global then target no longer has exact terminal proof.");
|
|
1152
|
+
}
|
|
1153
|
+
store.updateGlobalRoleMessage({ ...message, control: {
|
|
1154
|
+
requestId: message.inputControl?.requestId ?? message.interruptThen.requestId,
|
|
1155
|
+
receiptId: input.attemptId, outcome: "pending", observedAt: input.now.toISOString()
|
|
1156
|
+
} });
|
|
1157
|
+
}
|
|
1158
|
+
binding = admitOwnedProviderInput(store, binding, input);
|
|
1159
|
+
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, beginProviderTurn(binding, { attemptId: input.attemptId,
|
|
1160
|
+
input: binding.retry?.currentAttemptId === input.attemptId ? binding.retry.input : ownedProviderInput(input),
|
|
1161
|
+
retrySupported: input.retrySupported,
|
|
1162
|
+
authorityEpoch: input.authorityEpoch, submittedAt: input.now.toISOString() }), input.now));
|
|
1163
|
+
});
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
this.beginTaskAgentHostProviderTurn({ ...input, taskId: input.taskId });
|
|
1167
|
+
}
|
|
1168
|
+
beginTaskAgentHostProviderTurn(input) {
|
|
874
1169
|
this.store.transaction((store) => {
|
|
875
1170
|
const task = store.getTask(input.taskId);
|
|
876
1171
|
if (task === null || !["active", "draft"].includes(task.status) || task.executionGate.state !== "enabled"
|
|
@@ -881,7 +1176,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
881
1176
|
}
|
|
882
1177
|
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
883
1178
|
const session = sessions?.sessions[input.agentId];
|
|
884
|
-
|
|
1179
|
+
let binding = sessions?.providerBinding;
|
|
885
1180
|
const active = store.getActiveRun(input.taskId, input.roleName);
|
|
886
1181
|
if (sessions === null || sessions === undefined || binding === null || binding === undefined || (input.runId !== undefined && (active?.id !== input.runId || active.effective.agentId !== input.agentId)) || session === undefined || session.nativeSessionId !== input.nativeSessionId || currentProviderConversation(binding).conversationId !== input.nativeSessionId || binding.authority.owner !== input.authorityOwner || binding.authority.epoch !== input.authorityEpoch || binding.authority.holderId !== input.holderId) {
|
|
887
1182
|
throw new AgentHostProviderTurnFenceError("Agent Host Provider Turn carries a stale durable writer fence. Release and reacquire Provider authority before retrying input.");
|
|
@@ -908,15 +1203,76 @@ export class FileSchedulerStoreAdapter {
|
|
|
908
1203
|
.includes(binding.run.status)) {
|
|
909
1204
|
throw new AgentHostProviderSessionBusyError("Provider Conversation already has an unsettled AgentRun.");
|
|
910
1205
|
}
|
|
1206
|
+
if (!exactReplay) {
|
|
1207
|
+
const claims = store.listMessages(input.taskId).filter(message => message.interruptThen?.targetRoleName === input.roleName
|
|
1208
|
+
&& message.interruptThen.targetNativeSessionId === input.nativeSessionId
|
|
1209
|
+
&& message.interruptThen.targetAgentId === input.agentId
|
|
1210
|
+
&& message.interruptThen.targetAuthorityEpoch === input.authorityEpoch
|
|
1211
|
+
&& message.interruptThen.targetAuthorityHolderId === input.holderId
|
|
1212
|
+
&& message.interruptThen.notDeliveredReason === undefined
|
|
1213
|
+
&& message.continuation?.notDeliveredReason === undefined);
|
|
1214
|
+
for (const message of claims) {
|
|
1215
|
+
if (input.roleName === "leader") {
|
|
1216
|
+
const wakes = store.listTaskWakes(input.taskId).filter(wake => wake.refs?.some(ref => ref.type === "message" && ref.id === message.id));
|
|
1217
|
+
if (wakes.some(wake => wake.status === "consumed"))
|
|
1218
|
+
continue;
|
|
1219
|
+
const mailbox = store.getWorkMailbox({ kind: "role", taskId: input.taskId, roleName: "leader" });
|
|
1220
|
+
const selected = wakes.some(wake => mailbox?.processing?.owner === `leader-notification:${wake.id}`
|
|
1221
|
+
&& mailbox.processing.batchId === input.attemptId);
|
|
1222
|
+
if (!selected)
|
|
1223
|
+
throw new AgentHostProviderSessionBusyError("The next Leader input belongs to an explicit then handoff.");
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
const continuationId = message.continuation?.runId;
|
|
1227
|
+
const continuation = continuationId === undefined ? null : store.getRun(input.taskId, continuationId);
|
|
1228
|
+
if (continuation !== null && (continuation.status !== "active" || currentRun?.runId === continuation.id))
|
|
1229
|
+
continue;
|
|
1230
|
+
if (input.runId !== continuationId || continuationId === undefined) {
|
|
1231
|
+
throw new AgentHostProviderSessionBusyError("The next execution input belongs to an explicit then handoff.");
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
binding = admitOwnedProviderInput(store, binding, input);
|
|
911
1237
|
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
|
|
912
1238
|
...(input.runId === undefined ? {} : { runId: input.runId }),
|
|
913
1239
|
attemptId: input.attemptId,
|
|
914
1240
|
authorityEpoch: input.authorityEpoch,
|
|
915
|
-
submittedAt: input.now.toISOString()
|
|
1241
|
+
submittedAt: input.now.toISOString(),
|
|
1242
|
+
input: binding.retry?.currentAttemptId === input.attemptId ? binding.retry.input : ownedProviderInput(input),
|
|
1243
|
+
retrySupported: input.retrySupported
|
|
916
1244
|
}), input.now));
|
|
917
1245
|
});
|
|
918
1246
|
}
|
|
919
1247
|
resolveAgentHostProviderTurnSubmission(input) {
|
|
1248
|
+
if (input.taskId === undefined) {
|
|
1249
|
+
this.store.transaction((store) => {
|
|
1250
|
+
const sessions = store.getGlobalRoleSessionSet(input.roleName);
|
|
1251
|
+
const binding = sessions?.providerBinding;
|
|
1252
|
+
if (input.runId !== undefined || sessions == null || binding?.run?.attemptId !== input.attemptId) {
|
|
1253
|
+
throw new Error("Global input submission is no longer current.");
|
|
1254
|
+
}
|
|
1255
|
+
let resolved = settleProviderTurnSubmission(binding, { attemptId: input.attemptId, status: input.status,
|
|
1256
|
+
reason: input.reason, resolvedAt: input.now.toISOString() });
|
|
1257
|
+
resolved = this.retrySubmissionResolution(resolved, input, sessions.sessions[sessions.activeAgentId]?.adapterId);
|
|
1258
|
+
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, resolved, input.now));
|
|
1259
|
+
settleGlobalRetryInput(store, input.roleName, resolved, input.now);
|
|
1260
|
+
const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
|
|
1261
|
+
if (message !== undefined && message.delivery?.via !== "provider") {
|
|
1262
|
+
const outcome = input.status === "delivery-unknown" ? "delivery-unknown" : "rejected";
|
|
1263
|
+
const updated = { ...message, control: {
|
|
1264
|
+
requestId: message.inputControl?.requestId ?? message.interruptThen.requestId,
|
|
1265
|
+
receiptId: input.attemptId, outcome, observedAt: input.now.toISOString()
|
|
1266
|
+
} };
|
|
1267
|
+
store.updateGlobalRoleMessage(input.status === "rejected" && updated.delivery === undefined && !providerRetryPending(resolved)
|
|
1268
|
+
? markGlobalRoleMessageNotDelivered(updated, input.reason, input.now) : updated);
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
this.resolveTaskAgentHostProviderTurnSubmission({ ...input, taskId: input.taskId });
|
|
1274
|
+
}
|
|
1275
|
+
resolveTaskAgentHostProviderTurnSubmission(input) {
|
|
920
1276
|
this.store.transaction((store) => {
|
|
921
1277
|
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
922
1278
|
const binding = sessions?.providerBinding;
|
|
@@ -926,20 +1282,18 @@ export class FileSchedulerStoreAdapter {
|
|
|
926
1282
|
|| binding.run?.attemptId !== input.attemptId) {
|
|
927
1283
|
throw new Error("Agent Host Provider Turn submission is no longer current.");
|
|
928
1284
|
}
|
|
929
|
-
|
|
1285
|
+
let updated = settleProviderTurnSubmission(binding, {
|
|
930
1286
|
attemptId: input.attemptId,
|
|
931
1287
|
status: input.status,
|
|
932
1288
|
reason: input.reason,
|
|
933
1289
|
resolvedAt: input.now.toISOString()
|
|
934
1290
|
});
|
|
1291
|
+
updated = this.retrySubmissionResolution(updated, input, sessions.sessions[sessions.activeAgentId]?.adapterId);
|
|
935
1292
|
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, input.now));
|
|
936
|
-
|
|
937
|
-
return;
|
|
938
|
-
const run = store.getRun(input.taskId, input.runId);
|
|
1293
|
+
const run = input.runId === undefined ? null : store.getRun(input.taskId, input.runId);
|
|
939
1294
|
const session = sessions.sessions[sessions.activeAgentId];
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
: this.drivers.findByAdapterId(run.effective.adapterId);
|
|
1295
|
+
const adapterId = run?.effective.adapterId ?? session?.adapterId;
|
|
1296
|
+
const driver = adapterId === undefined ? null : this.drivers.findByAdapterId(adapterId);
|
|
943
1297
|
const error = standardAgentError({
|
|
944
1298
|
source: "driver",
|
|
945
1299
|
phase: "turn-submit",
|
|
@@ -953,7 +1307,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
953
1307
|
});
|
|
954
1308
|
const errorEvent = createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.agent-error", {
|
|
955
1309
|
sourceEventId: input.attemptId,
|
|
956
|
-
runId: input.runId,
|
|
1310
|
+
runId: input.runId ?? "",
|
|
957
1311
|
roleName: input.roleName,
|
|
958
1312
|
agentId: run?.effective.agentId ?? sessions.activeAgentId,
|
|
959
1313
|
adapterId: run?.effective.adapterId ?? session?.adapterId ?? "unknown",
|
|
@@ -975,7 +1329,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
975
1329
|
sessionDisposition: error.sessionDisposition
|
|
976
1330
|
}, input.now);
|
|
977
1331
|
store.saveEvent(input.taskId, errorEvent);
|
|
978
|
-
if (input.status === "deferred")
|
|
1332
|
+
if (input.status === "deferred" || providerRetryPending(updated))
|
|
979
1333
|
return;
|
|
980
1334
|
const route = input.roleName === "leader"
|
|
981
1335
|
? { kind: "operator" }
|
|
@@ -988,6 +1342,30 @@ export class FileSchedulerStoreAdapter {
|
|
|
988
1342
|
});
|
|
989
1343
|
});
|
|
990
1344
|
}
|
|
1345
|
+
retrySubmissionResolution(binding, input, adapterId) {
|
|
1346
|
+
if (input.status === "deferred")
|
|
1347
|
+
return deferProviderRetry(binding, input.now.getTime());
|
|
1348
|
+
const driver = adapterId === undefined ? null : this.drivers.findByAdapterId(adapterId);
|
|
1349
|
+
const classification = driver?.runtime.mapError({ message: input.reason, raw: input.raw });
|
|
1350
|
+
const error = standardAgentError({
|
|
1351
|
+
source: "driver", phase: "turn-submit", classification,
|
|
1352
|
+
message: input.reason, raw: input.raw,
|
|
1353
|
+
inputDisposition: input.status === "delivery-unknown" ? "unknown" : "not-accepted"
|
|
1354
|
+
});
|
|
1355
|
+
// Native preflight issued no mutation. Transient metadata failures may
|
|
1356
|
+
// cool down within the same deadline; unavailable proof needs an Agent.
|
|
1357
|
+
const noProviderWrite = input.status === "rejected" && input.noProviderWrite === true;
|
|
1358
|
+
if (noProviderWrite && classification?.retryable !== true) {
|
|
1359
|
+
const recorded = binding.run === null ? binding : {
|
|
1360
|
+
...binding, run: { ...binding.run, failure: { error, ref: input.attemptId } }
|
|
1361
|
+
};
|
|
1362
|
+
return deferProviderRetry(recorded, input.now.getTime(), input.reason);
|
|
1363
|
+
}
|
|
1364
|
+
return recordProviderFailure(binding, {
|
|
1365
|
+
error,
|
|
1366
|
+
failureRef: input.attemptId, at: input.now.getTime(), noProviderWrite
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
991
1369
|
getProviderAuthorityFence(input) {
|
|
992
1370
|
const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
993
1371
|
const session = sessions?.sessions[input.agentId];
|
|
@@ -1014,6 +1392,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
1014
1392
|
}
|
|
1015
1393
|
enqueueLeaderWakeup(taskId, reason, now) {
|
|
1016
1394
|
return this.store.transaction((store) => {
|
|
1395
|
+
if (store.getTask(taskId)?.status === "archived")
|
|
1396
|
+
return null;
|
|
1017
1397
|
const mailbox = enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, reason, now);
|
|
1018
1398
|
const wakeup = pendingWakeupProjection(mailbox);
|
|
1019
1399
|
if (wakeup === null)
|
|
@@ -1462,11 +1842,47 @@ export class FileSchedulerStoreAdapter {
|
|
|
1462
1842
|
if (mailbox.pending === null || (provider !== null && provider !== undefined
|
|
1463
1843
|
&& ["submitting", "accepted", "delivery-unknown"].includes(provider.status)))
|
|
1464
1844
|
return null;
|
|
1845
|
+
const pending = mailbox.pending;
|
|
1846
|
+
const pendingMessageIds = new Set(pending.refs.filter((ref) => ref.type === "message").map((ref) => ref.id));
|
|
1847
|
+
const claimedMessages = store.listMessages(taskId).filter((message) => pendingMessageIds.has(message.id) && message.interruptThen?.targetRoleName === "leader");
|
|
1848
|
+
const sessions = store.getTaskRoleSessionSet(taskId, "leader");
|
|
1849
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
1850
|
+
const obsoleteIds = new Set();
|
|
1851
|
+
for (const message of claimedMessages) {
|
|
1852
|
+
const claim = message.interruptThen;
|
|
1853
|
+
if (claim.notDeliveredReason !== undefined
|
|
1854
|
+
|| session?.nativeSessionId !== claim.targetNativeSessionId
|
|
1855
|
+
|| session.agentId !== claim.targetAgentId
|
|
1856
|
+
|| session.adapterId !== claim.targetAdapterId
|
|
1857
|
+
|| (sessions?.providerBinding != null && (sessions.providerBinding.authority.epoch !== claim.targetAuthorityEpoch
|
|
1858
|
+
|| sessions.providerBinding.authority.holderId !== claim.targetAuthorityHolderId))) {
|
|
1859
|
+
obsoleteIds.add(message.id);
|
|
1860
|
+
if (claim.notDeliveredReason === undefined)
|
|
1861
|
+
store.updateMessage(taskId, {
|
|
1862
|
+
...message, interruptThen: { ...claim, notDeliveredReason: "target-session-or-authority-changed" }
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
const availableRefs = pending.refs.filter(ref => !(ref.type === "message" && obsoleteIds.has(ref.id)));
|
|
1867
|
+
const handoffs = claimedMessages.filter(message => !obsoleteIds.has(message.id));
|
|
1868
|
+
// A quiet/new Session is not evidence that the claimed old Turn stopped.
|
|
1869
|
+
// Keep the exact pending intent until the original fence is provable.
|
|
1870
|
+
if (handoffs.some((message) => interruptThenTerminalState(store, message) !== "ready"))
|
|
1871
|
+
return null;
|
|
1872
|
+
const handoffIds = new Set(handoffs.map((message) => message.id));
|
|
1873
|
+
if (availableRefs.length === 0 && obsoleteIds.size > 0) {
|
|
1874
|
+
store.saveWorkMailbox(consumePendingBatch(mailbox, pending));
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
const selectedRefs = handoffs.length === 0 ? availableRefs
|
|
1878
|
+
: availableRefs.filter((ref) => ref.type === "message" && handoffIds.has(ref.id));
|
|
1879
|
+
const remainingRefs = handoffs.length === 0 ? []
|
|
1880
|
+
: availableRefs.filter((ref) => !(ref.type === "message" && handoffIds.has(ref.id)));
|
|
1465
1881
|
const latest = latestTaskWake(store.listTaskWakes(taskId));
|
|
1466
1882
|
const wakeId = store.nextTaskWakeId(taskId);
|
|
1467
1883
|
const wake = createTaskWake({
|
|
1468
1884
|
id: wakeId, taskId, reasons: mailbox.pending.reasons,
|
|
1469
|
-
refs:
|
|
1885
|
+
refs: selectedRefs,
|
|
1470
1886
|
fromCursor: latest?.toCursor ?? task.createdAt,
|
|
1471
1887
|
toCursor: new Date(Math.max(now.getTime(), Date.parse(mailbox.pending.lastQueuedAt))).toISOString(), now
|
|
1472
1888
|
});
|
|
@@ -1475,6 +1891,10 @@ export class FileSchedulerStoreAdapter {
|
|
|
1475
1891
|
owner: `leader-notification:${wakeId}`, startedAt: now.toISOString() });
|
|
1476
1892
|
store.saveTaskWake(taskId, wake);
|
|
1477
1893
|
store.saveWorkMailbox(mailbox);
|
|
1894
|
+
// The mailbox is only a hint. Carry ordinary refs into its next pending
|
|
1895
|
+
// batch, rather than delivering them with the chosen immediate successor.
|
|
1896
|
+
if (remainingRefs.length > 0)
|
|
1897
|
+
enqueueWork(store, target, "user-message", now, remainingRefs, { source: "controller", dedupeKey: `then-remainder:${wakeId}` });
|
|
1478
1898
|
return { wakeId, attemptId, disposition: "submit" };
|
|
1479
1899
|
});
|
|
1480
1900
|
}
|
|
@@ -1490,6 +1910,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
1490
1910
|
&& event.payload.attemptId === attemptId && event.payload.outcome === outcome)) {
|
|
1491
1911
|
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "notification.delivery", { attemptId, outcome, ...(detail === undefined ? {} : { detail }) }, now));
|
|
1492
1912
|
}
|
|
1913
|
+
if (store.getTask(taskId)?.status === "archived")
|
|
1914
|
+
return;
|
|
1493
1915
|
if (outcome !== "accepted" && outcome !== "rejected")
|
|
1494
1916
|
return;
|
|
1495
1917
|
store.saveWorkMailbox(completeProcessing(mailbox, attemptId));
|
|
@@ -1612,7 +2034,32 @@ export class FileSchedulerStoreAdapter {
|
|
|
1612
2034
|
nativeSessionId: input.nativeSessionId,
|
|
1613
2035
|
effective: input.effective
|
|
1614
2036
|
}, now);
|
|
1615
|
-
if (input.owner.scope === "
|
|
2037
|
+
if (input.owner.scope === "global") {
|
|
2038
|
+
let sessions = store.getGlobalRoleSessionSet(input.owner.roleName);
|
|
2039
|
+
let binding = sessions.providerBinding;
|
|
2040
|
+
if (binding == null) {
|
|
2041
|
+
sessions = bindGlobalRoleProviderRuntime(sessions, createProviderRuntimeBinding({
|
|
2042
|
+
providerNamespace: this.drivers.requireByAdapterId(input.adapterId).id,
|
|
2043
|
+
accountScope: input.agentId, conversationId: input.nativeSessionId,
|
|
2044
|
+
startedAt: now.toISOString()
|
|
2045
|
+
}), now);
|
|
2046
|
+
}
|
|
2047
|
+
else {
|
|
2048
|
+
if (currentProviderConversation(binding).conversationId !== input.nativeSessionId) {
|
|
2049
|
+
binding = supersedeProviderConversation(binding, {
|
|
2050
|
+
conversationId: input.nativeSessionId, switchedAt: now.toISOString(), basis: "terminal-session"
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
if (binding.authority.owner === "none")
|
|
2054
|
+
binding = transferProviderAuthority(binding, {
|
|
2055
|
+
expectedEpoch: binding.authority.epoch, expectedOwner: "none",
|
|
2056
|
+
owner: "controller", holderId: "controller", changedAt: now.toISOString()
|
|
2057
|
+
});
|
|
2058
|
+
sessions = updateGlobalRoleProviderRuntime(sessions, binding, now);
|
|
2059
|
+
}
|
|
2060
|
+
store.saveGlobalRoleSessionSet(sessions);
|
|
2061
|
+
}
|
|
2062
|
+
else {
|
|
1616
2063
|
let sessions = store.getTaskRoleSessionSet(input.owner.taskId, input.owner.roleName);
|
|
1617
2064
|
if (sessions !== null && sessions.providerBinding === null) {
|
|
1618
2065
|
sessions = bindTaskRoleProviderRuntime(sessions, createProviderRuntimeBinding({
|
|
@@ -1740,6 +2187,12 @@ export class FileSchedulerStoreAdapter {
|
|
|
1740
2187
|
&& sessions.providerBinding !== null) {
|
|
1741
2188
|
sessions = updateTaskRoleProviderRuntime(sessions, updateProviderConversationRecoverability(sessions.providerBinding, "unrecoverable"), now);
|
|
1742
2189
|
}
|
|
2190
|
+
if (input.observation?.kind === "turn.failed" && input.observation.payload.failure !== undefined
|
|
2191
|
+
&& sessions.providerBinding !== null) {
|
|
2192
|
+
sessions = updateTaskRoleProviderRuntime(sessions, recordProviderFailure(sessions.providerBinding, {
|
|
2193
|
+
error: input.observation.payload.failure.error, failureRef: input.observation.eventId, at: now.getTime()
|
|
2194
|
+
}), now);
|
|
2195
|
+
}
|
|
1743
2196
|
store.saveTaskRoleSessionSet(sessions);
|
|
1744
2197
|
let terminalRun;
|
|
1745
2198
|
if (recordedProviderTurn && observedRun?.status === "active") {
|
|
@@ -2082,6 +2535,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
2082
2535
|
return "obsolete";
|
|
2083
2536
|
}
|
|
2084
2537
|
store.saveTaskRoleSessionSet(recordStructuredProviderAcceptance(sessions, input, now));
|
|
2538
|
+
const retry = binding.retry;
|
|
2539
|
+
if (retry?.currentAttemptId === ordinaryAttemptId && retry.input.kind === "wake") {
|
|
2540
|
+
const originalWakeId = retry.input.wakeId;
|
|
2541
|
+
const original = store.listTaskWakes(input.fence.taskId).find(w => w.id === originalWakeId);
|
|
2542
|
+
if (original !== undefined && original.status !== "consumed") {
|
|
2543
|
+
store.saveTaskWake(input.fence.taskId, markTaskWakeConsumed(original, now));
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2085
2546
|
return "applied";
|
|
2086
2547
|
}
|
|
2087
2548
|
}
|
|
@@ -2261,6 +2722,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
2261
2722
|
effective
|
|
2262
2723
|
}, now);
|
|
2263
2724
|
current = rememberRoleAgentCompletedTurn(current, input.agentId, nativeSessionId, input.nativeTurnId, now);
|
|
2725
|
+
// decision-3 §9: when a controller-owned native Turn is actually bound for
|
|
2726
|
+
// this Global Role, settle it to the observed terminal so the scope-generic
|
|
2727
|
+
// resolver and the interrupt-then gate see the real stop on the live binding
|
|
2728
|
+
// — the same edge Task settles via settleStructuredProviderTurn. This is a
|
|
2729
|
+
// strict guarded no-op for an older unmanaged Session or a
|
|
2730
|
+
// binding holding a different/already-terminal Turn, and the
|
|
2731
|
+
// durable native terminal recorded just above remains the proof of record.
|
|
2732
|
+
current = settleGlobalRoleRuntimeTurn(current, input.nativeTurnId, input.providerStatus, now);
|
|
2264
2733
|
store.saveGlobalRoleSessionSet(current);
|
|
2265
2734
|
if (input.roleName === SYSTEM_OPERATOR_ROLE
|
|
2266
2735
|
&& input.adapterId === "codex"
|
|
@@ -2368,7 +2837,8 @@ function recordCanonicalObservationObsolete(store, input, reason, now) {
|
|
|
2368
2837
|
roleName: input.fence.roleName,
|
|
2369
2838
|
agentId: input.fence.agentId,
|
|
2370
2839
|
...(input.fence.runId === undefined ? {} : { runId: input.fence.runId }),
|
|
2371
|
-
...(input.fence.nativeSessionId === undefined ? {} : { nativeSessionId: input.fence.nativeSessionId })
|
|
2840
|
+
...(input.fence.nativeSessionId === undefined ? {} : { nativeSessionId: input.fence.nativeSessionId }),
|
|
2841
|
+
...(reason === "task-archived" ? { originalEvent: input } : {})
|
|
2372
2842
|
}, reason, now);
|
|
2373
2843
|
}
|
|
2374
2844
|
function recordObsoleteRuntimeEvent(store, input, reason, now) {
|
|
@@ -2383,7 +2853,8 @@ function recordObsoleteRuntimeEvent(store, input, reason, now) {
|
|
|
2383
2853
|
agentId: input.agentId,
|
|
2384
2854
|
...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
|
|
2385
2855
|
...(input.runId === undefined ? {} : { runId: input.runId }),
|
|
2386
|
-
reason
|
|
2856
|
+
reason,
|
|
2857
|
+
...(input.originalEvent === undefined ? {} : { originalEvent: JSON.stringify(input.originalEvent) })
|
|
2387
2858
|
}, now));
|
|
2388
2859
|
}
|
|
2389
2860
|
function sessionPreview(value) {
|
|
@@ -3033,6 +3504,29 @@ function settleStructuredProviderTurn(sessions, runId, status, now, attemptId) {
|
|
|
3033
3504
|
settledAt: now.toISOString()
|
|
3034
3505
|
}), now);
|
|
3035
3506
|
}
|
|
3507
|
+
/**
|
|
3508
|
+
* The Global twin of {@link settleStructuredProviderTurn}: settle a Global Role's
|
|
3509
|
+
* own live Provider Turn to the observed native terminal, keyed by the exact
|
|
3510
|
+
* native Turn id the real Provider Stop/StopFailure hook carries (decision-3 §9).
|
|
3511
|
+
* It is a strict guarded no-op unless a controller-owned Turn with that exact
|
|
3512
|
+
* native id is bound and still in flight (`accepted`) — an absent binding, a
|
|
3513
|
+
* different/only-submitting Turn, or an already-terminal one is left untouched, so
|
|
3514
|
+
* an older unmanaged Session without a controlled binding settles nothing
|
|
3515
|
+
* and the durable `recentCompletedTurnIds` terminal remains the proof of record.
|
|
3516
|
+
*/
|
|
3517
|
+
function settleGlobalRoleRuntimeTurn(sessions, nativeTurnId, status, now) {
|
|
3518
|
+
const binding = sessions.providerBinding;
|
|
3519
|
+
if (binding === null || binding === undefined || binding.run === null
|
|
3520
|
+
|| binding.run.nativeTurnId !== nativeTurnId
|
|
3521
|
+
|| binding.run.status !== "accepted") {
|
|
3522
|
+
return sessions;
|
|
3523
|
+
}
|
|
3524
|
+
return updateGlobalRoleProviderRuntime(sessions, settleProviderTurn(binding, {
|
|
3525
|
+
nativeTurnId,
|
|
3526
|
+
status,
|
|
3527
|
+
settledAt: now.toISOString()
|
|
3528
|
+
}), now);
|
|
3529
|
+
}
|
|
3036
3530
|
function usageSnapshotIsSuperseded(events, incoming) {
|
|
3037
3531
|
if (incoming.kind !== "activity.observed" || incoming.payload.usage === undefined) {
|
|
3038
3532
|
return false;
|