@zq-silk/yui 0.15.9 → 0.15.11
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/dist/agent/launchEnvironment.js +7 -0
- package/dist/cli/commandCatalog.js +26 -6
- package/dist/cli/interactionPolicy.js +3 -3
- 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 +336 -93
- package/dist/commands/globalRoleCommands.js +314 -4
- package/dist/commands/projectCommands.js +6 -7
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +511 -83
- package/dist/commands/taskIntegrationCommands.js +16 -38
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/taskContext.js +38 -2
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +3 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
- package/dist/controller/globalInputDelivery.js +119 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +81 -6
- 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 +312 -382
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/message/globalInterrupt.js +33 -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/orchestrationMetrics.js +1 -1
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +72 -15
- package/dist/repository/taskWorkspaceCoordinator.js +134 -0
- package/dist/repository/taskWorkspacePreparer.js +120 -49
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentHost.js +152 -72
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +12 -1
- 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 +136 -4
- package/dist/storage/sqliteStore.js +40 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +128 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/web/assets/client/app.js +55 -0
- package/dist/web/assets/client/components.js +1 -0
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/assets/client/taskSurface.js +106 -0
- package/dist/web/assets/client/view.js +7 -4
- package/dist/web/assets/shell.js +23 -0
- package/dist/web/assets/styles/layout.js +1 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +117 -1
- package/dist/web/webSnapshot.js +4 -3
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/workItemChangeSetManager.js +18 -2
- 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/testing/verification-levels.md +24 -0
- package/docs/testing/verification-levels.zh-CN.md +11 -0
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +151 -49
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +6 -1
- package/skills/yui-runtime/SKILL.md +27 -0
- package/skills/yui-runtime/references/publication.md +20 -0
|
@@ -1,10 +1,12 @@
|
|
|
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 { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindGlobalRoleProviderRuntime, createRoleSessionSet, recordRoleAgentSession, replaceTaskRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, detachRoleAgentSessionHost, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime, updateGlobalRoleProviderRuntime, selectNewTaskRoleSession, taskRoleControlTarget } from "../executor/agentExecutor.js";
|
|
8
10
|
import { acceptProviderTurn, cancelQuiescentProviderInput, beginProviderTurn, createProviderRuntimeBinding, currentProviderConversation, managedProviderTurnId, clearProviderGoal, settleProviderTurnSubmission, settleProviderTurn, transferProviderAuthority, supersedeProviderConversation, updateProviderConversationRecoverability, updateProviderGoal } from "../runtime/providerRuntimeIdentity.js";
|
|
9
11
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
10
12
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
@@ -17,6 +19,7 @@ import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
|
|
|
17
19
|
import { appendRunInput, createRun, withRunContextSnapshot, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
18
20
|
import { transportAgentResult } from "../domain/agentResultTransport.js";
|
|
19
21
|
import { createRunInput } from "../context/runInputContract.js";
|
|
22
|
+
import { recordTaskMessageControlOutcome, markGlobalRoleMessageDelivered, markGlobalRoleMessageNotDelivered } from "../message/message.js";
|
|
20
23
|
import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
|
|
21
24
|
import { terminalizeExactTaskRun, cancelQuiescentRoleRuns } from "../lifecycle/exactRunTerminalization.js";
|
|
22
25
|
import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
|
|
@@ -29,7 +32,7 @@ import { pendingWakeupProjection } from "../storage/taskStore.js";
|
|
|
29
32
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
30
33
|
import { providerContinuationKey } from "../runtime/providerContinuation.js";
|
|
31
34
|
import { formatRunReceiptId } from "../task/taskRecordReference.js";
|
|
32
|
-
import { bindExecution, claimPending, completeProcessing, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
|
|
35
|
+
import { bindExecution, claimPending, completeProcessing, consumePendingBatch, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
|
|
33
36
|
import { enqueueWork, settleRoleRunDispatch as settleRoleRunDispatchMailbox } from "../coordination/workMailboxQueue.js";
|
|
34
37
|
import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
35
38
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
@@ -58,6 +61,57 @@ export class FileSchedulerStoreAdapter {
|
|
|
58
61
|
this.drivers = drivers;
|
|
59
62
|
this.snapshotExecutionLaneWorkspace = snapshotExecutionLaneWorkspace;
|
|
60
63
|
}
|
|
64
|
+
observeAgentHostObservation(event, now = new Date()) {
|
|
65
|
+
// Match canonical ingress: archive and Host fact settlement share the
|
|
66
|
+
// same write boundary, including the original transport envelope.
|
|
67
|
+
return this.store.transaction(() => this.foldAgentHostObservation(event, now));
|
|
68
|
+
}
|
|
69
|
+
foldAgentHostObservation(event, now) {
|
|
70
|
+
const raw = createRuntimeObservation(event.observation);
|
|
71
|
+
const task = raw.fence.taskId === undefined ? null : this.store.getTask(raw.fence.taskId);
|
|
72
|
+
if (raw.fence.taskId !== undefined && task === null)
|
|
73
|
+
return "obsolete";
|
|
74
|
+
if (task?.status === "archived") {
|
|
75
|
+
this.observeObsoleteRuntimeEvent({
|
|
76
|
+
eventId: event.id, eventType: event.type, taskId: task.id,
|
|
77
|
+
roleName: raw.fence.roleName, agentId: raw.fence.agentId,
|
|
78
|
+
nativeSessionId: raw.fence.nativeSessionId ?? "unknown",
|
|
79
|
+
reason: "task-archived", originalEvent: event
|
|
80
|
+
}, now);
|
|
81
|
+
return "obsolete";
|
|
82
|
+
}
|
|
83
|
+
if (Date.parse(raw.receivedAt) > now.getTime()) {
|
|
84
|
+
return this.recordObsoleteCanonicalObservation(raw, "received-at-in-future", now);
|
|
85
|
+
}
|
|
86
|
+
let input;
|
|
87
|
+
try {
|
|
88
|
+
if (event.observation.kind === "host.observed") {
|
|
89
|
+
return this.store.transaction(store => {
|
|
90
|
+
const connection = resolveAgentHostObservation(store, event);
|
|
91
|
+
recordAgentHostConnection(store, event, connection, now);
|
|
92
|
+
if (connection.fence.taskId !== undefined)
|
|
93
|
+
this.persistRuntimeObservation(connection, now);
|
|
94
|
+
return "applied";
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
input = resolveAgentHostObservation(this.store, event);
|
|
98
|
+
if (input.kind === "input.accepted" && input.fence.runId === undefined
|
|
99
|
+
&& input.fence.receiptId?.startsWith("native-input:")) {
|
|
100
|
+
// A valid ordinary conversation has no managed Run input to mirror.
|
|
101
|
+
// Consuming this fact is not rejection, nor assignment acceptance.
|
|
102
|
+
return "applied";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof AgentHostObservationDeferred)
|
|
107
|
+
return "deferred";
|
|
108
|
+
if (!(error instanceof RuntimeHookRunFenceError))
|
|
109
|
+
throw error;
|
|
110
|
+
// Rejection is durable diagnostic evidence, never business acceptance.
|
|
111
|
+
return this.recordObsoleteCanonicalObservation(event.observation, error.message, now);
|
|
112
|
+
}
|
|
113
|
+
return this.observeRuntimeObservation(input, now);
|
|
114
|
+
}
|
|
61
115
|
/**
|
|
62
116
|
* Sole provider-independent ingress for structured runtime state. Driver
|
|
63
117
|
* mapping has already happened before this boundary; this method validates
|
|
@@ -65,10 +119,19 @@ export class FileSchedulerStoreAdapter {
|
|
|
65
119
|
* transitions, and retains the canonical observation for status projection.
|
|
66
120
|
*/
|
|
67
121
|
observeRuntimeObservation(raw, now = new Date()) {
|
|
122
|
+
// Archive and observation settlement share the same write boundary.
|
|
123
|
+
// An ingress read outside this transaction must never authorize clearing
|
|
124
|
+
// a claim after another CLI has committed archive.
|
|
125
|
+
return this.store.transaction(() => this.foldRuntimeObservation(raw, now));
|
|
126
|
+
}
|
|
127
|
+
foldRuntimeObservation(raw, now) {
|
|
68
128
|
const input = createRuntimeObservation(raw);
|
|
69
129
|
const taskId = input.fence.taskId;
|
|
70
130
|
if (taskId === undefined)
|
|
71
|
-
return
|
|
131
|
+
return this.observeGlobalProviderObservation(input, now);
|
|
132
|
+
if (this.store.getTask(taskId)?.status === "archived") {
|
|
133
|
+
return this.recordObsoleteCanonicalObservation(input, "task-archived", now);
|
|
134
|
+
}
|
|
72
135
|
if (Date.parse(input.receivedAt) > now.getTime()) {
|
|
73
136
|
return this.recordObsoleteCanonicalObservation(input, "received-at-in-future", now);
|
|
74
137
|
}
|
|
@@ -83,12 +146,18 @@ export class FileSchedulerStoreAdapter {
|
|
|
83
146
|
const run = input.fence.runId === undefined ? null : store.getRun(taskId, input.fence.runId);
|
|
84
147
|
const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
|
|
85
148
|
const native = sessions?.providerBinding?.run;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
149
|
+
const isSteer = input.fence.receiptId.startsWith("steer:");
|
|
150
|
+
// A Worker/Reviewer steer's accepted input is folded onto the owning
|
|
151
|
+
// AgentRun. A no-Run Leader steer (decision-3 §9) has no run to append to,
|
|
152
|
+
// but its accepted disposition is still a real fact that gap D records on
|
|
153
|
+
// the Message; only the Worker append requires the exact Run/Turn fence.
|
|
154
|
+
const runFolded = run !== null && run.roleName === input.fence.roleName
|
|
155
|
+
&& run.effective.agentId === input.fence.agentId && native?.runId === run.id
|
|
156
|
+
&& native.nativeTurnId === input.fence.nativeTurnId
|
|
157
|
+
&& sessions?.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId;
|
|
158
|
+
if (!runFolded && !(isSteer && input.fence.runId === undefined))
|
|
90
159
|
return "obsolete";
|
|
91
|
-
if (!hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
|
|
160
|
+
if (runFolded && !hasPersistedRuntimeObservation(store.listEvents(taskId), input) && run.status === "active") {
|
|
92
161
|
const updated = appendRunInput(run, createRunInput({
|
|
93
162
|
source: input.fence.receiptId.startsWith("native-input:")
|
|
94
163
|
? { type: "provider", channel: "visible-input" }
|
|
@@ -98,6 +167,31 @@ export class FileSchedulerStoreAdapter {
|
|
|
98
167
|
store.saveRun(updated);
|
|
99
168
|
store.saveActiveRun(updated);
|
|
100
169
|
}
|
|
170
|
+
// decision-3 §3/§8, message-5 gap D: promote the Message's independent
|
|
171
|
+
// control op to its proven terminal from the Host's own settlement, keyed
|
|
172
|
+
// by this exact steer receipt. Monotonic and idempotent — never a rewind
|
|
173
|
+
// of an already-proven outcome, and a replayed observation is absorbed.
|
|
174
|
+
if (isSteer)
|
|
175
|
+
this.recordSteerControlOutcome(store, input.fence.receiptId, "accepted", now);
|
|
176
|
+
this.persistRuntimeObservation(input, now);
|
|
177
|
+
return "applied";
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// A steer's rejected/delivery-unknown disposition is the Host's proven
|
|
181
|
+
// terminal for the one live control attempt (message-5 gap D). It carries no
|
|
182
|
+
// AgentRun append — a rejected input changed no execution — so it is recorded
|
|
183
|
+
// only on the Message's control op, for a Worker/Reviewer or a no-Run Leader
|
|
184
|
+
// steer alike, keyed by the exact steer receipt.
|
|
185
|
+
if ((input.kind === "input.rejected" || input.kind === "input.delivery-unknown")
|
|
186
|
+
&& input.fence.receiptId?.startsWith("steer:") === true) {
|
|
187
|
+
return this.store.transaction((store) => {
|
|
188
|
+
if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
|
|
189
|
+
return "applied";
|
|
190
|
+
const recorded = this.recordSteerControlOutcome(store, input.fence.receiptId, input.kind === "input.rejected" ? "rejected" : "delivery-unknown", now);
|
|
191
|
+
if (!recorded) {
|
|
192
|
+
recordCanonicalObservationObsolete(store, input, "steer-message-not-found", now);
|
|
193
|
+
return "obsolete";
|
|
194
|
+
}
|
|
101
195
|
this.persistRuntimeObservation(input, now);
|
|
102
196
|
return "applied";
|
|
103
197
|
});
|
|
@@ -173,6 +267,38 @@ export class FileSchedulerStoreAdapter {
|
|
|
173
267
|
}
|
|
174
268
|
return outcome;
|
|
175
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Promote a steer Message's independent control op to a proven outcome from the
|
|
272
|
+
* Host's own settlement (decision-3 §3/§8, message-5 gap D). The `steer:` fence
|
|
273
|
+
* names the exact `steer:<taskId>/<messageId>` receipt, so the outcome binds to
|
|
274
|
+
* that one Message and never another; the requestId is read from the Message's
|
|
275
|
+
* own steer identity (its live input, or the reused-steer provenance a handoff
|
|
276
|
+
* preserved) so the recorded op matches the same idempotency key the live edge
|
|
277
|
+
* used. Returns false when the receipt names no such steer Message, so the
|
|
278
|
+
* caller can mark the observation obsolete rather than silently drop it. The
|
|
279
|
+
* record is monotonic and idempotent inside {@link recordTaskMessageControlOutcome}.
|
|
280
|
+
*/
|
|
281
|
+
recordSteerControlOutcome(store, receiptId, outcome, now) {
|
|
282
|
+
const parsed = /^steer:([^/]+)\/(.+)$/.exec(receiptId);
|
|
283
|
+
if (parsed === null)
|
|
284
|
+
return false;
|
|
285
|
+
const [, taskId, messageId] = parsed;
|
|
286
|
+
const message = store.listMessages(taskId).find((entry) => entry.id === messageId);
|
|
287
|
+
const requestId = message?.inputControl?.requestId ?? message?.interruptThen?.reusedInput?.requestId;
|
|
288
|
+
const wasSteer = message?.inputControl?.action === "steer"
|
|
289
|
+
|| message?.interruptThen?.reusedInput?.action === "steer";
|
|
290
|
+
if (message === undefined || requestId === undefined || !wasSteer)
|
|
291
|
+
return false;
|
|
292
|
+
const recorded = recordTaskMessageControlOutcome(message, {
|
|
293
|
+
requestId, receiptId, outcome, observedAt: now
|
|
294
|
+
});
|
|
295
|
+
// The monotonic guard returns the same Message when the record is absorbed
|
|
296
|
+
// (an idempotent repeat, or a stale outcome that must not rewind a proven
|
|
297
|
+
// terminal); only persist a real change.
|
|
298
|
+
if (recorded !== message)
|
|
299
|
+
store.updateMessage(taskId, recorded);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
176
302
|
/** A delayed steer receipt settles only its original claimed mailbox batch. */
|
|
177
303
|
observeLeaderSteerReceipt(input, now) {
|
|
178
304
|
return this.store.transaction((store) => {
|
|
@@ -238,6 +364,95 @@ export class FileSchedulerStoreAdapter {
|
|
|
238
364
|
return "applied";
|
|
239
365
|
});
|
|
240
366
|
}
|
|
367
|
+
observeGlobalProviderObservation(input, now) {
|
|
368
|
+
return this.store.transaction((store) => {
|
|
369
|
+
const fence = input.fence;
|
|
370
|
+
let sessions = store.getGlobalRoleSessionSet(fence.roleName);
|
|
371
|
+
const session = sessions?.sessions[fence.agentId];
|
|
372
|
+
let binding = sessions?.providerBinding;
|
|
373
|
+
if (sessions == null || session == null || binding == null || fence.runId !== undefined
|
|
374
|
+
|| sessions.activeAgentId !== fence.agentId
|
|
375
|
+
|| session.nativeSessionId !== fence.nativeSessionId
|
|
376
|
+
|| this.drivers.requireByAdapterId(session.adapterId).id !== fence.driverId
|
|
377
|
+
|| currentProviderConversation(binding).conversationId !== fence.nativeSessionId
|
|
378
|
+
|| Date.parse(input.receivedAt) > now.getTime())
|
|
379
|
+
return "obsolete";
|
|
380
|
+
const at = input.observedAt ?? input.receivedAt;
|
|
381
|
+
const receipt = fence.receiptId;
|
|
382
|
+
const matches = receipt !== undefined && binding.run?.attemptId === receipt
|
|
383
|
+
&& (fence.nativeTurnId === undefined || binding.run.nativeTurnId === undefined
|
|
384
|
+
|| binding.run.nativeTurnId === fence.nativeTurnId);
|
|
385
|
+
if (input.kind === "turn.accepted") {
|
|
386
|
+
if (input.authority === "transport" && matches) {
|
|
387
|
+
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
|
|
388
|
+
if (message !== undefined)
|
|
389
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at), "transport"));
|
|
390
|
+
return "applied";
|
|
391
|
+
}
|
|
392
|
+
if (input.authority !== "provider-structured")
|
|
393
|
+
return "applied";
|
|
394
|
+
if (!matches) {
|
|
395
|
+
if (receipt?.startsWith("direct:") !== true || binding.run != null
|
|
396
|
+
&& ["submitting", "accepted", "delivery-unknown"].includes(binding.run.status))
|
|
397
|
+
return "obsolete";
|
|
398
|
+
binding = beginProviderTurn(binding, { attemptId: receipt,
|
|
399
|
+
authorityEpoch: binding.authority.epoch, submittedAt: at });
|
|
400
|
+
}
|
|
401
|
+
if (binding.run?.status === "submitting" || binding.run?.status === "delivery-unknown") {
|
|
402
|
+
binding = acceptProviderTurn(binding, { attemptId: receipt,
|
|
403
|
+
nativeTurnId: fence.nativeTurnId, acceptedAt: at });
|
|
404
|
+
}
|
|
405
|
+
else if (binding.run?.status !== "accepted")
|
|
406
|
+
return "applied";
|
|
407
|
+
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
|
|
408
|
+
if (message !== undefined)
|
|
409
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
|
|
410
|
+
}
|
|
411
|
+
else if (["turn.completed", "turn.cancelled", "turn.failed"].includes(input.kind)) {
|
|
412
|
+
if (!matches || input.authority !== "provider-structured")
|
|
413
|
+
return "obsolete";
|
|
414
|
+
// A provider final result proves this exact submitted input ran (Claude
|
|
415
|
+
// may emit its result without an earlier assistant acceptance event).
|
|
416
|
+
// Process exit/failure alone must not turn unconfirmed input into proof.
|
|
417
|
+
if (input.kind === "turn.completed" && binding.run != null
|
|
418
|
+
&& ["submitting", "delivery-unknown"].includes(binding.run.status)) {
|
|
419
|
+
binding = acceptProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId, acceptedAt: at });
|
|
420
|
+
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
|
|
421
|
+
if (message !== undefined)
|
|
422
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageDelivered(message, new Date(at)));
|
|
423
|
+
}
|
|
424
|
+
if (binding.run?.status !== "accepted")
|
|
425
|
+
return binding.run != null
|
|
426
|
+
&& ["completed", "cancelled", "failed"].includes(binding.run.status) ? "applied" : "obsolete";
|
|
427
|
+
binding = settleProviderTurn(binding, { attemptId: receipt, nativeTurnId: fence.nativeTurnId,
|
|
428
|
+
status: input.kind === "turn.completed" ? "completed" : input.kind === "turn.cancelled" ? "cancelled" : "failed",
|
|
429
|
+
settledAt: at });
|
|
430
|
+
if (fence.nativeTurnId !== undefined)
|
|
431
|
+
sessions = rememberRoleAgentCompletedTurn(sessions, fence.agentId, fence.nativeSessionId, fence.nativeTurnId, new Date(at));
|
|
432
|
+
}
|
|
433
|
+
else if (["input.accepted", "input.rejected", "input.delivery-unknown"].includes(input.kind)) {
|
|
434
|
+
if (input.authority !== "provider-structured" || binding.run?.nativeTurnId !== fence.nativeTurnId
|
|
435
|
+
|| binding.run?.status !== "accepted")
|
|
436
|
+
return "obsolete";
|
|
437
|
+
const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `steer:${fence.roleName}/${entry.id}`);
|
|
438
|
+
if (message !== undefined && message.inputControl !== undefined) {
|
|
439
|
+
const outcome = input.kind === "input.accepted" ? "accepted"
|
|
440
|
+
: input.kind === "input.rejected" ? "rejected" : "delivery-unknown";
|
|
441
|
+
if (message.control?.outcome !== "accepted" && message.control?.outcome !== "rejected") {
|
|
442
|
+
const updated = { ...message, control: { requestId: message.inputControl.requestId,
|
|
443
|
+
receiptId: receipt, outcome, observedAt: at } };
|
|
444
|
+
store.updateGlobalRoleMessage(outcome === "accepted"
|
|
445
|
+
? markGlobalRoleMessageDelivered(updated, new Date(at)) : updated);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else if (input.kind === "conversation.observed" && input.payload.recoverability !== undefined) {
|
|
450
|
+
binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
|
|
451
|
+
}
|
|
452
|
+
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, binding, now));
|
|
453
|
+
return "applied";
|
|
454
|
+
});
|
|
455
|
+
}
|
|
241
456
|
adapterForRuntimeObservation(input) {
|
|
242
457
|
const taskId = input.fence.taskId;
|
|
243
458
|
const runId = input.fence.runId;
|
|
@@ -871,6 +1086,67 @@ export class FileSchedulerStoreAdapter {
|
|
|
871
1086
|
});
|
|
872
1087
|
}
|
|
873
1088
|
beginAgentHostProviderTurn(input) {
|
|
1089
|
+
if (input.taskId === undefined) {
|
|
1090
|
+
this.store.transaction((store) => {
|
|
1091
|
+
if (input.runId !== undefined || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: input.roleName })))) {
|
|
1092
|
+
throw new AgentHostProviderTurnFenceError("Global Session admission is stopped.");
|
|
1093
|
+
}
|
|
1094
|
+
const sessions = store.getGlobalRoleSessionSet(input.roleName);
|
|
1095
|
+
const binding = sessions?.providerBinding;
|
|
1096
|
+
const session = sessions?.sessions[input.agentId];
|
|
1097
|
+
if (sessions === null || binding == null || session === undefined
|
|
1098
|
+
|| sessions.activeAgentId !== input.agentId || session.status !== "active"
|
|
1099
|
+
|| session.nativeSessionId !== input.nativeSessionId
|
|
1100
|
+
|| currentProviderConversation(binding).conversationId !== input.nativeSessionId
|
|
1101
|
+
|| binding.authority.owner !== input.authorityOwner
|
|
1102
|
+
|| binding.authority.epoch !== input.authorityEpoch
|
|
1103
|
+
|| binding.authority.holderId !== input.holderId) {
|
|
1104
|
+
throw new AgentHostProviderTurnFenceError("Global input carries a stale Session or writer fence.");
|
|
1105
|
+
}
|
|
1106
|
+
const turn = binding.run;
|
|
1107
|
+
if (turn?.attemptId === input.attemptId && turn.status === "submitting")
|
|
1108
|
+
return;
|
|
1109
|
+
if (turn != null && ["submitting", "accepted", "delivery-unknown"].includes(turn.status)) {
|
|
1110
|
+
throw new AgentHostProviderSessionBusyError("Global Conversation has an unsettled input.");
|
|
1111
|
+
}
|
|
1112
|
+
const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
|
|
1113
|
+
const reserved = store.listGlobalRoleMessages(input.roleName).find(entry => entry.delivery === undefined && entry.notDelivered === undefined
|
|
1114
|
+
&& entry.interruptThen?.targetNativeSessionId === input.nativeSessionId
|
|
1115
|
+
&& entry.interruptThen.targetAgentId === input.agentId
|
|
1116
|
+
&& entry.interruptThen.targetAuthorityEpoch === input.authorityEpoch
|
|
1117
|
+
&& entry.interruptThen.targetAuthorityHolderId === input.holderId);
|
|
1118
|
+
if (reserved !== undefined && reserved.id !== message?.id) {
|
|
1119
|
+
throw new AgentHostProviderSessionBusyError("The next Global input belongs to an explicit then handoff.");
|
|
1120
|
+
}
|
|
1121
|
+
if (message !== undefined) {
|
|
1122
|
+
if (message.delivery !== undefined || message.notDelivered !== undefined
|
|
1123
|
+
|| message.control !== undefined && message.control.outcome !== "rejected"
|
|
1124
|
+
|| message.deliveryTarget?.agentId !== input.agentId
|
|
1125
|
+
|| message.deliveryTarget?.nativeSessionId !== input.nativeSessionId) {
|
|
1126
|
+
throw new AgentHostProviderTurnFenceError("Global Message is settled, unconfirmed, or bound to another Session.");
|
|
1127
|
+
}
|
|
1128
|
+
const claim = message.interruptThen;
|
|
1129
|
+
if (claim !== undefined && (claim.targetAgentId !== input.agentId
|
|
1130
|
+
|| claim.targetNativeSessionId !== input.nativeSessionId
|
|
1131
|
+
|| claim.targetAuthorityEpoch !== input.authorityEpoch
|
|
1132
|
+
|| claim.targetAuthorityHolderId !== input.holderId
|
|
1133
|
+
|| turn?.attemptId !== claim.targetAttemptId
|
|
1134
|
+
|| !["completed", "failed", "cancelled"].includes(turn.status))) {
|
|
1135
|
+
throw new AgentHostProviderTurnFenceError("Global then target no longer has exact terminal proof.");
|
|
1136
|
+
}
|
|
1137
|
+
store.updateGlobalRoleMessage({ ...message, control: {
|
|
1138
|
+
requestId: message.inputControl?.requestId ?? message.interruptThen.requestId,
|
|
1139
|
+
receiptId: input.attemptId, outcome: "pending", observedAt: input.now.toISOString()
|
|
1140
|
+
} });
|
|
1141
|
+
}
|
|
1142
|
+
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, beginProviderTurn(binding, { attemptId: input.attemptId,
|
|
1143
|
+
authorityEpoch: input.authorityEpoch, submittedAt: input.now.toISOString() }), input.now));
|
|
1144
|
+
});
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
this.beginTaskAgentHostProviderTurn({ ...input, taskId: input.taskId });
|
|
1148
|
+
}
|
|
1149
|
+
beginTaskAgentHostProviderTurn(input) {
|
|
874
1150
|
this.store.transaction((store) => {
|
|
875
1151
|
const task = store.getTask(input.taskId);
|
|
876
1152
|
if (task === null || !["active", "draft"].includes(task.status) || task.executionGate.state !== "enabled"
|
|
@@ -908,6 +1184,36 @@ export class FileSchedulerStoreAdapter {
|
|
|
908
1184
|
.includes(binding.run.status)) {
|
|
909
1185
|
throw new AgentHostProviderSessionBusyError("Provider Conversation already has an unsettled AgentRun.");
|
|
910
1186
|
}
|
|
1187
|
+
if (!exactReplay) {
|
|
1188
|
+
const claims = store.listMessages(input.taskId).filter(message => message.interruptThen?.targetRoleName === input.roleName
|
|
1189
|
+
&& message.interruptThen.targetNativeSessionId === input.nativeSessionId
|
|
1190
|
+
&& message.interruptThen.targetAgentId === input.agentId
|
|
1191
|
+
&& message.interruptThen.targetAuthorityEpoch === input.authorityEpoch
|
|
1192
|
+
&& message.interruptThen.targetAuthorityHolderId === input.holderId
|
|
1193
|
+
&& message.interruptThen.notDeliveredReason === undefined
|
|
1194
|
+
&& message.continuation?.notDeliveredReason === undefined);
|
|
1195
|
+
for (const message of claims) {
|
|
1196
|
+
if (input.roleName === "leader") {
|
|
1197
|
+
const wakes = store.listTaskWakes(input.taskId).filter(wake => wake.refs?.some(ref => ref.type === "message" && ref.id === message.id));
|
|
1198
|
+
if (wakes.some(wake => wake.status === "consumed"))
|
|
1199
|
+
continue;
|
|
1200
|
+
const mailbox = store.getWorkMailbox({ kind: "role", taskId: input.taskId, roleName: "leader" });
|
|
1201
|
+
const selected = wakes.some(wake => mailbox?.processing?.owner === `leader-notification:${wake.id}`
|
|
1202
|
+
&& mailbox.processing.batchId === input.attemptId);
|
|
1203
|
+
if (!selected)
|
|
1204
|
+
throw new AgentHostProviderSessionBusyError("The next Leader input belongs to an explicit then handoff.");
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
const continuationId = message.continuation?.runId;
|
|
1208
|
+
const continuation = continuationId === undefined ? null : store.getRun(input.taskId, continuationId);
|
|
1209
|
+
if (continuation !== null && (continuation.status !== "active" || currentRun?.runId === continuation.id))
|
|
1210
|
+
continue;
|
|
1211
|
+
if (input.runId !== continuationId || continuationId === undefined) {
|
|
1212
|
+
throw new AgentHostProviderSessionBusyError("The next execution input belongs to an explicit then handoff.");
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
911
1217
|
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
|
|
912
1218
|
...(input.runId === undefined ? {} : { runId: input.runId }),
|
|
913
1219
|
attemptId: input.attemptId,
|
|
@@ -917,6 +1223,31 @@ export class FileSchedulerStoreAdapter {
|
|
|
917
1223
|
});
|
|
918
1224
|
}
|
|
919
1225
|
resolveAgentHostProviderTurnSubmission(input) {
|
|
1226
|
+
if (input.taskId === undefined) {
|
|
1227
|
+
this.store.transaction((store) => {
|
|
1228
|
+
const sessions = store.getGlobalRoleSessionSet(input.roleName);
|
|
1229
|
+
const binding = sessions?.providerBinding;
|
|
1230
|
+
if (input.runId !== undefined || sessions == null || binding?.run?.attemptId !== input.attemptId) {
|
|
1231
|
+
throw new Error("Global input submission is no longer current.");
|
|
1232
|
+
}
|
|
1233
|
+
store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, settleProviderTurnSubmission(binding, { attemptId: input.attemptId, status: input.status,
|
|
1234
|
+
reason: input.reason, resolvedAt: input.now.toISOString() }), input.now));
|
|
1235
|
+
const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
|
|
1236
|
+
if (message !== undefined && message.delivery?.via !== "provider") {
|
|
1237
|
+
const outcome = input.status === "delivery-unknown" ? "delivery-unknown" : "rejected";
|
|
1238
|
+
const updated = { ...message, control: {
|
|
1239
|
+
requestId: message.inputControl?.requestId ?? message.interruptThen.requestId,
|
|
1240
|
+
receiptId: input.attemptId, outcome, observedAt: input.now.toISOString()
|
|
1241
|
+
} };
|
|
1242
|
+
store.updateGlobalRoleMessage(input.status === "rejected" && updated.delivery === undefined
|
|
1243
|
+
? markGlobalRoleMessageNotDelivered(updated, input.reason, input.now) : updated);
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
this.resolveTaskAgentHostProviderTurnSubmission({ ...input, taskId: input.taskId });
|
|
1249
|
+
}
|
|
1250
|
+
resolveTaskAgentHostProviderTurnSubmission(input) {
|
|
920
1251
|
this.store.transaction((store) => {
|
|
921
1252
|
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
922
1253
|
const binding = sessions?.providerBinding;
|
|
@@ -1014,6 +1345,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
1014
1345
|
}
|
|
1015
1346
|
enqueueLeaderWakeup(taskId, reason, now) {
|
|
1016
1347
|
return this.store.transaction((store) => {
|
|
1348
|
+
if (store.getTask(taskId)?.status === "archived")
|
|
1349
|
+
return null;
|
|
1017
1350
|
const mailbox = enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, reason, now);
|
|
1018
1351
|
const wakeup = pendingWakeupProjection(mailbox);
|
|
1019
1352
|
if (wakeup === null)
|
|
@@ -1462,11 +1795,47 @@ export class FileSchedulerStoreAdapter {
|
|
|
1462
1795
|
if (mailbox.pending === null || (provider !== null && provider !== undefined
|
|
1463
1796
|
&& ["submitting", "accepted", "delivery-unknown"].includes(provider.status)))
|
|
1464
1797
|
return null;
|
|
1798
|
+
const pending = mailbox.pending;
|
|
1799
|
+
const pendingMessageIds = new Set(pending.refs.filter((ref) => ref.type === "message").map((ref) => ref.id));
|
|
1800
|
+
const claimedMessages = store.listMessages(taskId).filter((message) => pendingMessageIds.has(message.id) && message.interruptThen?.targetRoleName === "leader");
|
|
1801
|
+
const sessions = store.getTaskRoleSessionSet(taskId, "leader");
|
|
1802
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
1803
|
+
const obsoleteIds = new Set();
|
|
1804
|
+
for (const message of claimedMessages) {
|
|
1805
|
+
const claim = message.interruptThen;
|
|
1806
|
+
if (claim.notDeliveredReason !== undefined
|
|
1807
|
+
|| session?.nativeSessionId !== claim.targetNativeSessionId
|
|
1808
|
+
|| session.agentId !== claim.targetAgentId
|
|
1809
|
+
|| session.adapterId !== claim.targetAdapterId
|
|
1810
|
+
|| (sessions?.providerBinding != null && (sessions.providerBinding.authority.epoch !== claim.targetAuthorityEpoch
|
|
1811
|
+
|| sessions.providerBinding.authority.holderId !== claim.targetAuthorityHolderId))) {
|
|
1812
|
+
obsoleteIds.add(message.id);
|
|
1813
|
+
if (claim.notDeliveredReason === undefined)
|
|
1814
|
+
store.updateMessage(taskId, {
|
|
1815
|
+
...message, interruptThen: { ...claim, notDeliveredReason: "target-session-or-authority-changed" }
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const availableRefs = pending.refs.filter(ref => !(ref.type === "message" && obsoleteIds.has(ref.id)));
|
|
1820
|
+
const handoffs = claimedMessages.filter(message => !obsoleteIds.has(message.id));
|
|
1821
|
+
// A quiet/new Session is not evidence that the claimed old Turn stopped.
|
|
1822
|
+
// Keep the exact pending intent until the original fence is provable.
|
|
1823
|
+
if (handoffs.some((message) => interruptThenTerminalState(store, message) !== "ready"))
|
|
1824
|
+
return null;
|
|
1825
|
+
const handoffIds = new Set(handoffs.map((message) => message.id));
|
|
1826
|
+
if (availableRefs.length === 0 && obsoleteIds.size > 0) {
|
|
1827
|
+
store.saveWorkMailbox(consumePendingBatch(mailbox, pending));
|
|
1828
|
+
return null;
|
|
1829
|
+
}
|
|
1830
|
+
const selectedRefs = handoffs.length === 0 ? availableRefs
|
|
1831
|
+
: availableRefs.filter((ref) => ref.type === "message" && handoffIds.has(ref.id));
|
|
1832
|
+
const remainingRefs = handoffs.length === 0 ? []
|
|
1833
|
+
: availableRefs.filter((ref) => !(ref.type === "message" && handoffIds.has(ref.id)));
|
|
1465
1834
|
const latest = latestTaskWake(store.listTaskWakes(taskId));
|
|
1466
1835
|
const wakeId = store.nextTaskWakeId(taskId);
|
|
1467
1836
|
const wake = createTaskWake({
|
|
1468
1837
|
id: wakeId, taskId, reasons: mailbox.pending.reasons,
|
|
1469
|
-
refs:
|
|
1838
|
+
refs: selectedRefs,
|
|
1470
1839
|
fromCursor: latest?.toCursor ?? task.createdAt,
|
|
1471
1840
|
toCursor: new Date(Math.max(now.getTime(), Date.parse(mailbox.pending.lastQueuedAt))).toISOString(), now
|
|
1472
1841
|
});
|
|
@@ -1475,6 +1844,10 @@ export class FileSchedulerStoreAdapter {
|
|
|
1475
1844
|
owner: `leader-notification:${wakeId}`, startedAt: now.toISOString() });
|
|
1476
1845
|
store.saveTaskWake(taskId, wake);
|
|
1477
1846
|
store.saveWorkMailbox(mailbox);
|
|
1847
|
+
// The mailbox is only a hint. Carry ordinary refs into its next pending
|
|
1848
|
+
// batch, rather than delivering them with the chosen immediate successor.
|
|
1849
|
+
if (remainingRefs.length > 0)
|
|
1850
|
+
enqueueWork(store, target, "user-message", now, remainingRefs, { source: "controller", dedupeKey: `then-remainder:${wakeId}` });
|
|
1478
1851
|
return { wakeId, attemptId, disposition: "submit" };
|
|
1479
1852
|
});
|
|
1480
1853
|
}
|
|
@@ -1490,6 +1863,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
1490
1863
|
&& event.payload.attemptId === attemptId && event.payload.outcome === outcome)) {
|
|
1491
1864
|
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "notification.delivery", { attemptId, outcome, ...(detail === undefined ? {} : { detail }) }, now));
|
|
1492
1865
|
}
|
|
1866
|
+
if (store.getTask(taskId)?.status === "archived")
|
|
1867
|
+
return;
|
|
1493
1868
|
if (outcome !== "accepted" && outcome !== "rejected")
|
|
1494
1869
|
return;
|
|
1495
1870
|
store.saveWorkMailbox(completeProcessing(mailbox, attemptId));
|
|
@@ -1612,7 +1987,32 @@ export class FileSchedulerStoreAdapter {
|
|
|
1612
1987
|
nativeSessionId: input.nativeSessionId,
|
|
1613
1988
|
effective: input.effective
|
|
1614
1989
|
}, now);
|
|
1615
|
-
if (input.owner.scope === "
|
|
1990
|
+
if (input.owner.scope === "global") {
|
|
1991
|
+
let sessions = store.getGlobalRoleSessionSet(input.owner.roleName);
|
|
1992
|
+
let binding = sessions.providerBinding;
|
|
1993
|
+
if (binding == null) {
|
|
1994
|
+
sessions = bindGlobalRoleProviderRuntime(sessions, createProviderRuntimeBinding({
|
|
1995
|
+
providerNamespace: this.drivers.requireByAdapterId(input.adapterId).id,
|
|
1996
|
+
accountScope: input.agentId, conversationId: input.nativeSessionId,
|
|
1997
|
+
startedAt: now.toISOString()
|
|
1998
|
+
}), now);
|
|
1999
|
+
}
|
|
2000
|
+
else {
|
|
2001
|
+
if (currentProviderConversation(binding).conversationId !== input.nativeSessionId) {
|
|
2002
|
+
binding = supersedeProviderConversation(binding, {
|
|
2003
|
+
conversationId: input.nativeSessionId, switchedAt: now.toISOString(), basis: "terminal-session"
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
if (binding.authority.owner === "none")
|
|
2007
|
+
binding = transferProviderAuthority(binding, {
|
|
2008
|
+
expectedEpoch: binding.authority.epoch, expectedOwner: "none",
|
|
2009
|
+
owner: "controller", holderId: "controller", changedAt: now.toISOString()
|
|
2010
|
+
});
|
|
2011
|
+
sessions = updateGlobalRoleProviderRuntime(sessions, binding, now);
|
|
2012
|
+
}
|
|
2013
|
+
store.saveGlobalRoleSessionSet(sessions);
|
|
2014
|
+
}
|
|
2015
|
+
else {
|
|
1616
2016
|
let sessions = store.getTaskRoleSessionSet(input.owner.taskId, input.owner.roleName);
|
|
1617
2017
|
if (sessions !== null && sessions.providerBinding === null) {
|
|
1618
2018
|
sessions = bindTaskRoleProviderRuntime(sessions, createProviderRuntimeBinding({
|
|
@@ -2261,6 +2661,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
2261
2661
|
effective
|
|
2262
2662
|
}, now);
|
|
2263
2663
|
current = rememberRoleAgentCompletedTurn(current, input.agentId, nativeSessionId, input.nativeTurnId, now);
|
|
2664
|
+
// decision-3 §9: when a controller-owned native Turn is actually bound for
|
|
2665
|
+
// this Global Role, settle it to the observed terminal so the scope-generic
|
|
2666
|
+
// resolver and the interrupt-then gate see the real stop on the live binding
|
|
2667
|
+
// — the same edge Task settles via settleStructuredProviderTurn. This is a
|
|
2668
|
+
// strict guarded no-op for an older unmanaged Session or a
|
|
2669
|
+
// binding holding a different/already-terminal Turn, and the
|
|
2670
|
+
// durable native terminal recorded just above remains the proof of record.
|
|
2671
|
+
current = settleGlobalRoleRuntimeTurn(current, input.nativeTurnId, input.providerStatus, now);
|
|
2264
2672
|
store.saveGlobalRoleSessionSet(current);
|
|
2265
2673
|
if (input.roleName === SYSTEM_OPERATOR_ROLE
|
|
2266
2674
|
&& input.adapterId === "codex"
|
|
@@ -2368,7 +2776,8 @@ function recordCanonicalObservationObsolete(store, input, reason, now) {
|
|
|
2368
2776
|
roleName: input.fence.roleName,
|
|
2369
2777
|
agentId: input.fence.agentId,
|
|
2370
2778
|
...(input.fence.runId === undefined ? {} : { runId: input.fence.runId }),
|
|
2371
|
-
...(input.fence.nativeSessionId === undefined ? {} : { nativeSessionId: input.fence.nativeSessionId })
|
|
2779
|
+
...(input.fence.nativeSessionId === undefined ? {} : { nativeSessionId: input.fence.nativeSessionId }),
|
|
2780
|
+
...(reason === "task-archived" ? { originalEvent: input } : {})
|
|
2372
2781
|
}, reason, now);
|
|
2373
2782
|
}
|
|
2374
2783
|
function recordObsoleteRuntimeEvent(store, input, reason, now) {
|
|
@@ -2383,7 +2792,8 @@ function recordObsoleteRuntimeEvent(store, input, reason, now) {
|
|
|
2383
2792
|
agentId: input.agentId,
|
|
2384
2793
|
...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
|
|
2385
2794
|
...(input.runId === undefined ? {} : { runId: input.runId }),
|
|
2386
|
-
reason
|
|
2795
|
+
reason,
|
|
2796
|
+
...(input.originalEvent === undefined ? {} : { originalEvent: JSON.stringify(input.originalEvent) })
|
|
2387
2797
|
}, now));
|
|
2388
2798
|
}
|
|
2389
2799
|
function sessionPreview(value) {
|
|
@@ -3033,6 +3443,29 @@ function settleStructuredProviderTurn(sessions, runId, status, now, attemptId) {
|
|
|
3033
3443
|
settledAt: now.toISOString()
|
|
3034
3444
|
}), now);
|
|
3035
3445
|
}
|
|
3446
|
+
/**
|
|
3447
|
+
* The Global twin of {@link settleStructuredProviderTurn}: settle a Global Role's
|
|
3448
|
+
* own live Provider Turn to the observed native terminal, keyed by the exact
|
|
3449
|
+
* native Turn id the real Provider Stop/StopFailure hook carries (decision-3 §9).
|
|
3450
|
+
* It is a strict guarded no-op unless a controller-owned Turn with that exact
|
|
3451
|
+
* native id is bound and still in flight (`accepted`) — an absent binding, a
|
|
3452
|
+
* different/only-submitting Turn, or an already-terminal one is left untouched, so
|
|
3453
|
+
* an older unmanaged Session without a controlled binding settles nothing
|
|
3454
|
+
* and the durable `recentCompletedTurnIds` terminal remains the proof of record.
|
|
3455
|
+
*/
|
|
3456
|
+
function settleGlobalRoleRuntimeTurn(sessions, nativeTurnId, status, now) {
|
|
3457
|
+
const binding = sessions.providerBinding;
|
|
3458
|
+
if (binding === null || binding === undefined || binding.run === null
|
|
3459
|
+
|| binding.run.nativeTurnId !== nativeTurnId
|
|
3460
|
+
|| binding.run.status !== "accepted") {
|
|
3461
|
+
return sessions;
|
|
3462
|
+
}
|
|
3463
|
+
return updateGlobalRoleProviderRuntime(sessions, settleProviderTurn(binding, {
|
|
3464
|
+
nativeTurnId,
|
|
3465
|
+
status,
|
|
3466
|
+
settledAt: now.toISOString()
|
|
3467
|
+
}), now);
|
|
3468
|
+
}
|
|
3036
3469
|
function usageSnapshotIsSuperseded(events, incoming) {
|
|
3037
3470
|
if (incoming.kind !== "activity.observed" || incoming.payload.usage === undefined) {
|
|
3038
3471
|
return false;
|