@zq-silk/yui 0.15.12 → 0.16.0

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 (236) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/ARCHITECTURE.zh-CN.md +3 -2
  3. package/dist/artifacts/artifactCommitLock.js +16 -55
  4. package/dist/artifacts/taskArtifactRepository.js +3 -3
  5. package/dist/cli/agentConfigurationPicker.js +1 -1
  6. package/dist/cli/commandCatalog.js +57 -41
  7. package/dist/cli/completion.js +18 -18
  8. package/dist/cli/completionWizard.js +1 -1
  9. package/dist/cli/dynamicCompletion.js +1 -1
  10. package/dist/cli/interactionPolicy.js +7 -3
  11. package/dist/cli/invocationAuthority.js +64 -0
  12. package/dist/cli/managedDiagnostics.js +1 -1
  13. package/dist/cli/parseRepeatable.js +35 -0
  14. package/dist/cli/updatePorts.js +26 -5
  15. package/dist/cli.js +1280 -1243
  16. package/dist/commands/agentCommands.js +4 -4
  17. package/dist/commands/capabilityCommands.js +1 -1
  18. package/dist/commands/configCommands.js +6 -57
  19. package/dist/commands/configOverview.js +2 -2
  20. package/dist/commands/durableJobCommands.js +12 -6
  21. package/dist/commands/globalRoleCommands.js +53 -29
  22. package/dist/commands/grantCommands.js +0 -3
  23. package/dist/commands/jobCommands.js +1 -4
  24. package/dist/commands/operatorCommands.js +3 -3
  25. package/dist/commands/projectCommands.js +1 -10
  26. package/dist/commands/resourcesCommands.js +10 -40
  27. package/dist/commands/roleConfiguration.js +2 -6
  28. package/dist/commands/taskActivationCommands.js +4 -8
  29. package/dist/commands/taskAgentCapabilities.js +9 -0
  30. package/dist/commands/taskCommandSupport.js +155 -0
  31. package/dist/commands/taskCommandTypes.js +1 -0
  32. package/dist/commands/taskCommands.js +271 -710
  33. package/dist/commands/taskCompletionGate.js +1 -1
  34. package/dist/commands/taskExecutionCommands.js +1 -1
  35. package/dist/commands/taskFactCommands.js +325 -0
  36. package/dist/commands/taskInputCommands.js +12 -42
  37. package/dist/commands/taskIntegrationCommands.js +17 -57
  38. package/dist/commands/taskNextActionCommand.js +7 -6
  39. package/dist/commands/taskPublicationAdoptCommand.js +3 -3
  40. package/dist/commands/taskPublicationCommands.js +1 -1
  41. package/dist/commands/taskPublicationVerifyCommand.js +1 -1
  42. package/dist/commands/taskRemoteDeliveryCommand.js +2 -94
  43. package/dist/commands/taskUpstreamCommands.js +19 -8
  44. package/dist/commands/workflowCommands.js +6 -1
  45. package/dist/completion/completionInstaller.js +26 -26
  46. package/dist/completion/completionState.js +26 -26
  47. package/dist/completion/fileCompletionManager.js +6 -11
  48. package/dist/config/configCatalog.js +0 -2
  49. package/dist/config/timeZone.js +14 -0
  50. package/dist/config/yuiConfig.js +0 -29
  51. package/dist/context/runContextPack.js +2 -3
  52. package/dist/context/taskCatalog.js +3 -5
  53. package/dist/context/taskContext.js +139 -151
  54. package/dist/context/wakeRunReferences.js +11 -0
  55. package/dist/controller/agentCapabilities.js +58 -0
  56. package/dist/controller/agentRuntimeObserver.js +6 -11
  57. package/dist/controller/clientRuntime.js +22 -14
  58. package/dist/controller/controller.js +9 -22
  59. package/dist/controller/fileSchedulerStoreAdapter.js +117 -212
  60. package/dist/controller/jobClient.js +4 -4
  61. package/dist/controller/jobControl.js +60 -25
  62. package/dist/controller/jobSupervisor.js +4 -4
  63. package/dist/controller/providerRetryDelivery.js +7 -7
  64. package/dist/controller/runtime.js +51 -44
  65. package/dist/controller/runtimeEventProcessor.js +0 -5
  66. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  67. package/dist/controller/taskAgentError.js +35 -0
  68. package/dist/coordination/workMailboxQueue.js +2 -24
  69. package/dist/core/controllerClient.js +2 -2
  70. package/dist/core/fileLockOwner.js +74 -0
  71. package/dist/decision/decision.js +17 -0
  72. package/dist/doctor/doctor.js +18 -9
  73. package/dist/event/taskEvent.js +12 -0
  74. package/dist/execution/workItemExecutionProjection.js +2 -2
  75. package/dist/executor/agentCapabilityConfig.js +43 -0
  76. package/dist/executor/agentConfigurationCatalog.js +49 -11
  77. package/dist/executor/agentExecutor.js +5 -8
  78. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  79. package/dist/executor/taskAgentCapabilities.js +64 -0
  80. package/dist/integration/deliveryObligation.js +1 -69
  81. package/dist/integration/gitIntegrationService.js +60 -84
  82. package/dist/integration/integrationAttempt.js +3 -14
  83. package/dist/integration/integrationSourceApplication.js +4 -12
  84. package/dist/integration/manifestTags.js +2 -2
  85. package/dist/job/durableJob.js +4 -25
  86. package/dist/job/jobAssignmentScope.js +22 -0
  87. package/dist/job/jobOperation.js +16 -0
  88. package/dist/job/jobRunner.js +2 -1
  89. package/dist/job/stepDirectory.js +15 -0
  90. package/dist/kernel/builtinCapabilities.js +15 -11
  91. package/dist/kernel/kernelPorts.js +1 -17
  92. package/dist/lifecycle/exactRunTerminalization.js +1 -9
  93. package/dist/message/inputControlResolution.js +3 -4
  94. package/dist/message/message.js +17 -20
  95. package/dist/message/messageContinuation.js +1 -1
  96. package/dist/milestone/milestone.js +11 -0
  97. package/dist/observability/orchestrationMetrics.js +24 -1
  98. package/dist/observability/runtimeIdentity.js +1 -17
  99. package/dist/output/agentConfigurationPresentation.js +2 -0
  100. package/dist/output/timePresentation.js +1 -14
  101. package/dist/plugins/pluginService.js +12 -4
  102. package/dist/release/releaseWorkflowPorts.js +45 -96
  103. package/dist/release/runtimeRelease.js +9 -15
  104. package/dist/repository/projectMaintenanceLock.js +23 -64
  105. package/dist/repository/taskBaseFreshness.js +1 -1
  106. package/dist/repository/taskWorkspaceCoordinator.js +13 -22
  107. package/dist/repository/taskWorkspacePreparer.js +29 -25
  108. package/dist/repository/workspaceCleanupInspection.js +5 -5
  109. package/dist/resources/autoResourceGc.js +4 -77
  110. package/dist/resources/liveReferences.js +7 -54
  111. package/dist/resources/resourceDiscovery.js +5 -118
  112. package/dist/resources/resourceGc.js +233 -181
  113. package/dist/resources/resourceRegistrar.js +5 -4
  114. package/dist/resources/resourceRegistry.js +3 -37
  115. package/dist/resources/resourceRegistryStore.js +13 -36
  116. package/dist/resources/sqliteResourceRegistry.js +43 -26
  117. package/dist/review/deltaRecheck.js +1 -1
  118. package/dist/review/reviewAcceptance.js +1 -1
  119. package/dist/review/reviewDecision.js +1 -1
  120. package/dist/review/reviewRound.js +8 -7
  121. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  122. package/dist/runtime/acpProtocol.js +4 -51
  123. package/dist/runtime/acpSession.js +10 -75
  124. package/dist/runtime/acpSessionConfiguration.js +1 -8
  125. package/dist/runtime/agentEndpoint.js +3 -2
  126. package/dist/runtime/agentFailureContext.js +43 -0
  127. package/dist/runtime/agentHost.js +15 -15
  128. package/dist/runtime/codexInteractiveHost.js +2 -2
  129. package/dist/runtime/index.js +0 -1
  130. package/dist/runtime/managedCaller.js +21 -1
  131. package/dist/runtime/providerErrors.js +38 -0
  132. package/dist/runtime/runtimeObservation.js +0 -5
  133. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  134. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  135. package/dist/runtime/structuredProviderHost.js +4 -41
  136. package/dist/runtime/tmuxAdapters.js +6 -8
  137. package/dist/scheduler/activeRoleRunDelivery.js +7 -7
  138. package/dist/scheduler/leaderWakeupProcessor.js +19 -6
  139. package/dist/scheduler/operatorEvent.js +12 -20
  140. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  141. package/dist/scheduler/ports.js +5 -8
  142. package/dist/scheduler/roleRunLiveness.js +4 -4
  143. package/dist/scheduler/roleRunStall.js +16 -23
  144. package/dist/scheduler/taskExecutionProjection.js +25 -33
  145. package/dist/scheduler/taskObservabilityProjection.js +0 -1
  146. package/dist/scheduler/taskWake.js +5 -10
  147. package/dist/scheduler/wakeReason.js +2 -0
  148. package/dist/scheduler/wakeupQueue.js +2 -4
  149. package/dist/setup/setupCommand.js +1 -1
  150. package/dist/storage/contextRecords.js +106 -0
  151. package/dist/storage/currentTaskStore.js +1 -1
  152. package/dist/storage/homeLayout.js +13 -17
  153. package/dist/storage/migrations/agentFailureContext.js +22 -0
  154. package/dist/storage/migrations/currentInputContract.js +86 -0
  155. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  156. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  157. package/dist/storage/migrations/integrationContinuation.js +5 -4
  158. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  159. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  160. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  161. package/dist/storage/migrations/verificationPolicy.js +74 -0
  162. package/dist/storage/migrations/workItemHistory.js +46 -0
  163. package/dist/storage/persistenceWorker.js +12 -4
  164. package/dist/storage/recordValidation.js +87 -0
  165. package/dist/storage/sqliteSchema.js +142 -1
  166. package/dist/storage/sqliteStore.js +156 -162
  167. package/dist/storage/storageSchema.js +2 -14
  168. package/dist/storage/storageVersions.js +1 -1
  169. package/dist/storage/storeRpc.js +15 -7
  170. package/dist/storage/taskStore.js +3 -7
  171. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  172. package/dist/task/completionReadiness.js +5 -24
  173. package/dist/task/draftPlan.js +0 -53
  174. package/dist/task/nextAction.js +7 -13
  175. package/dist/task/remoteDeliveryService.js +89 -0
  176. package/dist/task/taskActivation.js +2 -25
  177. package/dist/task/taskActivationService.js +0 -2
  178. package/dist/task/taskRecordReference.js +0 -1
  179. package/dist/task/taskSubmission.js +3 -14
  180. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  181. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  182. package/dist/telemetry/telemetryWiring.js +4 -3
  183. package/dist/tmux/tmuxManager.js +29 -20
  184. package/dist/verification/gateArtifact.js +15 -24
  185. package/dist/verification/gateArtifactStore.js +14 -7
  186. package/dist/verification/verificationGateService.js +29 -88
  187. package/dist/verification/verificationPlan.js +27 -72
  188. package/dist/web/assets/client/app.js +1 -1
  189. package/dist/web/assets/client/taskSurface.js +4 -4
  190. package/dist/web/webServer.js +4 -6
  191. package/dist/web/webSnapshot.js +3 -83
  192. package/dist/web/webTaskSurface.js +17 -46
  193. package/dist/workItem/workItem.js +1 -12
  194. package/docs/architecture/capabilities-and-resources.md +14 -2
  195. package/docs/architecture/capabilities-and-resources.zh-CN.md +11 -1
  196. package/docs/managed-turn-and-session-runtime.md +83 -10
  197. package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -11
  198. package/docs/plugin-sdk.md +4 -2
  199. package/docs/provider-runtime.md +3 -1
  200. package/docs/provider-runtime.zh-CN.md +4 -2
  201. package/docs/release-workflow.md +190 -7
  202. package/docs/release-workflow.zh-CN.md +139 -5
  203. package/docs/roles-and-configuration.md +29 -0
  204. package/docs/roles-and-configuration.zh-CN.md +21 -0
  205. package/docs/sqlite-control-plane-design.md +16 -3
  206. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  207. package/docs/task-dag-semantics.md +3 -3
  208. package/docs/task-dag-semantics.zh-CN.md +2 -2
  209. package/docs/task-delivery.md +91 -5
  210. package/docs/task-delivery.zh-CN.md +72 -4
  211. package/docs/task-discovery.md +10 -11
  212. package/docs/task-discovery.zh-CN.md +8 -9
  213. package/docs/testing/verification-levels.md +145 -9
  214. package/docs/testing/verification-levels.zh-CN.md +103 -8
  215. package/package.json +1 -1
  216. package/skills/yui-leader/SKILL.md +5 -0
  217. package/skills/yui-leader/references/integration.md +9 -5
  218. package/skills/yui-leader/references/planning.md +9 -3
  219. package/skills/yui-operator/SKILL.md +15 -10
  220. package/skills/yui-runtime/SKILL.md +6 -6
  221. package/skills/yui-runtime/references/recovery.md +47 -0
  222. package/dist/agent/agentRegistry.js +0 -10
  223. package/dist/agentRun/runIdentity.js +0 -22
  224. package/dist/commands/deliveryGuardPreflight.js +0 -30
  225. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  226. package/dist/commands/taskOverviewCommand.js +0 -377
  227. package/dist/completion/completionWizard.js +0 -125
  228. package/dist/context/dispatchContext.js +0 -110
  229. package/dist/context/wakeNotification.js +0 -144
  230. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  231. package/dist/integration/integrationQueueEntry.js +0 -191
  232. package/dist/integration/integrationQueueService.js +0 -810
  233. package/dist/release/fakeReleasePorts.js +0 -55
  234. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  235. package/dist/task/deliveryGuard.js +0 -226
  236. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { resolveJobCaller } from "../commands/taskActor.js";
1
+ import { resolveJobCaller } from "../task/taskAuthority.js";
2
2
  import { callFileTaskController } from "./clientRuntime.js";
3
3
  export async function startDurableJob(home, params, clientOptions = {}) {
4
4
  // rr6/f2: Route through callFileTaskController so an Integration-issued
@@ -7,8 +7,8 @@ export async function startDurableJob(home, params, clientOptions = {}) {
7
7
  const result = await callFileTaskController(home, "job.start", params, clientOptions);
8
8
  return parseStartResult(result);
9
9
  }
10
- export async function getDurableJob(home, taskId, jobId, clientOptions = {}) {
11
- const result = await callFileTaskController(home, "job.get", { taskId, jobId }, clientOptions);
10
+ export async function getDurableJob(home, taskId, jobId, caller, clientOptions = {}) {
11
+ const result = await callFileTaskController(home, "job.get", { taskId, jobId, caller }, clientOptions);
12
12
  return parseJobResult(result);
13
13
  }
14
14
  export async function acknowledgeDurableJob(home, taskId, jobId, caller, clientOptions = {}) {
@@ -89,7 +89,7 @@ export function createControllerIntegrationJobPort(home, clientOptions = {}) {
89
89
  return job;
90
90
  },
91
91
  async getJob(taskId, jobId) {
92
- return getDurableJob(home, taskId, jobId, clientOptions);
92
+ return getDurableJob(home, taskId, jobId, resolveJobCaller(clientOptions.environment, taskId), clientOptions);
93
93
  },
94
94
  async cancelJob(taskId, jobId) {
95
95
  // rr8/rr12: Bind the cancel request to the caller's managed identity.
@@ -3,18 +3,22 @@
3
3
  * record is created or cancel-requested. The Controller socket layer calls
4
4
  * this port for `job.*` requests; nothing else writes queued jobs.
5
5
  *
6
- * Creation is idempotent per (owner, project, head, steps, workspace, env):
7
- * a repeated `job.start` with the same inputs returns the existing job with
8
- * `created: false`, so a Leader retry can never spawn duplicate runners.
6
+ * Creation is idempotent per authenticated actor and explicit request ID.
7
+ * Repeating that request with identical inputs returns the original Job;
8
+ * changing its input is a conflict. Integration owner identity additionally
9
+ * preserves its admitted Job across authorized Session replacement.
9
10
  */
10
11
  import { execSync } from "node:child_process";
11
12
  import { createHash } from "node:crypto";
12
13
  import { resolve, sep } from "node:path";
13
- import { acknowledgeUnknownDurableJob, createDurableJob, durableJobIdempotencyKey, isDurableJobTerminal, requestDurableJobCancel, retryDurableJobIdempotencyKey } from "../job/durableJob.js";
14
14
  import { activeLiveRoleAgentSession } from "../executor/agentExecutor.js";
15
+ import { acknowledgeUnknownDurableJob, createDurableJob, durableJobIdempotencyKey, isDurableJobTerminal, requestDurableJobCancel, retryDurableJobIdempotencyKey } from "../job/durableJob.js";
15
16
  import { CallAuthority } from "../kernel/callAuthority.js";
16
17
  import { redactLaunchText } from "../runtime/launchDiagnostics.js";
17
- import { requireManagedTaskCaller } from "../runtime/managedCaller.js";
18
+ import { requireManagedTaskCaller, ManagedRuntimeDriftError } from "../runtime/managedCaller.js";
19
+ import { CliError } from "../errors/cliError.js";
20
+ import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAssignmentScope.js";
21
+ import { assertContextRecordReadable } from "../context/taskContext.js";
18
22
  export function createDurableJobControl(store) {
19
23
  const authority = createJobCallAuthority(store);
20
24
  return {
@@ -25,6 +29,7 @@ export function createDurableJobControl(store) {
25
29
  // startJob with the same key create a duplicate job.
26
30
  return store.transaction((tx) => {
27
31
  const context = authority.authenticate(Object.freeze({ ...params.caller }), params.taskId);
32
+ requireJobAssignment(tx, params, params.caller);
28
33
  assertNonSecretJobInput(params);
29
34
  const baseKey = durableJobIdempotencyKey({
30
35
  owner: params.owner,
@@ -37,8 +42,7 @@ export function createDurableJobControl(store) {
37
42
  const inputDigest = params.retryOf === undefined
38
43
  ? baseKey
39
44
  : retryDurableJobIdempotencyKey(baseKey, params.retryOf);
40
- const requestId = params.requestId === undefined ? inputDigest
41
- : requiredId(params.requestId, "job.start requestId");
45
+ const requestId = requiredId(params.requestId, "job.start requestId");
42
46
  // An IntegrationAttempt already is a durable operation identity.
43
47
  // Recovery by another authorized Role must find its original Job,
44
48
  // including the window before Integration persisted the returned id.
@@ -68,12 +72,6 @@ export function createDurableJobControl(store) {
68
72
  }
69
73
  return { job: existing, created: false };
70
74
  }
71
- // A historical content-addressed request has no attributable caller.
72
- // Do not silently execute it again under a newly attributed identity.
73
- const historical = tx.findDurableJobByIdempotencyKey(params.taskId, inputDigest);
74
- if (params.requestId === undefined && historical !== null) {
75
- throw jobDomainError(`Historical request already exists: ${historical.id}; inspect it or select an explicit new requestId.`);
76
- }
77
75
  authority.authorize(context, params.taskId);
78
76
  validateStartParams(tx, params);
79
77
  const id = tx.nextDurableJobId(params.taskId);
@@ -97,7 +95,21 @@ export function createDurableJobControl(store) {
97
95
  return { job, created: true };
98
96
  });
99
97
  },
100
- getJob(taskId, jobId) {
98
+ getJob(taskId, jobId, caller) {
99
+ if (caller === undefined)
100
+ throw jobControlError("UNAUTHORIZED", "Job reads require an explicit caller.");
101
+ try {
102
+ assertContextRecordReadable(store, taskId, "job", jobId, caller.scope === "user" ? {} : {
103
+ YUI_SESSION_SCOPE: caller.scope, YUI_TASK_ID: caller.taskId, YUI_ROLE: caller.role,
104
+ YUI_AGENT_ID: caller.agentId, YUI_ADAPTER_ID: caller.adapterId, YUI_NATIVE_SESSION_ID: caller.nativeSessionId
105
+ });
106
+ }
107
+ catch (error) {
108
+ if (error instanceof ManagedRuntimeDriftError || (error instanceof CliError && error.code === "USAGE_ERROR")) {
109
+ throw jobControlError("UNAUTHORIZED", error.message);
110
+ }
111
+ throw error;
112
+ }
101
113
  return store.getDurableJob(taskId, jobId);
102
114
  },
103
115
  cancelJob(taskId, jobId, now, caller) {
@@ -108,6 +120,7 @@ export function createDurableJobControl(store) {
108
120
  // rr8: Bind the cancel request to the caller's managed identity. The
109
121
  // same rules as job.start apply, checked against the job's owner.
110
122
  assertCallerAuthorized(tx, caller, taskId);
123
+ requireJobAssignment(tx, current, caller);
111
124
  const next = requestDurableJobCancel(current, now);
112
125
  if (next !== current)
113
126
  tx.saveDurableJob(taskId, next);
@@ -120,6 +133,7 @@ export function createDurableJobControl(store) {
120
133
  if (current === null)
121
134
  return null;
122
135
  assertCallerAuthorized(tx, caller, taskId);
136
+ requireJobAssignment(tx, current, caller);
123
137
  const next = acknowledgeUnknownDurableJob(current, now);
124
138
  if (next !== current)
125
139
  tx.saveDurableJob(taskId, next);
@@ -152,8 +166,19 @@ export function authorizeJobStart(store, job) {
152
166
  if (jobAuthorityBinding(store, scope, role, job.taskId) !== job.operation.authorityRef) {
153
167
  throw jobDomainError("Job caller binding was revoked; no execution was started.");
154
168
  }
169
+ requireJobAssignment(store, job, { scope, role });
155
170
  validateJobTarget(store, job);
156
171
  }
172
+ function requireJobAssignment(store, target, caller) {
173
+ try {
174
+ assertJobAssignmentScope(store, target, caller);
175
+ }
176
+ catch (error) {
177
+ if (error instanceof JobAssignmentScopeError)
178
+ throw jobControlError("UNAUTHORIZED", error.message);
179
+ throw error;
180
+ }
181
+ }
157
182
  function jobAuthorityBinding(store, scope, roleName, taskId) {
158
183
  // A Host detach/reattach preserves the native Session and its queued work.
159
184
  // Authenticate the live launch at ingress, but bind accepted Jobs to the
@@ -328,8 +353,8 @@ function validateJobTarget(store, params) {
328
353
  * - `task` + mismatched taskId: rejected.
329
354
  * - A non-Leader Task caller requires its current active Assignment.
330
355
  * - A Leader Task caller requires its current, unrevoked native Session.
331
- * - `task`: authority inside the matching Task after current Session and
332
- * native Session verification; Role does not narrow it.
356
+ * - Non-Leader Task callers additionally pass the shared exact Assignment
357
+ * check for every Job operation and before a queued Job starts.
333
358
  */
334
359
  /**
335
360
  * Verify the caller's current native Session and Task scope. This local Home
@@ -470,28 +495,26 @@ export function parseDurableJobStartParams(value) {
470
495
  env,
471
496
  steps,
472
497
  caller,
473
- ...(record.requestId === undefined ? {} : {
474
- requestId: requiredId(record.requestId, "job.start requestId")
475
- }),
498
+ requestId: requiredId(record.requestId, "job.start requestId"),
476
499
  ...(retryOf === undefined ? {} : { retryOf })
477
500
  };
478
501
  }
479
502
  export function parseDurableJobRefParams(value) {
480
503
  if (typeof value !== "object" || value === null || Array.isArray(value)
481
- || Object.keys(value).length !== 2) {
504
+ || Object.keys(value).length !== 3) {
482
505
  throw jobControlError("INVALID_PARAMS", "DurableJob ref params are invalid.");
483
506
  }
484
507
  const record = value;
485
508
  return {
486
509
  taskId: requiredId(record.taskId, "DurableJob taskId"),
487
- jobId: requiredId(record.jobId, "DurableJob jobId")
510
+ jobId: requiredId(record.jobId, "DurableJob jobId"),
511
+ caller: parseCaller(record.caller)
488
512
  };
489
513
  }
490
514
  /**
491
515
  * rr8: `job.cancel` params carry the caller identity so the Controller can
492
- * bind the cancel request to the caller's managed scope. Distinct from
493
- * `parseDurableJobRefParams` (used by `job.get`) because cancel requires the
494
- * third `caller` key.
516
+ * bind the cancel request to the caller's managed scope. Reads also carry
517
+ * an explicit caller but use Context's read authority rather than write authority.
495
518
  */
496
519
  export function parseDurableJobCancelParams(value) {
497
520
  if (typeof value !== "object" || value === null || Array.isArray(value)
@@ -551,7 +574,7 @@ function parseSteps(value) {
551
574
  throw jobControlError("INVALID_PARAMS", "job.start steps are invalid.");
552
575
  }
553
576
  const record = entry;
554
- const allowed = new Set(["name", "command", "timeoutMs"]);
577
+ const allowed = new Set(["name", "command", "argv", "cwd", "env", "timeoutMs"]);
555
578
  for (const key of Object.keys(record)) {
556
579
  if (!allowed.has(key)) {
557
580
  throw jobControlError("INVALID_PARAMS", "job.start steps are invalid.");
@@ -559,6 +582,15 @@ function parseSteps(value) {
559
582
  }
560
583
  const name = requiredId(record.name, "job.start step name");
561
584
  const command = requiredId(record.command, "job.start step command");
585
+ const argv = record.argv === undefined ? undefined : record.argv;
586
+ if (argv !== undefined && (!Array.isArray(argv) || argv.length === 0
587
+ || argv.some(value => typeof value !== "string" || value.length === 0 || value.includes("\0")))) {
588
+ throw jobControlError("INVALID_PARAMS", `job.start step argv is invalid: ${name}.`);
589
+ }
590
+ const cwd = record.cwd === undefined ? undefined : requiredId(record.cwd, "job.start step cwd");
591
+ if (cwd !== undefined && !cwd.startsWith("/")) {
592
+ throw jobControlError("INVALID_PARAMS", `job.start step cwd must be absolute: ${name}.`);
593
+ }
562
594
  if (names.has(name)) {
563
595
  throw jobControlError("INVALID_PARAMS", `job.start step names must be unique: ${name}.`);
564
596
  }
@@ -572,6 +604,9 @@ function parseSteps(value) {
572
604
  const step = {
573
605
  name,
574
606
  command,
607
+ ...(argv === undefined ? {} : { argv: argv }),
608
+ ...(cwd === undefined ? {} : { cwd }),
609
+ ...(record.env === undefined ? {} : { env: parseStringMap(record.env, "job.start step env") }),
575
610
  ...(record.timeoutMs === undefined ? {} : { timeoutMs: record.timeoutMs })
576
611
  };
577
612
  steps.push(step);
@@ -12,14 +12,14 @@
12
12
  * cancelRequestedAt -> write cancel fence + SIGTERM
13
13
  */
14
14
  import { spawn } from "node:child_process";
15
- import { chmodSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, statSync } from "node:fs";
15
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
- import { writeTextFileAtomically } from "../storage/durableFile.js";
19
18
  import { cancelQueuedDurableJob, completeDurableJob, isDurableJobTerminal, markDurableJobUnknown, markDurableJobWakeupNotified, rejectQueuedDurableJob, startDurableJob, touchDurableJobHeartbeat } from "../job/durableJob.js";
20
- import { readLinuxProcessStartIdentity } from "./domainIdentity.js";
21
- import { wakeReason } from "../scheduler/wakeReason.js";
22
19
  import { recordOperationEvidence } from "../kernel/operationFacts.js";
20
+ import { wakeReason } from "../scheduler/wakeReason.js";
21
+ import { writeTextFileAtomically } from "../storage/durableFile.js";
22
+ import { readLinuxProcessStartIdentity } from "./domainIdentity.js";
23
23
  const DEFAULT_STEP_TIMEOUT_MS = 30 * 60_000;
24
24
  const HEARTBEAT_STALE_MS = 2 * 60_000;
25
25
  const SIGKILL_GRACE_MS = 30_000;
@@ -1,14 +1,14 @@
1
1
  import { performance } from "node:perf_hooks";
2
2
  import { runTaskCommand } from "../commands/taskCommands.js";
3
- import { retryIntentBlocker } from "./providerRetryAdmission.js";
4
- import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
5
- import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
6
- import { runtimeLifecycleTarget, hasRuntimeCleanupObligation } from "../runtime/lifecycleReservation.js";
7
- import { sendAgentHostRunControl, inspectAgentHostSocket, agentHostControlSocketPath, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
8
- import { redactAgentErrorText } from "../runtime/agentError.js";
9
- import { createTaskEvent } from "../event/taskEvent.js";
10
3
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
4
+ import { createTaskEvent } from "../event/taskEvent.js";
11
5
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
6
+ import { redactAgentErrorText } from "../runtime/agentError.js";
7
+ import { AGENT_HOST_CONTROL_PROTOCOL, agentHostControlSocketPath, inspectAgentHostSocket, sendAgentHostRunControl } from "../runtime/agentHost.js";
8
+ import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
9
+ import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
10
+ import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
11
+ import { retryIntentBlocker } from "./providerRetryAdmission.js";
12
12
  /** Hooks in the existing Controller pass and nearest-deadline timer. SQLite's
13
13
  * current Provider input is the only pending intent; nothing is queued here. */
14
14
  export function createProviderRetryHooks(home, store, ports = {}) {
@@ -1,59 +1,60 @@
1
- import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
2
1
  import { isDeepStrictEqual } from "node:util";
3
- import { controllerSocketPath } from "../core/controllerEndpoint.js";
4
2
  import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
5
- import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
6
- import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
7
- import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
8
- import { roleSessionMayContinue, effectiveLaunchConfig, resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
3
+ import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
4
+ import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
5
+ import { controllerSocketPath } from "../core/controllerEndpoint.js";
6
+ import { createTaskEvent } from "../event/taskEvent.js";
9
7
  import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "../executor/agentConfigurationCatalog.js";
10
- import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
8
+ import { activeLiveRoleAgentSession, roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
9
+ import { effectiveLaunchConfig, resolveEffectiveLaunch, roleSessionMayContinue } from "../executor/effectiveLaunch.js";
10
+ import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
11
11
  import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
12
+ import { createKernelPorts } from "../kernel/kernelPorts.js";
13
+ import { createGlobalRoleMessage } from "../message/message.js";
14
+ import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
15
+ import { createResourceAutoGc } from "../resources/autoResourceGc.js";
16
+ import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
17
+ import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
18
+ import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExitStore.js";
19
+ import { AgentHostPromptPushAdapter, FileTaskRuntimeIsolation, ProviderContinuationReconciliationService, TmuxSessionHost } from "../runtime/index.js";
20
+ import { launchBrokerForHome } from "../runtime/launchBroker.js";
21
+ import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
22
+ import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
23
+ import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
24
+ import { providerRetryPrompt } from "../runtime/providerRetry.js";
25
+ import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
12
26
  import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
13
27
  import { managedRuntimeRoot } from "../storage/homeLayout.js";
14
28
  import { SqliteTaskStore } from "../storage/sqliteStore.js";
15
29
  import { AsyncTaskStoreClient, resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
16
- import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
30
+ import { SurfaceContributions } from "../surface/surfaceContributions.js";
31
+ import { taskOwnsManagedWorkspace } from "../task/task.js";
32
+ import { openSchedulerTelemetry } from "../telemetry/telemetryWiring.js";
17
33
  import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
18
34
  import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
19
- import { AgentHostPromptPushAdapter, FileTaskRuntimeIsolation, TmuxSessionHost, ProviderContinuationReconciliationService } from "../runtime/index.js";
20
- import { startFileTaskController } from "./controller.js";
21
- import { AgentHostProviderTurnFenceError, AgentHostProviderSessionBusyError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
22
- import { openSchedulerTelemetry } from "../telemetry/telemetryWiring.js";
23
- import { createFileArtifactPort, createLinuxProcessPort, DurableJobSupervisor } from "./jobSupervisor.js";
24
- import { authorizeJobStart } from "./jobControl.js";
25
- import { createKernelPorts } from "../kernel/kernelPorts.js";
26
- import { createCapabilityDispatcher } from "./capabilityBridge.js";
27
35
  import { createControllerWeb } from "../web/controllerWeb.js";
28
- import { createWebTaskSurface } from "../web/webTaskSurface.js";
29
- import { SurfaceContributions } from "../surface/surfaceContributions.js";
30
36
  import { TmuxWebTerminalService } from "../web/tmuxWebTerminal.js";
31
- import { FileTaskWorkflowRuntime } from "./clientRuntime.js";
32
- import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
37
+ import { createWebTaskSurface } from "../web/webTaskSurface.js";
38
+ import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
33
39
  import { AgentRuntimeObserver } from "./agentRuntimeObserver.js";
34
- import { AsyncRuntimeEventProcessor, FileRuntimeEventProcessor, createAsyncRuntimeObserver } from "./runtimeEventProcessor.js";
35
- import { RuntimeLaunchCoordinator } from "./runtimeLaunchCoordinator.js";
40
+ import { createCapabilityDispatcher } from "./capabilityBridge.js";
41
+ import { FileTaskWorkflowRuntime } from "./clientRuntime.js";
42
+ import { startFileTaskController } from "./controller.js";
36
43
  import { ephemeralDomainFromEnvironment, recordEphemeralTmuxTarget } from "./domainIdentity.js";
37
44
  import { createEphemeralResourceReaper } from "./ephemeralResourceReaper.js";
45
+ import { AgentHostProviderSessionBusyError, AgentHostProviderTurnFenceError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
46
+ import { deliverGlobalInputs } from "./globalInputDelivery.js";
47
+ import { authorizeJobStart } from "./jobControl.js";
48
+ import { createFileArtifactPort, createLinuxProcessPort, DurableJobSupervisor } from "./jobSupervisor.js";
49
+ import { createProviderRetryHooks } from "./providerRetryDelivery.js";
50
+ import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
38
51
  import { scanControllerResourceInventory } from "./resourceInventoryLinux.js";
39
52
  import { ResourceInventoryClient } from "./resourceInventoryRpc.js";
40
- import { createResourceAutoGc } from "../resources/autoResourceGc.js";
41
- import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
53
+ import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
54
+ import { AsyncRuntimeEventProcessor, createAsyncRuntimeObserver, FileRuntimeEventProcessor } from "./runtimeEventProcessor.js";
55
+ import { RuntimeLaunchCoordinator } from "./runtimeLaunchCoordinator.js";
42
56
  import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
43
- import { launchBrokerForHome } from "../runtime/launchBroker.js";
44
- import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
45
- import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
46
- import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
47
- import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExitStore.js";
48
- import { deliverGlobalInputs } from "./globalInputDelivery.js";
49
- import { providerRetryPrompt } from "../runtime/providerRetry.js";
50
- import { createProviderRetryHooks } from "./providerRetryDelivery.js";
51
- import { createGlobalRoleMessage } from "../message/message.js";
52
- import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
53
- import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
54
- import { createTaskEvent } from "../event/taskEvent.js";
55
- import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
56
- import { taskOwnsManagedWorkspace } from "../task/task.js";
57
+ import { readTaskAgentCapabilities } from "./agentCapabilities.js";
57
58
  /** Production composition root for the current SQLite TaskStore + tmux Controller. */
58
59
  export async function startFileTaskControllerRuntime(home, options = {}) {
59
60
  // The current Home always uses SQLite. The persistence worker is enabled by
@@ -86,9 +87,13 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
86
87
  : undefined;
87
88
  let closeKernel = async () => { };
88
89
  let closeWeb = async () => { };
90
+ let closeTelemetry = async () => { };
89
91
  try {
92
+ const telemetry = options.schedulerStore === undefined
93
+ ? openSchedulerTelemetry(home, store.getConfig(), asyncStoreClient) : null;
94
+ closeTelemetry = () => telemetry?.sink.close() ?? Promise.resolve();
90
95
  const schedulerStore = options.schedulerStore
91
- ?? new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home, store.getConfig()));
96
+ ?? new FileSchedulerStoreAdapter(store, telemetry);
92
97
  const domainIdentity = options.domainIdentity
93
98
  ?? ephemeralDomainFromEnvironment(options.environment ?? process.env);
94
99
  const planner = options.planner ?? new FileRoleLaunchPlanner(home, store, {
@@ -387,7 +392,6 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
387
392
  environment: options.environment ?? process.env, onError: options.onError
388
393
  });
389
394
  const webSurface = createWebTaskSurface(store, { runtime: {
390
- notifyStateChanged: (taskId) => runningRuntime?.signal(`task:${taskId}`),
391
395
  notifyMailboxChanged: (target) => {
392
396
  if (target.kind === "role")
393
397
  runningRuntime?.signal(`role:${target.taskId}/${target.roleName}`);
@@ -454,8 +458,10 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
454
458
  const continuationReconciler = options.continuationMetadata === undefined
455
459
  ? undefined
456
460
  : new ProviderContinuationReconciliationService(store, schedulerStore, options.continuationMetadata);
457
- const running = await startFileTaskController(home, schedulerStore, delivery, (method, params) => method === "web.start" || method === "web.stop" || method === "web.status"
458
- ? web.dispatch(method, params) : lifecycleDispatcher(method, params), {
461
+ const running = await startFileTaskController(home, schedulerStore, delivery, (method, params) => method === "task.role-capabilities"
462
+ ? readTaskAgentCapabilities(store, catalogs, params)
463
+ : method === "web.start" || method === "web.stop" || method === "web.status"
464
+ ? web.dispatch(method, params) : lifecycleDispatcher(method, params), {
459
465
  intervalMs: options.intervalMs
460
466
  ?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
461
467
  signalWindowMs: options.signalWindowMs,
@@ -543,12 +549,12 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
543
549
  }
544
550
  let resourceClose;
545
551
  const closeResources = () => {
546
- resourceClose ??= Promise.all([
552
+ resourceClose ??= closeTelemetry().finally(() => Promise.all([
547
553
  web.close(),
548
554
  kernel.close(),
549
555
  asyncStoreClient?.close() ?? Promise.resolve(),
550
556
  inventoryClient?.close() ?? Promise.resolve()
551
- ]).then(() => undefined).finally(() => {
557
+ ])).then(() => undefined).finally(() => {
552
558
  ownedStore?.close();
553
559
  });
554
560
  return resourceClose;
@@ -583,6 +589,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
583
589
  catch (error) {
584
590
  // The socket may never have opened. Startup failure must still release
585
591
  // this attempt's workers/instances instead of leaving an uncallable process.
592
+ await closeTelemetry().catch(() => undefined);
586
593
  await Promise.allSettled([
587
594
  closeWeb(),
588
595
  closeKernel(),
@@ -1,18 +1,15 @@
1
1
  import { isRuntimeTokenEvidence } from "../runtime/runtimeObservation.js";
2
- import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
3
2
  const DEFAULT_MAX_RUNTIME_EVENTS_PER_DRAIN = 64;
4
3
  /** Folds immutable Hook facts in one bounded transaction before acknowledging them. */
5
4
  export class FileRuntimeEventProcessor {
6
5
  inbox;
7
6
  observer;
8
7
  options;
9
- drivers;
10
8
  drainLaneCursor = 0;
11
9
  constructor(inbox, observer, options = {}) {
12
10
  this.inbox = inbox;
13
11
  this.observer = observer;
14
12
  this.options = options;
15
- this.drivers = options.drivers ?? builtinAgentDriverRegistry();
16
13
  }
17
14
  drain(now) {
18
15
  const acknowledgedEventIds = [];
@@ -540,13 +537,11 @@ export class AsyncRuntimeEventProcessor {
540
537
  inbox;
541
538
  observer;
542
539
  options;
543
- drivers;
544
540
  drainLaneCursor = 0;
545
541
  constructor(inbox, observer, options = {}) {
546
542
  this.inbox = inbox;
547
543
  this.observer = observer;
548
544
  this.options = options;
549
- this.drivers = options.drivers ?? builtinAgentDriverRegistry();
550
545
  }
551
546
  async drainAsync(now) {
552
547
  const acknowledgedEventIds = [];
@@ -132,6 +132,11 @@ export class SessionOwnerReconciliation {
132
132
  }
133
133
  const result = await terminateSessionOwners(owner, records, this.#terminationPorts(), options);
134
134
  if (result.outcome === "stop-confirmed") {
135
+ // A signalled Host exits into a retained tmux pane. Explicit release
136
+ // removes that window too, after physical exit proof, not on observation.
137
+ if (records.length > 0) {
138
+ this.#tmux?.killRole(owner.scope === "task" ? owner.taskId : "operator", owner.roleName);
139
+ }
135
140
  for (const record of result.confirmed) {
136
141
  this.#store.removeSessionOwner(sessionOwnerProcessKey(record));
137
142
  }
@@ -0,0 +1,35 @@
1
+ import { createTaskEvent } from "../event/taskEvent.js";
2
+ import { redactAgentErrorText } from "../runtime/agentError.js";
3
+ import { captureAgentFailureContext } from "../runtime/agentFailureContext.js";
4
+ /** One persistence boundary. Ingress owns Provider classification, execution
5
+ * fences and routing; this writer never invents an execution or recovery plan. */
6
+ export function recordTaskAgentError(store, input, now) {
7
+ return store.transaction(tx => {
8
+ const error = input.error;
9
+ const payload = {
10
+ ...Object.fromEntries(Object.entries(input.evidence ?? {}).filter((entry) => typeof entry[1] === "string" && (entry[0] === "lastOutput" || entry[1].trim().length > 0))),
11
+ sourceEventId: input.sourceEventId, roleName: input.roleName,
12
+ agentId: input.agentId, adapterId: input.adapterId, driverId: input.driverId,
13
+ source: error.source, phase: error.phase, category: error.category, code: error.code,
14
+ message: redactAgentErrorText(error.message), raw: redactAgentErrorText(error.raw),
15
+ inputDisposition: error.inputDisposition, sessionDisposition: error.sessionDisposition,
16
+ ...(error.retryAfterMs === undefined ? {} : { retryAfterMs: String(error.retryAfterMs) })
17
+ };
18
+ const identity = ["sourceEventId", "roleName", "agentId", "adapterId", "driverId",
19
+ "source", "phase", "category", "code", "inputDisposition", "sessionDisposition",
20
+ "runId", "attemptId", "nativeSessionId", "nativeTurnId", "observationEventId",
21
+ "message", "errorName", "causeName", "hostState", "registrationDisposition",
22
+ "retryAfterMs", "lastOutput", ...(payload.attemptId === undefined ? ["raw"] : [])];
23
+ const duplicate = [...tx.listEventsByType(input.taskId, ["runtime.agent-error"])].reverse().find(event => identity.every(key => event.payload[key] === payload[key]));
24
+ // Keep the first original raw cause/configuration for a fixed failure.
25
+ // Changed normalized facts are distinct evidence, not a replay.
26
+ if (duplicate !== undefined)
27
+ return { event: duplicate, created: false };
28
+ const event = createTaskEvent(tx.nextEventId(input.taskId), input.taskId, "runtime.agent-error", {
29
+ ...payload,
30
+ capabilityContext: captureAgentFailureContext(input.effective, input.effective === undefined ? null : tx.getConfiguredAgent(input.effective.agentId))
31
+ }, now);
32
+ tx.saveEvent(input.taskId, event);
33
+ return { event, created: true };
34
+ });
35
+ }
@@ -1,18 +1,11 @@
1
1
  import { completeProcessing, consumePendingBatch, createWorkMailbox, enqueueSignal, mailboxBatches, mailboxEntityRefKey } from "./workMailbox.js";
2
- const LEGACY_ROLE_RUN_DISPATCH_REASONS = new Set([
3
- "turn-dispatched",
4
- "turn-retried",
5
- "review-requested",
6
- "workitem-synthesis-ready",
7
- "review-synthesis-ready"
8
- ]);
9
2
  /** Atomically useful when called inside the caller's TaskStore transaction. */
10
3
  export function enqueueWork(store, target, reason, occurredAt, refs = [], metadata = {}) {
11
4
  const mailbox = store.getWorkMailbox(target) ?? createWorkMailbox(target);
12
5
  // Archive is an admission boundary, not an acknowledgement of old input.
13
6
  // Runtime cleanup mailboxes remain usable for exact-owner recovery.
14
7
  if ((target.kind === "task" || target.kind === "role")
15
- && store.getTask?.(target.taskId)?.status === "archived")
8
+ && store.getTask(target.taskId)?.status === "archived")
16
9
  return mailbox;
17
10
  const queued = enqueueSignal(mailbox, {
18
11
  reason,
@@ -79,26 +72,11 @@ export function captureRoleRunDispatch(mailbox, input) {
79
72
  };
80
73
  }
81
74
  }
82
- // Valid earlier dispatches used sequence-generated dedupe keys and could
83
- // include a companion WorkItem ref. Consume that complete legacy batch once
84
- // the exact AgentRun reaches an accepted or terminal boundary.
85
- if (pending.requestCount === 1
86
- && pending.sources.length === 1
87
- && pending.sources[0] === "yui"
88
- && pending.reasons.some((reason) => LEGACY_ROLE_RUN_DISPATCH_REASONS.has(reason))
89
- && pending.refs.some((ref) => (mailboxEntityRefKey(ref) === mailboxEntityRefKey(exactRef)))) {
90
- return {
91
- kind: "pending",
92
- fromSequence: pending.fromSequence,
93
- toSequence: pending.toSequence
94
- };
95
- }
96
75
  return null;
97
76
  }
98
77
  /**
99
78
  * Settles one exact ordinary Role AgentRun dispatch. Provider acceptance is the
100
- * normal boundary; terminalization calls the same operation for conclusively
101
- * unaccepted and valid earlier dispatches.
79
+ * normal boundary; terminalization uses the same exact dispatch identity.
102
80
  */
103
81
  export function settleRoleRunDispatch(store, input, expected) {
104
82
  if (expected === null)
@@ -2,10 +2,10 @@ import { randomUUID } from "node:crypto";
2
2
  import { lstat, readFile } from "node:fs/promises";
3
3
  import { createConnection } from "node:net";
4
4
  import { join } from "node:path";
5
- import { CONTROLLER_DISCOVERY_PATH, FILE_TASK_CONTROLLER_PROTOCOL_VERSION, MAX_CONTROLLER_MESSAGE_BYTES, ControllerProtocolError, encodeControllerRequest, parseControllerDiscovery, parseControllerResponse } from "./protocol.js";
6
5
  import { isControllerSocketPathForHome } from "./controllerEndpoint.js";
7
- import { readHomeFilesystemId } from "./homeFilesystemIdentity.js";
8
6
  import { findLiveControllerProcessForHome, inspectLiveControllerProcess } from "./controllerProcessIdentity.js";
7
+ import { readHomeFilesystemId } from "./homeFilesystemIdentity.js";
8
+ import { CONTROLLER_DISCOVERY_PATH, ControllerProtocolError, encodeControllerRequest, FILE_TASK_CONTROLLER_PROTOCOL_VERSION, MAX_CONTROLLER_MESSAGE_BYTES, parseControllerDiscovery, parseControllerResponse } from "./protocol.js";
9
9
  export class ControllerClientError extends Error {
10
10
  code;
11
11
  constructor(code, message) {