@zq-silk/yui 0.15.9 → 0.15.11

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 (100) hide show
  1. package/dist/agent/launchEnvironment.js +7 -0
  2. package/dist/cli/commandCatalog.js +26 -6
  3. package/dist/cli/interactionPolicy.js +3 -3
  4. package/dist/cli/updateOrchestrator.js +24 -1
  5. package/dist/cli/updatePorts.js +7 -3
  6. package/dist/cli/upgradeCommand.js +42 -2
  7. package/dist/cli.js +336 -93
  8. package/dist/commands/globalRoleCommands.js +314 -4
  9. package/dist/commands/projectCommands.js +6 -7
  10. package/dist/commands/releaseCommands.js +18 -0
  11. package/dist/commands/taskActor.js +25 -0
  12. package/dist/commands/taskCommands.js +511 -83
  13. package/dist/commands/taskIntegrationCommands.js +16 -38
  14. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  15. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  16. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  17. package/dist/context/taskContext.js +38 -2
  18. package/dist/controller/agentHostObservation.js +155 -0
  19. package/dist/controller/clientRuntime.js +17 -2
  20. package/dist/controller/controller.js +3 -0
  21. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  22. package/dist/controller/globalInputDelivery.js +119 -0
  23. package/dist/controller/jobControl.js +6 -2
  24. package/dist/controller/resourceInventory.js +14 -4
  25. package/dist/controller/resourceInventoryLinux.js +2 -6
  26. package/dist/controller/runtime.js +81 -6
  27. package/dist/controller/runtimeEventInbox.js +32 -3
  28. package/dist/controller/runtimeEventProcessor.js +26 -6
  29. package/dist/controller/runtimeHookRunFence.js +75 -19
  30. package/dist/controller/structuredProviderObservation.js +133 -70
  31. package/dist/coordination/workMailboxQueue.js +5 -0
  32. package/dist/execution/workItemExecutionProjection.js +1 -1
  33. package/dist/executor/agentExecutor.js +64 -4
  34. package/dist/executor/executorRegistry.js +3 -0
  35. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  36. package/dist/integration/deliveryObligation.js +2 -1
  37. package/dist/integration/gitIntegrationService.js +312 -382
  38. package/dist/integration/integrationAttempt.js +30 -4
  39. package/dist/integration/integrationQueueService.js +7 -7
  40. package/dist/integration/integrationSourceApplication.js +323 -0
  41. package/dist/message/globalInterrupt.js +33 -0
  42. package/dist/message/inputControlResolution.js +106 -0
  43. package/dist/message/message.js +367 -0
  44. package/dist/message/messageContinuation.js +126 -3
  45. package/dist/message/taskInterrupt.js +34 -0
  46. package/dist/observability/orchestrationMetrics.js +1 -1
  47. package/dist/release/releaseHandover.js +22 -0
  48. package/dist/release/releaseWorkflowPorts.js +15 -7
  49. package/dist/repository/gitWorkspace.js +72 -15
  50. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  51. package/dist/repository/taskWorkspacePreparer.js +120 -49
  52. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  53. package/dist/resources/resourceDiscovery.js +3 -2
  54. package/dist/runtime/agentHost.js +152 -72
  55. package/dist/runtime/agentHostCompatibility.js +127 -0
  56. package/dist/runtime/agentHostProtocol.js +53 -0
  57. package/dist/runtime/executionEnvironment.js +0 -19
  58. package/dist/runtime/launchBroker.js +6 -0
  59. package/dist/runtime/sessionReconciliation.js +4 -4
  60. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  61. package/dist/runtime/tmuxAdapters.js +5 -3
  62. package/dist/scheduler/operatorEvent.js +4 -0
  63. package/dist/scheduler/taskExecutionProjection.js +12 -1
  64. package/dist/scheduler/wakeReason.js +7 -1
  65. package/dist/scheduler/wakeupQueue.js +2 -0
  66. package/dist/setup/setupCommand.js +26 -8
  67. package/dist/storage/homeLayout.js +130 -0
  68. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  69. package/dist/storage/migrations/integrationContinuation.js +104 -0
  70. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  71. package/dist/storage/sqliteSchema.js +136 -4
  72. package/dist/storage/sqliteStore.js +40 -1
  73. package/dist/storage/storageVersions.js +1 -1
  74. package/dist/storage/storeRpc.js +1 -0
  75. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  76. package/dist/task/archiveDiagnostics.js +128 -0
  77. package/dist/task/nextAction.js +44 -11
  78. package/dist/web/assets/client/app.js +55 -0
  79. package/dist/web/assets/client/components.js +1 -0
  80. package/dist/web/assets/client/i18n.js +6 -0
  81. package/dist/web/assets/client/taskSurface.js +106 -0
  82. package/dist/web/assets/client/view.js +7 -4
  83. package/dist/web/assets/shell.js +23 -0
  84. package/dist/web/assets/styles/layout.js +1 -1
  85. package/dist/web/assets/styles/widgets.js +12 -0
  86. package/dist/web/webServer.js +117 -1
  87. package/dist/web/webSnapshot.js +4 -3
  88. package/dist/web/webTaskSurface.js +222 -5
  89. package/dist/workspace/workItemChangeSetManager.js +18 -2
  90. package/docs/release-workflow.md +39 -0
  91. package/docs/release-workflow.zh-CN.md +29 -0
  92. package/docs/sqlite-control-plane-design.md +223 -1
  93. package/docs/testing/verification-levels.md +24 -0
  94. package/docs/testing/verification-levels.zh-CN.md +11 -0
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +151 -49
  97. package/skills/yui-leader/references/integration.md +52 -2
  98. package/skills/yui-operator/SKILL.md +6 -1
  99. package/skills/yui-runtime/SKILL.md +27 -0
  100. package/skills/yui-runtime/references/publication.md +20 -0
@@ -0,0 +1,119 @@
1
+ import { sendAgentHostRunControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
2
+ import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
3
+ import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
4
+ import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
5
+ /** Messages are the only queue. Host's atomic begin arbitrates native writers. */
6
+ export async function deliverGlobalInputs(home, store, ensure, onError) {
7
+ for (const role of store.listGlobalRoles()) {
8
+ try {
9
+ let pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
10
+ && (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
11
+ if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
12
+ continue;
13
+ let sessions = store.getGlobalRoleSessionSet(role.name);
14
+ const current = sessions?.providerBinding?.run;
15
+ const obsolete = pending.filter(message => {
16
+ const claim = message.interruptThen;
17
+ return claim !== undefined && (sessions?.activeAgentId !== claim.targetAgentId
18
+ || sessions.sessions[claim.targetAgentId]?.nativeSessionId !== claim.targetNativeSessionId
19
+ || sessions.providerBinding?.authority.epoch !== claim.targetAuthorityEpoch
20
+ || sessions.providerBinding.authority.holderId !== claim.targetAuthorityHolderId);
21
+ });
22
+ for (const message of obsolete)
23
+ store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, "native-session-or-authority-changed", new Date()));
24
+ pending = pending.filter(message => !obsolete.includes(message));
25
+ if (pending.length === 0)
26
+ continue;
27
+ if (current != null && ["submitting", "accepted", "delivery-unknown"].includes(current.status))
28
+ continue;
29
+ const claims = pending.filter(message => message.interruptThen !== undefined);
30
+ let next;
31
+ if (claims.length > 0) {
32
+ next = claims.find(message => {
33
+ const claim = message.interruptThen;
34
+ const binding = sessions?.providerBinding;
35
+ return current?.attemptId === claim.targetAttemptId
36
+ && ["completed", "failed", "cancelled"].includes(current.status)
37
+ && sessions?.activeAgentId === claim.targetAgentId
38
+ && sessions.sessions[claim.targetAgentId]?.nativeSessionId === claim.targetNativeSessionId
39
+ && binding?.authority.owner === "controller"
40
+ && binding.authority.epoch === claim.targetAuthorityEpoch
41
+ && binding.authority.holderId === claim.targetAuthorityHolderId;
42
+ });
43
+ if (next === undefined)
44
+ continue;
45
+ }
46
+ else
47
+ next = pending[0];
48
+ const attemptId = `global-input:${role.name}/${next.id}`;
49
+ if (current?.attemptId === attemptId)
50
+ continue;
51
+ if (next.control !== undefined && next.control.outcome !== "rejected")
52
+ continue;
53
+ const matchesTarget = () => next.deliveryTarget === undefined
54
+ || sessions?.activeAgentId === next.deliveryTarget.agentId
55
+ && sessions.sessions[next.deliveryTarget.agentId]?.nativeSessionId === next.deliveryTarget.nativeSessionId;
56
+ if (!matchesTarget()) {
57
+ store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(next, "native-session-changed", new Date()));
58
+ continue;
59
+ }
60
+ await ensure(role.name);
61
+ sessions = store.getGlobalRoleSessionSet(role.name);
62
+ const binding = sessions?.providerBinding;
63
+ if (binding == null || binding.authority.owner !== "controller")
64
+ continue;
65
+ const nativeSessionId = currentProviderConversation(binding).conversationId;
66
+ const persisted = store.listGlobalRoleMessages(role.name).find(message => message.id === next.id);
67
+ if (persisted === undefined || JSON.stringify(persisted) !== JSON.stringify(next))
68
+ continue;
69
+ const claim = next.interruptThen;
70
+ if (!matchesTarget() || claim !== undefined
71
+ && (sessions?.activeAgentId !== claim.targetAgentId
72
+ || nativeSessionId !== claim.targetNativeSessionId
73
+ || binding.authority.epoch !== claim.targetAuthorityEpoch
74
+ || binding.authority.holderId !== claim.targetAuthorityHolderId
75
+ || binding.run?.attemptId !== claim.targetAttemptId
76
+ || !["completed", "failed", "cancelled"].includes(binding.run.status))) {
77
+ store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(next, "native-session-changed", new Date()));
78
+ continue;
79
+ }
80
+ if (next.deliveryTarget === undefined) {
81
+ next = { ...next, deliveryTarget: { agentId: sessions.activeAgentId, nativeSessionId } };
82
+ store.updateGlobalRoleMessage(next);
83
+ }
84
+ const result = await sendAgentHostRunControl({
85
+ home, scope: "global", roleName: role.name,
86
+ control: {
87
+ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn", nativeSessionId,
88
+ authority: { epoch: binding.authority.epoch, owner: "controller",
89
+ holderId: binding.authority.holderId },
90
+ run: { attemptId, boundedText: [
91
+ `Yui Global Message: role=${role.name} message=${next.id}.`,
92
+ "Read your Session Context and the referenced Message in full, then act on its durable input."
93
+ ].join("\n") }
94
+ }
95
+ });
96
+ if (result.outcome === "rejected") {
97
+ store.transaction(tx => {
98
+ const latest = tx.listGlobalRoleMessages(role.name).find(message => message.id === next.id);
99
+ if (latest === undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
100
+ return;
101
+ const turn = tx.getGlobalRoleSessionSet(role.name)?.providerBinding?.run;
102
+ // A later canonical acceptance wins over transport diagnostics.
103
+ if (turn?.attemptId === attemptId && ["accepted", "completed", "failed", "cancelled"].includes(turn.status))
104
+ return;
105
+ const unknown = result.failure?.inputDisposition === "unknown" || result.snapshot.state === "delivery-unknown";
106
+ const updated = { ...latest, control: {
107
+ requestId: latest.inputControl?.requestId ?? latest.interruptThen.requestId,
108
+ receiptId: attemptId, outcome: unknown ? "delivery-unknown" : "rejected",
109
+ observedAt: new Date().toISOString()
110
+ } };
111
+ tx.updateGlobalRoleMessage(unknown ? updated : markGlobalRoleMessageNotDelivered(updated, result.failure?.detail ?? result.snapshot.detail ?? "provider-input-rejected", new Date()));
112
+ });
113
+ }
114
+ }
115
+ catch (error) {
116
+ onError(error);
117
+ }
118
+ }
119
+ }
@@ -253,10 +253,14 @@ function validateJobTarget(store, params) {
253
253
  if (attempt === null) {
254
254
  throw jobDomainError(`Integration Attempt not found: ${params.taskId}/${params.owner.integrationAttemptId}.`);
255
255
  }
256
- if (attempt.status === "committed" || attempt.status === "failed") {
257
- throw jobDomainError(`DurableJob owner Integration Attempt is terminal: `
256
+ if (attempt.status !== "running") {
257
+ throw jobDomainError(`DurableJob owner Integration Attempt must be running: `
258
258
  + `${params.taskId}/${params.owner.integrationAttemptId} is ${attempt.status}.`);
259
259
  }
260
+ if (attempt.candidateCommit !== params.head || attempt.checkInputDigest === undefined
261
+ || attempt.checkInputDigest !== durableJobIdempotencyKey(params)) {
262
+ throw jobDomainError("Integration Job has no matching admitted candidate/check specification.");
263
+ }
260
264
  }
261
265
  const project = store.getProject(params.projectId);
262
266
  if (project === null) {
@@ -1,4 +1,11 @@
1
1
  import { resolve } from "node:path";
2
+ /** Archiving may retain execution; a Task lifecycle label is not exit proof. */
3
+ export function runtimeRoleProtectsDomain(role) {
4
+ return (role.ownerKind === "task-role"
5
+ && (role.taskStatus === "active" || role.taskStatus === "archived"))
6
+ // Global Roles have no Task lifecycle; require a current native Session.
7
+ || (role.ownerKind === "global-role" && role.nativeSessionId !== undefined);
8
+ }
2
9
  export function createRuntimeResourceActivityTracker() {
3
10
  const previous = new Map();
4
11
  return (identity, resource) => {
@@ -112,8 +119,7 @@ export function buildControllerResourceInventory(facts) {
112
119
  for (const process of paneProcesses)
113
120
  claimed.add(process.pid);
114
121
  const role = findRole(homeFact.roles, pane);
115
- const terminalIsolation = (role?.taskStatus === "cancelled" && role.taskRetirementIsolated === true)
116
- || role?.taskStatus === "archived";
122
+ const terminalIsolation = role?.taskStatus === "cancelled" && role.taskRetirementIsolated === true;
117
123
  resources.push(processResource({
118
124
  kind: "agent-session",
119
125
  state: pane.dead ? "dead" : role === undefined ? "orphaned" : "running",
@@ -122,7 +128,8 @@ export function buildControllerResourceInventory(facts) {
122
128
  : terminalIsolation ? "safe" : "protected",
123
129
  reasonCode: role === undefined
124
130
  ? pane.dead ? "dead-orphan-pane" : "orphan-pane"
125
- : terminalIsolation ? (role.taskStatus === "cancelled" ? "retired-task-pane" : "archived-task-pane") : "owned-role-pane",
131
+ : terminalIsolation ? "retired-task-pane"
132
+ : role.taskStatus === "archived" ? "archived-task-pane" : "owned-role-pane",
126
133
  yuiHome,
127
134
  owner: role === undefined ? { kind: "none" } : roleOwner(role),
128
135
  processes: paneProcesses,
@@ -358,7 +365,10 @@ function artifactResource(artifact, yuiHome, domain, homeId) {
358
365
  };
359
366
  }
360
367
  function domainDisposition(base, reason, domain, target) {
361
- if (reason === "retired-task-pane" || reason === "archived-task-pane") {
368
+ // Even an expired test domain cannot authorize deleting retained execution.
369
+ if (reason === "archived-task-pane")
370
+ return "protected";
371
+ if (reason === "retired-task-pane") {
362
372
  return "safe";
363
373
  }
364
374
  if (domain === undefined)
@@ -11,7 +11,7 @@ import { readControllerDiscovery } from "../core/controllerClient.js";
11
11
  import { CONTROLLER_DISCOVERY_PATH } from "../core/protocol.js";
12
12
  import { controllerSocketPath } from "../core/controllerEndpoint.js";
13
13
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
14
- import { buildControllerResourceInventory } from "./resourceInventory.js";
14
+ import { buildControllerResourceInventory, runtimeRoleProtectsDomain } from "./resourceInventory.js";
15
15
  import { CONTROLLER_DOMAIN_PATH, EPHEMERAL_DOMAIN_GRACE_MS, ephemeralDomainFingerprint, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "./domainIdentity.js";
16
16
  const LINUX_PROCESS_SCAN_BATCH_SIZE = 64;
17
17
  export const INVENTORY_EVENT_LOOP_RUN_BUDGET_MS = 25;
@@ -518,11 +518,7 @@ function inspectRuntimeDomain(home, processes, roles, storageStatus, now) {
518
518
  graceMs: EPHEMERAL_DOMAIN_GRACE_MS
519
519
  };
520
520
  }
521
- const activeRole = roles.some((role) => ((role.ownerKind === "task-role" && role.taskStatus === "active")
522
- // Global Roles have no Task status. Only a live native Session is a
523
- // durable liveness fact for that scope; stopped/broken history must not
524
- // protect an expired disposable domain merely because it has an ID.
525
- || (role.ownerKind === "global-role" && role.nativeSessionId !== undefined)));
521
+ const activeRole = roles.some(runtimeRoleProtectsDomain);
526
522
  if (!storageSafe) {
527
523
  return {
528
524
  kind: "ephemeral-test",
@@ -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,12 @@ 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 { createGlobalRoleMessage } from "../message/message.js";
47
50
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
48
51
  import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
49
52
  import { createTaskEvent } from "../event/taskEvent.js";
@@ -174,11 +177,14 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
174
177
  ?? new AgentHostPromptPushAdapter(home);
175
178
  const runtimeIsolation = options.runtimeIsolation
176
179
  ?? 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`,
180
+ // Provider data/cache/tmp live in a dedicated Home partition. The
181
+ // control boundary permits runtime roots to overlap only this exact
182
+ // subtree, keeping them out of the database, managed Git workspaces and
183
+ // every other part of the control plane.
184
+ runtimeRoot: managedRuntimeRoot(home),
180
185
  controlPlane: {
181
186
  yuiHome: home,
187
+ managedRuntimeRoot: managedRuntimeRoot(home),
182
188
  controllerSocketPath: controllerSocketPath(homeId),
183
189
  tmuxNamespace: yuiTmuxServerName(home),
184
190
  globalInstallPaths: [process.execPath]
@@ -239,6 +245,31 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
239
245
  sessionHost,
240
246
  promptPush,
241
247
  launchCoordinator,
248
+ notifyOperatorInputOnce: async (input) => {
249
+ // The original attention mailbox remains pending until this one
250
+ // Global Message has an actual Provider receipt. Never inject keys
251
+ // into a busy Host console and mistake that for accepted attention.
252
+ const message = store.transaction(tx => {
253
+ const requestId = `operator-notice:${encodeURIComponent(input.receiptId)}`;
254
+ const previous = tx.listGlobalRoleMessages("operator").find(entry => (entry.inputControl ?? entry.interruptThen?.reusedInput)?.requestId === requestId);
255
+ if (previous !== undefined)
256
+ return previous;
257
+ const sessions = tx.getGlobalRoleSessionSet("operator");
258
+ const session = sessions?.sessions[sessions.activeAgentId];
259
+ if (session?.status !== "active" || sessions?.providerBinding == null)
260
+ return null;
261
+ const created = { ...createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), "operator", input.text, "system", { type: "system" }, new Date(), { inputControl: { action: "queue", requestId } }),
262
+ deliveryTarget: { agentId: session.agentId, nativeSessionId: session.nativeSessionId } };
263
+ tx.saveGlobalRoleMessage(created);
264
+ return created;
265
+ });
266
+ if (message === null)
267
+ return "unavailable";
268
+ if (message.delivery?.via === "provider")
269
+ return "already-sent";
270
+ runningRuntime?.signal("global-role:operator");
271
+ return "not-ready";
272
+ },
242
273
  roleResourceInventory: async (panes, inputs) => {
243
274
  const inventory = await scanInventory(panes);
244
275
  return inventory.resources.flatMap((resource) => {
@@ -440,6 +471,9 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
440
471
  onError: options.onError,
441
472
  lifecycleHost,
442
473
  jobSupervisor,
474
+ globalInputDelivery: () => deliverGlobalInputs(home, store, async (roleName) => {
475
+ await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
476
+ }, options.onError ?? (() => undefined)),
443
477
  jobControl,
444
478
  capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
445
479
  ...(continuationReconciler === undefined ? {} : { continuationReconciler }),
@@ -546,6 +580,20 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
546
580
  });
547
581
  const lifecycleTails = new Map();
548
582
  return async (method, params) => {
583
+ if (method === "runtime.host-observation-apply") {
584
+ const id = params.eventId;
585
+ if (typeof id !== "string")
586
+ throw applicationError("INVALID_PARAMS", "Host event id is required.");
587
+ // Eager delivery is only a hint about the exact immutable Inbox file.
588
+ // Missing means the normal drainer has already committed and ACKed it.
589
+ const event = new FileRuntimeEventInbox(store.rootDirectory()).read(id);
590
+ if (event === null)
591
+ return { outcome: "applied" };
592
+ if (event.type !== "runtime-observation" || event.host === undefined) {
593
+ throw applicationError("INVALID_PARAMS", "Expected an Agent Host observation.");
594
+ }
595
+ return { outcome: schedulerStore.observeAgentHostObservation(event, new Date()) };
596
+ }
549
597
  if (method === "runtime.observation-apply") {
550
598
  try {
551
599
  return {
@@ -676,7 +724,34 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
676
724
  }
677
725
  // The launch payload is validated at reservation time and every member
678
726
  // of its discriminated Provider-control union is JSON serializable.
679
- return launchBrokerForHome(store.rootDirectory()).redeem(ticket);
727
+ const payload = launchBrokerForHome(store.rootDirectory()).redeem(ticket);
728
+ if (payload.executionEnvironment !== undefined) {
729
+ if (payload.environment.YUI_SESSION_SCOPE !== "task"
730
+ || payload.environment.YUI_TASK_ID !== payload.executionEnvironment.taskId
731
+ || payload.cwd !== payload.executionEnvironment.directory.path) {
732
+ throw applicationError("INVALID_PARAMS", "Launch does not match its adopted execution environment.");
733
+ }
734
+ assertExecutionEnvironmentCurrent(store, payload.executionEnvironment.taskId, payload.executionEnvironment);
735
+ }
736
+ return payload;
737
+ }
738
+ if (method === "runtime.execution-environment-check") {
739
+ const value = params;
740
+ const { taskId, roleName, agentId, nativeSessionId, workspace } = value;
741
+ if ([taskId, roleName, agentId, nativeSessionId, workspace].some(v => typeof v !== "string")) {
742
+ throw applicationError("INVALID_PARAMS", "Exact Host execution identity is required.");
743
+ }
744
+ const sessions = store.getTaskRoleSessionSet(taskId, roleName);
745
+ const session = sessions?.sessions[agentId];
746
+ if (sessions?.activeAgentId !== agentId || session?.status !== "active"
747
+ || session.nativeSessionId !== nativeSessionId
748
+ || (session.effective.executionEnvironment?.directory.path ?? session.effective.workspace.root) !== workspace) {
749
+ throw applicationError("INVALID_PARAMS", "Host no longer owns the current execution environment.");
750
+ }
751
+ if (session.effective.executionEnvironment !== undefined) {
752
+ assertExecutionEnvironmentCurrent(store, taskId, session.effective.executionEnvironment);
753
+ }
754
+ return { current: true };
680
755
  }
681
756
  if (method === "runtime.replace-agent-environment") {
682
757
  if (environmentRefresher === undefined) {
@@ -1123,7 +1198,7 @@ function providerTurnControlParams(params) {
1123
1198
  throw applicationError("INVALID_PARAMS", "Provider Turn control fence is invalid.");
1124
1199
  }
1125
1200
  return {
1126
- taskId: requiredParam(value.taskId),
1201
+ ...(value.taskId === undefined ? {} : { taskId: requiredParam(value.taskId) }),
1127
1202
  roleName: requiredParam(value.roleName),
1128
1203
  ...(value.runId === undefined ? {} : { runId: requiredParam(value.runId) }),
1129
1204
  agentId: requiredParam(value.agentId),
@@ -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) {