@zq-silk/yui 2.3.0 → 2.5.0

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 (197) hide show
  1. package/README.md +15 -1
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +18 -9
  7. package/dist/cli/invocationAuthority.js +1 -1
  8. package/dist/commands/executionAuditCommands.js +5 -0
  9. package/dist/commands/globalRoleCommands.js +41 -3
  10. package/dist/commands/projectCommands.js +89 -94
  11. package/dist/commands/roleSkillValidation.js +4 -8
  12. package/dist/commands/taskCommands.js +65 -14
  13. package/dist/commands/taskUsageCommand.js +38 -0
  14. package/dist/context/roleSessionContext.js +18 -2
  15. package/dist/context/runContextPack.js +5 -1
  16. package/dist/context/sessionBootstrapManifest.js +18 -3
  17. package/dist/context/skillCatalog.js +108 -0
  18. package/dist/context/skillPackage.js +161 -0
  19. package/dist/context/taskContext.js +3 -2
  20. package/dist/context/taskSearch.js +127 -0
  21. package/dist/controlPlaneCli.js +25 -20
  22. package/dist/controller/fileSchedulerStoreAdapter.js +39 -8
  23. package/dist/controller/globalInputDelivery.js +2 -4
  24. package/dist/controller/jobControl.js +2 -0
  25. package/dist/controller/nativeHostInput.js +37 -0
  26. package/dist/controller/providerRetryDelivery.js +4 -2
  27. package/dist/controller/runtime.js +93 -2
  28. package/dist/controller/runtimeEventInbox.js +2 -2
  29. package/dist/controller/runtimeEventProcessor.js +3 -3
  30. package/dist/doctor/doctor.js +10 -0
  31. package/dist/execution/reviewMainRun.js +1 -0
  32. package/dist/execution/workItemMainRun.js +1 -0
  33. package/dist/executor/agentAdapter.js +3 -5
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +15 -7
  37. package/dist/integration/gitIntegrationService.js +7 -12
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/interaction/notificationPresentation.js +85 -0
  40. package/dist/interaction/operatorPresentation.js +1 -2
  41. package/dist/job/httpPreview.js +61 -0
  42. package/dist/job/jobRunner.js +12 -2
  43. package/dist/kernel/builtinCapabilities.js +3 -1
  44. package/dist/message/message.js +15 -1
  45. package/dist/output/rolePresentation.js +4 -1
  46. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  47. package/dist/repository/gitCompatibility.js +62 -0
  48. package/dist/repository/gitWorkspace.js +48 -71
  49. package/dist/repository/gitWorktreeInventory.js +121 -0
  50. package/dist/repository/project.js +74 -25
  51. package/dist/resources/resourceDiscovery.js +2 -2
  52. package/dist/resources/resourceGc.js +7 -4
  53. package/dist/resources/resourceRegistrar.js +2 -1
  54. package/dist/runtime/agentEndpoint.js +8 -0
  55. package/dist/runtime/agentHost.js +170 -3
  56. package/dist/runtime/codexAppServerRuntime.js +1 -1
  57. package/dist/runtime/codexNativeAccess.js +175 -0
  58. package/dist/runtime/controlledCodexTui.js +116 -0
  59. package/dist/runtime/publicReply.js +23 -0
  60. package/dist/runtime/runtimeObservation.js +48 -0
  61. package/dist/runtime/structuredProviderHost.js +16 -7
  62. package/dist/runtime/taskCostMetrics.js +133 -0
  63. package/dist/runtime/taskUsageMetrics.js +11 -2
  64. package/dist/runtime/taskUsageQuery.js +38 -0
  65. package/dist/scheduler/leaderWakeupProcessor.js +1 -9
  66. package/dist/scheduler/taskExecutionProjection.js +2 -0
  67. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  68. package/dist/storage/homeLayout.js +13 -0
  69. package/dist/storage/sqliteSchema.js +44 -0
  70. package/dist/storage/sqliteStore.js +53 -0
  71. package/dist/storage/storageVersions.js +1 -1
  72. package/dist/storage/storeRpc.js +2 -0
  73. package/dist/storage/taskSearch.js +35 -0
  74. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  75. package/dist/task/ordinaryArchive.js +42 -0
  76. package/dist/tmux/tmuxManager.js +8 -8
  77. package/dist/web/assets/assetManifest.js +11 -51
  78. package/dist/web/assets/client/app/catalog.js +215 -0
  79. package/dist/web/assets/client/app/main.js +148 -0
  80. package/dist/web/assets/client/app/selection.js +139 -0
  81. package/dist/web/assets/client/app/settings.js +326 -0
  82. package/dist/web/assets/client/app/taskView.js +280 -0
  83. package/dist/web/assets/client/domain/context.js +96 -0
  84. package/dist/web/assets/client/domain/records.js +126 -0
  85. package/dist/web/assets/client/domain/runs.js +119 -0
  86. package/dist/web/assets/client/domain/taskForms.js +197 -0
  87. package/dist/web/assets/client/domain/vocab.js +69 -0
  88. package/dist/web/assets/client/domain/work.js +116 -0
  89. package/dist/web/assets/client/i18n/en.js +900 -0
  90. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  91. package/dist/web/assets/client/index.js +114 -0
  92. package/dist/web/assets/client/layout/resizer.js +37 -0
  93. package/dist/web/assets/client/layout/router.js +35 -0
  94. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  95. package/dist/web/assets/client/layout/sizing.js +95 -0
  96. package/dist/web/assets/client/layout/workspace.js +247 -0
  97. package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
  98. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  99. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  100. package/dist/web/assets/client/lib/i18n.js +54 -0
  101. package/dist/web/assets/client/lib/markdown.js +167 -0
  102. package/dist/web/assets/client/lib/prefs.js +28 -0
  103. package/dist/web/assets/client/lib/theme.js +68 -0
  104. package/dist/web/assets/client/ui/containers.js +101 -0
  105. package/dist/web/assets/client/ui/controls.js +51 -0
  106. package/dist/web/assets/client/ui/forms.js +48 -0
  107. package/dist/web/assets/client/ui/metrics.js +137 -0
  108. package/dist/web/assets/client/ui/primitives.js +92 -0
  109. package/dist/web/assets/client/ui/text.js +33 -0
  110. package/dist/web/assets/client/ui/toast.js +12 -0
  111. package/dist/web/assets/client/views/bodySearch.js +172 -0
  112. package/dist/web/assets/client/views/createTask.js +85 -0
  113. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  114. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  115. package/dist/web/assets/client/views/dock/preview.js +129 -0
  116. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  117. package/dist/web/assets/client/views/globalInput.js +67 -0
  118. package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
  119. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  120. package/dist/web/assets/client/views/skills.js +152 -0
  121. package/dist/web/assets/client/views/task/actions.js +36 -0
  122. package/dist/web/assets/client/views/task/delivery.js +185 -0
  123. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  124. package/dist/web/assets/client/views/task/files.js +272 -0
  125. package/dist/web/assets/client/views/task/observation.js +153 -0
  126. package/dist/web/assets/client/views/task/overview.js +161 -0
  127. package/dist/web/assets/client/views/task/page.js +131 -0
  128. package/dist/web/assets/client/views/task/records.js +163 -0
  129. package/dist/web/assets/client/views/task/runtime.js +130 -0
  130. package/dist/web/assets/client/views/task/timeline.js +190 -0
  131. package/dist/web/assets/shared/geometry.js +19 -0
  132. package/dist/web/assets/shared/taskTabs.js +8 -0
  133. package/dist/web/assets/shared/themes.js +93 -0
  134. package/dist/web/assets/shell/dialogs.js +63 -0
  135. package/dist/web/assets/shell/dock.js +40 -0
  136. package/dist/web/assets/shell/index.js +40 -0
  137. package/dist/web/assets/shell/settings.js +34 -0
  138. package/dist/web/assets/shell/sidebar.js +45 -0
  139. package/dist/web/assets/styles/index.js +41 -0
  140. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
  141. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  142. package/dist/web/assets/styles/markdown.js +22 -3
  143. package/dist/web/assets/styles/tokens.js +10 -63
  144. package/dist/web/assets/styles/ui/controls.js +76 -0
  145. package/dist/web/assets/styles/ui/forms.js +46 -0
  146. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  147. package/dist/web/assets/styles/views/delivery.js +29 -0
  148. package/dist/web/assets/styles/views/dock.js +71 -0
  149. package/dist/web/assets/styles/views/page.js +32 -0
  150. package/dist/web/assets/styles/views/records.js +70 -0
  151. package/dist/web/assets/styles/views/settings.js +26 -0
  152. package/dist/web/assets/styles/views/sidebar.js +74 -0
  153. package/dist/web/assets/styles/views/task.js +80 -0
  154. package/dist/web/assets/styles/views/terminal.js +38 -0
  155. package/dist/web/taskPreviews.js +205 -0
  156. package/dist/web/tmuxWebTerminal.js +16 -9
  157. package/dist/web/webConversation.js +373 -0
  158. package/dist/web/webMaterials.js +155 -0
  159. package/dist/web/webServer.js +295 -18
  160. package/dist/web/webSettings.js +331 -0
  161. package/dist/web/webSkills.js +86 -0
  162. package/dist/web/webSnapshot.js +40 -8
  163. package/dist/web/webTaskSurface.js +106 -8
  164. package/dist/workspace/gitChangeSetCapture.js +3 -1
  165. package/docs/cli-information-contract.md +1 -1
  166. package/docs/cli-information-contract.zh-CN.md +1 -1
  167. package/docs/managed-turn-and-session-runtime.md +31 -0
  168. package/docs/managed-turn-and-session-runtime.zh-CN.md +22 -0
  169. package/docs/release-workflow.md +54 -0
  170. package/docs/release-workflow.zh-CN.md +39 -0
  171. package/docs/storage-baseline.md +38 -1
  172. package/docs/storage-baseline.zh-CN.md +22 -0
  173. package/docs/task-discovery.md +28 -0
  174. package/docs/testing/verification-levels.md +2 -2
  175. package/i18n/README.zh-CN.md +1 -1
  176. package/package.json +1 -1
  177. package/skills/yui-leader/SKILL.md +5 -4
  178. package/skills/yui-leader/references/execution.md +2 -1
  179. package/skills/yui-operator/SKILL.md +3 -0
  180. package/skills/yui-reviewer/SKILL.md +4 -2
  181. package/skills/yui-runtime/SKILL.md +43 -2
  182. package/skills/yui-worker/SKILL.md +3 -2
  183. package/dist/web/assets/client/app.js +0 -808
  184. package/dist/web/assets/client/components.js +0 -477
  185. package/dist/web/assets/client/detail.js +0 -379
  186. package/dist/web/assets/client/dock.js +0 -309
  187. package/dist/web/assets/client/evidence.js +0 -364
  188. package/dist/web/assets/client/forms.js +0 -230
  189. package/dist/web/assets/client/i18n.js +0 -1277
  190. package/dist/web/assets/client/markdown.js +0 -114
  191. package/dist/web/assets/client/records.js +0 -198
  192. package/dist/web/assets/client/sections.js +0 -349
  193. package/dist/web/assets/client/theme.js +0 -71
  194. package/dist/web/assets/shell.js +0 -161
  195. package/dist/web/assets/styles/components.js +0 -216
  196. package/dist/web/assets/styles/views.js +0 -309
  197. /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
@@ -16,6 +16,7 @@
16
16
  * - `restoreAllResourceGc` is the explicit rollback entry point.
17
17
  */
18
18
  import { execFileSync } from "node:child_process";
19
+ import { supportedGit } from "../repository/gitCompatibility.js";
19
20
  import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
20
21
  import { dirname, join, resolve, relative, isAbsolute } from "node:path";
21
22
  import { isDeepStrictEqual } from "node:util";
@@ -384,13 +385,14 @@ function quarantineResource(home, record, now) {
384
385
  const receiptPath = `${quarantinePath}.receipt.json`;
385
386
  try {
386
387
  if (isGitWorktreePath(record.path)) {
388
+ const program = supportedGit();
387
389
  // Move the linked worktree, including its exact checkout and Git
388
390
  // metadata, into the Home-local quarantine. Restore is the inverse
389
391
  // move, so the recorded HEAD cannot drift with a branch.
390
392
  mkdirSync(quarantineRoot, { recursive: true, mode: 0o700 });
391
393
  // Keep the existing SQLite writer fence through this bounded move, so a
392
394
  // Task reopen or new durable owner cannot commit between check and move.
393
- execFileSync("git", ["-C", record.path, "worktree", "move", "--", record.path, quarantinePath], { timeout: 30_000 });
395
+ execFileSync(program.path, ["-C", record.path, "worktree", "move", "--", record.path, quarantinePath], { timeout: 30_000 });
394
396
  writeQuarantineReceipt(home, record, now, "move", receiptPath);
395
397
  return {
396
398
  ok: true,
@@ -496,9 +498,10 @@ function restoreQuarantinedRecord(record, liveRefs, now) {
496
498
  updatedAt: now.toISOString()
497
499
  };
498
500
  }
501
+ const program = isGitWorktreePath(quarantine.path) ? supportedGit() : undefined;
499
502
  mkdirSync(dirname(quarantine.originalPath), { recursive: true });
500
- if (isGitWorktreePath(quarantine.path)) {
501
- execFileSync("git", ["-C", quarantine.path, "worktree", "move", "--", quarantine.path, quarantine.originalPath], { timeout: 30_000 });
503
+ if (program !== undefined) {
504
+ execFileSync(program.path, ["-C", quarantine.path, "worktree", "move", "--", quarantine.path, quarantine.originalPath], { timeout: 30_000 });
502
505
  }
503
506
  else {
504
507
  renameSync(quarantine.path, quarantine.originalPath);
@@ -604,7 +607,7 @@ export async function purgeResourceQuarantine(home, options, store) {
604
607
  try {
605
608
  if (existsSync(quarantine.path)) {
606
609
  if (isGitWorktreePath(quarantine.path)) {
607
- execFileSync("git", ["-C", quarantine.path, "worktree", "remove", "--force", "--", quarantine.path], { timeout: 30_000 });
610
+ execFileSync(supportedGit().path, ["-C", quarantine.path, "worktree", "remove", "--force", "--", quarantine.path], { timeout: 30_000 });
608
611
  }
609
612
  rmSync(quarantine.path, { recursive: true, force: true });
610
613
  }
@@ -9,6 +9,7 @@
9
9
  import { execFileSync } from "node:child_process";
10
10
  import { existsSync } from "node:fs";
11
11
  import { basename, dirname, resolve } from "node:path";
12
+ import { gitOutputLine, supportedGit } from "../repository/gitCompatibility.js";
12
13
  import { upsertResourceRecord } from "./resourceRegistry.js";
13
14
  import { createResourceRegistryStore } from "./resourceRegistryStore.js";
14
15
  import { createResourceRecord, isReleaseNamespacePath, resourceId } from "./resourceTypes.js";
@@ -212,7 +213,7 @@ function ownerFromManagedWorkspace(home, owner, projectId) {
212
213
  };
213
214
  }
214
215
  function readGitWorktreeMetadata(path) {
215
- const commonDir = execFileSync("git", ["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
216
+ const commonDir = resolve(path, gitOutputLine(execFileSync(supportedGit().path, ["-C", path, "rev-parse", "--git-common-dir"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })));
216
217
  const repositoryPath = basename(commonDir) === ".git" ? dirname(commonDir) : commonDir;
217
218
  let branch;
218
219
  try {
@@ -41,6 +41,12 @@ export function createAgentEndpointFactory(start = startStructuredProviderSessio
41
41
  endpoint.observe(event);
42
42
  };
43
43
  const opened = await start(payload, {
44
+ // The native TUI owns rendering while attached. Raw JSON mirroring into
45
+ // its inherited PTY would corrupt its screen on every Provider event.
46
+ mirrorOutput: (stream, text) => {
47
+ if (!endpoint?.nativeAccess?.attached)
48
+ (stream === "stdout" ? process.stdout : process.stderr).write(text);
49
+ },
44
50
  onAccepted: (value) => emit({ type: "accepted", value }),
45
51
  onActivity: (value) => emit({ type: "activity", value }),
46
52
  onStarted: (value) => emit({ type: "started", value }),
@@ -74,6 +80,7 @@ function freezeConfiguration(value) {
74
80
  class BuiltinAgentEndpoint {
75
81
  driver;
76
82
  configuration;
83
+ get nativeAccess() { return this.driver.nativeAccess; }
77
84
  get ownedProcessId() { return this.driver.ownedProcessId; }
78
85
  get nativeAccountHome() { return this.driver.nativeAccountHome; }
79
86
  capabilities;
@@ -118,6 +125,7 @@ class BuiltinAgentEndpoint {
118
125
  get nativeSessionId() { return this.driver.nativeSessionId; }
119
126
  get conversationId() { return this.driver.conversationId; }
120
127
  get processInstanceId() { return this.driver.processInstanceId; }
128
+ get publicReply() { return this.driver.publicReply; }
121
129
  /**
122
130
  * Delegated rather than copied at construction: the Session reads its own
123
131
  * current state, and a value captured here would freeze the configuration as
@@ -18,6 +18,7 @@ import { closeAgentHostEndpoints } from "./agentHostCleanup.js";
18
18
  import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
19
19
  import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
20
20
  import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
21
+ import { openControlledCodexTui } from "./controlledCodexTui.js";
21
22
  import { validateAgentHostLaunchPayload } from "./launchBroker.js";
22
23
  import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
23
24
  import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox } from "./processExitOutbox.js";
@@ -108,6 +109,10 @@ export async function runAgentHost(input) {
108
109
  let snapshot = hostSnapshot("idle");
109
110
  let dispatchTail = Promise.resolve();
110
111
  let promptHuman = () => { };
112
+ let humanConsole;
113
+ let nativeTui;
114
+ let nativeOpening;
115
+ let modelChanging = false;
111
116
  const recentSteerAttempts = new Map();
112
117
  await replayExitOutbox(input.home);
113
118
  const updateSnapshot = (next) => {
@@ -444,6 +449,8 @@ export async function runAgentHost(input) {
444
449
  && providerSession.adapterId === "codex"
445
450
  && !hostStopRequested;
446
451
  if (reconnectableCodexClient) {
452
+ nativeTui?.close();
453
+ nativeTui = undefined;
447
454
  session = undefined;
448
455
  if (codexClientAttachedAt !== undefined
449
456
  && Date.now() - codexClientAttachedAt >= CODEX_CLIENT_STABLE_MS) {
@@ -1028,7 +1035,128 @@ export async function runAgentHost(input) {
1028
1035
  promptHuman();
1029
1036
  return snapshot;
1030
1037
  };
1031
- const control = await openAgentHostControl(input.home, payload, () => snapshot, async (request) => {
1038
+ const nativeMutation = async (expected, method, params) => {
1039
+ if (session !== expected || params.threadId !== expected.nativeSessionId || !authority
1040
+ || (sessionPayload?.environment.YUI_SESSION_SCOPE === "task" && sessionPayload.environment.YUI_ROLE !== "leader")) {
1041
+ throw new Error("Native input targets a replaced or unsupported Session.");
1042
+ }
1043
+ if (method === "turn/interrupt") {
1044
+ if (!activeRunAttemptId || params.turnId !== activeNativeTurnId)
1045
+ throw new Error("Native stop targets a different Turn.");
1046
+ const cancelled = await expected.cancel(activeRunAttemptId);
1047
+ if (cancelled.status !== "requested")
1048
+ throw new Error(`Native stop is ${cancelled.status}; do not replay.`);
1049
+ return {};
1050
+ }
1051
+ if (!Array.isArray(params.input) || params.input.length === 0
1052
+ || params.input.some(part => !part || typeof part !== "object"
1053
+ || part.type !== "text" || typeof part.text !== "string")) {
1054
+ throw new Error("Managed native access currently supports text input only.");
1055
+ }
1056
+ const boundedText = params.input.map(part => part.text).join("\n");
1057
+ if (!boundedText.trim() || boundedText.includes("\0") || Buffer.byteLength(boundedText) > 12000)
1058
+ throw new Error("Native input exceeds its text bound.");
1059
+ const run = { attemptId: `human:${authority.holderId}:${randomUUID()}`, boundedText };
1060
+ if (method === "turn/steer") {
1061
+ const turnId = activeNativeTurnId;
1062
+ if (params.expectedTurnId !== turnId || !turnId)
1063
+ throw new Error("Native steer targets a different Turn.");
1064
+ if (!sessionPayload)
1065
+ throw new Error("Native Session launch context is unavailable.");
1066
+ // Persist human intent and the existing steer receipt before Provider
1067
+ // write. A fresh human turn id is not an additional-input binding.
1068
+ const prepared = await callControllerIdempotently(input.home, "runtime.native-steer-prepare", {
1069
+ ...hostRunControlParams(sessionPayload, expected.nativeSessionId, authority, run.attemptId),
1070
+ nativeTurnId: turnId, boundedText
1071
+ });
1072
+ const result = await handleControl({ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "steer-turn",
1073
+ nativeSessionId: expected.nativeSessionId, nativeTurnId: turnId, authority, run: prepared });
1074
+ if (result.outcome !== "accepted")
1075
+ throw new Error("Native steer acceptance is unconfirmed; do not resubmit.");
1076
+ return { turnId };
1077
+ }
1078
+ const result = await handleControl({ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn",
1079
+ nativeSessionId: expected.nativeSessionId, authority, run });
1080
+ const accepted = result.snapshot;
1081
+ if (result.outcome !== "accepted" || accepted.inputAcceptance !== "provider" || !accepted.nativeTurnId) {
1082
+ throw new Error("Native acceptance is unconfirmed; do not resubmit.");
1083
+ }
1084
+ return { turn: { id: accepted.nativeTurnId, items: [], status: "inProgress", error: null } };
1085
+ };
1086
+ const handleControl = async (request) => {
1087
+ if (request.type === "native-model") {
1088
+ return enqueueSerialized(async () => {
1089
+ const current = session;
1090
+ if (!current?.nativeAccess || current.nativeSessionId !== request.nativeSessionId
1091
+ || !sessionPayload || activeNativeTurnId || authority?.owner !== "controller"
1092
+ || (sessionPayload.environment.YUI_SESSION_SCOPE === "task" && sessionPayload.environment.YUI_ROLE !== "leader")
1093
+ || (sessionPayload.environment.YUI_SESSION_SCOPE === "global" && sessionPayload.environment.YUI_ROLE !== "operator")) {
1094
+ throw new Error("Model switching requires the idle current controlled Codex Session, without a native terminal writer.");
1095
+ }
1096
+ if (nativeTui || nativeOpening) {
1097
+ throw new Error("Exit the native TUI before changing the Session model. Closing a browser terminal viewer only detaches it; the Host-owned TUI remains open.");
1098
+ }
1099
+ modelChanging = true;
1100
+ try {
1101
+ await assertHostExecutionEnvironment(input.home, sessionPayload, current.nativeSessionId);
1102
+ try {
1103
+ await current.nativeAccess.setModel(request.model);
1104
+ }
1105
+ catch (error) {
1106
+ if (error instanceof CodexPreSubmissionError)
1107
+ throw error;
1108
+ throw new ProviderDeliveryUnknownError(`Native model adoption is unconfirmed: ${errorText(error)}`, `model:${current.nativeSessionId}`, { cause: error });
1109
+ }
1110
+ return controlResult("accepted", { ...snapshot, runConfiguration: current.runConfiguration });
1111
+ }
1112
+ finally {
1113
+ modelChanging = false;
1114
+ }
1115
+ });
1116
+ }
1117
+ if (request.type === "native-open" || request.type === "native-respond") {
1118
+ const current = session;
1119
+ if (!current?.nativeAccess || current.nativeSessionId !== request.nativeSessionId
1120
+ || !sessionPayload || (sessionPayload.environment.YUI_SESSION_SCOPE === "task"
1121
+ && sessionPayload.environment.YUI_ROLE !== "leader"))
1122
+ throw new Error("Native access requires the current controlled Codex Operator/Leader.");
1123
+ if (request.type === "native-respond") {
1124
+ await assertHostExecutionEnvironment(input.home, sessionPayload, current.nativeSessionId);
1125
+ await current.nativeAccess.respond(request.requestId, request.nativeSessionId, request.nativeTurnId ?? null, request.result);
1126
+ }
1127
+ else if (!nativeTui) {
1128
+ if (modelChanging)
1129
+ throw new Error("Wait for model adoption before attaching native input.");
1130
+ if (!process.stdin.isTTY)
1131
+ throw new Error("Native TUI requires the Host's owned terminal.");
1132
+ humanConsole?.close();
1133
+ humanConsole = undefined;
1134
+ nativeOpening ??= openControlledCodexTui(sessionPayload, current.nativeSessionId, current.nativeAccess, {
1135
+ mutate: (method, params) => nativeMutation(current, method, params),
1136
+ respond: async (requestId, nativeTurnId, result) => {
1137
+ await handleControl({ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "native-respond",
1138
+ nativeSessionId: current.nativeSessionId, requestId, nativeTurnId, result });
1139
+ },
1140
+ onExit: () => {
1141
+ nativeTui = undefined;
1142
+ process.stdout.write("\r\nNative TUI detached. Reattach the terminal to reopen this same Thread; the Agent Host remains running.\r\n");
1143
+ },
1144
+ onError: error => process.stderr.write(`Native TUI: ${errorText(error)}\n`)
1145
+ });
1146
+ try {
1147
+ nativeTui = await nativeOpening;
1148
+ }
1149
+ finally {
1150
+ nativeOpening = undefined;
1151
+ }
1152
+ if (session !== current) {
1153
+ nativeTui.close();
1154
+ nativeTui = undefined;
1155
+ throw new Error("Session changed while opening native access.");
1156
+ }
1157
+ }
1158
+ return controlResult("accepted", { ...snapshot, nativeTui: !!nativeTui });
1159
+ }
1032
1160
  if (request.type === "status") {
1033
1161
  // Read the Agent's configuration at answer time, not from the stored
1034
1162
  // snapshot. The Agent may have changed it since the last state
@@ -1036,10 +1164,20 @@ export async function runAgentHost(input) {
1036
1164
  // current reading rather than the one that was true at launch. Nothing is
1037
1165
  // sent to the Agent to obtain it: this reads what the Session has already
1038
1166
  // been told.
1167
+ let requestBytes = 0;
1168
+ const nativeRequests = session?.nativeAccess?.requests.filter(request => {
1169
+ requestBytes += Buffer.byteLength(JSON.stringify(request));
1170
+ return requestBytes <= 8000;
1171
+ }).slice(0, 8);
1039
1172
  return controlResult("status", session === undefined
1040
1173
  ? snapshot
1041
1174
  : validateSnapshot({
1042
1175
  ...snapshot,
1176
+ nativeTui: !!nativeTui,
1177
+ ...(nativeRequests === undefined ? {} : { nativeRequests,
1178
+ nativeRequestsOmitted: nativeRequests.length !== session.nativeAccess.requests.length }),
1179
+ ...(session.publicReply === undefined ? {} : { publicReply: session.publicReply }),
1180
+ ...(session.adapterId === "codex" ? { publicReplyObservation: "supported" } : {}),
1043
1181
  runConfiguration: session.runConfiguration
1044
1182
  }));
1045
1183
  }
@@ -1138,6 +1276,8 @@ export async function runAgentHost(input) {
1138
1276
  const accepted = await enqueueSerialized(async () => setAuthority(request));
1139
1277
  return controlResult("accepted", accepted);
1140
1278
  }
1279
+ if (request.type !== "launch")
1280
+ throw new Error("Unsupported Host request.");
1141
1281
  const redeemed = await redeem(input.home, request.ticket);
1142
1282
  if (activeRunPayload !== undefined
1143
1283
  || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
@@ -1149,8 +1289,9 @@ export async function runAgentHost(input) {
1149
1289
  }
1150
1290
  const accepted = await enqueueDispatch(redeemed);
1151
1291
  return controlResult("accepted", accepted);
1152
- });
1153
- const humanConsole = process.stdin.isTTY
1292
+ };
1293
+ const control = await openAgentHostControl(input.home, payload, () => snapshot, handleControl);
1294
+ humanConsole = process.stdin.isTTY
1154
1295
  ? createInterface({ input: process.stdin, output: process.stdout, terminal: true })
1155
1296
  : undefined;
1156
1297
  promptHuman = () => {
@@ -1231,6 +1372,7 @@ export async function runAgentHost(input) {
1231
1372
  if (forceKillTimer !== undefined)
1232
1373
  clearTimeout(forceKillTimer);
1233
1374
  humanConsole?.close();
1375
+ nativeTui?.close();
1234
1376
  await closeAgentHostEndpoints({
1235
1377
  owner: endpointOwner, session, lease: endpointLease, adapterId: endpointAdapterId,
1236
1378
  attemptId: activeRunAttemptId ?? lastTerminal?.attemptId,
@@ -1309,6 +1451,9 @@ export async function sendAgentHostAuthorityControl(input) {
1309
1451
  export async function sendAgentHostCancelControl(input) {
1310
1452
  return await sendAgentHostControl(input);
1311
1453
  }
1454
+ export async function sendAgentHostNativeControl(input) {
1455
+ return sendAgentHostControl(input);
1456
+ }
1312
1457
  export async function inspectAgentHost(input) {
1313
1458
  const result = await sendAgentHostControl({
1314
1459
  ...input,
@@ -1550,6 +1695,12 @@ function boundControlResponse(result) {
1550
1695
  };
1551
1696
  if (withinControlBound(result))
1552
1697
  return result;
1698
+ if (result.snapshot.publicReply !== undefined) {
1699
+ const { publicReply: _omitted, ...snapshot } = result.snapshot;
1700
+ result = { ...result, snapshot };
1701
+ if (withinControlBound(result))
1702
+ return result;
1703
+ }
1553
1704
  const clip = (text, chars) => {
1554
1705
  if (text.length <= chars)
1555
1706
  return text;
@@ -1613,6 +1764,22 @@ function validateControl(control) {
1613
1764
  }
1614
1765
  if (control.type === "status")
1615
1766
  return Object.freeze({ ...control });
1767
+ if (control.type === "native-open" || control.type === "native-respond" || control.type === "native-model") {
1768
+ validateIdentity(control.nativeSessionId, "native Session id");
1769
+ if (control.type === "native-model" && (typeof control.model !== "string"
1770
+ || !control.model.trim() || control.model.length > 200 || control.model.includes("\0"))) {
1771
+ throw new Error("Invalid native model.");
1772
+ }
1773
+ if (control.type === "native-respond") {
1774
+ if (control.nativeTurnId !== null)
1775
+ validateIdentity(control.nativeTurnId, "native Turn id");
1776
+ if ((typeof control.requestId !== "string" && typeof control.requestId !== "number")
1777
+ || control.result === null || typeof control.result !== "object" || Array.isArray(control.result)) {
1778
+ throw new Error("Invalid native request response.");
1779
+ }
1780
+ }
1781
+ return Object.freeze({ ...control });
1782
+ }
1616
1783
  if (control.type === "cancel") {
1617
1784
  if (control.nativeOnly !== undefined && typeof control.nativeOnly !== "boolean") {
1618
1785
  throw new Error("Native-only cancellation flag is invalid.");
@@ -42,7 +42,7 @@ export class CodexAppServerRuntime {
42
42
  cwd: text(input.cwd, "Codex thread cwd"),
43
43
  ...threadOptions(input)
44
44
  });
45
- return { conversationId: threadId(result) };
45
+ return { conversationId: threadId(result), raw: result };
46
46
  }
47
47
  async resumeConversation(conversationId, options = {}) {
48
48
  const id = text(conversationId, "Codex thread id");
@@ -0,0 +1,175 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
3
+ import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
4
+ const READS = new Set([
5
+ "config/read", "configRequirements/read", "model/list", "collaborationMode/list",
6
+ "experimentalFeature/list", "skills/list", "hooks/list", "mcpServerStatus/list", "account/read",
7
+ "account/rateLimits/read", "modelProvider/capabilities/read", "permissionProfile/list",
8
+ "thread/read", "thread/turns/list", "thread/items/list", "thread/goal/get"
9
+ ]);
10
+ const REQUESTS = new Set([
11
+ "item/commandExecution/requestApproval", "item/fileChange/requestApproval",
12
+ "item/tool/requestUserInput", "item/permissions/requestApproval", "mcpServer/elicitation/request"
13
+ ]);
14
+ /** A view of one existing connection, never another Thread or durable input queue.
15
+ * Both the native TUI and Web answer the original server request on this transport. */
16
+ export function createCodexNativeAccess(threadId, initialized, channel, openingMessages = [], initialConfiguration) {
17
+ const pending = new Map();
18
+ const listeners = new Set();
19
+ let configuration = initialConfiguration;
20
+ let observedAt = new Date().toISOString();
21
+ const observed = () => typeof configuration?.model !== "string"
22
+ ? unknownAgentRunConfiguration("No confirmed native model response. Reconnect to restore the fixed launch configuration.")
23
+ : {
24
+ status: "observed", observedAt,
25
+ handshake: { status: "unsupported", reason: "Model reported by Codex thread/start or thread/resume; not a capability handshake." },
26
+ requested: [],
27
+ axes: [{ key: "model", category: "model", current: { status: "observed", value: configuration.model }, offered: [] }]
28
+ };
29
+ const observe = (message) => {
30
+ const params = message.params;
31
+ if (params?.threadId !== threadId)
32
+ return;
33
+ if ((typeof message.id === "string" || typeof message.id === "number")
34
+ && typeof message.method === "string" && REQUESTS.has(message.method)
35
+ && (typeof params.turnId === "string"
36
+ || (message.method === "mcpServer/elicitation/request" && params.turnId == null))) {
37
+ pending.set(message.id, { id: message.id, method: message.method,
38
+ threadId, turnId: typeof params.turnId === "string" ? params.turnId : null, params });
39
+ }
40
+ if (message.method === "serverRequest/resolved")
41
+ pending.delete(params.requestId);
42
+ if (message.method === "turn/completed") {
43
+ const turn = params.turn;
44
+ for (const [id, request] of pending)
45
+ if (request.turnId === turn?.id)
46
+ pending.delete(id);
47
+ }
48
+ for (const listener of listeners)
49
+ listener(message);
50
+ };
51
+ for (const message of openingMessages)
52
+ observe(message);
53
+ channel.onMessage(observe);
54
+ return {
55
+ initialized,
56
+ get attached() { return listeners.size > 0; },
57
+ get requests() { return [...pending.values()]; },
58
+ get runConfiguration() { return observed(); },
59
+ async setModel(model) {
60
+ try {
61
+ if (!model.trim() || model.length > 200 || model.includes("\0"))
62
+ throw new Error("Invalid model.");
63
+ const read = await channel.request("thread/read", { threadId, includeTurns: false });
64
+ const thread = read.thread;
65
+ if (thread?.id !== threadId || thread.status?.type !== "idle") {
66
+ throw new Error("Model switching requires a confirmed idle Thread.");
67
+ }
68
+ }
69
+ catch (error) {
70
+ throw new CodexPreSubmissionError(error instanceof Error ? error.message : "Native idle state is unavailable.");
71
+ }
72
+ // No fork/start, account, permission or Role-default mutation. Do not
73
+ // preserve an old observation if the native mutation becomes uncertain.
74
+ configuration = undefined;
75
+ const result = await channel.request("thread/resume", { threadId, model, excludeTurns: true });
76
+ if (result.thread?.id !== threadId || result.model !== model) {
77
+ throw new Error("Native model adoption was not confirmed; inspect before another change.");
78
+ }
79
+ configuration = result;
80
+ observedAt = new Date().toISOString();
81
+ },
82
+ async read(method, params) {
83
+ // TUI startup attaches to the already opened Thread without resuming it
84
+ // with new options or changing the Host's configured execution boundary.
85
+ if (method === "thread/resume") {
86
+ if (params.threadId !== threadId)
87
+ throw new Error("Select the existing Session; new/forked Threads are not supported here.");
88
+ const page = params.initialTurnsPage;
89
+ configuration = await channel.request("thread/resume", { threadId,
90
+ ...(typeof params.excludeTurns === "boolean" ? { excludeTurns: params.excludeTurns } : {}),
91
+ ...(page ? { initialTurnsPage: { ...page, limit: Math.min(40, typeof page.limit === "number" && page.limit > 0 ? page.limit : 40) } } : {}) });
92
+ observedAt = new Date().toISOString();
93
+ return configuration;
94
+ }
95
+ if (!READS.has(method))
96
+ throw new Error(`Native operation is not available in managed access: ${method}.`);
97
+ if (method.startsWith("thread/") && params.threadId !== threadId) {
98
+ throw new Error("Native read targets a different Thread.");
99
+ }
100
+ // Native TUI owns its hydration protocol (legacy TUI can explicitly ask
101
+ // for full turns). Web history remains independently bounded/paginated.
102
+ return channel.request(method, method === "thread/turns/list" || method === "thread/items/list"
103
+ ? { ...params, limit: Math.min(40, typeof params.limit === "number" && params.limit > 0 ? params.limit : 40) }
104
+ : params);
105
+ },
106
+ validateTurn(params) {
107
+ if (!configuration)
108
+ throw new Error("Native TUI has not attached to the existing Thread.");
109
+ for (const [key, source] of Object.entries({
110
+ model: "model", cwd: "cwd", runtimeWorkspaceRoots: "runtimeWorkspaceRoots",
111
+ approvalPolicy: "approvalPolicy", approvalsReviewer: "approvalsReviewer",
112
+ effort: "reasoningEffort", collaborationMode: "collaborationMode"
113
+ })) {
114
+ if (params[key] != null && !isDeepStrictEqual(params[key], configuration[source])) {
115
+ throw new Error(`Native ${key} differs from the managed Session; change it through Yui configuration.`);
116
+ }
117
+ }
118
+ for (const key of ["sandboxPolicy", "permissions", "environments", "toolOutput", "additionalContext", "outputSchema", "disabledPluginIds", "serviceTierForTurn", "summary", "personality", "cyberAccessProgram"]) {
119
+ if (params[key] != null)
120
+ throw new Error(`Native ${key} overrides are not supported in managed access.`);
121
+ }
122
+ if (params.serviceTier != null && params.serviceTier !== (configuration.serviceTier ?? "default")) {
123
+ throw new Error("Native service tier differs from the managed Session.");
124
+ }
125
+ },
126
+ async respond(id, expectedThread, turnId, result) {
127
+ const request = pending.get(id);
128
+ if (!request || request.threadId !== expectedThread || request.turnId !== turnId) {
129
+ throw new Error("Native request is no longer pending for this exact Thread and Turn.");
130
+ }
131
+ validateNativeResponse(request, result);
132
+ // Claim before awaiting transport. A lost send acknowledgement is unknown,
133
+ // not permission for another entry to repeat a possibly applied answer.
134
+ pending.delete(id);
135
+ await channel.send({ id, result });
136
+ },
137
+ events(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; }
138
+ };
139
+ }
140
+ function validateNativeResponse(request, result) {
141
+ if (!result || typeof result !== "object" || Array.isArray(result))
142
+ throw new Error("Missing native response.");
143
+ if (request.method === "mcpServer/elicitation/request") {
144
+ if (!["accept", "decline", "cancel"].includes(String(result.action))
145
+ || (result.action !== "accept" && result.content != null)) {
146
+ throw new Error("Invalid native MCP elicitation response.");
147
+ }
148
+ }
149
+ else if (request.method === "item/tool/requestUserInput") {
150
+ const answers = result.answers;
151
+ const questions = request.params.questions;
152
+ if (!answers || typeof answers !== "object" || Array.isArray(answers) || !Array.isArray(questions)) {
153
+ throw new Error("Native question requires answers keyed by question id.");
154
+ }
155
+ const ids = questions.map(q => q.id);
156
+ if (Object.keys(answers).some(id => !ids.includes(id))
157
+ || ids.some(id => {
158
+ if (typeof id !== "string")
159
+ return true;
160
+ const answer = answers[id];
161
+ return !Array.isArray(answer?.answers) || answer.answers.some(value => typeof value !== "string");
162
+ })) {
163
+ throw new Error("Native answers do not match the original questions.");
164
+ }
165
+ }
166
+ else if (request.method === "item/permissions/requestApproval") {
167
+ // Web currently exposes decline only; richer native grants remain the TUI's
168
+ // protocol responsibility. Never infer broad grants from an accept button.
169
+ if (!result.permissions || typeof result.permissions !== "object")
170
+ throw new Error("Missing native permissions response.");
171
+ }
172
+ else if (!["accept", "acceptForSession", "decline", "cancel"].includes(String(result.decision))) {
173
+ throw new Error("Invalid native approval decision.");
174
+ }
175
+ }
@@ -0,0 +1,116 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
3
+ import { once } from "node:events";
4
+ import WebSocket, { WebSocketServer } from "ws";
5
+ import { localCodexTuiEnvironment } from "./codexInteractiveHost.js";
6
+ /** The TUI is a view of the Host-owned connection. It cannot launch, fork,
7
+ * reconfigure, or submit outside the Host's existing input admission. */
8
+ export async function openControlledCodexTui(payload, threadId, access, input) {
9
+ const baseArgs = JSON.parse(payload.environment.YUI_AGENT_BASE_ARGS ?? "[]");
10
+ if (!Array.isArray(baseArgs) || baseArgs.some(arg => typeof arg !== "string"))
11
+ throw new Error("Invalid Codex base arguments.");
12
+ const token = randomBytes(32).toString("hex");
13
+ const server = new WebSocketServer({
14
+ host: "127.0.0.1", port: 0, maxPayload: 1024 * 1024, perMessageDeflate: false,
15
+ verifyClient: (info) => info.req.headers.authorization === `Bearer ${token}`
16
+ });
17
+ let socket;
18
+ let closed = false;
19
+ let markReady;
20
+ let failReady;
21
+ const ready = new Promise((resolve, reject) => { markReady = resolve; failReady = reject; });
22
+ void ready.catch(() => { });
23
+ const unsubscribe = access.events(message => {
24
+ if (socket?.readyState === WebSocket.OPEN)
25
+ socket.send(JSON.stringify(message));
26
+ });
27
+ server.on("connection", client => {
28
+ if (socket || closed) {
29
+ client.close(1008, "TUI already attached.");
30
+ return;
31
+ }
32
+ socket = client;
33
+ client.on("error", input.onError);
34
+ client.on("message", (bytes, binary) => {
35
+ void (async () => {
36
+ if (binary)
37
+ throw new Error("Expected native JSON request.");
38
+ const message = JSON.parse(bytes.toString());
39
+ const id = message.id;
40
+ if (typeof id !== "number" && typeof id !== "string")
41
+ return; // notifications
42
+ try {
43
+ if (message.method === undefined) {
44
+ const request = access.requests.find(request => request.id === id);
45
+ if (!request)
46
+ throw new Error("Native request is no longer pending.");
47
+ await input.respond(id, request.turnId, message.result);
48
+ return;
49
+ }
50
+ const method = String(message.method), params = (message.params ?? {});
51
+ if (method === "turn/start")
52
+ access.validateTurn(params);
53
+ const result = method === "initialize" ? access.initialized
54
+ : ["turn/start", "turn/steer", "turn/interrupt"].includes(method) ? await input.mutate(method, params)
55
+ : await access.read(method, params);
56
+ if (client.readyState === WebSocket.OPEN)
57
+ client.send(JSON.stringify({ id, result }));
58
+ if (method === "thread/resume") {
59
+ markReady();
60
+ for (const request of access.requests)
61
+ client.send(JSON.stringify({
62
+ id: request.id, method: request.method, params: request.params
63
+ }));
64
+ }
65
+ }
66
+ catch (error) {
67
+ if (client.readyState === WebSocket.OPEN)
68
+ client.send(JSON.stringify({
69
+ id, error: { code: -32600, message: error instanceof Error ? error.message : String(error) }
70
+ }));
71
+ }
72
+ })().catch(input.onError);
73
+ });
74
+ });
75
+ try {
76
+ await once(server, "listening");
77
+ }
78
+ catch (error) {
79
+ unsubscribe();
80
+ server.close();
81
+ throw error;
82
+ }
83
+ const address = server.address();
84
+ if (!address || typeof address === "string")
85
+ throw new Error("Native TUI relay did not bind.");
86
+ const child = spawn(payload.command, [...baseArgs,
87
+ "--remote", `ws://127.0.0.1:${address.port}`, "--remote-auth-token-env", "YUI_CODEX_REMOTE_AUTH_TOKEN",
88
+ "resume", threadId], {
89
+ cwd: payload.cwd, stdio: "inherit",
90
+ env: { ...localCodexTuiEnvironment(payload.environment), YUI_CODEX_REMOTE_AUTH_TOKEN: token }
91
+ });
92
+ const close = () => {
93
+ if (closed)
94
+ return;
95
+ closed = true;
96
+ unsubscribe();
97
+ socket?.terminate();
98
+ server.close();
99
+ child.kill("SIGTERM");
100
+ };
101
+ server.on("error", input.onError);
102
+ child.on("error", error => { failReady(error); input.onError(error); close(); input.onExit(); });
103
+ child.once("exit", () => { failReady(new Error("Native TUI exited before attachment.")); close(); input.onExit(); });
104
+ const deadline = setTimeout(() => failReady(new Error("Native TUI did not attach to the selected Thread.")), 10_000);
105
+ try {
106
+ await ready;
107
+ }
108
+ catch (error) {
109
+ close();
110
+ throw error;
111
+ }
112
+ finally {
113
+ clearTimeout(deadline);
114
+ }
115
+ return { close };
116
+ }