@zq-silk/yui 0.2.0 → 0.4.2
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 +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -1,29 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { activeLiveRoleAgentSession, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, recordObservedTaskRoleCompletion, recordRoleAgentSession, rememberRoleAgentCompletedTurn, settleTaskRoleCompletion, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
|
|
3
|
+
import { createPendingTurnCompletion, hasRecentTurnId } from "../executor/turnCompletion.js";
|
|
2
4
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
3
5
|
import { answerInputRequest } from "../input/inputRequest.js";
|
|
4
6
|
import { activeRoleAgentBinding, updateRoleStatus } from "../role/role.js";
|
|
7
|
+
import { effectiveLaunchSnapshotsCompatible, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
5
8
|
import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
|
|
6
|
-
import { failAgentRun, markAgentRunDelivered } from "../run/agentRun.js";
|
|
9
|
+
import { failAgentRun, markAgentRunDelivered, markAgentRunPushed } from "../run/agentRun.js";
|
|
10
|
+
import { terminalizeExactTaskRun } from "../lifecycle/exactRunTerminalization.js";
|
|
11
|
+
import { foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
|
|
12
|
+
import { mapNativeLifecycleSignal } from "../lifecycle/providerLifecycleMapping.js";
|
|
13
|
+
import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
|
|
14
|
+
import { createLeaderRecoveryNotification, createLeaderStallNotification } from "../scheduler/operatorNotification.js";
|
|
7
15
|
import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
|
|
8
16
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
9
|
-
import {
|
|
17
|
+
import { latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, isRoleRunStalled, clearMatchingLeaderStallAttention, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT, RUN_RESOURCE_SUPPRESSED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
|
|
18
|
+
import { currentWorkItemExecutionGroup, updateWorkItemExecutionGroup, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
19
|
+
import { recordExecutionLaneResult } from "../execution/executionGroup.js";
|
|
20
|
+
import { formatAgentRunReceiptId, formatTaskRecordReference } from "../task/taskRecordReference.js";
|
|
21
|
+
import { bindExecution, claimPending, completeProcessing, releaseProcessing } from "../coordination/workMailbox.js";
|
|
22
|
+
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
23
|
+
import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_LAUNCH_RESERVED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
24
|
+
import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
|
|
10
25
|
/** Maps the authoritative FileTaskStore records to the scheduler's narrow port. */
|
|
11
26
|
export class FileSchedulerStoreAdapter {
|
|
12
27
|
store;
|
|
13
28
|
constructor(store) {
|
|
14
29
|
this.store = store;
|
|
15
30
|
}
|
|
31
|
+
getPresentationContext() {
|
|
32
|
+
return { timeZone: this.store.getConfig().timeZone };
|
|
33
|
+
}
|
|
16
34
|
listTasks() { return this.store.listTasks(); }
|
|
17
35
|
getTask(taskId) { return this.store.getTask(taskId); }
|
|
36
|
+
getTaskWorkspace(taskId) { return this.store.getTaskWorkspace(taskId); }
|
|
18
37
|
getTaskBrief(taskId) { return this.store.getTaskBrief(taskId); }
|
|
19
38
|
listDecisions(taskId) { return this.store.listDecisions(taskId); }
|
|
20
39
|
listMilestones(taskId) { return this.store.listMilestones(taskId); }
|
|
21
40
|
listRoles(taskId) {
|
|
22
|
-
return this.store.listRoles(taskId).map(mapRole);
|
|
41
|
+
return this.store.listRoles(taskId).map((role) => mapRole(this.store, role));
|
|
23
42
|
}
|
|
24
43
|
getRole(taskId, roleName) {
|
|
25
44
|
const role = this.store.getRole(taskId, roleName);
|
|
26
|
-
return role === null ? null : mapRole(role);
|
|
45
|
+
return role === null ? null : mapRole(this.store, role);
|
|
27
46
|
}
|
|
28
47
|
getActiveAgentRun(taskId, roleName) {
|
|
29
48
|
return this.store.getActiveAgentRun(taskId, roleName);
|
|
@@ -34,20 +53,28 @@ export class FileSchedulerStoreAdapter {
|
|
|
34
53
|
listOpenInputRequests() {
|
|
35
54
|
return this.store.listAllInputRequests().filter((request) => request.status === "open");
|
|
36
55
|
}
|
|
56
|
+
getInputRequest(taskId, inputRequestId) {
|
|
57
|
+
return this.store.getInputRequest(taskId, inputRequestId);
|
|
58
|
+
}
|
|
37
59
|
getOperatorDeliveryTarget() {
|
|
38
60
|
const role = this.store.getGlobalRole(SYSTEM_OPERATOR_ROLE);
|
|
39
61
|
if (role === null)
|
|
40
62
|
return null;
|
|
63
|
+
const sessions = this.store.getGlobalRoleSessionSet(SYSTEM_OPERATOR_ROLE);
|
|
64
|
+
const effectiveSession = sessions?.sessions[sessions.activeAgentId];
|
|
65
|
+
if (effectiveSession?.status !== "ready")
|
|
66
|
+
return null;
|
|
41
67
|
return {
|
|
42
68
|
roleName: SYSTEM_OPERATOR_ROLE,
|
|
43
|
-
adapterId:
|
|
69
|
+
adapterId: effectiveSession.effective.adapterId
|
|
44
70
|
};
|
|
45
71
|
}
|
|
46
|
-
resolveExpiredInputRecommendations(now) {
|
|
72
|
+
resolveExpiredInputRecommendations(now, taskIds) {
|
|
47
73
|
return this.store.transaction((store) => {
|
|
48
74
|
const expired = store.listAllInputRequests().filter((request) => (request.status === "open"
|
|
49
75
|
&& request.policy.kind === "recommended"
|
|
50
|
-
&& Date.parse(request.policy.timeoutAt) <= now.getTime()
|
|
76
|
+
&& Date.parse(request.policy.timeoutAt) <= now.getTime()
|
|
77
|
+
&& (taskIds === undefined || taskIds.has(request.taskId))));
|
|
51
78
|
const resolved = [];
|
|
52
79
|
for (const request of expired) {
|
|
53
80
|
const task = store.getTask(request.taskId);
|
|
@@ -56,18 +83,341 @@ export class FileSchedulerStoreAdapter {
|
|
|
56
83
|
const choiceKey = request.policy.recommendedChoiceKey;
|
|
57
84
|
const answered = answerInputRequest(request, { choiceKey }, "agent-timeout", now);
|
|
58
85
|
store.saveInputRequest(task.id, answered);
|
|
59
|
-
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), "input.auto-answered", { requestId: request.id, choiceKey }, now));
|
|
86
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, "input.auto-answered", { requestId: request.id, choiceKey }, now));
|
|
60
87
|
queueLeaderWakeup(store, task.id, `input-timeout:${request.id}`, now);
|
|
61
88
|
resolved.push({ inputRequestId: request.id, taskId: task.id, choiceKey });
|
|
62
89
|
}
|
|
63
90
|
return resolved;
|
|
64
91
|
});
|
|
65
92
|
}
|
|
66
|
-
getRoleSession(taskId, roleName) {
|
|
67
|
-
const
|
|
68
|
-
|
|
93
|
+
getRoleSession(taskId, roleName, agentId) {
|
|
94
|
+
const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
|
|
95
|
+
const session = agentId === undefined
|
|
96
|
+
? sessions?.sessions[sessions.activeAgentId]
|
|
97
|
+
: sessions?.sessions[agentId];
|
|
98
|
+
return session === undefined ? null : mapSession(session);
|
|
99
|
+
}
|
|
100
|
+
listEvents(taskId) {
|
|
101
|
+
return this.store.listEvents(taskId);
|
|
102
|
+
}
|
|
103
|
+
getRunDurableProgress(taskId, roleName, runId) {
|
|
104
|
+
return latestRunDurableProgressAt(this.store, taskId, roleName, runId);
|
|
105
|
+
}
|
|
106
|
+
recordRoleRunStall(input) {
|
|
107
|
+
return this.store.transaction((store) => {
|
|
108
|
+
const task = store.getTask(input.taskId);
|
|
109
|
+
const role = store.getRole(input.taskId, input.roleName);
|
|
110
|
+
const run = store.getActiveAgentRun(input.taskId, input.roleName);
|
|
111
|
+
if (task === null
|
|
112
|
+
|| task.status !== "active"
|
|
113
|
+
|| role === null
|
|
114
|
+
|| run === null
|
|
115
|
+
|| run.id !== input.runId
|
|
116
|
+
|| run.status !== "active"
|
|
117
|
+
|| run.effective.agentId !== input.agentId
|
|
118
|
+
|| run.effective.adapterId !== input.adapterId)
|
|
119
|
+
return "state-changed";
|
|
120
|
+
const progress = latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId);
|
|
121
|
+
if (progress?.progressAt !== input.progressAt)
|
|
122
|
+
return "state-changed";
|
|
123
|
+
const session = store.getRoleSession(input.taskId, input.roleName);
|
|
124
|
+
if (!matchesStallSessionFence(session, input.session))
|
|
125
|
+
return "state-changed";
|
|
126
|
+
const existing = latestStallEvidenceKey(store.listEvents(task.id), run.id);
|
|
127
|
+
if (existing?.progressAt === input.progressAt)
|
|
128
|
+
return "already-raised";
|
|
129
|
+
const event = createTaskEvent(store.nextEventId(task.id), task.id, RUN_STALLED_EVENT, {
|
|
130
|
+
runId: run.id,
|
|
131
|
+
roleName: role.name,
|
|
132
|
+
kind: input.kind,
|
|
133
|
+
classification: input.classification,
|
|
134
|
+
progressAt: input.progressAt,
|
|
135
|
+
idleMs: String(Math.max(0, Math.floor(input.idleMs))),
|
|
136
|
+
evidenceKey: input.evidenceKey,
|
|
137
|
+
status: "needs-attention"
|
|
138
|
+
}, input.now);
|
|
139
|
+
store.saveEvent(task.id, event);
|
|
140
|
+
if (role.name === "leader") {
|
|
141
|
+
store.saveOperatorNotification(createLeaderStallNotification(task.id, run.id, input.progressAt, input.evidenceKey, input.now, store.getOperatorNotification(task.id)));
|
|
142
|
+
enqueueWork(store, { kind: "operator" }, "leader-run-stalled", input.now, [
|
|
143
|
+
{ type: "task", id: task.id }
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
queueLeaderWakeup(store, task.id, "role-run-stalled", input.now);
|
|
148
|
+
}
|
|
149
|
+
return "raised";
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
recordRoleRunProgress(input) {
|
|
153
|
+
return this.store.transaction((store) => {
|
|
154
|
+
const task = store.getTask(input.taskId);
|
|
155
|
+
const run = store.getActiveAgentRun(input.taskId, input.roleName);
|
|
156
|
+
if (task === null
|
|
157
|
+
|| task.status !== "active"
|
|
158
|
+
|| run === null
|
|
159
|
+
|| run.id !== input.runId
|
|
160
|
+
|| run.status !== "active")
|
|
161
|
+
return "state-changed";
|
|
162
|
+
const events = store.listEvents(task.id);
|
|
163
|
+
const previousStall = latestStallEvidenceKey(events, run.id);
|
|
164
|
+
// A progress fact can close only the matching, older stall episode. A
|
|
165
|
+
// stale/native observation must never clear a newer attention point.
|
|
166
|
+
if (previousStall !== undefined
|
|
167
|
+
&& Date.parse(input.progressAt) <= Date.parse(previousStall.progressAt)) {
|
|
168
|
+
return "already-recorded";
|
|
169
|
+
}
|
|
170
|
+
const existing = events.some((event) => (event.type === RUN_PROGRESS_EVENT
|
|
171
|
+
&& event.payload.runId === run.id
|
|
172
|
+
&& (event.payload.progressAt === input.progressAt
|
|
173
|
+
|| (typeof event.payload.progressAt === "string"
|
|
174
|
+
&& Number.isFinite(Date.parse(event.payload.progressAt))
|
|
175
|
+
&& Date.parse(event.payload.progressAt) >= Date.parse(input.progressAt)))));
|
|
176
|
+
const stalledAt = latestRunEventTime(events, RUN_STALLED_EVENT, run.id);
|
|
177
|
+
const recoveredAt = latestRunEventTime(events, RUN_RECOVERED_EVENT, run.id);
|
|
178
|
+
const recovered = stalledAt !== undefined
|
|
179
|
+
&& (recoveredAt === undefined || Date.parse(recoveredAt) <= Date.parse(stalledAt));
|
|
180
|
+
if (!existing) {
|
|
181
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_PROGRESS_EVENT, {
|
|
182
|
+
runId: run.id,
|
|
183
|
+
roleName: input.roleName,
|
|
184
|
+
kind: "durable-fold",
|
|
185
|
+
progressAt: input.progressAt,
|
|
186
|
+
evidence: input.evidence ?? ""
|
|
187
|
+
}, input.now));
|
|
188
|
+
}
|
|
189
|
+
if (recovered) {
|
|
190
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RECOVERED_EVENT, {
|
|
191
|
+
runId: run.id,
|
|
192
|
+
roleName: input.roleName,
|
|
193
|
+
progressAt: input.progressAt,
|
|
194
|
+
kind: "durable-progress"
|
|
195
|
+
}, input.now));
|
|
196
|
+
clearMatchingLeaderStallAttention(store, task.id, run.id);
|
|
197
|
+
}
|
|
198
|
+
return existing && !recovered ? "already-recorded" : "recorded";
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
recordRoleRunResourceSuppression(input) {
|
|
202
|
+
return this.store.transaction((store) => {
|
|
203
|
+
const task = store.getTask(input.taskId);
|
|
204
|
+
const run = store.getActiveAgentRun(input.taskId, input.roleName);
|
|
205
|
+
if (task === null
|
|
206
|
+
|| task.status !== "active"
|
|
207
|
+
|| run === null
|
|
208
|
+
|| run.id !== input.runId
|
|
209
|
+
|| run.status !== "active")
|
|
210
|
+
return "state-changed";
|
|
211
|
+
const session = store.getRoleSession(input.taskId, input.roleName);
|
|
212
|
+
if (session === null
|
|
213
|
+
|| session.agentId !== input.agentId
|
|
214
|
+
|| session.adapterId !== input.adapterId
|
|
215
|
+
|| session.status === "stopped"
|
|
216
|
+
|| session.status === "broken"
|
|
217
|
+
|| session.nativeSessionId !== input.nativeSessionId
|
|
218
|
+
|| session.launchId !== input.launchId)
|
|
219
|
+
return "state-changed";
|
|
220
|
+
const existing = store.listEvents(task.id).some((event) => (event.type === RUN_RESOURCE_SUPPRESSED_EVENT
|
|
221
|
+
&& event.payload.runId === run.id
|
|
222
|
+
&& event.payload.roleName === input.roleName
|
|
223
|
+
&& event.payload.progressAt === input.progressAt));
|
|
224
|
+
if (existing)
|
|
225
|
+
return "already-recorded";
|
|
226
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RESOURCE_SUPPRESSED_EVENT, {
|
|
227
|
+
runId: run.id,
|
|
228
|
+
roleName: input.roleName,
|
|
229
|
+
progressAt: input.progressAt,
|
|
230
|
+
observedAt: input.observedAt,
|
|
231
|
+
kind: "advisory-resource"
|
|
232
|
+
}, input.now));
|
|
233
|
+
return "recorded";
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
hasInFlightTurn(taskId, roleName) {
|
|
237
|
+
const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
|
|
238
|
+
return sessions !== null && sessions.inFlight !== null;
|
|
239
|
+
}
|
|
240
|
+
isRoleGenerationProviderReady(input) {
|
|
241
|
+
// A provider-ready fact for this exact generation is proven by a folded
|
|
242
|
+
// runtime.provider-session-lifecycle event with preInputReady true and a
|
|
243
|
+
// matching agent/launch/nativeSession fence. This reads durable folded truth
|
|
244
|
+
// only — no liveness, screen, or pane/PID inference.
|
|
245
|
+
return this.store.listEvents(input.taskId).some((event) => (event.type === "runtime.provider-session-lifecycle"
|
|
246
|
+
&& event.payload.roleName === input.roleName
|
|
247
|
+
&& event.payload.agentId === input.agentId
|
|
248
|
+
&& event.payload.preInputReady === "true"
|
|
249
|
+
&& (input.launchId === undefined || event.payload.launchId === input.launchId)
|
|
250
|
+
&& (input.nativeSessionId === undefined
|
|
251
|
+
|| event.payload.nativeSessionId === input.nativeSessionId)));
|
|
252
|
+
}
|
|
253
|
+
peekNextAgentRunId(taskId) {
|
|
254
|
+
return this.store.peekNextAgentRunId(taskId);
|
|
255
|
+
}
|
|
256
|
+
getWorkMailbox(target) { return this.store.getWorkMailbox(target); }
|
|
257
|
+
listWorkMailboxes() { return this.store.listWorkMailboxes(); }
|
|
258
|
+
queueTaskProgress(taskId, reason, now) {
|
|
259
|
+
this.store.transaction((store) => {
|
|
260
|
+
enqueueWork(store, { kind: "task", taskId }, reason, now, [
|
|
261
|
+
{ type: "task", id: taskId }
|
|
262
|
+
]);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
enqueueLeaderWakeup(taskId, reason, now) {
|
|
266
|
+
return this.store.transaction((store) => (queueLeaderWakeup(store, taskId, reason, now)));
|
|
267
|
+
}
|
|
268
|
+
releaseLeaderWakeupAndEnqueue(taskId, batchId, reason, now) {
|
|
269
|
+
const target = { kind: "role", taskId, roleName: "leader" };
|
|
270
|
+
return this.store.transaction((store) => {
|
|
271
|
+
const mailbox = store.getWorkMailbox(target);
|
|
272
|
+
if (mailbox?.processing?.batchId !== batchId)
|
|
273
|
+
return false;
|
|
274
|
+
store.saveWorkMailbox(releaseProcessing(mailbox, batchId));
|
|
275
|
+
enqueueWork(store, target, reason, now);
|
|
276
|
+
return true;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
claimWorkMailbox(input) {
|
|
280
|
+
return this.store.transaction((store) => {
|
|
281
|
+
const mailbox = store.getWorkMailbox(input.target);
|
|
282
|
+
if (mailbox === null || (mailbox.processing === null && mailbox.pending === null)) {
|
|
283
|
+
return { status: "empty" };
|
|
284
|
+
}
|
|
285
|
+
if (mailbox.processing !== null) {
|
|
286
|
+
return { status: "processing", processing: mailbox.processing };
|
|
287
|
+
}
|
|
288
|
+
let claimed = claimPending(mailbox, {
|
|
289
|
+
batchId: input.batchId,
|
|
290
|
+
owner: input.owner,
|
|
291
|
+
startedAt: input.now.toISOString()
|
|
292
|
+
});
|
|
293
|
+
if (input.executionRef !== undefined) {
|
|
294
|
+
claimed = bindExecution(claimed, input.batchId, input.executionRef);
|
|
295
|
+
}
|
|
296
|
+
store.saveWorkMailbox(claimed);
|
|
297
|
+
return { status: "claimed", processing: claimed.processing };
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
completeWorkMailbox(target, batchId) {
|
|
301
|
+
return this.store.transaction((store) => {
|
|
302
|
+
const mailbox = store.getWorkMailbox(target);
|
|
303
|
+
if (mailbox?.processing?.batchId !== batchId)
|
|
304
|
+
return false;
|
|
305
|
+
const completed = completeProcessing(mailbox, batchId);
|
|
306
|
+
if (target.kind === "role-runtime"
|
|
307
|
+
|| target.kind === "global-role-runtime") {
|
|
308
|
+
saveRuntimeLifecycleMailbox(store, completed);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
store.saveWorkMailbox(completed);
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
releaseWorkMailbox(target, batchId) {
|
|
317
|
+
return this.store.transaction((store) => {
|
|
318
|
+
const mailbox = store.getWorkMailbox(target);
|
|
319
|
+
if (mailbox?.processing?.batchId !== batchId)
|
|
320
|
+
return false;
|
|
321
|
+
store.saveWorkMailbox(releaseProcessing(mailbox, batchId));
|
|
322
|
+
return true;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
completeRuntimeCleanup(target, now) {
|
|
326
|
+
return this.store.transaction((store) => {
|
|
327
|
+
let mailbox = store.getWorkMailbox(target);
|
|
328
|
+
if (mailbox === null || !hasRuntimeCleanupObligation(mailbox))
|
|
329
|
+
return false;
|
|
330
|
+
if (mailbox.processing !== null) {
|
|
331
|
+
if (!isRuntimeLaunchReservation(mailbox.processing)
|
|
332
|
+
&& !mailbox.processing.batch.reasons.includes(RUNTIME_CLEANUP_REQUIRED_REASON)) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
mailbox = completeProcessing(mailbox, mailbox.processing.batchId);
|
|
336
|
+
}
|
|
337
|
+
if (mailbox.pending !== null) {
|
|
338
|
+
if (mailbox.pending.reasons.length !== 1
|
|
339
|
+
|| mailbox.pending.reasons[0] !== RUNTIME_CLEANUP_REQUIRED_REASON) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
const batchId = `runtime-cleanup-complete:${mailbox.pending.fromSequence}-${mailbox.pending.toSequence}`;
|
|
343
|
+
mailbox = completeProcessing(claimPending(mailbox, {
|
|
344
|
+
batchId,
|
|
345
|
+
owner: RUNTIME_LIFECYCLE_OWNER,
|
|
346
|
+
startedAt: now.toISOString()
|
|
347
|
+
}), batchId);
|
|
348
|
+
}
|
|
349
|
+
markRuntimeOwnerSessionStopped(store, runtimeOwnerFromTarget(target), now);
|
|
350
|
+
saveRuntimeLifecycleMailbox(store, mailbox);
|
|
351
|
+
return true;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
completeStoppedRuntimeReservation(target, batchId, now) {
|
|
355
|
+
return this.store.transaction((store) => {
|
|
356
|
+
const mailbox = store.getWorkMailbox(target);
|
|
357
|
+
if (!isRuntimeLaunchReservation(mailbox?.processing, batchId)) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
markRuntimeOwnerSessionStopped(store, runtimeOwnerFromTarget(target), now);
|
|
361
|
+
saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, batchId));
|
|
362
|
+
return true;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
listDormantRuntimeOwners() {
|
|
366
|
+
const taskOwners = this.store.listTasks().flatMap((task) => (this.store.listRoleSessionSets(task.id).flatMap((sessions) => {
|
|
367
|
+
const active = sessions.sessions[sessions.activeAgentId];
|
|
368
|
+
return active !== undefined
|
|
369
|
+
&& active.status !== "stopped"
|
|
370
|
+
&& !hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget(sessions.owner)))
|
|
371
|
+
&& this.store.getActiveAgentRun(task.id, sessions.owner.roleName) === null
|
|
372
|
+
? [{
|
|
373
|
+
owner: sessions.owner,
|
|
374
|
+
agentId: active.agentId,
|
|
375
|
+
adapterId: active.adapterId,
|
|
376
|
+
nativeSessionId: active.nativeSessionId,
|
|
377
|
+
...(active.launchId === undefined ? {} : { launchId: active.launchId }),
|
|
378
|
+
sessionUpdatedAt: active.updatedAt
|
|
379
|
+
}]
|
|
380
|
+
: [];
|
|
381
|
+
})));
|
|
382
|
+
const globalOwners = this.store.listGlobalRoleSessionSets().flatMap((sessions) => {
|
|
383
|
+
const active = sessions.sessions[sessions.activeAgentId];
|
|
384
|
+
return active !== undefined
|
|
385
|
+
&& active.status !== "stopped"
|
|
386
|
+
&& !hasRuntimeLifecycleWork(this.store.getWorkMailbox(runtimeLifecycleTarget(sessions.owner)))
|
|
387
|
+
? [{
|
|
388
|
+
owner: sessions.owner,
|
|
389
|
+
agentId: active.agentId,
|
|
390
|
+
adapterId: active.adapterId,
|
|
391
|
+
nativeSessionId: active.nativeSessionId,
|
|
392
|
+
...(active.launchId === undefined ? {} : { launchId: active.launchId }),
|
|
393
|
+
sessionUpdatedAt: active.updatedAt
|
|
394
|
+
}]
|
|
395
|
+
: [];
|
|
396
|
+
});
|
|
397
|
+
return [...taskOwners, ...globalOwners];
|
|
398
|
+
}
|
|
399
|
+
markRuntimeOwnerStopped(candidate, now) {
|
|
400
|
+
return this.store.transaction((store) => {
|
|
401
|
+
if (!dormantRuntimeCandidateIsCurrent(store, candidate))
|
|
402
|
+
return false;
|
|
403
|
+
return markRuntimeOwnerSessionStopped(store, candidate.owner, now);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
enqueueRuntimeCleanup(owner, now = new Date(), expectedDormantCandidate) {
|
|
407
|
+
return this.store.transaction((store) => {
|
|
408
|
+
if (expectedDormantCandidate !== undefined
|
|
409
|
+
&& (!sameRuntimeOwner(owner, expectedDormantCandidate.owner)
|
|
410
|
+
|| !dormantRuntimeCandidateIsCurrent(store, expectedDormantCandidate))) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
if (owner.scope === "task" && store.getTask(owner.taskId) === null) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const target = runtimeLifecycleTarget(owner);
|
|
417
|
+
enqueueWork(store, target, RUNTIME_CLEANUP_REQUIRED_REASON, now, owner.scope === "task" ? [{ type: "task", id: owner.taskId }] : []);
|
|
418
|
+
return target;
|
|
419
|
+
});
|
|
69
420
|
}
|
|
70
|
-
nextAgentRunId(taskId) { return this.store.nextAgentRunId(taskId); }
|
|
71
421
|
getPendingWakeup(taskId) { return this.store.getPendingWakeup(taskId); }
|
|
72
422
|
listPendingWakeups() { return this.store.listPendingWakeups(); }
|
|
73
423
|
savePendingWakeup(wakeup) {
|
|
@@ -83,18 +433,49 @@ export class FileSchedulerStoreAdapter {
|
|
|
83
433
|
return "unavailable";
|
|
84
434
|
}
|
|
85
435
|
const role = requireRole(store, input.task.id, input.role.name);
|
|
436
|
+
const binding = activeRoleAgentBinding(role);
|
|
86
437
|
if (role.activeAgentId !== input.role.activeAgentId
|
|
87
|
-
||
|
|
438
|
+
|| binding.adapterId !== input.role.adapterId
|
|
439
|
+
|| binding.config.model !== input.role.model
|
|
440
|
+
|| binding.config.effort !== input.role.effort
|
|
441
|
+
|| role.workspace !== input.role.workspace) {
|
|
88
442
|
return "state-changed";
|
|
89
443
|
}
|
|
444
|
+
if (!isDeepStrictEqual(input.run.effective, input.role.effective)) {
|
|
445
|
+
return "state-changed";
|
|
446
|
+
}
|
|
447
|
+
if (store.getAgentRun(input.task.id, input.run.id) !== null)
|
|
448
|
+
return "state-changed";
|
|
90
449
|
if (store.getActiveAgentRun(input.task.id, input.role.name) !== null)
|
|
91
450
|
return "busy";
|
|
92
451
|
const pending = store.getPendingWakeup(input.task.id);
|
|
93
452
|
if (pending === null || !pendingWakeupsMatch(pending, input.wakeup)) {
|
|
94
453
|
return "state-changed";
|
|
95
454
|
}
|
|
455
|
+
const target = { kind: "role", taskId: input.task.id, roleName: input.role.name };
|
|
456
|
+
const mailbox = store.getWorkMailbox(target);
|
|
457
|
+
if (mailbox === null || mailbox.pending === null)
|
|
458
|
+
return "state-changed";
|
|
459
|
+
if (mailbox.processing !== null)
|
|
460
|
+
return "busy";
|
|
461
|
+
if (store.peekNextAgentRunId(input.task.id) !== input.run.id) {
|
|
462
|
+
return "state-changed";
|
|
463
|
+
}
|
|
464
|
+
const allocatedRunId = store.nextAgentRunId(input.task.id);
|
|
465
|
+
if (allocatedRunId !== input.run.id) {
|
|
466
|
+
throw new Error(`Leader Run allocation changed unexpectedly: ${input.task.id}.`);
|
|
467
|
+
}
|
|
468
|
+
const batchId = formatAgentRunReceiptId(input.task.id, input.run.id);
|
|
469
|
+
const claimed = bindExecution(claimPending(mailbox, {
|
|
470
|
+
batchId,
|
|
471
|
+
owner: "controller",
|
|
472
|
+
startedAt: input.now.toISOString()
|
|
473
|
+
}), batchId, { type: "run", taskId: input.task.id, id: input.run.id });
|
|
96
474
|
store.saveActiveAgentRun(input.run);
|
|
475
|
+
store.saveWorkMailbox(claimed);
|
|
97
476
|
store.saveRole(input.task.id, updateRoleStatus(role, "running", input.now));
|
|
477
|
+
bindTaskRoleRunInFlight(store, role, input.run, input.now);
|
|
478
|
+
store.saveEvent(input.task.id, createTaskEvent(store.nextEventId(input.task.id), input.task.id, "run.dispatched", runLaunchEventPayload(input.run), input.now));
|
|
98
479
|
if (input.session !== null && input.session.nativeSessionId !== undefined) {
|
|
99
480
|
saveTaskSession(store, role, {
|
|
100
481
|
...input.session,
|
|
@@ -103,7 +484,6 @@ export class FileSchedulerStoreAdapter {
|
|
|
103
484
|
}
|
|
104
485
|
store.clearLeaderFailure(input.task.id);
|
|
105
486
|
store.clearOperatorNotification(input.task.id);
|
|
106
|
-
store.clearPendingWakeup(input.task.id);
|
|
107
487
|
return "claimed";
|
|
108
488
|
});
|
|
109
489
|
}
|
|
@@ -118,187 +498,1826 @@ export class FileSchedulerStoreAdapter {
|
|
|
118
498
|
if (active === null || active.id !== input.run.id) {
|
|
119
499
|
throw new Error(`Active Agent run changed before delivery was persisted: ${input.run.id}.`);
|
|
120
500
|
}
|
|
121
|
-
|
|
122
|
-
|
|
501
|
+
// Transport success records prompt-pushed ONLY. Acceptance (deliveredAt)
|
|
502
|
+
// is written exclusively by an exact provider-accepted fold, never by the
|
|
503
|
+
// transport receipt — that removes the transport-to-delivered false path.
|
|
504
|
+
if (active.pushedAt === undefined) {
|
|
505
|
+
store.saveAgentRun(markAgentRunPushed(active, input.now));
|
|
506
|
+
markTaskRoleRunPushedInFlight(store, role, input.run, input.now);
|
|
507
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, "run.pushed", runLaunchEventPayload(active), input.now));
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
const sessions = store.getTaskRoleSessionSet(input.task.id, input.role.name);
|
|
511
|
+
if (sessions?.inFlight?.runId === input.run.id
|
|
512
|
+
&& sessions.inFlight.pushedAt === undefined) {
|
|
513
|
+
markTaskRoleRunPushedInFlight(store, role, input.run, input.now);
|
|
514
|
+
}
|
|
123
515
|
}
|
|
124
516
|
if (role.status !== "running") {
|
|
125
517
|
store.saveRole(input.task.id, updateRoleStatus(role, "running", input.now));
|
|
126
518
|
}
|
|
127
519
|
if (input.session !== null && input.session.nativeSessionId !== undefined) {
|
|
128
520
|
const existing = store.getRoleSession(input.task.id, input.role.name);
|
|
521
|
+
const reservation = matchingPreparedRuntimeReservation(store, input);
|
|
129
522
|
if (existing?.agentId !== input.session.agentId
|
|
130
523
|
|| existing.adapterId !== input.session.adapterId
|
|
131
524
|
|| existing.nativeSessionId !== input.session.nativeSessionId
|
|
525
|
+
|| (input.launchId !== undefined && existing.launchId !== input.launchId)
|
|
132
526
|
|| existing.status !== "running") {
|
|
133
527
|
saveTaskSession(store, role, {
|
|
134
528
|
...input.session,
|
|
135
529
|
nativeSessionId: input.session.nativeSessionId
|
|
136
|
-
}, "running", input.now);
|
|
530
|
+
}, "running", input.now, input.launchId);
|
|
137
531
|
}
|
|
532
|
+
completePreparedRuntimeReservation(store, reservation);
|
|
138
533
|
}
|
|
139
534
|
});
|
|
140
535
|
}
|
|
141
|
-
|
|
536
|
+
saveRoleRunPrepared(input) {
|
|
142
537
|
this.store.transaction((store) => {
|
|
143
538
|
const task = store.getTask(input.task.id);
|
|
144
|
-
if (task === null || task.status !== "active")
|
|
145
|
-
|
|
539
|
+
if (task === null || task.status !== "active") {
|
|
540
|
+
throw new Error(`Task is not active: ${input.task.id}.`);
|
|
541
|
+
}
|
|
146
542
|
const role = requireRole(store, input.task.id, input.role.name);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
543
|
+
const active = store.getActiveAgentRun(input.task.id, input.role.name);
|
|
544
|
+
if (active === null || active.id !== input.run.id) {
|
|
545
|
+
throw new Error(`Active Agent run changed before preparation was persisted: ${input.run.id}.`);
|
|
546
|
+
}
|
|
547
|
+
if (input.session !== null && input.session.nativeSessionId !== undefined) {
|
|
548
|
+
const existing = store.getRoleSession(input.task.id, input.role.name);
|
|
549
|
+
matchingPreparedRuntimeReservation(store, input);
|
|
550
|
+
if (existing?.nativeSessionId !== input.session.nativeSessionId
|
|
551
|
+
|| (input.launchId !== undefined && existing.launchId !== input.launchId)
|
|
552
|
+
|| existing.status !== "running") {
|
|
553
|
+
saveTaskSession(store, role, {
|
|
554
|
+
...input.session,
|
|
555
|
+
nativeSessionId: input.session.nativeSessionId
|
|
556
|
+
}, "running", input.now, input.launchId);
|
|
152
557
|
}
|
|
153
|
-
store.savePendingWakeup(restorePendingWakeup(input.claimed.wakeup, store.getPendingWakeup(input.task.id)));
|
|
154
558
|
}
|
|
155
|
-
store
|
|
156
|
-
breakTaskSessionIfPresent(store, input.task.id, role.name, role.activeAgentId, input.now);
|
|
157
|
-
store.saveLeaderFailure(input.failure);
|
|
158
|
-
store.saveOperatorNotification(input.notification);
|
|
559
|
+
bindTaskRoleRunInFlight(store, role, input.run, input.now);
|
|
159
560
|
});
|
|
160
561
|
}
|
|
161
|
-
|
|
162
|
-
this.store.transaction((store) => {
|
|
163
|
-
const
|
|
164
|
-
store.
|
|
165
|
-
store.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
562
|
+
saveRoleRunDeliveryFailure(input) {
|
|
563
|
+
return this.store.transaction((store) => {
|
|
564
|
+
const task = store.getTask(input.taskId);
|
|
565
|
+
const role = store.getRole(input.taskId, input.roleName);
|
|
566
|
+
const active = store.getActiveAgentRun(input.taskId, input.roleName);
|
|
567
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
568
|
+
const session = sessions?.sessions[input.agentId];
|
|
569
|
+
const mailbox = store.getWorkMailbox({
|
|
570
|
+
kind: "role",
|
|
571
|
+
taskId: input.taskId,
|
|
572
|
+
roleName: input.roleName
|
|
573
|
+
});
|
|
574
|
+
const runtimeMailbox = store.getWorkMailbox(runtimeLifecycleTarget({
|
|
575
|
+
scope: "task",
|
|
576
|
+
taskId: input.taskId,
|
|
577
|
+
roleName: input.roleName
|
|
578
|
+
}));
|
|
579
|
+
if (task === null
|
|
580
|
+
|| task.status !== "active"
|
|
581
|
+
|| role === null
|
|
582
|
+
|| active === null
|
|
583
|
+
|| active.id !== input.runId
|
|
584
|
+
|| active.status !== "active"
|
|
585
|
+
|| active.pushedAt !== undefined
|
|
586
|
+
|| active.effective.agentId !== input.agentId
|
|
587
|
+
|| active.effective.adapterId !== input.adapterId
|
|
588
|
+
|| mailbox?.processing?.batchId !== input.mailboxBatchId
|
|
589
|
+
|| mailbox.processing.executionRef?.type !== "run"
|
|
590
|
+
|| mailbox.processing.executionRef.taskId !== input.taskId
|
|
591
|
+
|| mailbox.processing.executionRef.id !== input.runId
|
|
592
|
+
|| !preparedDeliveryFailureSessionFenceMatches(sessions, session, input)
|
|
593
|
+
|| !preparedReservationMatches(runtimeMailbox, input.taskId, input.runId, input.launchId)) {
|
|
594
|
+
return "state-changed";
|
|
595
|
+
}
|
|
596
|
+
const summary = `Role delivery retry limit exhausted before exact Run input delivery: ${input.runId}.`;
|
|
597
|
+
const result = terminalizeExactTaskRun(store, {
|
|
598
|
+
taskId: input.taskId,
|
|
599
|
+
roleName: input.roleName,
|
|
600
|
+
agentId: input.agentId,
|
|
601
|
+
runId: input.runId,
|
|
602
|
+
receiptId: formatAgentRunReceiptId(input.taskId, input.runId),
|
|
603
|
+
...(session?.nativeSessionId === undefined
|
|
604
|
+
? {}
|
|
605
|
+
: { nativeSessionId: session.nativeSessionId }),
|
|
606
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
607
|
+
outcome: { status: "failed", summary }
|
|
608
|
+
}, input.now);
|
|
609
|
+
if (result.disposition !== "applied" || result.run === null) {
|
|
610
|
+
return "state-changed";
|
|
611
|
+
}
|
|
612
|
+
enqueueWork(store, runtimeLifecycleTarget({
|
|
613
|
+
scope: "task",
|
|
614
|
+
taskId: input.taskId,
|
|
615
|
+
roleName: input.roleName
|
|
616
|
+
}), RUNTIME_CLEANUP_REQUIRED_REASON, input.now, [{ type: "task", id: input.taskId }]);
|
|
617
|
+
const terminal = result.run;
|
|
618
|
+
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.role-delivery-failed", {
|
|
619
|
+
runId: terminal.id,
|
|
620
|
+
roleName: input.roleName,
|
|
621
|
+
outcome: terminal.status
|
|
622
|
+
}, input.now));
|
|
623
|
+
if (terminal.purpose === "execution" && terminal.workItemId !== undefined) {
|
|
624
|
+
const item = store.getWorkItem(input.taskId, terminal.workItemId);
|
|
625
|
+
if (item !== null && ![
|
|
626
|
+
"completed", "failed", "retired"
|
|
627
|
+
].includes(item.status)) {
|
|
628
|
+
const group = currentWorkItemExecutionGroup(item);
|
|
629
|
+
const groupedPanel = group !== undefined
|
|
630
|
+
&& (group.lanes.length > 1 || group.strategy.mode === "adaptive")
|
|
631
|
+
&& group.resolution === undefined;
|
|
632
|
+
if (!groupedPanel) {
|
|
633
|
+
store.saveWorkItem(input.taskId, updateWorkItemStatus(item, "failed", input.now, summary));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (input.roleName !== "leader") {
|
|
638
|
+
enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, terminal.purpose === "review" ? "review-failed" : "role-run-failed", input.now, [
|
|
639
|
+
{ type: "run", taskId: input.taskId, id: terminal.id },
|
|
640
|
+
...(terminal.workItemId === undefined
|
|
641
|
+
? []
|
|
642
|
+
: [{ type: "work-item", taskId: input.taskId, id: terminal.workItemId }])
|
|
643
|
+
]);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
const failedRole = store.getRole(input.taskId, input.roleName);
|
|
647
|
+
if (failedRole !== null) {
|
|
648
|
+
store.saveRole(input.taskId, updateRoleStatus(failedRole, "failed", input.now));
|
|
170
649
|
}
|
|
650
|
+
store.saveLeaderFailure(recordLeaderFailure(input.taskId, session?.nativeSessionId ?? "(unregistered)", summary, input.now, store.getLeaderFailure(input.taskId)));
|
|
651
|
+
store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, summary, input.now, store.getOperatorNotification(input.taskId)));
|
|
652
|
+
enqueueWork(store, { kind: "operator" }, "leader-run-failed", input.now, [
|
|
653
|
+
{ type: "task", id: input.taskId },
|
|
654
|
+
{ type: "run", taskId: input.taskId, id: terminal.id }
|
|
655
|
+
]);
|
|
171
656
|
}
|
|
172
|
-
|
|
173
|
-
stopTaskSessionIfPresent(store, input.task.id, role.name, role.activeAgentId, input.now);
|
|
657
|
+
return "failed";
|
|
174
658
|
});
|
|
175
659
|
}
|
|
176
|
-
|
|
177
|
-
this.store.transaction((store) => {
|
|
178
|
-
const task = store.getTask(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
660
|
+
saveLeaderDispatchFailure(input) {
|
|
661
|
+
return this.store.transaction((store) => {
|
|
662
|
+
const task = store.getTask(input.task.id);
|
|
663
|
+
const role = store.getRole(input.task.id, input.role.name);
|
|
664
|
+
const active = store.getActiveAgentRun(input.task.id, input.role.name);
|
|
665
|
+
const target = {
|
|
666
|
+
kind: "role",
|
|
667
|
+
taskId: input.task.id,
|
|
668
|
+
roleName: input.role.name
|
|
669
|
+
};
|
|
670
|
+
const mailbox = store.getWorkMailbox(target);
|
|
671
|
+
if (task === null
|
|
672
|
+
|| task.status !== "active"
|
|
673
|
+
|| role === null
|
|
674
|
+
|| active === null
|
|
675
|
+
|| active.id !== input.claimed.run.id
|
|
676
|
+
|| active.status !== "active"
|
|
677
|
+
|| mailbox?.processing?.executionRef?.type !== "run"
|
|
678
|
+
|| mailbox.processing.executionRef.taskId !== input.task.id
|
|
679
|
+
|| mailbox.processing.executionRef.id !== input.claimed.run.id
|
|
680
|
+
|| !schedulerRoleSessionsMatch(store.getRoleSession(input.task.id, input.role.name), input.session)) {
|
|
681
|
+
return "state-changed";
|
|
682
|
+
}
|
|
683
|
+
store.saveAgentRun(failAgentRun(active, input.failure.message, input.now));
|
|
684
|
+
store.clearActiveAgentRun(input.task.id, input.role.name);
|
|
685
|
+
clearTaskRoleRunInFlight(store, role, active, input.now);
|
|
686
|
+
store.saveWorkMailbox(releaseProcessing(mailbox, mailbox.processing.batchId));
|
|
687
|
+
store.saveRole(input.task.id, updateRoleStatus(role, "failed", input.now));
|
|
688
|
+
breakTaskSessionIfPresent(store, input.task.id, role.name, active.effective.agentId, input.now);
|
|
689
|
+
const failure = recordLeaderFailure(input.task.id, input.failure.nativeSessionId, input.failure.message, input.now, store.getLeaderFailure(input.task.id));
|
|
690
|
+
store.saveLeaderFailure(failure);
|
|
691
|
+
store.saveOperatorNotification(createLeaderRecoveryNotification(input.task.id, input.notification.message, input.now, store.getOperatorNotification(input.task.id)));
|
|
692
|
+
enqueueWork(store, { kind: "operator" }, "leader-recovery-failed", input.now, [
|
|
693
|
+
{ type: "task", id: input.task.id }
|
|
694
|
+
]);
|
|
695
|
+
return "failed";
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
saveExitedRoleRun(input) {
|
|
699
|
+
return this.store.transaction((store) => {
|
|
700
|
+
const task = store.getTask(input.task.id);
|
|
701
|
+
const role = store.getRole(input.task.id, input.role.name);
|
|
702
|
+
const currentRun = store.getActiveAgentRun(input.task.id, input.role.name);
|
|
703
|
+
if (task === null
|
|
704
|
+
|| task.status !== "active"
|
|
705
|
+
|| role === null
|
|
706
|
+
|| currentRun === null
|
|
707
|
+
|| currentRun.id !== input.run.id
|
|
708
|
+
|| currentRun.status !== "active") {
|
|
709
|
+
return "state-changed";
|
|
710
|
+
}
|
|
711
|
+
const sessions = store.getTaskRoleSessionSet(task.id, role.name);
|
|
712
|
+
if (sessions?.inFlight !== null
|
|
713
|
+
&& sessions !== null
|
|
714
|
+
&& (sessions.inFlight.agentId !== currentRun.effective.agentId
|
|
715
|
+
|| sessions.inFlight.runId !== currentRun.id
|
|
716
|
+
|| sessions.inFlight.receiptId !== formatAgentRunReceiptId(task.id, currentRun.id))) {
|
|
717
|
+
return "state-changed";
|
|
718
|
+
}
|
|
719
|
+
// Review launch failure uses the aggregate exact-terminalization path.
|
|
720
|
+
// It owns mailbox settlement and ReviewRound convergence together, so a
|
|
721
|
+
// pending pre-delivery dispatch cannot be consumed before its Run and
|
|
722
|
+
// Round pass the same immutable identity fences.
|
|
723
|
+
if (currentRun.purpose === "review") {
|
|
724
|
+
if (input.run.taskId !== currentRun.taskId
|
|
725
|
+
|| input.run.roleName !== currentRun.roleName
|
|
726
|
+
|| input.run.purpose !== "review"
|
|
727
|
+
|| input.run.reviewRoundId !== currentRun.reviewRoundId
|
|
728
|
+
|| input.run.workItemId !== currentRun.workItemId
|
|
729
|
+
|| input.run.effective.agentId !== currentRun.effective.agentId
|
|
730
|
+
|| input.run.effective.adapterId !== currentRun.effective.adapterId) {
|
|
731
|
+
return "state-changed";
|
|
184
732
|
}
|
|
733
|
+
const terminal = terminalizeExactTaskRun(store, {
|
|
734
|
+
taskId: task.id,
|
|
735
|
+
roleName: role.name,
|
|
736
|
+
agentId: currentRun.effective.agentId,
|
|
737
|
+
runId: currentRun.id,
|
|
738
|
+
receiptId: formatAgentRunReceiptId(task.id, currentRun.id),
|
|
739
|
+
...(input.session?.nativeSessionId === undefined
|
|
740
|
+
? {}
|
|
741
|
+
: { nativeSessionId: input.session.nativeSessionId }),
|
|
742
|
+
outcome: { status: "failed", summary: input.summary }
|
|
743
|
+
}, input.now);
|
|
744
|
+
if (terminal.disposition !== "applied")
|
|
745
|
+
return "state-changed";
|
|
746
|
+
clearMatchingLeaderStallAttention(store, task.id, currentRun.id);
|
|
747
|
+
store.saveRole(task.id, updateRoleStatus(role, "exited", input.now));
|
|
748
|
+
stopTaskSessionIfPresent(store, task.id, role.name, currentRun.effective.agentId, input.now);
|
|
749
|
+
queueLeaderWakeup(store, task.id, "review-failed", input.now);
|
|
750
|
+
return "failed";
|
|
185
751
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
752
|
+
const target = { kind: "role", taskId: task.id, roleName: role.name };
|
|
753
|
+
const mailbox = store.getWorkMailbox(target);
|
|
754
|
+
const processing = mailbox?.processing;
|
|
755
|
+
if (mailbox !== null && processing !== null && processing !== undefined) {
|
|
756
|
+
if (processing.executionRef === undefined
|
|
757
|
+
|| processing.executionRef.type !== "run"
|
|
758
|
+
|| processing.executionRef.taskId !== task.id
|
|
759
|
+
|| processing.executionRef.id !== currentRun.id) {
|
|
760
|
+
return "state-changed";
|
|
189
761
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
762
|
+
}
|
|
763
|
+
store.saveAgentRun(failAgentRun(currentRun, input.summary, input.now));
|
|
764
|
+
clearMatchingLeaderStallAttention(store, task.id, currentRun.id);
|
|
765
|
+
store.clearActiveAgentRun(task.id, role.name);
|
|
766
|
+
clearTaskRoleRunInFlight(store, role, currentRun, input.now);
|
|
767
|
+
if (mailbox !== null && processing !== null && processing !== undefined) {
|
|
768
|
+
store.saveWorkMailbox(completeProcessing(mailbox, processing.batchId));
|
|
769
|
+
}
|
|
770
|
+
if (currentRun.purpose === "execution" && currentRun.workItemId !== undefined) {
|
|
771
|
+
const workItem = store.getWorkItem(task.id, currentRun.workItemId);
|
|
772
|
+
if (workItem !== null && !["completed", "failed", "retired"].includes(workItem.status)) {
|
|
773
|
+
// Terminalize the bound execution lane in the same transaction that
|
|
774
|
+
// fails the Run. Without this, a scheduler-driven Worker failure
|
|
775
|
+
// leaves the lane "running" behind a "failed" WorkItem, and a later
|
|
776
|
+
// `yui task run retry` is rejected because the lane is not terminal.
|
|
777
|
+
// The lane result and the WorkItem status are two ordered single-step
|
|
778
|
+
// record revisions, matching the aggregate terminalization path.
|
|
779
|
+
if (currentRun.executionGroupId !== undefined
|
|
780
|
+
&& currentRun.executionLaneId !== undefined
|
|
781
|
+
&& currentWorkItemExecutionGroup(workItem) !== undefined) {
|
|
782
|
+
const group = currentWorkItemExecutionGroup(workItem);
|
|
783
|
+
store.saveWorkItem(task.id, updateWorkItemExecutionGroup(workItem, recordExecutionLaneResult(group, currentRun.executionLaneId, { summary: input.summary }, "failed", input.now), input.now));
|
|
784
|
+
}
|
|
785
|
+
const laneUpdated = store.getWorkItem(task.id, currentRun.workItemId);
|
|
786
|
+
const laneGroup = currentWorkItemExecutionGroup(laneUpdated);
|
|
787
|
+
const groupedPanel = laneGroup !== undefined
|
|
788
|
+
&& (laneGroup.lanes.length > 1 || laneGroup.strategy.mode === "adaptive")
|
|
789
|
+
&& laneGroup.resolution === undefined;
|
|
790
|
+
if (!groupedPanel) {
|
|
791
|
+
store.saveWorkItem(task.id, updateWorkItemStatus(laneUpdated, "failed", input.now, input.summary));
|
|
792
|
+
}
|
|
193
793
|
}
|
|
194
|
-
store.saveRoleSessionSet(updated);
|
|
195
794
|
}
|
|
795
|
+
store.saveRole(task.id, updateRoleStatus(role, "exited", input.now));
|
|
796
|
+
stopTaskSessionIfPresent(store, task.id, role.name, currentRun.effective.agentId, input.now);
|
|
797
|
+
queueLeaderWakeup(store, task.id, role.name === "leader" ? "leader-run-failed" : "role-run-failed", input.now);
|
|
798
|
+
return "failed";
|
|
196
799
|
});
|
|
197
800
|
}
|
|
198
801
|
/** Called by the internal Codex notify hook, never by an LLM prompt. */
|
|
199
802
|
recordRuntimeNativeSession(input, now = new Date()) {
|
|
803
|
+
return this.store.transaction((store) => (recordTaskRuntimeNativeSession(store, input, now)));
|
|
804
|
+
}
|
|
805
|
+
reserveRuntimeLaunch(input, assertCurrent, now = new Date()) {
|
|
806
|
+
return this.store.transaction((store) => {
|
|
807
|
+
assertCurrent();
|
|
808
|
+
const target = runtimeLifecycleTarget(input.owner);
|
|
809
|
+
const existing = store.getWorkMailbox(target);
|
|
810
|
+
if (isRuntimeLaunchReservation(existing?.processing)) {
|
|
811
|
+
if (hasRuntimeCleanupObligation(existing)) {
|
|
812
|
+
throw new Error("Runtime cleanup is still pending.");
|
|
813
|
+
}
|
|
814
|
+
const executionRef = existing.processing.executionRef;
|
|
815
|
+
if (executionRef?.type === "run"
|
|
816
|
+
&& (input.owner.scope !== "task"
|
|
817
|
+
|| executionRef.taskId !== input.owner.taskId)) {
|
|
818
|
+
throw new Error("Runtime launch reservation belongs to another Task Run.");
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
status: "existing",
|
|
822
|
+
launchId: existing.processing.batchId,
|
|
823
|
+
...(executionRef?.type === "run"
|
|
824
|
+
? { runId: executionRef.id }
|
|
825
|
+
: {})
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
if (existing !== null
|
|
829
|
+
&& (existing.processing !== null || existing.pending !== null)) {
|
|
830
|
+
throw new Error("Runtime lifecycle work is already pending.");
|
|
831
|
+
}
|
|
832
|
+
enqueueWork(store, target, RUNTIME_LAUNCH_RESERVED_REASON, now, input.owner.scope === "task"
|
|
833
|
+
? [{ type: "task", id: input.owner.taskId }]
|
|
834
|
+
: []);
|
|
835
|
+
const queued = store.getWorkMailbox(target);
|
|
836
|
+
if (queued === null || queued.pending === null || queued.processing !== null) {
|
|
837
|
+
throw new Error("Runtime launch reservation could not be queued.");
|
|
838
|
+
}
|
|
839
|
+
let claimed = claimPending(queued, {
|
|
840
|
+
batchId: input.launchId,
|
|
841
|
+
owner: RUNTIME_LIFECYCLE_OWNER,
|
|
842
|
+
startedAt: now.toISOString()
|
|
843
|
+
});
|
|
844
|
+
if (input.runId !== undefined) {
|
|
845
|
+
if (input.owner.scope !== "task") {
|
|
846
|
+
throw new Error("A Run-bound runtime reservation requires a Task owner.");
|
|
847
|
+
}
|
|
848
|
+
claimed = bindExecution(claimed, input.launchId, { type: "run", taskId: input.owner.taskId, id: input.runId });
|
|
849
|
+
}
|
|
850
|
+
store.saveWorkMailbox(claimed);
|
|
851
|
+
return {
|
|
852
|
+
status: "reserved",
|
|
853
|
+
launchId: input.launchId,
|
|
854
|
+
...(input.runId === undefined ? {} : { runId: input.runId })
|
|
855
|
+
};
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
confirmRuntimeLaunchReservation(input, assertCurrent) {
|
|
859
|
+
return this.store.transaction((store) => {
|
|
860
|
+
assertCurrent();
|
|
861
|
+
const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(input.owner));
|
|
862
|
+
if (isRuntimeLaunchReservation(mailbox?.processing, input.launchId)) {
|
|
863
|
+
return "reserved";
|
|
864
|
+
}
|
|
865
|
+
if (hasRuntimeCleanupObligation(mailbox)) {
|
|
866
|
+
throw new Error("Runtime cleanup is still pending.");
|
|
867
|
+
}
|
|
868
|
+
// A synchronous provider lifecycle Hook may bind a fresh native Session
|
|
869
|
+
// and complete the reservation while the host start call is still
|
|
870
|
+
// unwinding. Preserve that exact hook-won generation instead of treating
|
|
871
|
+
// the already-settled reservation as a launch failure.
|
|
872
|
+
const sessions = runtimeOwnerSessionSet(store, input.owner);
|
|
873
|
+
const active = sessions?.sessions[sessions.activeAgentId];
|
|
874
|
+
if (active?.status === "running"
|
|
875
|
+
&& active.launchId === input.launchId)
|
|
876
|
+
return "provider-bound";
|
|
877
|
+
throw new Error("Runtime launch reservation no longer matches the launch.");
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
recordReservedRuntimeNativeSession(input, assertCurrent, now = new Date()) {
|
|
881
|
+
return this.store.transaction((store) => {
|
|
882
|
+
assertCurrent();
|
|
883
|
+
const mailbox = requireRuntimeLaunchReservation(store, input.owner, input.launchId);
|
|
884
|
+
if (hasRuntimeCleanupObligation(mailbox)) {
|
|
885
|
+
throw new Error("Runtime cleanup is still pending.");
|
|
886
|
+
}
|
|
887
|
+
const session = input.owner.scope === "task"
|
|
888
|
+
? recordTaskRuntimeNativeSession(store, {
|
|
889
|
+
taskId: input.owner.taskId,
|
|
890
|
+
roleName: input.owner.roleName,
|
|
891
|
+
agentId: input.agentId,
|
|
892
|
+
adapterId: input.adapterId,
|
|
893
|
+
nativeSessionId: input.nativeSessionId,
|
|
894
|
+
launchId: input.launchId,
|
|
895
|
+
effective: input.effective
|
|
896
|
+
}, now)
|
|
897
|
+
: recordGlobalRuntimeNativeSession(store, {
|
|
898
|
+
roleName: input.owner.roleName,
|
|
899
|
+
agentId: input.agentId,
|
|
900
|
+
adapterId: input.adapterId,
|
|
901
|
+
nativeSessionId: input.nativeSessionId,
|
|
902
|
+
launchId: input.launchId,
|
|
903
|
+
effective: input.effective
|
|
904
|
+
}, now);
|
|
905
|
+
saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, input.launchId));
|
|
906
|
+
return session;
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
completeRuntimeLaunchReservation(owner, launchId, expectedTerminalRunId, beforeComplete) {
|
|
910
|
+
return this.store.transaction((store) => {
|
|
911
|
+
const target = runtimeLifecycleTarget(owner);
|
|
912
|
+
const mailbox = store.getWorkMailbox(target);
|
|
913
|
+
if (!isRuntimeLaunchReservation(mailbox?.processing, launchId))
|
|
914
|
+
return false;
|
|
915
|
+
if (expectedTerminalRunId !== undefined) {
|
|
916
|
+
const expectedRun = owner.scope === "task"
|
|
917
|
+
? store.getAgentRun(owner.taskId, expectedTerminalRunId)
|
|
918
|
+
: null;
|
|
919
|
+
if (owner.scope !== "task"
|
|
920
|
+
|| mailbox?.processing?.executionRef?.type !== "run"
|
|
921
|
+
|| mailbox.processing.executionRef.taskId !== owner.taskId
|
|
922
|
+
|| mailbox.processing.executionRef.id !== expectedTerminalRunId
|
|
923
|
+
|| expectedRun === null
|
|
924
|
+
|| expectedRun.status === "active") {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
beforeComplete?.();
|
|
929
|
+
saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, launchId));
|
|
930
|
+
return true;
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Atomically settles a launch whose host is confirmed absent.
|
|
935
|
+
*
|
|
936
|
+
* The exact reservation is authoritative. If its matching Hook won the
|
|
937
|
+
* transaction race and already cleared that reservation, the session
|
|
938
|
+
* fallback is permitted only while the same Agent/native identity remains
|
|
939
|
+
* current and no later lifecycle work or Task Run exists.
|
|
940
|
+
*/
|
|
941
|
+
settleStoppedRuntimeLaunch(input, now = new Date()) {
|
|
942
|
+
return this.store.transaction((store) => {
|
|
943
|
+
const target = runtimeLifecycleTarget(input.owner);
|
|
944
|
+
const mailbox = store.getWorkMailbox(target);
|
|
945
|
+
if (isRuntimeLaunchReservation(mailbox?.processing, input.launchId)) {
|
|
946
|
+
const sessions = runtimeOwnerSessionSet(store, input.owner);
|
|
947
|
+
const active = sessions?.sessions[sessions.activeAgentId];
|
|
948
|
+
if (runtimeSessionMatchesSettledLaunch(active, input)) {
|
|
949
|
+
markRuntimeOwnerSessionStopped(store, input.owner, now);
|
|
950
|
+
}
|
|
951
|
+
saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, input.launchId));
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
if (hasRuntimeLifecycleWork(mailbox))
|
|
955
|
+
return false;
|
|
956
|
+
if (input.owner.scope === "task"
|
|
957
|
+
&& store.getActiveAgentRun(input.owner.taskId, input.owner.roleName) !== null) {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
const sessions = runtimeOwnerSessionSet(store, input.owner);
|
|
961
|
+
if (sessions === null)
|
|
962
|
+
return true;
|
|
963
|
+
const active = sessions.sessions[sessions.activeAgentId];
|
|
964
|
+
if (!runtimeSessionMatchesSettledLaunch(active, input))
|
|
965
|
+
return false;
|
|
966
|
+
if (active.status !== "stopped") {
|
|
967
|
+
markRuntimeOwnerSessionStopped(store, input.owner, now);
|
|
968
|
+
}
|
|
969
|
+
return true;
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Fast hook path: durably records the native Turn boundary and a two-second
|
|
974
|
+
* closure deadline. It never performs tmux, workspace, or Controller I/O.
|
|
975
|
+
*/
|
|
976
|
+
classifyRuntimeTurnCompleted(input) {
|
|
977
|
+
const task = this.store.getTask(input.taskId);
|
|
978
|
+
const role = this.store.getRole(input.taskId, input.roleName);
|
|
979
|
+
if (task === null || task.status === "archived" || role === null)
|
|
980
|
+
return "obsolete";
|
|
981
|
+
const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
982
|
+
const existing = sessions?.sessions[input.agentId];
|
|
983
|
+
const owner = {
|
|
984
|
+
scope: "task",
|
|
985
|
+
taskId: input.taskId,
|
|
986
|
+
roleName: input.roleName
|
|
987
|
+
};
|
|
988
|
+
if (existing === undefined
|
|
989
|
+
&& !runtimeHookMatchesReservation(this.store, owner, input.launchId))
|
|
990
|
+
return "obsolete";
|
|
991
|
+
try {
|
|
992
|
+
const effectiveExisting = nativeTransitionExisting(this.store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed Role session.");
|
|
993
|
+
const effective = taskSessionEffective(this.store, input.taskId, input.roleName, input.agentId, effectiveExisting);
|
|
994
|
+
if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
|
|
995
|
+
return "obsolete";
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
return "obsolete";
|
|
1000
|
+
}
|
|
1001
|
+
if (sessions?.pendingTurnCompletion !== null && sessions !== null) {
|
|
1002
|
+
return sessions.pendingTurnCompletion.turnId === input.turnId
|
|
1003
|
+
? "apply"
|
|
1004
|
+
: "deferred";
|
|
1005
|
+
}
|
|
1006
|
+
// A normal explicit CLI yield may precede its native Hook; that Hook still
|
|
1007
|
+
// owns the turn fact. A forced cleanup boundary or stopped process instead
|
|
1008
|
+
// makes the old generation obsolete.
|
|
1009
|
+
if (sessions?.inFlight === null || sessions === null) {
|
|
1010
|
+
const cleanup = hasRuntimeCleanupObligation(this.store.getWorkMailbox(runtimeLifecycleTarget({
|
|
1011
|
+
scope: "task",
|
|
1012
|
+
taskId: input.taskId,
|
|
1013
|
+
roleName: input.roleName
|
|
1014
|
+
})));
|
|
1015
|
+
return cleanup || isObsoleteTerminalRuntimeRun(this.store, input)
|
|
1016
|
+
? "obsolete"
|
|
1017
|
+
: "apply";
|
|
1018
|
+
}
|
|
1019
|
+
if (input.runId === undefined || sessions.inFlight.runId !== input.runId) {
|
|
1020
|
+
return "obsolete";
|
|
1021
|
+
}
|
|
1022
|
+
// Provider completion is not a transport acknowledgement. Retain the
|
|
1023
|
+
// immutable Hook until the exact pane receipt independently reconciles
|
|
1024
|
+
// run.pushed; never infer transport from provider progress/terminal facts.
|
|
1025
|
+
return sessions.inFlight.pushedAt === undefined ? "deferred" : "apply";
|
|
1026
|
+
}
|
|
1027
|
+
observeRuntimeTurnCompleted(input, now = new Date()) {
|
|
200
1028
|
return this.store.transaction((store) => {
|
|
201
1029
|
const task = store.getTask(input.taskId);
|
|
202
1030
|
if (task === null)
|
|
203
1031
|
throw new Error(`Task not found: ${input.taskId}.`);
|
|
204
|
-
if (task.status === "archived") {
|
|
205
|
-
throw new Error(`Cannot register a native session for archived Task: ${input.taskId}.`);
|
|
206
|
-
}
|
|
207
1032
|
if (task.status !== "active") {
|
|
208
|
-
throw new Error(`Cannot
|
|
1033
|
+
throw new Error(`Cannot complete a runtime turn for inactive Task: ${input.taskId}.`);
|
|
209
1034
|
}
|
|
210
1035
|
const role = requireRole(store, input.taskId, input.roleName);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1036
|
+
let sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName)
|
|
1037
|
+
?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
|
|
1038
|
+
const existing = sessions.sessions[input.agentId];
|
|
1039
|
+
const owner = {
|
|
1040
|
+
scope: "task",
|
|
1041
|
+
taskId: input.taskId,
|
|
1042
|
+
roleName: input.roleName
|
|
1043
|
+
};
|
|
1044
|
+
if (existing === undefined
|
|
1045
|
+
&& !runtimeHookMatchesReservation(store, owner, input.launchId)) {
|
|
1046
|
+
throw new Error("Runtime turn completion does not match the launch reservation.");
|
|
214
1047
|
}
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
1048
|
+
const effectiveExisting = nativeTransitionExisting(store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed Role session.");
|
|
1049
|
+
const effective = taskSessionEffective(store, task.id, role.name, input.agentId, effectiveExisting);
|
|
1050
|
+
if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
|
|
1051
|
+
throw new Error("Runtime turn completion does not match the effective launch identity.");
|
|
1052
|
+
}
|
|
1053
|
+
if (effectiveExisting !== undefined
|
|
1054
|
+
&& hasRecentTurnId(effectiveExisting.recentCompletedTurnIds, input.turnId)) {
|
|
1055
|
+
return { session: effectiveExisting, duplicate: true };
|
|
1056
|
+
}
|
|
1057
|
+
const pending = sessions.pendingTurnCompletion;
|
|
1058
|
+
if (effectiveExisting !== undefined
|
|
1059
|
+
&& pending !== null
|
|
1060
|
+
&& pending.agentId === input.agentId
|
|
1061
|
+
&& pending.nativeSessionId === input.nativeSessionId
|
|
1062
|
+
&& pending.turnId === input.turnId
|
|
1063
|
+
&& pending.runId === input.runId) {
|
|
1064
|
+
return {
|
|
1065
|
+
session: effectiveExisting,
|
|
1066
|
+
duplicate: true,
|
|
1067
|
+
pendingRunId: pending.runId
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
if (sessions.inFlight === null
|
|
1071
|
+
&& existing !== undefined
|
|
1072
|
+
&& (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))
|
|
1073
|
+
|| isObsoleteTerminalRuntimeRun(store, input))) {
|
|
1074
|
+
return { session: existing, duplicate: false, disposition: "obsolete" };
|
|
1075
|
+
}
|
|
1076
|
+
// Classification is only an optimization. Revalidate the Run inside the
|
|
1077
|
+
// authoritative transaction before a Hook may claim a native identity.
|
|
1078
|
+
if (sessions.inFlight !== null
|
|
1079
|
+
&& (input.runId === undefined
|
|
1080
|
+
|| sessions.inFlight.runId !== input.runId)) {
|
|
1081
|
+
if (effectiveExisting === undefined) {
|
|
1082
|
+
throw new Error("Runtime turn completion no longer matches the in-flight Run.");
|
|
1083
|
+
}
|
|
1084
|
+
return { session: effectiveExisting, duplicate: false };
|
|
1085
|
+
}
|
|
1086
|
+
if (sessions.inFlight !== null && sessions.inFlight.pushedAt === undefined) {
|
|
1087
|
+
throw new Error("Runtime turn completion requires an independently committed transport receipt.");
|
|
220
1088
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
1089
|
+
const idleStatus = effectiveExisting?.status === "stopped"
|
|
1090
|
+
|| effectiveExisting?.status === "broken"
|
|
1091
|
+
? effectiveExisting.status
|
|
1092
|
+
: "ready";
|
|
1093
|
+
sessions = recordRoleAgentSession(sessions, {
|
|
224
1094
|
agentId: input.agentId,
|
|
225
1095
|
adapterId: input.adapterId,
|
|
226
1096
|
nativeSessionId: input.nativeSessionId,
|
|
1097
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
227
1098
|
policy: "fixed",
|
|
228
|
-
status: "running"
|
|
1099
|
+
status: sessions.inFlight === null ? idleStatus : "running",
|
|
1100
|
+
effective
|
|
229
1101
|
}, now);
|
|
230
|
-
|
|
231
|
-
|
|
1102
|
+
if (sessions.inFlight === null) {
|
|
1103
|
+
sessions = rememberRoleAgentCompletedTurn(sessions, input.agentId, input.nativeSessionId, input.turnId, now);
|
|
1104
|
+
store.saveTaskRoleSessionSet(sessions);
|
|
1105
|
+
completeRuntimeHookReservation(store, owner, input.launchId);
|
|
1106
|
+
return { session: sessions.sessions[input.agentId], duplicate: false };
|
|
1107
|
+
}
|
|
1108
|
+
const fence = {
|
|
1109
|
+
agentId: sessions.inFlight.agentId,
|
|
1110
|
+
runId: sessions.inFlight.runId,
|
|
1111
|
+
receiptId: sessions.inFlight.receiptId
|
|
1112
|
+
};
|
|
1113
|
+
const completion = createPendingTurnCompletion({
|
|
1114
|
+
taskId: task.id,
|
|
1115
|
+
roleName: role.name,
|
|
1116
|
+
agentId: input.agentId,
|
|
1117
|
+
nativeSessionId: input.nativeSessionId,
|
|
1118
|
+
turnId: input.turnId,
|
|
1119
|
+
runId: fence.runId,
|
|
1120
|
+
summary: input.summary,
|
|
1121
|
+
observedAt: now,
|
|
1122
|
+
dueAt: new Date(now.getTime() + 2_000)
|
|
1123
|
+
});
|
|
1124
|
+
const wasStalled = isRoleRunStalled(store.listEvents(task.id), fence.runId);
|
|
1125
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_PROGRESS_EVENT, {
|
|
1126
|
+
runId: fence.runId,
|
|
1127
|
+
roleName: role.name,
|
|
1128
|
+
kind: "native-turn",
|
|
1129
|
+
turnId: input.turnId,
|
|
1130
|
+
note: boundedRuntimeNote(input.summary)
|
|
1131
|
+
}, now));
|
|
1132
|
+
if (wasStalled) {
|
|
1133
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RECOVERED_EVENT, {
|
|
1134
|
+
runId: fence.runId,
|
|
1135
|
+
roleName: role.name,
|
|
1136
|
+
progressAt: now.toISOString(),
|
|
1137
|
+
kind: "native-turn"
|
|
1138
|
+
}, now));
|
|
1139
|
+
}
|
|
1140
|
+
clearMatchingLeaderStallAttention(store, task.id, fence.runId);
|
|
1141
|
+
sessions = recordObservedTaskRoleCompletion(sessions, completion);
|
|
1142
|
+
const active = store.getActiveAgentRun(task.id, role.name);
|
|
1143
|
+
if (active === null || active.id !== fence.runId || active.status !== "active") {
|
|
1144
|
+
sessions = settleTaskRoleCompletion(sessions, {
|
|
1145
|
+
agentId: input.agentId,
|
|
1146
|
+
runId: fence.runId,
|
|
1147
|
+
turnId: input.turnId
|
|
1148
|
+
}, now);
|
|
1149
|
+
sessions = updateRoleAgentSessionStatus(sessions, input.agentId, "ready", now);
|
|
1150
|
+
store.saveTaskRoleSessionSet(sessions);
|
|
1151
|
+
completeRuntimeHookReservation(store, owner, input.launchId);
|
|
1152
|
+
return { session: sessions.sessions[input.agentId], duplicate: false };
|
|
1153
|
+
}
|
|
1154
|
+
store.saveTaskRoleSessionSet(sessions);
|
|
1155
|
+
completeRuntimeHookReservation(store, owner, input.launchId);
|
|
1156
|
+
return {
|
|
1157
|
+
session: sessions.sessions[input.agentId],
|
|
1158
|
+
duplicate: false,
|
|
1159
|
+
pendingRunId: fence.runId
|
|
1160
|
+
};
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
listPendingRuntimeTurnCompletions() {
|
|
1164
|
+
return this.store.listTasks().flatMap((task) => (this.store.listRoleSessionSets(task.id).flatMap((sessions) => (sessions.pendingTurnCompletion === null ? [] : [sessions.pendingTurnCompletion]))));
|
|
1165
|
+
}
|
|
1166
|
+
/** Resolves only completions whose grace deadline has elapsed. */
|
|
1167
|
+
resolveDueRuntimeTurnCompletions(now, taskIds) {
|
|
1168
|
+
const due = this.listPendingRuntimeTurnCompletions().filter((completion) => (Date.parse(completion.dueAt) <= now.getTime()
|
|
1169
|
+
&& (taskIds === undefined || taskIds.has(completion.taskId))));
|
|
1170
|
+
const finalized = [];
|
|
1171
|
+
for (const completion of due) {
|
|
1172
|
+
this.store.transaction((store) => {
|
|
1173
|
+
const current = store.getTaskRoleSessionSet(completion.taskId, completion.roleName);
|
|
1174
|
+
if (current === null)
|
|
1175
|
+
return;
|
|
1176
|
+
const pending = current.pendingTurnCompletion;
|
|
1177
|
+
if (pending === null
|
|
1178
|
+
|| pending.turnId !== completion.turnId
|
|
1179
|
+
|| pending.runId !== completion.runId)
|
|
1180
|
+
return;
|
|
1181
|
+
const active = store.getActiveAgentRun(completion.taskId, completion.roleName);
|
|
1182
|
+
if (active?.id === completion.runId) {
|
|
1183
|
+
const result = this.recordRuntimeTurnCompleted({
|
|
1184
|
+
taskId: completion.taskId,
|
|
1185
|
+
roleName: completion.roleName,
|
|
1186
|
+
agentId: completion.agentId,
|
|
1187
|
+
adapterId: current.sessions[completion.agentId].adapterId,
|
|
1188
|
+
nativeSessionId: completion.nativeSessionId,
|
|
1189
|
+
turnId: completion.turnId,
|
|
1190
|
+
expectedRunId: completion.runId,
|
|
1191
|
+
summary: completion.summary
|
|
1192
|
+
}, now);
|
|
1193
|
+
if (result.finalizedRunId !== undefined) {
|
|
1194
|
+
finalized.push(formatTaskRecordReference(completion.taskId, result.finalizedRunId, "agentRun"));
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
let settled = store.getTaskRoleSessionSet(completion.taskId, completion.roleName);
|
|
1198
|
+
if (settled?.pendingTurnCompletion?.turnId !== completion.turnId)
|
|
1199
|
+
return;
|
|
1200
|
+
settled = settleTaskRoleCompletion(settled, {
|
|
1201
|
+
agentId: completion.agentId,
|
|
1202
|
+
runId: completion.runId,
|
|
1203
|
+
turnId: completion.turnId
|
|
1204
|
+
}, now);
|
|
1205
|
+
settled = updateRoleAgentSessionStatus(settled, completion.agentId, "ready", now);
|
|
1206
|
+
store.saveTaskRoleSessionSet(settled);
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
return finalized;
|
|
1210
|
+
}
|
|
1211
|
+
/** A structured native Turn-completion event terminalizes its exact Run fence. */
|
|
1212
|
+
recordRuntimeTurnCompleted(input, now = new Date()) {
|
|
1213
|
+
return this.store.transaction((store) => {
|
|
1214
|
+
const task = store.getTask(input.taskId);
|
|
1215
|
+
if (task === null)
|
|
1216
|
+
throw new Error(`Task not found: ${input.taskId}.`);
|
|
1217
|
+
if (task.status !== "active") {
|
|
1218
|
+
throw new Error(`Cannot complete a runtime turn for inactive Task: ${input.taskId}.`);
|
|
1219
|
+
}
|
|
1220
|
+
const role = requireRole(store, input.taskId, input.roleName);
|
|
1221
|
+
const current = store.getRoleSessionSet(input.taskId, input.roleName)
|
|
1222
|
+
?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
|
|
1223
|
+
const existing = current.sessions[input.agentId];
|
|
1224
|
+
const owner = {
|
|
1225
|
+
scope: "task",
|
|
1226
|
+
taskId: input.taskId,
|
|
1227
|
+
roleName: input.roleName
|
|
1228
|
+
};
|
|
1229
|
+
if (existing === undefined
|
|
1230
|
+
&& !runtimeHookMatchesReservation(store, owner, input.launchId)) {
|
|
1231
|
+
throw new Error("Runtime turn completion does not match the launch reservation.");
|
|
1232
|
+
}
|
|
1233
|
+
const effectiveExisting = nativeTransitionExisting(store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed Role session.");
|
|
1234
|
+
const effective = taskSessionEffective(store, task.id, role.name, input.agentId, effectiveExisting);
|
|
1235
|
+
if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
|
|
1236
|
+
throw new Error("Runtime turn completion does not match the effective launch identity.");
|
|
1237
|
+
}
|
|
1238
|
+
const sessions = effectiveExisting?.status === "ready"
|
|
1239
|
+
? current
|
|
1240
|
+
: recordRoleAgentSession(current, {
|
|
1241
|
+
agentId: input.agentId,
|
|
1242
|
+
adapterId: input.adapterId,
|
|
1243
|
+
nativeSessionId: input.nativeSessionId,
|
|
1244
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
1245
|
+
policy: "fixed",
|
|
1246
|
+
status: "ready",
|
|
1247
|
+
effective
|
|
1248
|
+
}, now);
|
|
1249
|
+
if (input.expectedRunId !== undefined
|
|
1250
|
+
&& store.getActiveAgentRun(task.id, role.name)?.id !== input.expectedRunId) {
|
|
1251
|
+
return { session: sessions.sessions[input.agentId] };
|
|
1252
|
+
}
|
|
1253
|
+
if (sessions !== current)
|
|
1254
|
+
store.saveRoleSessionSet(sessions);
|
|
1255
|
+
completeRuntimeHookReservation(store, owner, input.launchId);
|
|
1256
|
+
const session = sessions.sessions[input.agentId];
|
|
1257
|
+
const active = store.getActiveAgentRun(task.id, role.name);
|
|
1258
|
+
if (task.status !== "active"
|
|
1259
|
+
|| active === null
|
|
1260
|
+
|| active.pushedAt === undefined
|
|
1261
|
+
|| (input.expectedRunId !== undefined && active.id !== input.expectedRunId)) {
|
|
1262
|
+
return { session };
|
|
1263
|
+
}
|
|
1264
|
+
if (role.name !== "leader" || active.workItemId !== undefined) {
|
|
1265
|
+
const summary = `Role turn completed without yui task run yield. Last assistant message: ${input.summary}`;
|
|
1266
|
+
const result = terminalizeExactTaskRun(store, {
|
|
1267
|
+
taskId: task.id,
|
|
1268
|
+
roleName: role.name,
|
|
1269
|
+
agentId: active.effective.agentId,
|
|
1270
|
+
runId: active.id,
|
|
1271
|
+
receiptId: formatAgentRunReceiptId(task.id, active.id),
|
|
1272
|
+
nativeSessionId: input.nativeSessionId,
|
|
1273
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
1274
|
+
outcome: { status: "failed", summary }
|
|
1275
|
+
}, now);
|
|
1276
|
+
if (result.disposition !== "applied" || result.run === null) {
|
|
1277
|
+
throw new Error(`Role Run changed during native Turn completion: ${active.id}/${result.reason}.`);
|
|
1278
|
+
}
|
|
1279
|
+
const terminal = result.run;
|
|
1280
|
+
if (active.purpose === "execution" && active.workItemId !== undefined) {
|
|
1281
|
+
const item = store.getWorkItem(task.id, active.workItemId);
|
|
1282
|
+
if (item !== null && !["completed", "failed", "retired"].includes(item.status)) {
|
|
1283
|
+
store.saveWorkItem(task.id, updateWorkItemStatus(item, "failed", now, summary));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
store.saveRole(task.id, updateRoleStatus(role, "idle", now));
|
|
1287
|
+
clearMatchingLeaderStallAttention(store, task.id, terminal.id);
|
|
1288
|
+
enqueueWork(store, { kind: "role", taskId: task.id, roleName: "leader" }, "role-run-failed", now, [
|
|
1289
|
+
{ type: "run", taskId: task.id, id: terminal.id },
|
|
1290
|
+
...(terminal.workItemId === undefined
|
|
1291
|
+
? []
|
|
1292
|
+
: [{
|
|
1293
|
+
type: "work-item",
|
|
1294
|
+
taskId: task.id,
|
|
1295
|
+
id: terminal.workItemId
|
|
1296
|
+
}])
|
|
1297
|
+
]);
|
|
1298
|
+
return { session, finalizedRunId: terminal.id };
|
|
1299
|
+
}
|
|
1300
|
+
const leaderTarget = { kind: "role", taskId: task.id, roleName: role.name };
|
|
1301
|
+
const leaderMailbox = store.getWorkMailbox(leaderTarget);
|
|
1302
|
+
const recoveryTurn = leaderMailbox?.processing?.executionRef?.type === "run"
|
|
1303
|
+
&& leaderMailbox.processing.executionRef.taskId === task.id
|
|
1304
|
+
&& leaderMailbox.processing.executionRef.id === active.id
|
|
1305
|
+
&& leaderMailbox.processing.batch.reasons.includes("leader-turn-unclosed");
|
|
1306
|
+
const quiescent = leaderMailbox?.pending === null
|
|
1307
|
+
&& !store.listRoles(task.id).some((candidate) => (candidate.name !== "leader"
|
|
1308
|
+
&& store.getActiveAgentRun(task.id, candidate.name) !== null))
|
|
1309
|
+
&& !store.listWorkItems(task.id).some((item) => item.status === "running")
|
|
1310
|
+
&& !store.listInputRequests(task.id).some((request) => request.status === "open");
|
|
1311
|
+
const result = terminalizeExactTaskRun(store, {
|
|
1312
|
+
taskId: task.id,
|
|
1313
|
+
roleName: role.name,
|
|
1314
|
+
agentId: active.effective.agentId,
|
|
1315
|
+
runId: active.id,
|
|
1316
|
+
receiptId: formatAgentRunReceiptId(task.id, active.id),
|
|
1317
|
+
nativeSessionId: input.nativeSessionId,
|
|
1318
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
1319
|
+
outcome: { status: "yielded", summary: input.summary }
|
|
1320
|
+
}, now);
|
|
1321
|
+
if (result.disposition !== "applied" || result.run === null) {
|
|
1322
|
+
throw new Error(`Leader Run changed during native Turn completion: ${active.id}/${result.reason}.`);
|
|
1323
|
+
}
|
|
1324
|
+
const terminal = result.run;
|
|
1325
|
+
if (input.origin === "recovery") {
|
|
1326
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), task.id, RUN_RECOVERED_EVENT, { runId: terminal.id, roleName: role.name, summary: input.summary }, now));
|
|
1327
|
+
}
|
|
1328
|
+
clearMatchingLeaderStallAttention(store, task.id, terminal.id);
|
|
1329
|
+
if (quiescent) {
|
|
1330
|
+
if (recoveryTurn) {
|
|
1331
|
+
const message = "Leader ended a recovery Turn without completing, blocking, or continuing the Task.";
|
|
1332
|
+
store.saveLeaderFailure(recordLeaderFailure(task.id, input.nativeSessionId, message, now, store.getLeaderFailure(task.id)));
|
|
1333
|
+
store.saveOperatorNotification(createLeaderRecoveryNotification(task.id, message, now, store.getOperatorNotification(task.id)));
|
|
1334
|
+
store.saveRole(task.id, updateRoleStatus(role, "failed", now));
|
|
1335
|
+
enqueueWork(store, { kind: "operator" }, "leader-recovery-failed", now, [
|
|
1336
|
+
{ type: "task", id: task.id },
|
|
1337
|
+
{ type: "run", taskId: task.id, id: terminal.id }
|
|
1338
|
+
]);
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
enqueueWork(store, leaderTarget, "leader-turn-unclosed", now, [
|
|
1342
|
+
{ type: "task", id: task.id },
|
|
1343
|
+
{ type: "run", taskId: task.id, id: terminal.id }
|
|
1344
|
+
]);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
return { session, finalizedRunId: terminal.id };
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
classifyClaudeStopFailureEvent(input) {
|
|
1351
|
+
const task = this.store.getTask(input.taskId);
|
|
1352
|
+
if (task?.status !== "active")
|
|
1353
|
+
return "obsolete";
|
|
1354
|
+
const role = this.store.getRole(input.taskId, input.roleName);
|
|
1355
|
+
if (role === null)
|
|
1356
|
+
return "obsolete";
|
|
1357
|
+
const run = this.store.getAgentRun(input.taskId, input.runId);
|
|
1358
|
+
const active = this.store.getActiveAgentRun(input.taskId, input.roleName);
|
|
1359
|
+
if (run === null
|
|
1360
|
+
|| run.status !== "active"
|
|
1361
|
+
|| active?.id !== run.id
|
|
1362
|
+
|| run.roleName !== input.roleName
|
|
1363
|
+
|| run.effective.agentId !== input.agentId
|
|
1364
|
+
|| run.effective.adapterId !== "claude"
|
|
1365
|
+
|| input.adapterId !== "claude")
|
|
1366
|
+
return "obsolete";
|
|
1367
|
+
const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
1368
|
+
const session = sessions?.sessions[input.agentId];
|
|
1369
|
+
if (sessions === null
|
|
1370
|
+
|| session?.nativeSessionId !== input.nativeSessionId
|
|
1371
|
+
|| session.launchId !== input.launchId
|
|
1372
|
+
|| sessions.inFlight?.agentId !== input.agentId
|
|
1373
|
+
|| sessions.inFlight.runId !== input.runId
|
|
1374
|
+
|| sessions.inFlight.receiptId !== `agent-run:${input.taskId}/${input.runId}`)
|
|
1375
|
+
return "obsolete";
|
|
1376
|
+
return "apply";
|
|
1377
|
+
}
|
|
1378
|
+
observeClaudeStopFailureEvent(input, now = new Date()) {
|
|
1379
|
+
return this.store.transaction((store) => {
|
|
1380
|
+
const before = store.getAgentRun(input.taskId, input.runId);
|
|
1381
|
+
if (before === null) {
|
|
1382
|
+
recordObsoleteRuntimeEvent(store, input, "run-missing", now);
|
|
1383
|
+
return { disposition: "obsolete", runId: input.runId };
|
|
1384
|
+
}
|
|
1385
|
+
const summary = claudeStopFailureSummary(input);
|
|
1386
|
+
const result = terminalizeExactTaskRun(store, {
|
|
1387
|
+
taskId: input.taskId,
|
|
1388
|
+
roleName: input.roleName,
|
|
1389
|
+
agentId: input.agentId,
|
|
1390
|
+
runId: input.runId,
|
|
1391
|
+
receiptId: `agent-run:${input.taskId}/${input.runId}`,
|
|
1392
|
+
nativeSessionId: input.nativeSessionId,
|
|
1393
|
+
launchId: input.launchId,
|
|
1394
|
+
outcome: {
|
|
1395
|
+
status: "failed",
|
|
1396
|
+
summary
|
|
1397
|
+
}
|
|
1398
|
+
}, now);
|
|
1399
|
+
if (result.disposition === "obsolete" || result.run === null) {
|
|
1400
|
+
recordObsoleteRuntimeEvent(store, input, result.reason ?? "identity-mismatch-or-terminal", now);
|
|
1401
|
+
return { disposition: "obsolete", runId: input.runId };
|
|
1402
|
+
}
|
|
1403
|
+
enqueueWork(store, runtimeLifecycleTarget({
|
|
1404
|
+
scope: "task",
|
|
1405
|
+
taskId: input.taskId,
|
|
1406
|
+
roleName: input.roleName
|
|
1407
|
+
}), RUNTIME_CLEANUP_REQUIRED_REASON, now, [{ type: "task", id: input.taskId }]);
|
|
1408
|
+
const terminal = result.run;
|
|
1409
|
+
if (before.purpose === "execution" && before.workItemId !== undefined) {
|
|
1410
|
+
const item = store.getWorkItem(input.taskId, before.workItemId);
|
|
1411
|
+
if (item !== null && ![
|
|
1412
|
+
"completed", "failed", "retired"
|
|
1413
|
+
].includes(item.status)) {
|
|
1414
|
+
store.saveWorkItem(input.taskId, updateWorkItemStatus(item, "failed", now, summary));
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const role = store.getRole(input.taskId, input.roleName);
|
|
1418
|
+
if (role !== null) {
|
|
1419
|
+
store.saveRole(input.taskId, updateRoleStatus(role, input.roleName === "leader" ? "failed" : "idle", now));
|
|
1420
|
+
}
|
|
1421
|
+
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.claude-stop-failure", {
|
|
1422
|
+
eventId: input.eventId,
|
|
1423
|
+
runId: input.runId,
|
|
1424
|
+
roleName: input.roleName,
|
|
1425
|
+
outcome: terminal.status
|
|
1426
|
+
}, now));
|
|
1427
|
+
if (input.roleName !== "leader") {
|
|
1428
|
+
enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, before.purpose === "review" ? "review-failed" : "role-run-failed", now, [
|
|
1429
|
+
{ type: "run", taskId: input.taskId, id: terminal.id },
|
|
1430
|
+
...(terminal.workItemId === undefined
|
|
1431
|
+
? []
|
|
1432
|
+
: [{ type: "work-item", taskId: input.taskId, id: terminal.workItemId }])
|
|
1433
|
+
]);
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, summary, now, store.getOperatorNotification(input.taskId)));
|
|
1437
|
+
enqueueWork(store, { kind: "operator" }, "leader-run-failed", now, [
|
|
1438
|
+
{ type: "task", id: input.taskId },
|
|
1439
|
+
{ type: "run", taskId: input.taskId, id: terminal.id }
|
|
1440
|
+
]);
|
|
1441
|
+
}
|
|
1442
|
+
return { disposition: "applied", runId: input.runId };
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Folds a provider SessionStart lifecycle fact
|
|
1447
|
+
* through the canonical contract. Session-started/ready never advance Run
|
|
1448
|
+
* acceptance; this only records generation facts and binds a discovered native
|
|
1449
|
+
* id under the exact launch fence. Idempotent replays are no-ops.
|
|
1450
|
+
*/
|
|
1451
|
+
observeProviderSessionLifecycle(input, now = new Date()) {
|
|
1452
|
+
return this.store.transaction((store) => {
|
|
1453
|
+
const decision = this.foldProviderSignal(store, {
|
|
1454
|
+
kind: "native-session-start",
|
|
1455
|
+
...(input.sessionSource === undefined ? {} : { sessionSource: input.sessionSource }),
|
|
1456
|
+
fence: providerFence(input)
|
|
1457
|
+
}, input.runId);
|
|
1458
|
+
switch (decision.kind) {
|
|
1459
|
+
case "obsolete":
|
|
1460
|
+
recordCanonicalObsolete(store, input, "native-session-lifecycle", decision.reason, now);
|
|
1461
|
+
return "obsolete";
|
|
1462
|
+
case "deferred":
|
|
1463
|
+
return "deferred";
|
|
1464
|
+
case "idempotent":
|
|
1465
|
+
return "applied";
|
|
1466
|
+
case "apply":
|
|
1467
|
+
if (decision.outcome.outcome === "mark-ready"
|
|
1468
|
+
&& input.adapterId === "claude"
|
|
1469
|
+
&& input.sessionSource === "startup"
|
|
1470
|
+
&& store.getTaskRoleSessionSet(input.taskId, input.roleName)
|
|
1471
|
+
?.sessions[input.agentId] === undefined) {
|
|
1472
|
+
// Claude emits startup readiness after native process creation but
|
|
1473
|
+
// before the Controller projects its preallocated Session. Keep
|
|
1474
|
+
// the immutable fact queued until that exact Session is durable.
|
|
1475
|
+
return "deferred";
|
|
1476
|
+
}
|
|
1477
|
+
if (decision.outcome.outcome === "bind-native-session") {
|
|
1478
|
+
const role = store.getRole(input.taskId, input.roleName);
|
|
1479
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
1480
|
+
const run = input.runId === undefined
|
|
1481
|
+
? null
|
|
1482
|
+
: store.getAgentRun(input.taskId, input.runId);
|
|
1483
|
+
if (role === null || sessions === null || run === null) {
|
|
1484
|
+
recordCanonicalObsolete(store, input, "native-session-lifecycle", "bind-state-missing", now);
|
|
1485
|
+
return "obsolete";
|
|
1486
|
+
}
|
|
1487
|
+
const bound = recordRoleAgentSession(sessions, {
|
|
1488
|
+
agentId: input.agentId,
|
|
1489
|
+
adapterId: input.adapterId,
|
|
1490
|
+
nativeSessionId: decision.outcome.nativeSessionId,
|
|
1491
|
+
launchId: input.launchId,
|
|
1492
|
+
policy: "fixed",
|
|
1493
|
+
status: "running",
|
|
1494
|
+
effective: run.effective
|
|
1495
|
+
}, now);
|
|
1496
|
+
store.saveTaskRoleSessionSet(bound);
|
|
1497
|
+
// Codex 0.145 emits SessionStart from inside run_turn(input), so a
|
|
1498
|
+
// runtime-discovered bind is durable evidence that this Run's
|
|
1499
|
+
// initial transport input already crossed the provider boundary.
|
|
1500
|
+
// Preserve that truth before clearing the launch reservation: a
|
|
1501
|
+
// restarted Controller must not allocate a second generation or
|
|
1502
|
+
// push the same first input again. UserPromptSubmit remains the
|
|
1503
|
+
// sole acceptance/delivered transition.
|
|
1504
|
+
if (input.adapterId === "codex" && run.pushedAt === undefined) {
|
|
1505
|
+
const pushed = markAgentRunPushed(run, now);
|
|
1506
|
+
store.saveAgentRun(pushed);
|
|
1507
|
+
markTaskRoleRunPushedInFlight(store, role, pushed, now);
|
|
1508
|
+
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "run.pushed", runLaunchEventPayload(pushed), now));
|
|
1509
|
+
}
|
|
1510
|
+
completeRuntimeHookReservation(store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, input.launchId);
|
|
1511
|
+
}
|
|
1512
|
+
// Session-started/ready/bind facts are recorded provider-neutrally as
|
|
1513
|
+
// an auditable event. The
|
|
1514
|
+
// payload carries the generation fence + preInputReady so the fresh
|
|
1515
|
+
// push gate can read the exact provider-ready fact for this generation.
|
|
1516
|
+
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-session-lifecycle", {
|
|
1517
|
+
eventId: input.eventId,
|
|
1518
|
+
roleName: input.roleName,
|
|
1519
|
+
agentId: input.agentId,
|
|
1520
|
+
adapterId: input.adapterId,
|
|
1521
|
+
launchId: input.launchId,
|
|
1522
|
+
nativeSessionId: input.nativeSessionId,
|
|
1523
|
+
outcome: decision.outcome.outcome,
|
|
1524
|
+
preInputReady: (decision.outcome.outcome === "mark-ready"
|
|
1525
|
+
&& decision.outcome.preInputReady) ? "true" : "false",
|
|
1526
|
+
...(input.runId === undefined ? {} : { runId: input.runId })
|
|
1527
|
+
}, now));
|
|
1528
|
+
return "applied";
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Folds a provider UserPromptSubmit acceptance fact. This is the ONLY path
|
|
1534
|
+
* that may advance a Run to accepted/delivered, and only after an exact
|
|
1535
|
+
* transport receipt has independently committed run.pushed.
|
|
1536
|
+
*/
|
|
1537
|
+
observeProviderPromptAccepted(input, now = new Date()) {
|
|
1538
|
+
return this.store.transaction((store) => {
|
|
1539
|
+
const decision = this.foldProviderSignal(store, {
|
|
1540
|
+
kind: "native-prompt-submit",
|
|
1541
|
+
fence: providerFence(input)
|
|
1542
|
+
}, input.runId);
|
|
1543
|
+
if (decision.kind === "obsolete") {
|
|
1544
|
+
recordCanonicalObsolete(store, input, "native-prompt-accepted", decision.reason, now);
|
|
1545
|
+
return "obsolete";
|
|
1546
|
+
}
|
|
1547
|
+
if (decision.kind === "deferred")
|
|
1548
|
+
return "deferred";
|
|
1549
|
+
if (decision.kind === "idempotent")
|
|
1550
|
+
return "applied";
|
|
1551
|
+
// decision.kind === "apply" && outcome advance-accepted
|
|
1552
|
+
const active = store.getActiveAgentRun(input.taskId, input.roleName);
|
|
1553
|
+
if (active === null || active.id !== input.runId || active.status !== "active") {
|
|
1554
|
+
recordCanonicalObsolete(store, input, "native-prompt-accepted", "run-not-active", now);
|
|
1555
|
+
return "obsolete";
|
|
1556
|
+
}
|
|
1557
|
+
const role = store.getRole(input.taskId, input.roleName);
|
|
1558
|
+
if (role === null) {
|
|
1559
|
+
recordCanonicalObsolete(store, input, "native-prompt-accepted", "role-missing", now);
|
|
1560
|
+
return "obsolete";
|
|
1561
|
+
}
|
|
1562
|
+
if (active.deliveredAt === undefined) {
|
|
1563
|
+
const delivered = markAgentRunDelivered(active, now);
|
|
1564
|
+
store.saveAgentRun(delivered);
|
|
1565
|
+
markTaskRoleRunDeliveredInFlight(store, role, delivered, now);
|
|
1566
|
+
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "run.delivered", runLaunchEventPayload(delivered), now));
|
|
1567
|
+
}
|
|
1568
|
+
return "applied";
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Folds provider-native in-turn progress through the canonical lifecycle
|
|
1573
|
+
* contract. Progress is an advisory semantic fence only: it never advances
|
|
1574
|
+
* Run delivery/acceptance and never reads pane text or process counters.
|
|
1575
|
+
*/
|
|
1576
|
+
observeProviderTurnProgress(input, now = new Date()) {
|
|
1577
|
+
return this.store.transaction((store) => {
|
|
1578
|
+
const receivedAtMs = Date.parse(input.receivedAt);
|
|
1579
|
+
if (!Number.isFinite(receivedAtMs) || receivedAtMs > now.getTime()) {
|
|
1580
|
+
recordCanonicalObsolete(store, input, "native-turn-progress", "received-at-invalid", now);
|
|
1581
|
+
return "obsolete";
|
|
1582
|
+
}
|
|
1583
|
+
const decision = this.foldProviderSignal(store, {
|
|
1584
|
+
kind: "native-turn-progress",
|
|
1585
|
+
...(input.sequence === undefined ? {} : { sequence: input.sequence }),
|
|
1586
|
+
fence: providerFence(input)
|
|
1587
|
+
}, input.runId);
|
|
1588
|
+
switch (decision.kind) {
|
|
1589
|
+
case "obsolete":
|
|
1590
|
+
recordCanonicalObsolete(store, input, "native-turn-progress", decision.reason, now);
|
|
1591
|
+
return "obsolete";
|
|
1592
|
+
case "deferred":
|
|
1593
|
+
return "deferred";
|
|
1594
|
+
case "idempotent":
|
|
1595
|
+
return "applied";
|
|
1596
|
+
case "apply":
|
|
1597
|
+
if (decision.outcome.outcome !== "advance-progress")
|
|
1598
|
+
return "obsolete";
|
|
1599
|
+
if (store.listEvents(input.taskId).some((event) => (event.type === "runtime.provider-turn-progress"
|
|
1600
|
+
&& event.payload.eventId === input.eventId)))
|
|
1601
|
+
return "applied";
|
|
1602
|
+
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-turn-progress", {
|
|
1603
|
+
eventId: input.eventId,
|
|
1604
|
+
roleName: input.roleName,
|
|
1605
|
+
agentId: input.agentId,
|
|
1606
|
+
adapterId: input.adapterId,
|
|
1607
|
+
launchId: input.launchId,
|
|
1608
|
+
nativeSessionId: input.nativeSessionId,
|
|
1609
|
+
runId: input.runId,
|
|
1610
|
+
progressId: input.progressId,
|
|
1611
|
+
progressAt: input.receivedAt,
|
|
1612
|
+
...(input.sequence === undefined ? {} : { sequence: String(input.sequence) })
|
|
1613
|
+
}, now));
|
|
1614
|
+
return "applied";
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Shared projection + canonical fold for provider-native lifecycle signals.
|
|
1620
|
+
* Reads current durable Run/session state into a CanonicalRunExpectation,
|
|
1621
|
+
* maps the neutral signal through the per-adapter registry, and folds. The
|
|
1622
|
+
* fold owns every ordering/identity/evidence decision; this method only reads
|
|
1623
|
+
* durable truth and never branches on the provider name.
|
|
1624
|
+
*/
|
|
1625
|
+
foldProviderSignal(store, signal, runId) {
|
|
1626
|
+
let event;
|
|
1627
|
+
try {
|
|
1628
|
+
event = mapNativeLifecycleSignal(signal);
|
|
1629
|
+
}
|
|
1630
|
+
catch (error) {
|
|
1631
|
+
return { kind: "obsolete", reason: error instanceof Error ? error.message : "map-failed" };
|
|
1632
|
+
}
|
|
1633
|
+
const expectation = this.projectRunExpectation(store, signal.fence, runId);
|
|
1634
|
+
if (expectation === null) {
|
|
1635
|
+
if (preallocatedClaudeStartupAwaitingProjection(store, signal, runId)) {
|
|
1636
|
+
return { kind: "deferred", reason: "preallocated-session-not-yet-projected" };
|
|
1637
|
+
}
|
|
1638
|
+
return { kind: "obsolete", reason: "run-or-role-missing" };
|
|
1639
|
+
}
|
|
1640
|
+
const outcome = foldCanonicalLifecycleEvent(event, expectation);
|
|
1641
|
+
switch (outcome.outcome) {
|
|
1642
|
+
case "obsolete":
|
|
1643
|
+
return { kind: "obsolete", reason: outcome.reason };
|
|
1644
|
+
case "fail-closed":
|
|
1645
|
+
return { kind: "obsolete", reason: `fail-closed:${outcome.reason}` };
|
|
1646
|
+
case "deferred":
|
|
1647
|
+
return { kind: "deferred", reason: outcome.reason };
|
|
1648
|
+
case "idempotent":
|
|
1649
|
+
return { kind: "idempotent", reason: outcome.reason };
|
|
1650
|
+
default:
|
|
1651
|
+
return { kind: "apply", outcome };
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
/** Reads durable Run + session state into the pure fold's expectation shape. */
|
|
1655
|
+
projectRunExpectation(store, fence, runId) {
|
|
1656
|
+
const role = store.getRole(fence.taskId, fence.roleName);
|
|
1657
|
+
if (role === null)
|
|
1658
|
+
return null;
|
|
1659
|
+
const sessionSet = store.getTaskRoleSessionSet(fence.taskId, fence.roleName);
|
|
1660
|
+
const session = sessionSet?.sessions[fence.agentId] ?? null;
|
|
1661
|
+
const boundNativeSessionId = session?.nativeSessionId;
|
|
1662
|
+
if (runId === undefined) {
|
|
1663
|
+
return {
|
|
1664
|
+
fence,
|
|
1665
|
+
sessionStarted: false,
|
|
1666
|
+
ready: false,
|
|
1667
|
+
pushed: false,
|
|
1668
|
+
accepted: false,
|
|
1669
|
+
terminal: false,
|
|
1670
|
+
...(boundNativeSessionId === undefined ? {} : { boundNativeSessionId })
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
const run = store.getAgentRun(fence.taskId, runId);
|
|
1674
|
+
const inFlight = sessionSet?.inFlight;
|
|
1675
|
+
if (run === null
|
|
1676
|
+
|| inFlight === null
|
|
1677
|
+
|| inFlight === undefined
|
|
1678
|
+
|| inFlight.runId !== run.id
|
|
1679
|
+
|| inFlight.agentId !== run.effective.agentId)
|
|
1680
|
+
return null;
|
|
1681
|
+
const owner = { scope: "task", taskId: fence.taskId, roleName: fence.roleName };
|
|
1682
|
+
const launchId = session?.launchId
|
|
1683
|
+
?? (runtimeHookMatchesReservation(store, owner, fence.launchId) ? fence.launchId : undefined);
|
|
1684
|
+
if (launchId === undefined)
|
|
1685
|
+
return null;
|
|
1686
|
+
const expectedFence = {
|
|
1687
|
+
taskId: fence.taskId,
|
|
1688
|
+
roleName: fence.roleName,
|
|
1689
|
+
agentId: run.effective.agentId,
|
|
1690
|
+
adapterId: run.effective.adapterId,
|
|
1691
|
+
runId: run.id,
|
|
1692
|
+
launchId,
|
|
1693
|
+
receiptId: inFlight.receiptId,
|
|
1694
|
+
...(boundNativeSessionId === undefined ? {} : { nativeSessionId: boundNativeSessionId })
|
|
1695
|
+
};
|
|
1696
|
+
const lifecycleEvents = store.listEvents(fence.taskId).filter((event) => (event.type === "runtime.provider-session-lifecycle"
|
|
1697
|
+
&& event.payload.roleName === fence.roleName
|
|
1698
|
+
&& event.payload.agentId === run.effective.agentId
|
|
1699
|
+
&& event.payload.adapterId === run.effective.adapterId
|
|
1700
|
+
&& event.payload.launchId === launchId
|
|
1701
|
+
&& event.payload.nativeSessionId === (boundNativeSessionId ?? fence.nativeSessionId)));
|
|
1702
|
+
return {
|
|
1703
|
+
fence: expectedFence,
|
|
1704
|
+
sessionStarted: lifecycleEvents.length > 0,
|
|
1705
|
+
ready: lifecycleEvents.some((event) => event.payload.preInputReady === "true"),
|
|
1706
|
+
pushed: run.pushedAt !== undefined,
|
|
1707
|
+
accepted: run.deliveredAt !== undefined,
|
|
1708
|
+
terminal: run.status !== "active",
|
|
1709
|
+
...(boundNativeSessionId === undefined ? {} : { boundNativeSessionId })
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
observeObsoleteRuntimeEvent(input, now = new Date()) {
|
|
1713
|
+
this.store.transaction((store) => {
|
|
1714
|
+
if (store.getTask(input.taskId) === null)
|
|
1715
|
+
return;
|
|
1716
|
+
recordObsoleteRuntimeEvent(store, input, input.reason, now);
|
|
232
1717
|
});
|
|
233
1718
|
}
|
|
234
1719
|
recordGlobalRuntimeNativeSession(input, now = new Date()) {
|
|
1720
|
+
return this.store.transaction((store) => (recordGlobalRuntimeNativeSession(store, input, now)));
|
|
1721
|
+
}
|
|
1722
|
+
observeGlobalRuntimeTurnCompleted(input, now = new Date()) {
|
|
235
1723
|
return this.store.transaction((store) => {
|
|
236
1724
|
const role = store.getGlobalRole(input.roleName);
|
|
237
1725
|
if (role === null)
|
|
238
1726
|
throw new Error(`Global Role not found: ${input.roleName}.`);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
throw new Error("Native session registration does not match the active global Role Agent binding.");
|
|
242
|
-
}
|
|
243
|
-
const current = store.getGlobalRoleSessionSet(input.roleName)
|
|
244
|
-
?? createRoleSessionSet({ scope: "global", roleName: input.roleName }, role.activeAgentId, now);
|
|
1727
|
+
let current = store.getGlobalRoleSessionSet(input.roleName)
|
|
1728
|
+
?? createRoleSessionSet({ scope: "global", roleName: input.roleName }, input.agentId, now);
|
|
245
1729
|
const existing = current.sessions[input.agentId];
|
|
246
|
-
|
|
247
|
-
|
|
1730
|
+
const owner = {
|
|
1731
|
+
scope: "global",
|
|
1732
|
+
roleName: input.roleName
|
|
1733
|
+
};
|
|
1734
|
+
if (existing === undefined
|
|
1735
|
+
&& !runtimeHookMatchesReservation(store, owner, input.launchId)) {
|
|
1736
|
+
throw new Error("Runtime turn completion does not match the global launch reservation.");
|
|
248
1737
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1738
|
+
const effectiveExisting = nativeTransitionExisting(store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed global Role session.");
|
|
1739
|
+
const effective = globalSessionEffective(role, effectiveExisting);
|
|
1740
|
+
if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
|
|
1741
|
+
throw new Error("Runtime turn completion does not match the effective global launch identity.");
|
|
1742
|
+
}
|
|
1743
|
+
const completedStatus = effectiveExisting?.status === "stopped"
|
|
1744
|
+
|| effectiveExisting?.status === "broken"
|
|
1745
|
+
? effectiveExisting.status
|
|
1746
|
+
: "ready";
|
|
1747
|
+
current = recordRoleAgentSession(current, {
|
|
252
1748
|
agentId: input.agentId,
|
|
253
1749
|
adapterId: input.adapterId,
|
|
254
1750
|
nativeSessionId: input.nativeSessionId,
|
|
1751
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
1752
|
+
title: effectiveExisting?.title ?? input.title,
|
|
1753
|
+
preview: effectiveExisting?.preview ?? (input.summary === undefined ? undefined : sessionPreview(input.summary)),
|
|
255
1754
|
policy: "fixed",
|
|
256
|
-
status:
|
|
1755
|
+
status: completedStatus,
|
|
1756
|
+
effective
|
|
257
1757
|
}, now);
|
|
258
|
-
|
|
259
|
-
|
|
1758
|
+
current = rememberRoleAgentCompletedTurn(current, input.agentId, input.nativeSessionId, input.turnId, now);
|
|
1759
|
+
store.saveGlobalRoleSessionSet(current);
|
|
1760
|
+
completeRuntimeHookReservation(store, owner, input.launchId);
|
|
1761
|
+
return current.sessions[input.agentId];
|
|
260
1762
|
});
|
|
261
1763
|
}
|
|
1764
|
+
classifyGlobalRuntimeTurnCompleted(input) {
|
|
1765
|
+
const role = this.store.getGlobalRole(input.roleName);
|
|
1766
|
+
if (role === null)
|
|
1767
|
+
return "obsolete";
|
|
1768
|
+
const existing = this.store.getGlobalRoleSessionSet(input.roleName)
|
|
1769
|
+
?.sessions[input.agentId];
|
|
1770
|
+
const owner = {
|
|
1771
|
+
scope: "global",
|
|
1772
|
+
roleName: input.roleName
|
|
1773
|
+
};
|
|
1774
|
+
if (existing === undefined
|
|
1775
|
+
&& !runtimeHookMatchesReservation(this.store, owner, input.launchId))
|
|
1776
|
+
return "obsolete";
|
|
1777
|
+
let effectiveExisting;
|
|
1778
|
+
try {
|
|
1779
|
+
effectiveExisting = nativeTransitionExisting(this.store, owner, existing, input.nativeSessionId, input.launchId, "Runtime turn completion conflicts with the fixed global Role session.");
|
|
1780
|
+
}
|
|
1781
|
+
catch {
|
|
1782
|
+
return "obsolete";
|
|
1783
|
+
}
|
|
1784
|
+
const effective = globalSessionEffective(role, effectiveExisting);
|
|
1785
|
+
if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
|
|
1786
|
+
return "obsolete";
|
|
1787
|
+
}
|
|
1788
|
+
return "apply";
|
|
1789
|
+
}
|
|
262
1790
|
}
|
|
263
|
-
function
|
|
264
|
-
|
|
265
|
-
|
|
1791
|
+
function claudeStopFailureSummary(input) {
|
|
1792
|
+
return [
|
|
1793
|
+
"Claude StopFailure.",
|
|
1794
|
+
`error: ${input.error}`,
|
|
1795
|
+
...(input.errorDetails === undefined
|
|
1796
|
+
? []
|
|
1797
|
+
: [`error_details: ${input.errorDetails}`]),
|
|
1798
|
+
...(input.lastAssistantMessage === undefined
|
|
1799
|
+
? []
|
|
1800
|
+
: [`last_assistant_message: ${input.lastAssistantMessage}`])
|
|
1801
|
+
].join("\n");
|
|
1802
|
+
}
|
|
1803
|
+
function providerFence(input) {
|
|
266
1804
|
return {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
1805
|
+
taskId: input.taskId,
|
|
1806
|
+
roleName: input.roleName,
|
|
1807
|
+
agentId: input.agentId,
|
|
1808
|
+
adapterId: input.adapterId,
|
|
1809
|
+
launchId: input.launchId,
|
|
1810
|
+
nativeSessionId: input.nativeSessionId,
|
|
1811
|
+
...(input.runId === undefined ? {} : { runId: input.runId }),
|
|
1812
|
+
...(input.receiptId === undefined ? {} : { receiptId: input.receiptId })
|
|
273
1813
|
};
|
|
274
1814
|
}
|
|
275
|
-
function
|
|
1815
|
+
function recordCanonicalObsolete(store, input, eventType, reason, now) {
|
|
1816
|
+
recordObsoleteRuntimeEvent(store, { ...input, eventType }, reason, now);
|
|
1817
|
+
}
|
|
1818
|
+
function recordObsoleteRuntimeEvent(store, input, reason, now) {
|
|
1819
|
+
if (store.listEvents(input.taskId).some((event) => (event.type === "runtime.event-obsolete"
|
|
1820
|
+
&& event.payload.eventId === input.eventId)))
|
|
1821
|
+
return;
|
|
1822
|
+
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.event-obsolete", {
|
|
1823
|
+
eventId: input.eventId,
|
|
1824
|
+
eventType: input.eventType ?? input.type ?? "unknown",
|
|
1825
|
+
roleName: input.roleName,
|
|
1826
|
+
agentId: input.agentId,
|
|
1827
|
+
nativeSessionId: input.nativeSessionId,
|
|
1828
|
+
...(input.runId === undefined ? {} : { runId: input.runId }),
|
|
1829
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
1830
|
+
reason
|
|
1831
|
+
}, now));
|
|
1832
|
+
}
|
|
1833
|
+
function isLeaderDisposedWorkItemRun(store, input) {
|
|
1834
|
+
if (input.runId === undefined)
|
|
1835
|
+
return false;
|
|
1836
|
+
const run = store.getAgentRun(input.taskId, input.runId);
|
|
1837
|
+
if (run === null
|
|
1838
|
+
|| run.status !== "failed"
|
|
1839
|
+
|| run.roleName !== input.roleName
|
|
1840
|
+
|| run.effective.agentId !== input.agentId
|
|
1841
|
+
|| run.workItemId === undefined) {
|
|
1842
|
+
return false;
|
|
1843
|
+
}
|
|
1844
|
+
return store.getWorkItem(input.taskId, run.workItemId)?.disposition !== undefined;
|
|
1845
|
+
}
|
|
1846
|
+
function isObsoleteTerminalRuntimeRun(store, input) {
|
|
1847
|
+
if (isLeaderDisposedWorkItemRun(store, input))
|
|
1848
|
+
return true;
|
|
1849
|
+
if (input.runId === undefined)
|
|
1850
|
+
return false;
|
|
1851
|
+
const run = store.getAgentRun(input.taskId, input.runId);
|
|
1852
|
+
if (run === null
|
|
1853
|
+
|| run.status !== "failed"
|
|
1854
|
+
|| run.roleName !== input.roleName
|
|
1855
|
+
|| run.effective.agentId !== input.agentId) {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
return store.listEvents(input.taskId).some((event) => (event.type === "runtime.role-delivery-failed"
|
|
1859
|
+
&& event.payload.runId === input.runId));
|
|
1860
|
+
}
|
|
1861
|
+
function sessionPreview(value) {
|
|
1862
|
+
const normalized = value.trim().replaceAll(/\s+/g, " ");
|
|
1863
|
+
const truncated = normalized.slice(0, 1_024);
|
|
1864
|
+
return /[\uD800-\uDBFF]$/.test(truncated)
|
|
1865
|
+
? truncated.slice(0, -1)
|
|
1866
|
+
: truncated;
|
|
1867
|
+
}
|
|
1868
|
+
function boundedRuntimeNote(value) {
|
|
1869
|
+
const normalized = value.trim().replaceAll(/\s+/gu, " ");
|
|
1870
|
+
return normalized.length <= 280 ? normalized : `${normalized.slice(0, 279)}…`;
|
|
1871
|
+
}
|
|
1872
|
+
function requireRuntimeLaunchReservation(store, owner, launchId) {
|
|
1873
|
+
const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
|
|
1874
|
+
if (!isRuntimeLaunchReservation(mailbox?.processing, launchId)) {
|
|
1875
|
+
throw new Error("Runtime launch reservation no longer matches the launch.");
|
|
1876
|
+
}
|
|
1877
|
+
return mailbox;
|
|
1878
|
+
}
|
|
1879
|
+
function runtimeHookMatchesReservation(store, owner, launchId) {
|
|
1880
|
+
const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
|
|
1881
|
+
if (hasRuntimeCleanupObligation(mailbox))
|
|
1882
|
+
return false;
|
|
1883
|
+
const processing = mailbox?.processing;
|
|
1884
|
+
return launchId !== undefined
|
|
1885
|
+
&& isRuntimeLaunchReservation(processing, launchId);
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Claude may publish SessionStart(startup) synchronously from host.start(),
|
|
1889
|
+
* before the Controller can atomically project the returned Session and bind
|
|
1890
|
+
* the Run. Preserve that exact immutable fact only while the matching launch
|
|
1891
|
+
* reservation is still the durable owner of the active Run. Everything else
|
|
1892
|
+
* remains obsolete/fail-closed.
|
|
1893
|
+
*/
|
|
1894
|
+
function preallocatedClaudeStartupAwaitingProjection(store, signal, runId) {
|
|
1895
|
+
if (signal.kind !== "native-session-start"
|
|
1896
|
+
|| signal.sessionSource !== "startup"
|
|
1897
|
+
|| signal.fence.adapterId !== "claude"
|
|
1898
|
+
|| runId === undefined
|
|
1899
|
+
|| signal.fence.runId !== runId
|
|
1900
|
+
|| signal.fence.launchId === undefined
|
|
1901
|
+
|| signal.fence.nativeSessionId === undefined)
|
|
1902
|
+
return false;
|
|
1903
|
+
const task = store.getTask(signal.fence.taskId);
|
|
1904
|
+
const role = store.getRole(signal.fence.taskId, signal.fence.roleName);
|
|
1905
|
+
const run = store.getActiveAgentRun(signal.fence.taskId, signal.fence.roleName);
|
|
1906
|
+
const sessions = store.getTaskRoleSessionSet(signal.fence.taskId, signal.fence.roleName);
|
|
1907
|
+
if (task === null
|
|
1908
|
+
|| task.status !== "active"
|
|
1909
|
+
|| role === null
|
|
1910
|
+
|| role.activeAgentId !== signal.fence.agentId
|
|
1911
|
+
|| run === null
|
|
1912
|
+
|| run.id !== runId
|
|
1913
|
+
|| run.status !== "active"
|
|
1914
|
+
|| run.effective.agentId !== signal.fence.agentId
|
|
1915
|
+
|| run.effective.adapterId !== "claude"
|
|
1916
|
+
|| (sessions?.inFlight !== null && sessions?.inFlight !== undefined)
|
|
1917
|
+
|| sessions?.sessions[signal.fence.agentId] !== undefined)
|
|
1918
|
+
return false;
|
|
1919
|
+
const mailbox = store.getWorkMailbox(runtimeLifecycleTarget({
|
|
1920
|
+
scope: "task",
|
|
1921
|
+
taskId: signal.fence.taskId,
|
|
1922
|
+
roleName: signal.fence.roleName
|
|
1923
|
+
}));
|
|
1924
|
+
const executionRef = mailbox?.processing?.executionRef;
|
|
1925
|
+
return isRuntimeLaunchReservation(mailbox?.processing, signal.fence.launchId)
|
|
1926
|
+
&& !hasRuntimeCleanupObligation(mailbox)
|
|
1927
|
+
&& executionRef?.type === "run"
|
|
1928
|
+
&& executionRef.taskId === signal.fence.taskId
|
|
1929
|
+
&& executionRef.id === runId
|
|
1930
|
+
&& signal.fence.nativeSessionId === nativeSessionIdForLaunch(store.rootDirectory(), signal.fence.launchId, signal.fence.agentId, "claude");
|
|
1931
|
+
}
|
|
1932
|
+
function completeRuntimeHookReservation(store, owner, launchId) {
|
|
1933
|
+
if (launchId === undefined)
|
|
1934
|
+
return;
|
|
1935
|
+
const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
|
|
1936
|
+
if (!isRuntimeLaunchReservation(mailbox?.processing, launchId))
|
|
1937
|
+
return;
|
|
1938
|
+
saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, launchId));
|
|
1939
|
+
}
|
|
1940
|
+
function saveRuntimeLifecycleMailbox(store, mailbox) {
|
|
1941
|
+
if (mailbox.processing === null && mailbox.pending === null) {
|
|
1942
|
+
store.removeWorkMailbox(mailbox.target);
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
store.saveWorkMailbox(mailbox);
|
|
1946
|
+
}
|
|
1947
|
+
function runtimeOwnerFromTarget(target) {
|
|
1948
|
+
return target.kind === "role-runtime"
|
|
1949
|
+
? {
|
|
1950
|
+
scope: "task",
|
|
1951
|
+
taskId: target.taskId,
|
|
1952
|
+
roleName: target.roleName
|
|
1953
|
+
}
|
|
1954
|
+
: {
|
|
1955
|
+
scope: "global",
|
|
1956
|
+
roleName: target.roleName
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
function sameRuntimeOwner(left, right) {
|
|
1960
|
+
return left.scope === "task"
|
|
1961
|
+
? right.scope === "task"
|
|
1962
|
+
&& left.taskId === right.taskId
|
|
1963
|
+
&& left.roleName === right.roleName
|
|
1964
|
+
: right.scope === "global" && left.roleName === right.roleName;
|
|
1965
|
+
}
|
|
1966
|
+
function dormantRuntimeCandidateIsCurrent(store, candidate) {
|
|
1967
|
+
const { owner } = candidate;
|
|
1968
|
+
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
if (owner.scope === "task"
|
|
1972
|
+
&& store.getActiveAgentRun(owner.taskId, owner.roleName) !== null) {
|
|
1973
|
+
return false;
|
|
1974
|
+
}
|
|
1975
|
+
const sessions = runtimeOwnerSessionSet(store, owner);
|
|
1976
|
+
const active = sessions?.sessions[sessions.activeAgentId];
|
|
1977
|
+
return active !== undefined
|
|
1978
|
+
&& active.status !== "stopped"
|
|
1979
|
+
&& active.agentId === candidate.agentId
|
|
1980
|
+
&& active.adapterId === candidate.adapterId
|
|
1981
|
+
&& active.nativeSessionId === candidate.nativeSessionId
|
|
1982
|
+
&& active.launchId === candidate.launchId
|
|
1983
|
+
&& active.updatedAt === candidate.sessionUpdatedAt;
|
|
1984
|
+
}
|
|
1985
|
+
function markRuntimeOwnerSessionStopped(store, owner, now) {
|
|
1986
|
+
if (owner.scope === "task") {
|
|
1987
|
+
const sessions = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
|
|
1988
|
+
if (sessions === null)
|
|
1989
|
+
return false;
|
|
1990
|
+
const active = sessions.sessions[sessions.activeAgentId];
|
|
1991
|
+
if (active === undefined || active.status === "stopped")
|
|
1992
|
+
return false;
|
|
1993
|
+
store.saveTaskRoleSessionSet(updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "stopped", now));
|
|
1994
|
+
return true;
|
|
1995
|
+
}
|
|
1996
|
+
const sessions = store.getGlobalRoleSessionSet(owner.roleName);
|
|
1997
|
+
if (sessions === null)
|
|
1998
|
+
return false;
|
|
1999
|
+
const active = sessions.sessions[sessions.activeAgentId];
|
|
2000
|
+
if (active === undefined || active.status === "stopped")
|
|
2001
|
+
return false;
|
|
2002
|
+
store.saveGlobalRoleSessionSet(updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "stopped", now));
|
|
2003
|
+
return true;
|
|
2004
|
+
}
|
|
2005
|
+
function runtimeOwnerSessionSet(store, owner) {
|
|
2006
|
+
return owner.scope === "task"
|
|
2007
|
+
? store.getTaskRoleSessionSet(owner.taskId, owner.roleName)
|
|
2008
|
+
: store.getGlobalRoleSessionSet(owner.roleName);
|
|
2009
|
+
}
|
|
2010
|
+
function runtimeSessionMatchesSettledLaunch(session, input) {
|
|
2011
|
+
return session !== undefined
|
|
2012
|
+
&& session.agentId === input.agentId
|
|
2013
|
+
&& session.adapterId === input.adapterId
|
|
2014
|
+
&& (input.nativeSessionId === undefined
|
|
2015
|
+
|| session.nativeSessionId === input.nativeSessionId);
|
|
2016
|
+
}
|
|
2017
|
+
function preparedSessionMatches(session, agentId, adapterId, nativeSessionId, launchId) {
|
|
2018
|
+
if (session === undefined)
|
|
2019
|
+
return nativeSessionId === undefined;
|
|
2020
|
+
if (session.agentId !== agentId
|
|
2021
|
+
|| session.adapterId !== adapterId
|
|
2022
|
+
|| session.status === "stopped"
|
|
2023
|
+
|| session.status === "broken"
|
|
2024
|
+
|| (nativeSessionId !== undefined
|
|
2025
|
+
&& session.nativeSessionId !== nativeSessionId)) {
|
|
2026
|
+
return false;
|
|
2027
|
+
}
|
|
2028
|
+
return launchId === undefined
|
|
2029
|
+
? session.launchId === undefined
|
|
2030
|
+
: session.launchId === launchId;
|
|
2031
|
+
}
|
|
2032
|
+
function preparedDeliveryFailureSessionFenceMatches(sessions, session, input) {
|
|
2033
|
+
if (sessions === null)
|
|
2034
|
+
return input.nativeSessionId === undefined;
|
|
2035
|
+
if (sessions.activeAgentId !== input.agentId)
|
|
2036
|
+
return false;
|
|
2037
|
+
if (sessions.inFlight === null) {
|
|
2038
|
+
return input.nativeSessionId === undefined
|
|
2039
|
+
&& session === undefined
|
|
2040
|
+
&& sessions.pendingTurnCompletion === null;
|
|
2041
|
+
}
|
|
2042
|
+
return sessions.inFlight.agentId === input.agentId
|
|
2043
|
+
&& sessions.inFlight.runId === input.runId
|
|
2044
|
+
&& sessions.inFlight.receiptId === formatAgentRunReceiptId(input.taskId, input.runId)
|
|
2045
|
+
&& sessions.inFlight.pushedAt === undefined
|
|
2046
|
+
&& preparedSessionMatches(session, input.agentId, input.adapterId, input.nativeSessionId, input.launchId);
|
|
2047
|
+
}
|
|
2048
|
+
function preparedReservationMatches(mailbox, taskId, runId, launchId) {
|
|
2049
|
+
if (launchId === undefined) {
|
|
2050
|
+
return !isRuntimeLaunchReservation(mailbox?.processing);
|
|
2051
|
+
}
|
|
2052
|
+
const processing = mailbox?.processing;
|
|
2053
|
+
return isRuntimeLaunchReservation(processing, launchId)
|
|
2054
|
+
&& !hasRuntimeCleanupObligation(mailbox)
|
|
2055
|
+
&& processing?.executionRef?.type === "run"
|
|
2056
|
+
&& processing.executionRef.taskId === taskId
|
|
2057
|
+
&& processing.executionRef.id === runId;
|
|
2058
|
+
}
|
|
2059
|
+
function matchingPreparedRuntimeReservation(store, input) {
|
|
2060
|
+
if (input.launchId === undefined
|
|
2061
|
+
|| input.session?.nativeSessionId === undefined) {
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
const owner = {
|
|
2065
|
+
scope: "task",
|
|
2066
|
+
taskId: input.task.id,
|
|
2067
|
+
roleName: input.role.name
|
|
2068
|
+
};
|
|
2069
|
+
const mailbox = store.getWorkMailbox(runtimeLifecycleTarget(owner));
|
|
2070
|
+
if (isRuntimeLaunchReservation(mailbox?.processing, input.launchId)
|
|
2071
|
+
&& mailbox?.processing?.executionRef?.type === "run"
|
|
2072
|
+
&& mailbox.processing.executionRef.id === input.run.id
|
|
2073
|
+
&& !hasRuntimeCleanupObligation(mailbox)) {
|
|
2074
|
+
return mailbox;
|
|
2075
|
+
}
|
|
2076
|
+
throw new Error("Prepared Role session does not match its launch generation.");
|
|
2077
|
+
}
|
|
2078
|
+
function completePreparedRuntimeReservation(store, mailbox) {
|
|
2079
|
+
if (mailbox === null || mailbox.processing === null)
|
|
2080
|
+
return;
|
|
2081
|
+
saveRuntimeLifecycleMailbox(store, completeProcessing(mailbox, mailbox.processing.batchId));
|
|
2082
|
+
}
|
|
2083
|
+
function recordTaskRuntimeNativeSession(store, input, now) {
|
|
2084
|
+
const task = store.getTask(input.taskId);
|
|
2085
|
+
if (task === null)
|
|
2086
|
+
throw new Error(`Task not found: ${input.taskId}.`);
|
|
2087
|
+
if (task.status === "archived") {
|
|
2088
|
+
throw new Error(`Cannot register a native session for archived Task: ${input.taskId}.`);
|
|
2089
|
+
}
|
|
2090
|
+
if (task.status !== "active") {
|
|
2091
|
+
throw new Error(`Cannot register a native session for a Task that is not active: ${input.taskId}.`);
|
|
2092
|
+
}
|
|
2093
|
+
const role = requireRole(store, input.taskId, input.roleName);
|
|
2094
|
+
const current = store.getRoleSessionSet(input.taskId, input.roleName)
|
|
2095
|
+
?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, input.agentId, now);
|
|
2096
|
+
const existing = current.sessions[input.agentId];
|
|
2097
|
+
const effectiveExisting = nativeTransitionExisting(store, { scope: "task", taskId: input.taskId, roleName: input.roleName }, existing, input.nativeSessionId, input.launchId, "Native session registration conflicts with the fixed Role session.");
|
|
2098
|
+
if (existing?.status === "running"
|
|
2099
|
+
&& (input.launchId === undefined || existing.launchId === input.launchId))
|
|
2100
|
+
return existing;
|
|
2101
|
+
const resolvedEffective = taskSessionEffective(store, input.taskId, input.roleName, input.agentId, effectiveExisting);
|
|
2102
|
+
const effective = input.effective === undefined
|
|
2103
|
+
? resolvedEffective
|
|
2104
|
+
: validateEffectiveLaunchSnapshot(input.effective);
|
|
2105
|
+
if (!effectiveLaunchSnapshotsCompatible(resolvedEffective, effective)) {
|
|
2106
|
+
throw new Error("Reserved native Session effective launch changed before persistence.");
|
|
2107
|
+
}
|
|
2108
|
+
if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
|
|
2109
|
+
throw new Error("Native session registration does not match the effective launch identity.");
|
|
2110
|
+
}
|
|
2111
|
+
const updated = recordRoleAgentSession(current, {
|
|
2112
|
+
agentId: input.agentId,
|
|
2113
|
+
adapterId: input.adapterId,
|
|
2114
|
+
nativeSessionId: input.nativeSessionId,
|
|
2115
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
2116
|
+
policy: "fixed",
|
|
2117
|
+
status: "running",
|
|
2118
|
+
effective
|
|
2119
|
+
}, now);
|
|
2120
|
+
store.saveRoleSessionSet(updated);
|
|
2121
|
+
return updated.sessions[input.agentId];
|
|
2122
|
+
}
|
|
2123
|
+
function recordGlobalRuntimeNativeSession(store, input, now) {
|
|
2124
|
+
const role = store.getGlobalRole(input.roleName);
|
|
2125
|
+
if (role === null)
|
|
2126
|
+
throw new Error(`Global Role not found: ${input.roleName}.`);
|
|
2127
|
+
const current = store.getGlobalRoleSessionSet(input.roleName)
|
|
2128
|
+
?? createRoleSessionSet({ scope: "global", roleName: input.roleName }, input.agentId, now);
|
|
2129
|
+
const existing = current.sessions[input.agentId];
|
|
2130
|
+
const effectiveExisting = nativeTransitionExisting(store, { scope: "global", roleName: input.roleName }, existing, input.nativeSessionId, input.launchId, "Native session registration conflicts with the fixed global Role session.");
|
|
2131
|
+
if (existing?.status === "running"
|
|
2132
|
+
&& (input.launchId === undefined || existing.launchId === input.launchId))
|
|
2133
|
+
return existing;
|
|
2134
|
+
const resolvedEffective = globalSessionEffective(role, effectiveExisting);
|
|
2135
|
+
const effective = input.effective === undefined
|
|
2136
|
+
? resolvedEffective
|
|
2137
|
+
: validateEffectiveLaunchSnapshot(input.effective);
|
|
2138
|
+
if (!effectiveLaunchSnapshotsCompatible(resolvedEffective, effective)) {
|
|
2139
|
+
throw new Error("Reserved global native Session effective launch changed before persistence.");
|
|
2140
|
+
}
|
|
2141
|
+
if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
|
|
2142
|
+
throw new Error("Native session registration does not match the effective global launch identity.");
|
|
2143
|
+
}
|
|
2144
|
+
const updated = recordRoleAgentSession(current, {
|
|
2145
|
+
agentId: input.agentId,
|
|
2146
|
+
adapterId: input.adapterId,
|
|
2147
|
+
nativeSessionId: input.nativeSessionId,
|
|
2148
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
2149
|
+
policy: "fixed",
|
|
2150
|
+
status: "running",
|
|
2151
|
+
effective
|
|
2152
|
+
}, now);
|
|
2153
|
+
store.saveGlobalRoleSessionSet(updated);
|
|
2154
|
+
return updated.sessions[input.agentId];
|
|
2155
|
+
}
|
|
2156
|
+
function nativeTransitionExisting(store, owner, existing, nativeSessionId, launchId, conflictMessage) {
|
|
2157
|
+
if (existing === undefined || existing.nativeSessionId === nativeSessionId) {
|
|
2158
|
+
return existing;
|
|
2159
|
+
}
|
|
2160
|
+
if ((existing.status === "stopped" || existing.status === "broken")
|
|
2161
|
+
&& runtimeHookMatchesReservation(store, owner, launchId)) {
|
|
2162
|
+
return undefined;
|
|
2163
|
+
}
|
|
2164
|
+
throw new Error(conflictMessage);
|
|
2165
|
+
}
|
|
2166
|
+
function mapRole(store, role) {
|
|
276
2167
|
const binding = activeRoleAgentBinding(role);
|
|
2168
|
+
const item = store.listWorkItems(role.taskId).find((candidate) => (candidate.assignee === role.name
|
|
2169
|
+
&& !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
|
|
2170
|
+
const workspace = (item === null
|
|
2171
|
+
? store.getTaskWorkspace(role.taskId)
|
|
2172
|
+
: store.getWorkItemWorkspace(role.taskId, item.id))
|
|
2173
|
+
?? store.getTaskWorkspace(role.taskId)
|
|
2174
|
+
?? undefined;
|
|
2175
|
+
const reopened = role.name === "leader"
|
|
2176
|
+
&& store.getPendingWakeup(role.taskId)?.reasons.includes("task-reopened") === true;
|
|
2177
|
+
const effective = reopened
|
|
2178
|
+
? resolveEffectiveLaunch({
|
|
2179
|
+
role,
|
|
2180
|
+
purpose: "execution",
|
|
2181
|
+
...(workspace === undefined ? {} : { workspace })
|
|
2182
|
+
})
|
|
2183
|
+
: activeLiveRoleAgentSession(store.getTaskRoleSessionSet(role.taskId, role.name))?.effective ?? resolveEffectiveLaunch({
|
|
2184
|
+
role,
|
|
2185
|
+
purpose: "execution",
|
|
2186
|
+
...(workspace === undefined ? {} : { workspace })
|
|
2187
|
+
});
|
|
277
2188
|
return {
|
|
278
2189
|
taskId: role.taskId,
|
|
279
2190
|
name: role.name,
|
|
280
2191
|
activeAgentId: role.activeAgentId,
|
|
281
2192
|
adapterId: binding.adapterId,
|
|
2193
|
+
...(binding.config.model === undefined ? {} : { model: binding.config.model }),
|
|
2194
|
+
...(binding.config.effort === undefined ? {} : { effort: binding.config.effort }),
|
|
2195
|
+
effective,
|
|
2196
|
+
workspace: role.workspace,
|
|
2197
|
+
...(workspace === undefined ? {} : { managedWorkspace: workspace }),
|
|
282
2198
|
status: role.status
|
|
283
2199
|
};
|
|
284
2200
|
}
|
|
2201
|
+
function taskSessionEffective(store, taskId, roleName, agentId, existing) {
|
|
2202
|
+
const active = store.getActiveAgentRun(taskId, roleName);
|
|
2203
|
+
if (active !== null) {
|
|
2204
|
+
if (active.effective.agentId !== agentId) {
|
|
2205
|
+
throw new Error(`Native Session registration does not match the effective Run Agent: ${taskId}/${roleName}.`);
|
|
2206
|
+
}
|
|
2207
|
+
return active.effective;
|
|
2208
|
+
}
|
|
2209
|
+
if (existing !== undefined)
|
|
2210
|
+
return existing.effective;
|
|
2211
|
+
const role = store.getRole(taskId, roleName);
|
|
2212
|
+
if (role === null)
|
|
2213
|
+
throw new Error(`Role not found: ${taskId}/${roleName}.`);
|
|
2214
|
+
const item = store.listWorkItems(taskId).find((candidate) => (candidate.assignee === roleName
|
|
2215
|
+
&& !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
|
|
2216
|
+
const workspace = (item === null
|
|
2217
|
+
? store.getTaskWorkspace(taskId)
|
|
2218
|
+
: store.getWorkItemWorkspace(taskId, item.id))
|
|
2219
|
+
?? store.getTaskWorkspace(taskId)
|
|
2220
|
+
?? undefined;
|
|
2221
|
+
const effective = resolveEffectiveLaunch({
|
|
2222
|
+
role,
|
|
2223
|
+
purpose: "execution",
|
|
2224
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
2225
|
+
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
2226
|
+
});
|
|
2227
|
+
if (effective.agentId !== agentId) {
|
|
2228
|
+
throw new Error(`Native Session registration does not match Role desired Agent: ${agentId}.`);
|
|
2229
|
+
}
|
|
2230
|
+
return effective;
|
|
2231
|
+
}
|
|
2232
|
+
function globalSessionEffective(role, existing) {
|
|
2233
|
+
return existing?.effective ?? resolveEffectiveLaunch({ role, purpose: "execution" });
|
|
2234
|
+
}
|
|
285
2235
|
function mapSession(session) {
|
|
286
2236
|
return {
|
|
287
2237
|
agentId: session.agentId,
|
|
288
2238
|
adapterId: session.adapterId,
|
|
289
2239
|
nativeSessionId: session.nativeSessionId,
|
|
290
|
-
|
|
2240
|
+
...(session.launchId === undefined ? {} : { launchId: session.launchId }),
|
|
2241
|
+
status: session.status,
|
|
2242
|
+
effective: session.effective,
|
|
2243
|
+
updatedAt: session.updatedAt
|
|
291
2244
|
};
|
|
292
2245
|
}
|
|
293
|
-
function saveTaskSession(store, role, session, status, now) {
|
|
2246
|
+
function saveTaskSession(store, role, session, status, now, launchId) {
|
|
294
2247
|
const current = store.getRoleSessionSet(role.taskId, role.name)
|
|
295
|
-
?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name },
|
|
2248
|
+
?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name }, session.agentId, now);
|
|
296
2249
|
const updated = recordRoleAgentSession(current, {
|
|
297
2250
|
agentId: session.agentId,
|
|
298
2251
|
adapterId: session.adapterId,
|
|
299
2252
|
nativeSessionId: session.nativeSessionId,
|
|
2253
|
+
...(launchId === undefined ? {} : { launchId }),
|
|
300
2254
|
policy: "fixed",
|
|
301
|
-
status
|
|
2255
|
+
status,
|
|
2256
|
+
effective: session.effective
|
|
2257
|
+
}, now);
|
|
2258
|
+
store.saveRoleSessionSet(updated);
|
|
2259
|
+
}
|
|
2260
|
+
function bindTaskRoleRunInFlight(store, role, run, now) {
|
|
2261
|
+
const agentId = run.effective.agentId;
|
|
2262
|
+
let current = store.getRoleSessionSet(role.taskId, role.name)
|
|
2263
|
+
?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name }, agentId, now);
|
|
2264
|
+
if (current.activeAgentId !== agentId) {
|
|
2265
|
+
if (current.inFlight !== null || current.pendingTurnCompletion !== null) {
|
|
2266
|
+
throw new Error("Task Role Session identity cannot change with an unsettled Run fence.");
|
|
2267
|
+
}
|
|
2268
|
+
const liveConflict = Object.values(current.sessions).find((session) => (session.agentId !== agentId
|
|
2269
|
+
&& session.status !== "stopped"
|
|
2270
|
+
&& session.status !== "broken"));
|
|
2271
|
+
if (liveConflict !== undefined) {
|
|
2272
|
+
throw new Error(`Task Role Session identity cannot change while ${liveConflict.agentId} is live.`);
|
|
2273
|
+
}
|
|
2274
|
+
current = {
|
|
2275
|
+
...current,
|
|
2276
|
+
activeAgentId: agentId,
|
|
2277
|
+
updatedAt: now.toISOString()
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
const updated = bindTaskRoleRun(current, {
|
|
2281
|
+
agentId,
|
|
2282
|
+
runId: run.id,
|
|
2283
|
+
receiptId: formatAgentRunReceiptId(role.taskId, run.id)
|
|
2284
|
+
}, now);
|
|
2285
|
+
store.saveRoleSessionSet(updated);
|
|
2286
|
+
}
|
|
2287
|
+
function markTaskRoleRunPushedInFlight(store, role, run, now) {
|
|
2288
|
+
const current = store.getRoleSessionSet(role.taskId, role.name);
|
|
2289
|
+
if (current === null) {
|
|
2290
|
+
throw new Error(`Task Role session set is missing for pushed Run: ${run.id}.`);
|
|
2291
|
+
}
|
|
2292
|
+
const updated = markTaskRoleRunPushed(current, {
|
|
2293
|
+
agentId: run.effective.agentId,
|
|
2294
|
+
runId: run.id,
|
|
2295
|
+
receiptId: formatAgentRunReceiptId(role.taskId, run.id)
|
|
2296
|
+
}, now);
|
|
2297
|
+
store.saveRoleSessionSet(updated);
|
|
2298
|
+
}
|
|
2299
|
+
function markTaskRoleRunDeliveredInFlight(store, role, run, now) {
|
|
2300
|
+
const current = store.getRoleSessionSet(role.taskId, role.name);
|
|
2301
|
+
if (current === null) {
|
|
2302
|
+
throw new Error(`Task Role session set is missing for delivered Run: ${run.id}.`);
|
|
2303
|
+
}
|
|
2304
|
+
const updated = markTaskRoleRunDelivered(current, {
|
|
2305
|
+
agentId: run.effective.agentId,
|
|
2306
|
+
runId: run.id,
|
|
2307
|
+
receiptId: formatAgentRunReceiptId(role.taskId, run.id)
|
|
2308
|
+
}, now);
|
|
2309
|
+
store.saveRoleSessionSet(updated);
|
|
2310
|
+
}
|
|
2311
|
+
function clearTaskRoleRunInFlight(store, role, run, now) {
|
|
2312
|
+
const current = store.getRoleSessionSet(role.taskId, role.name);
|
|
2313
|
+
if (current === null)
|
|
2314
|
+
return;
|
|
2315
|
+
if (current.inFlight?.runId !== run.id)
|
|
2316
|
+
return;
|
|
2317
|
+
const updated = clearTaskRoleRun(current, {
|
|
2318
|
+
agentId: run.effective.agentId,
|
|
2319
|
+
runId: run.id,
|
|
2320
|
+
receiptId: formatAgentRunReceiptId(role.taskId, run.id)
|
|
302
2321
|
}, now);
|
|
303
2322
|
store.saveRoleSessionSet(updated);
|
|
304
2323
|
}
|
|
@@ -314,9 +2333,40 @@ function updateTaskSessionStatusIfPresent(store, taskId, roleName, agentId, stat
|
|
|
314
2333
|
return;
|
|
315
2334
|
store.saveRoleSessionSet(updateRoleAgentSessionStatus(set, agentId, status, now));
|
|
316
2335
|
}
|
|
2336
|
+
function schedulerRoleSessionsMatch(current, expected) {
|
|
2337
|
+
if (current === null || expected === null)
|
|
2338
|
+
return current === expected;
|
|
2339
|
+
return current.agentId === expected.agentId
|
|
2340
|
+
&& current.adapterId === expected.adapterId
|
|
2341
|
+
&& current.nativeSessionId === expected.nativeSessionId
|
|
2342
|
+
&& current.launchId === expected.launchId
|
|
2343
|
+
&& isDeepStrictEqual(current.effective, expected.effective);
|
|
2344
|
+
}
|
|
2345
|
+
function matchesStallSessionFence(current, expected) {
|
|
2346
|
+
if (current === null || expected === null)
|
|
2347
|
+
return current === expected;
|
|
2348
|
+
return current.agentId === expected.agentId
|
|
2349
|
+
&& current.adapterId === expected.adapterId
|
|
2350
|
+
&& current.nativeSessionId === expected.nativeSessionId
|
|
2351
|
+
&& current.launchId === expected.launchId
|
|
2352
|
+
&& current.status === expected.status;
|
|
2353
|
+
}
|
|
317
2354
|
function requireRole(store, taskId, roleName) {
|
|
318
2355
|
const role = store.getRole(taskId, roleName);
|
|
319
2356
|
if (role === null)
|
|
320
2357
|
throw new Error(`Role not found: ${taskId}/${roleName}.`);
|
|
321
2358
|
return role;
|
|
322
2359
|
}
|
|
2360
|
+
function runLaunchEventPayload(run) {
|
|
2361
|
+
return {
|
|
2362
|
+
runId: run.id,
|
|
2363
|
+
role: run.roleName,
|
|
2364
|
+
purpose: run.purpose,
|
|
2365
|
+
mode: run.mode,
|
|
2366
|
+
agent: `${run.effective.agentId}/${run.effective.adapterId}`,
|
|
2367
|
+
effectiveRevision: String(run.effective.sourceDesiredRevision),
|
|
2368
|
+
profileAccess: run.effective.profileAccess,
|
|
2369
|
+
effectivePermission: run.effective.permission.strategy,
|
|
2370
|
+
writeProjectIds: run.effective.writeProjectIds.join(",") || "none"
|
|
2371
|
+
};
|
|
2372
|
+
}
|