@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
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { requireSafeIdentity, requireText, requireTimestamp } from "./validation.js";
|
|
3
|
+
/** A concrete OS process identity, not an Agent launch identity. */
|
|
4
|
+
export function sessionOwnerProcessKey(record) {
|
|
5
|
+
return `${record.providerRoot.pid}-${record.providerRoot.startIdentity}`;
|
|
6
|
+
}
|
|
3
7
|
/** Validates and freezes one physical owner record; fails closed on malformed input. */
|
|
4
8
|
export function createSessionOwnerIdentity(input) {
|
|
5
9
|
const owner = input.owner;
|
|
@@ -13,6 +17,9 @@ export function createSessionOwnerIdentity(input) {
|
|
|
13
17
|
throw new TypeError("Global session owner must not carry a task id.");
|
|
14
18
|
}
|
|
15
19
|
const providerRoot = input.providerRoot;
|
|
20
|
+
if (!["launch-env", "pane-pid", "owned-child"].includes(providerRoot.attribution)) {
|
|
21
|
+
throw new TypeError("Session owner process attribution is invalid.");
|
|
22
|
+
}
|
|
16
23
|
if (!Number.isSafeInteger(providerRoot.pid) || providerRoot.pid <= 0) {
|
|
17
24
|
throw new TypeError("Session owner provider root pid is invalid.");
|
|
18
25
|
}
|
|
@@ -38,7 +45,6 @@ export function createSessionOwnerIdentity(input) {
|
|
|
38
45
|
}),
|
|
39
46
|
agentId: requireSafeIdentity(input.agentId, "Agent id"),
|
|
40
47
|
adapterId: requireText(input.adapterId, "Adapter id"),
|
|
41
|
-
runtimeGenerationId: requireSafeIdentity(input.runtimeGenerationId, "Runtime generation id"),
|
|
42
48
|
...(input.nativeSessionId === undefined
|
|
43
49
|
? {}
|
|
44
50
|
: { nativeSessionId: requireText(input.nativeSessionId, "Native session id") }),
|
|
@@ -133,59 +139,6 @@ export function isLinuxProcessLive(pid, startIdentity) {
|
|
|
133
139
|
return false;
|
|
134
140
|
return current.startIdentity === startIdentity;
|
|
135
141
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Discovers the Provider root for one launch by the exact YUI_RUNTIME_GENERATION_ID
|
|
138
|
-
* environment fence. Returns undefined when no same-user process carries the
|
|
139
|
-
* fence — the caller must not guess from a process name or cwd.
|
|
140
|
-
*/
|
|
141
|
-
export function discoverProviderRootByLaunchEnv(runtimeGenerationId) {
|
|
142
|
-
const matches = listLaunchFencedProcesses(runtimeGenerationId);
|
|
143
|
-
for (const pid of matches) {
|
|
144
|
-
const identity = readLinuxProcessIdentity(pid);
|
|
145
|
-
if (identity !== undefined)
|
|
146
|
-
return { pid, identity };
|
|
147
|
-
}
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Lists every live process whose initial environment carries the exact
|
|
152
|
-
* `YUI_RUNTIME_GENERATION_ID=<runtimeGenerationId>` fence. The fence is inherited by Provider
|
|
153
|
-
* children, so this is the exact, PID-reuse-safe attribution for a surviving
|
|
154
|
-
* child after its root has exited: a recycled PID never carries the fence.
|
|
155
|
-
* Processes without the fence are unattributed and must never be signaled.
|
|
156
|
-
*/
|
|
157
|
-
export function listLaunchFencedProcesses(runtimeGenerationId) {
|
|
158
|
-
if (!Number.isSafeInteger(runtimeGenerationId) && !/^[A-Za-z0-9_.:-]+$/u.test(runtimeGenerationId)) {
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
let entries;
|
|
162
|
-
try {
|
|
163
|
-
entries = readdirSync("/proc", { encoding: "utf8" });
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
return [];
|
|
167
|
-
}
|
|
168
|
-
const needle = `YUI_RUNTIME_GENERATION_ID=${runtimeGenerationId}`;
|
|
169
|
-
const matches = [];
|
|
170
|
-
for (const entry of entries) {
|
|
171
|
-
if (!/^[0-9]+$/u.test(entry))
|
|
172
|
-
continue;
|
|
173
|
-
const pid = Number(entry);
|
|
174
|
-
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
175
|
-
continue;
|
|
176
|
-
let environment;
|
|
177
|
-
try {
|
|
178
|
-
environment = readFileSync(`/proc/${pid}/environ`, "utf8");
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (!environment.includes(needle))
|
|
184
|
-
continue;
|
|
185
|
-
matches.push(pid);
|
|
186
|
-
}
|
|
187
|
-
return matches;
|
|
188
|
-
}
|
|
189
142
|
/**
|
|
190
143
|
* Lists the process tree owned by one Provider root: the root itself plus
|
|
191
144
|
* descendants proven by process-group membership or an ancestor chain. A
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import { createSessionOwnerIdentity } from "./sessionOwnerIdentity.js";
|
|
3
|
+
import { createSessionOwnerIdentity, sessionOwnerProcessKey } from "./sessionOwnerIdentity.js";
|
|
4
4
|
/**
|
|
5
|
-
* Durable, enumerable physical owner records for runtime
|
|
5
|
+
* Durable, enumerable physical owner records for runtime Sessions.
|
|
6
6
|
*
|
|
7
|
-
* One JSON file per
|
|
7
|
+
* One JSON file per process identity under `<home>/runtime/session-owners/`. The
|
|
8
8
|
* directory is runtime state (like the tmux socket), not aggregate domain
|
|
9
9
|
* state, so it needs no schema migration; it survives a Controller restart and
|
|
10
10
|
* stays enumerable after the durable Session map or history is cleared, which
|
|
@@ -19,19 +19,19 @@ export class FileSessionOwnerRegistry {
|
|
|
19
19
|
get directory() {
|
|
20
20
|
return this.#directory;
|
|
21
21
|
}
|
|
22
|
-
/** Atomically records (or replaces) one
|
|
22
|
+
/** Atomically records (or replaces) one Session's owner identity. */
|
|
23
23
|
record(identity) {
|
|
24
24
|
mkdirSync(this.#directory, { recursive: true, mode: 0o700 });
|
|
25
|
-
const target = this.#path(identity
|
|
25
|
+
const target = this.#path(sessionOwnerProcessKey(identity));
|
|
26
26
|
const temporary = `${target}.tmp-${process.pid}`;
|
|
27
27
|
writeFileSync(temporary, JSON.stringify(identity, null, 2) + "\n", { mode: 0o600 });
|
|
28
28
|
renameSync(temporary, target);
|
|
29
29
|
}
|
|
30
|
-
get(
|
|
31
|
-
if (!
|
|
30
|
+
get(processKey) {
|
|
31
|
+
if (!isSafeProcessKey(processKey))
|
|
32
32
|
return null;
|
|
33
33
|
try {
|
|
34
|
-
return parseOwnerRecord(readFileSync(this.#path(
|
|
34
|
+
return parseOwnerRecord(readFileSync(this.#path(processKey), "utf8"));
|
|
35
35
|
}
|
|
36
36
|
catch {
|
|
37
37
|
return null;
|
|
@@ -66,11 +66,11 @@ export class FileSessionOwnerRegistry {
|
|
|
66
66
|
&& record.owner.roleName === owner.roleName));
|
|
67
67
|
}
|
|
68
68
|
/** Removes a record whose physical resources were proven absent. */
|
|
69
|
-
remove(
|
|
70
|
-
if (!
|
|
69
|
+
remove(processKey) {
|
|
70
|
+
if (!isSafeProcessKey(processKey))
|
|
71
71
|
return;
|
|
72
72
|
try {
|
|
73
|
-
rmSync(this.#path(
|
|
73
|
+
rmSync(this.#path(processKey), { force: true });
|
|
74
74
|
}
|
|
75
75
|
catch {
|
|
76
76
|
// A stale record is harmless: reconciliation re-verifies physical
|
|
@@ -80,12 +80,14 @@ export class FileSessionOwnerRegistry {
|
|
|
80
80
|
exists() {
|
|
81
81
|
return existsSync(this.#directory);
|
|
82
82
|
}
|
|
83
|
-
#path(
|
|
84
|
-
|
|
83
|
+
#path(processKey) {
|
|
84
|
+
if (!isSafeProcessKey(processKey))
|
|
85
|
+
throw new Error("Invalid process key.");
|
|
86
|
+
return join(this.#directory, `${processKey}.json`);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
|
-
function
|
|
88
|
-
return /^[
|
|
89
|
+
function isSafeProcessKey(processKey) {
|
|
90
|
+
return /^[1-9][0-9]*-[0-9]+$/u.test(processKey);
|
|
89
91
|
}
|
|
90
92
|
function parseOwnerRecord(raw) {
|
|
91
93
|
const parsed = JSON.parse(raw);
|
|
@@ -96,6 +98,9 @@ function parseOwnerRecord(raw) {
|
|
|
96
98
|
const owner = record.owner;
|
|
97
99
|
const tmux = record.tmux;
|
|
98
100
|
const providerRoot = record.providerRoot;
|
|
101
|
+
if (!["launch-env", "pane-pid", "owned-child"].includes(String(providerRoot.attribution))) {
|
|
102
|
+
throw new Error("Session owner process attribution is invalid.");
|
|
103
|
+
}
|
|
99
104
|
return createSessionOwnerIdentity({
|
|
100
105
|
owner: {
|
|
101
106
|
scope: owner.scope === "global" ? "global" : "task",
|
|
@@ -104,7 +109,6 @@ function parseOwnerRecord(raw) {
|
|
|
104
109
|
},
|
|
105
110
|
agentId: String(record.agentId),
|
|
106
111
|
adapterId: String(record.adapterId),
|
|
107
|
-
runtimeGenerationId: String(record.runtimeGenerationId),
|
|
108
112
|
...(record.nativeSessionId === undefined
|
|
109
113
|
? {}
|
|
110
114
|
: { nativeSessionId: String(record.nativeSessionId) }),
|
|
@@ -124,7 +128,8 @@ function parseOwnerRecord(raw) {
|
|
|
124
128
|
...(providerRoot.processSessionId === undefined
|
|
125
129
|
? {}
|
|
126
130
|
: { processSessionId: Number(providerRoot.processSessionId) }),
|
|
127
|
-
attribution: providerRoot.attribution === "pane-pid" ? "pane-pid"
|
|
131
|
+
attribution: providerRoot.attribution === "pane-pid" ? "pane-pid"
|
|
132
|
+
: providerRoot.attribution === "owned-child" ? "owned-child" : "launch-env"
|
|
128
133
|
},
|
|
129
134
|
...(record.runtimeRoot === undefined ? {} : { runtimeRoot: String(record.runtimeRoot) }),
|
|
130
135
|
recordedAt: new Date(String(record.recordedAt))
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bidirectional durable <-> physical reconciliation for Runtime
|
|
2
|
+
* Bidirectional durable <-> physical reconciliation for Runtime Sessions.
|
|
3
3
|
*
|
|
4
4
|
* Durable -> physical: every owner record's Provider root must be absent once
|
|
5
5
|
* its durable Session is terminal; a live root with a terminal durable state
|
|
6
6
|
* is the exact leak the audit found. Physical -> durable: the owner registry
|
|
7
7
|
* stays enumerable after the durable Session map or history is cleared, so a
|
|
8
|
-
* live
|
|
8
|
+
* live Session can always be re-attributed and reported.
|
|
9
9
|
*
|
|
10
10
|
* Pure: all I/O is injected. Unknown owners are reported, never cleaned.
|
|
11
11
|
* An entry is archive-blocking when its durable state is terminal but the
|
|
@@ -34,7 +34,7 @@ function reconcileOne(record, input) {
|
|
|
34
34
|
? input.taskStatus(owner.taskId)
|
|
35
35
|
: undefined;
|
|
36
36
|
const tmuxPane = input.inspectPane(owner.taskId, owner.roleName);
|
|
37
|
-
const lastStopOutcome = input.lastStopOutcome(owner.taskId, owner.roleName
|
|
37
|
+
const lastStopOutcome = input.lastStopOutcome(owner.taskId, owner.roleName);
|
|
38
38
|
let mismatch;
|
|
39
39
|
let verificationGap;
|
|
40
40
|
if (physical?.identityConflict === true) {
|
|
@@ -52,7 +52,7 @@ function reconcileOne(record, input) {
|
|
|
52
52
|
mismatch = "durable-live-physical-absent";
|
|
53
53
|
}
|
|
54
54
|
const terminalTask = taskStatus === "completed"
|
|
55
|
-
|| taskStatus === "
|
|
55
|
+
|| taskStatus === "cancelled"
|
|
56
56
|
|| taskStatus === "archived";
|
|
57
57
|
const archiveBlocked = mismatch === "durable-terminal-physical-live"
|
|
58
58
|
&& terminalTask;
|
|
@@ -64,7 +64,6 @@ function reconcileOne(record, input) {
|
|
|
64
64
|
},
|
|
65
65
|
agentId: record.agentId,
|
|
66
66
|
adapterId: record.adapterId,
|
|
67
|
-
runtimeGenerationId: record.runtimeGenerationId,
|
|
68
67
|
...(record.nativeSessionId === undefined
|
|
69
68
|
? {}
|
|
70
69
|
: { nativeSessionId: record.nativeSessionId }),
|
|
@@ -83,9 +82,6 @@ function matchDurable(record, durable) {
|
|
|
83
82
|
&& fact.roleName === record.owner.roleName
|
|
84
83
|
&& (record.owner.scope === "global" || fact.taskId === record.owner.taskId)
|
|
85
84
|
&& fact.agentId === record.agentId));
|
|
86
|
-
const byLaunch = candidates.find((fact) => fact.runtimeGenerationId !== undefined && fact.runtimeGenerationId === record.runtimeGenerationId);
|
|
87
|
-
if (byLaunch !== undefined)
|
|
88
|
-
return byLaunch;
|
|
89
85
|
const byNative = candidates.find((fact) => fact.nativeSessionId !== undefined
|
|
90
86
|
&& record.nativeSessionId !== undefined
|
|
91
87
|
&& fact.nativeSessionId === record.nativeSessionId);
|
|
@@ -2,279 +2,90 @@ export const DEFAULT_GRACEFUL_GRACE_MS = 3_000;
|
|
|
2
2
|
export const DEFAULT_FORCED_GRACE_MS = 2_000;
|
|
3
3
|
export const DEFAULT_TERMINATION_POLL_MS = 100;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Sequence: graceful exact-owner tmux stop -> bounded grace -> forced
|
|
8
|
-
* SIGTERM/SIGKILL of the exact Provider root and its process group -> final
|
|
9
|
-
* /proc re-verification. Only when every owned root is absent (PID gone or
|
|
10
|
-
* start identity changed) does the result become `stop-confirmed`; otherwise
|
|
11
|
-
* the Session stays non-terminal and the caller must not commit `stopped`.
|
|
12
|
-
*
|
|
13
|
-
* PID reuse is handled structurally: liveness always pairs the PID with its
|
|
14
|
-
* recorded start identity, so a recycled PID is treated as absent and never
|
|
15
|
-
* signaled.
|
|
5
|
+
* Stop only recorded Host roots. Descendants remain observable resources;
|
|
6
|
+
* neither a shared environment nor a process group authorizes killing them.
|
|
16
7
|
*/
|
|
17
8
|
export async function terminateSessionOwners(owner, records, ports, options = {}) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
ports.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
: { nativeSessionId: record.nativeSessionId }),
|
|
33
|
-
at: now
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
// A launch-fence scan is a point-in-time observation, not a durable owner
|
|
37
|
-
// inventory: /proc entries can disappear between the directory and
|
|
38
|
-
// environment reads. Retain every exact child identity observed during the
|
|
39
|
-
// stop so one later scan miss cannot be mistaken for physical zero.
|
|
40
|
-
const trackedChildren = new Map(records.map((record) => [record.runtimeGenerationId, new Map()]));
|
|
9
|
+
const emit = (stage, detail) => {
|
|
10
|
+
try {
|
|
11
|
+
ports.emit({ stage, owner, at: ports.now(), ...(detail ? { detail } : {}) });
|
|
12
|
+
}
|
|
13
|
+
catch { /* Audit failure never broadens process authority. */ }
|
|
14
|
+
};
|
|
15
|
+
const state = (record) => {
|
|
16
|
+
const { pid, startIdentity } = record.providerRoot;
|
|
17
|
+
const observed = ports.processIdentity(pid);
|
|
18
|
+
if (observed === undefined)
|
|
19
|
+
return ports.procEntryExists(pid) ? "unknown" : "absent";
|
|
20
|
+
return observed.startIdentity !== startIdentity || observed.state === "Z"
|
|
21
|
+
? "absent" : "live";
|
|
22
|
+
};
|
|
41
23
|
for (const record of records) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
gracefulOk = await ports.gracefulStop(owner);
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
gracefulOk = false;
|
|
50
|
-
ports.emit({
|
|
51
|
-
stage: "graceful-stop",
|
|
52
|
-
owner,
|
|
53
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
54
|
-
at: ports.now()
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
if (gracefulOk) {
|
|
58
|
-
ports.emit({ stage: "graceful-stop", owner, at: ports.now() });
|
|
24
|
+
if (record.owner.scope !== owner.scope || record.owner.roleName !== owner.roleName
|
|
25
|
+
|| (owner.scope === "task" && record.owner.taskId !== owner.taskId)) {
|
|
26
|
+
throw new Error("Process record does not belong to the requested Role.");
|
|
27
|
+
}
|
|
59
28
|
}
|
|
29
|
+
emit("stop-requested");
|
|
60
30
|
if (records.length === 0) {
|
|
61
|
-
//
|
|
62
|
-
|
|
31
|
+
// Without a recorded root only the exact Role pane can be stopped.
|
|
32
|
+
const stopped = await ports.gracefulStop(owner);
|
|
33
|
+
const outcome = stopped ? "stop-confirmed" : "stop-blocked";
|
|
34
|
+
emit(outcome);
|
|
63
35
|
return {
|
|
64
|
-
outcome
|
|
65
|
-
|
|
66
|
-
confirmed: [],
|
|
67
|
-
remaining: [],
|
|
68
|
-
...(gracefulOk
|
|
69
|
-
? {}
|
|
70
|
-
: { verificationGap: "no-owner-record: tmux stop unconfirmed" })
|
|
36
|
+
outcome, owner, confirmed: [], remaining: [],
|
|
37
|
+
...(stopped ? {} : { verificationGap: "Role pane stop is unconfirmed." })
|
|
71
38
|
};
|
|
72
39
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// safety requires pairing the PID with its start identity.
|
|
86
|
-
ports.emit({ stage: "forced-stop", owner, at: ports.now() });
|
|
87
|
-
for (const record of records) {
|
|
88
|
-
escalateRecord(record, ports, trackedChildren, "SIGTERM");
|
|
89
|
-
}
|
|
90
|
-
await waitForTreesAbsent(records, ports, trackedChildren, forcedGraceMs, pollMs);
|
|
91
|
-
for (const record of records) {
|
|
92
|
-
escalateRecord(record, ports, trackedChildren, "SIGKILL");
|
|
93
|
-
}
|
|
94
|
-
await waitForTreesAbsent(records, ports, trackedChildren, forcedGraceMs, pollMs);
|
|
95
|
-
const remaining = [];
|
|
96
|
-
const confirmed = [];
|
|
97
|
-
let verificationGap;
|
|
98
|
-
for (const record of records) {
|
|
99
|
-
const verdict = treeVerdict(record, ports, trackedChildren);
|
|
100
|
-
if (verdict.kind === "absent") {
|
|
101
|
-
confirmed.push(record);
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
if (verdict.kind === "gap") {
|
|
105
|
-
verificationGap = `/proc unreadable for launch ${record.runtimeGenerationId}`;
|
|
106
|
-
remaining.push({ record, detail: verificationGap });
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
const root = rootVerdict(record, ports);
|
|
110
|
-
remaining.push({
|
|
111
|
-
record,
|
|
112
|
-
detail: root.kind === "live"
|
|
113
|
-
? `provider root pid ${record.providerRoot.pid} still live`
|
|
114
|
-
: `provider children still live for launch ${record.runtimeGenerationId}`
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
const outcome = remaining.length === 0 ? "stop-confirmed" : "stop-blocked";
|
|
118
|
-
ports.emit({
|
|
119
|
-
stage: outcome === "stop-confirmed" ? "stop-confirmed" : "stop-blocked",
|
|
120
|
-
owner,
|
|
121
|
-
...(outcome === "stop-blocked"
|
|
122
|
-
? {
|
|
123
|
-
detail: remaining
|
|
124
|
-
.map(({ record, detail }) => `${record.runtimeGenerationId}: ${detail}`)
|
|
125
|
-
.join("; ")
|
|
126
|
-
}
|
|
127
|
-
: {}),
|
|
128
|
-
at: ports.now()
|
|
129
|
-
});
|
|
130
|
-
return {
|
|
131
|
-
outcome,
|
|
132
|
-
owner,
|
|
133
|
-
confirmed,
|
|
134
|
-
remaining,
|
|
135
|
-
...(verificationGap === undefined ? {} : { verificationGap })
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
function rootVerdict(record, ports) {
|
|
139
|
-
const { pid, startIdentity } = record.providerRoot;
|
|
140
|
-
const current = ports.processIdentity(pid);
|
|
141
|
-
if (current === undefined) {
|
|
142
|
-
// /proc unreadable or entry gone. Gone is absent; unreadable is a gap.
|
|
143
|
-
return ports.procEntryExists(pid) ? { kind: "gap" } : { kind: "absent" };
|
|
144
|
-
}
|
|
145
|
-
// A zombie has already exited; only the unreaped task struct remains. It
|
|
146
|
-
// cannot execute or hold resources, so it counts as absent for physical
|
|
147
|
-
// exit proof (the parent reaping the zombie does not change liveness).
|
|
148
|
-
if (current.state === "Z")
|
|
149
|
-
return { kind: "absent" };
|
|
150
|
-
return current.startIdentity === startIdentity
|
|
151
|
-
? { kind: "live" }
|
|
152
|
-
: { kind: "absent" };
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Verdict for the whole owned tree: the Provider root plus any surviving
|
|
156
|
-
* child carrying the exact launch fence. A verification gap counts as
|
|
157
|
-
* present: the grace polls must not short-circuit to `stop-confirmed` while
|
|
158
|
-
* `/proc` is unreadable, because that would claim physical zero without
|
|
159
|
-
* proof. The final verdict turns a persistent gap into `stop-blocked`.
|
|
160
|
-
*/
|
|
161
|
-
function treeVerdict(record, ports, trackedChildren) {
|
|
162
|
-
const scanGap = refreshTrackedFencedChildren(record, ports, trackedChildren);
|
|
163
|
-
const root = rootVerdict(record, ports);
|
|
164
|
-
const children = trackedChildrenVerdict(record, ports, trackedChildren);
|
|
165
|
-
if (root.kind === "live" || children.kind === "live")
|
|
166
|
-
return { kind: "live" };
|
|
167
|
-
if (root.kind === "gap" || children.kind === "gap" || scanGap)
|
|
168
|
-
return { kind: "gap" };
|
|
169
|
-
return { kind: "absent" };
|
|
170
|
-
}
|
|
171
|
-
function refreshTrackedFencedChildren(record, ports, trackedChildren) {
|
|
172
|
-
const tracked = trackedChildren.get(record.runtimeGenerationId);
|
|
173
|
-
if (tracked === undefined)
|
|
174
|
-
return true;
|
|
175
|
-
let verificationGap = false;
|
|
176
|
-
for (const pid of ports.listLaunchFencedProcesses(record.runtimeGenerationId)) {
|
|
177
|
-
if (pid === record.providerRoot.pid)
|
|
178
|
-
continue;
|
|
179
|
-
const identity = ports.processIdentity(pid);
|
|
180
|
-
if (identity === undefined) {
|
|
181
|
-
if (ports.procEntryExists(pid))
|
|
182
|
-
verificationGap = true;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if (identity.state === "Z")
|
|
186
|
-
continue;
|
|
187
|
-
tracked.set(pid, identity.startIdentity);
|
|
188
|
-
}
|
|
189
|
-
return verificationGap;
|
|
190
|
-
}
|
|
191
|
-
function trackedChildrenVerdict(record, ports, trackedChildren) {
|
|
192
|
-
const tracked = trackedChildren.get(record.runtimeGenerationId);
|
|
193
|
-
if (tracked === undefined)
|
|
194
|
-
return { kind: "gap" };
|
|
195
|
-
let verificationGap = false;
|
|
196
|
-
for (const [pid, startIdentity] of tracked) {
|
|
197
|
-
const current = ports.processIdentity(pid);
|
|
198
|
-
if (current === undefined) {
|
|
199
|
-
if (ports.procEntryExists(pid)) {
|
|
200
|
-
verificationGap = true;
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
tracked.delete(pid);
|
|
204
|
-
}
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (current.state === "Z" || current.startIdentity !== startIdentity) {
|
|
208
|
-
tracked.delete(pid);
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
return { kind: "live" };
|
|
212
|
-
}
|
|
213
|
-
return verificationGap ? { kind: "gap" } : { kind: "absent" };
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Signals the exact root (only when its identity is verified) and, while the
|
|
217
|
-
* root is live, its process group. When the root has already exited but a
|
|
218
|
-
* fenced child survives, each fenced child is signaled individually: the
|
|
219
|
-
* group can no longer be proven ours once its leader is gone.
|
|
220
|
-
*/
|
|
221
|
-
function escalateRecord(record, ports, trackedChildren, signal) {
|
|
222
|
-
const root = rootVerdict(record, ports);
|
|
223
|
-
if (root.kind === "live") {
|
|
224
|
-
try {
|
|
225
|
-
ports.signalProcess(record.providerRoot.pid, signal);
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
228
|
-
// The root may have exited between verdict and signal; the final
|
|
229
|
-
// verification is authoritative.
|
|
230
|
-
}
|
|
231
|
-
const { processGroupId } = record.providerRoot;
|
|
232
|
-
if (processGroupId !== undefined) {
|
|
40
|
+
const pollMs = duration(options.pollMs, DEFAULT_TERMINATION_POLL_MS);
|
|
41
|
+
for (const [signal, grace] of [
|
|
42
|
+
["SIGTERM", duration(options.gracefulGraceMs, DEFAULT_GRACEFUL_GRACE_MS)],
|
|
43
|
+
["SIGKILL", duration(options.forcedGraceMs, DEFAULT_FORCED_GRACE_MS)]
|
|
44
|
+
]) {
|
|
45
|
+
for (const record of records) {
|
|
46
|
+
// The dedicated execution supervisor reaps its descendants before exit.
|
|
47
|
+
// Killing the supervisor would discard that custody proof.
|
|
48
|
+
if (signal === "SIGKILL" && record.providerRoot.attribution === "owned-child")
|
|
49
|
+
continue;
|
|
50
|
+
if (state(record) !== "live")
|
|
51
|
+
continue;
|
|
233
52
|
try {
|
|
234
|
-
ports.
|
|
53
|
+
ports.signalProcess(record.providerRoot.pid, signal);
|
|
235
54
|
}
|
|
236
|
-
catch {
|
|
237
|
-
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code !== "ESRCH") {
|
|
57
|
+
emit("stop-blocked", error instanceof Error ? error.message : String(error));
|
|
58
|
+
}
|
|
238
59
|
}
|
|
239
60
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
refreshTrackedFencedChildren(record, ports, trackedChildren);
|
|
245
|
-
const tracked = trackedChildren.get(record.runtimeGenerationId);
|
|
246
|
-
if (tracked === undefined)
|
|
247
|
-
return;
|
|
248
|
-
for (const [pid, startIdentity] of tracked) {
|
|
249
|
-
const current = ports.processIdentity(pid);
|
|
250
|
-
if (current === undefined
|
|
251
|
-
|| current.state === "Z"
|
|
252
|
-
|| current.startIdentity !== startIdentity) {
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
try {
|
|
256
|
-
ports.signalProcess(pid, signal);
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
// A child may have exited between scan and signal.
|
|
61
|
+
emit(signal === "SIGTERM" ? "graceful-stop" : "forced-stop");
|
|
62
|
+
const deadline = Date.now() + grace;
|
|
63
|
+
while (records.some((record) => state(record) === "live") && Date.now() < deadline) {
|
|
64
|
+
await ports.sleep(Math.min(pollMs, Math.max(1, deadline - Date.now())));
|
|
260
65
|
}
|
|
66
|
+
if (records.every((record) => state(record) === "absent"))
|
|
67
|
+
break;
|
|
261
68
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
69
|
+
const confirmed = [];
|
|
70
|
+
const remaining = [];
|
|
71
|
+
for (const record of records) {
|
|
72
|
+
const observed = state(record);
|
|
73
|
+
if (observed === "absent")
|
|
74
|
+
confirmed.push(record);
|
|
75
|
+
else
|
|
76
|
+
remaining.push({
|
|
77
|
+
record,
|
|
78
|
+
detail: observed === "unknown" ? "Host process identity is unreadable." : "Host process is still live."
|
|
79
|
+
});
|
|
272
80
|
}
|
|
81
|
+
const outcome = remaining.length === 0 ? "stop-confirmed" : "stop-blocked";
|
|
82
|
+
emit(outcome);
|
|
83
|
+
return { outcome, owner, confirmed, remaining };
|
|
273
84
|
}
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
return
|
|
85
|
+
function duration(value, fallback) {
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
return fallback;
|
|
88
|
+
if (!Number.isFinite(value) || value < 0)
|
|
89
|
+
throw new Error("Stop duration must be non-negative.");
|
|
90
|
+
return value;
|
|
280
91
|
}
|
|
@@ -2,12 +2,7 @@ import { builtinDriverIdForAdapter } from "./builtinAgentDrivers.js";
|
|
|
2
2
|
import { isRuntimeTokenEvidence, runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
|
|
3
3
|
const UNOBSERVED = Object.freeze({ status: "unobserved" });
|
|
4
4
|
export function resolveSessionTokenIdentity(input) {
|
|
5
|
-
if (input?.taskId === undefined
|
|
6
|
-
|| input.roleName === undefined
|
|
7
|
-
|| input.agentId === undefined
|
|
8
|
-
|| input.adapterId === undefined
|
|
9
|
-
|| input.runtimeGenerationId === undefined
|
|
10
|
-
|| input.nativeSessionId === undefined)
|
|
5
|
+
if (input?.taskId === undefined || input.roleName === undefined || input.agentId === undefined || input.adapterId === undefined || input.nativeSessionId === undefined)
|
|
11
6
|
return null;
|
|
12
7
|
let driverId;
|
|
13
8
|
try {
|
|
@@ -21,7 +16,6 @@ export function resolveSessionTokenIdentity(input) {
|
|
|
21
16
|
roleName: input.roleName,
|
|
22
17
|
agentId: input.agentId,
|
|
23
18
|
driverId,
|
|
24
|
-
runtimeGenerationId: input.runtimeGenerationId,
|
|
25
19
|
nativeSessionId: input.nativeSessionId
|
|
26
20
|
});
|
|
27
21
|
}
|
|
@@ -36,12 +30,12 @@ export function projectSessionTokenMetrics(events, identity) {
|
|
|
36
30
|
.map(runtimeObservationFromTaskEvent)
|
|
37
31
|
.filter((observation) => (observation !== null
|
|
38
32
|
&& isRuntimeTokenEvidence(observation)
|
|
39
|
-
&&
|
|
33
|
+
&& matchesSessionIdentity(observation, identity)))
|
|
40
34
|
.sort(compareObservations);
|
|
41
35
|
const incompleteBoundaries = observations.filter(({ payload }) => (payload.usage === undefined));
|
|
42
36
|
const request = observations.filter(({ payload }) => (payload.usage?.semantics === "request-context"));
|
|
43
37
|
const cumulative = observations.filter(({ payload }) => (payload.usage?.semantics === "cumulative-session"));
|
|
44
|
-
// A
|
|
38
|
+
// A Session cannot switch counter semantics without an explicit fact
|
|
45
39
|
// explaining how the two streams overlap. Remaining-context is capacity,
|
|
46
40
|
// not consumption, and is intentionally absent from both metrics.
|
|
47
41
|
if (request.length > 0 && cumulative.length > 0) {
|
|
@@ -149,14 +143,9 @@ function projectCumulativeSnapshots(identity, observations, hasIncompleteBoundar
|
|
|
149
143
|
: Object.freeze({ status: "observed", inputTokens: maximumRequestInput })
|
|
150
144
|
});
|
|
151
145
|
}
|
|
152
|
-
function
|
|
146
|
+
function matchesSessionIdentity(observation, identity) {
|
|
153
147
|
const fence = observation.fence;
|
|
154
|
-
return fence.taskId === identity.taskId
|
|
155
|
-
&& fence.roleName === identity.roleName
|
|
156
|
-
&& fence.agentId === identity.agentId
|
|
157
|
-
&& fence.driverId === identity.driverId
|
|
158
|
-
&& fence.runtimeGenerationId === identity.runtimeGenerationId
|
|
159
|
-
&& fence.nativeSessionId === identity.nativeSessionId;
|
|
148
|
+
return fence.taskId === identity.taskId && fence.roleName === identity.roleName && fence.agentId === identity.agentId && fence.driverId === identity.driverId && fence.nativeSessionId === identity.nativeSessionId;
|
|
160
149
|
}
|
|
161
150
|
function compareObservations(left, right) {
|
|
162
151
|
return left.receivedAt.localeCompare(right.receivedAt)
|