@zq-silk/yui 0.15.8 → 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 (151) hide show
  1. package/ARCHITECTURE.md +2 -0
  2. package/ARCHITECTURE.zh-CN.md +151 -0
  3. package/README.md +211 -14
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/artifacts/artifactCapability.js +74 -0
  6. package/dist/artifacts/artifactCommitLock.js +249 -0
  7. package/dist/artifacts/artifactPaths.js +151 -0
  8. package/dist/artifacts/gitArtifactRef.js +146 -0
  9. package/dist/artifacts/managedGit.js +332 -0
  10. package/dist/artifacts/taskArtifactRepository.js +277 -0
  11. package/dist/cli/commandCatalog.js +40 -16
  12. package/dist/cli/interactionPolicy.js +3 -3
  13. package/dist/cli/updateOrchestrator.js +24 -1
  14. package/dist/cli/updatePorts.js +7 -3
  15. package/dist/cli/upgradeCommand.js +42 -2
  16. package/dist/cli.js +403 -93
  17. package/dist/commands/globalRoleCommands.js +314 -4
  18. package/dist/commands/operatorCommands.js +33 -2
  19. package/dist/commands/projectCommands.js +6 -7
  20. package/dist/commands/releaseCommands.js +18 -0
  21. package/dist/commands/taskActivationCommands.js +22 -0
  22. package/dist/commands/taskActor.js +25 -0
  23. package/dist/commands/taskCommands.js +846 -155
  24. package/dist/commands/taskIntegrationCommands.js +16 -38
  25. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  26. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  27. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  28. package/dist/context/runContextPack.js +28 -16
  29. package/dist/context/taskContext.js +64 -5
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +11 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  34. package/dist/controller/globalInputDelivery.js +119 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/resourceInventory.js +14 -4
  37. package/dist/controller/resourceInventoryLinux.js +2 -6
  38. package/dist/controller/runtime.js +81 -6
  39. package/dist/controller/runtimeEventInbox.js +32 -3
  40. package/dist/controller/runtimeEventProcessor.js +26 -6
  41. package/dist/controller/runtimeHookRunFence.js +75 -19
  42. package/dist/controller/structuredProviderObservation.js +133 -70
  43. package/dist/coordination/workMailboxQueue.js +5 -0
  44. package/dist/execution/workItemExecutionProjection.js +1 -1
  45. package/dist/executor/agentExecutor.js +64 -4
  46. package/dist/executor/executorRegistry.js +3 -0
  47. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  48. package/dist/integration/deliveryObligation.js +2 -1
  49. package/dist/integration/gitIntegrationService.js +312 -382
  50. package/dist/integration/integrationAttempt.js +30 -4
  51. package/dist/integration/integrationQueueService.js +7 -7
  52. package/dist/integration/integrationSourceApplication.js +323 -0
  53. package/dist/kernel/builtinCapabilities.js +32 -24
  54. package/dist/message/globalInterrupt.js +33 -0
  55. package/dist/message/inputControlResolution.js +106 -0
  56. package/dist/message/message.js +423 -0
  57. package/dist/message/messageContinuation.js +126 -3
  58. package/dist/message/taskInterrupt.js +34 -0
  59. package/dist/observability/orchestrationMetrics.js +1 -1
  60. package/dist/plugins/pluginService.js +11 -3
  61. package/dist/release/releaseHandover.js +22 -0
  62. package/dist/release/releaseWorkflowPorts.js +15 -7
  63. package/dist/repository/gitWorkspace.js +72 -15
  64. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  65. package/dist/repository/taskWorkspacePreparer.js +120 -49
  66. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  67. package/dist/resources/projectResource.js +0 -48
  68. package/dist/resources/projectResourceService.js +3 -81
  69. package/dist/resources/resourceDiscovery.js +3 -2
  70. package/dist/runtime/agentHost.js +152 -72
  71. package/dist/runtime/agentHostCompatibility.js +127 -0
  72. package/dist/runtime/agentHostProtocol.js +53 -0
  73. package/dist/runtime/executionEnvironment.js +0 -19
  74. package/dist/runtime/launchBroker.js +6 -0
  75. package/dist/runtime/sessionReconciliation.js +4 -4
  76. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  77. package/dist/runtime/tmuxAdapters.js +5 -3
  78. package/dist/scheduler/operatorEvent.js +4 -0
  79. package/dist/scheduler/taskExecutionProjection.js +12 -1
  80. package/dist/scheduler/wakeReason.js +7 -1
  81. package/dist/scheduler/wakeupQueue.js +2 -0
  82. package/dist/setup/setupCommand.js +29 -16
  83. package/dist/storage/homeLayout.js +130 -0
  84. package/dist/storage/migrations/artifactsToGit.js +338 -0
  85. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  86. package/dist/storage/migrations/integrationContinuation.js +104 -0
  87. package/dist/storage/migrations/submitIntent.js +126 -0
  88. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  89. package/dist/storage/sqliteSchema.js +173 -7
  90. package/dist/storage/sqliteStore.js +41 -22
  91. package/dist/storage/storageVersions.js +1 -1
  92. package/dist/storage/storeRpc.js +2 -1
  93. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  94. package/dist/task/archiveDiagnostics.js +128 -0
  95. package/dist/task/nextAction.js +44 -11
  96. package/dist/task/taskActivation.js +26 -0
  97. package/dist/task/taskActivationService.js +85 -69
  98. package/dist/task/taskSubmission.js +236 -0
  99. package/dist/web/assets/client/app.js +58 -2
  100. package/dist/web/assets/client/components.js +1 -0
  101. package/dist/web/assets/client/i18n.js +6 -0
  102. package/dist/web/assets/client/taskSurface.js +202 -7
  103. package/dist/web/assets/client/view.js +7 -4
  104. package/dist/web/assets/shell.js +23 -0
  105. package/dist/web/assets/styles/layout.js +1 -1
  106. package/dist/web/assets/styles/widgets.js +12 -0
  107. package/dist/web/webServer.js +135 -4
  108. package/dist/web/webSnapshot.js +4 -3
  109. package/dist/web/webTaskSurface.js +225 -8
  110. package/dist/workItem/workItem.js +14 -10
  111. package/dist/workspace/workItemChangeSetManager.js +18 -2
  112. package/docs/agent-result-consumption.md +2 -0
  113. package/docs/agent-result-consumption.zh-CN.md +81 -0
  114. package/docs/agent-runtime-drivers.md +2 -0
  115. package/docs/agent-runtime-drivers.zh-CN.md +77 -0
  116. package/docs/architecture/README.md +44 -32
  117. package/docs/architecture/README.zh-CN.md +43 -0
  118. package/docs/architecture/capabilities-and-resources.md +118 -79
  119. package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
  120. package/docs/managed-turn-and-session-runtime.md +2 -0
  121. package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
  122. package/docs/observability/README.md +2 -0
  123. package/docs/observability/README.zh-CN.md +71 -0
  124. package/docs/plugin-sdk.md +320 -217
  125. package/docs/plugin-sdk.zh-CN.md +293 -0
  126. package/docs/provider-runtime.md +2 -0
  127. package/docs/provider-runtime.zh-CN.md +132 -0
  128. package/docs/release-workflow.md +41 -0
  129. package/docs/release-workflow.zh-CN.md +266 -0
  130. package/docs/roles-and-configuration.md +2 -0
  131. package/docs/roles-and-configuration.zh-CN.md +96 -0
  132. package/docs/sqlite-control-plane-design.md +225 -1
  133. package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
  134. package/docs/task-dag-semantics.md +80 -57
  135. package/docs/task-dag-semantics.zh-CN.md +59 -0
  136. package/docs/task-delivery.md +2 -0
  137. package/docs/task-delivery.zh-CN.md +82 -0
  138. package/docs/task-local-identity.md +2 -0
  139. package/docs/task-local-identity.zh-CN.md +58 -0
  140. package/docs/testing/verification-levels.md +26 -0
  141. package/docs/testing/verification-levels.zh-CN.md +80 -0
  142. package/i18n/README.zh-CN.md +199 -10
  143. package/package.json +2 -1
  144. package/skills/yui-leader/SKILL.md +88 -331
  145. package/skills/yui-leader/references/execution.md +405 -0
  146. package/skills/yui-leader/references/integration.md +52 -2
  147. package/skills/yui-leader/references/planning.md +109 -0
  148. package/skills/yui-leader/references/task-plugins.md +8 -4
  149. package/skills/yui-operator/SKILL.md +22 -4
  150. package/skills/yui-runtime/SKILL.md +27 -0
  151. package/skills/yui-runtime/references/publication.md +20 -0
@@ -4,13 +4,11 @@ import { tmpdir, homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { createConnection, createServer } from "node:net";
6
6
  import { createInterface } from "node:readline";
7
- import { assertAgentExecutionEnvironment } from "./executionEnvironment.js";
8
7
  import { withSessionContextPointer } from "../context/sessionBootstrapManifest.js";
9
8
  import { callController, controllerCallMayHaveApplied, ControllerClientError } from "../core/controllerClient.js";
10
9
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
11
- import { callFileTaskController } from "../controller/clientRuntime.js";
12
10
  import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
13
- import { publishStructuredProviderAccepted, publishStructuredProviderAttachmentExit, publishStructuredProviderActivity, publishStructuredProviderInputSettlement, publishStructuredProviderInputObserved, publishStructuredProviderGoal, publishStructuredConversationRecoverability, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal } from "../controller/structuredProviderObservation.js";
11
+ import { publishStructuredProviderAccepted, publishStructuredProviderAttachmentExit, publishStructuredProviderActivity, publishStructuredProviderInputSettlement, publishStructuredProviderInputObserved, publishStructuredProviderGoal, publishStructuredConversationRecoverability, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, publishStructuredProviderConnection, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
14
12
  import { validateAgentHostLaunchPayload } from "./launchBroker.js";
15
13
  import { ProviderDeliveryUnknownError, ProviderConversationMissingError, ProviderTurnBusyError, ProviderTurnRejectedError } from "./structuredProviderHost.js";
16
14
  import { createAgentEndpointOwner } from "./agentEndpointOwnership.js";
@@ -21,15 +19,48 @@ import { AGENT_HOST_CLIENT_EXIT_GRACE_MS, AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_H
21
19
  import { providerDeliveryFailure, providerDeliveryFailureFrom, redactAgentErrorText, serializeAgentErrorRaw } from "./agentError.js";
22
20
  import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
23
21
  import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
24
- import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
25
22
  import { createSessionOwnerIdentity, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
26
23
  import { yuiTmuxServerName, yuiTmuxSessionName } from "../tmux/tmuxManager.js";
27
24
  import { tmuxSocketDirectory } from "../tmux/tmuxSocketEndpoint.js";
28
- import { createTaskEvent } from "../event/taskEvent.js";
29
- export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v5";
25
+ import { AGENT_HOST_CONTROL_PROTOCOL, AGENT_HOST_EVENT_PROTOCOL } from "./agentHostProtocol.js";
26
+ import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
27
+ export { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
30
28
  const HOST_CONTROL_MAX_BYTES = 32 * 1024;
31
29
  const CODEX_CLIENT_STABLE_MS = 5_000;
32
30
  const MAX_CONSECUTIVE_CODEX_DISCONNECTS = 3;
31
+ export function foldSteerLiveReceipt(control) {
32
+ const detail = control.failure?.detail ?? control.snapshot.detail;
33
+ const withDetail = detail === undefined ? {} : { detail };
34
+ switch (control.outcome) {
35
+ case "accepted":
36
+ // The only ok state. A live-accepted steer keeps `not-steered` (static,
37
+ // pre-flight) and this live disposition strictly separate phases.
38
+ return { state: "steered", outcome: "accepted" };
39
+ case "pending":
40
+ return { state: "steer-unknown", outcome: "pending", ...withDetail };
41
+ case "rejected":
42
+ return { state: "steer-rejected", outcome: "rejected", ...withDetail };
43
+ default:
44
+ // A steer edge only ever answers accepted/pending/rejected; `busy`,
45
+ // `status`, or `cancel-requested` here means the Host could not run the
46
+ // steer. Report it faithfully rather than flattening to success.
47
+ return { state: "steer-unavailable", outcome: control.outcome, ...withDetail };
48
+ }
49
+ }
50
+ export function foldInterruptLiveReceipt(control) {
51
+ const cancellation = control.cancellation;
52
+ const withCancellation = cancellation === undefined ? {} : { cancellation };
53
+ if (control.outcome !== "cancel-requested") {
54
+ const detail = control.failure?.detail ?? control.snapshot.detail;
55
+ return { state: "interrupt-unavailable", outcome: control.outcome,
56
+ ...withCancellation, ...(detail === undefined ? {} : { detail }) };
57
+ }
58
+ // A transport outcome without native cancellation evidence proves no stop.
59
+ const state = cancellation?.status === "not-active" ? "interrupt-not-active"
60
+ : cancellation?.status === "requested" ? "interrupt-requested"
61
+ : "interrupt-unknown";
62
+ return { state, outcome: "cancel-requested", ...withCancellation };
63
+ }
33
64
  export function serializeAgentHostLaunchControl(control) {
34
65
  return JSON.stringify(validateControl(control));
35
66
  }
@@ -37,6 +68,11 @@ export async function runAgentHost(input) {
37
68
  const hostInstanceId = randomUUID();
38
69
  let hostSequence = 0;
39
70
  let payload = await redeem(input.home, input.ticket);
71
+ if (payload.environment.YUI_SESSION_SCOPE === "global" && payload.providerControl !== undefined) {
72
+ process.stdout.write("Yui controlled Global Session: this is the Host console, not the native Agent TUI. "
73
+ + "Type a line to submit to this Session; model, permissions and workspace remain as configured. "
74
+ + "Queued inputs and live controls use the same Endpoint.\n");
75
+ }
40
76
  if (payload.environment.YUI_SESSION_SCOPE === "global"
41
77
  && payload.environment.YUI_ADAPTER_ID === "codex"
42
78
  && payload.providerControl === undefined) {
@@ -304,7 +340,7 @@ export async function runAgentHost(input) {
304
340
  if (hostStopRequested)
305
341
  return;
306
342
  try {
307
- assertAgentExecutionEnvironment(input.home, reconnectPayload);
343
+ await assertHostExecutionEnvironment(input.home, reconnectPayload, disconnectedSession.nativeSessionId);
308
344
  // Reattaching a dropped client continues the same Session on the same
309
345
  // pinned generation; it never re-selects an implementation.
310
346
  if (endpointLease === undefined) {
@@ -549,7 +585,8 @@ export async function runAgentHost(input) {
549
585
  activeRunAttemptId = providerControl.ownedTurn.attemptId;
550
586
  activeNativeTurnId = providerControl.ownedTurn.turnId;
551
587
  }
552
- assertAgentExecutionEnvironment(input.home, next);
588
+ // The Controller revalidates the adopted execution environment while
589
+ // redeeming this launch; a persistent Host must not reopen its schema.
553
590
  // Pin before opening: the Session holds this exact generation for every
554
591
  // later Turn. A pinned reference naming code this process is not running
555
592
  // fails here, rather than silently starting on a different generation.
@@ -559,8 +596,7 @@ export async function runAgentHost(input) {
559
596
  ? endpointLease.open(next) : endpointLease.resume(next));
560
597
  session = started.session;
561
598
  sessionPayload = next;
562
- recordOwnedProviderProcess(input.home, next.environment, started.session);
563
- recordNativeConnection(input.home, next, started.session);
599
+ await recordProviderConnection(input.home, next, started.session);
564
600
  lastTerminalPayload = undefined;
565
601
  lastTerminal = undefined;
566
602
  // Recoverable means a later process can rebind this Conversation by
@@ -611,6 +647,7 @@ export async function runAgentHost(input) {
611
647
  await publishStructuredProviderOpened({
612
648
  home: input.home,
613
649
  environment: next.environment,
650
+ startupRunId: next.startupRunId,
614
651
  conversationId: session.conversationId,
615
652
  nativeSessionId: session.nativeSessionId,
616
653
  recoverability: conversationRecoverability,
@@ -790,7 +827,8 @@ export async function runAgentHost(input) {
790
827
  let providerAccepted = false;
791
828
  let transportAccepted = false;
792
829
  try {
793
- assertAgentExecutionEnvironment(input.home, activeRunPayload);
830
+ // beginDurableProviderTurn validated current execution-environment,
831
+ // authority and Session identity before this native write.
794
832
  const receipt = endpointReceipt(await session.submit({
795
833
  ...request.run,
796
834
  boundedText: withSessionContextPointer(request.run.boundedText, activeRunPayload.environment),
@@ -906,7 +944,7 @@ export async function runAgentHost(input) {
906
944
  throw new Error("Agent Host rejected a stale Provider writer fence.");
907
945
  }
908
946
  try {
909
- assertAgentExecutionEnvironment(input.home, sessionPayload);
947
+ await assertHostExecutionEnvironment(input.home, sessionPayload, session.nativeSessionId);
910
948
  const receipt = endpointReceipt(await session.steer({
911
949
  ...request.run,
912
950
  boundedText: withSessionContextPointer(request.run.boundedText, sessionPayload.environment),
@@ -972,6 +1010,9 @@ export async function runAgentHost(input) {
972
1010
  }));
973
1011
  }
974
1012
  if (request.type === "cancel") {
1013
+ if (request.nativeOnly === true && session?.capabilities.cancel !== "native-interrupt") {
1014
+ throw new Error("This Endpoint cannot natively interrupt; owned-process termination is not input control.");
1015
+ }
975
1016
  if (session === undefined || request.nativeSessionId !== session.nativeSessionId
976
1017
  || request.attemptId !== activeRunAttemptId
977
1018
  || authority === undefined || !sameProviderAuthorityFence(authority, request.authority)) {
@@ -1079,7 +1120,8 @@ export async function runAgentHost(input) {
1079
1120
  ? createInterface({ input: process.stdin, output: process.stdout, terminal: true })
1080
1121
  : undefined;
1081
1122
  promptHuman = () => {
1082
- if (humanConsole !== undefined && authority?.owner === "human"
1123
+ if (humanConsole !== undefined && (authority?.owner === "human"
1124
+ || sessionPayload?.environment.YUI_SESSION_SCOPE === "global" && authority?.owner === "controller")
1083
1125
  && activeRunPayload === undefined
1084
1126
  && ["idle", "rejected", "failed"].includes(snapshot.state)) {
1085
1127
  humanConsole.setPrompt("yui(provider)> ");
@@ -1091,7 +1133,8 @@ export async function runAgentHost(input) {
1091
1133
  const currentAuthority = authority;
1092
1134
  const currentSession = session;
1093
1135
  const currentPayload = sessionPayload;
1094
- if (currentAuthority?.owner !== "human"
1136
+ if ((currentAuthority?.owner !== "human"
1137
+ && !(currentPayload?.environment.YUI_SESSION_SCOPE === "global" && currentAuthority?.owner === "controller"))
1095
1138
  || currentSession === undefined
1096
1139
  || currentPayload === undefined) {
1097
1140
  process.stderr.write("Provider input rejected: human authority is not active.\n");
@@ -1111,8 +1154,10 @@ export async function runAgentHost(input) {
1111
1154
  boundedText
1112
1155
  }
1113
1156
  };
1114
- await submitRun(runControl, {});
1115
- process.stdout.write("Provider accepted the human AgentRun; waiting for its terminal boundary.\n");
1157
+ const submitted = await submitRun(runControl, {});
1158
+ process.stdout.write(submitted.inputAcceptance === "provider"
1159
+ ? "Provider accepted this console input; waiting for its terminal boundary.\n"
1160
+ : "Input written to transport; Provider acceptance is not yet proven.\n");
1116
1161
  }).catch((error) => {
1117
1162
  process.stderr.write(`Provider input failed: ${errorText(error)}\n`);
1118
1163
  promptHuman();
@@ -1204,58 +1249,42 @@ export async function runAgentHost(input) {
1204
1249
  }
1205
1250
  }
1206
1251
  /** Host-restart-independent OS custody. No secret or launch payload is stored. */
1207
- function recordOwnedProviderProcess(home, environment, endpoint) {
1208
- if (endpoint.ownedProcessId === undefined || environment.YUI_SESSION_SCOPE !== "task")
1209
- return;
1210
- const identity = readLinuxProcessIdentity(endpoint.ownedProcessId);
1211
- if (identity === undefined)
1252
+ async function recordProviderConnection(home, payload, endpoint) {
1253
+ const environment = payload.environment;
1254
+ const identity = endpoint.ownedProcessId === undefined ? undefined : readLinuxProcessIdentity(endpoint.ownedProcessId);
1255
+ if (endpoint.ownedProcessId !== undefined && identity === undefined) {
1212
1256
  throw new Error("Dedicated Provider process identity was lost before registration.");
1257
+ }
1213
1258
  const taskId = environment.YUI_TASK_ID;
1214
1259
  const roleName = environment.YUI_ROLE;
1215
- const store = openCurrentTaskStore(home);
1216
- try {
1217
- store.saveSessionOwner(createSessionOwnerIdentity({
1218
- owner: { scope: "task", taskId, roleName },
1219
- agentId: environment.YUI_AGENT_ID, adapterId: endpoint.adapterId,
1220
- nativeSessionId: endpoint.nativeSessionId,
1221
- tmux: {
1222
- serverName: yuiTmuxServerName(home),
1223
- socketPath: join(tmuxSocketDirectory(environment), yuiTmuxServerName(home)),
1224
- sessionName: yuiTmuxSessionName(home, taskId), windowName: roleName, panePid: process.pid
1225
- },
1226
- providerRoot: { pid: identity.pid, startIdentity: identity.startIdentity,
1227
- processGroupId: identity.processGroupId, processSessionId: identity.processSessionId,
1228
- attribution: "owned-child" },
1229
- recordedAt: new Date()
1230
- }));
1231
- }
1232
- finally {
1233
- store.close();
1234
- }
1235
- }
1236
- function recordNativeConnection(home, payload, endpoint) {
1237
- if (endpoint.adapterId !== "codex" || payload.environment.YUI_SESSION_SCOPE !== "task")
1238
- return;
1239
- const taskId = payload.environment.YUI_TASK_ID;
1240
- const store = openCurrentTaskStore(home);
1241
- try {
1242
- if (store.listEvents(taskId).some(event => event.type === "runtime.native-connection-bound"
1243
- && event.payload.nativeSessionId === endpoint.nativeSessionId))
1244
- return;
1245
- const userHome = resolve(payload.cwd, payload.environment.HOME ?? homedir());
1246
- store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, "runtime.native-connection-bound", {
1247
- roleName: payload.environment.YUI_ROLE, agentId: payload.environment.YUI_AGENT_ID,
1248
- nativeSessionId: endpoint.nativeSessionId,
1249
- // Account location is needed after Controller/Host loss. Never retain
1250
- // resolved credentials or duplicate the full process environment.
1251
- home: userHome,
1252
- codexHome: resolve(payload.cwd, payload.environment.CODEX_HOME ?? join(userHome, ".codex")),
1253
- ...(endpoint.nativeAccountHome === undefined ? {} : { nativeAccountHome: endpoint.nativeAccountHome })
1254
- }, new Date()));
1255
- }
1256
- finally {
1257
- store.close();
1258
- }
1260
+ const userHome = resolve(payload.cwd, environment.HOME ?? homedir());
1261
+ await publishStructuredProviderConnection({
1262
+ home, environment, nativeSessionId: endpoint.nativeSessionId, startupRunId: payload.startupRunId,
1263
+ connection: {
1264
+ ...(identity === undefined ? {} : { processOwner: createSessionOwnerIdentity({
1265
+ owner: environment.YUI_SESSION_SCOPE === "global"
1266
+ ? { scope: "global", roleName } : { scope: "task", taskId, roleName },
1267
+ agentId: environment.YUI_AGENT_ID, adapterId: endpoint.adapterId,
1268
+ nativeSessionId: endpoint.nativeSessionId,
1269
+ tmux: {
1270
+ serverName: yuiTmuxServerName(home),
1271
+ socketPath: join(tmuxSocketDirectory(environment), yuiTmuxServerName(home)),
1272
+ sessionName: yuiTmuxSessionName(home, taskId ?? "operator"), windowName: roleName, panePid: process.pid
1273
+ },
1274
+ providerRoot: { pid: identity.pid, startIdentity: identity.startIdentity,
1275
+ processGroupId: identity.processGroupId, processSessionId: identity.processSessionId,
1276
+ attribution: "owned-child" },
1277
+ recordedAt: new Date()
1278
+ }) }),
1279
+ ...(endpoint.adapterId !== "codex" ? {} : { account: {
1280
+ // Account location is needed after Controller/Host loss. Never retain
1281
+ // resolved credentials or duplicate the full process environment.
1282
+ home: userHome,
1283
+ codexHome: resolve(payload.cwd, environment.CODEX_HOME ?? join(userHome, ".codex")),
1284
+ ...(endpoint.nativeAccountHome === undefined ? {} : { nativeAccountHome: endpoint.nativeAccountHome })
1285
+ } })
1286
+ }
1287
+ });
1259
1288
  }
1260
1289
  export function agentHostControlSocketPath(input) {
1261
1290
  const owner = input.scope === "task" ? input.taskId ?? "missing-task" : "global";
@@ -1329,13 +1358,22 @@ export async function waitForAgentHostLaunchAck(input) {
1329
1358
  }
1330
1359
  async function sendAgentHostControl(input) {
1331
1360
  const path = agentHostControlSocketPath(input);
1361
+ return exchangeAgentHostControl(path, input.control, AGENT_HOST_CONTROL_TIMEOUT_MS);
1362
+ }
1363
+ /** Read-only upgrade/diagnostic probe of a Home-fenced socket. Never launches a Host. */
1364
+ export async function inspectAgentHostSocket(path, timeoutMs = 250) {
1365
+ return (await exchangeAgentHostControl(path, {
1366
+ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "status"
1367
+ }, timeoutMs)).snapshot;
1368
+ }
1369
+ async function exchangeAgentHostControl(path, control, timeoutMs) {
1332
1370
  return await new Promise((resolvePromise, reject) => {
1333
1371
  const client = createConnection(path);
1334
1372
  let response = "";
1335
1373
  const timer = setTimeout(() => {
1336
1374
  client.destroy();
1337
1375
  reject(new Error("Agent Host control request timed out."));
1338
- }, AGENT_HOST_CONTROL_TIMEOUT_MS);
1376
+ }, timeoutMs);
1339
1377
  let settled = false;
1340
1378
  const settle = (callback, value) => {
1341
1379
  if (settled)
@@ -1345,7 +1383,7 @@ async function sendAgentHostControl(input) {
1345
1383
  callback(value);
1346
1384
  };
1347
1385
  client.setEncoding("utf8");
1348
- client.once("connect", () => client.end(`${JSON.stringify(validateControl(input.control))}\n`));
1386
+ client.once("connect", () => client.end(`${JSON.stringify(validateControl(control))}\n`));
1349
1387
  client.on("data", (chunk) => {
1350
1388
  response += chunk;
1351
1389
  if (Buffer.byteLength(response, "utf8") > HOST_CONTROL_MAX_BYTES) {
@@ -1370,6 +1408,17 @@ async function redeem(home, ticket) {
1370
1408
  });
1371
1409
  return validateAgentHostLaunchPayload(result);
1372
1410
  }
1411
+ async function assertHostExecutionEnvironment(home, payload, nativeSessionId) {
1412
+ if (payload.executionEnvironment === undefined)
1413
+ return;
1414
+ await callController(home, "runtime.execution-environment-check", {
1415
+ taskId: payload.environment.YUI_TASK_ID,
1416
+ roleName: payload.environment.YUI_ROLE,
1417
+ agentId: payload.environment.YUI_AGENT_ID,
1418
+ workspace: payload.cwd,
1419
+ nativeSessionId
1420
+ });
1421
+ }
1373
1422
  async function persistAndSubmitExit(home, observation) {
1374
1423
  try {
1375
1424
  await persistRuntimeProcessExitObservation(home, observation, (value) => callController(home, "runtime.process-exit-observe", value).then(() => undefined));
@@ -1426,7 +1475,26 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
1426
1475
  void (async () => {
1427
1476
  try {
1428
1477
  const request = validateControl(JSON.parse(body.trim()));
1429
- socket.end(`${JSON.stringify(boundControlResponse(await dispatch(request)))}\n`);
1478
+ const result = await dispatch(request);
1479
+ socket.end(`${JSON.stringify(boundControlResponse({
1480
+ ...result,
1481
+ snapshot: {
1482
+ ...result.snapshot,
1483
+ compatibility: {
1484
+ control: AGENT_HOST_CONTROL_PROTOCOL, events: AGENT_HOST_EVENT_PROTOCOL,
1485
+ rpc: FILE_TASK_CONTROLLER_PROTOCOL_VERSION, storage: "controller-owned"
1486
+ },
1487
+ hostProcess: { pid: process.pid, startIdentity: readLinuxProcessIdentity(process.pid).startIdentity },
1488
+ owner: {
1489
+ scope: payload.environment.YUI_SESSION_SCOPE ?? "task",
1490
+ ...(payload.environment.YUI_TASK_ID === undefined ? {} : { taskId: payload.environment.YUI_TASK_ID }),
1491
+ roleName: payload.environment.YUI_ROLE ?? "unknown-role"
1492
+ },
1493
+ ...(payload.environment.YUI_SESSION_SCOPE !== "task" ? {} : {
1494
+ eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
1495
+ })
1496
+ }
1497
+ }))}\n`);
1430
1498
  }
1431
1499
  catch (caught) {
1432
1500
  const error = caught instanceof AgentHostOperationError ? caught.cause : caught;
@@ -1565,6 +1633,9 @@ function validateControl(control) {
1565
1633
  if (control.type === "status")
1566
1634
  return Object.freeze({ ...control });
1567
1635
  if (control.type === "cancel") {
1636
+ if (control.nativeOnly !== undefined && typeof control.nativeOnly !== "boolean") {
1637
+ throw new Error("Native-only cancellation flag is invalid.");
1638
+ }
1568
1639
  validateIdentity(control.nativeSessionId, "native Session id");
1569
1640
  validateIdentity(control.attemptId, "Provider input attempt id");
1570
1641
  validateProviderAuthorityFence(control.authority);
@@ -1746,9 +1817,11 @@ async function observePendingHostSubmission(submission, snapshot) {
1746
1817
  function signalRoleMailbox(home, payload) {
1747
1818
  const taskId = payload.environment.YUI_TASK_ID;
1748
1819
  const roleName = payload.environment.YUI_ROLE;
1749
- if (taskId === undefined || roleName === undefined)
1820
+ if (roleName === undefined)
1750
1821
  return;
1751
- const key = `role:${encodeURIComponent(taskId)}/${encodeURIComponent(roleName)}`;
1822
+ const key = taskId === undefined
1823
+ ? `global-role:${encodeURIComponent(roleName)}`
1824
+ : `role:${encodeURIComponent(taskId)}/${encodeURIComponent(roleName)}`;
1752
1825
  void callController(home, "scheduler.signal", { key }).catch(() => {
1753
1826
  // This is a low-latency hint. Durable mailbox state and periodic
1754
1827
  // reconciliation remain the recovery path across a Controller handover.
@@ -1763,7 +1836,9 @@ async function delay(milliseconds) {
1763
1836
  function hostRunControlParams(payload, nativeSessionId, authority, attemptId, runId) {
1764
1837
  const environment = payload.environment;
1765
1838
  return Object.freeze({
1766
- taskId: requiredEnvironment(environment.YUI_TASK_ID, "Task id"),
1839
+ ...(environment.YUI_SESSION_SCOPE === "global" ? {} : {
1840
+ taskId: requiredEnvironment(environment.YUI_TASK_ID, "Task id")
1841
+ }),
1767
1842
  roleName: requiredEnvironment(environment.YUI_ROLE, "Role name"),
1768
1843
  ...(runId === undefined ? {} : { runId: requiredEnvironment(runId, "AgentRun id") }),
1769
1844
  agentId: requiredEnvironment(environment.YUI_AGENT_ID, "Agent id"),
@@ -1868,7 +1943,12 @@ async function callAgentController(home, method, params) {
1868
1943
  throw error;
1869
1944
  }
1870
1945
  }
1871
- await callFileTaskController(home, method, params);
1946
+ const deadline = Date.now() + AGENT_HOST_CONTROL_TIMEOUT_MS;
1947
+ while (isForeignHandoverLockHeld(home) && Date.now() < deadline)
1948
+ await delay(50);
1949
+ // The pinned Host can rediscover a replacement, but must never start a
1950
+ // Controller using its own old implementation or open the Home's database.
1951
+ await callController(home, method, params);
1872
1952
  }
1873
1953
  function isControllerUnavailable(error) {
1874
1954
  return error instanceof ControllerClientError
@@ -0,0 +1,127 @@
1
+ import { lstatSync, readdirSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { agentHostControlSocketPath, inspectAgentHostSocket } from "./agentHost.js";
4
+ import { AGENT_HOST_CONTROL_PROTOCOL, AGENT_HOST_EVENT_PROTOCOL } from "./agentHostProtocol.js";
5
+ import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
6
+ import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
7
+ import { readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
8
+ /** Inspect the concrete Home's existing control sockets, independently of the
9
+ * database layout. No discovery write, process signal, input replay or repair.
10
+ * Idle legacy Hosts are not safe: their next event still opens their old schema.
11
+ */
12
+ export async function inspectAgentHostCompatibility(home) {
13
+ const sample = agentHostControlSocketPath({ home, scope: "global", roleName: "compatibility-probe" });
14
+ const directory = dirname(sample);
15
+ const prefix = `${basename(sample).split("-")[0]}-`;
16
+ let names;
17
+ try {
18
+ names = readdirSync(directory).filter(name => name.startsWith(prefix) && /^[a-f0-9]{16}-[a-f0-9]{16}\.sock$/.test(name));
19
+ }
20
+ catch (error) {
21
+ if (error.code === "ENOENT")
22
+ names = [];
23
+ else
24
+ return [{ socket: directory, reason: `Host inventory is unreadable: ${messageOf(error)}` }];
25
+ }
26
+ // A launch can have redeemed its payload but not yet bound its socket.
27
+ // It is still a loaded Host implementation. Do not miss it just because
28
+ // preflight happened in that short startup interval.
29
+ const processes = liveHostProcesses(home);
30
+ const sockets = new Set([...names.map(name => join(directory, name)), ...processes.map(p => p.socket)]);
31
+ const results = await Promise.all([...sockets].map(async (socket) => {
32
+ const live = processes.filter(p => p.socket === socket);
33
+ try {
34
+ const stat = lstatSync(socket);
35
+ if (!stat.isSocket() || (stat.mode & 0o077) !== 0
36
+ || (process.getuid !== undefined && stat.uid !== process.getuid())) {
37
+ return { socket, reason: "Host socket ownership cannot be verified." };
38
+ }
39
+ const snapshot = await inspectAgentHostSocket(socket);
40
+ if (live.some(p => snapshot.hostProcess?.pid !== p.pid
41
+ || snapshot.hostProcess?.startIdentity !== p.startIdentity)) {
42
+ return {
43
+ socket, pid: live[0].pid,
44
+ nativeSessionId: snapshot.nativeSessionId,
45
+ reason: "The live Host process has no exact matching compatibility response (legacy or still starting)."
46
+ };
47
+ }
48
+ const compatibility = snapshot.compatibility;
49
+ if (compatibility?.control === AGENT_HOST_CONTROL_PROTOCOL
50
+ && compatibility.events === AGENT_HOST_EVENT_PROTOCOL
51
+ && compatibility.rpc === FILE_TASK_CONTROLLER_PROTOCOL_VERSION
52
+ && compatibility.storage === "controller-owned")
53
+ return undefined;
54
+ return {
55
+ socket,
56
+ ...(snapshot.owner === undefined ? {} : { owner: snapshot.owner }),
57
+ ...(snapshot.nativeSessionId === undefined ? {} : { nativeSessionId: snapshot.nativeSessionId }),
58
+ ...(snapshot.attemptId === undefined ? {} : { attemptId: snapshot.attemptId }),
59
+ state: snapshot.state,
60
+ reason: compatibility === undefined
61
+ ? "Legacy Host has no schema-independent event capability; its loaded code cannot be upgraded in place."
62
+ : "Host control/event/RPC compatibility is outside this Controller's supported range."
63
+ };
64
+ }
65
+ catch (error) {
66
+ const stillLive = live.find(p => readLinuxProcessIdentity(p.pid)?.startIdentity === p.startIdentity);
67
+ if (["ENOENT", "ECONNREFUSED"].includes(error.code ?? "")
68
+ && stillLive === undefined)
69
+ return undefined;
70
+ return { socket, ...(stillLive === undefined ? {} : { pid: stillLive.pid }),
71
+ reason: `Live Host compatibility is unconfirmed: ${messageOf(error)}` };
72
+ }
73
+ }));
74
+ return results.filter((entry) => entry !== undefined);
75
+ }
76
+ export function describeAgentHostUpgradeBlockers(blockers) {
77
+ return blockers.map(blocker => [
78
+ blocker.owner === undefined ? blocker.socket
79
+ : `${blocker.owner.taskId ?? blocker.owner.scope}/${blocker.owner.roleName}`,
80
+ blocker.nativeSessionId === undefined ? "" : `Session=${blocker.nativeSessionId}`,
81
+ blocker.attemptId === undefined ? "" : `input=${blocker.attemptId}`,
82
+ blocker.state === undefined ? "" : `Host=${blocker.state}`,
83
+ blocker.pid === undefined ? "" : `PID=${blocker.pid}`,
84
+ blocker.reason
85
+ ].filter(Boolean).join("; ")).join("\n");
86
+ }
87
+ function liveHostProcesses(home) {
88
+ if (process.platform !== "linux")
89
+ return [];
90
+ const expected = readHomeFilesystemId(home);
91
+ const hosts = [];
92
+ for (const name of readdirSync("/proc")) {
93
+ if (!/^[1-9][0-9]*$/.test(name))
94
+ continue;
95
+ const pid = Number(name);
96
+ try {
97
+ const status = readFileSync(`/proc/${pid}/status`, "utf8");
98
+ if (Number(/^Uid:\s+([0-9]+)/mu.exec(status)?.[1]) !== process.getuid?.())
99
+ continue;
100
+ const args = readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0");
101
+ if (!args.some((arg, i) => /(?:^|\/)cli\.js$/.test(arg)
102
+ && args[i + 1] === "internal" && args[i + 2] === "agent-host"))
103
+ continue;
104
+ const env = readFileSync(`/proc/${pid}/environ`, "utf8").split("\0");
105
+ const value = (key) => env.find(entry => entry.startsWith(`${key}=`))?.slice(key.length + 1);
106
+ const processHome = value("YUI_HOME");
107
+ if (processHome === undefined || readHomeFilesystemId(processHome) !== expected)
108
+ continue;
109
+ const identity = readLinuxProcessIdentity(pid);
110
+ if (identity === undefined || identity.state === "Z")
111
+ continue;
112
+ hosts.push({ pid, startIdentity: identity.startIdentity,
113
+ socket: agentHostControlSocketPath({
114
+ home, scope: value("YUI_SESSION_SCOPE") ?? "task",
115
+ taskId: value("YUI_TASK_ID"), roleName: value("YUI_ROLE") ?? "unknown-role"
116
+ }) });
117
+ }
118
+ catch {
119
+ // Other owners and processes that exited while being read are not this
120
+ // Home's proven Hosts. No PID is ever acted on by this read-only inventory.
121
+ }
122
+ }
123
+ return hosts;
124
+ }
125
+ function messageOf(error) {
126
+ return error instanceof Error ? error.message : String(error);
127
+ }
@@ -0,0 +1,53 @@
1
+ import { createSessionOwnerIdentity } from "./sessionOwnerIdentity.js";
2
+ /** Independent of CLI/RPC and Home schema. Keep v1 readable while v1 Hosts live. */
3
+ export const AGENT_HOST_EVENT_PROTOCOL = "yui-agent-host-events/v1";
4
+ export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v5";
5
+ export function readAgentHostObservationSource(value) {
6
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
7
+ throw new Error("Agent Host observation source is invalid.");
8
+ }
9
+ const source = value;
10
+ if (source.protocol !== AGENT_HOST_EVENT_PROTOCOL) {
11
+ throw new Error("Agent Host event protocol is unsupported.");
12
+ }
13
+ if (source.connection !== undefined && (source.connection === null
14
+ || typeof source.connection !== "object" || Array.isArray(source.connection))) {
15
+ throw new Error("Agent Host connection evidence is invalid.");
16
+ }
17
+ if (source.connection?.processOwner !== undefined
18
+ && (source.connection.processOwner.schemaVersion !== 2
19
+ || source.connection.processOwner.kind !== "yui-session-owner")) {
20
+ throw new Error("Agent Host process custody wire contract is invalid.");
21
+ }
22
+ const text = (value) => {
23
+ if (typeof value !== "string" || !value.trim() || value.includes("\0") || value.length > 4096) {
24
+ throw new Error("Agent Host observation identity is invalid.");
25
+ }
26
+ return value;
27
+ };
28
+ return {
29
+ protocol: AGENT_HOST_EVENT_PROTOCOL,
30
+ adapterId: text(source.adapterId),
31
+ workspace: text(source.workspace),
32
+ ...(source.startupRunId === undefined ? {} : { startupRunId: text(source.startupRunId) }),
33
+ ...(source.connection === undefined ? {} : {
34
+ connection: {
35
+ ...(source.connection.processOwner === undefined ? {} : {
36
+ processOwner: createSessionOwnerIdentity({
37
+ ...source.connection.processOwner,
38
+ recordedAt: new Date(source.connection.processOwner.recordedAt)
39
+ })
40
+ }),
41
+ ...(source.connection.account === undefined ? {} : {
42
+ account: {
43
+ home: text(source.connection.account.home),
44
+ codexHome: text(source.connection.account.codexHome),
45
+ ...(source.connection.account.nativeAccountHome === undefined ? {} : {
46
+ nativeAccountHome: text(source.connection.account.nativeAccountHome)
47
+ })
48
+ }
49
+ })
50
+ }
51
+ })
52
+ };
53
+ }
@@ -1,5 +1,4 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
- import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
3
2
  import { createProjectResources } from "../resources/projectResourceService.js";
4
3
  /** Revalidate the adopted owner; a saved path alone never authorizes execution. */
5
4
  export function assertExecutionEnvironmentCurrent(store, taskId, snapshot) {
@@ -10,21 +9,3 @@ export function assertExecutionEnvironmentCurrent(store, taskId, snapshot) {
10
9
  throw new Error("Execution environment changed; select an adopted environment and start a new Session.");
11
10
  }
12
11
  }
13
- /** Last local check before native process creation or another Provider input. */
14
- export function assertAgentExecutionEnvironment(home, payload) {
15
- const snapshot = payload.executionEnvironment;
16
- if (snapshot === undefined)
17
- return;
18
- if (payload.environment.YUI_SESSION_SCOPE !== "task"
19
- || payload.environment.YUI_TASK_ID !== snapshot.taskId
20
- || payload.cwd !== snapshot.directory.path) {
21
- throw new Error("Agent launch does not match its adopted execution environment.");
22
- }
23
- const store = openCurrentTaskStore(home);
24
- try {
25
- assertExecutionEnvironmentCurrent(store, snapshot.taskId, snapshot);
26
- }
27
- finally {
28
- store.close();
29
- }
30
- }
@@ -60,6 +60,12 @@ function validatePayload(payload) {
60
60
  throw new Error("Agent Host launch payload version is invalid.");
61
61
  text(payload.command, "command");
62
62
  text(payload.cwd, "cwd");
63
+ if (payload.startupRunId !== undefined) {
64
+ text(payload.startupRunId, "startup Run id");
65
+ if (payload.environment?.YUI_SESSION_SCOPE !== "task") {
66
+ throw new Error("Only a Task launch may carry a startup Run identity.");
67
+ }
68
+ }
63
69
  if (!Array.isArray(payload.args))
64
70
  throw new Error("Agent Host launch args must be an array.");
65
71
  payload.args.forEach((value) => text(value, "argument"));
@@ -8,8 +8,8 @@
8
8
  * live Session can always be re-attributed and reported.
9
9
  *
10
10
  * Pure: all I/O is injected. Unknown owners are reported, never cleaned.
11
- * An entry is archive-blocking when its durable state is terminal but the
12
- * physical Provider root is still live, and the Task is terminal/archived.
11
+ * An entry blocks ordinary archive / workspace cleanup when a terminal Task
12
+ * still owns a live or unverified physical root, regardless of Session status.
13
13
  */
14
14
  export function reconcileSessionOwners(input) {
15
15
  const entries = input.records.map((record) => reconcileOne(record, input));
@@ -54,8 +54,8 @@ function reconcileOne(record, input) {
54
54
  const terminalTask = taskStatus === "completed"
55
55
  || taskStatus === "cancelled"
56
56
  || taskStatus === "archived";
57
- const archiveBlocked = mismatch === "durable-terminal-physical-live"
58
- && terminalTask;
57
+ const archiveBlocked = terminalTask && (physical === undefined
58
+ || physical.alive || physical.identityConflict);
59
59
  return {
60
60
  owner: {
61
61
  scope: owner.scope,