@zq-silk/yui 0.16.0 → 0.16.2

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 (114) hide show
  1. package/ARCHITECTURE.md +12 -0
  2. package/ARCHITECTURE.zh-CN.md +8 -0
  3. package/README.md +3 -0
  4. package/dist/agent/managedRuntimeEnvironment.js +0 -4
  5. package/dist/agentRun/agentRun.js +7 -17
  6. package/dist/cli/agentConfigurationPicker.js +24 -21
  7. package/dist/cli/commandCatalog.js +18 -10
  8. package/dist/cli/roleWizard.js +41 -30
  9. package/dist/cli/updateCommand.js +35 -13
  10. package/dist/cli/updateOrchestrator.js +73 -33
  11. package/dist/cli/updatePorts.js +51 -20
  12. package/dist/cli.js +35 -26
  13. package/dist/commands/controllerCommands.js +1 -1
  14. package/dist/commands/executionAuditCommands.js +2 -2
  15. package/dist/commands/projectCommands.js +1 -1
  16. package/dist/commands/releaseCommands.js +9 -38
  17. package/dist/commands/taskCommands.js +84 -81
  18. package/dist/commands/taskCompletionGate.js +0 -117
  19. package/dist/commands/taskRoleRuntimeStatus.js +1 -1
  20. package/dist/commands/taskUpstreamCommands.js +93 -54
  21. package/dist/context/runContextPack.js +30 -54
  22. package/dist/context/runInputContract.js +9 -0
  23. package/dist/context/sessionBootstrapManifest.js +11 -19
  24. package/dist/controller/agentHostObservation.js +4 -2
  25. package/dist/controller/controller.js +8 -6
  26. package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
  27. package/dist/controller/globalInputDelivery.js +28 -7
  28. package/dist/controller/globalRuntimeAttention.js +34 -0
  29. package/dist/controller/operatorNotification.js +31 -0
  30. package/dist/controller/providerRetryAdmission.js +3 -1
  31. package/dist/controller/providerRetryDelivery.js +10 -3
  32. package/dist/controller/runtime.js +1 -27
  33. package/dist/controller/sessionOwnerReconciliation.js +42 -24
  34. package/dist/controller/structuredProviderObservation.js +27 -0
  35. package/dist/controller/updateReconciliation.js +72 -20
  36. package/dist/doctor/doctor.js +2 -1
  37. package/dist/executor/agentAdapter.js +28 -96
  38. package/dist/executor/agentConfigurationCatalog.js +13 -68
  39. package/dist/executor/agentConfigurationFields.js +120 -0
  40. package/dist/executor/agentConfigurationProbe.js +55 -80
  41. package/dist/executor/agentExecutor.js +5 -2
  42. package/dist/executor/executorRegistry.js +0 -9
  43. package/dist/executor/fileRoleLaunchPlanner.js +12 -11
  44. package/dist/interaction/operatorPresentation.js +2 -1
  45. package/dist/message/messageContinuation.js +8 -9
  46. package/dist/observability/executionAudit.js +1 -10
  47. package/dist/observability/runtimeIdentity.js +0 -23
  48. package/dist/output/agentConfigurationPresentation.js +4 -2
  49. package/dist/release/releaseHandover.js +2 -2
  50. package/dist/release/releaseIdempotencyStore.js +0 -23
  51. package/dist/release/releaseWorkflowPorts.js +8 -4
  52. package/dist/repository/gitWorkspace.js +66 -19
  53. package/dist/repository/taskWorkspacePreparer.js +75 -68
  54. package/dist/resources/resourceRegistry.js +90 -44
  55. package/dist/resources/sqliteResourceRegistry.js +3 -5
  56. package/dist/runtime/acpSession.js +21 -5
  57. package/dist/runtime/agentEndpoint.js +4 -1
  58. package/dist/runtime/agentHost.js +22 -62
  59. package/dist/runtime/agentHostCleanup.js +85 -0
  60. package/dist/runtime/agentHostProtocol.js +1 -1
  61. package/dist/runtime/builtinAgentDrivers.js +46 -119
  62. package/dist/runtime/builtinTranscriptObserver.js +10 -6
  63. package/dist/runtime/codexAppServerRuntime.js +55 -89
  64. package/dist/runtime/jsonLineChannel.js +35 -7
  65. package/dist/runtime/providerContinuationReconciliationService.js +42 -3
  66. package/dist/runtime/providerRetry.js +15 -0
  67. package/dist/runtime/providerRuntimeReconciler.js +12 -2
  68. package/dist/runtime/structuredProviderHost.js +106 -81
  69. package/dist/scheduler/activeRoleRunDelivery.js +5 -1
  70. package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
  71. package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
  72. package/dist/task/nextAction.js +33 -23
  73. package/dist/task/task.js +2 -1
  74. package/dist/web/assets/assetManifest.js +2 -0
  75. package/dist/web/assets/client/app.js +72 -3
  76. package/dist/web/assets/client/components.js +2 -1
  77. package/dist/web/assets/client/i18n.js +2 -0
  78. package/dist/web/assets/client/taskSummary.js +345 -0
  79. package/dist/web/assets/client/taskSurface.js +44 -26
  80. package/dist/web/assets/client/view.js +33 -0
  81. package/dist/web/assets/shell.js +1 -0
  82. package/dist/web/assets/styles/cards.js +21 -0
  83. package/dist/web/webServer.js +39 -5
  84. package/dist/web/webSessions.js +165 -0
  85. package/dist/web/webSnapshot.js +23 -0
  86. package/dist/web/webTaskSurface.js +23 -0
  87. package/docs/managed-turn-and-session-runtime.md +31 -0
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
  89. package/docs/observability/README.md +47 -0
  90. package/docs/observability/README.zh-CN.md +37 -0
  91. package/docs/project-refresh.md +9 -0
  92. package/docs/project-refresh.zh-CN.md +8 -0
  93. package/docs/provider-retry.md +35 -0
  94. package/docs/release-workflow.md +139 -20
  95. package/docs/release-workflow.zh-CN.md +115 -14
  96. package/docs/roles-and-configuration.md +30 -0
  97. package/docs/roles-and-configuration.zh-CN.md +20 -0
  98. package/docs/task-delivery.md +109 -0
  99. package/docs/task-delivery.zh-CN.md +80 -0
  100. package/docs/task-discovery.md +9 -0
  101. package/docs/task-discovery.zh-CN.md +6 -0
  102. package/docs/testing/verification-levels.md +33 -12
  103. package/docs/testing/verification-levels.zh-CN.md +28 -8
  104. package/i18n/README.zh-CN.md +2 -0
  105. package/package.json +1 -1
  106. package/skills/yui-leader/SKILL.md +11 -0
  107. package/skills/yui-leader/references/execution.md +22 -0
  108. package/skills/yui-leader/references/planning.md +6 -2
  109. package/skills/yui-operator/SKILL.md +45 -13
  110. package/skills/yui-operator/references/task-delivery.md +99 -0
  111. package/skills/yui-runtime/SKILL.md +5 -3
  112. package/skills/yui-runtime/references/publication.md +56 -5
  113. package/skills/yui-runtime/references/recovery.md +12 -2
  114. package/dist/runtime/agentHostCompatibility.js +0 -127
@@ -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) {
@@ -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.
@@ -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);
@@ -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);
@@ -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
@@ -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
+ }
@@ -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;
@@ -6,9 +6,10 @@ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
6
6
  import { redactAgentErrorText } from "../runtime/agentError.js";
7
7
  import { AGENT_HOST_CONTROL_PROTOCOL, agentHostControlSocketPath, inspectAgentHostSocket, sendAgentHostRunControl } from "../runtime/agentHost.js";
8
8
  import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
9
- import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
9
+ import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending, providerRetryProjection } from "../runtime/providerRetry.js";
10
10
  import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
11
11
  import { retryIntentBlocker } from "./providerRetryAdmission.js";
12
+ import { recordGlobalRuntimeAttention } from "./globalRuntimeAttention.js";
12
13
  /** Hooks in the existing Controller pass and nearest-deadline timer. SQLite's
13
14
  * current Provider input is the only pending intent; nothing is queued here. */
14
15
  export function createProviderRetryHooks(home, store, ports = {}) {
@@ -45,8 +46,11 @@ export function createProviderRetryHooks(home, store, ports = {}) {
45
46
  save({ ...current, providerBinding: exhausted
46
47
  ? { ...cancelled, retry: { ...cancelled.retry, status: "exhausted" } } : cancelled });
47
48
  const owner = set.owner;
48
- if (owner.scope === "global")
49
- settleGlobalRetryInput(store, owner.roleName, load(owner)?.providerBinding, new Date(at));
49
+ if (owner.scope === "global") {
50
+ const stopped = load(owner)?.providerBinding;
51
+ settleGlobalRetryInput(store, owner.roleName, stopped, new Date(at));
52
+ recordGlobalRuntimeAttention(store, owner.roleName, `${binding.retry.chainId}:stopped`, providerRetryProjection(stopped), new Date(at));
53
+ }
50
54
  if (owner.scope === "task") {
51
55
  const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-stopped", {
52
56
  roleName: owner.roleName, chainId: binding.retry.chainId, reason
@@ -128,6 +132,9 @@ export function createProviderRetryHooks(home, store, ports = {}) {
128
132
  return;
129
133
  save({ ...latest, providerBinding: { ...latest.providerBinding,
130
134
  retry: { ...latest.providerBinding.retry, reason: detail } } });
135
+ if (owner.scope === "global") {
136
+ recordGlobalRuntimeAttention(store, owner.roleName, `${retry.currentAttemptId}:unconfirmed`, providerRetryProjection(load(owner)?.providerBinding), new Date(at));
137
+ }
131
138
  if (owner.scope === "task") {
132
139
  const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-unconfirmed", {
133
140
  roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
@@ -10,7 +10,6 @@ import { effectiveLaunchConfig, resolveEffectiveLaunch, roleSessionMayContinue }
10
10
  import { agentProcessReadinessProbe, ExecutorRegistry } from "../executor/executorRegistry.js";
11
11
  import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
12
12
  import { createKernelPorts } from "../kernel/kernelPorts.js";
13
- import { createGlobalRoleMessage } from "../message/message.js";
14
13
  import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
15
14
  import { createResourceAutoGc } from "../resources/autoResourceGc.js";
16
15
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
@@ -117,7 +116,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
117
116
  store,
118
117
  environment: options.environment,
119
118
  tmux,
120
- nativeConnection: (taskId, roleName) => planner.planNativeControl(taskId, roleName),
119
+ nativeConnection: (owner) => planner.planNativeControl(owner),
121
120
  onWarning: options.onError
122
121
  });
123
122
  // The runtime inbox is also the low-latency discovery source during a
@@ -252,31 +251,6 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
252
251
  sessionHost,
253
252
  promptPush,
254
253
  launchCoordinator,
255
- notifyOperatorInputOnce: async (input) => {
256
- // The original attention mailbox remains pending until this one
257
- // Global Message has an actual Provider receipt. Never inject keys
258
- // into a busy Host console and mistake that for accepted attention.
259
- const message = store.transaction(tx => {
260
- const requestId = `operator-notice:${encodeURIComponent(input.receiptId)}`;
261
- const previous = tx.listGlobalRoleMessages("operator").find(entry => (entry.inputControl ?? entry.interruptThen?.reusedInput)?.requestId === requestId);
262
- if (previous !== undefined)
263
- return previous;
264
- const sessions = tx.getGlobalRoleSessionSet("operator");
265
- const session = sessions?.sessions[sessions.activeAgentId];
266
- if (session?.status !== "active" || sessions?.providerBinding == null)
267
- return null;
268
- const created = { ...createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), "operator", input.text, "system", { type: "system" }, new Date(), { inputControl: { action: "queue", requestId } }),
269
- deliveryTarget: { agentId: session.agentId, nativeSessionId: session.nativeSessionId } };
270
- tx.saveGlobalRoleMessage(created);
271
- return created;
272
- });
273
- if (message === null)
274
- return "unavailable";
275
- if (message.delivery?.via === "provider")
276
- return "already-sent";
277
- runningRuntime?.signal("global-role:operator");
278
- return "not-ready";
279
- },
280
254
  roleResourceInventory: async (panes, inputs) => {
281
255
  const inventory = await scanInventory(panes);
282
256
  return inventory.resources.flatMap((resource) => {
@@ -1,11 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { createTaskEvent } from "../event/taskEvent.js";
4
+ import { createGlobalRoleMessage } from "../message/message.js";
4
5
  import { createSessionOwnerIdentity, isLinuxProcessLive, listOwnedProcessTree, readLinuxProcessIdentity, reconcileSessionOwners, terminateSessionOwners } from "../runtime/index.js";
5
6
  import { sessionOwnerProcessKey } from "../runtime/sessionOwnerIdentity.js";
6
7
  import { AGENT_HOST_CONTROL_PROTOCOL, sendAgentHostCancelControl } from "../runtime/agentHost.js";
7
8
  import { settleProviderTurn, cancelQuiescentProviderInput, clearProviderGoal } from "../runtime/providerRuntimeIdentity.js";
8
- import { updateTaskRoleProviderRuntime, taskRoleControlTarget } from "../executor/agentExecutor.js";
9
+ import { updateTaskRoleProviderRuntime, updateGlobalRoleProviderRuntime, roleSessionControlTarget } from "../executor/agentExecutor.js";
9
10
  import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
10
11
  import { stopCodexNativeSession } from "../runtime/nativeSessionControl.js";
11
12
  import { routeRoleEvent } from "../scheduler/operatorEvent.js";
@@ -34,8 +35,8 @@ export class SessionOwnerReconciliation {
34
35
  this.#tmux = deps.tmux;
35
36
  this.#onWarning = deps.onWarning ?? (() => undefined);
36
37
  this.#cancelInput = deps.cancelInput ?? sendAgentHostCancelControl;
37
- this.#nativeConnection = deps.nativeConnection ?? ((taskId, roleName) => new FileRoleLaunchPlanner(this.#home, this.#store, { environment: this.#environment })
38
- .planNativeControl(taskId, roleName));
38
+ this.#nativeConnection = deps.nativeConnection ?? ((owner) => new FileRoleLaunchPlanner(this.#home, this.#store, { environment: this.#environment })
39
+ .planNativeControl(owner));
39
40
  this.#stopNative = deps.stopNative ?? stopCodexNativeSession;
40
41
  }
41
42
  /**
@@ -173,33 +174,36 @@ export class SessionOwnerReconciliation {
173
174
  };
174
175
  }
175
176
  async #quiesceInput(owner) {
176
- if (owner.scope !== "task")
177
- return;
178
- if (runtimeCleanupDisposition(this.#store.getWorkMailbox(runtimeLifecycleTarget(owner))) === "detach-host")
177
+ const disposition = runtimeCleanupDisposition(this.#store.getWorkMailbox(runtimeLifecycleTarget(owner)));
178
+ if (disposition === "detach-host")
179
179
  return;
180
- const set = this.#store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
180
+ const set = owner.scope === "task"
181
+ ? this.#store.getTaskRoleSessionSet(owner.taskId, owner.roleName)
182
+ : this.#store.getGlobalRoleSessionSet(owner.roleName);
181
183
  const binding = set?.providerBinding;
182
184
  const input = binding?.run;
183
185
  const unsettled = input != null && ["submitting", "accepted", "delivery-unknown"].includes(input.status);
184
186
  const hasGoal = binding?.goal != null && binding.goal.status !== "complete";
185
- const selected = taskRoleControlTarget(set);
187
+ const selected = roleSessionControlTarget(set);
186
188
  if (set == null || (!unsettled && !hasGoal && selected?.adapterId !== "codex"))
187
189
  return;
188
190
  if (selected === undefined) {
189
191
  throw new Error("Cannot identify the native execution to stop.");
190
192
  }
191
- const request = this.#store.listEvents(owner.taskId).filter(event => ["runtime.session-replacement-requested", "runtime.session-stop-requested"].includes(event.type)
193
+ const request = owner.scope === "task" ? this.#store.listEvents(owner.taskId).filter(event => ["runtime.session-replacement-requested", "runtime.session-stop-requested"].includes(event.type)
192
194
  && event.payload.roleName === owner.roleName
193
- && event.payload.nativeSessionId === selected.nativeSessionId).at(-1);
194
- const operatorStop = this.#store.getTask(owner.taskId)?.executionGate.state === "stopped"
195
- || (request !== undefined && ["user", "operator"].includes(request.payload.requestedBy));
195
+ && event.payload.nativeSessionId === selected.nativeSessionId).at(-1) : undefined;
196
+ const operatorStop = owner.scope === "global"
197
+ ? disposition === "end-session" || disposition === "replace-session"
198
+ : this.#store.getTask(owner.taskId)?.executionGate.state === "stopped"
199
+ || (request !== undefined && ["user", "operator"].includes(request.payload.requestedBy));
196
200
  if (binding?.authority.owner === "human" && !operatorStop) {
197
201
  throw new Error("This native Session is under human control; Operator/user must request its stop or replacement.");
198
202
  }
199
203
  let terminal;
200
204
  try {
201
205
  const result = unsettled && binding?.authority.owner === "controller" && binding.authority.holderId !== undefined ? await this.#cancelInput({
202
- home: this.#home, scope: "task", taskId: owner.taskId, roleName: owner.roleName,
206
+ home: this.#home, ...owner,
203
207
  control: {
204
208
  protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "cancel",
205
209
  nativeSessionId: selected.nativeSessionId,
@@ -220,7 +224,7 @@ export class SessionOwnerReconciliation {
220
224
  }
221
225
  if (terminal === undefined || hasGoal || selected.adapterId === "codex") {
222
226
  if (selected.adapterId === "codex") {
223
- await this.#stopNative(this.#nativeConnection(owner.taskId, owner.roleName), {
227
+ await this.#stopNative(this.#nativeConnection(owner), {
224
228
  conversationId: selected.nativeSessionId,
225
229
  nativeTurnId: unsettled && terminal === undefined ? input?.nativeTurnId : undefined,
226
230
  clearGoal: true
@@ -237,7 +241,7 @@ export class SessionOwnerReconciliation {
237
241
  }
238
242
  else if (selected.status !== "ended"
239
243
  || records.some(ownerRootIsLive)
240
- || this.#tmux?.probeRoleStatus(owner.taskId, owner.roleName) !== "exited") {
244
+ || this.#tmux?.probeRoleStatus(owner.scope === "task" ? owner.taskId : "operator", owner.roleName) !== "exited") {
241
245
  throw new Error("Native process custody is unavailable; inspect the exact local execution before reusing its workspace.");
242
246
  }
243
247
  }
@@ -246,9 +250,12 @@ export class SessionOwnerReconciliation {
246
250
  }
247
251
  }
248
252
  this.#store.transaction(tx => {
249
- const current = tx.getTaskRoleSessionSet(owner.taskId, owner.roleName);
253
+ const current = owner.scope === "task"
254
+ ? tx.getTaskRoleSessionSet(owner.taskId, owner.roleName)
255
+ : tx.getGlobalRoleSessionSet(owner.roleName);
250
256
  const provider = current?.providerBinding;
251
257
  if (current == null || provider?.run?.attemptId !== input?.attemptId
258
+ || provider?.run?.nativeTurnId !== input?.nativeTurnId
252
259
  || (selected.fromBinding
253
260
  ? current.providerBinding?.conversations.find(entry => entry.status === "current")?.conversationId
254
261
  : current.sessions[current.activeAgentId]?.nativeSessionId) !== selected.nativeSessionId
@@ -268,15 +275,26 @@ export class SessionOwnerReconciliation {
268
275
  }
269
276
  if (hasGoal || selected.adapterId === "codex")
270
277
  stopped = clearProviderGoal(stopped);
271
- tx.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(current, stopped, now));
272
- tx.saveEvent(owner.taskId, createTaskEvent(tx.nextEventId(owner.taskId), owner.taskId, "runtime.input-stop-confirmed", {
273
- roleName: owner.roleName, ...(input == null ? {} : { attemptId: input.attemptId }),
274
- nativeSessionId: selected.nativeSessionId, status: stopped.run?.status ?? "quiescent",
275
- evidence: terminal === undefined ? "native-resource-inspection" : "native-terminal",
276
- ...(input?.nativeTurnId === undefined ? {} : { nativeTurnId: input.nativeTurnId })
277
- }, now));
278
+ if (owner.scope === "global") {
279
+ tx.saveGlobalRoleSessionSet(updateGlobalRoleProviderRuntime(current, stopped, now));
280
+ if (unsettled || hasGoal)
281
+ tx.saveGlobalRoleMessage(createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), owner.roleName, `Explicit Session stop confirmed: ${JSON.stringify({
282
+ nativeSessionId: selected.nativeSessionId, attemptId: input?.attemptId,
283
+ nativeTurnId: input?.nativeTurnId, status: stopped.run?.status ?? "quiescent",
284
+ evidence: terminal === undefined ? "native-resource-inspection" : "native-terminal"
285
+ })}`, "system", { type: "system" }, now));
286
+ }
287
+ else {
288
+ tx.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(current, stopped, now));
289
+ tx.saveEvent(owner.taskId, createTaskEvent(tx.nextEventId(owner.taskId), owner.taskId, "runtime.input-stop-confirmed", {
290
+ roleName: owner.roleName, ...(input == null ? {} : { attemptId: input.attemptId }),
291
+ nativeSessionId: selected.nativeSessionId, status: stopped.run?.status ?? "quiescent",
292
+ evidence: terminal === undefined ? "native-resource-inspection" : "native-terminal",
293
+ ...(input?.nativeTurnId === undefined ? {} : { nativeTurnId: input.nativeTurnId })
294
+ }, now));
295
+ }
278
296
  }
279
- else if (selected.adapterId === "codex") {
297
+ else if (selected.adapterId === "codex" && owner.scope === "task") {
280
298
  tx.saveEvent(owner.taskId, createTaskEvent(tx.nextEventId(owner.taskId), owner.taskId, "runtime.input-stop-confirmed", {
281
299
  roleName: owner.roleName, nativeSessionId: selected.nativeSessionId,
282
300
  status: "quiescent", evidence: "native-resource-inspection"