@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.
Files changed (281) hide show
  1. package/ARCHITECTURE.md +192 -399
  2. package/README.md +127 -1149
  3. package/dist/agent/adapterCatalog.js +15 -2
  4. package/dist/agent/agent.js +23 -3
  5. package/dist/agent/argumentPolicy.js +7 -1
  6. package/dist/agent/connectionPlan.js +62 -0
  7. package/dist/agent/executionComponents.js +158 -0
  8. package/dist/agent/launchEnvironment.js +31 -3
  9. package/dist/agent/managedRuntimeEnvironment.js +3 -5
  10. package/dist/{turn/turn.js → agentRun/agentRun.js} +166 -109
  11. package/dist/{turn/turnIdentity.js → agentRun/runIdentity.js} +4 -4
  12. package/dist/brief/taskBrief.js +12 -0
  13. package/dist/cli/agentConfigurationPicker.js +13 -0
  14. package/dist/cli/commandCatalog.js +157 -70
  15. package/dist/cli/interactionCandidates.js +5 -5
  16. package/dist/cli/interactionPolicy.js +38 -8
  17. package/dist/cli/invocationRouter.js +1 -1
  18. package/dist/cli/managedDiagnostics.js +28 -0
  19. package/dist/cli/operatorWizard.js +1 -7
  20. package/dist/cli/roleOptionOrder.js +27 -0
  21. package/dist/cli/roleWizard.js +50 -14
  22. package/dist/cli/updateOrchestrator.js +1 -1
  23. package/dist/cli/updatePorts.js +3 -4
  24. package/dist/cli.js +178 -95
  25. package/dist/commands/agentCommands.js +72 -14
  26. package/dist/commands/capabilityCommands.js +9 -6
  27. package/dist/commands/configCommands.js +20 -20
  28. package/dist/commands/deliveryGuardPreflight.js +2 -2
  29. package/dist/commands/executionAuditCommands.js +24 -24
  30. package/dist/commands/globalRoleCommands.js +1 -1
  31. package/dist/commands/grantCommands.js +4 -4
  32. package/dist/commands/operatorCommands.js +1 -7
  33. package/dist/commands/projectCommands.js +4 -4
  34. package/dist/commands/resourcesCommands.js +2 -2
  35. package/dist/commands/roleConfiguration.js +25 -5
  36. package/dist/commands/roleRuntimeGuard.js +4 -5
  37. package/dist/commands/sessionCommands.js +3 -7
  38. package/dist/commands/taskActivationCommands.js +259 -0
  39. package/dist/commands/taskActor.js +28 -49
  40. package/dist/commands/taskCommands.js +1188 -710
  41. package/dist/commands/taskContextCommand.js +39 -583
  42. package/dist/commands/taskExecutionCommands.js +32 -32
  43. package/dist/commands/taskInputCommands.js +40 -104
  44. package/dist/commands/taskIntegrationCommands.js +3 -2
  45. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  46. package/dist/commands/taskNextActionCommand.js +8 -8
  47. package/dist/commands/taskOverviewCommand.js +33 -45
  48. package/dist/commands/taskRemoteDeliveryCommand.js +2 -2
  49. package/dist/commands/taskRoleRuntimeStatus.js +133 -102
  50. package/dist/commands/telemetryCommands.js +36 -38
  51. package/dist/config/configCatalog.js +4 -4
  52. package/dist/config/yuiConfig.js +8 -8
  53. package/dist/context/contextSnapshot.js +10 -10
  54. package/dist/context/dispatchContext.js +11 -11
  55. package/dist/context/roleSessionContext.js +6 -3
  56. package/dist/context/{turnContextPack.js → runContextPack.js} +146 -81
  57. package/dist/context/{turnInputContract.js → runInputContract.js} +73 -60
  58. package/dist/context/sessionBootstrapManifest.js +21 -2
  59. package/dist/context/sourceRunContext.js +30 -0
  60. package/dist/context/taskContext.js +458 -0
  61. package/dist/context/wakeNotification.js +27 -27
  62. package/dist/controller/agentRuntimeObserver.js +21 -24
  63. package/dist/controller/capabilityBridge.js +17 -6
  64. package/dist/controller/clientRuntime.js +65 -92
  65. package/dist/controller/controller.js +114 -188
  66. package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
  67. package/dist/controller/jobControl.js +54 -85
  68. package/dist/controller/resourceInventory.js +8 -27
  69. package/dist/controller/resourceInventoryLinux.js +12 -13
  70. package/dist/controller/runtime.js +529 -479
  71. package/dist/controller/runtimeEventInbox.js +55 -25
  72. package/dist/controller/runtimeEventProcessor.js +22 -31
  73. package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
  74. package/dist/controller/runtimeLaunchCoordinator.js +80 -426
  75. package/dist/controller/runtimeObservationHook.js +14 -18
  76. package/dist/controller/sessionNotify.js +16 -24
  77. package/dist/controller/sessionOwnerReconciliation.js +168 -50
  78. package/dist/controller/structuredProviderObservation.js +138 -99
  79. package/dist/coordination/workMailbox.js +3 -3
  80. package/dist/coordination/workMailboxQueue.js +36 -33
  81. package/dist/core/boundedRpc.js +8 -1
  82. package/dist/core/controllerClient.js +20 -1
  83. package/dist/core/controllerServer.js +4 -4
  84. package/dist/doctor/doctor.js +13 -2
  85. package/dist/domain/agentResultTransport.js +9 -9
  86. package/dist/execution/codexThreadNaming.js +2 -8
  87. package/dist/execution/executionHealth.js +51 -63
  88. package/dist/execution/reviewMainRun.js +137 -0
  89. package/dist/execution/workItemExecution.js +28 -29
  90. package/dist/execution/workItemExecutionProjection.js +99 -107
  91. package/dist/execution/workItemMainRun.js +141 -0
  92. package/dist/executor/agentAdapter.js +227 -20
  93. package/dist/executor/agentConfigurationCatalog.js +126 -4
  94. package/dist/executor/agentConfigurationProbe.js +162 -4
  95. package/dist/executor/agentExecutor.js +79 -78
  96. package/dist/executor/effectiveLaunch.js +105 -18
  97. package/dist/executor/executorRegistry.js +29 -44
  98. package/dist/executor/fileRoleLaunchPlanner.js +229 -154
  99. package/dist/executor/workspacePreflightClassification.js +16 -16
  100. package/dist/grant/capabilityGrant.js +6 -3
  101. package/dist/input/inputRequest.js +12 -10
  102. package/dist/integration/gitIntegrationService.js +4 -11
  103. package/dist/integration/integrationQueueService.js +4 -4
  104. package/dist/interaction/operatorPresentation.js +1 -1
  105. package/dist/kernel/builtinCapabilities.js +247 -12
  106. package/dist/kernel/capabilityRegistry.js +64 -18
  107. package/dist/kernel/instanceHost.js +12 -1
  108. package/dist/kernel/kernelPorts.js +2 -2
  109. package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
  110. package/dist/lifecycle/exactRunTerminalization.js +449 -0
  111. package/dist/message/message.js +62 -6
  112. package/dist/message/messageContinuation.js +204 -0
  113. package/dist/observability/executionAudit.js +70 -72
  114. package/dist/observability/faultClassification.js +2 -2
  115. package/dist/observability/orchestrationMetrics.js +8 -8
  116. package/dist/operator/operatorSessionHistory.js +1 -7
  117. package/dist/output/agentConfigurationPresentation.js +8 -3
  118. package/dist/output/agentRunConfigurationPresentation.js +128 -0
  119. package/dist/output/rolePresentation.js +54 -3
  120. package/dist/plugins/pluginChild.js +104 -0
  121. package/dist/plugins/pluginIntent.js +26 -0
  122. package/dist/plugins/pluginInterpreter.js +43 -0
  123. package/dist/plugins/pluginPackage.js +101 -0
  124. package/dist/plugins/pluginProcess.js +112 -0
  125. package/dist/plugins/pluginService.js +380 -0
  126. package/dist/profile/agentProfile.js +1 -1
  127. package/dist/repository/gitWorkspace.js +26 -4
  128. package/dist/repository/project.js +19 -4
  129. package/dist/repository/taskBaseFreshness.js +13 -13
  130. package/dist/repository/taskWorkspaceCoordinator.js +20 -27
  131. package/dist/repository/taskWorkspacePreparer.js +344 -83
  132. package/dist/resources/autoResourceGc.js +3 -3
  133. package/dist/resources/liveReferences.js +3 -3
  134. package/dist/resources/projectResource.js +123 -0
  135. package/dist/resources/projectResourceService.js +421 -0
  136. package/dist/resources/resourceDiscovery.js +6 -6
  137. package/dist/resources/resourceGc.js +1 -1
  138. package/dist/resources/resourceRegistrar.js +1 -1
  139. package/dist/resources/resourceTypes.js +1 -1
  140. package/dist/review/deltaRecheck.js +3 -3
  141. package/dist/review/reviewAcceptance.js +16 -16
  142. package/dist/review/reviewDecision.js +7 -7
  143. package/dist/review/reviewRound.js +21 -20
  144. package/dist/review/reviewerAvailability.js +2 -2
  145. package/dist/role/role.js +51 -7
  146. package/dist/role/taskRoleUpdate.js +30 -0
  147. package/dist/runtime/acpProtocol.js +425 -0
  148. package/dist/runtime/acpSession.js +731 -0
  149. package/dist/runtime/acpSessionConfiguration.js +260 -0
  150. package/dist/runtime/agentDriver.js +30 -11
  151. package/dist/runtime/agentEndpoint.js +278 -0
  152. package/dist/runtime/agentEndpointIdentity.js +86 -0
  153. package/dist/runtime/agentEndpointOwnership.js +239 -0
  154. package/dist/runtime/agentError.js +2 -10
  155. package/dist/runtime/agentHost.js +565 -314
  156. package/dist/runtime/agentRunConfiguration.js +258 -0
  157. package/dist/runtime/builtinAgentDrivers.js +134 -18
  158. package/dist/runtime/builtinAgentErrorMappers.js +55 -3
  159. package/dist/runtime/builtinTranscriptUsage.js +1 -1
  160. package/dist/runtime/claude-process-owner +0 -0
  161. package/dist/runtime/codexAppServerRuntime.js +38 -30
  162. package/dist/runtime/codexInteractiveHost.js +41 -6
  163. package/dist/runtime/continuationManager.js +2 -6
  164. package/dist/runtime/executionEnvironment.js +30 -0
  165. package/dist/runtime/firstProgressAdvisory.js +11 -11
  166. package/dist/runtime/index.js +4 -3
  167. package/dist/runtime/jsonLineChannel.js +109 -0
  168. package/dist/runtime/launchBroker.js +91 -16
  169. package/dist/runtime/launchDiagnostics.js +2 -2
  170. package/dist/runtime/lifecycleReservation.js +10 -18
  171. package/dist/runtime/managedCaller.js +61 -17
  172. package/dist/runtime/nativeSessionControl.js +102 -0
  173. package/dist/runtime/ports.js +6 -21
  174. package/dist/runtime/processExitObservation.js +8 -7
  175. package/dist/runtime/promptEnvelope.js +17 -6
  176. package/dist/runtime/providerContinuation.js +3 -9
  177. package/dist/runtime/providerContinuationReconciliationService.js +4 -13
  178. package/dist/runtime/providerControl.js +2 -7
  179. package/dist/runtime/providerRuntimeIdentity.js +110 -222
  180. package/dist/runtime/providerRuntimeReconciler.js +5 -9
  181. package/dist/runtime/runtimeBinding.js +0 -1
  182. package/dist/runtime/runtimeContinuationProjection.js +4 -7
  183. package/dist/runtime/runtimeDeadlines.js +9 -0
  184. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  185. package/dist/runtime/runtimeObservation.js +29 -65
  186. package/dist/runtime/runtimeProjection.js +43 -51
  187. package/dist/runtime/runtimeSessionCandidate.js +1 -3
  188. package/dist/runtime/sessionLaunchRequest.js +3 -7
  189. package/dist/runtime/sessionOwnerIdentity.js +7 -54
  190. package/dist/runtime/sessionOwnerRegistry.js +22 -17
  191. package/dist/runtime/sessionReconciliation.js +4 -8
  192. package/dist/runtime/sessionTerminationGuard.js +70 -259
  193. package/dist/runtime/sessionTokenMetrics.js +5 -16
  194. package/dist/runtime/structuredProviderHost.js +237 -117
  195. package/dist/runtime/taskRuntimeIsolation.js +39 -122
  196. package/dist/runtime/tmuxAdapters.js +39 -86
  197. package/dist/scheduler/activeRoleRunDelivery.js +354 -0
  198. package/dist/scheduler/leaderWakeupProcessor.js +75 -266
  199. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  200. package/dist/scheduler/ports.js +80 -9
  201. package/dist/scheduler/{roleTurnLiveness.js → roleRunLiveness.js} +26 -30
  202. package/dist/scheduler/{roleTurnStall.js → roleRunStall.js} +128 -139
  203. package/dist/scheduler/taskExecutionProjection.js +120 -124
  204. package/dist/scheduler/taskObservabilityProjection.js +29 -29
  205. package/dist/scheduler/taskWake.js +11 -4
  206. package/dist/scheduler/wakeReason.js +9 -1
  207. package/dist/setup/setupCommand.js +1 -0
  208. package/dist/storage/migrations/agentRunContract.js +159 -0
  209. package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
  210. package/dist/storage/sqliteSchema.js +431 -5
  211. package/dist/storage/sqliteStore.js +375 -220
  212. package/dist/storage/storageVersions.js +1 -1
  213. package/dist/storage/storeRpc.js +10 -5
  214. package/dist/storage/taskStore.js +13 -11
  215. package/dist/storage/upgrade/upgradeOrchestrator.js +5 -7
  216. package/dist/surface/surfaceContributions.js +102 -0
  217. package/dist/task/completionReadiness.js +32 -6
  218. package/dist/task/deliveryGuard.js +16 -16
  219. package/dist/task/draftPlan.js +72 -12
  220. package/dist/task/nextAction.js +144 -128
  221. package/dist/task/remoteDelivery.js +6 -6
  222. package/dist/task/task.js +184 -18
  223. package/dist/task/taskActivation.js +301 -0
  224. package/dist/task/taskActivationService.js +392 -0
  225. package/dist/task/taskRecordReference.js +5 -4
  226. package/dist/task/taskRecordRetirement.js +1 -1
  227. package/dist/telemetry/sqliteTelemetryStore.js +55 -68
  228. package/dist/telemetry/telemetryConfig.js +14 -14
  229. package/dist/telemetry/telemetryWiring.js +2 -2
  230. package/dist/web/assets/assetManifest.js +2 -0
  231. package/dist/web/assets/client/app.js +120 -20
  232. package/dist/web/assets/client/components.js +87 -54
  233. package/dist/web/assets/client/i18n.js +83 -41
  234. package/dist/web/assets/client/markdown.js +1 -1
  235. package/dist/web/assets/client/taskSurface.js +353 -0
  236. package/dist/web/assets/client/view.js +49 -44
  237. package/dist/web/assets/shell.js +1 -1
  238. package/dist/web/assets/styles/cards.js +22 -4
  239. package/dist/web/controllerWeb.js +60 -0
  240. package/dist/web/webMutation.js +28 -0
  241. package/dist/web/webServer.js +118 -8
  242. package/dist/web/webSnapshot.js +81 -74
  243. package/dist/web/webTaskSurface.js +64 -0
  244. package/dist/workItem/dependencyGate.js +1 -1
  245. package/dist/workItem/workItem.js +80 -48
  246. package/dist/workspace/workItemChangeSetManager.js +16 -9
  247. package/docs/agent-result-consumption.md +94 -0
  248. package/docs/agent-runtime-drivers.md +91 -0
  249. package/docs/architecture/README.md +38 -0
  250. package/docs/architecture/capabilities-and-resources.md +79 -0
  251. package/docs/managed-turn-and-session-runtime.md +222 -0
  252. package/docs/observability/README.md +81 -0
  253. package/docs/plugin-sdk.md +290 -0
  254. package/docs/provider-runtime.md +163 -0
  255. package/docs/release-workflow.md +303 -0
  256. package/docs/roles-and-configuration.md +113 -0
  257. package/docs/sqlite-control-plane-design.md +76 -0
  258. package/docs/task-dag-semantics.md +57 -0
  259. package/docs/task-delivery.md +103 -0
  260. package/docs/task-local-identity.md +6 -6
  261. package/docs/testing/verification-levels.md +86 -0
  262. package/i18n/README.zh-CN.md +98 -739
  263. package/package.json +2 -2
  264. package/skills/yui-leader/SKILL.md +130 -103
  265. package/skills/yui-leader/references/integration.md +39 -0
  266. package/skills/yui-leader/references/replicated-execution.md +42 -0
  267. package/skills/yui-leader/references/task-plugins.md +33 -0
  268. package/skills/yui-operator/SKILL.md +30 -59
  269. package/skills/yui-reviewer/SKILL.md +35 -36
  270. package/skills/yui-runtime/SKILL.md +88 -24
  271. package/skills/yui-runtime/references/publication.md +22 -0
  272. package/skills/yui-runtime/references/recovery.md +64 -0
  273. package/skills/yui-worker/SKILL.md +37 -39
  274. package/dist/cli/roleOptionCatalog.js +0 -68
  275. package/dist/context/sourceTurnContext.js +0 -30
  276. package/dist/execution/reviewMainTurn.js +0 -161
  277. package/dist/execution/workItemMainTurn.js +0 -164
  278. package/dist/lifecycle/exactTurnTerminalization.js +0 -407
  279. package/dist/runtime/preallocatedNativeSession.js +0 -13
  280. package/dist/runtime/runtimeStopReceipt.js +0 -42
  281. 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 generations.
5
+ * Durable, enumerable physical owner records for runtime Sessions.
6
6
  *
7
- * One JSON file per runtime generation id under `<home>/runtime/session-owners/`. The
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 generation's owner identity. */
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.runtimeGenerationId);
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(runtimeGenerationId) {
31
- if (!isSafeRuntimeGenerationId(runtimeGenerationId))
30
+ get(processKey) {
31
+ if (!isSafeProcessKey(processKey))
32
32
  return null;
33
33
  try {
34
- return parseOwnerRecord(readFileSync(this.#path(runtimeGenerationId), "utf8"));
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(runtimeGenerationId) {
70
- if (!isSafeRuntimeGenerationId(runtimeGenerationId))
69
+ remove(processKey) {
70
+ if (!isSafeProcessKey(processKey))
71
71
  return;
72
72
  try {
73
- rmSync(this.#path(runtimeGenerationId), { force: true });
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(runtimeGenerationId) {
84
- return join(this.#directory, `${runtimeGenerationId}.json`);
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 isSafeRuntimeGenerationId(runtimeGenerationId) {
88
- return /^[A-Za-z0-9_.:-]+$/u.test(runtimeGenerationId);
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" : "launch-env"
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 generations.
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 generation can always be re-attributed and reported.
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, record.runtimeGenerationId);
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 === "retired"
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
- * Stops one Role owner with physical exit proof.
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 gracefulGraceMs = positiveDuration(options.gracefulGraceMs, DEFAULT_GRACEFUL_GRACE_MS, "gracefulGraceMs");
19
- const forcedGraceMs = positiveDuration(options.forcedGraceMs, DEFAULT_FORCED_GRACE_MS, "forcedGraceMs");
20
- const pollMs = positiveDuration(options.pollMs, DEFAULT_TERMINATION_POLL_MS, "pollMs");
21
- const now = ports.now();
22
- ports.emit({ stage: "stop-requested", owner, at: now });
23
- // Persist one exact launch receipt before any graceful stop can kill the
24
- // Host. The owner-wide event remains for compatibility and summary display.
25
- for (const record of records) {
26
- ports.emit({
27
- stage: "stop-requested",
28
- owner,
29
- runtimeGenerationId: record.runtimeGenerationId,
30
- ...(record.nativeSessionId === undefined
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
- refreshTrackedFencedChildren(record, ports, trackedChildren);
43
- }
44
- let gracefulOk = true;
45
- try {
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
- // No physical owner record: tmux-level cleanup is the only proof
62
- // available. Report the gap rather than claiming physical zero.
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: gracefulOk ? "stop-confirmed" : "stop-blocked",
65
- owner,
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
- // Bounded graceful grace: poll the exact owned trees for exit.
74
- if (await waitForTreesAbsent(records, ports, trackedChildren, gracefulGraceMs, pollMs)) {
75
- ports.emit({ stage: "stop-confirmed", owner, at: ports.now() });
76
- return {
77
- outcome: "stop-confirmed",
78
- owner,
79
- confirmed: records,
80
- remaining: []
81
- };
82
- }
83
- // Forced escalation against the exact roots only. A root whose identity
84
- // cannot be verified (verification gap) is never signaled: PID reuse
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.signalProcessGroup(processGroupId, signal);
53
+ ports.signalProcess(record.providerRoot.pid, signal);
235
54
  }
236
- catch {
237
- // Group may already be gone or the root may not be its leader.
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
- return;
241
- }
242
- if (root.kind !== "absent")
243
- return;
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
- async function waitForTreesAbsent(records, ports, trackedChildren, timeoutMs, pollMs) {
264
- const start = Date.now();
265
- for (;;) {
266
- if (records.every((record) => treeVerdict(record, ports, trackedChildren).kind === "absent")) {
267
- return true;
268
- }
269
- if (Date.now() - start >= timeoutMs)
270
- return false;
271
- await ports.sleep(Math.min(pollMs, Math.max(1, timeoutMs - (Date.now() - start))));
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 positiveDuration(value, fallback, label) {
275
- const resolved = value ?? fallback;
276
- if (!Number.isSafeInteger(resolved) || resolved <= 0) {
277
- throw new TypeError(`${label} must be a positive integer.`);
278
- }
279
- return resolved;
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
- && matchesSessionGeneration(observation, identity)))
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 generation cannot switch counter semantics without an explicit fact
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 matchesSessionGeneration(observation, identity) {
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)