@zq-silk/yui 0.16.1 → 1.0.0-alpha

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 (193) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +9 -4
  4. package/dist/agent/agent.js +4 -9
  5. package/dist/agent/executionComponents.js +4 -4
  6. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  7. package/dist/agentRun/agentRun.js +13 -23
  8. package/dist/artifacts/managedGit.js +3 -56
  9. package/dist/brief/taskBrief.js +3 -3
  10. package/dist/cli/agentConfigurationPicker.js +24 -21
  11. package/dist/cli/commandCatalog.js +18 -10
  12. package/dist/cli/roleWizard.js +41 -30
  13. package/dist/cli/updateCommand.js +35 -13
  14. package/dist/cli/updateOrchestrator.js +73 -44
  15. package/dist/cli/updatePorts.js +76 -86
  16. package/dist/cli/upgradeCommand.js +4 -8
  17. package/dist/cli.js +39 -33
  18. package/dist/commands/controllerCommands.js +2 -2
  19. package/dist/commands/executionAuditCommands.js +2 -2
  20. package/dist/commands/projectCommands.js +1 -1
  21. package/dist/commands/releaseCommands.js +9 -38
  22. package/dist/commands/taskCommands.js +91 -81
  23. package/dist/commands/taskCompletionGate.js +0 -117
  24. package/dist/commands/taskRoleRuntimeStatus.js +2 -2
  25. package/dist/commands/taskUpstreamCommands.js +93 -54
  26. package/dist/context/runContextPack.js +30 -54
  27. package/dist/context/runInputContract.js +9 -0
  28. package/dist/context/sessionBootstrapManifest.js +11 -19
  29. package/dist/controller/agentHostObservation.js +4 -2
  30. package/dist/controller/agentRuntimeObserver.js +4 -4
  31. package/dist/controller/clientRuntime.js +19 -44
  32. package/dist/controller/controller.js +24 -10
  33. package/dist/controller/fileSchedulerStoreAdapter.js +91 -24
  34. package/dist/controller/globalInputDelivery.js +31 -8
  35. package/dist/controller/globalRuntimeAttention.js +34 -0
  36. package/dist/controller/jobSupervisor.js +3 -3
  37. package/dist/controller/operatorNotification.js +31 -0
  38. package/dist/controller/providerRetryAdmission.js +3 -1
  39. package/dist/controller/providerRetryDelivery.js +136 -119
  40. package/dist/controller/runtime.js +2 -28
  41. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  42. package/dist/controller/structuredProviderObservation.js +28 -1
  43. package/dist/controller/updateReconciliation.js +72 -20
  44. package/dist/coordination/workMailbox.js +4 -4
  45. package/dist/core/controllerIdentity.js +25 -0
  46. package/dist/core/controllerProcessIdentity.js +2 -2
  47. package/dist/core/controllerServer.js +4 -2
  48. package/dist/core/protocol.js +1 -1
  49. package/dist/doctor/doctor.js +2 -1
  50. package/dist/domain/validation.js +6 -0
  51. package/dist/event/taskEvent.js +3 -3
  52. package/dist/execution/workItemExecution.js +2 -2
  53. package/dist/executor/agentAdapter.js +28 -96
  54. package/dist/executor/agentConfigurationCatalog.js +13 -68
  55. package/dist/executor/agentConfigurationFields.js +120 -0
  56. package/dist/executor/agentConfigurationProbe.js +55 -80
  57. package/dist/executor/agentExecutor.js +11 -8
  58. package/dist/executor/effectiveLaunch.js +3 -3
  59. package/dist/executor/executorRegistry.js +0 -9
  60. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  61. package/dist/grant/capabilityGrant.js +2 -2
  62. package/dist/input/inputRequest.js +4 -4
  63. package/dist/integration/changeSet.js +3 -3
  64. package/dist/integration/integrationAttempt.js +3 -3
  65. package/dist/integration/integrationSourceApplication.js +1 -1
  66. package/dist/interaction/operatorPresentation.js +2 -1
  67. package/dist/job/durableJob.js +1 -1
  68. package/dist/job/jobRunner.js +2 -2
  69. package/dist/message/message.js +8 -6
  70. package/dist/message/messageContinuation.js +8 -9
  71. package/dist/milestone/milestone.js +3 -3
  72. package/dist/observability/executionAudit.js +1 -10
  73. package/dist/observability/runtimeIdentity.js +0 -23
  74. package/dist/output/agentConfigurationPresentation.js +4 -2
  75. package/dist/profile/agentProfile.js +3 -3
  76. package/dist/release/releaseHandover.js +2 -2
  77. package/dist/release/releaseIdempotencyStore.js +0 -23
  78. package/dist/release/releaseWorkflowPorts.js +8 -4
  79. package/dist/release/runtimeRelease.js +9 -7
  80. package/dist/repository/gitWorkspace.js +66 -19
  81. package/dist/repository/project.js +3 -3
  82. package/dist/repository/taskWorkspacePreparer.js +75 -68
  83. package/dist/resources/liveReferences.js +1 -1
  84. package/dist/resources/resourceRegistry.js +90 -44
  85. package/dist/resources/sqliteResourceRegistry.js +5 -7
  86. package/dist/review/reviewRound.js +5 -5
  87. package/dist/role/role.js +7 -8
  88. package/dist/runtime/acpProtocol.js +2 -3
  89. package/dist/runtime/acpSession.js +21 -5
  90. package/dist/runtime/agentDriverObservation.js +1 -1
  91. package/dist/runtime/agentEndpoint.js +4 -1
  92. package/dist/runtime/agentHost.js +25 -65
  93. package/dist/runtime/agentHostCleanup.js +85 -0
  94. package/dist/runtime/agentHostProtocol.js +3 -3
  95. package/dist/runtime/builtinAgentDrivers.js +46 -119
  96. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  97. package/dist/runtime/codexAppServerRuntime.js +55 -89
  98. package/dist/runtime/codexInteractiveHost.js +1 -1
  99. package/dist/runtime/jsonLineChannel.js +35 -7
  100. package/dist/runtime/launchBroker.js +1 -1
  101. package/dist/runtime/processExitObservation.js +1 -1
  102. package/dist/runtime/providerContinuationReconciliationService.js +86 -38
  103. package/dist/runtime/providerRetry.js +15 -0
  104. package/dist/runtime/providerRuntimeIdentity.js +3 -3
  105. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  106. package/dist/runtime/runtimeCoherence.js +7 -3
  107. package/dist/runtime/runtimeObservation.js +3 -3
  108. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  109. package/dist/runtime/structuredProviderHost.js +106 -81
  110. package/dist/runtime/taskRuntimeIsolation.js +3 -3
  111. package/dist/runtime/tmuxAdapters.js +3 -3
  112. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  113. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  114. package/dist/scheduler/taskWake.js +1 -1
  115. package/dist/storage/baselineSchema.js +606 -0
  116. package/dist/storage/homeLayout.js +5 -16
  117. package/dist/storage/recordValidation.js +16 -4
  118. package/dist/storage/sqliteSchema.js +106 -1741
  119. package/dist/storage/sqliteStore.js +16 -14
  120. package/dist/storage/storageSchema.js +8 -7
  121. package/dist/storage/storageVersions.js +23 -16
  122. package/dist/storage/taskStore.js +3 -29
  123. package/dist/storage/upgrade/upgradeOrchestrator.js +14 -112
  124. package/dist/task/nextAction.js +33 -23
  125. package/dist/task/task.js +17 -8
  126. package/dist/task/taskActivation.js +5 -4
  127. package/dist/telemetry/sqliteTelemetryStore.js +2 -2
  128. package/dist/verification/gateArtifact.js +5 -3
  129. package/dist/verification/verificationPlan.js +6 -7
  130. package/dist/web/assets/assetManifest.js +2 -0
  131. package/dist/web/assets/client/app.js +72 -3
  132. package/dist/web/assets/client/components.js +2 -1
  133. package/dist/web/assets/client/i18n.js +2 -0
  134. package/dist/web/assets/client/taskSummary.js +345 -0
  135. package/dist/web/assets/client/taskSurface.js +44 -26
  136. package/dist/web/assets/client/view.js +33 -0
  137. package/dist/web/assets/shell.js +1 -0
  138. package/dist/web/assets/styles/cards.js +21 -0
  139. package/dist/web/webServer.js +39 -5
  140. package/dist/web/webSessions.js +165 -0
  141. package/dist/web/webSnapshot.js +23 -0
  142. package/dist/web/webTaskSurface.js +23 -0
  143. package/dist/workItem/workItem.js +6 -6
  144. package/dist/workspace/cleanupInspection.js +1 -9
  145. package/dist/worktree/managedWorkspace.js +3 -3
  146. package/docs/managed-turn-and-session-runtime.md +50 -7
  147. package/docs/managed-turn-and-session-runtime.zh-CN.md +36 -5
  148. package/docs/observability/README.md +47 -0
  149. package/docs/observability/README.zh-CN.md +37 -0
  150. package/docs/project-refresh.md +9 -0
  151. package/docs/project-refresh.zh-CN.md +8 -0
  152. package/docs/provider-retry.md +35 -0
  153. package/docs/release-workflow.md +70 -214
  154. package/docs/release-workflow.zh-CN.md +53 -157
  155. package/docs/roles-and-configuration.md +30 -0
  156. package/docs/roles-and-configuration.zh-CN.md +20 -0
  157. package/docs/sqlite-control-plane-design.md +48 -289
  158. package/docs/sqlite-control-plane-design.zh-CN.md +37 -53
  159. package/docs/storage-baseline.md +132 -0
  160. package/docs/storage-baseline.zh-CN.md +106 -0
  161. package/docs/task-delivery.md +110 -2
  162. package/docs/task-delivery.zh-CN.md +82 -2
  163. package/docs/task-discovery.md +9 -0
  164. package/docs/task-discovery.zh-CN.md +6 -0
  165. package/docs/testing/verification-levels.md +42 -164
  166. package/docs/testing/verification-levels.zh-CN.md +29 -115
  167. package/i18n/README.zh-CN.md +7 -4
  168. package/package.json +1 -1
  169. package/skills/yui-leader/SKILL.md +11 -0
  170. package/skills/yui-leader/references/execution.md +22 -0
  171. package/skills/yui-leader/references/planning.md +6 -2
  172. package/skills/yui-operator/SKILL.md +45 -13
  173. package/skills/yui-operator/references/task-delivery.md +99 -0
  174. package/skills/yui-runtime/SKILL.md +5 -3
  175. package/skills/yui-runtime/references/publication.md +56 -5
  176. package/skills/yui-runtime/references/recovery.md +12 -2
  177. package/dist/runtime/agentHostCompatibility.js +0 -127
  178. package/dist/storage/migrations/agentFailureContext.js +0 -22
  179. package/dist/storage/migrations/agentRunContract.js +0 -159
  180. package/dist/storage/migrations/artifactsToGit.js +0 -338
  181. package/dist/storage/migrations/collapseWorktreeLayout.js +0 -963
  182. package/dist/storage/migrations/currentInputContract.js +0 -86
  183. package/dist/storage/migrations/currentRuntimeContract.js +0 -228
  184. package/dist/storage/migrations/historicalVerificationPlan.js +0 -35
  185. package/dist/storage/migrations/integrationContinuation.js +0 -105
  186. package/dist/storage/migrations/narrowAgentFailureContext.js +0 -65
  187. package/dist/storage/migrations/notificationOnlyWakes.js +0 -74
  188. package/dist/storage/migrations/removeRuntimeGeneration.js +0 -207
  189. package/dist/storage/migrations/submitIntent.js +0 -126
  190. package/dist/storage/migrations/unifyHomeLayout.js +0 -925
  191. package/dist/storage/migrations/verificationPlanV1.js +0 -162
  192. package/dist/storage/migrations/verificationPolicy.js +0 -74
  193. package/dist/storage/migrations/workItemHistory.js +0 -46
@@ -143,7 +143,7 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
143
143
  }));
144
144
  const laterWakeups = await processLeaderWakeups(store, delivery, now, exactTaskSelection(laterWakeupTaskIds));
145
145
  const inputNotifications = includeOperator
146
- ? await processOperatorInputNotifications(store, delivery, selection, now)
146
+ ? await processOperatorInputNotifications(store, selection, now)
147
147
  : [];
148
148
  return {
149
149
  activeRunDeliveries,
@@ -948,6 +948,18 @@ export class FileTaskController {
948
948
  #requestBackgroundPump() {
949
949
  void this.pump().catch(this.#onError);
950
950
  }
951
+ /** Optional maintenance is not execution admission. Keep its failure visible
952
+ * without bypassing the scheduler's own ownership and delivery fences. */
953
+ async #observeMaintenance(phase, action) {
954
+ try {
955
+ return await action();
956
+ }
957
+ catch (error) {
958
+ this.#onError(new Error(`${phase} failed: ${error instanceof Error ? error.message : String(error)}. `
959
+ + "No successful cleanup or quiescence is inferred; inspect the original failure.", { cause: error }));
960
+ return undefined;
961
+ }
962
+ }
951
963
  async #runCoalesced() {
952
964
  let result = emptyControllerSchedulerResult();
953
965
  let pendingRuntimeDrain = false;
@@ -966,15 +978,15 @@ export class FileTaskController {
966
978
  if (scope.kind === "full")
967
979
  this.reloadReconciliationInterval();
968
980
  if (scope.kind === "full" && this.#resourceReaper !== undefined) {
969
- const reap = await this.#resourceReaper();
970
- for (const failure of reap.failed) {
981
+ const reap = await this.#observeMaintenance("Ephemeral runtime reap", this.#resourceReaper);
982
+ for (const failure of reap?.failed ?? []) {
971
983
  this.#onError(new Error(`Ephemeral runtime reap failed for ${failure.id}: ${failure.message}`));
972
984
  }
973
985
  // The reaper owns the exact resource fences. Once a whole expired
974
986
  // domain converges, let the detached Controller close itself on a
975
987
  // later turn; never close while this reconciliation pass is still
976
988
  // the in-flight server request.
977
- if (reap.failed.length === 0 && (reap.expiredDomains?.length ?? 0) > 0) {
989
+ if (reap !== undefined && reap.failed.length === 0 && (reap.expiredDomains?.length ?? 0) > 0) {
978
990
  for (const domain of reap.expiredDomains ?? []) {
979
991
  queueMicrotask(() => {
980
992
  this.#onExpiredEphemeralDomain?.(domain);
@@ -992,7 +1004,7 @@ export class FileTaskController {
992
1004
  // full pass. It queries only identities already present in Task
993
1005
  // facts and cannot start a model AgentRun or scan unknown children.
994
1006
  if (scope.kind === "full" && this.#continuationReconciler !== undefined) {
995
- await this.#continuationReconciler.reconcile(this.#now());
1007
+ await this.#observeMaintenance("Continuation reconciliation", () => this.#continuationReconciler.reconcile(this.#now()));
996
1008
  }
997
1009
  // DurableJob reconciliation runs before the scheduler pass so a
998
1010
  // terminal job's Leader wakeup is enqueued in the same pass that
@@ -1407,18 +1419,20 @@ export class FileTaskController {
1407
1419
  else if (wakeup.status === "dispatched")
1408
1420
  settled.add(key);
1409
1421
  }
1410
- const operatorRetries = result.inputNotifications.filter((notification) => notification.reason === "operator-not-ready"
1411
- || (notification.reason === "operator-unavailable"
1412
- && this.#operatorStartupRetryArmed));
1422
+ const operatorRetries = result.inputNotifications.filter((notification) => notification.reason === "operator-unavailable"
1423
+ && this.#operatorStartupRetryArmed);
1413
1424
  if (operatorRetries.length > 0) {
1414
1425
  retry.set("operator", {
1415
1426
  identity: operatorRetries.map((notification) => (`${notification.sourceKind}:${notification.taskId}:${notification.sourceId}`)).join("|")
1416
1427
  });
1417
1428
  }
1418
- else if (result.inputNotifications.some((notification) => notification.status === "sent"
1419
- || notification.status === "already-sent")) {
1429
+ else if (result.inputNotifications.some((notification) => notification.status === "queued"
1430
+ || notification.status === "already-queued")) {
1420
1431
  this.#operatorStartupRetryArmed = false;
1421
1432
  settled.add("operator");
1433
+ // The Global queue owns delivery now. Request its next pass without
1434
+ // retaining a second attention retry loop while the Provider is busy.
1435
+ resignal.add("global-role:operator");
1422
1436
  }
1423
1437
  for (const key of settled)
1424
1438
  this.#clearDeliveryRetry(key);
@@ -5,17 +5,17 @@ import { freezeRunContextSnapshot } from "../context/runContextPack.js";
5
5
  import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
6
6
  import { interruptThenTerminalState, prepareMessageContinuations } from "../message/messageContinuation.js";
7
7
  import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
8
- import { deferProviderRetry, providerRetryPending, recordProviderFailure } from "../runtime/providerRetry.js";
8
+ import { deferProviderRetry, providerRetryPending, providerRetryProjection, recordProviderFailure } from "../runtime/providerRetry.js";
9
9
  import { AgentHostObservationDeferred, recordAgentHostConnection, resolveAgentHostObservation } from "./agentHostObservation.js";
10
10
  import { admitOwnedProviderInput, ownedProviderInput } from "./providerRetryAdmission.js";
11
11
  import { RuntimeHookRunFenceError } from "./runtimeHookRunFence.js";
12
- import { appendRunInput, createRun, runPurposeAdmitsTaskState, withRunContextSnapshot } from "../agentRun/agentRun.js";
12
+ import { appendRunInput, createRun, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
13
13
  import { createRunInput } from "../context/runInputContract.js";
14
14
  import { bindExecution, claimPending, completeProcessing, consumePendingBatch, mailboxHasWork, releaseProcessing } from "../coordination/workMailbox.js";
15
15
  import { enqueueWork, settleRoleRunDispatch as settleRoleRunDispatchMailbox } from "../coordination/workMailboxQueue.js";
16
16
  import { transportAgentResult } from "../domain/agentResultTransport.js";
17
17
  import { createTaskEvent } from "../event/taskEvent.js";
18
- import { activeLiveRoleAgentSession, bindGlobalRoleProviderRuntime, bindTaskRoleProviderRuntime, createRoleSessionSet, detachRoleAgentSessionHost, recordRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, replaceTaskRoleAgentSession, selectNewTaskRoleSession, taskRoleControlTarget, updateGlobalRoleProviderRuntime, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
18
+ import { activeLiveRoleAgentSession, bindGlobalRoleProviderRuntime, bindTaskRoleProviderRuntime, createRoleSessionSet, detachRoleAgentSessionHost, recordRoleAgentSession, recordTaskRoleNativeTurnBoundary, rememberRoleAgentCompletedTurn, replaceTaskRoleAgentSession, selectNewTaskRoleSession, roleSessionControlTarget, updateGlobalRoleProviderRuntime, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
19
19
  import { effectiveLaunchWithTaskMainWorkspace, resolveEffectiveLaunch, roleSessionMayContinue, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
20
20
  import { answerInputRequest } from "../input/inputRequest.js";
21
21
  import { createCanonicalLifecycleEvent, foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
@@ -26,6 +26,8 @@ import { activeRoleAgentBinding } from "../role/role.js";
26
26
  import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
27
27
  import { standardAgentError } from "../runtime/agentError.js";
28
28
  import { recordTaskAgentError } from "./taskAgentError.js";
29
+ import { recordGlobalRuntimeAttention, recordGlobalRuntimeFailure } from "./globalRuntimeAttention.js";
30
+ import { queueOperatorNotification } from "./operatorNotification.js";
29
31
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
30
32
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
31
33
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
@@ -384,6 +386,16 @@ export class FileSchedulerStoreAdapter {
384
386
  const matches = receipt !== undefined && binding.run?.attemptId === receipt
385
387
  && (fence.nativeTurnId === undefined || binding.run.nativeTurnId === undefined
386
388
  || binding.run.nativeTurnId === fence.nativeTurnId);
389
+ if ((input.kind === "observer.health" || input.kind === "conversation.observed") && input.payload.failure !== undefined) {
390
+ if (receipt !== undefined && !matches)
391
+ return "obsolete";
392
+ recordGlobalRuntimeAttention(store, fence.roleName, input.eventId, {
393
+ fence, failure: input.payload.failure,
394
+ confirmed: binding.run?.status ?? "no-owned-input",
395
+ unknown: "Native execution outcome and resource quiescence are not proven by this diagnostic."
396
+ }, now);
397
+ return "applied";
398
+ }
387
399
  if (input.kind === "turn.accepted") {
388
400
  if (input.authority === "transport" && matches) {
389
401
  const message = store.listGlobalRoleMessages(fence.roleName).find(entry => receipt === `global-input:${fence.roleName}/${entry.id}`);
@@ -456,12 +468,27 @@ export class FileSchedulerStoreAdapter {
456
468
  binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
457
469
  }
458
470
  if (input.kind === "turn.failed" && matches && input.payload.failure !== undefined) {
471
+ const failureRef = recordGlobalRuntimeFailure(store, fence.roleName, input.eventId, {
472
+ fence, failure: input.payload.failure,
473
+ chainId: binding.retry?.chainId ?? receipt,
474
+ attempts: binding.retry?.attempts ?? 0,
475
+ confirmed: "Native Turn failed. Prior side effects are not undone.",
476
+ unknown: "External effects and remaining unfinished work."
477
+ }, now);
459
478
  binding = recordProviderFailure(binding, {
460
- error: input.payload.failure.error, failureRef: input.eventId, at: now.getTime()
479
+ error: input.payload.failure.error, failureRef, at: now.getTime()
461
480
  });
462
481
  }
463
482
  store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, binding, now));
464
483
  settleGlobalRetryInput(store, fence.roleName, binding, now);
484
+ if (input.kind === "turn.failed" && matches && input.payload.failure !== undefined
485
+ && !providerRetryPending(binding)) {
486
+ recordGlobalRuntimeAttention(store, fence.roleName, input.eventId, {
487
+ fence, failure: input.payload.failure, retry: providerRetryProjection(binding),
488
+ confirmed: "Native Turn failed; this does not prove absence of prior side effects.",
489
+ unknown: "External effects must be inspected before continuing."
490
+ }, now);
491
+ }
465
492
  return "applied";
466
493
  });
467
494
  }
@@ -518,11 +545,19 @@ export class FileSchedulerStoreAdapter {
518
545
  && (native.nativeTurnId === undefined || native.nativeTurnId === input.fence.nativeTurnId)
519
546
  ? "applied" : "obsolete";
520
547
  }
521
- const knownContinuation = input.kind.startsWith("continuation.") && projectProviderContinuations(store.listEvents(input.fence.taskId)).some((entry) => (entry.runId === input.fence.runId
522
- && entry.identity.providerNamespace === input.fence.driverId
523
- && entry.identity.accountScope === input.fence.agentId
524
- && entry.identity.conversationId === input.fence.conversationId
525
- && entry.identity.continuationId === input.fence.continuationId));
548
+ const knownContinuation = (input.kind.startsWith("continuation.") || input.kind === "observer.health")
549
+ && projectProviderContinuations(store.listEvents(input.fence.taskId)).some((entry) => (entry.runId === input.fence.runId
550
+ && entry.identity.providerNamespace === input.fence.driverId
551
+ && entry.identity.accountScope === input.fence.agentId
552
+ && entry.identity.conversationId === input.fence.conversationId
553
+ && entry.identity.continuationId === input.fence.continuationId));
554
+ if (input.kind === "observer.health" && input.payload.failure !== undefined) {
555
+ const native = sessions?.providerBinding?.run;
556
+ return knownContinuation || session?.nativeSessionId === input.fence.nativeSessionId
557
+ && (input.fence.receiptId === undefined || native?.attemptId === input.fence.receiptId
558
+ && (input.fence.nativeTurnId === undefined || native.nativeTurnId === input.fence.nativeTurnId))
559
+ ? "applied" : "obsolete";
560
+ }
526
561
  const requiresCurrentRuntime = input.kind !== "turn.cancelled" && !knownContinuation;
527
562
  const valid = run !== null && (!requiresCurrentRuntime || (run.status === "active" && active?.id === run.id)) && run.roleName === input.fence.roleName && run.effective.agentId === input.fence.agentId && this.drivers.requireByAdapterId(run.effective.adapterId).id === input.fence.driverId && (knownContinuation || (session !== undefined && session.nativeSessionId === input.fence.nativeSessionId)) && runtimeReceiptBelongsToRun(store, input);
528
563
  if (!valid) {
@@ -600,7 +635,7 @@ export class FileSchedulerStoreAdapter {
600
635
  const key = providerContinuationKey(continuation.identity);
601
636
  const identityDigest = createHash("sha256").update(key).digest("hex");
602
637
  const detached = createRuntimeObservation({
603
- schemaVersion: 4,
638
+ schemaVersion: 1,
604
639
  eventId: `derived-continuation-detached:${identityDigest}`,
605
640
  semanticKey: `continuation-detached:${identityDigest}`,
606
641
  kind: "continuation.started",
@@ -689,7 +724,7 @@ export class FileSchedulerStoreAdapter {
689
724
  .update(`${input.semanticKey}\u0000${identityKey}`)
690
725
  .digest("hex");
691
726
  const settled = createRuntimeObservation({
692
- schemaVersion: 4,
727
+ schemaVersion: 1,
693
728
  eventId: `native-snapshot-settled:${digest}`,
694
729
  semanticKey: `native-snapshot-settled:${digest}`,
695
730
  kind: "continuation.settled",
@@ -733,7 +768,7 @@ export class FileSchedulerStoreAdapter {
733
768
  ? "provider-goal-cleared"
734
769
  : `provider-goal-${input.payload.goalStatus}`, now);
735
770
  }
736
- if (input.kind === "turn.failed" && input.payload.failure !== undefined) {
771
+ if (["turn.failed", "observer.health", "conversation.observed"].includes(input.kind) && input.payload.failure !== undefined) {
737
772
  const failure = input.payload.failure;
738
773
  const error = failure.error;
739
774
  const run = input.fence.runId === undefined
@@ -753,10 +788,12 @@ export class FileSchedulerStoreAdapter {
753
788
  runId: input.fence.runId,
754
789
  nativeSessionId: input.fence.nativeSessionId,
755
790
  nativeTurnId: input.fence.nativeTurnId,
791
+ attemptId: input.fence.receiptId,
756
792
  lastOutput: failure.lastOutput
757
793
  },
758
794
  }, now);
759
- if (created && !providerRetryPending(store.getTaskRoleSessionSet(taskId, input.fence.roleName)?.providerBinding))
795
+ if (created && (input.kind !== "turn.failed"
796
+ || !providerRetryPending(store.getTaskRoleSessionSet(taskId, input.fence.roleName)?.providerBinding)))
760
797
  routeRoleEvent(store, errorEvent, input.fence.roleName, wakeReason("agent-error", errorEvent.id), now);
761
798
  }
762
799
  if ((input.kind === "operation.completed" || input.kind === "operation.failed")
@@ -857,6 +894,9 @@ export class FileSchedulerStoreAdapter {
857
894
  getActiveRun(taskId, roleName) {
858
895
  return this.store.getActiveRun(taskId, roleName);
859
896
  }
897
+ getContextSnapshot(taskId, snapshotId) {
898
+ return this.store.getContextSnapshot(taskId, snapshotId);
899
+ }
860
900
  hasOpenInputRequest(taskId) {
861
901
  return this.store.listOpenInputRequests([taskId]).length > 0;
862
902
  }
@@ -879,8 +919,8 @@ export class FileSchedulerStoreAdapter {
879
919
  adapterId: effectiveSession.effective.adapterId
880
920
  };
881
921
  }
882
- markOperatorRunStarted(now) {
883
- void now;
922
+ queueOperatorNotification(input, now) {
923
+ return queueOperatorNotification(this.store, input, now);
884
924
  }
885
925
  resolveExpiredInputRecommendations(now, taskIds) {
886
926
  return this.store.transaction((store) => {
@@ -1224,8 +1264,26 @@ export class FileSchedulerStoreAdapter {
1224
1264
  let resolved = settleProviderTurnSubmission(binding, { attemptId: input.attemptId, status: input.status,
1225
1265
  reason: input.reason, resolvedAt: input.now.toISOString() });
1226
1266
  resolved = this.retrySubmissionResolution(resolved, input, sessions.sessions[sessions.activeAgentId]?.adapterId);
1267
+ if (input.status !== "deferred" && resolved.run?.failure !== undefined) {
1268
+ const failureRef = recordGlobalRuntimeFailure(store, input.roleName, `${input.attemptId}:submission`, {
1269
+ nativeSessionId: currentProviderConversation(resolved).conversationId,
1270
+ attemptId: input.attemptId, status: input.status, failure: resolved.run.failure,
1271
+ chainId: resolved.retry?.chainId ?? input.attemptId, attempts: resolved.retry?.attempts ?? 0
1272
+ }, input.now);
1273
+ resolved = { ...resolved, run: { ...resolved.run, failure: { ...resolved.run.failure, ref: failureRef } },
1274
+ ...(resolved.retry?.failedAttemptId === input.attemptId
1275
+ ? { retry: { ...resolved.retry, failureRef } } : {}) };
1276
+ }
1227
1277
  store.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(sessions, resolved, input.now));
1228
1278
  settleGlobalRetryInput(store, input.roleName, resolved, input.now);
1279
+ if (input.status !== "deferred" && !providerRetryPending(resolved)) {
1280
+ recordGlobalRuntimeAttention(store, input.roleName, `${input.attemptId}:submission`, {
1281
+ nativeSessionId: currentProviderConversation(resolved).conversationId,
1282
+ attemptId: input.attemptId, status: input.status,
1283
+ failure: resolved.run?.failure, retry: providerRetryProjection(resolved),
1284
+ unknown: "External effects are not established by submission settlement."
1285
+ }, input.now);
1286
+ }
1229
1287
  const message = store.listGlobalRoleMessages(input.roleName).find(entry => input.attemptId === `global-input:${input.roleName}/${entry.id}`);
1230
1288
  if (message !== undefined && message.delivery?.via !== "provider") {
1231
1289
  const outcome = input.status === "delivery-unknown" ? "delivery-unknown" : "rejected";
@@ -1681,15 +1739,17 @@ export class FileSchedulerStoreAdapter {
1681
1739
  const mailbox = store.getWorkMailbox(target);
1682
1740
  if (mailbox?.pending == null || mailbox.processing !== null)
1683
1741
  return false;
1742
+ const snapshot = freezeRunContextSnapshot(store, {
1743
+ taskId, roleName: "leader", purpose: "planning"
1744
+ }, now);
1684
1745
  const run = createRun(store.nextRunId(taskId), taskId, "leader", "new", createRunInput({ source: { type: "yui", channel: "task-dispatch" },
1685
1746
  directive: `Plan Task ${taskId} with the user. Persist requirements and decisions. Request activation explicitly; planning grants no delivery authority.`,
1747
+ contextSnapshotRef: contextSnapshotRef(snapshot),
1686
1748
  deltaRefIds: [] }), now, {
1687
1749
  purpose: "planning", effective: resolveEffectiveLaunch({ role, purpose: "planning" })
1688
1750
  });
1689
- const snapshot = freezeRunContextSnapshot(store, run, now);
1690
- const frozen = withRunContextSnapshot(run, contextSnapshotRef(snapshot));
1691
- store.saveRun(frozen);
1692
- store.saveActiveRun(frozen);
1751
+ store.saveRun(run);
1752
+ store.saveActiveRun(run);
1693
1753
  // Freeze the initial notification prefix against this planning Run.
1694
1754
  // Later messages remain pending; acceptance or an exact terminal settles
1695
1755
  // only this batch, so a failed initial launch cannot replay it forever.
@@ -2211,7 +2271,7 @@ export class FileSchedulerStoreAdapter {
2211
2271
  && event.payload.observationId === observationId)))
2212
2272
  return;
2213
2273
  const observation = validateRuntimeProcessExitObservation({
2214
- schemaVersion: 2,
2274
+ schemaVersion: 1,
2215
2275
  observationId,
2216
2276
  hostSequence: 1,
2217
2277
  hostInstanceId: `tmux-${input.roleName}`,
@@ -2256,6 +2316,13 @@ export class FileSchedulerStoreAdapter {
2256
2316
  recordCanonicalObservationObsolete(store, input, "provider-conversation-mismatch", now);
2257
2317
  return "obsolete";
2258
2318
  }
2319
+ if (input.payload.failure !== undefined) {
2320
+ // A diagnostic without an owned Turn describes this exact connection,
2321
+ // not a new recoverability observation or proof of resource release.
2322
+ return sessions.activeAgentId === input.fence.agentId
2323
+ && sessions.sessions[input.fence.agentId]?.nativeSessionId === input.fence.nativeSessionId
2324
+ ? "applied" : "obsolete";
2325
+ }
2259
2326
  try {
2260
2327
  if (input.kind === "conversation.observed") {
2261
2328
  binding = updateProviderConversationRecoverability(binding, input.payload.recoverability);
@@ -2653,7 +2720,7 @@ export class FileSchedulerStoreAdapter {
2653
2720
  // this Global Role, settle it to the observed terminal so the scope-generic
2654
2721
  // resolver and the interrupt-then gate see the real stop on the live binding
2655
2722
  // — the same edge Task settles via settleStructuredProviderTurn. This is a
2656
- // strict guarded no-op for an older unmanaged Session or a
2723
+ // strict guarded no-op for a Session without a controlled binding or a
2657
2724
  // binding holding a different/already-terminal Turn, and the
2658
2725
  // durable native terminal recorded just above remains the proof of record.
2659
2726
  current = settleGlobalRoleRuntimeTurn(current, input.nativeTurnId, input.providerStatus, now);
@@ -2864,7 +2931,7 @@ function dormantRuntimeCandidateIsCurrent(store, candidate, allowActiveRun = fal
2864
2931
  }
2865
2932
  const sessions = runtimeOwnerSessionSet(store, owner);
2866
2933
  const active = allowActiveRun && owner.scope === "task"
2867
- ? taskRoleControlTarget(store.getTaskRoleSessionSet(owner.taskId, owner.roleName))
2934
+ ? roleSessionControlTarget(store.getTaskRoleSessionSet(owner.taskId, owner.roleName))
2868
2935
  : sessions?.sessions[sessions.activeAgentId];
2869
2936
  return active !== undefined && active.agentId === candidate.agentId && active.adapterId === candidate.adapterId
2870
2937
  && active.nativeSessionId === candidate.nativeSessionId && (allowActiveRun || active.updatedAt === candidate.sessionUpdatedAt);
@@ -3416,12 +3483,12 @@ function settleStructuredProviderTurn(sessions, runId, status, now, attemptId) {
3416
3483
  * It is a strict guarded no-op unless a controller-owned Turn with that exact
3417
3484
  * native id is bound and still in flight (`accepted`) — an absent binding, a
3418
3485
  * different/only-submitting Turn, or an already-terminal one is left untouched, so
3419
- * an older unmanaged Session without a controlled binding settles nothing
3486
+ * a Session without a controlled binding settles nothing
3420
3487
  * and the durable `recentCompletedTurnIds` terminal remains the proof of record.
3421
3488
  */
3422
3489
  function settleGlobalRoleRuntimeTurn(sessions, nativeTurnId, status, now) {
3423
3490
  const binding = sessions.providerBinding;
3424
- if (binding === null || binding === undefined || binding.run === null
3491
+ if (binding === null || binding.run === null
3425
3492
  || binding.run.nativeTurnId !== nativeTurnId
3426
3493
  || binding.run.status !== "accepted") {
3427
3494
  return sessions;
@@ -13,6 +13,10 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
13
13
  if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
14
14
  continue;
15
15
  let sessions = store.getGlobalRoleSessionSet(role.name);
16
+ // A detached Host keeps an active Session and may reconnect. An explicit
17
+ // Session stop/switch must not be undone by background notification work.
18
+ if (sessions !== null && sessions.sessions[sessions.activeAgentId]?.status !== "active")
19
+ continue;
16
20
  if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
17
21
  store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
18
22
  pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
@@ -37,6 +41,9 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
37
41
  continue;
38
42
  if (current != null && ["submitting", "accepted", "delivery-unknown"].includes(current.status))
39
43
  continue;
44
+ const matchesTarget = (message) => message.deliveryTarget === undefined
45
+ || sessions?.activeAgentId === message.deliveryTarget.agentId
46
+ && sessions.sessions[message.deliveryTarget.agentId]?.nativeSessionId === message.deliveryTarget.nativeSessionId;
40
47
  const claims = pending.filter(message => message.interruptThen !== undefined);
41
48
  let next;
42
49
  if (claims.length > 0) {
@@ -54,22 +61,36 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
54
61
  if (next === undefined)
55
62
  continue;
56
63
  }
57
- else
58
- next = pending[0];
64
+ else {
65
+ // Retain each stale original with its exact failure, but do not spend
66
+ // one reconciliation interval per unsent notice before reaching user
67
+ // input for the successor. An existing attempt/uncertain control still
68
+ // fences the queue; target mismatch alone is not non-acceptance.
69
+ next = pending.find(message => {
70
+ if (matchesTarget(message)
71
+ || current?.attemptId === `global-input:${role.name}/${message.id}`
72
+ || message.control !== undefined && message.control.outcome !== "rejected")
73
+ return true;
74
+ store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, "native-session-changed", new Date()));
75
+ return false;
76
+ });
77
+ }
78
+ if (next === undefined)
79
+ continue;
59
80
  const attemptId = `global-input:${role.name}/${next.id}`;
60
81
  if (current?.attemptId === attemptId)
61
82
  continue;
62
83
  if (next.control !== undefined && next.control.outcome !== "rejected")
63
84
  continue;
64
- const matchesTarget = () => next.deliveryTarget === undefined
65
- || sessions?.activeAgentId === next.deliveryTarget.agentId
66
- && sessions.sessions[next.deliveryTarget.agentId]?.nativeSessionId === next.deliveryTarget.nativeSessionId;
67
- if (!matchesTarget()) {
85
+ if (!matchesTarget(next)) {
68
86
  store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(next, "native-session-changed", new Date()));
69
87
  continue;
70
88
  }
71
89
  await ensure(role.name);
72
90
  sessions = store.getGlobalRoleSessionSet(role.name);
91
+ if (hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name })))
92
+ || sessions?.sessions[sessions.activeAgentId]?.status !== "active")
93
+ continue;
73
94
  const binding = sessions?.providerBinding;
74
95
  if (binding == null || binding.authority.owner !== "controller")
75
96
  continue;
@@ -78,7 +99,7 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
78
99
  if (persisted === undefined || JSON.stringify(persisted) !== JSON.stringify(next))
79
100
  continue;
80
101
  const claim = next.interruptThen;
81
- if (!matchesTarget() || claim !== undefined
102
+ if (!matchesTarget(next) || claim !== undefined
82
103
  && (sessions?.activeAgentId !== claim.targetAgentId
83
104
  || nativeSessionId !== claim.targetNativeSessionId
84
105
  || binding.authority.epoch !== claim.targetAuthorityEpoch
@@ -126,7 +147,9 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
126
147
  }
127
148
  }
128
149
  catch (error) {
129
- onError(error);
150
+ onError(new Error(`Global input delivery failed for ${role.name}: `
151
+ + `${error instanceof Error ? error.message : String(error)}. `
152
+ + "Inspect the persisted Message and attempt before retrying; delivery is not inferred.", { cause: error }));
130
153
  }
131
154
  }
132
155
  }
@@ -0,0 +1,34 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createGlobalRoleMessage } from "../message/message.js";
3
+ import { serializeAgentErrorRaw } from "../runtime/agentError.js";
4
+ /** Global observations have no Task event history. Keep original failed
5
+ * attempts as ordinary record-only Messages before replacing the live binding. */
6
+ export function recordGlobalRuntimeFailure(store, roleName, identity, facts, now) {
7
+ const header = `Yui Global failure evidence ${createHash("sha256").update(identity).digest("hex")}\n`;
8
+ const previous = store.listGlobalRoleMessages(roleName).find(message => message.kind === "system" && message.body.startsWith(header));
9
+ if (previous !== undefined)
10
+ return `${roleName}/${previous.id}`;
11
+ const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), roleName, header + serializeAgentErrorRaw(facts), "system", { type: "system" }, now);
12
+ store.saveGlobalRoleMessage(message);
13
+ return `${roleName}/${message.id}`;
14
+ }
15
+ /** Reuse the Global Message inbox for an exact failure, not a recovery queue.
16
+ * A failed Operator cannot be asked to retry its own diagnostic automatically. */
17
+ export function recordGlobalRuntimeAttention(store, roleName, identity, facts, now) {
18
+ // A Home may have a custom Global Role without an Operator. Keep its own
19
+ // diagnostic readable; never create a Role or launch a substitute Agent.
20
+ const recipient = store.getGlobalRole("operator") === null ? roleName : "operator";
21
+ const requestId = `runtime-attention:${createHash("sha256").update(`${roleName}\0${identity}`).digest("hex")}`;
22
+ if (store.listGlobalRoleMessages(recipient).some(message => message.inputControl?.requestId === requestId))
23
+ return;
24
+ const message = createGlobalRoleMessage(store.nextGlobalRoleMessageId(), recipient, [
25
+ `Global Role ${roleName} needs attention. No recovery or new authority is implied.`,
26
+ "Inspect the exact Session and original input before choosing a next action; unknown effects must not be replayed.",
27
+ `Original failed attempts remain in Role ${roleName}'s record-only failure Messages; failureRef names the latest evidence.`,
28
+ serializeAgentErrorRaw(facts)
29
+ ].join("\n"), "system", { type: "system" }, now, { inputControl: { action: "queue", requestId } });
30
+ store.saveGlobalRoleMessage(recipient === roleName ? {
31
+ ...message,
32
+ notDelivered: { reason: "runtime-needs-attention; retained for user or successor inspection", at: now.toISOString() }
33
+ } : message);
34
+ }
@@ -50,9 +50,9 @@ export class DurableJobSupervisor {
50
50
  this.#reconcileJob(job, now);
51
51
  }
52
52
  catch (error) {
53
- this.#onError(error instanceof Error
54
- ? error
55
- : new Error(`DurableJob supervision failed for ${job.id}: ${String(error)}`));
53
+ this.#onError(new Error(`DurableJob supervision failed for ${job.taskId}/${job.id}: `
54
+ + `${error instanceof Error ? error.message : String(error)}. `
55
+ + "Inspect retained process/artifact evidence before retry or cancellation; effects remain unconfirmed.", { cause: error }));
56
56
  }
57
57
  }
58
58
  }
@@ -0,0 +1,31 @@
1
+ import { completeProcessing } from "../coordination/workMailbox.js";
2
+ import { createGlobalRoleMessage } from "../message/message.js";
3
+ /**
4
+ * Transfer one exact attention claim to the Global Message queue atomically.
5
+ * Queue ownership is not Provider acceptance or Agent acknowledgement. A failed
6
+ * or uncertain Message retains its identity/evidence; it is never reissued here.
7
+ */
8
+ export function queueOperatorNotification(store, input, now = new Date()) {
9
+ return store.transaction(tx => {
10
+ const requestId = `operator-notice:${encodeURIComponent(input.receiptId)}`;
11
+ const previous = tx.listGlobalRoleMessages("operator").find(entry => (entry.inputControl ?? entry.interruptThen?.reusedInput)?.requestId === requestId);
12
+ const mailbox = tx.getWorkMailbox({ kind: "operator" });
13
+ const claimed = mailbox?.processing?.batchId === input.batchId;
14
+ if (previous !== undefined) {
15
+ if (claimed)
16
+ tx.saveWorkMailbox(completeProcessing(mailbox, input.batchId));
17
+ return { status: "already-queued", messageId: previous.id };
18
+ }
19
+ if (!claimed)
20
+ throw new Error(`Operator notification claim changed: ${input.batchId}`);
21
+ const sessions = tx.getGlobalRoleSessionSet("operator");
22
+ const session = sessions?.sessions[sessions.activeAgentId];
23
+ if (session?.status !== "active" || sessions?.providerBinding == null)
24
+ return { status: "unavailable" };
25
+ const created = { ...createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), "operator", input.text, "system", { type: "system" }, now, { inputControl: { action: "queue", requestId } }),
26
+ deliveryTarget: { agentId: session.agentId, nativeSessionId: session.nativeSessionId } };
27
+ tx.saveGlobalRoleMessage(created);
28
+ tx.saveWorkMailbox(completeProcessing(mailbox, input.batchId));
29
+ return { status: "queued", messageId: created.id };
30
+ });
31
+ }
@@ -24,7 +24,9 @@ export function retryIntentBlocker(store, input, retry) {
24
24
  return "role-changed";
25
25
  if (sessions.sessions[sessions.activeAgentId]?.effective.sourceDesiredRevision !== role.launchRevision)
26
26
  return "role-configuration-changed";
27
- if (store.listGlobalRoleMessages(input.roleName).some(message => Date.parse(message.createdAt) > Date.parse(retry.intentAt)
27
+ if (store.listGlobalRoleMessages(input.roleName).some(message => (message.kind === "user" || message.kind === "operator"
28
+ || message.inputControl !== undefined || message.interruptThen !== undefined)
29
+ && Date.parse(message.createdAt) > Date.parse(retry.intentAt)
28
30
  && message.delivery === undefined && message.notDelivered === undefined))
29
31
  return "new-user-input";
30
32
  return undefined;