@zq-silk/yui 0.15.6 → 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 +167 -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 +188 -95
- package/dist/commands/agentCommands.js +72 -14
- package/dist/commands/capabilityCommands.js +57 -0
- 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 +1216 -730
- 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 +77 -0
- package/dist/controller/clientRuntime.js +65 -92
- package/dist/controller/controller.js +127 -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 +530 -476
- 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 +423 -0
- package/dist/kernel/capabilityRegistry.js +314 -0
- package/dist/kernel/capabilitySchema.js +91 -0
- package/dist/kernel/instanceHost.js +18 -1
- package/dist/kernel/kernelPorts.js +4 -1
- 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 +5 -5
- 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,14 +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
|
+
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";
|
|
26
32
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
27
33
|
import { AgentRuntimeObserver } from "./agentRuntimeObserver.js";
|
|
28
|
-
import { AsyncRuntimeEventProcessor, FileRuntimeEventProcessor, createAsyncRuntimeObserver
|
|
34
|
+
import { AsyncRuntimeEventProcessor, FileRuntimeEventProcessor, createAsyncRuntimeObserver } from "./runtimeEventProcessor.js";
|
|
29
35
|
import { RuntimeLaunchCoordinator } from "./runtimeLaunchCoordinator.js";
|
|
30
36
|
import { ephemeralDomainFromEnvironment, recordEphemeralTmuxTarget } from "./domainIdentity.js";
|
|
31
37
|
import { createEphemeralResourceReaper } from "./ephemeralResourceReaper.js";
|
|
@@ -41,18 +47,22 @@ import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExit
|
|
|
41
47
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
42
48
|
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
43
49
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
50
|
+
import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
51
|
+
import { taskOwnsManagedWorkspace } from "../task/task.js";
|
|
44
52
|
/** Production composition root for the current SQLite TaskStore + tmux Controller. */
|
|
45
53
|
export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
46
54
|
// The current Home always uses SQLite. The persistence worker is enabled by
|
|
47
55
|
// default; YUI_STORE_WORKER=0/false keeps the same database in-process.
|
|
48
56
|
const useWorker = resolveStoreWorkerEnabledForHome(home, options.environment ?? process.env);
|
|
49
|
-
const
|
|
50
|
-
|
|
57
|
+
const ownedStore = options.store === undefined
|
|
58
|
+
? (useWorker
|
|
51
59
|
// The worker owns the event-processing hot path while the scheduler uses
|
|
52
60
|
// the synchronous connection. Both point at the same WAL database and
|
|
53
61
|
// serialize via BEGIN IMMEDIATE + busy_timeout.
|
|
54
62
|
? new SqliteTaskStore(home)
|
|
55
|
-
: openCurrentTaskStore(home))
|
|
63
|
+
: openCurrentTaskStore(home))
|
|
64
|
+
: undefined;
|
|
65
|
+
const store = options.store ?? ownedStore;
|
|
56
66
|
const homeId = store.getHomeIdentity().homeId;
|
|
57
67
|
const durableConfig = store.getConfig();
|
|
58
68
|
// When the worker backend is active, the db-touching observer folds run in
|
|
@@ -69,442 +79,470 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
69
79
|
const inventoryClient = useWorker
|
|
70
80
|
? new ResourceInventoryClient()
|
|
71
81
|
: undefined;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
}
|
|
88
102
|
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
-
// The runtime inbox is also the low-latency discovery source during a
|
|
100
|
-
// scheduler-owned launch. Waiting only for the post-pass durable projection
|
|
101
|
-
// would deadlock behind the same pass that is currently starting the host.
|
|
102
|
-
const runtimeEventInbox = new FileRuntimeEventInbox(home);
|
|
103
|
-
const catalogs = options.catalogs
|
|
104
|
-
?? new AgentConfigurationCatalogService(home, {
|
|
105
|
-
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
|
|
106
112
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
cwd: request.workspace,
|
|
115
|
-
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
|
|
116
120
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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.");
|
|
131
137
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
&&
|
|
136
|
-
&&
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
&& event.observation.
|
|
141
|
-
|
|
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
|
+
}
|
|
142
155
|
}
|
|
156
|
+
await abortableDelay(50, signal);
|
|
143
157
|
}
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
: { nativeSessionId: binding.nativeSessionId }),
|
|
158
|
-
...(pane.pid === undefined ? {} : { panePid: pane.pid })
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
const promptPush = options.promptPush
|
|
163
|
-
?? new AgentHostPromptPushAdapter(home);
|
|
164
|
-
const runtimeIsolation = options.runtimeIsolation
|
|
165
|
-
?? new FileTaskRuntimeIsolation({
|
|
166
|
-
// A sibling of the exact control Home keeps provider data/cache/tmp out
|
|
167
|
-
// of both the shared control plane and every managed Git workspace.
|
|
168
|
-
runtimeRoot: `${resolve(home)}.task-runtimes`,
|
|
169
|
-
controlPlane: {
|
|
170
|
-
yuiHome: home,
|
|
171
|
-
controllerSocketPath: controllerSocketPath(homeId),
|
|
172
|
-
tmuxNamespace: yuiTmuxServerName(home),
|
|
173
|
-
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
|
+
});
|
|
174
171
|
}
|
|
175
172
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (result.outcome === "stop-blocked") {
|
|
189
|
-
(options.onError ?? (() => undefined))(new Error(`Role runtime cleanup could not prove physical exit: ${result.remaining
|
|
190
|
-
.map(({ record, detail }) => `${record.runtimeGenerationId}: ${detail}`)
|
|
191
|
-
.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]
|
|
192
185
|
}
|
|
193
|
-
return result.outcome === "stop-confirmed";
|
|
194
186
|
});
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
: {
|
|
199
|
-
cleanupTaskLaunch: (input) => runtimeIsolation.cleanupTaskLaunch(input)
|
|
200
|
-
})
|
|
201
|
-
};
|
|
202
|
-
const resourceActivity = createRuntimeResourceActivityTracker();
|
|
203
|
-
let runningRuntime;
|
|
204
|
-
let runningController;
|
|
205
|
-
const signalRuntimeCleanup = (target) => {
|
|
206
|
-
runningRuntime?.signal(runtimeLifecycleSignalKey(target.kind === "role-runtime"
|
|
207
|
-
? {
|
|
208
|
-
scope: "task",
|
|
209
|
-
taskId: target.taskId,
|
|
210
|
-
roleName: target.roleName
|
|
211
|
-
}
|
|
212
|
-
: { scope: "global", roleName: target.roleName }));
|
|
213
|
-
};
|
|
214
|
-
const launchCoordinator = new RuntimeLaunchCoordinator(schedulerStore, sessionHost, {
|
|
215
|
-
...(options.now === undefined ? {} : { now: options.now }),
|
|
216
|
-
assertCurrent: (request) => {
|
|
217
|
-
assertRuntimeLaunchRequestCurrent(store, request);
|
|
218
|
-
},
|
|
219
|
-
onCleanupRequired: signalRuntimeCleanup,
|
|
220
|
-
runtimeIsolation
|
|
221
|
-
});
|
|
222
|
-
// One inventory scan per scheduler pass. When the inventory worker is active
|
|
223
|
-
// the blocking /proc scan runs there; otherwise it runs on the main thread.
|
|
224
|
-
const scanInventory = (panes) => inventoryClient !== undefined
|
|
225
|
-
? inventoryClient.scan({
|
|
226
|
-
currentHome: home,
|
|
227
|
-
scope: "current",
|
|
228
|
-
panes,
|
|
229
|
-
...(options.environment === undefined
|
|
230
|
-
? {}
|
|
231
|
-
: { environment: options.environment })
|
|
232
|
-
})
|
|
233
|
-
: scanControllerResourceInventory({
|
|
234
|
-
currentHome: home,
|
|
235
|
-
scope: "current",
|
|
236
|
-
panes,
|
|
237
|
-
tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin),
|
|
238
|
-
...(options.environment === undefined
|
|
187
|
+
const lifecycleHost = {
|
|
188
|
+
inspectOwner: (owner) => (sessionHost.inspectOwner(owner)),
|
|
189
|
+
...(sessionHost.inspectOwners === undefined
|
|
239
190
|
? {}
|
|
240
|
-
: {
|
|
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
|
|
241
217
|
});
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
roleResourceInventory: async (panes, inputs) => {
|
|
247
|
-
const inventory = await scanInventory(panes);
|
|
248
|
-
return inventory.resources.flatMap((resource) => {
|
|
249
|
-
if (resource.kind !== "agent-session")
|
|
250
|
-
return [];
|
|
251
|
-
const owner = resource.owner;
|
|
252
|
-
if (owner.kind !== "task-role")
|
|
253
|
-
return [];
|
|
254
|
-
const active = resource.state === "running" || resource.state === "current";
|
|
255
|
-
const input = inputs.find((candidate) => (candidate.taskId === owner.taskId
|
|
256
|
-
&& candidate.roleName === owner.roleName));
|
|
257
|
-
if (input === undefined)
|
|
258
|
-
return [];
|
|
259
|
-
const identity = owner.turnId === undefined
|
|
260
|
-
|| owner.adapterId === undefined
|
|
261
|
-
|| (owner.nativeSessionId === undefined && owner.runtimeGenerationId === undefined)
|
|
262
|
-
|| (owner.nativeSessionId !== undefined && owner.nativeSessionId.trim().length === 0)
|
|
263
|
-
|| (owner.runtimeGenerationId !== undefined && owner.runtimeGenerationId.trim().length === 0)
|
|
264
|
-
? undefined
|
|
265
|
-
: {
|
|
266
|
-
taskId: owner.taskId,
|
|
267
|
-
roleName: owner.roleName,
|
|
268
|
-
turnId: owner.turnId,
|
|
269
|
-
agentId: owner.agentId,
|
|
270
|
-
adapterId: owner.adapterId,
|
|
271
|
-
...(owner.nativeSessionId === undefined
|
|
272
|
-
? {}
|
|
273
|
-
: { nativeSessionId: owner.nativeSessionId }),
|
|
274
|
-
...(owner.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: owner.runtimeGenerationId })
|
|
275
|
-
};
|
|
276
|
-
const changed = !active || input.turnId === undefined
|
|
277
|
-
? false
|
|
278
|
-
: resourceActivity({
|
|
279
|
-
taskId: input.taskId,
|
|
280
|
-
roleName: input.roleName,
|
|
281
|
-
turnId: input.turnId,
|
|
282
|
-
agentId: input.agentId,
|
|
283
|
-
adapterId: input.adapterId,
|
|
284
|
-
...(input.nativeSessionId === undefined
|
|
285
|
-
? {}
|
|
286
|
-
: { nativeSessionId: input.nativeSessionId }),
|
|
287
|
-
...(input.runtimeGenerationId === undefined ? {} : { runtimeGenerationId: input.runtimeGenerationId })
|
|
288
|
-
}, resource);
|
|
289
|
-
return [{
|
|
290
|
-
taskId: owner.taskId,
|
|
291
|
-
roleName: owner.roleName,
|
|
292
|
-
resource: {
|
|
293
|
-
observedAt: inventory.observedAt,
|
|
294
|
-
active,
|
|
295
|
-
changed: active && changed,
|
|
296
|
-
...(identity === undefined ? {} : { identity }),
|
|
297
|
-
...(input.progressAt === undefined ? {} : { progressAt: input.progressAt }),
|
|
298
|
-
cpuTimeMs: resource.cpuTimeMs,
|
|
299
|
-
...(resource.ioReadBytes === undefined
|
|
300
|
-
? {}
|
|
301
|
-
: { ioReadBytes: resource.ioReadBytes }),
|
|
302
|
-
...(resource.ioWriteBytes === undefined
|
|
303
|
-
? {}
|
|
304
|
-
: { ioWriteBytes: resource.ioWriteBytes }),
|
|
305
|
-
rssBytes: resource.rssBytes
|
|
306
|
-
}
|
|
307
|
-
}];
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
const workspacePreparer = options.workspacePreparer
|
|
312
|
-
?? new FileTaskWorkspacePreparer(home, store);
|
|
313
|
-
const resourceReaper = options.resourceReaper
|
|
314
|
-
?? (domainIdentity === undefined
|
|
315
|
-
? undefined
|
|
316
|
-
: 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({
|
|
317
222
|
currentHome: home,
|
|
318
|
-
// The detached Controller owns one YUI_HOME. Keep automatic
|
|
319
|
-
// recovery bounded to that domain; cross-home cleanup remains an
|
|
320
|
-
// explicit `controller cleanup --all` inventory operation.
|
|
321
223
|
scope: "current",
|
|
322
|
-
|
|
224
|
+
panes,
|
|
225
|
+
...(options.environment === undefined
|
|
226
|
+
? {}
|
|
227
|
+
: { environment: options.environment })
|
|
228
|
+
})
|
|
229
|
+
: scanControllerResourceInventory({
|
|
230
|
+
currentHome: home,
|
|
231
|
+
scope: "current",
|
|
232
|
+
panes,
|
|
323
233
|
tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin),
|
|
324
|
-
|
|
325
|
-
// inventory worker too (same cadence, same inventory shape).
|
|
326
|
-
...(inventoryClient === undefined
|
|
234
|
+
...(options.environment === undefined
|
|
327
235
|
? {}
|
|
328
|
-
: {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
333
264
|
? {}
|
|
334
|
-
: {
|
|
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
|
|
276
|
+
? {}
|
|
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
|
+
})
|
|
335
326
|
})
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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);
|
|
346
343
|
});
|
|
347
|
-
|
|
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
|
-
|
|
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) => {
|
|
381
393
|
try {
|
|
382
|
-
|
|
383
|
-
scope: "task",
|
|
384
|
-
taskId: notice.taskId,
|
|
385
|
-
jobId: notice.jobId,
|
|
386
|
-
status: notice.status,
|
|
387
|
-
outcome: notice.outcome
|
|
388
|
-
});
|
|
394
|
+
runningRuntime?.signal(`task:${taskId}`);
|
|
389
395
|
}
|
|
390
|
-
catch
|
|
391
|
-
//
|
|
392
|
-
// committed. A delivery failure must not fail the reconcile pass.
|
|
393
|
-
(options.onError ?? (() => undefined))(error);
|
|
396
|
+
catch {
|
|
397
|
+
// Controller stopped; the recovery interval remains the fallback.
|
|
394
398
|
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
deliveryTimeoutMs: options.deliveryTimeoutMs
|
|
412
|
-
?? resolveDeliveryTimeoutSeconds(durableConfig.deliveryTimeoutSeconds) * 1_000,
|
|
413
|
-
stallWindowMs: options.stallWindowMs
|
|
414
|
-
?? resolveRuntimeHealth(durableConfig.runtimeHealth).stallWindowMs,
|
|
415
|
-
diagnosticAfterMs: options.diagnosticAfterMs
|
|
416
|
-
?? resolveRuntimeHealth(durableConfig.runtimeHealth).diagnosticAfterMs,
|
|
417
|
-
now: options.now,
|
|
418
|
-
onError: options.onError,
|
|
419
|
-
lifecycleHost,
|
|
420
|
-
jobSupervisor,
|
|
421
|
-
jobControl,
|
|
422
|
-
...(continuationReconciler === undefined ? {} : { continuationReconciler }),
|
|
423
|
-
...(resourceReaper === undefined ? {} : { resourceReaper }),
|
|
424
|
-
resourceAutoGc,
|
|
425
|
-
onExpiredEphemeralDomain: (domain) => {
|
|
426
|
-
if (domain.yuiHome !== home)
|
|
427
|
-
return;
|
|
428
|
-
void runningController?.close().catch(options.onError ?? (() => undefined));
|
|
429
|
-
},
|
|
430
|
-
workspacePreparer,
|
|
431
|
-
runtimeEventProcessor: options.runtimeEventProcessor
|
|
432
|
-
?? (useWorker && asyncStoreClient !== undefined
|
|
433
|
-
? new AsyncRuntimeEventProcessor(runtimeEventInbox, createAsyncRuntimeObserver((method, args) => asyncStoreClient.invokeObserver(method, args)))
|
|
434
|
-
: new FileRuntimeEventProcessor(runtimeEventInbox, schedulerStore)),
|
|
435
|
-
runtimeObserver: options.runtimeObserver
|
|
436
|
-
?? new AgentRuntimeObserver(store, runtimeEventInbox),
|
|
437
|
-
domainIdentity,
|
|
438
|
-
...(options.configuration !== undefined
|
|
439
|
-
? { configuration: options.configuration }
|
|
440
|
-
: options.intervalMs === undefined
|
|
441
|
-
? {
|
|
442
|
-
configuration: {
|
|
443
|
-
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);
|
|
444
415
|
}
|
|
445
416
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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);
|
|
460
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
|
+
};
|
|
461
526
|
}
|
|
462
527
|
catch (error) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
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(),
|
|
469
533
|
asyncStoreClient?.close() ?? Promise.resolve(),
|
|
470
534
|
inventoryClient?.close() ?? Promise.resolve()
|
|
471
|
-
])
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return {
|
|
476
|
-
...running,
|
|
477
|
-
kernel,
|
|
478
|
-
closed,
|
|
479
|
-
close: async () => {
|
|
480
|
-
try {
|
|
481
|
-
await running.close();
|
|
482
|
-
}
|
|
483
|
-
finally {
|
|
484
|
-
// RPC-driven Controller stops resolve `running.closed` without calling
|
|
485
|
-
// this wrapper. Share one cleanup promise so both lifecycle paths
|
|
486
|
-
// release the worker connections before the process can linger.
|
|
487
|
-
await closeResources();
|
|
488
|
-
}
|
|
489
|
-
},
|
|
490
|
-
store,
|
|
491
|
-
schedulerStore,
|
|
492
|
-
planner,
|
|
493
|
-
tmux,
|
|
494
|
-
delivery,
|
|
495
|
-
sessionHost,
|
|
496
|
-
promptPush,
|
|
497
|
-
runtimeIsolation,
|
|
498
|
-
workspacePreparer
|
|
499
|
-
};
|
|
535
|
+
]);
|
|
536
|
+
ownedStore?.close();
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
500
539
|
}
|
|
501
|
-
export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, fallback,
|
|
540
|
+
export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, fallback, sharedLaunchCoordinator, environmentRefresher) {
|
|
502
541
|
const launchCoordinator = sharedLaunchCoordinator
|
|
503
542
|
?? new RuntimeLaunchCoordinator(schedulerStore, sessionHost, {
|
|
504
543
|
assertCurrent: (request) => {
|
|
505
544
|
assertRuntimeLaunchRequestCurrent(store, request);
|
|
506
545
|
},
|
|
507
|
-
onCleanupRequired
|
|
508
546
|
});
|
|
509
547
|
const lifecycleTails = new Map();
|
|
510
548
|
return async (method, params) => {
|
|
@@ -524,9 +562,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
524
562
|
schedulerStore.beginAgentHostProviderTurn({
|
|
525
563
|
taskId: value.taskId,
|
|
526
564
|
roleName: value.roleName,
|
|
527
|
-
...(value.
|
|
565
|
+
...(value.runId === undefined ? {} : { runId: value.runId }),
|
|
528
566
|
agentId: value.agentId,
|
|
529
|
-
runtimeGenerationId: value.runtimeGenerationId,
|
|
530
567
|
nativeSessionId: value.nativeSessionId,
|
|
531
568
|
attemptId: value.attemptId,
|
|
532
569
|
authorityEpoch: value.authorityEpoch,
|
|
@@ -536,6 +573,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
536
573
|
});
|
|
537
574
|
}
|
|
538
575
|
catch (error) {
|
|
576
|
+
if (error instanceof AgentHostProviderSessionBusyError)
|
|
577
|
+
throw error;
|
|
539
578
|
if (error instanceof AgentHostProviderTurnFenceError) {
|
|
540
579
|
throw applicationError("INVALID_PARAMS", error.message);
|
|
541
580
|
}
|
|
@@ -548,7 +587,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
548
587
|
const status = params.status;
|
|
549
588
|
const reason = params.reason;
|
|
550
589
|
const raw = params.raw;
|
|
551
|
-
if ((status !== "rejected" && status !== "delivery-unknown")
|
|
590
|
+
if ((status !== "rejected" && status !== "deferred" && status !== "delivery-unknown")
|
|
552
591
|
|| typeof reason !== "string" || reason.trim().length === 0
|
|
553
592
|
|| typeof raw !== "string" || raw.trim().length === 0) {
|
|
554
593
|
throw applicationError("INVALID_PARAMS", "Provider Turn resolution is invalid.");
|
|
@@ -556,7 +595,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
556
595
|
schedulerStore.resolveAgentHostProviderTurnSubmission({
|
|
557
596
|
taskId: value.taskId,
|
|
558
597
|
roleName: value.roleName,
|
|
559
|
-
...(value.
|
|
598
|
+
...(value.runId === undefined ? {} : { runId: value.runId }),
|
|
560
599
|
attemptId: value.attemptId,
|
|
561
600
|
status,
|
|
562
601
|
reason,
|
|
@@ -567,9 +606,9 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
567
606
|
}
|
|
568
607
|
if (method === "runtime.process-exit-observe") {
|
|
569
608
|
const observation = validateRuntimeProcessExitObservation(params);
|
|
570
|
-
const run = observation.taskId === undefined || observation.
|
|
609
|
+
const run = observation.taskId === undefined || observation.runId === undefined
|
|
571
610
|
? null
|
|
572
|
-
: store.
|
|
611
|
+
: store.getRun(observation.taskId, observation.runId);
|
|
573
612
|
const globalRole = observation.taskId === undefined
|
|
574
613
|
? store.getGlobalRole(observation.roleName)
|
|
575
614
|
: null;
|
|
@@ -578,31 +617,29 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
578
617
|
const driver = adapterId === undefined
|
|
579
618
|
? null
|
|
580
619
|
: builtinAgentDriverRegistry().findByAdapterId(adapterId);
|
|
581
|
-
const
|
|
582
|
-
&& observation.
|
|
620
|
+
const runTerminalObserved = observation.taskId !== undefined
|
|
621
|
+
&& observation.runId !== undefined
|
|
583
622
|
&& store.listEvents(observation.taskId).some((event) => {
|
|
584
623
|
const runtime = runtimeObservationFromTaskEvent(event);
|
|
585
624
|
return runtime !== null
|
|
586
|
-
&& runtime.fence.
|
|
625
|
+
&& runtime.fence.runId === observation.runId
|
|
587
626
|
&& ["turn.completed", "turn.failed", "turn.cancelled"].includes(runtime.kind)
|
|
588
627
|
&& Date.parse(runtime.receivedAt) <= Date.parse(observation.observedAt);
|
|
589
628
|
});
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
&& Date.parse(runtime.receivedAt) <= Date.parse(observation.observedAt);
|
|
599
|
-
});
|
|
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
|
+
});
|
|
600
637
|
const classification = classifyRuntimeProcessExit(observation, {
|
|
601
638
|
...(driver === null
|
|
602
639
|
? {}
|
|
603
640
|
: { childLifecycle: driver.capabilities.lifecycle.providerProcess }),
|
|
604
|
-
|
|
605
|
-
|
|
641
|
+
runTerminalObserved,
|
|
642
|
+
runFailureObserved
|
|
606
643
|
});
|
|
607
644
|
if (observation.taskId === undefined) {
|
|
608
645
|
const recorded = appendGlobalProcessExitObservation(store.rootDirectory(), observation, classification);
|
|
@@ -620,7 +657,6 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
620
657
|
observationId: observation.observationId,
|
|
621
658
|
processKind: observation.processKind,
|
|
622
659
|
roleName: observation.roleName,
|
|
623
|
-
runtimeGenerationId: observation.runtimeGenerationId,
|
|
624
660
|
observedAt: observation.observedAt,
|
|
625
661
|
classification,
|
|
626
662
|
observation: JSON.stringify(observation)
|
|
@@ -633,16 +669,14 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
633
669
|
if (params === null || typeof params !== "object" || Array.isArray(params)) {
|
|
634
670
|
throw applicationError("INVALID_PARAMS", "Launch redemption params are invalid.");
|
|
635
671
|
}
|
|
636
|
-
const runtimeGenerationId = params.runtimeGenerationId;
|
|
637
672
|
const ticket = params.ticket;
|
|
638
673
|
const hostPid = params.hostPid;
|
|
639
|
-
if (typeof
|
|
640
|
-
|| !Number.isSafeInteger(hostPid) || hostPid <= 0) {
|
|
674
|
+
if (typeof ticket !== "string" || !Number.isSafeInteger(hostPid) || hostPid <= 0) {
|
|
641
675
|
throw applicationError("INVALID_PARAMS", "Launch redemption identity is invalid.");
|
|
642
676
|
}
|
|
643
677
|
// The launch payload is validated at reservation time and every member
|
|
644
678
|
// of its discriminated Provider-control union is JSON serializable.
|
|
645
|
-
return launchBrokerForHome(store.rootDirectory()).redeem(
|
|
679
|
+
return launchBrokerForHome(store.rootDirectory()).redeem(ticket);
|
|
646
680
|
}
|
|
647
681
|
if (method === "runtime.replace-agent-environment") {
|
|
648
682
|
if (environmentRefresher === undefined) {
|
|
@@ -676,13 +710,31 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
676
710
|
const task = request.scope === "task"
|
|
677
711
|
? store.getTask(request.taskId)
|
|
678
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
|
+
}
|
|
679
722
|
if (request.scope === "task"
|
|
680
|
-
&& (task
|
|
723
|
+
&& (task === null || !runPurposeAdmitsTaskState(activeRun.purpose, task))) {
|
|
681
724
|
throw applicationError("INVALID_PARAMS", task === null
|
|
682
725
|
? `Task not found: ${request.taskId}.`
|
|
683
726
|
: `Task execution is not enabled: ${request.taskId}.`);
|
|
684
727
|
}
|
|
685
|
-
|
|
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)) {
|
|
686
738
|
const taskWorkspace = store.getTaskWorkspace(task.id);
|
|
687
739
|
if (!isTaskOwnedWorkspace(taskWorkspace, task.id, task.cwd, task.projectBindings.map(({ projectId, directory }) => ({ projectId, directory })))) {
|
|
688
740
|
throw new Error(`Task workspace is not ready: ${task.id}.`);
|
|
@@ -704,20 +756,24 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
704
756
|
? `Role not found: ${request.taskId}/${request.roleName}.`
|
|
705
757
|
: `Global Role not found: ${request.roleName}.`);
|
|
706
758
|
}
|
|
707
|
-
const activeTurn = request.scope === "task"
|
|
708
|
-
? store.getActiveTurn(request.taskId, request.roleName)
|
|
709
|
-
: null;
|
|
710
|
-
if (request.scope === "task" && activeTurn === null) {
|
|
711
|
-
throw applicationError("INVALID_PARAMS", "Task Role runtime attachment requires an admitted active Turn; it cannot create an empty Provider Conversation.");
|
|
712
|
-
}
|
|
713
759
|
const managedWorkspace = request.scope === "task"
|
|
714
|
-
?
|
|
760
|
+
? activeRun?.workspace
|
|
715
761
|
?? currentDesiredManagedWorkspace(store, request.taskId, request.roleName)
|
|
716
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));
|
|
717
773
|
const sessions = request.scope === "task"
|
|
718
774
|
? store.getTaskRoleSessionSet(request.taskId, request.roleName)
|
|
719
775
|
: store.getGlobalRoleSessionSet(request.roleName);
|
|
720
|
-
const effective =
|
|
776
|
+
const effective = activeRun?.effective
|
|
721
777
|
?? activeLiveRoleAgentSession(sessions)?.effective
|
|
722
778
|
?? resolveRuntimeDesiredEffective(store, request, role);
|
|
723
779
|
const binding = role.agentBindings[effective.agentId];
|
|
@@ -730,12 +786,12 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
730
786
|
}
|
|
731
787
|
validateLifecycleEnvironment(request.environment, agent);
|
|
732
788
|
const session = sessions?.sessions[effective.agentId];
|
|
733
|
-
// An ensure call may reattach the already-admitted
|
|
789
|
+
// An ensure call may reattach the already-admitted AgentRun to its existing
|
|
734
790
|
// Conversation, but it may not manufacture a fresh Conversation without a
|
|
735
|
-
// pending managed
|
|
736
|
-
const mode =
|
|
791
|
+
// pending managed AgentRun. Fresh replacement remains owned by AgentRun dispatch.
|
|
792
|
+
const mode = activeRun === null
|
|
737
793
|
? roleAgentSessionResumeMode(sessions, effective.agentId, effective)
|
|
738
|
-
: session?.nativeSessionId === undefined ?
|
|
794
|
+
: session?.nativeSessionId === undefined ? activeRun.mode : "resume";
|
|
739
795
|
if (request.scope === "task" && mode === "resume"
|
|
740
796
|
&& (session?.nativeSessionId === undefined
|
|
741
797
|
|| session.nativeSessionId.trim().length === 0)) {
|
|
@@ -751,7 +807,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
751
807
|
effective,
|
|
752
808
|
workspace: effective.workspace.root,
|
|
753
809
|
...(managedWorkspace === undefined ? {} : { managedWorkspace }),
|
|
754
|
-
...(
|
|
810
|
+
...(workspaceFree ? { workspaceFree: true } : {}),
|
|
811
|
+
...(activeRun === null ? {} : { runId: activeRun.id }),
|
|
755
812
|
...(request.environment === undefined
|
|
756
813
|
? {}
|
|
757
814
|
: { environment: request.environment })
|
|
@@ -764,7 +821,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
764
821
|
nativeSessionId: session.nativeSessionId
|
|
765
822
|
};
|
|
766
823
|
const assertCurrent = () => assertRuntimeLaunchRequestCurrent(store, launchRequest);
|
|
767
|
-
const runtimeBinding = await launchCoordinator.prepare(launchRequest,
|
|
824
|
+
const runtimeBinding = await launchCoordinator.prepare(launchRequest, assertCurrent);
|
|
768
825
|
return {
|
|
769
826
|
ensured: true,
|
|
770
827
|
sessionStarted: runtimeBinding.hostCreated === true,
|
|
@@ -790,7 +847,7 @@ function resolveRuntimeDesiredEffective(store, request, role) {
|
|
|
790
847
|
}
|
|
791
848
|
const taskRole = role;
|
|
792
849
|
const item = store.listWorkItems(request.taskId).find((candidate) => (candidate.assignee === request.roleName
|
|
793
|
-
&& !["
|
|
850
|
+
&& !["accepted", "retired"].includes(candidate.status))) ?? null;
|
|
794
851
|
const workspace = (item === null
|
|
795
852
|
? store.getTaskWorkspace(request.taskId)
|
|
796
853
|
: store.getWorkItemWorkspace(request.taskId, item.id))
|
|
@@ -798,24 +855,28 @@ function resolveRuntimeDesiredEffective(store, request, role) {
|
|
|
798
855
|
?? undefined;
|
|
799
856
|
return resolveEffectiveLaunch({
|
|
800
857
|
role: taskRole,
|
|
801
|
-
purpose: "
|
|
858
|
+
purpose: store.getTask(request.taskId)?.status === "draft" && request.roleName === "leader"
|
|
859
|
+
? "planning" : "execution",
|
|
802
860
|
...(workspace === undefined ? {} : { workspace }),
|
|
803
861
|
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
804
862
|
});
|
|
805
863
|
}
|
|
806
864
|
function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
807
|
-
let
|
|
865
|
+
let activeRun = null;
|
|
808
866
|
if (request.owner.scope === "task") {
|
|
809
867
|
const task = store.getTask(request.owner.taskId);
|
|
810
|
-
if (task === null
|
|
811
|
-
|| task.status !== "active"
|
|
812
|
-
|| task.executionGate.state !== "enabled") {
|
|
868
|
+
if (task === null) {
|
|
813
869
|
throw new Error(`Task is no longer active: ${request.owner.taskId}.`);
|
|
814
870
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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}.`);
|
|
819
880
|
}
|
|
820
881
|
}
|
|
821
882
|
const role = request.owner.scope === "task"
|
|
@@ -827,8 +888,8 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
827
888
|
const sessions = request.owner.scope === "task"
|
|
828
889
|
? store.getTaskRoleSessionSet(request.owner.taskId, request.owner.roleName)
|
|
829
890
|
: store.getGlobalRoleSessionSet(request.owner.roleName);
|
|
830
|
-
const expectedEffective = request.owner.scope === "task" && request.
|
|
831
|
-
?
|
|
891
|
+
const expectedEffective = request.owner.scope === "task" && request.runId !== undefined
|
|
892
|
+
? activeRun?.effective
|
|
832
893
|
: activeLiveRoleAgentSession(sessions)?.effective
|
|
833
894
|
?? currentDesiredEffective(store, request, role);
|
|
834
895
|
if (expectedEffective === undefined
|
|
@@ -839,7 +900,7 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
839
900
|
throw new Error(`Role launch state changed: ${request.owner.roleName}.`);
|
|
840
901
|
}
|
|
841
902
|
if (request.owner.scope === "task" && request.managedWorkspace !== undefined) {
|
|
842
|
-
const expectedWorkspace =
|
|
903
|
+
const expectedWorkspace = activeRun?.workspace
|
|
843
904
|
?? currentDesiredManagedWorkspace(store, request.owner.taskId, request.owner.roleName);
|
|
844
905
|
if (expectedWorkspace === undefined
|
|
845
906
|
|| !sameManagedWorkspaceIdentity(expectedWorkspace, request.managedWorkspace)) {
|
|
@@ -864,13 +925,6 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
864
925
|
|| session.nativeSessionId !== request.nativeSessionId) {
|
|
865
926
|
throw new Error(`Native session changed: ${request.owner.roleName}.`);
|
|
866
927
|
}
|
|
867
|
-
// The Role's durable Session record is the only authority for which Host
|
|
868
|
-
// activation may be restored. Targeting anything else would revive a
|
|
869
|
-
// historical activation.
|
|
870
|
-
if (request.hostActivationId !== undefined
|
|
871
|
-
&& session.runtimeGenerationId !== request.hostActivationId) {
|
|
872
|
-
throw new Error(`Session restore does not target the Role's current Host activation: ${request.owner.roleName}.`);
|
|
873
|
-
}
|
|
874
928
|
if (!roleSessionMayContinue(session.effective, request.effective)) {
|
|
875
929
|
throw new Error(`Native session cannot continue under this launch: ${request.owner.roleName}.`);
|
|
876
930
|
}
|
|
@@ -881,7 +935,7 @@ function currentDesiredEffective(store, request, role) {
|
|
|
881
935
|
return resolveEffectiveLaunch({ role: role, purpose: "execution" });
|
|
882
936
|
}
|
|
883
937
|
const item = store.listWorkItems(request.owner.taskId).find((candidate) => (candidate.assignee === request.owner.roleName
|
|
884
|
-
&& !["
|
|
938
|
+
&& !["accepted", "retired"].includes(candidate.status))) ?? null;
|
|
885
939
|
const workspace = (item === null
|
|
886
940
|
? store.getTaskWorkspace(request.owner.taskId)
|
|
887
941
|
: store.getWorkItemWorkspace(request.owner.taskId, item.id))
|
|
@@ -889,14 +943,15 @@ function currentDesiredEffective(store, request, role) {
|
|
|
889
943
|
?? undefined;
|
|
890
944
|
return resolveEffectiveLaunch({
|
|
891
945
|
role: role,
|
|
892
|
-
purpose: "
|
|
946
|
+
purpose: store.getTask(request.owner.taskId)?.status === "draft" && request.owner.roleName === "leader"
|
|
947
|
+
? "planning" : "execution",
|
|
893
948
|
...(workspace === undefined ? {} : { workspace }),
|
|
894
949
|
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
895
950
|
});
|
|
896
951
|
}
|
|
897
952
|
function currentDesiredManagedWorkspace(store, taskId, roleName) {
|
|
898
953
|
const item = store.listWorkItems(taskId).find((candidate) => (candidate.assignee === roleName
|
|
899
|
-
&& !["
|
|
954
|
+
&& !["accepted", "retired"].includes(candidate.status))) ?? null;
|
|
900
955
|
return (item === null
|
|
901
956
|
? store.getTaskWorkspace(taskId)
|
|
902
957
|
: store.getWorkItemWorkspace(taskId, item.id))
|
|
@@ -1040,7 +1095,7 @@ function validateEnvironmentRefreshSources(store, refresh) {
|
|
|
1040
1095
|
throw applicationError("INVALID_PARAMS", `Runtime native Agent environment source is not declared: ${name}.`);
|
|
1041
1096
|
}
|
|
1042
1097
|
}
|
|
1043
|
-
const allowedNative = new Set(
|
|
1098
|
+
const allowedNative = new Set(NATIVE_AGENT_ENVIRONMENT_NAMES);
|
|
1044
1099
|
if (refresh.nativeNames.some((name) => !allowedNative.has(name))) {
|
|
1045
1100
|
throw applicationError("INVALID_PARAMS", "Runtime native Agent environment scope is invalid.");
|
|
1046
1101
|
}
|
|
@@ -1070,9 +1125,8 @@ function providerTurnControlParams(params) {
|
|
|
1070
1125
|
return {
|
|
1071
1126
|
taskId: requiredParam(value.taskId),
|
|
1072
1127
|
roleName: requiredParam(value.roleName),
|
|
1073
|
-
...(value.
|
|
1128
|
+
...(value.runId === undefined ? {} : { runId: requiredParam(value.runId) }),
|
|
1074
1129
|
agentId: requiredParam(value.agentId),
|
|
1075
|
-
runtimeGenerationId: requiredParam(value.runtimeGenerationId),
|
|
1076
1130
|
nativeSessionId: requiredParam(value.nativeSessionId),
|
|
1077
1131
|
attemptId: requiredParam(value.attemptId),
|
|
1078
1132
|
authorityEpoch: authorityEpoch,
|