@zq-silk/yui 0.15.7 → 0.15.8
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 +192 -399
- package/README.md +127 -1149
- package/dist/agent/adapterCatalog.js +15 -2
- package/dist/agent/agent.js +23 -3
- package/dist/agent/argumentPolicy.js +7 -1
- package/dist/agent/connectionPlan.js +62 -0
- package/dist/agent/executionComponents.js +158 -0
- package/dist/agent/launchEnvironment.js +31 -3
- package/dist/agent/managedRuntimeEnvironment.js +3 -5
- package/dist/{turn/turn.js → agentRun/agentRun.js} +166 -109
- package/dist/{turn/turnIdentity.js → agentRun/runIdentity.js} +4 -4
- package/dist/brief/taskBrief.js +12 -0
- package/dist/cli/agentConfigurationPicker.js +13 -0
- package/dist/cli/commandCatalog.js +157 -70
- package/dist/cli/interactionCandidates.js +5 -5
- package/dist/cli/interactionPolicy.js +38 -8
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/managedDiagnostics.js +28 -0
- package/dist/cli/operatorWizard.js +1 -7
- package/dist/cli/roleOptionOrder.js +27 -0
- package/dist/cli/roleWizard.js +50 -14
- package/dist/cli/updateOrchestrator.js +1 -1
- package/dist/cli/updatePorts.js +3 -4
- package/dist/cli.js +178 -95
- package/dist/commands/agentCommands.js +72 -14
- package/dist/commands/capabilityCommands.js +9 -6
- package/dist/commands/configCommands.js +20 -20
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +24 -24
- package/dist/commands/globalRoleCommands.js +1 -1
- package/dist/commands/grantCommands.js +4 -4
- package/dist/commands/operatorCommands.js +1 -7
- package/dist/commands/projectCommands.js +4 -4
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/roleConfiguration.js +25 -5
- package/dist/commands/roleRuntimeGuard.js +4 -5
- package/dist/commands/sessionCommands.js +3 -7
- package/dist/commands/taskActivationCommands.js +259 -0
- package/dist/commands/taskActor.js +28 -49
- package/dist/commands/taskCommands.js +1188 -710
- package/dist/commands/taskContextCommand.js +39 -583
- package/dist/commands/taskExecutionCommands.js +32 -32
- package/dist/commands/taskInputCommands.js +40 -104
- package/dist/commands/taskIntegrationCommands.js +3 -2
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskNextActionCommand.js +8 -8
- package/dist/commands/taskOverviewCommand.js +33 -45
- package/dist/commands/taskRemoteDeliveryCommand.js +2 -2
- package/dist/commands/taskRoleRuntimeStatus.js +133 -102
- package/dist/commands/telemetryCommands.js +36 -38
- package/dist/config/configCatalog.js +4 -4
- package/dist/config/yuiConfig.js +8 -8
- package/dist/context/contextSnapshot.js +10 -10
- package/dist/context/dispatchContext.js +11 -11
- package/dist/context/roleSessionContext.js +6 -3
- package/dist/context/{turnContextPack.js → runContextPack.js} +146 -81
- package/dist/context/{turnInputContract.js → runInputContract.js} +73 -60
- package/dist/context/sessionBootstrapManifest.js +21 -2
- package/dist/context/sourceRunContext.js +30 -0
- package/dist/context/taskContext.js +458 -0
- package/dist/context/wakeNotification.js +27 -27
- package/dist/controller/agentRuntimeObserver.js +21 -24
- package/dist/controller/capabilityBridge.js +17 -6
- package/dist/controller/clientRuntime.js +65 -92
- package/dist/controller/controller.js +114 -188
- package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
- package/dist/controller/jobControl.js +54 -85
- package/dist/controller/resourceInventory.js +8 -27
- package/dist/controller/resourceInventoryLinux.js +12 -13
- package/dist/controller/runtime.js +529 -479
- package/dist/controller/runtimeEventInbox.js +55 -25
- package/dist/controller/runtimeEventProcessor.js +22 -31
- package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
- package/dist/controller/runtimeLaunchCoordinator.js +80 -426
- package/dist/controller/runtimeObservationHook.js +14 -18
- package/dist/controller/sessionNotify.js +16 -24
- package/dist/controller/sessionOwnerReconciliation.js +168 -50
- package/dist/controller/structuredProviderObservation.js +138 -99
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/coordination/workMailboxQueue.js +36 -33
- package/dist/core/boundedRpc.js +8 -1
- package/dist/core/controllerClient.js +20 -1
- package/dist/core/controllerServer.js +4 -4
- package/dist/doctor/doctor.js +13 -2
- package/dist/domain/agentResultTransport.js +9 -9
- package/dist/execution/codexThreadNaming.js +2 -8
- package/dist/execution/executionHealth.js +51 -63
- package/dist/execution/reviewMainRun.js +137 -0
- package/dist/execution/workItemExecution.js +28 -29
- package/dist/execution/workItemExecutionProjection.js +99 -107
- package/dist/execution/workItemMainRun.js +141 -0
- package/dist/executor/agentAdapter.js +227 -20
- package/dist/executor/agentConfigurationCatalog.js +126 -4
- package/dist/executor/agentConfigurationProbe.js +162 -4
- package/dist/executor/agentExecutor.js +79 -78
- package/dist/executor/effectiveLaunch.js +105 -18
- package/dist/executor/executorRegistry.js +29 -44
- package/dist/executor/fileRoleLaunchPlanner.js +229 -154
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +6 -3
- package/dist/input/inputRequest.js +12 -10
- package/dist/integration/gitIntegrationService.js +4 -11
- package/dist/integration/integrationQueueService.js +4 -4
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/kernel/builtinCapabilities.js +247 -12
- package/dist/kernel/capabilityRegistry.js +64 -18
- package/dist/kernel/instanceHost.js +12 -1
- package/dist/kernel/kernelPorts.js +2 -2
- package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
- package/dist/lifecycle/exactRunTerminalization.js +449 -0
- package/dist/message/message.js +62 -6
- package/dist/message/messageContinuation.js +204 -0
- package/dist/observability/executionAudit.js +70 -72
- package/dist/observability/faultClassification.js +2 -2
- package/dist/observability/orchestrationMetrics.js +8 -8
- package/dist/operator/operatorSessionHistory.js +1 -7
- package/dist/output/agentConfigurationPresentation.js +8 -3
- package/dist/output/agentRunConfigurationPresentation.js +128 -0
- package/dist/output/rolePresentation.js +54 -3
- package/dist/plugins/pluginChild.js +104 -0
- package/dist/plugins/pluginIntent.js +26 -0
- package/dist/plugins/pluginInterpreter.js +43 -0
- package/dist/plugins/pluginPackage.js +101 -0
- package/dist/plugins/pluginProcess.js +112 -0
- package/dist/plugins/pluginService.js +380 -0
- package/dist/profile/agentProfile.js +1 -1
- package/dist/repository/gitWorkspace.js +26 -4
- package/dist/repository/project.js +19 -4
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +20 -27
- package/dist/repository/taskWorkspacePreparer.js +344 -83
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +3 -3
- package/dist/resources/projectResource.js +123 -0
- package/dist/resources/projectResourceService.js +421 -0
- package/dist/resources/resourceDiscovery.js +6 -6
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/resourceRegistrar.js +1 -1
- package/dist/resources/resourceTypes.js +1 -1
- package/dist/review/deltaRecheck.js +3 -3
- package/dist/review/reviewAcceptance.js +16 -16
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewRound.js +21 -20
- package/dist/review/reviewerAvailability.js +2 -2
- package/dist/role/role.js +51 -7
- package/dist/role/taskRoleUpdate.js +30 -0
- package/dist/runtime/acpProtocol.js +425 -0
- package/dist/runtime/acpSession.js +731 -0
- package/dist/runtime/acpSessionConfiguration.js +260 -0
- package/dist/runtime/agentDriver.js +30 -11
- package/dist/runtime/agentEndpoint.js +278 -0
- package/dist/runtime/agentEndpointIdentity.js +86 -0
- package/dist/runtime/agentEndpointOwnership.js +239 -0
- package/dist/runtime/agentError.js +2 -10
- package/dist/runtime/agentHost.js +565 -314
- package/dist/runtime/agentRunConfiguration.js +258 -0
- package/dist/runtime/builtinAgentDrivers.js +134 -18
- package/dist/runtime/builtinAgentErrorMappers.js +55 -3
- package/dist/runtime/builtinTranscriptUsage.js +1 -1
- package/dist/runtime/claude-process-owner +0 -0
- package/dist/runtime/codexAppServerRuntime.js +38 -30
- package/dist/runtime/codexInteractiveHost.js +41 -6
- package/dist/runtime/continuationManager.js +2 -6
- package/dist/runtime/executionEnvironment.js +30 -0
- package/dist/runtime/firstProgressAdvisory.js +11 -11
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/jsonLineChannel.js +109 -0
- package/dist/runtime/launchBroker.js +91 -16
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +10 -18
- package/dist/runtime/managedCaller.js +61 -17
- package/dist/runtime/nativeSessionControl.js +102 -0
- package/dist/runtime/ports.js +6 -21
- package/dist/runtime/processExitObservation.js +8 -7
- package/dist/runtime/promptEnvelope.js +17 -6
- package/dist/runtime/providerContinuation.js +3 -9
- package/dist/runtime/providerContinuationReconciliationService.js +4 -13
- package/dist/runtime/providerControl.js +2 -7
- package/dist/runtime/providerRuntimeIdentity.js +110 -222
- package/dist/runtime/providerRuntimeReconciler.js +5 -9
- package/dist/runtime/runtimeBinding.js +0 -1
- package/dist/runtime/runtimeContinuationProjection.js +4 -7
- package/dist/runtime/runtimeDeadlines.js +9 -0
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +29 -65
- package/dist/runtime/runtimeProjection.js +43 -51
- package/dist/runtime/runtimeSessionCandidate.js +1 -3
- package/dist/runtime/sessionLaunchRequest.js +3 -7
- package/dist/runtime/sessionOwnerIdentity.js +7 -54
- package/dist/runtime/sessionOwnerRegistry.js +22 -17
- package/dist/runtime/sessionReconciliation.js +4 -8
- package/dist/runtime/sessionTerminationGuard.js +70 -259
- package/dist/runtime/sessionTokenMetrics.js +5 -16
- package/dist/runtime/structuredProviderHost.js +237 -117
- package/dist/runtime/taskRuntimeIsolation.js +39 -122
- package/dist/runtime/tmuxAdapters.js +39 -86
- package/dist/scheduler/activeRoleRunDelivery.js +354 -0
- package/dist/scheduler/leaderWakeupProcessor.js +75 -266
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +80 -9
- package/dist/scheduler/{roleTurnLiveness.js → roleRunLiveness.js} +26 -30
- package/dist/scheduler/{roleTurnStall.js → roleRunStall.js} +128 -139
- package/dist/scheduler/taskExecutionProjection.js +120 -124
- package/dist/scheduler/taskObservabilityProjection.js +29 -29
- package/dist/scheduler/taskWake.js +11 -4
- package/dist/scheduler/wakeReason.js +9 -1
- package/dist/setup/setupCommand.js +1 -0
- package/dist/storage/migrations/agentRunContract.js +159 -0
- package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
- package/dist/storage/sqliteSchema.js +431 -5
- package/dist/storage/sqliteStore.js +375 -220
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +10 -5
- package/dist/storage/taskStore.js +13 -11
- package/dist/storage/upgrade/upgradeOrchestrator.js +5 -7
- package/dist/surface/surfaceContributions.js +102 -0
- package/dist/task/completionReadiness.js +32 -6
- package/dist/task/deliveryGuard.js +16 -16
- package/dist/task/draftPlan.js +72 -12
- package/dist/task/nextAction.js +144 -128
- package/dist/task/remoteDelivery.js +6 -6
- package/dist/task/task.js +184 -18
- package/dist/task/taskActivation.js +301 -0
- package/dist/task/taskActivationService.js +392 -0
- package/dist/task/taskRecordReference.js +5 -4
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +55 -68
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +2 -2
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app.js +120 -20
- package/dist/web/assets/client/components.js +87 -54
- package/dist/web/assets/client/i18n.js +83 -41
- package/dist/web/assets/client/markdown.js +1 -1
- package/dist/web/assets/client/taskSurface.js +353 -0
- package/dist/web/assets/client/view.js +49 -44
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +22 -4
- package/dist/web/controllerWeb.js +60 -0
- package/dist/web/webMutation.js +28 -0
- package/dist/web/webServer.js +118 -8
- package/dist/web/webSnapshot.js +81 -74
- package/dist/web/webTaskSurface.js +64 -0
- package/dist/workItem/dependencyGate.js +1 -1
- package/dist/workItem/workItem.js +80 -48
- package/dist/workspace/workItemChangeSetManager.js +16 -9
- package/docs/agent-result-consumption.md +94 -0
- package/docs/agent-runtime-drivers.md +91 -0
- package/docs/architecture/README.md +38 -0
- package/docs/architecture/capabilities-and-resources.md +79 -0
- package/docs/managed-turn-and-session-runtime.md +222 -0
- package/docs/observability/README.md +81 -0
- package/docs/plugin-sdk.md +290 -0
- package/docs/provider-runtime.md +163 -0
- package/docs/release-workflow.md +303 -0
- package/docs/roles-and-configuration.md +113 -0
- package/docs/sqlite-control-plane-design.md +76 -0
- package/docs/task-dag-semantics.md +57 -0
- package/docs/task-delivery.md +103 -0
- package/docs/task-local-identity.md +6 -6
- package/docs/testing/verification-levels.md +86 -0
- package/i18n/README.zh-CN.md +98 -739
- package/package.json +2 -2
- package/skills/yui-leader/SKILL.md +130 -103
- package/skills/yui-leader/references/integration.md +39 -0
- package/skills/yui-leader/references/replicated-execution.md +42 -0
- package/skills/yui-leader/references/task-plugins.md +33 -0
- package/skills/yui-operator/SKILL.md +30 -59
- package/skills/yui-reviewer/SKILL.md +35 -36
- package/skills/yui-runtime/SKILL.md +88 -24
- package/skills/yui-runtime/references/publication.md +22 -0
- package/skills/yui-runtime/references/recovery.md +64 -0
- package/skills/yui-worker/SKILL.md +37 -39
- package/dist/cli/roleOptionCatalog.js +0 -68
- package/dist/context/sourceTurnContext.js +0 -30
- package/dist/execution/reviewMainTurn.js +0 -161
- package/dist/execution/workItemMainTurn.js +0 -164
- package/dist/lifecycle/exactTurnTerminalization.js +0 -407
- package/dist/runtime/preallocatedNativeSession.js +0 -13
- package/dist/runtime/runtimeStopReceipt.js +0 -42
- package/dist/scheduler/activeRoleTurnDelivery.js +0 -315
|
@@ -2,8 +2,8 @@ import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeout
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
5
|
-
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
6
|
-
import { hasRuntimeCleanupObligation,
|
|
5
|
+
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
6
|
+
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
7
7
|
import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
|
|
8
8
|
import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
9
9
|
import { roleSessionMayContinue, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
@@ -18,15 +18,20 @@ import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
|
|
|
18
18
|
import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
19
19
|
import { AgentHostPromptPushAdapter, FileTaskRuntimeIsolation, TmuxSessionHost, ProviderContinuationReconciliationService } from "../runtime/index.js";
|
|
20
20
|
import { startFileTaskController } from "./controller.js";
|
|
21
|
-
import { AgentHostProviderTurnFenceError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
|
|
21
|
+
import { AgentHostProviderTurnFenceError, AgentHostProviderSessionBusyError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
|
|
22
22
|
import { openSchedulerTelemetry } from "../telemetry/telemetryWiring.js";
|
|
23
23
|
import { createFileArtifactPort, createLinuxProcessPort, DurableJobSupervisor } from "./jobSupervisor.js";
|
|
24
24
|
import { authorizeJobStart } from "./jobControl.js";
|
|
25
25
|
import { createKernelPorts } from "../kernel/kernelPorts.js";
|
|
26
26
|
import { createCapabilityDispatcher } from "./capabilityBridge.js";
|
|
27
|
+
import { createControllerWeb } from "../web/controllerWeb.js";
|
|
28
|
+
import { createWebTaskSurface } from "../web/webTaskSurface.js";
|
|
29
|
+
import { SurfaceContributions } from "../surface/surfaceContributions.js";
|
|
30
|
+
import { TmuxWebTerminalService } from "../web/tmuxWebTerminal.js";
|
|
31
|
+
import { FileTaskWorkflowRuntime } from "./clientRuntime.js";
|
|
27
32
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
28
33
|
import { AgentRuntimeObserver } from "./agentRuntimeObserver.js";
|
|
29
|
-
import { AsyncRuntimeEventProcessor, FileRuntimeEventProcessor, createAsyncRuntimeObserver
|
|
34
|
+
import { AsyncRuntimeEventProcessor, FileRuntimeEventProcessor, createAsyncRuntimeObserver } from "./runtimeEventProcessor.js";
|
|
30
35
|
import { RuntimeLaunchCoordinator } from "./runtimeLaunchCoordinator.js";
|
|
31
36
|
import { ephemeralDomainFromEnvironment, recordEphemeralTmuxTarget } from "./domainIdentity.js";
|
|
32
37
|
import { createEphemeralResourceReaper } from "./ephemeralResourceReaper.js";
|
|
@@ -42,18 +47,22 @@ import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExit
|
|
|
42
47
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
43
48
|
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
44
49
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
50
|
+
import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
51
|
+
import { taskOwnsManagedWorkspace } from "../task/task.js";
|
|
45
52
|
/** Production composition root for the current SQLite TaskStore + tmux Controller. */
|
|
46
53
|
export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
47
54
|
// The current Home always uses SQLite. The persistence worker is enabled by
|
|
48
55
|
// default; YUI_STORE_WORKER=0/false keeps the same database in-process.
|
|
49
56
|
const useWorker = resolveStoreWorkerEnabledForHome(home, options.environment ?? process.env);
|
|
50
|
-
const
|
|
51
|
-
|
|
57
|
+
const ownedStore = options.store === undefined
|
|
58
|
+
? (useWorker
|
|
52
59
|
// The worker owns the event-processing hot path while the scheduler uses
|
|
53
60
|
// the synchronous connection. Both point at the same WAL database and
|
|
54
61
|
// serialize via BEGIN IMMEDIATE + busy_timeout.
|
|
55
62
|
? new SqliteTaskStore(home)
|
|
56
|
-
: openCurrentTaskStore(home))
|
|
63
|
+
: openCurrentTaskStore(home))
|
|
64
|
+
: undefined;
|
|
65
|
+
const store = options.store ?? ownedStore;
|
|
57
66
|
const homeId = store.getHomeIdentity().homeId;
|
|
58
67
|
const durableConfig = store.getConfig();
|
|
59
68
|
// When the worker backend is active, the db-touching observer folds run in
|
|
@@ -70,445 +79,470 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
70
79
|
const inventoryClient = useWorker
|
|
71
80
|
? new ResourceInventoryClient()
|
|
72
81
|
: undefined;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
let closeKernel = async () => { };
|
|
83
|
+
let closeWeb = async () => { };
|
|
84
|
+
try {
|
|
85
|
+
const schedulerStore = options.schedulerStore
|
|
86
|
+
?? new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home, store.getConfig()));
|
|
87
|
+
const domainIdentity = options.domainIdentity
|
|
88
|
+
?? ephemeralDomainFromEnvironment(options.environment ?? process.env);
|
|
89
|
+
const planner = options.planner ?? new FileRoleLaunchPlanner(home, store, {
|
|
90
|
+
environment: options.environment
|
|
91
|
+
});
|
|
92
|
+
const tmux = options.tmux ?? new TmuxManager(resolveTmuxBin(durableConfig.tmuxBin), new NodeCommandExecutor(), {
|
|
93
|
+
yuiHome: home,
|
|
94
|
+
historyLimit: resolveTmuxHistoryLimit(durableConfig.tmuxHistoryLimit),
|
|
95
|
+
...(domainIdentity === undefined
|
|
96
|
+
? {}
|
|
97
|
+
: {
|
|
98
|
+
onRoleTargetRecorded: (target) => {
|
|
99
|
+
if (!recordEphemeralTmuxTarget(home, domainIdentity.token, target)) {
|
|
100
|
+
throw new Error(`Ephemeral tmux target fence could not be recorded: ${target}.`);
|
|
101
|
+
}
|
|
89
102
|
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
-
// The runtime inbox is also the low-latency discovery source during a
|
|
101
|
-
// scheduler-owned launch. Waiting only for the post-pass durable projection
|
|
102
|
-
// would deadlock behind the same pass that is currently starting the host.
|
|
103
|
-
const runtimeEventInbox = new FileRuntimeEventInbox(home);
|
|
104
|
-
const catalogs = options.catalogs
|
|
105
|
-
?? new AgentConfigurationCatalogService(home, {
|
|
106
|
-
environment: options.environment ?? process.env
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
const sessionOwners = new SessionOwnerReconciliation({
|
|
106
|
+
home,
|
|
107
|
+
store,
|
|
108
|
+
environment: options.environment,
|
|
109
|
+
tmux,
|
|
110
|
+
nativeConnection: (taskId, roleName) => planner.planNativeControl(taskId, roleName),
|
|
111
|
+
onWarning: options.onError
|
|
107
112
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
cwd: request.workspace,
|
|
116
|
-
config: effectiveLaunchConfig(request.effective)
|
|
113
|
+
// The runtime inbox is also the low-latency discovery source during a
|
|
114
|
+
// scheduler-owned launch. Waiting only for the post-pass durable projection
|
|
115
|
+
// would deadlock behind the same pass that is currently starting the host.
|
|
116
|
+
const runtimeEventInbox = new FileRuntimeEventInbox(home);
|
|
117
|
+
const catalogs = options.catalogs
|
|
118
|
+
?? new AgentConfigurationCatalogService(home, {
|
|
119
|
+
environment: options.environment ?? process.env
|
|
117
120
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
121
|
+
const sessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
|
|
122
|
+
validateLaunch: async (request) => {
|
|
123
|
+
const agent = store.getConfiguredAgent(request.agentId);
|
|
124
|
+
if (agent === null)
|
|
125
|
+
return;
|
|
126
|
+
const resolved = await catalogs.resolve({
|
|
127
|
+
agent,
|
|
128
|
+
cwd: request.workspace,
|
|
129
|
+
config: effectiveLaunchConfig(request.effective)
|
|
130
|
+
});
|
|
131
|
+
validateAgentLaunchConfiguration(resolved.catalog, effectiveLaunchConfig(request.effective));
|
|
132
|
+
},
|
|
133
|
+
waitForNativeSession: async (request, signal) => {
|
|
134
|
+
const owner = request.owner;
|
|
135
|
+
if (owner.scope !== "task") {
|
|
136
|
+
throw new Error("Native session discovery requires a Task runtime owner.");
|
|
132
137
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
&&
|
|
137
|
-
&&
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
&& event.observation.
|
|
142
|
-
|
|
138
|
+
while (!signal.aborted) {
|
|
139
|
+
const session = schedulerStore.getRoleSession(owner.taskId, owner.roleName, request.agentId);
|
|
140
|
+
if (session !== null && session.status === "active"
|
|
141
|
+
&& session.adapterId === request.adapterId
|
|
142
|
+
&& typeof session.nativeSessionId === "string" && session.nativeSessionId.trim().length > 0) {
|
|
143
|
+
return session.nativeSessionId;
|
|
144
|
+
}
|
|
145
|
+
for (const event of runtimeEventInbox.list()) {
|
|
146
|
+
if (event.type === "runtime-observation" && event.observation.kind === "session.started"
|
|
147
|
+
&& event.observation.fence.taskId === owner.taskId
|
|
148
|
+
&& event.observation.fence.roleName === owner.roleName
|
|
149
|
+
&& event.observation.fence.agentId === request.agentId
|
|
150
|
+
&& event.observation.fence.runId === request.runId
|
|
151
|
+
&& typeof event.observation.fence.nativeSessionId === "string"
|
|
152
|
+
&& event.observation.fence.nativeSessionId.trim().length > 0) {
|
|
153
|
+
return event.observation.fence.nativeSessionId;
|
|
154
|
+
}
|
|
143
155
|
}
|
|
156
|
+
await abortableDelay(50, signal);
|
|
144
157
|
}
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
: { nativeSessionId: binding.nativeSessionId }),
|
|
159
|
-
...(pane.pid === undefined ? {} : { panePid: pane.pid })
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
const promptPush = options.promptPush
|
|
164
|
-
?? new AgentHostPromptPushAdapter(home);
|
|
165
|
-
const runtimeIsolation = options.runtimeIsolation
|
|
166
|
-
?? new FileTaskRuntimeIsolation({
|
|
167
|
-
// A sibling of the exact control Home keeps provider data/cache/tmp out
|
|
168
|
-
// of both the shared control plane and every managed Git workspace.
|
|
169
|
-
runtimeRoot: `${resolve(home)}.task-runtimes`,
|
|
170
|
-
controlPlane: {
|
|
171
|
-
yuiHome: home,
|
|
172
|
-
controllerSocketPath: controllerSocketPath(homeId),
|
|
173
|
-
tmuxNamespace: yuiTmuxServerName(home),
|
|
174
|
-
globalInstallPaths: [process.execPath]
|
|
158
|
+
throw new Error("Native session discovery was aborted.");
|
|
159
|
+
},
|
|
160
|
+
inactivityTimeoutMs: resolveAgentLaunchInactivityTimeoutSeconds(durableConfig.agentLaunchInactivityTimeoutSeconds) * 1_000,
|
|
161
|
+
onHostCreated: ({ binding, pane }) => {
|
|
162
|
+
sessionOwners.recordHostOwner({
|
|
163
|
+
owner: binding.owner,
|
|
164
|
+
agentId: binding.agentId,
|
|
165
|
+
adapterId: binding.adapterId,
|
|
166
|
+
...(binding.nativeSessionId === undefined
|
|
167
|
+
? {}
|
|
168
|
+
: { nativeSessionId: binding.nativeSessionId }),
|
|
169
|
+
...(pane.pid === undefined ? {} : { panePid: pane.pid })
|
|
170
|
+
});
|
|
175
171
|
}
|
|
176
172
|
});
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (result.outcome === "stop-blocked") {
|
|
190
|
-
(options.onError ?? (() => undefined))(new Error(`Role runtime cleanup could not prove physical exit: ${result.remaining
|
|
191
|
-
.map(({ record, detail }) => `${record.runtimeGenerationId}: ${detail}`)
|
|
192
|
-
.join("; ")}`));
|
|
173
|
+
const promptPush = options.promptPush
|
|
174
|
+
?? new AgentHostPromptPushAdapter(home);
|
|
175
|
+
const runtimeIsolation = options.runtimeIsolation
|
|
176
|
+
?? new FileTaskRuntimeIsolation({
|
|
177
|
+
// A sibling of the exact control Home keeps provider data/cache/tmp out
|
|
178
|
+
// of both the shared control plane and every managed Git workspace.
|
|
179
|
+
runtimeRoot: `${resolve(home)}.task-runtimes`,
|
|
180
|
+
controlPlane: {
|
|
181
|
+
yuiHome: home,
|
|
182
|
+
controllerSocketPath: controllerSocketPath(homeId),
|
|
183
|
+
tmuxNamespace: yuiTmuxServerName(home),
|
|
184
|
+
globalInstallPaths: [process.execPath]
|
|
193
185
|
}
|
|
194
|
-
return result.outcome === "stop-confirmed";
|
|
195
186
|
});
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
: {
|
|
200
|
-
cleanupTaskLaunch: (input) => runtimeIsolation.cleanupTaskLaunch(input)
|
|
201
|
-
})
|
|
202
|
-
};
|
|
203
|
-
const resourceActivity = createRuntimeResourceActivityTracker();
|
|
204
|
-
let runningRuntime;
|
|
205
|
-
let runningController;
|
|
206
|
-
const signalRuntimeCleanup = (target) => {
|
|
207
|
-
runningRuntime?.signal(runtimeLifecycleSignalKey(target.kind === "role-runtime"
|
|
208
|
-
? {
|
|
209
|
-
scope: "task",
|
|
210
|
-
taskId: target.taskId,
|
|
211
|
-
roleName: target.roleName
|
|
212
|
-
}
|
|
213
|
-
: { scope: "global", roleName: target.roleName }));
|
|
214
|
-
};
|
|
215
|
-
const launchCoordinator = new RuntimeLaunchCoordinator(schedulerStore, sessionHost, {
|
|
216
|
-
...(options.now === undefined ? {} : { now: options.now }),
|
|
217
|
-
assertCurrent: (request) => {
|
|
218
|
-
assertRuntimeLaunchRequestCurrent(store, request);
|
|
219
|
-
},
|
|
220
|
-
onCleanupRequired: signalRuntimeCleanup,
|
|
221
|
-
runtimeIsolation
|
|
222
|
-
});
|
|
223
|
-
// One inventory scan per scheduler pass. When the inventory worker is active
|
|
224
|
-
// the blocking /proc scan runs there; otherwise it runs on the main thread.
|
|
225
|
-
const scanInventory = (panes) => inventoryClient !== undefined
|
|
226
|
-
? inventoryClient.scan({
|
|
227
|
-
currentHome: home,
|
|
228
|
-
scope: "current",
|
|
229
|
-
panes,
|
|
230
|
-
...(options.environment === undefined
|
|
231
|
-
? {}
|
|
232
|
-
: { environment: options.environment })
|
|
233
|
-
})
|
|
234
|
-
: scanControllerResourceInventory({
|
|
235
|
-
currentHome: home,
|
|
236
|
-
scope: "current",
|
|
237
|
-
panes,
|
|
238
|
-
tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin),
|
|
239
|
-
...(options.environment === undefined
|
|
187
|
+
const lifecycleHost = {
|
|
188
|
+
inspectOwner: (owner) => (sessionHost.inspectOwner(owner)),
|
|
189
|
+
...(sessionHost.inspectOwners === undefined
|
|
240
190
|
? {}
|
|
241
|
-
: {
|
|
191
|
+
: {
|
|
192
|
+
inspectOwners: (owners) => sessionHost.inspectOwners(owners)
|
|
193
|
+
}),
|
|
194
|
+
stopOwner: (owner) => {
|
|
195
|
+
// Issue 03: the durable `stopped` transition is gated on physical
|
|
196
|
+
// exit proof. A blocked result keeps the Session non-terminal and
|
|
197
|
+
// preserves owner records for Operator recovery.
|
|
198
|
+
return sessionOwners.terminateOwner(owner).then((result) => {
|
|
199
|
+
if (result.outcome === "stop-blocked") {
|
|
200
|
+
(options.onError ?? (() => undefined))(new Error(`Role runtime cleanup could not prove physical exit: ${result.remaining
|
|
201
|
+
.map(({ record, detail }) => `PID ${record.providerRoot.pid}: ${detail}`)
|
|
202
|
+
.join("; ")}`));
|
|
203
|
+
}
|
|
204
|
+
return result.outcome === "stop-confirmed";
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
const resourceActivity = createRuntimeResourceActivityTracker();
|
|
209
|
+
let runningRuntime;
|
|
210
|
+
let runningController;
|
|
211
|
+
const launchCoordinator = new RuntimeLaunchCoordinator(schedulerStore, sessionHost, {
|
|
212
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
213
|
+
assertCurrent: (request) => {
|
|
214
|
+
assertRuntimeLaunchRequestCurrent(store, request);
|
|
215
|
+
},
|
|
216
|
+
runtimeIsolation
|
|
242
217
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const active = resource.state === "running" || resource.state === "current";
|
|
256
|
-
const input = inputs.find((candidate) => (candidate.taskId === owner.taskId
|
|
257
|
-
&& candidate.roleName === owner.roleName));
|
|
258
|
-
if (input === undefined)
|
|
259
|
-
return [];
|
|
260
|
-
const identity = owner.turnId === undefined
|
|
261
|
-
|| owner.adapterId === undefined
|
|
262
|
-
|| (owner.nativeSessionId === undefined && owner.runtimeGenerationId === undefined)
|
|
263
|
-
|| (owner.nativeSessionId !== undefined && owner.nativeSessionId.trim().length === 0)
|
|
264
|
-
|| (owner.runtimeGenerationId !== undefined && owner.runtimeGenerationId.trim().length === 0)
|
|
265
|
-
? undefined
|
|
266
|
-
: {
|
|
267
|
-
taskId: owner.taskId,
|
|
268
|
-
roleName: owner.roleName,
|
|
269
|
-
turnId: owner.turnId,
|
|
270
|
-
agentId: owner.agentId,
|
|
271
|
-
adapterId: owner.adapterId,
|
|
272
|
-
...(owner.nativeSessionId === undefined
|
|
273
|
-
? {}
|
|
274
|
-
: { nativeSessionId: owner.nativeSessionId }),
|
|
275
|
-
...(owner.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: owner.runtimeGenerationId })
|
|
276
|
-
};
|
|
277
|
-
const changed = !active || input.turnId === undefined
|
|
278
|
-
? false
|
|
279
|
-
: resourceActivity({
|
|
280
|
-
taskId: input.taskId,
|
|
281
|
-
roleName: input.roleName,
|
|
282
|
-
turnId: input.turnId,
|
|
283
|
-
agentId: input.agentId,
|
|
284
|
-
adapterId: input.adapterId,
|
|
285
|
-
...(input.nativeSessionId === undefined
|
|
286
|
-
? {}
|
|
287
|
-
: { nativeSessionId: input.nativeSessionId }),
|
|
288
|
-
...(input.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: input.runtimeGenerationId })
|
|
289
|
-
}, resource);
|
|
290
|
-
return [{
|
|
291
|
-
taskId: owner.taskId,
|
|
292
|
-
roleName: owner.roleName,
|
|
293
|
-
resource: {
|
|
294
|
-
observedAt: inventory.observedAt,
|
|
295
|
-
active,
|
|
296
|
-
changed: active && changed,
|
|
297
|
-
...(identity === undefined ? {} : { identity }),
|
|
298
|
-
...(input.progressAt === undefined ? {} : { progressAt: input.progressAt }),
|
|
299
|
-
cpuTimeMs: resource.cpuTimeMs,
|
|
300
|
-
...(resource.ioReadBytes === undefined
|
|
301
|
-
? {}
|
|
302
|
-
: { ioReadBytes: resource.ioReadBytes }),
|
|
303
|
-
...(resource.ioWriteBytes === undefined
|
|
304
|
-
? {}
|
|
305
|
-
: { ioWriteBytes: resource.ioWriteBytes }),
|
|
306
|
-
rssBytes: resource.rssBytes
|
|
307
|
-
}
|
|
308
|
-
}];
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
const workspacePreparer = options.workspacePreparer
|
|
313
|
-
?? new FileTaskWorkspacePreparer(home, store);
|
|
314
|
-
const resourceReaper = options.resourceReaper
|
|
315
|
-
?? (domainIdentity === undefined
|
|
316
|
-
? undefined
|
|
317
|
-
: createEphemeralResourceReaper({
|
|
218
|
+
// One inventory scan per scheduler pass. When the inventory worker is active
|
|
219
|
+
// the blocking /proc scan runs there; otherwise it runs on the main thread.
|
|
220
|
+
const scanInventory = (panes) => inventoryClient !== undefined
|
|
221
|
+
? inventoryClient.scan({
|
|
222
|
+
currentHome: home,
|
|
223
|
+
scope: "current",
|
|
224
|
+
panes,
|
|
225
|
+
...(options.environment === undefined
|
|
226
|
+
? {}
|
|
227
|
+
: { environment: options.environment })
|
|
228
|
+
})
|
|
229
|
+
: scanControllerResourceInventory({
|
|
318
230
|
currentHome: home,
|
|
319
|
-
// The detached Controller owns one YUI_HOME. Keep automatic
|
|
320
|
-
// recovery bounded to that domain; cross-home cleanup remains an
|
|
321
|
-
// explicit `controller cleanup --all` inventory operation.
|
|
322
231
|
scope: "current",
|
|
323
|
-
|
|
232
|
+
panes,
|
|
324
233
|
tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin),
|
|
325
|
-
|
|
326
|
-
// inventory worker too (same cadence, same inventory shape).
|
|
327
|
-
...(inventoryClient === undefined
|
|
234
|
+
...(options.environment === undefined
|
|
328
235
|
? {}
|
|
329
|
-
: {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
236
|
+
: { environment: options.environment })
|
|
237
|
+
});
|
|
238
|
+
const delivery = options.delivery ?? new ExecutorRegistry(planner, tmux, agentProcessReadinessProbe, {
|
|
239
|
+
sessionHost,
|
|
240
|
+
promptPush,
|
|
241
|
+
launchCoordinator,
|
|
242
|
+
roleResourceInventory: async (panes, inputs) => {
|
|
243
|
+
const inventory = await scanInventory(panes);
|
|
244
|
+
return inventory.resources.flatMap((resource) => {
|
|
245
|
+
if (resource.kind !== "agent-session")
|
|
246
|
+
return [];
|
|
247
|
+
const owner = resource.owner;
|
|
248
|
+
if (owner.kind !== "task-role")
|
|
249
|
+
return [];
|
|
250
|
+
const active = resource.state === "running" || resource.state === "current";
|
|
251
|
+
const input = inputs.find((candidate) => (candidate.taskId === owner.taskId
|
|
252
|
+
&& candidate.roleName === owner.roleName));
|
|
253
|
+
if (input === undefined)
|
|
254
|
+
return [];
|
|
255
|
+
const identity = owner.runId === undefined || owner.adapterId === undefined || (owner.nativeSessionId === undefined) || (owner.nativeSessionId !== undefined && owner.nativeSessionId.trim().length === 0)
|
|
256
|
+
? undefined
|
|
257
|
+
: {
|
|
258
|
+
taskId: owner.taskId,
|
|
259
|
+
roleName: owner.roleName,
|
|
260
|
+
runId: owner.runId,
|
|
261
|
+
agentId: owner.agentId,
|
|
262
|
+
adapterId: owner.adapterId,
|
|
263
|
+
...(owner.nativeSessionId === undefined
|
|
264
|
+
? {}
|
|
265
|
+
: { nativeSessionId: owner.nativeSessionId }),
|
|
266
|
+
};
|
|
267
|
+
const changed = !active || input.runId === undefined
|
|
268
|
+
? false
|
|
269
|
+
: resourceActivity({
|
|
270
|
+
taskId: input.taskId,
|
|
271
|
+
roleName: input.roleName,
|
|
272
|
+
runId: input.runId,
|
|
273
|
+
agentId: input.agentId,
|
|
274
|
+
adapterId: input.adapterId,
|
|
275
|
+
...(input.nativeSessionId === undefined
|
|
334
276
|
? {}
|
|
335
|
-
: {
|
|
277
|
+
: { nativeSessionId: input.nativeSessionId }),
|
|
278
|
+
}, resource);
|
|
279
|
+
return [{
|
|
280
|
+
taskId: owner.taskId,
|
|
281
|
+
roleName: owner.roleName,
|
|
282
|
+
resource: {
|
|
283
|
+
observedAt: inventory.observedAt,
|
|
284
|
+
active,
|
|
285
|
+
changed: active && changed,
|
|
286
|
+
...(identity === undefined ? {} : { identity }),
|
|
287
|
+
...(input.progressAt === undefined ? {} : { progressAt: input.progressAt }),
|
|
288
|
+
cpuTimeMs: resource.cpuTimeMs,
|
|
289
|
+
...(resource.ioReadBytes === undefined
|
|
290
|
+
? {}
|
|
291
|
+
: { ioReadBytes: resource.ioReadBytes }),
|
|
292
|
+
...(resource.ioWriteBytes === undefined
|
|
293
|
+
? {}
|
|
294
|
+
: { ioWriteBytes: resource.ioWriteBytes }),
|
|
295
|
+
rssBytes: resource.rssBytes
|
|
296
|
+
}
|
|
297
|
+
}];
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
const workspacePreparer = options.workspacePreparer
|
|
302
|
+
?? new FileTaskWorkspacePreparer(home, store);
|
|
303
|
+
const resourceReaper = options.resourceReaper
|
|
304
|
+
?? (domainIdentity === undefined
|
|
305
|
+
? undefined
|
|
306
|
+
: createEphemeralResourceReaper({
|
|
307
|
+
currentHome: home,
|
|
308
|
+
// The detached Controller owns one YUI_HOME. Keep automatic
|
|
309
|
+
// recovery bounded to that domain; cross-home cleanup remains an
|
|
310
|
+
// explicit `controller cleanup --all` inventory operation.
|
|
311
|
+
scope: "current",
|
|
312
|
+
environment: options.environment,
|
|
313
|
+
tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin),
|
|
314
|
+
// When the worker backend is active, the reaper's scan runs in the
|
|
315
|
+
// inventory worker too (same cadence, same inventory shape).
|
|
316
|
+
...(inventoryClient === undefined
|
|
317
|
+
? {}
|
|
318
|
+
: {
|
|
319
|
+
scan: () => inventoryClient.scan({
|
|
320
|
+
currentHome: home,
|
|
321
|
+
scope: "current",
|
|
322
|
+
...(options.environment === undefined
|
|
323
|
+
? {}
|
|
324
|
+
: { environment: options.environment })
|
|
325
|
+
})
|
|
336
326
|
})
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
327
|
+
}));
|
|
328
|
+
// Issue 10: automatic Resource GC. The runner self-skips unless
|
|
329
|
+
// resourcesGcMode=quarantine and resourcesGcAutoQuarantine=true, so wiring
|
|
330
|
+
// it unconditionally costs one config read per full pass when disabled.
|
|
331
|
+
const resourceAutoGc = options.resourceAutoGc
|
|
332
|
+
?? createResourceAutoGc({
|
|
333
|
+
home,
|
|
334
|
+
store,
|
|
335
|
+
environment: options.environment
|
|
336
|
+
});
|
|
337
|
+
const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, launchCoordinator, planner);
|
|
338
|
+
// Process-exit observations are persisted by the Agent Host before socket
|
|
339
|
+
// delivery. Drain them while this Controller is still the only storage
|
|
340
|
+
// writer and before it begins accepting new work after a handover.
|
|
341
|
+
await replayRuntimeProcessExitOutbox(home, async (observation) => {
|
|
342
|
+
await lifecycleDispatcher("runtime.process-exit-observe", observation);
|
|
347
343
|
});
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
344
|
+
// f7/rr5: This same inbox feeds the supervisor's terminal channel and the
|
|
345
|
+
// runtime event processor. When a Job reaches a terminal state, the
|
|
346
|
+
// supervisor enqueues a durable-job-terminal event; the processor drains it
|
|
347
|
+
// on the next pass, waking the Controller immediately instead of waiting for
|
|
348
|
+
// the poll interval.
|
|
349
|
+
const kernel = createKernelPorts(store, createLinuxProcessPort(), (taskId) => {
|
|
350
|
+
runningRuntime?.signal(`task:${taskId}`);
|
|
351
|
+
});
|
|
352
|
+
closeKernel = () => kernel.close();
|
|
353
|
+
const webWorkflow = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
|
|
354
|
+
environment: options.environment ?? process.env, onError: options.onError
|
|
355
|
+
});
|
|
356
|
+
const webSurface = createWebTaskSurface(store, { runtime: {
|
|
357
|
+
notifyStateChanged: (taskId) => runningRuntime?.signal(`task:${taskId}`),
|
|
358
|
+
notifyMailboxChanged: (target) => {
|
|
359
|
+
if (target.kind === "role")
|
|
360
|
+
runningRuntime?.signal(`role:${target.taskId}/${target.roleName}`);
|
|
361
|
+
else if (target.kind === "task")
|
|
362
|
+
runningRuntime?.signal(`task:${target.taskId}`);
|
|
363
|
+
},
|
|
364
|
+
reconcileTask: (taskId) => runningRuntime?.signal(`task:${taskId}`)
|
|
365
|
+
}, yuiHome: home });
|
|
366
|
+
const surfaces = new SurfaceContributions(kernel.capabilities.registry);
|
|
367
|
+
const web = createControllerWeb(store, {
|
|
368
|
+
surface: webSurface,
|
|
369
|
+
answerInput: async ({ taskId, inputId, answer }) => webSurface.answer(taskId, inputId, answer),
|
|
370
|
+
panels: {
|
|
371
|
+
list: (taskId) => surfaces.listPanels(kernel.capabilities.authenticateWebQuery(taskId)),
|
|
372
|
+
read: (taskId, ref, input) => surfaces.readPanel(kernel.capabilities.authenticateWebQuery(taskId), ref, input)
|
|
373
|
+
},
|
|
374
|
+
terminal: new TmuxWebTerminalService({
|
|
375
|
+
yuiHome: home, tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin), tmux,
|
|
376
|
+
prepareGlobalRole: (roleName) => webWorkflow.prepareGlobalRoleEnter(roleName),
|
|
377
|
+
environment: options.environment ?? process.env, onError: options.onError
|
|
378
|
+
})
|
|
379
|
+
});
|
|
380
|
+
closeWeb = () => web.close();
|
|
381
|
+
const jobSupervisor = new DurableJobSupervisor({
|
|
382
|
+
store: schedulerStore,
|
|
383
|
+
process: kernel.runner,
|
|
384
|
+
artifacts: createFileArtifactPort(home),
|
|
385
|
+
authorizeStart: (job) => authorizeJobStart(store, job),
|
|
386
|
+
// rr6/f1: Bounded supervision wake. The supervisor signals the Controller
|
|
387
|
+
// after spawning a runner (queued→running adoption) and when a runner
|
|
388
|
+
// exits (terminal harvest), so a quick job converges without waiting for
|
|
389
|
+
// the recovery interval. Closes over runningRuntime, which is assigned
|
|
390
|
+
// once startFileTaskController resolves; a wake during shutdown is a
|
|
391
|
+
// no-op. The recovery interval stays the cross-restart fallback.
|
|
392
|
+
wake: (taskId) => {
|
|
384
393
|
try {
|
|
385
|
-
|
|
386
|
-
scope: "task",
|
|
387
|
-
taskId: notice.taskId,
|
|
388
|
-
jobId: notice.jobId,
|
|
389
|
-
status: notice.status,
|
|
390
|
-
outcome: notice.outcome
|
|
391
|
-
});
|
|
394
|
+
runningRuntime?.signal(`task:${taskId}`);
|
|
392
395
|
}
|
|
393
|
-
catch
|
|
394
|
-
//
|
|
395
|
-
// committed. A delivery failure must not fail the reconcile pass.
|
|
396
|
-
(options.onError ?? (() => undefined))(error);
|
|
396
|
+
catch {
|
|
397
|
+
// Controller stopped; the recovery interval remains the fallback.
|
|
397
398
|
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
deliveryTimeoutMs: options.deliveryTimeoutMs
|
|
415
|
-
?? resolveDeliveryTimeoutSeconds(durableConfig.deliveryTimeoutSeconds) * 1_000,
|
|
416
|
-
stallWindowMs: options.stallWindowMs
|
|
417
|
-
?? resolveRuntimeHealth(durableConfig.runtimeHealth).stallWindowMs,
|
|
418
|
-
diagnosticAfterMs: options.diagnosticAfterMs
|
|
419
|
-
?? resolveRuntimeHealth(durableConfig.runtimeHealth).diagnosticAfterMs,
|
|
420
|
-
now: options.now,
|
|
421
|
-
onError: options.onError,
|
|
422
|
-
lifecycleHost,
|
|
423
|
-
jobSupervisor,
|
|
424
|
-
jobControl,
|
|
425
|
-
capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
|
|
426
|
-
...(continuationReconciler === undefined ? {} : { continuationReconciler }),
|
|
427
|
-
...(resourceReaper === undefined ? {} : { resourceReaper }),
|
|
428
|
-
resourceAutoGc,
|
|
429
|
-
onExpiredEphemeralDomain: (domain) => {
|
|
430
|
-
if (domain.yuiHome !== home)
|
|
431
|
-
return;
|
|
432
|
-
void runningController?.close().catch(options.onError ?? (() => undefined));
|
|
433
|
-
},
|
|
434
|
-
workspacePreparer,
|
|
435
|
-
runtimeEventProcessor: options.runtimeEventProcessor
|
|
436
|
-
?? (useWorker && asyncStoreClient !== undefined
|
|
437
|
-
? new AsyncRuntimeEventProcessor(runtimeEventInbox, createAsyncRuntimeObserver((method, args) => asyncStoreClient.invokeObserver(method, args)))
|
|
438
|
-
: new FileRuntimeEventProcessor(runtimeEventInbox, schedulerStore)),
|
|
439
|
-
runtimeObserver: options.runtimeObserver
|
|
440
|
-
?? new AgentRuntimeObserver(store, runtimeEventInbox),
|
|
441
|
-
domainIdentity,
|
|
442
|
-
...(options.configuration !== undefined
|
|
443
|
-
? { configuration: options.configuration }
|
|
444
|
-
: options.intervalMs === undefined
|
|
445
|
-
? {
|
|
446
|
-
configuration: {
|
|
447
|
-
reconciliationIntervalMs: () => reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds)
|
|
399
|
+
},
|
|
400
|
+
terminalEvents: {
|
|
401
|
+
deliverTerminalEvent(notice) {
|
|
402
|
+
try {
|
|
403
|
+
runtimeEventInbox.enqueueDurableJobTerminal({
|
|
404
|
+
scope: "task",
|
|
405
|
+
taskId: notice.taskId,
|
|
406
|
+
jobId: notice.jobId,
|
|
407
|
+
status: notice.status,
|
|
408
|
+
outcome: notice.outcome
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
// Best-effort terminal channel: the terminal transition already
|
|
413
|
+
// committed. A delivery failure must not fail the reconcile pass.
|
|
414
|
+
(options.onError ?? (() => undefined))(error);
|
|
448
415
|
}
|
|
449
416
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
417
|
+
},
|
|
418
|
+
onError: options.onError
|
|
419
|
+
});
|
|
420
|
+
const jobControl = kernel.jobs;
|
|
421
|
+
const continuationReconciler = options.continuationMetadata === undefined
|
|
422
|
+
? undefined
|
|
423
|
+
: new ProviderContinuationReconciliationService(store, schedulerStore, options.continuationMetadata);
|
|
424
|
+
const running = await startFileTaskController(home, schedulerStore, delivery, (method, params) => method === "web.start" || method === "web.stop" || method === "web.status"
|
|
425
|
+
? web.dispatch(method, params) : lifecycleDispatcher(method, params), {
|
|
426
|
+
intervalMs: options.intervalMs
|
|
427
|
+
?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
|
|
428
|
+
signalWindowMs: options.signalWindowMs,
|
|
429
|
+
taskConcurrency: options.taskConcurrency
|
|
430
|
+
?? resolveControllerTaskConcurrency(durableConfig.controllerTaskConcurrency),
|
|
431
|
+
deliveryRetryMs: options.deliveryRetryMs,
|
|
432
|
+
deliveryRetryLimit: options.deliveryRetryLimit,
|
|
433
|
+
deliveryTimeoutMs: options.deliveryTimeoutMs
|
|
434
|
+
?? resolveDeliveryTimeoutSeconds(durableConfig.deliveryTimeoutSeconds) * 1_000,
|
|
435
|
+
stallWindowMs: options.stallWindowMs
|
|
436
|
+
?? resolveRuntimeHealth(durableConfig.runtimeHealth).stallWindowMs,
|
|
437
|
+
diagnosticAfterMs: options.diagnosticAfterMs
|
|
438
|
+
?? resolveRuntimeHealth(durableConfig.runtimeHealth).diagnosticAfterMs,
|
|
439
|
+
now: options.now,
|
|
440
|
+
onError: options.onError,
|
|
441
|
+
lifecycleHost,
|
|
442
|
+
jobSupervisor,
|
|
443
|
+
jobControl,
|
|
444
|
+
capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
|
|
445
|
+
...(continuationReconciler === undefined ? {} : { continuationReconciler }),
|
|
446
|
+
...(resourceReaper === undefined ? {} : { resourceReaper }),
|
|
447
|
+
resourceAutoGc,
|
|
448
|
+
onExpiredEphemeralDomain: (domain) => {
|
|
449
|
+
if (domain.yuiHome !== home)
|
|
450
|
+
return;
|
|
451
|
+
void runningController?.close().catch(options.onError ?? (() => undefined));
|
|
452
|
+
},
|
|
453
|
+
workspacePreparer,
|
|
454
|
+
runtimeEventProcessor: options.runtimeEventProcessor
|
|
455
|
+
?? (useWorker && asyncStoreClient !== undefined
|
|
456
|
+
? new AsyncRuntimeEventProcessor(runtimeEventInbox, createAsyncRuntimeObserver((method, args) => asyncStoreClient.invokeObserver(method, args)))
|
|
457
|
+
: new FileRuntimeEventProcessor(runtimeEventInbox, schedulerStore)),
|
|
458
|
+
runtimeObserver: options.runtimeObserver
|
|
459
|
+
?? new AgentRuntimeObserver(store, runtimeEventInbox),
|
|
460
|
+
domainIdentity,
|
|
461
|
+
...(options.configuration !== undefined
|
|
462
|
+
? { configuration: options.configuration }
|
|
463
|
+
: options.intervalMs === undefined
|
|
464
|
+
? {
|
|
465
|
+
configuration: {
|
|
466
|
+
reconciliationIntervalMs: () => reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
: {})
|
|
470
|
+
});
|
|
471
|
+
runningController = running;
|
|
472
|
+
runningRuntime = running.runtime;
|
|
473
|
+
// Issue 03: read-only startup reconciliation. Surfaces durable/physical
|
|
474
|
+
// Session mismatches (including generations whose durable map was cleared)
|
|
475
|
+
// without changing stop or archive behavior. Cleanup stays an explicit
|
|
476
|
+
// Operator action in exact-owner-cleanup mode.
|
|
477
|
+
try {
|
|
478
|
+
const startupReport = sessionOwners.report();
|
|
479
|
+
if (startupReport.summary.livePhysicalRoots > 0) {
|
|
480
|
+
(options.onError ?? (() => undefined))(new Error(`Session reconciliation: ${startupReport.summary.livePhysicalRoots} `
|
|
481
|
+
+ `live physical root(s) across ${startupReport.summary.owners} owner record(s); `
|
|
482
|
+
+ "run `yui session reconcile --report` for details."));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
(options.onError ?? (() => undefined))(error);
|
|
464
487
|
}
|
|
488
|
+
let resourceClose;
|
|
489
|
+
const closeResources = () => {
|
|
490
|
+
resourceClose ??= Promise.all([
|
|
491
|
+
web.close(),
|
|
492
|
+
kernel.close(),
|
|
493
|
+
asyncStoreClient?.close() ?? Promise.resolve(),
|
|
494
|
+
inventoryClient?.close() ?? Promise.resolve()
|
|
495
|
+
]).then(() => undefined).finally(() => {
|
|
496
|
+
ownedStore?.close();
|
|
497
|
+
});
|
|
498
|
+
return resourceClose;
|
|
499
|
+
};
|
|
500
|
+
const closed = running.closed.then(closeResources);
|
|
501
|
+
return {
|
|
502
|
+
...running,
|
|
503
|
+
kernel,
|
|
504
|
+
closed,
|
|
505
|
+
close: async () => {
|
|
506
|
+
try {
|
|
507
|
+
await running.close();
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
// RPC-driven Controller stops resolve `running.closed` without calling
|
|
511
|
+
// this wrapper. Share one cleanup promise so both lifecycle paths
|
|
512
|
+
// release the worker connections before the process can linger.
|
|
513
|
+
await closeResources();
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
store,
|
|
517
|
+
schedulerStore,
|
|
518
|
+
planner,
|
|
519
|
+
tmux,
|
|
520
|
+
delivery,
|
|
521
|
+
sessionHost,
|
|
522
|
+
promptPush,
|
|
523
|
+
runtimeIsolation,
|
|
524
|
+
workspacePreparer
|
|
525
|
+
};
|
|
465
526
|
}
|
|
466
527
|
catch (error) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
kernel.close(),
|
|
528
|
+
// The socket may never have opened. Startup failure must still release
|
|
529
|
+
// this attempt's workers/instances instead of leaving an uncallable process.
|
|
530
|
+
await Promise.allSettled([
|
|
531
|
+
closeWeb(),
|
|
532
|
+
closeKernel(),
|
|
473
533
|
asyncStoreClient?.close() ?? Promise.resolve(),
|
|
474
534
|
inventoryClient?.close() ?? Promise.resolve()
|
|
475
|
-
])
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return {
|
|
480
|
-
...running,
|
|
481
|
-
kernel,
|
|
482
|
-
closed,
|
|
483
|
-
close: async () => {
|
|
484
|
-
try {
|
|
485
|
-
await running.close();
|
|
486
|
-
}
|
|
487
|
-
finally {
|
|
488
|
-
// RPC-driven Controller stops resolve `running.closed` without calling
|
|
489
|
-
// this wrapper. Share one cleanup promise so both lifecycle paths
|
|
490
|
-
// release the worker connections before the process can linger.
|
|
491
|
-
await closeResources();
|
|
492
|
-
}
|
|
493
|
-
},
|
|
494
|
-
store,
|
|
495
|
-
schedulerStore,
|
|
496
|
-
planner,
|
|
497
|
-
tmux,
|
|
498
|
-
delivery,
|
|
499
|
-
sessionHost,
|
|
500
|
-
promptPush,
|
|
501
|
-
runtimeIsolation,
|
|
502
|
-
workspacePreparer
|
|
503
|
-
};
|
|
535
|
+
]);
|
|
536
|
+
ownedStore?.close();
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
504
539
|
}
|
|
505
|
-
export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, fallback,
|
|
540
|
+
export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, fallback, sharedLaunchCoordinator, environmentRefresher) {
|
|
506
541
|
const launchCoordinator = sharedLaunchCoordinator
|
|
507
542
|
?? new RuntimeLaunchCoordinator(schedulerStore, sessionHost, {
|
|
508
543
|
assertCurrent: (request) => {
|
|
509
544
|
assertRuntimeLaunchRequestCurrent(store, request);
|
|
510
545
|
},
|
|
511
|
-
onCleanupRequired
|
|
512
546
|
});
|
|
513
547
|
const lifecycleTails = new Map();
|
|
514
548
|
return async (method, params) => {
|
|
@@ -528,9 +562,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
528
562
|
schedulerStore.beginAgentHostProviderTurn({
|
|
529
563
|
taskId: value.taskId,
|
|
530
564
|
roleName: value.roleName,
|
|
531
|
-
...(value.
|
|
565
|
+
...(value.runId === undefined ? {} : { runId: value.runId }),
|
|
532
566
|
agentId: value.agentId,
|
|
533
|
-
runtimeGenerationId: value.runtimeGenerationId,
|
|
534
567
|
nativeSessionId: value.nativeSessionId,
|
|
535
568
|
attemptId: value.attemptId,
|
|
536
569
|
authorityEpoch: value.authorityEpoch,
|
|
@@ -540,6 +573,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
540
573
|
});
|
|
541
574
|
}
|
|
542
575
|
catch (error) {
|
|
576
|
+
if (error instanceof AgentHostProviderSessionBusyError)
|
|
577
|
+
throw error;
|
|
543
578
|
if (error instanceof AgentHostProviderTurnFenceError) {
|
|
544
579
|
throw applicationError("INVALID_PARAMS", error.message);
|
|
545
580
|
}
|
|
@@ -552,7 +587,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
552
587
|
const status = params.status;
|
|
553
588
|
const reason = params.reason;
|
|
554
589
|
const raw = params.raw;
|
|
555
|
-
if ((status !== "rejected" && status !== "delivery-unknown")
|
|
590
|
+
if ((status !== "rejected" && status !== "deferred" && status !== "delivery-unknown")
|
|
556
591
|
|| typeof reason !== "string" || reason.trim().length === 0
|
|
557
592
|
|| typeof raw !== "string" || raw.trim().length === 0) {
|
|
558
593
|
throw applicationError("INVALID_PARAMS", "Provider Turn resolution is invalid.");
|
|
@@ -560,7 +595,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
560
595
|
schedulerStore.resolveAgentHostProviderTurnSubmission({
|
|
561
596
|
taskId: value.taskId,
|
|
562
597
|
roleName: value.roleName,
|
|
563
|
-
...(value.
|
|
598
|
+
...(value.runId === undefined ? {} : { runId: value.runId }),
|
|
564
599
|
attemptId: value.attemptId,
|
|
565
600
|
status,
|
|
566
601
|
reason,
|
|
@@ -571,9 +606,9 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
571
606
|
}
|
|
572
607
|
if (method === "runtime.process-exit-observe") {
|
|
573
608
|
const observation = validateRuntimeProcessExitObservation(params);
|
|
574
|
-
const run = observation.taskId === undefined || observation.
|
|
609
|
+
const run = observation.taskId === undefined || observation.runId === undefined
|
|
575
610
|
? null
|
|
576
|
-
: store.
|
|
611
|
+
: store.getRun(observation.taskId, observation.runId);
|
|
577
612
|
const globalRole = observation.taskId === undefined
|
|
578
613
|
? store.getGlobalRole(observation.roleName)
|
|
579
614
|
: null;
|
|
@@ -582,31 +617,29 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
582
617
|
const driver = adapterId === undefined
|
|
583
618
|
? null
|
|
584
619
|
: builtinAgentDriverRegistry().findByAdapterId(adapterId);
|
|
585
|
-
const
|
|
586
|
-
&& observation.
|
|
620
|
+
const runTerminalObserved = observation.taskId !== undefined
|
|
621
|
+
&& observation.runId !== undefined
|
|
587
622
|
&& store.listEvents(observation.taskId).some((event) => {
|
|
588
623
|
const runtime = runtimeObservationFromTaskEvent(event);
|
|
589
624
|
return runtime !== null
|
|
590
|
-
&& runtime.fence.
|
|
625
|
+
&& runtime.fence.runId === observation.runId
|
|
591
626
|
&& ["turn.completed", "turn.failed", "turn.cancelled"].includes(runtime.kind)
|
|
592
627
|
&& Date.parse(runtime.receivedAt) <= Date.parse(observation.observedAt);
|
|
593
628
|
});
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
&& Date.parse(runtime.receivedAt) <= Date.parse(observation.observedAt);
|
|
603
|
-
});
|
|
629
|
+
const runFailureObserved = observation.taskId !== undefined && observation.runId !== undefined && store.listEvents(observation.taskId).some((event) => {
|
|
630
|
+
const runtime = runtimeObservationFromTaskEvent(event);
|
|
631
|
+
return runtime !== null
|
|
632
|
+
&& runtime.fence.runId === observation.runId
|
|
633
|
+
&& runtime.fence.nativeSessionId === observation.nativeSessionId
|
|
634
|
+
&& runtime.kind === "turn.failed"
|
|
635
|
+
&& Date.parse(runtime.receivedAt) <= Date.parse(observation.observedAt);
|
|
636
|
+
});
|
|
604
637
|
const classification = classifyRuntimeProcessExit(observation, {
|
|
605
638
|
...(driver === null
|
|
606
639
|
? {}
|
|
607
640
|
: { childLifecycle: driver.capabilities.lifecycle.providerProcess }),
|
|
608
|
-
|
|
609
|
-
|
|
641
|
+
runTerminalObserved,
|
|
642
|
+
runFailureObserved
|
|
610
643
|
});
|
|
611
644
|
if (observation.taskId === undefined) {
|
|
612
645
|
const recorded = appendGlobalProcessExitObservation(store.rootDirectory(), observation, classification);
|
|
@@ -624,7 +657,6 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
624
657
|
observationId: observation.observationId,
|
|
625
658
|
processKind: observation.processKind,
|
|
626
659
|
roleName: observation.roleName,
|
|
627
|
-
runtimeGenerationId: observation.runtimeGenerationId,
|
|
628
660
|
observedAt: observation.observedAt,
|
|
629
661
|
classification,
|
|
630
662
|
observation: JSON.stringify(observation)
|
|
@@ -637,16 +669,14 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
637
669
|
if (params === null || typeof params !== "object" || Array.isArray(params)) {
|
|
638
670
|
throw applicationError("INVALID_PARAMS", "Launch redemption params are invalid.");
|
|
639
671
|
}
|
|
640
|
-
const runtimeGenerationId = params.runtimeGenerationId;
|
|
641
672
|
const ticket = params.ticket;
|
|
642
673
|
const hostPid = params.hostPid;
|
|
643
|
-
if (typeof
|
|
644
|
-
|| !Number.isSafeInteger(hostPid) || hostPid <= 0) {
|
|
674
|
+
if (typeof ticket !== "string" || !Number.isSafeInteger(hostPid) || hostPid <= 0) {
|
|
645
675
|
throw applicationError("INVALID_PARAMS", "Launch redemption identity is invalid.");
|
|
646
676
|
}
|
|
647
677
|
// The launch payload is validated at reservation time and every member
|
|
648
678
|
// of its discriminated Provider-control union is JSON serializable.
|
|
649
|
-
return launchBrokerForHome(store.rootDirectory()).redeem(
|
|
679
|
+
return launchBrokerForHome(store.rootDirectory()).redeem(ticket);
|
|
650
680
|
}
|
|
651
681
|
if (method === "runtime.replace-agent-environment") {
|
|
652
682
|
if (environmentRefresher === undefined) {
|
|
@@ -680,13 +710,31 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
680
710
|
const task = request.scope === "task"
|
|
681
711
|
? store.getTask(request.taskId)
|
|
682
712
|
: null;
|
|
713
|
+
// Resolve the admitted Turn first: its purpose decides which Task
|
|
714
|
+
// lifecycle and workspace fences apply. A Draft Leader planning Turn runs
|
|
715
|
+
// without a Task workspace; every delivery Turn keeps the full fence.
|
|
716
|
+
const activeRun = request.scope === "task"
|
|
717
|
+
? store.getActiveRun(request.taskId, request.roleName)
|
|
718
|
+
: null;
|
|
719
|
+
if (request.scope === "task" && activeRun === null) {
|
|
720
|
+
throw applicationError("INVALID_PARAMS", "Task Role runtime attachment requires an admitted active Turn; it cannot create an empty Provider Conversation.");
|
|
721
|
+
}
|
|
683
722
|
if (request.scope === "task"
|
|
684
|
-
&& (task
|
|
723
|
+
&& (task === null || !runPurposeAdmitsTaskState(activeRun.purpose, task))) {
|
|
685
724
|
throw applicationError("INVALID_PARAMS", task === null
|
|
686
725
|
? `Task not found: ${request.taskId}.`
|
|
687
726
|
: `Task execution is not enabled: ${request.taskId}.`);
|
|
688
727
|
}
|
|
689
|
-
|
|
728
|
+
const planningDraft = request.scope === "task"
|
|
729
|
+
&& activeRun?.purpose === "planning"
|
|
730
|
+
&& task?.status === "draft";
|
|
731
|
+
// A Task activated with an empty environment plan owns no managed
|
|
732
|
+
// workspace, so there is nothing to prove ready. Every Task that does own
|
|
733
|
+
// one keeps the full ownership fence.
|
|
734
|
+
if (request.scope === "task"
|
|
735
|
+
&& task !== null
|
|
736
|
+
&& !planningDraft
|
|
737
|
+
&& taskOwnsManagedWorkspace(task)) {
|
|
690
738
|
const taskWorkspace = store.getTaskWorkspace(task.id);
|
|
691
739
|
if (!isTaskOwnedWorkspace(taskWorkspace, task.id, task.cwd, task.projectBindings.map(({ projectId, directory }) => ({ projectId, directory })))) {
|
|
692
740
|
throw new Error(`Task workspace is not ready: ${task.id}.`);
|
|
@@ -708,20 +756,24 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
708
756
|
? `Role not found: ${request.taskId}/${request.roleName}.`
|
|
709
757
|
: `Global Role not found: ${request.roleName}.`);
|
|
710
758
|
}
|
|
711
|
-
const activeTurn = request.scope === "task"
|
|
712
|
-
? store.getActiveTurn(request.taskId, request.roleName)
|
|
713
|
-
: null;
|
|
714
|
-
if (request.scope === "task" && activeTurn === null) {
|
|
715
|
-
throw applicationError("INVALID_PARAMS", "Task Role runtime attachment requires an admitted active Turn; it cannot create an empty Provider Conversation.");
|
|
716
|
-
}
|
|
717
759
|
const managedWorkspace = request.scope === "task"
|
|
718
|
-
?
|
|
760
|
+
? activeRun?.workspace
|
|
719
761
|
?? currentDesiredManagedWorkspace(store, request.taskId, request.roleName)
|
|
720
762
|
: undefined;
|
|
763
|
+
// Distinguish "this Task owns no workspace by design" from "the
|
|
764
|
+
// authoritative workspace is missing". Only the former may launch without
|
|
765
|
+
// one. Two cases qualify: an empty plan binding no Project, and a Draft
|
|
766
|
+
// planning conversation, which may bind a Project but has not activated, so
|
|
767
|
+
// no worktree exists or is owed. Without the planning term a Project-bound
|
|
768
|
+
// Draft fails closed on a workspace activation was never asked to create.
|
|
769
|
+
const workspaceFree = request.scope === "task"
|
|
770
|
+
&& managedWorkspace === undefined
|
|
771
|
+
&& task !== null
|
|
772
|
+
&& (planningDraft || !taskOwnsManagedWorkspace(task));
|
|
721
773
|
const sessions = request.scope === "task"
|
|
722
774
|
? store.getTaskRoleSessionSet(request.taskId, request.roleName)
|
|
723
775
|
: store.getGlobalRoleSessionSet(request.roleName);
|
|
724
|
-
const effective =
|
|
776
|
+
const effective = activeRun?.effective
|
|
725
777
|
?? activeLiveRoleAgentSession(sessions)?.effective
|
|
726
778
|
?? resolveRuntimeDesiredEffective(store, request, role);
|
|
727
779
|
const binding = role.agentBindings[effective.agentId];
|
|
@@ -734,12 +786,12 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
734
786
|
}
|
|
735
787
|
validateLifecycleEnvironment(request.environment, agent);
|
|
736
788
|
const session = sessions?.sessions[effective.agentId];
|
|
737
|
-
// An ensure call may reattach the already-admitted
|
|
789
|
+
// An ensure call may reattach the already-admitted AgentRun to its existing
|
|
738
790
|
// Conversation, but it may not manufacture a fresh Conversation without a
|
|
739
|
-
// pending managed
|
|
740
|
-
const mode =
|
|
791
|
+
// pending managed AgentRun. Fresh replacement remains owned by AgentRun dispatch.
|
|
792
|
+
const mode = activeRun === null
|
|
741
793
|
? roleAgentSessionResumeMode(sessions, effective.agentId, effective)
|
|
742
|
-
: session?.nativeSessionId === undefined ?
|
|
794
|
+
: session?.nativeSessionId === undefined ? activeRun.mode : "resume";
|
|
743
795
|
if (request.scope === "task" && mode === "resume"
|
|
744
796
|
&& (session?.nativeSessionId === undefined
|
|
745
797
|
|| session.nativeSessionId.trim().length === 0)) {
|
|
@@ -755,7 +807,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
755
807
|
effective,
|
|
756
808
|
workspace: effective.workspace.root,
|
|
757
809
|
...(managedWorkspace === undefined ? {} : { managedWorkspace }),
|
|
758
|
-
...(
|
|
810
|
+
...(workspaceFree ? { workspaceFree: true } : {}),
|
|
811
|
+
...(activeRun === null ? {} : { runId: activeRun.id }),
|
|
759
812
|
...(request.environment === undefined
|
|
760
813
|
? {}
|
|
761
814
|
: { environment: request.environment })
|
|
@@ -768,7 +821,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
768
821
|
nativeSessionId: session.nativeSessionId
|
|
769
822
|
};
|
|
770
823
|
const assertCurrent = () => assertRuntimeLaunchRequestCurrent(store, launchRequest);
|
|
771
|
-
const runtimeBinding = await launchCoordinator.prepare(launchRequest,
|
|
824
|
+
const runtimeBinding = await launchCoordinator.prepare(launchRequest, assertCurrent);
|
|
772
825
|
return {
|
|
773
826
|
ensured: true,
|
|
774
827
|
sessionStarted: runtimeBinding.hostCreated === true,
|
|
@@ -794,7 +847,7 @@ function resolveRuntimeDesiredEffective(store, request, role) {
|
|
|
794
847
|
}
|
|
795
848
|
const taskRole = role;
|
|
796
849
|
const item = store.listWorkItems(request.taskId).find((candidate) => (candidate.assignee === request.roleName
|
|
797
|
-
&& !["
|
|
850
|
+
&& !["accepted", "retired"].includes(candidate.status))) ?? null;
|
|
798
851
|
const workspace = (item === null
|
|
799
852
|
? store.getTaskWorkspace(request.taskId)
|
|
800
853
|
: store.getWorkItemWorkspace(request.taskId, item.id))
|
|
@@ -802,24 +855,28 @@ function resolveRuntimeDesiredEffective(store, request, role) {
|
|
|
802
855
|
?? undefined;
|
|
803
856
|
return resolveEffectiveLaunch({
|
|
804
857
|
role: taskRole,
|
|
805
|
-
purpose: "
|
|
858
|
+
purpose: store.getTask(request.taskId)?.status === "draft" && request.roleName === "leader"
|
|
859
|
+
? "planning" : "execution",
|
|
806
860
|
...(workspace === undefined ? {} : { workspace }),
|
|
807
861
|
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
808
862
|
});
|
|
809
863
|
}
|
|
810
864
|
function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
811
|
-
let
|
|
865
|
+
let activeRun = null;
|
|
812
866
|
if (request.owner.scope === "task") {
|
|
813
867
|
const task = store.getTask(request.owner.taskId);
|
|
814
|
-
if (task === null
|
|
815
|
-
|| task.status !== "active"
|
|
816
|
-
|| task.executionGate.state !== "enabled") {
|
|
868
|
+
if (task === null) {
|
|
817
869
|
throw new Error(`Task is no longer active: ${request.owner.taskId}.`);
|
|
818
870
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
871
|
+
activeRun = store.getActiveRun(request.owner.taskId, request.owner.roleName);
|
|
872
|
+
const purpose = activeRun?.purpose
|
|
873
|
+
?? (task.status === "draft" && request.owner.roleName === "leader" ? "planning" : "execution");
|
|
874
|
+
if (!runPurposeAdmitsTaskState(purpose, task)) {
|
|
875
|
+
throw new Error(`Task is no longer active: ${request.owner.taskId}.`);
|
|
876
|
+
}
|
|
877
|
+
if (request.runId !== undefined
|
|
878
|
+
&& activeRun?.id !== request.runId) {
|
|
879
|
+
throw new Error(`Role AgentRun is no longer current: ${request.runId}.`);
|
|
823
880
|
}
|
|
824
881
|
}
|
|
825
882
|
const role = request.owner.scope === "task"
|
|
@@ -831,8 +888,8 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
831
888
|
const sessions = request.owner.scope === "task"
|
|
832
889
|
? store.getTaskRoleSessionSet(request.owner.taskId, request.owner.roleName)
|
|
833
890
|
: store.getGlobalRoleSessionSet(request.owner.roleName);
|
|
834
|
-
const expectedEffective = request.owner.scope === "task" && request.
|
|
835
|
-
?
|
|
891
|
+
const expectedEffective = request.owner.scope === "task" && request.runId !== undefined
|
|
892
|
+
? activeRun?.effective
|
|
836
893
|
: activeLiveRoleAgentSession(sessions)?.effective
|
|
837
894
|
?? currentDesiredEffective(store, request, role);
|
|
838
895
|
if (expectedEffective === undefined
|
|
@@ -843,7 +900,7 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
843
900
|
throw new Error(`Role launch state changed: ${request.owner.roleName}.`);
|
|
844
901
|
}
|
|
845
902
|
if (request.owner.scope === "task" && request.managedWorkspace !== undefined) {
|
|
846
|
-
const expectedWorkspace =
|
|
903
|
+
const expectedWorkspace = activeRun?.workspace
|
|
847
904
|
?? currentDesiredManagedWorkspace(store, request.owner.taskId, request.owner.roleName);
|
|
848
905
|
if (expectedWorkspace === undefined
|
|
849
906
|
|| !sameManagedWorkspaceIdentity(expectedWorkspace, request.managedWorkspace)) {
|
|
@@ -868,13 +925,6 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
868
925
|
|| session.nativeSessionId !== request.nativeSessionId) {
|
|
869
926
|
throw new Error(`Native session changed: ${request.owner.roleName}.`);
|
|
870
927
|
}
|
|
871
|
-
// The Role's durable Session record is the only authority for which Host
|
|
872
|
-
// activation may be restored. Targeting anything else would revive a
|
|
873
|
-
// historical activation.
|
|
874
|
-
if (request.hostActivationId !== undefined
|
|
875
|
-
&& session.runtimeGenerationId !== request.hostActivationId) {
|
|
876
|
-
throw new Error(`Session restore does not target the Role's current Host activation: ${request.owner.roleName}.`);
|
|
877
|
-
}
|
|
878
928
|
if (!roleSessionMayContinue(session.effective, request.effective)) {
|
|
879
929
|
throw new Error(`Native session cannot continue under this launch: ${request.owner.roleName}.`);
|
|
880
930
|
}
|
|
@@ -885,7 +935,7 @@ function currentDesiredEffective(store, request, role) {
|
|
|
885
935
|
return resolveEffectiveLaunch({ role: role, purpose: "execution" });
|
|
886
936
|
}
|
|
887
937
|
const item = store.listWorkItems(request.owner.taskId).find((candidate) => (candidate.assignee === request.owner.roleName
|
|
888
|
-
&& !["
|
|
938
|
+
&& !["accepted", "retired"].includes(candidate.status))) ?? null;
|
|
889
939
|
const workspace = (item === null
|
|
890
940
|
? store.getTaskWorkspace(request.owner.taskId)
|
|
891
941
|
: store.getWorkItemWorkspace(request.owner.taskId, item.id))
|
|
@@ -893,14 +943,15 @@ function currentDesiredEffective(store, request, role) {
|
|
|
893
943
|
?? undefined;
|
|
894
944
|
return resolveEffectiveLaunch({
|
|
895
945
|
role: role,
|
|
896
|
-
purpose: "
|
|
946
|
+
purpose: store.getTask(request.owner.taskId)?.status === "draft" && request.owner.roleName === "leader"
|
|
947
|
+
? "planning" : "execution",
|
|
897
948
|
...(workspace === undefined ? {} : { workspace }),
|
|
898
949
|
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
899
950
|
});
|
|
900
951
|
}
|
|
901
952
|
function currentDesiredManagedWorkspace(store, taskId, roleName) {
|
|
902
953
|
const item = store.listWorkItems(taskId).find((candidate) => (candidate.assignee === roleName
|
|
903
|
-
&& !["
|
|
954
|
+
&& !["accepted", "retired"].includes(candidate.status))) ?? null;
|
|
904
955
|
return (item === null
|
|
905
956
|
? store.getTaskWorkspace(taskId)
|
|
906
957
|
: store.getWorkItemWorkspace(taskId, item.id))
|
|
@@ -1044,7 +1095,7 @@ function validateEnvironmentRefreshSources(store, refresh) {
|
|
|
1044
1095
|
throw applicationError("INVALID_PARAMS", `Runtime native Agent environment source is not declared: ${name}.`);
|
|
1045
1096
|
}
|
|
1046
1097
|
}
|
|
1047
|
-
const allowedNative = new Set(
|
|
1098
|
+
const allowedNative = new Set(NATIVE_AGENT_ENVIRONMENT_NAMES);
|
|
1048
1099
|
if (refresh.nativeNames.some((name) => !allowedNative.has(name))) {
|
|
1049
1100
|
throw applicationError("INVALID_PARAMS", "Runtime native Agent environment scope is invalid.");
|
|
1050
1101
|
}
|
|
@@ -1074,9 +1125,8 @@ function providerTurnControlParams(params) {
|
|
|
1074
1125
|
return {
|
|
1075
1126
|
taskId: requiredParam(value.taskId),
|
|
1076
1127
|
roleName: requiredParam(value.roleName),
|
|
1077
|
-
...(value.
|
|
1128
|
+
...(value.runId === undefined ? {} : { runId: requiredParam(value.runId) }),
|
|
1078
1129
|
agentId: requiredParam(value.agentId),
|
|
1079
|
-
runtimeGenerationId: requiredParam(value.runtimeGenerationId),
|
|
1080
1130
|
nativeSessionId: requiredParam(value.nativeSessionId),
|
|
1081
1131
|
attemptId: requiredParam(value.attemptId),
|
|
1082
1132
|
authorityEpoch: authorityEpoch,
|