@zq-silk/yui 0.15.9 → 0.15.12

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 (160) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/agentRun/agentRun.js +3 -0
  6. package/dist/cli/commandCatalog.js +64 -16
  7. package/dist/cli/interactionPolicy.js +7 -3
  8. package/dist/cli/managedDiagnostics.js +1 -1
  9. package/dist/cli/updateOrchestrator.js +24 -1
  10. package/dist/cli/updatePorts.js +7 -3
  11. package/dist/cli/upgradeCommand.js +42 -2
  12. package/dist/cli.js +381 -107
  13. package/dist/commands/executionAuditCommands.js +10 -0
  14. package/dist/commands/globalRoleCommands.js +339 -4
  15. package/dist/commands/projectCommands.js +50 -22
  16. package/dist/commands/releaseCommands.js +18 -0
  17. package/dist/commands/taskActor.js +25 -0
  18. package/dist/commands/taskCommands.js +586 -96
  19. package/dist/commands/taskIntegrationCommands.js +19 -39
  20. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  21. package/dist/commands/taskOverviewCommand.js +4 -3
  22. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  23. package/dist/commands/taskPublicationCommands.js +11 -2
  24. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  25. package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
  26. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  27. package/dist/context/runContextPack.js +3 -0
  28. package/dist/context/taskCatalog.js +187 -0
  29. package/dist/context/taskContext.js +55 -6
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +14 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
  34. package/dist/controller/globalInputDelivery.js +132 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/providerRetryAdmission.js +100 -0
  37. package/dist/controller/providerRetryDelivery.js +218 -0
  38. package/dist/controller/resourceInventory.js +14 -4
  39. package/dist/controller/resourceInventoryLinux.js +2 -6
  40. package/dist/controller/runtime.js +117 -7
  41. package/dist/controller/runtimeEventInbox.js +32 -3
  42. package/dist/controller/runtimeEventProcessor.js +26 -6
  43. package/dist/controller/runtimeHookRunFence.js +75 -19
  44. package/dist/controller/structuredProviderObservation.js +133 -70
  45. package/dist/coordination/workMailboxQueue.js +5 -0
  46. package/dist/execution/workItemExecutionProjection.js +1 -1
  47. package/dist/executor/agentExecutor.js +64 -4
  48. package/dist/executor/executorRegistry.js +3 -0
  49. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  50. package/dist/integration/deliveryObligation.js +2 -1
  51. package/dist/integration/gitIntegrationService.js +329 -386
  52. package/dist/integration/integrationAttempt.js +30 -4
  53. package/dist/integration/integrationQueueService.js +7 -7
  54. package/dist/integration/integrationSourceApplication.js +323 -0
  55. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  56. package/dist/message/globalInterrupt.js +33 -0
  57. package/dist/message/globalProviderRetry.js +15 -0
  58. package/dist/message/inputControlResolution.js +106 -0
  59. package/dist/message/message.js +367 -0
  60. package/dist/message/messageContinuation.js +126 -3
  61. package/dist/message/taskInterrupt.js +34 -0
  62. package/dist/observability/executionAudit.js +19 -0
  63. package/dist/observability/orchestrationMetrics.js +1 -1
  64. package/dist/release/releaseHandover.js +22 -0
  65. package/dist/release/releaseWorkflowPorts.js +15 -7
  66. package/dist/repository/gitWorkspace.js +430 -107
  67. package/dist/repository/projectMaintenanceLock.js +75 -18
  68. package/dist/repository/taskWorkspaceCoordinator.js +182 -101
  69. package/dist/repository/taskWorkspacePreparer.js +205 -72
  70. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  71. package/dist/repository/workspaceCleanupInspection.js +187 -0
  72. package/dist/resources/resourceDiscovery.js +3 -2
  73. package/dist/runtime/agentError.js +5 -3
  74. package/dist/runtime/agentHost.js +179 -82
  75. package/dist/runtime/agentHostCompatibility.js +127 -0
  76. package/dist/runtime/agentHostProtocol.js +53 -0
  77. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  78. package/dist/runtime/codexAppServerRuntime.js +34 -3
  79. package/dist/runtime/executionEnvironment.js +0 -19
  80. package/dist/runtime/launchBroker.js +6 -0
  81. package/dist/runtime/providerControl.js +5 -1
  82. package/dist/runtime/providerRetry.js +198 -0
  83. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  84. package/dist/runtime/sessionReconciliation.js +4 -4
  85. package/dist/runtime/sessionTokenMetrics.js +15 -5
  86. package/dist/runtime/structuredProviderHost.js +6 -2
  87. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  88. package/dist/runtime/taskUsageMetrics.js +275 -0
  89. package/dist/runtime/tmuxAdapters.js +5 -3
  90. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  91. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  92. package/dist/scheduler/operatorEvent.js +4 -0
  93. package/dist/scheduler/taskExecutionProjection.js +38 -6
  94. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  95. package/dist/scheduler/wakeReason.js +7 -1
  96. package/dist/scheduler/wakeupQueue.js +2 -0
  97. package/dist/setup/setupCommand.js +26 -8
  98. package/dist/storage/homeLayout.js +130 -0
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  100. package/dist/storage/migrations/integrationContinuation.js +104 -0
  101. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  102. package/dist/storage/sqliteSchema.js +167 -4
  103. package/dist/storage/sqliteStore.js +57 -1
  104. package/dist/storage/storageVersions.js +1 -1
  105. package/dist/storage/storeRpc.js +2 -0
  106. package/dist/storage/taskCatalog.js +123 -0
  107. package/dist/storage/taskStore.js +2 -0
  108. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  109. package/dist/task/archiveDiagnostics.js +129 -0
  110. package/dist/task/archivePreflight.js +124 -0
  111. package/dist/task/nextAction.js +44 -11
  112. package/dist/task/publicationAdoption.js +56 -0
  113. package/dist/task/publicationReference.js +10 -0
  114. package/dist/task/remoteDelivery.js +31 -16
  115. package/dist/web/assets/client/app.js +147 -17
  116. package/dist/web/assets/client/components.js +56 -13
  117. package/dist/web/assets/client/i18n.js +78 -4
  118. package/dist/web/assets/client/taskSurface.js +108 -1
  119. package/dist/web/assets/client/view.js +39 -8
  120. package/dist/web/assets/shell.js +29 -0
  121. package/dist/web/assets/styles/layout.js +8 -1
  122. package/dist/web/assets/styles/widgets.js +12 -0
  123. package/dist/web/webServer.js +131 -4
  124. package/dist/web/webSnapshot.js +16 -6
  125. package/dist/web/webTaskSurface.js +222 -5
  126. package/dist/workspace/cleanupInspection.js +63 -0
  127. package/dist/workspace/workItemChangeSetManager.js +111 -35
  128. package/docs/agent-result-consumption.md +4 -0
  129. package/docs/agent-result-consumption.zh-CN.md +3 -0
  130. package/docs/agent-runtime-drivers.md +7 -0
  131. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  132. package/docs/architecture/README.md +2 -0
  133. package/docs/architecture/README.zh-CN.md +3 -1
  134. package/docs/architecture/capabilities-and-resources.md +30 -5
  135. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  136. package/docs/managed-turn-and-session-runtime.md +47 -0
  137. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  138. package/docs/observability/README.md +62 -0
  139. package/docs/observability/README.zh-CN.md +47 -0
  140. package/docs/project-refresh.md +77 -0
  141. package/docs/project-refresh.zh-CN.md +59 -0
  142. package/docs/provider-retry.md +70 -0
  143. package/docs/release-workflow.md +39 -0
  144. package/docs/release-workflow.zh-CN.md +29 -0
  145. package/docs/sqlite-control-plane-design.md +223 -1
  146. package/docs/task-delivery.md +133 -13
  147. package/docs/task-delivery.zh-CN.md +99 -10
  148. package/docs/task-discovery.md +102 -0
  149. package/docs/task-discovery.zh-CN.md +86 -0
  150. package/docs/testing/verification-levels.md +40 -0
  151. package/docs/testing/verification-levels.zh-CN.md +23 -0
  152. package/i18n/README.zh-CN.md +13 -7
  153. package/package.json +1 -1
  154. package/skills/yui-leader/references/execution.md +154 -51
  155. package/skills/yui-leader/references/integration.md +52 -2
  156. package/skills/yui-operator/SKILL.md +19 -3
  157. package/skills/yui-reviewer/SKILL.md +4 -0
  158. package/skills/yui-runtime/SKILL.md +42 -0
  159. package/skills/yui-runtime/references/publication.md +42 -0
  160. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -1,5 +1,4 @@
1
1
  import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
2
- import { resolve } from "node:path";
3
2
  import { isDeepStrictEqual } from "node:util";
4
3
  import { controllerSocketPath } from "../core/controllerEndpoint.js";
5
4
  import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
@@ -11,6 +10,7 @@ import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } fr
11
10
  import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
12
11
  import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
13
12
  import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
13
+ import { managedRuntimeRoot } from "../storage/homeLayout.js";
14
14
  import { SqliteTaskStore } from "../storage/sqliteStore.js";
15
15
  import { AsyncTaskStoreClient, resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
16
16
  import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
@@ -41,9 +41,14 @@ import { createResourceAutoGc } from "../resources/autoResourceGc.js";
41
41
  import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
42
42
  import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
43
43
  import { launchBrokerForHome } from "../runtime/launchBroker.js";
44
+ import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
44
45
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
45
46
  import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
46
47
  import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExitStore.js";
48
+ import { deliverGlobalInputs } from "./globalInputDelivery.js";
49
+ import { providerRetryPrompt } from "../runtime/providerRetry.js";
50
+ import { createProviderRetryHooks } from "./providerRetryDelivery.js";
51
+ import { createGlobalRoleMessage } from "../message/message.js";
47
52
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
48
53
  import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
49
54
  import { createTaskEvent } from "../event/taskEvent.js";
@@ -174,11 +179,14 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
174
179
  ?? new AgentHostPromptPushAdapter(home);
175
180
  const runtimeIsolation = options.runtimeIsolation
176
181
  ?? new FileTaskRuntimeIsolation({
177
- // A sibling of the exact control Home keeps provider data/cache/tmp out
178
- // of both the shared control plane and every managed Git workspace.
179
- runtimeRoot: `${resolve(home)}.task-runtimes`,
182
+ // Provider data/cache/tmp live in a dedicated Home partition. The
183
+ // control boundary permits runtime roots to overlap only this exact
184
+ // subtree, keeping them out of the database, managed Git workspaces and
185
+ // every other part of the control plane.
186
+ runtimeRoot: managedRuntimeRoot(home),
180
187
  controlPlane: {
181
188
  yuiHome: home,
189
+ managedRuntimeRoot: managedRuntimeRoot(home),
182
190
  controllerSocketPath: controllerSocketPath(homeId),
183
191
  tmuxNamespace: yuiTmuxServerName(home),
184
192
  globalInstallPaths: [process.execPath]
@@ -239,6 +247,31 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
239
247
  sessionHost,
240
248
  promptPush,
241
249
  launchCoordinator,
250
+ notifyOperatorInputOnce: async (input) => {
251
+ // The original attention mailbox remains pending until this one
252
+ // Global Message has an actual Provider receipt. Never inject keys
253
+ // into a busy Host console and mistake that for accepted attention.
254
+ const message = store.transaction(tx => {
255
+ const requestId = `operator-notice:${encodeURIComponent(input.receiptId)}`;
256
+ const previous = tx.listGlobalRoleMessages("operator").find(entry => (entry.inputControl ?? entry.interruptThen?.reusedInput)?.requestId === requestId);
257
+ if (previous !== undefined)
258
+ return previous;
259
+ const sessions = tx.getGlobalRoleSessionSet("operator");
260
+ const session = sessions?.sessions[sessions.activeAgentId];
261
+ if (session?.status !== "active" || sessions?.providerBinding == null)
262
+ return null;
263
+ const created = { ...createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), "operator", input.text, "system", { type: "system" }, new Date(), { inputControl: { action: "queue", requestId } }),
264
+ deliveryTarget: { agentId: session.agentId, nativeSessionId: session.nativeSessionId } };
265
+ tx.saveGlobalRoleMessage(created);
266
+ return created;
267
+ });
268
+ if (message === null)
269
+ return "unavailable";
270
+ if (message.delivery?.via === "provider")
271
+ return "already-sent";
272
+ runningRuntime?.signal("global-role:operator");
273
+ return "not-ready";
274
+ },
242
275
  roleResourceInventory: async (panes, inputs) => {
243
276
  const inventory = await scanInventory(panes);
244
277
  return inventory.resources.flatMap((resource) => {
@@ -439,7 +472,30 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
439
472
  now: options.now,
440
473
  onError: options.onError,
441
474
  lifecycleHost,
475
+ providerRetry: createProviderRetryHooks(home, store, {
476
+ now: options.now,
477
+ onError: options.onError,
478
+ snapshotTaskCandidate: async (run) => {
479
+ if (run.purpose !== "review" || run.reviewRoundId === undefined)
480
+ return undefined;
481
+ const round = store.getReviewRound(run.taskId, run.reviewRoundId);
482
+ if (round?.scope !== "task")
483
+ return undefined;
484
+ const task = store.getTask(run.taskId);
485
+ const workspace = store.getTaskWorkspace(run.taskId);
486
+ if (task === null || workspace === null || !(workspacePreparer instanceof FileTaskWorkspacePreparer)) {
487
+ throw new Error("Task-final retry requires an observable authoritative Task workspace.");
488
+ }
489
+ const snapshot = await workspacePreparer.snapshotDirectTaskMain(workspace, task.projectBindings.map(p => p.projectId));
490
+ return { schemaVersion: 1, projects: snapshot.projects.map(p => ({
491
+ projectId: p.projectId, commit: p.headCommit
492
+ })) };
493
+ }
494
+ }),
442
495
  jobSupervisor,
496
+ globalInputDelivery: () => deliverGlobalInputs(home, store, async (roleName) => {
497
+ await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
498
+ }, options.onError ?? (() => undefined)),
443
499
  jobControl,
444
500
  capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
445
501
  ...(continuationReconciler === undefined ? {} : { continuationReconciler }),
@@ -546,6 +602,20 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
546
602
  });
547
603
  const lifecycleTails = new Map();
548
604
  return async (method, params) => {
605
+ if (method === "runtime.host-observation-apply") {
606
+ const id = params.eventId;
607
+ if (typeof id !== "string")
608
+ throw applicationError("INVALID_PARAMS", "Host event id is required.");
609
+ // Eager delivery is only a hint about the exact immutable Inbox file.
610
+ // Missing means the normal drainer has already committed and ACKed it.
611
+ const event = new FileRuntimeEventInbox(store.rootDirectory()).read(id);
612
+ if (event === null)
613
+ return { outcome: "applied" };
614
+ if (event.type !== "runtime-observation" || event.host === undefined) {
615
+ throw applicationError("INVALID_PARAMS", "Expected an Agent Host observation.");
616
+ }
617
+ return { outcome: schedulerStore.observeAgentHostObservation(event, new Date()) };
618
+ }
549
619
  if (method === "runtime.observation-apply") {
550
620
  try {
551
621
  return {
@@ -569,6 +639,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
569
639
  authorityEpoch: value.authorityEpoch,
570
640
  authorityOwner: value.authorityOwner,
571
641
  holderId: value.holderId,
642
+ boundedText: value.boundedText,
643
+ retrySupported: value.retrySupported,
572
644
  now: value.now
573
645
  });
574
646
  }
@@ -580,7 +652,15 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
580
652
  }
581
653
  throw error;
582
654
  }
583
- return { recorded: true };
655
+ const binding = (value.taskId === undefined
656
+ ? store.getGlobalRoleSessionSet(value.roleName)
657
+ : store.getTaskRoleSessionSet(value.taskId, value.roleName))?.providerBinding;
658
+ const retry = binding?.retry;
659
+ return { recorded: true, ...(retry?.currentAttemptId === value.attemptId && value.boundedText !== undefined
660
+ ? { retryInput: {
661
+ text: providerRetryPrompt(retry, value.boundedText),
662
+ ...(retry.failedNativeTurnId === undefined ? {} : { expectedFailedNativeTurnId: retry.failedNativeTurnId })
663
+ } } : {}) };
584
664
  }
585
665
  if (method === "runtime.provider-turn-submission-resolve") {
586
666
  const value = providerTurnControlParams(params);
@@ -600,6 +680,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
600
680
  status,
601
681
  reason,
602
682
  raw,
683
+ noProviderWrite: params.providerWrite === "not-issued",
603
684
  now: value.now
604
685
  });
605
686
  return { recorded: true };
@@ -676,7 +757,34 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
676
757
  }
677
758
  // The launch payload is validated at reservation time and every member
678
759
  // of its discriminated Provider-control union is JSON serializable.
679
- return launchBrokerForHome(store.rootDirectory()).redeem(ticket);
760
+ const payload = launchBrokerForHome(store.rootDirectory()).redeem(ticket);
761
+ if (payload.executionEnvironment !== undefined) {
762
+ if (payload.environment.YUI_SESSION_SCOPE !== "task"
763
+ || payload.environment.YUI_TASK_ID !== payload.executionEnvironment.taskId
764
+ || payload.cwd !== payload.executionEnvironment.directory.path) {
765
+ throw applicationError("INVALID_PARAMS", "Launch does not match its adopted execution environment.");
766
+ }
767
+ assertExecutionEnvironmentCurrent(store, payload.executionEnvironment.taskId, payload.executionEnvironment);
768
+ }
769
+ return payload;
770
+ }
771
+ if (method === "runtime.execution-environment-check") {
772
+ const value = params;
773
+ const { taskId, roleName, agentId, nativeSessionId, workspace } = value;
774
+ if ([taskId, roleName, agentId, nativeSessionId, workspace].some(v => typeof v !== "string")) {
775
+ throw applicationError("INVALID_PARAMS", "Exact Host execution identity is required.");
776
+ }
777
+ const sessions = store.getTaskRoleSessionSet(taskId, roleName);
778
+ const session = sessions?.sessions[agentId];
779
+ if (sessions?.activeAgentId !== agentId || session?.status !== "active"
780
+ || session.nativeSessionId !== nativeSessionId
781
+ || (session.effective.executionEnvironment?.directory.path ?? session.effective.workspace.root) !== workspace) {
782
+ throw applicationError("INVALID_PARAMS", "Host no longer owns the current execution environment.");
783
+ }
784
+ if (session.effective.executionEnvironment !== undefined) {
785
+ assertExecutionEnvironmentCurrent(store, taskId, session.effective.executionEnvironment);
786
+ }
787
+ return { current: true };
680
788
  }
681
789
  if (method === "runtime.replace-agent-environment") {
682
790
  if (environmentRefresher === undefined) {
@@ -1123,7 +1231,7 @@ function providerTurnControlParams(params) {
1123
1231
  throw applicationError("INVALID_PARAMS", "Provider Turn control fence is invalid.");
1124
1232
  }
1125
1233
  return {
1126
- taskId: requiredParam(value.taskId),
1234
+ ...(value.taskId === undefined ? {} : { taskId: requiredParam(value.taskId) }),
1127
1235
  roleName: requiredParam(value.roleName),
1128
1236
  ...(value.runId === undefined ? {} : { runId: requiredParam(value.runId) }),
1129
1237
  agentId: requiredParam(value.agentId),
@@ -1132,6 +1240,8 @@ function providerTurnControlParams(params) {
1132
1240
  authorityEpoch: authorityEpoch,
1133
1241
  authorityOwner,
1134
1242
  holderId: requiredParam(value.holderId),
1243
+ ...(value.boundedText === undefined ? {} : { boundedText: requiredParam(value.boundedText) }),
1244
+ ...(value.retrySupport === undefined ? {} : { retrySupported: value.retrySupport === "codex-failed-turn-v1" }),
1135
1245
  now: new Date(observedAt)
1136
1246
  };
1137
1247
  }
@@ -4,6 +4,7 @@ import { closeSync, constants, existsSync, fchmodSync, fsyncSync, linkSync, lsta
4
4
  import { join } from "node:path";
5
5
  import { createRuntimeObservation, isRuntimeTokenEvidence } from "../runtime/runtimeObservation.js";
6
6
  import { boundedRunFailureDiagnostic, transportAgentResult } from "../domain/agentResultTransport.js";
7
+ import { readAgentHostObservationSource } from "../runtime/agentHostProtocol.js";
7
8
  export const MAX_RUNTIME_EVENT_FILE_BYTES = 16 * 1024 * 1024;
8
9
  const RUNTIME_EVENT_DIRECTORY = join("runtime", "inbox");
9
10
  const INVALID_RUNTIME_EVENT_DIRECTORY = join("runtime", "inbox-invalid");
@@ -24,7 +25,7 @@ export class FileRuntimeEventInbox {
24
25
  this.hooks = hooks;
25
26
  this.directory = join(home, RUNTIME_EVENT_DIRECTORY);
26
27
  }
27
- enqueueObservation(input) {
28
+ enqueueObservation(input, host) {
28
29
  const observation = createRuntimeObservation(input);
29
30
  const scope = observation.fence.taskId === undefined ? "global" : "task";
30
31
  const event = Object.freeze({
@@ -36,7 +37,8 @@ export class FileRuntimeEventInbox {
36
37
  ...(observation.fence.taskId === undefined
37
38
  ? {}
38
39
  : { taskId: observation.fence.taskId }),
39
- observation
40
+ observation,
41
+ ...(host === undefined ? {} : { host: readAgentHostObservationSource(host) })
40
42
  });
41
43
  return this.publish(event);
42
44
  }
@@ -117,6 +119,20 @@ export class FileRuntimeEventInbox {
117
119
  }
118
120
  return event;
119
121
  }
122
+ /** Producer-local delivery diagnostics need only existence, never a read of
123
+ * another version's payload or permission to quarantine another producer. */
124
+ has(id) {
125
+ assertEventId(id);
126
+ try {
127
+ lstatSync(this.eventPath(id));
128
+ return true;
129
+ }
130
+ catch (error) {
131
+ if (isNodeError(error, "ENOENT"))
132
+ return false;
133
+ throw error;
134
+ }
135
+ }
120
136
  acknowledge(id) {
121
137
  assertEventId(id);
122
138
  try {
@@ -408,6 +424,7 @@ function parseRuntimeEvent(value) {
408
424
  function parseRuntimeObservationEvent(value) {
409
425
  const expected = [
410
426
  "schemaVersion", "id", "type", "receivedAt", "scope", "observation",
427
+ ...(value.host === undefined ? [] : ["host"]),
411
428
  ...(value.taskId === undefined ? [] : ["taskId"])
412
429
  ];
413
430
  if (value.schemaVersion !== 1 || !hasExactKeys(value, expected))
@@ -426,9 +443,18 @@ function parseRuntimeObservationEvent(value) {
426
443
  receivedAt: observation.receivedAt,
427
444
  scope,
428
445
  ...(scope === "task" ? { taskId: observation.fence.taskId } : {}),
429
- observation
446
+ observation,
447
+ ...(value.host === undefined ? {} : { host: parseHostSource(value.host) })
430
448
  });
431
449
  }
450
+ function parseHostSource(value) {
451
+ try {
452
+ return readAgentHostObservationSource(value);
453
+ }
454
+ catch (error) {
455
+ throw invalidEvent(error instanceof Error ? error.message : String(error));
456
+ }
457
+ }
432
458
  function parseDurableJobTerminalEvent(value) {
433
459
  const expected = [
434
460
  "schemaVersion", "id", "type", "receivedAt", "scope", "taskId",
@@ -520,6 +546,9 @@ function assertEventId(id) {
520
546
  throw invalidEvent("Runtime event id is invalid.");
521
547
  }
522
548
  function hasSameIdentity(left, right) {
549
+ if (left.type === "runtime-observation" && right.type === "runtime-observation"
550
+ && !isDeepStrictEqual(left.host, right.host))
551
+ return false;
523
552
  if (left.type === "runtime-observation" && right.type === "runtime-observation"
524
553
  && ["turn.completed", "turn.failed", "turn.cancelled"].includes(left.observation.kind)) {
525
554
  return left.id === right.id
@@ -143,11 +143,17 @@ export class FileRuntimeEventProcessor {
143
143
  return { candidate, outcome };
144
144
  }
145
145
  applyRuntimeObservation(event, now) {
146
+ if (event.host !== undefined) {
147
+ return this.observer.observeAgentHostObservation?.(event, now) ?? "deferred";
148
+ }
146
149
  const taskId = event.observation.fence.taskId;
147
150
  if (taskId !== undefined) {
148
151
  const task = this.observer.getTask(taskId);
152
+ if (task?.status === "archived") {
153
+ this.recordObsolete(event, "task-archived", now);
154
+ return "obsolete";
155
+ }
149
156
  if (task === null
150
- || task.status === "archived"
151
157
  || (!["turn.completed", "turn.failed", "turn.cancelled"].includes(event.observation.kind)
152
158
  && (task.status !== "active" || task.executionGate.state !== "enabled")))
153
159
  return "obsolete";
@@ -247,7 +253,8 @@ export class FileRuntimeEventProcessor {
247
253
  agentId: fence.agentId,
248
254
  ...(fence.runId === undefined ? {} : { runId: fence.runId }),
249
255
  nativeSessionId: fence.nativeSessionId,
250
- reason
256
+ reason,
257
+ ...(reason === "task-archived" ? { originalEvent: event } : {})
251
258
  }, now);
252
259
  return;
253
260
  }
@@ -263,7 +270,8 @@ export class FileRuntimeEventProcessor {
263
270
  agentId: event.agentId,
264
271
  ...(event.runId === undefined ? {} : { runId: event.runId }),
265
272
  nativeSessionId: event.nativeSessionId,
266
- reason
273
+ reason,
274
+ ...(reason === "task-archived" ? { originalEvent: event } : {})
267
275
  }, now);
268
276
  }
269
277
  acknowledge(ids, acknowledged) {
@@ -394,6 +402,9 @@ function progressStreamKey(event) {
394
402
  fence.driverId,
395
403
  fence.nativeSessionId ?? null,
396
404
  fence.runId ?? null,
405
+ fence.receiptId ?? null,
406
+ fence.nativeTurnId ?? null,
407
+ event.host ?? null,
397
408
  payload.activity,
398
409
  isRuntimeTokenEvidence(event.observation) ? "usage" : "signal"
399
410
  ]);
@@ -510,6 +521,7 @@ export function createAsyncRuntimeObserver(invoke) {
510
521
  const withNow = (method, input, now) => now === undefined ? call(method, [input]) : call(method, [input, now]);
511
522
  return {
512
523
  getTask: (taskId) => call("getTask", [taskId]),
524
+ observeAgentHostObservation: (input, now) => withNow("observeAgentHostObservation", input, now),
513
525
  observeRuntimeObservation: (input, now) => withNow("observeRuntimeObservation", input, now),
514
526
  observeRuntimeRunTerminal: (input, now) => withNow("observeRuntimeRunTerminal", input, now),
515
527
  observeGlobalRuntimeRunTerminal: (input, now) => withNow("observeGlobalRuntimeRunTerminal", input, now),
@@ -605,11 +617,17 @@ export class AsyncRuntimeEventProcessor {
605
617
  };
606
618
  }
607
619
  async applyRuntimeObservation(event, now) {
620
+ if (event.host !== undefined) {
621
+ return this.observer.observeAgentHostObservation?.(event, now) ?? "deferred";
622
+ }
608
623
  const taskId = event.observation.fence.taskId;
609
624
  if (taskId !== undefined) {
610
625
  const task = await this.observer.getTask(taskId);
626
+ if (task?.status === "archived") {
627
+ await this.recordObsolete(event, "task-archived", now);
628
+ return "obsolete";
629
+ }
611
630
  if (task === null
612
- || task.status === "archived"
613
631
  || (!["turn.completed", "turn.failed", "turn.cancelled"].includes(event.observation.kind)
614
632
  && (task.status !== "active" || task.executionGate.state !== "enabled")))
615
633
  return "obsolete";
@@ -685,7 +703,8 @@ export class AsyncRuntimeEventProcessor {
685
703
  agentId: fence.agentId,
686
704
  ...(fence.runId === undefined ? {} : { runId: fence.runId }),
687
705
  nativeSessionId: fence.nativeSessionId,
688
- reason
706
+ reason,
707
+ ...(reason === "task-archived" ? { originalEvent: event } : {})
689
708
  }, now);
690
709
  return;
691
710
  }
@@ -697,7 +716,8 @@ export class AsyncRuntimeEventProcessor {
697
716
  agentId: event.agentId,
698
717
  ...(event.runId === undefined ? {} : { runId: event.runId }),
699
718
  nativeSessionId: event.nativeSessionId,
700
- reason
719
+ reason,
720
+ ...(reason === "task-archived" ? { originalEvent: event } : {})
701
721
  }, now);
702
722
  }
703
723
  acknowledge(ids, acknowledged) {
@@ -5,27 +5,66 @@ import { formatRunReceiptId } from "../task/taskRecordReference.js";
5
5
  import { currentProviderConversation, managedProviderTurnId } from "../runtime/providerRuntimeIdentity.js";
6
6
  import { taskRoleRuntimeIdentity } from "../runtime/managedCaller.js";
7
7
  import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
8
+ export class RuntimeHookRunFenceError extends Error {
9
+ }
8
10
  /**
9
11
  * Resolve observations by Session and exact input identity. A startup Hook
10
12
  * may introduce a Session for an explicit new-Session AgentRun.
11
13
  */
12
- export function resolveRuntimeHookRunFence(environment, adapterId, payloadNativeSessionId, options = {}) {
14
+ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNativeSessionId, options = {}, currentStore) {
15
+ if (currentStore === undefined) {
16
+ const store = openCurrentTaskStore(requireIdentity(environment.YUI_HOME, "YUI_HOME"));
17
+ try {
18
+ return resolveRuntimeHookRunFence(environment, adapterId, payloadNativeSessionId, options, store);
19
+ }
20
+ finally {
21
+ store.close();
22
+ }
23
+ }
24
+ if (environment.YUI_SESSION_SCOPE === "global") {
25
+ if (environment.YUI_ADAPTER_ID !== adapterId)
26
+ throw new RuntimeHookRunFenceError("Global observation adapter mismatch.");
27
+ const roleName = requireIdentity(environment.YUI_ROLE, "Role name");
28
+ const agentId = requireIdentity(environment.YUI_AGENT_ID, "Agent id");
29
+ const workspace = requireIdentity(environment.YUI_WORKSPACE, "YUI workspace");
30
+ const nativeSessionId = requireIdentity(payloadNativeSessionId, "Provider session id");
31
+ const sessions = currentStore.getGlobalRoleSessionSet(roleName);
32
+ const role = currentStore.getGlobalRole(roleName);
33
+ const session = sessions?.sessions[agentId];
34
+ const turn = sessions?.providerBinding?.run;
35
+ if (role?.activeAgentId !== agentId || sessions?.activeAgentId !== agentId || session?.adapterId !== adapterId
36
+ || session.status === "ended"
37
+ || session.nativeSessionId !== nativeSessionId || session.effective.workspace.root !== workspace) {
38
+ throw new RuntimeHookRunFenceError("Global observation Session does not match durable state.");
39
+ }
40
+ if (options.attemptId !== undefined && turn?.attemptId !== options.attemptId
41
+ || options.nativeTurnId !== undefined && turn?.nativeTurnId !== undefined
42
+ && turn.nativeTurnId !== options.nativeTurnId) {
43
+ throw new RuntimeHookRunFenceError("Global observation does not match the exact current input.");
44
+ }
45
+ if (options.exactInput && (turn == null
46
+ || options.attemptId === undefined && (options.nativeTurnId === undefined || turn.nativeTurnId !== options.nativeTurnId)
47
+ || ["rejected", "deferred"].includes(turn.status))) {
48
+ throw new RuntimeHookRunFenceError("Global observation input has no exact execution binding.");
49
+ }
50
+ return { roleName, agentId, workspace, nativeSessionId,
51
+ ...(options.sessionOnly || turn == null ? {} : { receiptId: turn.attemptId }) };
52
+ }
13
53
  if (environment.YUI_SESSION_SCOPE !== "task") {
14
- throw new Error("Runtime observation Hook requires a Task session scope.");
54
+ throw new RuntimeHookRunFenceError("Runtime observation Hook requires a Task session scope.");
15
55
  }
16
56
  if (environment.YUI_ADAPTER_ID !== adapterId) {
17
- throw new Error(`Runtime observation Hook requires the ${adapterId} adapter.`);
57
+ throw new RuntimeHookRunFenceError(`Runtime observation Hook requires the ${adapterId} adapter.`);
18
58
  }
19
- const home = requireIdentity(environment.YUI_HOME, "YUI_HOME");
20
59
  const taskId = requireIdentity(environment.YUI_TASK_ID, "Task id");
21
60
  const roleName = requireIdentity(environment.YUI_ROLE, "Role name");
22
61
  const agentId = requireIdentity(environment.YUI_AGENT_ID, "Agent id");
23
62
  const workspace = requireIdentity(environment.YUI_WORKSPACE, "YUI workspace");
24
63
  const nativeSessionId = requireIdentity(payloadNativeSessionId, "Provider session id");
25
- const store = openCurrentTaskStore(home);
64
+ const store = currentStore;
26
65
  const task = store.getTask(taskId);
27
66
  if (task === null) {
28
- throw new Error("Runtime observation Hook Task does not accept this lifecycle boundary.");
67
+ throw new RuntimeHookRunFenceError("Runtime observation Hook Task does not accept this lifecycle boundary.");
29
68
  }
30
69
  const role = store.getRole(taskId, roleName);
31
70
  const sessions = store.getTaskRoleSessionSet(taskId, roleName);
@@ -56,6 +95,14 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
56
95
  taskId, roleName, agentId, nativeSessionId,
57
96
  continuationId: options.continuationId,
58
97
  }));
98
+ if (options.exactInput && acceptedBinding === null && !matchesProviderTurn) {
99
+ throw new RuntimeHookRunFenceError("Runtime observation Hook input has no exact accepted execution binding.");
100
+ }
101
+ if (options.startupRunId !== undefined && options.startupRunId !== activeRun?.id) {
102
+ // A historical Session can report terminal evidence, but its old startup
103
+ // must not bind a new Session under a successor's active Run.
104
+ throw new RuntimeHookRunFenceError("Runtime observation Hook startup does not match its exact launch Run.");
105
+ }
59
106
  // Collection of an exactly accepted terminal fact is not a new action by
60
107
  // the Role. A successor's active Agent or revoked execution permission
61
108
  // cannot erase the original AgentRun's evidence.
@@ -70,17 +117,26 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
70
117
  && activeRun.purpose === "planning"
71
118
  && activeRun.roleName === roleName
72
119
  && runPurposeAdmitsTaskState(activeRun.purpose, task);
120
+ // Restoring the Draft Leader's existing planning Session needs no new Run.
121
+ // Admit its Session facts; the shared identity/workspace fence below still
122
+ // requires the exact current Session and never grants delivery authority.
123
+ const planningSessionObservation = options.sessionOnly === true
124
+ && task.status === "draft" && roleName === "leader"
125
+ && runPurposeAdmitsTaskState("planning", task)
126
+ && session?.status === "active"
127
+ && session.effective.executionAuthority === "planning";
73
128
  if (!(task.status === "active" && task.executionGate.state === "enabled")
74
129
  && !(task.status === "completed" && options.sessionOnly === true)
75
130
  && !planningObservation
131
+ && !planningSessionObservation
76
132
  && !existingExecutionObservation) {
77
- throw new Error("Runtime observation Hook Task does not accept this lifecycle boundary.");
133
+ throw new RuntimeHookRunFenceError("Runtime observation Hook Task does not accept this lifecycle boundary.");
78
134
  }
79
135
  const runtimeIdentity = role === null ? null : taskRoleRuntimeIdentity(role, activeRun);
80
136
  if (!existingExecutionObservation && (runtimeIdentity === null || runtimeIdentity.agentId !== agentId
81
137
  || runtimeIdentity.adapterId !== adapterId
82
138
  || (sessions !== null && sessions.activeAgentId !== agentId))) {
83
- throw new Error("Runtime observation Hook Role or Agent is not current.");
139
+ throw new RuntimeHookRunFenceError("Runtime observation Hook Role or Agent is not current.");
84
140
  }
85
141
  const lifecycleMailbox = store.getWorkMailbox(runtimeLifecycleTarget({
86
142
  scope: "task",
@@ -97,7 +153,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
97
153
  || observedSession.adapterId !== adapterId
98
154
  || observedSession.nativeSessionId !== nativeSessionId
99
155
  || observedSession.effective.workspace.root !== workspace) {
100
- throw new Error("Runtime observation Hook Session does not match durable state.");
156
+ throw new RuntimeHookRunFenceError("Runtime observation Hook Session does not match durable state.");
101
157
  }
102
158
  return {
103
159
  taskId,
@@ -146,7 +202,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
146
202
  ? managedProviderTurnId(providerTurn) ?? undefined
147
203
  : undefined;
148
204
  if (options.terminal === true && acceptedBinding === null && registeredRunId === undefined) {
149
- throw new Error("Runtime observation Hook terminal has no exact accepted execution binding.");
205
+ throw new RuntimeHookRunFenceError("Runtime observation Hook terminal has no exact accepted execution binding.");
150
206
  }
151
207
  const terminalRun = acceptedBinding !== null
152
208
  ? store.getRun(taskId, acceptedBinding.fence.runId)
@@ -166,7 +222,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
166
222
  && !exactTerminal
167
223
  && registeredRunId === undefined
168
224
  && acceptedBinding === null) {
169
- throw new Error("Runtime observation Hook has no matching durable in-flight AgentRun.");
225
+ throw new RuntimeHookRunFenceError("Runtime observation Hook has no matching durable in-flight AgentRun.");
170
226
  }
171
227
  const runId = acceptedBinding?.fence.runId
172
228
  ?? registeredRunId
@@ -181,21 +237,21 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
181
237
  || run.roleName !== roleName
182
238
  || run.effective.agentId !== agentId
183
239
  || run.effective.adapterId !== adapterId) {
184
- throw new Error("Runtime observation Hook AgentRun does not match durable active state.");
240
+ throw new RuntimeHookRunFenceError("Runtime observation Hook AgentRun does not match durable active state.");
185
241
  }
186
242
  if (run.effective.workspace.root !== workspace) {
187
- throw new Error("Runtime observation Hook workspace does not match the durable AgentRun snapshot.");
243
+ throw new RuntimeHookRunFenceError("Runtime observation Hook workspace does not match the durable AgentRun snapshot.");
188
244
  }
189
245
  if (session !== undefined && acceptedBinding === null && !matchesProviderTurn && !replacementStartup && !resumedStartup) {
190
246
  if (session.adapterId !== adapterId
191
247
  || session.nativeSessionId !== nativeSessionId
192
248
  || session.effective.workspace.root !== workspace) {
193
- throw new Error("Runtime observation Hook Session does not match durable state.");
249
+ throw new RuntimeHookRunFenceError("Runtime observation Hook Session does not match durable state.");
194
250
  }
195
251
  }
196
252
  else if (acceptedBinding === null && !matchesProviderTurn && (session === undefined || replacementStartup)) {
197
253
  if (!discoveredStartup && !preallocatedStartup) {
198
- throw new Error("Runtime observation Hook has no matching new-Session intent.");
254
+ throw new RuntimeHookRunFenceError("Runtime observation Hook has no matching new-Session intent.");
199
255
  }
200
256
  }
201
257
  return {
@@ -230,7 +286,7 @@ function knownContinuationBinding(events, expected) {
230
286
  return null;
231
287
  if (matches.some((candidate) => candidate.fence.runId !== binding.fence.runId
232
288
  || candidate.fence.receiptId !== binding.fence.receiptId)) {
233
- throw new Error("Runtime observation Hook continuation has conflicting durable AgentRun bindings.");
289
+ throw new RuntimeHookRunFenceError("Runtime observation Hook continuation has conflicting durable AgentRun bindings.");
234
290
  }
235
291
  return binding;
236
292
  }
@@ -260,17 +316,17 @@ function acceptedRunBinding(events, expected) {
260
316
  && candidate.fence.nativeTurnId !== expected.nativeTurnId)
261
317
  || candidate.fence.runId !== binding.fence.runId
262
318
  || candidate.fence.receiptId !== binding.fence.receiptId))) {
263
- throw new Error("Runtime observation Hook native Turn has conflicting durable AgentRun bindings.");
319
+ throw new RuntimeHookRunFenceError("Runtime observation Hook native Turn has conflicting durable AgentRun bindings.");
264
320
  }
265
321
  return binding;
266
322
  }
267
323
  function requireIdentity(value, label) {
268
324
  if (typeof value !== "string" || value.includes("\0")) {
269
- throw new Error(`${label} is required.`);
325
+ throw new RuntimeHookRunFenceError(`${label} is required.`);
270
326
  }
271
327
  const normalized = value.trim();
272
328
  if (normalized.length === 0 || normalized.length > 1_024) {
273
- throw new Error(`${label} is invalid.`);
329
+ throw new RuntimeHookRunFenceError(`${label} is invalid.`);
274
330
  }
275
331
  return normalized;
276
332
  }