@zq-silk/yui 2.21.0 → 2.23.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 (145) hide show
  1. package/README.md +5 -0
  2. package/dist/cli/commandCatalog.js +87 -27
  3. package/dist/cli/dynamicCompletion.js +4 -1
  4. package/dist/cli/helpRenderer.js +12 -1
  5. package/dist/cli/interactionPolicy.js +2 -0
  6. package/dist/cli/invocationRouter.js +5 -5
  7. package/dist/cli/roleWizard.js +5 -1
  8. package/dist/cli/updateCommand.js +20 -3
  9. package/dist/cli/updateOrchestrator.js +13 -0
  10. package/dist/cli/updatePorts.js +27 -0
  11. package/dist/commands/globalRoleCommands.js +33 -8
  12. package/dist/commands/inboxCommands.js +39 -12
  13. package/dist/commands/operatorCommands.js +2 -5
  14. package/dist/commands/releaseCommands.js +16 -2
  15. package/dist/commands/roleConfiguration.js +25 -2
  16. package/dist/commands/sessionCommands.js +5 -2
  17. package/dist/commands/taskCommands.js +217 -64
  18. package/dist/commands/taskContextCommand.js +26 -4
  19. package/dist/commands/taskFactCommands.js +15 -2
  20. package/dist/commands/taskIntegrationCommands.js +3 -3
  21. package/dist/commands/taskRoleRuntimeStatus.js +19 -5
  22. package/dist/commands/textInput.js +2 -2
  23. package/dist/context/contextFamilies.js +10 -0
  24. package/dist/context/currentSkills.js +37 -4
  25. package/dist/context/runContextPack.js +44 -5
  26. package/dist/context/runInputContract.js +1 -1
  27. package/dist/context/sessionBootstrapManifest.js +21 -6
  28. package/dist/context/sessionSkillNotice.js +74 -0
  29. package/dist/context/taskContext.js +100 -16
  30. package/dist/controlPlaneCli.js +17 -7
  31. package/dist/controller/agentRuntimeObserver.js +57 -12
  32. package/dist/controller/clientRuntime.js +66 -18
  33. package/dist/controller/controller.js +19 -2
  34. package/dist/controller/diagnosticJournal.js +10 -2
  35. package/dist/controller/fileSchedulerStoreAdapter.js +74 -24
  36. package/dist/controller/hostLifecycle.js +153 -0
  37. package/dist/controller/hostLifecycleChecks.js +27 -0
  38. package/dist/controller/hostUpgrade.js +344 -0
  39. package/dist/controller/messageHealth.js +4 -3
  40. package/dist/controller/providerRetryAdmission.js +15 -1
  41. package/dist/controller/providerRetryDelivery.js +18 -2
  42. package/dist/controller/runtime.js +82 -21
  43. package/dist/controller/runtimeEventInbox.js +107 -13
  44. package/dist/controller/runtimeEventProcessor.js +3 -3
  45. package/dist/controller/sessionNotify.js +5 -46
  46. package/dist/controller/taskRunRecovery.js +153 -0
  47. package/dist/core/controllerClient.js +16 -2
  48. package/dist/core/controllerServer.js +14 -5
  49. package/dist/core/controllerTelemetry.js +51 -5
  50. package/dist/errors/cliFailure.js +14 -5
  51. package/dist/execution/codexThreadNaming.js +54 -43
  52. package/dist/executor/agentAdapter.js +32 -4
  53. package/dist/executor/agentConfigurationFields.js +2 -0
  54. package/dist/executor/fileRoleLaunchPlanner.js +18 -3
  55. package/dist/integration/gitIntegrationService.js +3 -2
  56. package/dist/integration/integrationSourceApplication.js +12 -1
  57. package/dist/interaction/notificationPresentation.js +11 -16
  58. package/dist/interaction/operatorPresentation.js +1 -1
  59. package/dist/message/messageContinuation.js +3 -1
  60. package/dist/output/boundedRead.js +36 -7
  61. package/dist/output/rolePresentation.js +3 -1
  62. package/dist/runtime/agentEndpoint.js +11 -1
  63. package/dist/runtime/agentEndpointIdentity.js +2 -0
  64. package/dist/runtime/agentHost.js +174 -40
  65. package/dist/runtime/agentHostImplementation.js +3 -1
  66. package/dist/runtime/codexAppServerRuntime.js +113 -24
  67. package/dist/runtime/codexInteractiveHost.js +102 -11
  68. package/dist/runtime/codexNativeAccess.js +1 -1
  69. package/dist/runtime/contextRecovery.js +15 -0
  70. package/dist/runtime/hostSessionLifecycle.js +83 -0
  71. package/dist/runtime/hostUpgradeGate.js +43 -0
  72. package/dist/runtime/lifecycleChecks.js +47 -0
  73. package/dist/runtime/lifecycleReservation.js +9 -1
  74. package/dist/runtime/nativeTitleReceipt.js +31 -0
  75. package/dist/runtime/nativeTitleSync.js +192 -0
  76. package/dist/runtime/providerRetry.js +9 -6
  77. package/dist/runtime/providerRuntimeIdentity.js +1 -1
  78. package/dist/runtime/structuredProviderHost.js +65 -35
  79. package/dist/scheduler/wakeupQueue.js +10 -4
  80. package/dist/storage/sqliteSchema.js +29 -0
  81. package/dist/storage/storageVersions.js +1 -1
  82. package/dist/tmux/tmuxManager.js +4 -2
  83. package/dist/web/webSettings.js +2 -0
  84. package/docs/authorization.md +18 -3
  85. package/docs/cli-information-contract.md +228 -6
  86. package/docs/cli-information-contract.zh-CN.md +95 -5
  87. package/docs/managed-turn-and-session-runtime.md +177 -21
  88. package/docs/managed-turn-and-session-runtime.zh-CN.md +126 -14
  89. package/docs/provider-retry.md +11 -0
  90. package/docs/release-workflow.md +65 -0
  91. package/docs/release-workflow.zh-CN.md +48 -0
  92. package/docs/roles-and-configuration.md +28 -0
  93. package/docs/roles-and-configuration.zh-CN.md +21 -0
  94. package/docs/storage-baseline.md +13 -1
  95. package/docs/storage-baseline.zh-CN.md +5 -1
  96. package/docs/task-delivery.md +7 -0
  97. package/docs/task-delivery.zh-CN.md +5 -0
  98. package/docs/testing/review-evaluation.md +7 -7
  99. package/docs/testing/review-evaluation.zh-CN.md +7 -7
  100. package/docs/testing/verification-levels.md +45 -2
  101. package/docs/testing/verification-levels.zh-CN.md +21 -2
  102. package/i18n/README.zh-CN.md +6 -0
  103. package/package.json +1 -1
  104. package/skills/yui-leader/SKILL.md +98 -193
  105. package/skills/yui-leader/VERSION +1 -1
  106. package/skills/yui-leader/references/artifacts.md +41 -0
  107. package/skills/yui-leader/references/assignment-execution.md +140 -0
  108. package/skills/yui-leader/references/completion.md +79 -0
  109. package/skills/yui-leader/references/delivery-scope.md +26 -0
  110. package/skills/yui-leader/references/execution.md +16 -487
  111. package/skills/yui-leader/references/implementation.md +81 -0
  112. package/skills/yui-leader/references/planning.md +7 -45
  113. package/skills/yui-leader/references/review.md +90 -0
  114. package/skills/yui-leader/references/task-progress.md +23 -0
  115. package/skills/yui-leader/references/work-allocation.md +118 -0
  116. package/skills/yui-operator/SKILL.md +77 -333
  117. package/skills/yui-operator/VERSION +1 -1
  118. package/skills/yui-operator/references/body-transport.md +62 -0
  119. package/skills/yui-operator/references/configuration.md +47 -0
  120. package/skills/yui-operator/references/lifecycle.md +41 -0
  121. package/skills/yui-operator/references/progress.md +39 -0
  122. package/skills/yui-operator/references/routing.md +57 -0
  123. package/skills/yui-operator/references/submission.md +56 -0
  124. package/skills/yui-operator/references/task-delivery.md +1 -1
  125. package/skills/yui-operator/references/web-lifecycle.md +19 -0
  126. package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
  127. package/skills/yui-runtime/SKILL.md +182 -531
  128. package/skills/yui-runtime/VERSION +1 -1
  129. package/skills/yui-runtime/references/archive.md +51 -0
  130. package/skills/yui-runtime/references/command-discovery.md +41 -0
  131. package/skills/yui-runtime/references/completed-followup.md +65 -0
  132. package/skills/yui-runtime/references/context-ordering.md +3 -34
  133. package/skills/yui-runtime/references/context-reads.md +61 -0
  134. package/skills/yui-runtime/references/dispatched-run.md +61 -0
  135. package/skills/yui-runtime/references/execution-failure.md +105 -0
  136. package/skills/yui-runtime/references/global-context.md +18 -0
  137. package/skills/yui-runtime/references/global-input-controls.md +44 -0
  138. package/skills/yui-runtime/references/host-custody.md +30 -0
  139. package/skills/yui-runtime/references/inbox-recovery.md +77 -0
  140. package/skills/yui-runtime/references/incremental-context.md +40 -0
  141. package/skills/yui-runtime/references/publication.md +4 -111
  142. package/skills/yui-runtime/references/recovery.md +32 -192
  143. package/skills/yui-runtime/references/session-identity.md +18 -0
  144. package/skills/yui-runtime/references/session-replacement.md +45 -0
  145. package/skills/yui-runtime/references/uncertain-execution.md +29 -0
@@ -0,0 +1,192 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, open, readFile, rename, rmdir } from "node:fs/promises";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { createNonBlockingChecks } from "./lifecycleChecks.js";
5
+ import { CodexNamingClient } from "../execution/codexThreadNaming.js";
6
+ import { nativeTitleDirectory, parseNativeTitleReceipt } from "./nativeTitleReceipt.js";
7
+ /** A standalone metadata process, NOT a second connection to the shared
8
+ * business daemon. It never starts/resumes a Thread or runs a Turn. Its hard
9
+ * process deadline releases even native locks after an unanswered request.
10
+ * Caller launches this in an independent lane; Goal/Turn never await it. */
11
+ export async function synchronizeNativeTitle(request) {
12
+ const result = (status, detail, extra = {}) => ({ threadId: request.identity.threadId, status, detail, ...extra });
13
+ let client;
14
+ let locked = false;
15
+ let submitted = false;
16
+ let receipt;
17
+ const cancel = () => client?.close("SIGKILL");
18
+ const directory = nativeTitleDirectory(request.home, request.identity);
19
+ try {
20
+ if (!isAbsolute(request.home) || !isAbsolute(request.accountHome)
21
+ || [request.title, request.generatedTitle].some(title => title !== undefined
22
+ && (!title || title.trim() !== title || title.length > 1024 || /[\r\n\0]/u.test(title)))) {
23
+ return result("not-submitted", "Native title request has an invalid local identity or title.");
24
+ }
25
+ if (request.signal?.aborted)
26
+ return result("not-submitted", "Title attachment closed.");
27
+ await mkdir(directory, { recursive: true, mode: 0o700 });
28
+ // Never remove another attempt's lock, including after a Host crash.
29
+ await mkdir(join(directory, "writing"), { mode: 0o700 });
30
+ locked = true;
31
+ try {
32
+ receipt = parseNativeTitleReceipt(await readFile(join(directory, "receipt.json"), "utf8"), request.identity);
33
+ }
34
+ catch (error) {
35
+ if (error.code !== "ENOENT")
36
+ throw error;
37
+ }
38
+ if (receipt !== undefined && (receipt.accountHome !== request.accountHome || receipt.status !== "confirmed")) {
39
+ return result("not-submitted", "Prior title identity/effect is unresolved; no rename replayed.", { attemptId: receipt.attemptId, receiptPath: join(directory, "receipt.json") });
40
+ }
41
+ if (request.localTitle !== undefined && request.localTitle !== request.previousTitle
42
+ && !receipt?.generatedTitles.includes(request.localTitle)) {
43
+ return result("preserved", "Yui Session has a custom or unowned local title; native name left unchanged.");
44
+ }
45
+ client = await CodexNamingClient.start(request.command, { ...request.environment, CODEX_HOME: request.accountHome }, request.baseArgs, request.timeoutMs ?? 2_000);
46
+ request.signal?.addEventListener("abort", cancel, { once: true });
47
+ if (request.signal?.aborted)
48
+ return result("not-submitted", "Title attachment closed.");
49
+ if (client.accountHome !== request.accountHome) {
50
+ return result("not-submitted", "Metadata process account identity did not match the business connection.");
51
+ }
52
+ const read = await client.request("thread/read", { threadId: request.identity.threadId, includeTurns: false });
53
+ const thread = read.thread;
54
+ if (thread?.id !== request.identity.threadId
55
+ || !(thread.name == null || typeof thread.name === "string")) {
56
+ return result("not-submitted", "Native title observation has no exact Thread/name identity.");
57
+ }
58
+ const current = typeof thread.name === "string" && thread.name.length > 0 ? thread.name : null;
59
+ const previous = receipt?.requestedTitle ?? request.previousTitle;
60
+ const title = request.generatedTitle !== undefined
61
+ && (request.title === previous || receipt?.generatedTitles.includes(request.title))
62
+ ? request.generatedTitle : request.title;
63
+ // A prefix (including the historic Skill notice) is not ownership proof.
64
+ if (current !== previous && !(request.newThread && current === null && receipt === undefined)) {
65
+ const pollution = [current, thread.preview].some(value => typeof value === "string"
66
+ && (value.startsWith("Yui Host checked current Skills;")
67
+ || value.startsWith("Yui Session Manifest:")
68
+ || value.startsWith("<!-- yui:skills-checked -->")));
69
+ return result("preserved", pollution
70
+ ? "Historical Skill-prefix title/preview candidate; no native ownership proof, so no repair submitted."
71
+ : "Native name has no matching generated-title provenance; left unchanged.");
72
+ }
73
+ if (current === title)
74
+ return result("confirmed", "Existing generated title observed.", { title, ...(receipt === undefined ? {} : { attemptId: receipt.attemptId }) });
75
+ receipt = {
76
+ schemaVersion: 1, identity: request.identity, accountHome: request.accountHome,
77
+ attemptId: randomUUID(), requestedTitle: title, expectedTitle: current,
78
+ status: "submitted", generatedTitles: receipt?.generatedTitles ?? (previous === undefined ? [] : [previous])
79
+ };
80
+ // Durable intent precedes the only mutation. A crash anywhere after this
81
+ // write is an unknown effect, never permission to resubmit.
82
+ await saveReceipt(directory, receipt);
83
+ if (request.signal?.aborted)
84
+ return result("not-submitted", "Title attachment closed before mutation.");
85
+ // Persistence may yield to a user's edit. Recheck immediately before
86
+ // sending; the native API has no expected-name compare-and-swap.
87
+ const prewrite = await client.request("thread/read", { threadId: request.identity.threadId, includeTurns: false });
88
+ const fresh = prewrite.thread;
89
+ if (fresh?.id !== request.identity.threadId || (fresh.name ?? null) !== current) {
90
+ await saveReceipt(directory, { ...receipt, status: "preserved" });
91
+ return result("preserved", "Native name changed before submission; no rename sent.", { attemptId: receipt.attemptId });
92
+ }
93
+ if (request.signal?.aborted)
94
+ return result("not-submitted", "Title attachment closed before mutation.");
95
+ submitted = true;
96
+ await client.request("thread/name/set", { threadId: request.identity.threadId, name: title });
97
+ const confirmation = await client.request("thread/read", { threadId: request.identity.threadId, includeTurns: false });
98
+ const observed = confirmation.thread;
99
+ if (observed?.id !== request.identity.threadId || observed.name !== title) {
100
+ throw new Error("Title mutation has no matching readback; preserve unknown effect.");
101
+ }
102
+ receipt = { ...receipt, status: "confirmed",
103
+ generatedTitles: [...new Set([...receipt.generatedTitles, title])] };
104
+ await saveReceipt(directory, receipt);
105
+ return result("confirmed", "Native title acknowledged and read back.", {
106
+ attemptId: receipt.attemptId, title, receiptPath: join(directory, "receipt.json")
107
+ });
108
+ }
109
+ catch (error) {
110
+ if (submitted && receipt !== undefined) {
111
+ await saveReceipt(directory, { ...receipt, status: "unknown" }).catch(() => { });
112
+ }
113
+ return result(submitted ? "unknown" : "not-submitted", error instanceof Error ? error.message : String(error), receipt === undefined ? {} : { attemptId: receipt.attemptId, receiptPath: join(directory, "receipt.json") });
114
+ }
115
+ finally {
116
+ request.signal?.removeEventListener("abort", cancel);
117
+ client?.close();
118
+ await client?.exited;
119
+ if (locked)
120
+ await rmdir(join(directory, "writing")).catch(() => { });
121
+ }
122
+ }
123
+ /** Use task-161's independent lifecycle-check lane without changing its FSM.
124
+ * One trigger per attachment; receipt fencing handles reconnects/other Hosts. */
125
+ export function createNativeTitleSync(payload, threadId, accountHome, report) {
126
+ let triggered = false;
127
+ const checks = createNonBlockingChecks([{
128
+ id: "native-title",
129
+ async run(_observation, signal) {
130
+ const env = payload.environment;
131
+ const title = payload.providerControl?.sessionTitle ?? env.YUI_SESSION_TITLE ?? env.YUI_NATIVE_TITLE_PROPOSAL;
132
+ if (title === undefined)
133
+ return;
134
+ const scope = env.YUI_SESSION_SCOPE;
135
+ if (!accountHome || !env.YUI_HOME || !env.YUI_ROLE || !env.YUI_AGENT_ID
136
+ || !["task", "global"].includes(scope ?? "") || scope === "task" && !env.YUI_TASK_ID
137
+ || !env.YUI_AGENT_BASE_ARGS) {
138
+ report({ threadId, status: "not-submitted", detail: "Native title synchronization lacks an exact local account/launch identity." });
139
+ return;
140
+ }
141
+ const baseArgs = JSON.parse(env.YUI_AGENT_BASE_ARGS);
142
+ if (!Array.isArray(baseArgs) || baseArgs.some(value => typeof value !== "string")) {
143
+ throw new Error("Invalid native naming executable arguments.");
144
+ }
145
+ const observed = await synchronizeNativeTitle({
146
+ home: env.YUI_HOME, identity: {
147
+ scope: scope,
148
+ ...(scope === "task" ? { taskId: env.YUI_TASK_ID } : {}),
149
+ roleName: env.YUI_ROLE, agentId: env.YUI_AGENT_ID, threadId
150
+ },
151
+ accountHome, command: payload.command, baseArgs,
152
+ environment: env, title,
153
+ ...(env.YUI_NATIVE_TITLE_PROPOSAL === undefined ? {} : { generatedTitle: env.YUI_NATIVE_TITLE_PROPOSAL }),
154
+ ...(env.YUI_NATIVE_TITLE_LOCAL === undefined ? {} : { localTitle: env.YUI_NATIVE_TITLE_LOCAL }),
155
+ newThread: payload.providerControl?.mode === "new"
156
+ || payload.providerControl === undefined && env.YUI_NATIVE_SESSION_ID === undefined,
157
+ ...(payload.providerControl?.previousSessionTitle === undefined ? {} : {
158
+ previousTitle: payload.providerControl.previousSessionTitle
159
+ }),
160
+ signal
161
+ });
162
+ if (!signal.aborted)
163
+ report(observed);
164
+ }
165
+ }], error => report({ threadId, status: "not-submitted", detail: String(error) }));
166
+ return {
167
+ notify() { if (!triggered) {
168
+ triggered = true;
169
+ checks.notify(true);
170
+ } },
171
+ close() { checks.close(); }
172
+ };
173
+ }
174
+ async function saveReceipt(directory, receipt) {
175
+ const temporary = join(directory, `${receipt.attemptId}-${receipt.status}.tmp`);
176
+ const file = await open(temporary, "wx", 0o600);
177
+ try {
178
+ await file.writeFile(JSON.stringify(receipt) + "\n");
179
+ await file.sync();
180
+ }
181
+ finally {
182
+ await file.close();
183
+ }
184
+ await rename(temporary, join(directory, "receipt.json"));
185
+ const parent = await open(directory, "r");
186
+ try {
187
+ await parent.sync();
188
+ }
189
+ finally {
190
+ await parent.close();
191
+ }
192
+ }
@@ -164,16 +164,16 @@ export function completeProviderRetry(binding, turn) {
164
164
  export function providerRetryAttemptId(retry) {
165
165
  return `provider-retry:${retry.chainId}/${retry.transportSequence + 1}`;
166
166
  }
167
- export function providerRetryPrompt(retry, original) {
167
+ /** Recovery guidance only. User input is delivered separately, once. */
168
+ export function providerRetryContext(retry) {
168
169
  if (retry.failedNativeTurnId === undefined)
169
- return original;
170
+ return undefined;
170
171
  return [
171
172
  "Yui infrastructure recovery, not new user authorization.",
172
173
  `Continue the existing input after failed attempt ${retry.failureAttemptId}`,
173
174
  `(native Turn ${retry.failedNativeTurnId}; failure ${retry.failureRef}).`,
174
175
  "Read current authorized context and the original input. Inspect existing work and receipts first;",
175
- "continue only unfinished work. Preserve local changes and do not repeat confirmed side effects.",
176
- original
176
+ "continue only unfinished work. Preserve local changes and do not repeat confirmed side effects."
177
177
  ].join("\n");
178
178
  }
179
179
  /** The same presentation is consumed by explicit detail reads and Web. */
@@ -211,7 +211,7 @@ export function providerRetryProjection(binding) {
211
211
  }
212
212
  /** Entry-point facts, never accumulated retry history or raw diagnostics.
213
213
  * Exact Session retry/show and failureRef retain the original evidence. */
214
- export function providerRetrySummary(binding) {
214
+ export function providerRetrySummary(binding, detailCommand) {
215
215
  const view = providerRetryProjection(binding);
216
216
  if (view === null)
217
217
  return null;
@@ -224,7 +224,10 @@ export function providerRetrySummary(binding) {
224
224
  currentTurn: currentTurn === null ? null : {
225
225
  attemptId: currentTurn.attemptId, status: currentTurn.status, nativeTurnId: currentTurn.nativeTurnId
226
226
  },
227
- historyCount: history.length, detailOmitted: true
227
+ historyCount: history.length, detailOmitted: true,
228
+ // This reads current Session evidence, not a frozen Run snapshot. The
229
+ // returned chain/attempt identities must still be compared by the caller.
230
+ ...(detailCommand === undefined ? {} : { currentDetailCommand: detailCommand })
228
231
  };
229
232
  }
230
233
  export function controlProviderRetry(binding, action, at) {
@@ -462,7 +462,7 @@ function orderedRunTimestamp(run, value, label) {
462
462
  }
463
463
  return normalized;
464
464
  }
465
- function providerTurnIsActive(run) {
465
+ export function providerTurnIsActive(run) {
466
466
  return run !== null && ["submitting", "accepted", "delivery-unknown"]
467
467
  .includes(run.status);
468
468
  }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { createNativeTitleSync } from "./nativeTitleSync.js";
3
4
  import { Duplex } from "node:stream";
4
5
  import { nativeExecutable } from "./nativeExecutable.js";
5
6
  import { startMissingCodexSharedDaemon } from "./codexSharedDaemon.js";
@@ -15,6 +16,7 @@ import { JsonLineChannel, PROVIDER_MESSAGE_MAX_BYTES, terminateProcessGroup } fr
15
16
  import { PROVIDER_ACCEPT_TIMEOUT_MS } from "./runtimeDeadlines.js";
16
17
  import { foldPublicReply } from "./publicReply.js";
17
18
  import { createCodexNativeAccess } from "./codexNativeAccess.js";
19
+ import { isNativeContextRecovery } from "./contextRecovery.js";
18
20
  const CODEX_PROXY_HANDSHAKE_TIMEOUT_MS = 10_000;
19
21
  /**
20
22
  * These Session implementations do not expose run-configuration observations.
@@ -31,7 +33,7 @@ export async function startStructuredProviderSession(payload, input = {}) {
31
33
  if (control.adapterId === "codex" && control.transport === "codex-app-server-proxy") {
32
34
  const proxy = await connectCodexProxy(payload, mirror, true);
33
35
  try {
34
- const opened = await CodexStructuredProviderSession.open(proxy.child, proxy.exit, proxy.processInstanceId, payload, control, proxy.channel, input.onStarted, input.onTerminal, input.onGoal, input.onInput, input.onActivity, input.onDiagnostic, mirror);
36
+ const opened = await CodexStructuredProviderSession.open(proxy.child, proxy.exit, proxy.processInstanceId, payload, control, proxy.channel, input.onStarted, input.onTerminal, input.onGoal, input.onInput, input.onActivity, mirror);
35
37
  return Object.freeze({
36
38
  session: opened.session,
37
39
  ...(opened.recoveredTerminal === undefined
@@ -238,7 +240,10 @@ class CodexProxyWebSocketChannel {
238
240
  this.#pending.delete(id);
239
241
  reject(new Error(`Provider request timed out: ${method}.`));
240
242
  }, PROVIDER_ACCEPT_TIMEOUT_MS);
241
- this.#pending.set(id, { resolve: resolvePromise, reject, timer });
243
+ this.#pending.set(id, {
244
+ resolve: resolvePromise, reject, timer, method,
245
+ requestBytes: Buffer.byteLength(JSON.stringify({ id, method, params }), "utf8")
246
+ });
242
247
  });
243
248
  try {
244
249
  await this.send({ id, method, params });
@@ -282,7 +287,7 @@ class CodexProxyWebSocketChannel {
282
287
  if (isBinary || encoded.byteLength > PROVIDER_MESSAGE_MAX_BYTES) {
283
288
  this.#close(new Error(isBinary
284
289
  ? "Provider returned an unsupported binary WebSocket message."
285
- : "Provider response message exceeds its bound."));
290
+ : `Provider response message exceeds its bound (receivedBytes=${encoded.byteLength}).`));
286
291
  terminateProcessGroup(this.child, "SIGTERM");
287
292
  return;
288
293
  }
@@ -320,6 +325,15 @@ class CodexProxyWebSocketChannel {
320
325
  #close(error) {
321
326
  if (this.#closedError !== undefined)
322
327
  return;
328
+ // A rejected WebSocket frame has not reached #receive: its full size and
329
+ // response/notification identity are unknown. Record pending request
330
+ // metadata, not parameters or transcript bytes, and never blame a specific
331
+ // response just because that request was in flight.
332
+ const pendingMetadata = [...this.#pending].slice(0, 8).map(([id, pending]) => ({
333
+ id, method: pending.method, requestBytes: pending.requestBytes
334
+ }));
335
+ error = new Error(`Codex proxy transport closed: ${error.message}; messageLimitBytes=${PROVIDER_MESSAGE_MAX_BYTES}; `
336
+ + `pendingRequests=${JSON.stringify(pendingMetadata)}; pendingCount=${this.#pending.size}.`, { cause: error });
323
337
  this.#closedError = error;
324
338
  if (!this.#ready)
325
339
  this.#rejectReady(error);
@@ -384,7 +398,22 @@ class CodexStructuredProviderSession {
384
398
  onActivity;
385
399
  mirror;
386
400
  nativeAccountHome;
401
+ contextEpoch = 0;
402
+ async checkQuiescent() {
403
+ const snapshot = await this.runtime.readConversation(this.conversationId, { includeTurns: false });
404
+ if (snapshot.threadId !== this.conversationId || snapshot.status !== "idle"
405
+ || snapshot.activeTurnId !== undefined)
406
+ return false;
407
+ const backgrounds = await this.nativeAccess.read("thread/backgroundTerminals/list", {
408
+ threadId: this.conversationId, limit: 1
409
+ });
410
+ if (!Array.isArray(backgrounds.data) || !("nextCursor" in backgrounds)) {
411
+ throw new Error("Native background execution response is invalid.");
412
+ }
413
+ return backgrounds.data.length === 0 && backgrounds.nextCursor === null;
414
+ }
387
415
  appliedGeneratedTitle;
416
+ #titleSync;
388
417
  nativeAccess;
389
418
  #turnAttempts = new Map();
390
419
  adapterId = "codex";
@@ -410,7 +439,7 @@ class CodexStructuredProviderSession {
410
439
  this.mirror = mirror;
411
440
  this.nativeAccountHome = nativeAccountHome;
412
441
  }
413
- static async open(child, exit, processInstanceId, payload, control, channel, onStarted, onTerminal, onGoal, onInput, onActivity, onDiagnostic, mirror) {
442
+ static async open(child, exit, processInstanceId, payload, control, channel, onStarted, onTerminal, onGoal, onInput, onActivity, mirror) {
414
443
  const openingMessages = [];
415
444
  const stopOpeningBuffer = channel.onMessage((message) => openingMessages.push(message));
416
445
  const initialized = await channel.request("initialize", codexClientInitialization());
@@ -419,7 +448,6 @@ class CodexStructuredProviderSession {
419
448
  let conversationId;
420
449
  let nativeConfiguration;
421
450
  let resumedActiveTurnId;
422
- let resumedTurns = [];
423
451
  if (control.mode === "new") {
424
452
  const opened = await runtime.openConversation({
425
453
  cwd: payload.cwd,
@@ -437,7 +465,6 @@ class CodexStructuredProviderSession {
437
465
  conversationId = resumed.threadId;
438
466
  nativeConfiguration = resumed.raw;
439
467
  resumedActiveTurnId = resumed.activeTurnId;
440
- resumedTurns = resumed.turns;
441
468
  }
442
469
  catch (error) {
443
470
  if (codexAppServerErrorIsMissing(error)) {
@@ -446,37 +473,15 @@ class CodexStructuredProviderSession {
446
473
  throw error;
447
474
  }
448
475
  }
449
- const threadMetadata = nativeConfiguration.thread;
450
- const nativeTitle = typeof threadMetadata === "object" && threadMetadata !== null
451
- && !Array.isArray(threadMetadata) ? threadMetadata.name : undefined;
452
- // A native rename belongs to the user. Missing metadata is not evidence
453
- // that an old title is still ours; leave it alone on resume.
454
- let appliedGeneratedTitle;
455
- if (control.sessionTitle !== undefined && (control.mode === "new"
456
- || (control.previousSessionTitle !== undefined && nativeTitle === control.previousSessionTitle))) {
457
- try {
458
- await runtime.setConversationName({
459
- conversationId,
460
- name: control.sessionTitle
461
- });
462
- appliedGeneratedTitle = control.sessionTitle;
463
- }
464
- catch (error) {
465
- onDiagnostic?.({
466
- nativeSessionId: conversationId,
467
- failure: providerDeliveryFailureFrom(error, {
468
- phase: "session-start",
469
- inputDisposition: "not-accepted",
470
- sessionDisposition: "recoverable"
471
- })
472
- });
473
- }
474
- }
476
+ const ownedTurn = control.kind === "restore" ? control.ownedTurn : undefined;
477
+ // Keep notifications buffered during paginated recovery. Otherwise a live
478
+ // terminal can be emitted before open returns and then returned again as a
479
+ // synthetic recovered terminal, or a newer start can be overwritten.
480
+ const recoveredOwnedTurn = ownedTurn !== undefined && resumedActiveTurnId !== ownedTurn.turnId
481
+ ? await runtime.readTurn(conversationId, ownedTurn.turnId) : undefined;
475
482
  const session = new CodexStructuredProviderSession(child, exit, processInstanceId, conversationId, runtime, onStarted, onTerminal, onGoal, onInput, onActivity, mirror, optionalId(initialized.codexHome));
476
483
  session.nativeAccess = createCodexNativeAccess(conversationId, initialized, channel, openingMessages, nativeConfiguration);
477
- session.appliedGeneratedTitle = appliedGeneratedTitle;
478
484
  session.#activeTurnId = resumedActiveTurnId;
479
- const ownedTurn = control.kind === "restore" ? control.ownedTurn : undefined;
480
485
  session.#clientOwnedTurnId = ownedTurn?.turnId;
481
486
  session.#clientOwnedAttemptId = ownedTurn?.attemptId;
482
487
  if (ownedTurn !== undefined)
@@ -493,7 +498,7 @@ class CodexStructuredProviderSession {
493
498
  });
494
499
  if (ownedTurn !== undefined && recoveredTerminal === undefined
495
500
  && session.#activeTurnId !== ownedTurn.turnId) {
496
- const recovered = resumedTurns.find((turn) => turn.turnId === ownedTurn.turnId);
501
+ const recovered = recoveredOwnedTurn;
497
502
  if (recovered?.status === "completed"
498
503
  || recovered?.status === "interrupted"
499
504
  || recovered?.status === "failed") {
@@ -523,6 +528,15 @@ class CodexStructuredProviderSession {
523
528
  }
524
529
  }
525
530
  const goal = await runtime.readGoal(conversationId);
531
+ session.#titleSync = createNativeTitleSync(payload, conversationId, session.nativeAccountHome, result => {
532
+ if (result.status === "confirmed")
533
+ session.appliedGeneratedTitle = result.title;
534
+ mirror("stderr", `Native title: ${JSON.stringify(result)}\n`);
535
+ });
536
+ void exit.then(() => session.#titleSync?.close());
537
+ // Fresh Threads may not be persisted until their first accepted input.
538
+ if (control.mode === "resume")
539
+ session.#titleSync.notify();
526
540
  return Object.freeze({
527
541
  session,
528
542
  ...(recoveredTerminal === undefined ? {} : { recoveredTerminal }),
@@ -532,6 +546,8 @@ class CodexStructuredProviderSession {
532
546
  #observeMessage(message, emit) {
533
547
  const method = typeof message.method === "string" ? message.method : "";
534
548
  const params = object(message.params) ?? {};
549
+ if (isNativeContextRecovery("codex", this.conversationId, message))
550
+ this.contextEpoch += 1;
535
551
  const threadId = optionalId(params.threadId);
536
552
  if (emit)
537
553
  this.publicReply = foldPublicReply(this.publicReply, this.conversationId, method, params);
@@ -541,6 +557,13 @@ class CodexStructuredProviderSession {
541
557
  }
542
558
  return undefined;
543
559
  }
560
+ if (method === "thread/name/updated") {
561
+ this.#titleSync?.close();
562
+ if (params.threadName !== this.appliedGeneratedTitle)
563
+ this.appliedGeneratedTitle = undefined;
564
+ }
565
+ if (method === "turn/started")
566
+ this.#titleSync?.notify();
544
567
  let goal;
545
568
  try {
546
569
  goal = codexGoalNotification({ method, params });
@@ -665,6 +688,7 @@ class CodexStructuredProviderSession {
665
688
  conversationId: this.conversationId,
666
689
  attemptId: turn.attemptId,
667
690
  text: turn.boundedText,
691
+ ...(turn.additionalContext === undefined ? {} : { additionalContext: turn.additionalContext }),
668
692
  expectedNoActiveTurn: true,
669
693
  ...(turn.requireQuiescent ? { requireQuiescent: true } : {}),
670
694
  ...(turn.expectedFailedNativeTurnId === undefined ? {} : {
@@ -684,6 +708,7 @@ class CodexStructuredProviderSession {
684
708
  this.#clientOwnedTurnId = acceptance.turnId;
685
709
  this.#clientOwnedAttemptId = turn.attemptId;
686
710
  this.#turnAttempts.set(acceptance.turnId, turn.attemptId);
711
+ this.#titleSync?.notify();
687
712
  return Object.freeze({
688
713
  attemptId: turn.attemptId,
689
714
  conversationId: this.conversationId,
@@ -714,6 +739,7 @@ class CodexStructuredProviderSession {
714
739
  conversationId: this.conversationId,
715
740
  expectedTurnId: nativeTurnId,
716
741
  text: turn.boundedText,
742
+ ...(turn.additionalContext === undefined ? {} : { additionalContext: turn.additionalContext }),
717
743
  clientUserMessageId: turn.attemptId
718
744
  });
719
745
  if (acceptance.status === "unknown") {
@@ -778,6 +804,7 @@ class CodexStructuredProviderSession {
778
804
  return result === "interrupted" ? "requested" : result;
779
805
  }
780
806
  terminate(signal) {
807
+ this.#titleSync?.close();
781
808
  terminateProcessGroup(this.child, signal);
782
809
  }
783
810
  }
@@ -789,6 +816,7 @@ class ClaudeStructuredProviderSession {
789
816
  channel;
790
817
  onGoal;
791
818
  mirror;
819
+ contextEpoch = 0;
792
820
  adapterId = "claude";
793
821
  get ownedProcessId() { return this.child.pid; }
794
822
  #activeAttemptId;
@@ -905,6 +933,8 @@ class ClaudeStructuredProviderSession {
905
933
  terminateProcessGroup(this.child, signal);
906
934
  }
907
935
  #receive(message, onTerminal, onAccepted, onActivity) {
936
+ if (isNativeContextRecovery("claude", this.conversationId, message))
937
+ this.contextEpoch += 1;
908
938
  const goal = claudeActiveGoal(message, this.conversationId);
909
939
  if (goal !== undefined) {
910
940
  const key = JSON.stringify(goal === null ? null : {
@@ -1,9 +1,15 @@
1
- import { mergePendingWakeup } from "./pendingWakeup.js";
1
+ import { enqueueWork } from "../coordination/workMailboxQueue.js";
2
2
  /** The caller's TaskStore transaction owns this read/merge/write operation. */
3
3
  export function queueLeaderWakeup(store, taskId, reason, now) {
4
4
  if (store.getTask(taskId)?.status === "archived")
5
5
  return null;
6
- const pending = mergePendingWakeup(taskId, reason, now, store.getPendingWakeup(taskId));
7
- store.savePendingWakeup(pending);
8
- return pending;
6
+ // The scheduler's pending-wakeup projection may expose an already-owned
7
+ // processing prefix. Append to the actual suffix, never write that read
8
+ // projection back over independent inputs.
9
+ const pending = enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, reason, now).pending;
10
+ return {
11
+ schemaVersion: 1, taskId, reasons: [...pending.reasons],
12
+ requestCount: pending.requestCount,
13
+ firstRequestedAt: pending.firstQueuedAt, lastRequestedAt: pending.lastQueuedAt
14
+ };
9
15
  }
@@ -286,6 +286,35 @@ const MINOR_UPGRADES = Object.freeze([{
286
286
  // Optional Role presentation and generated-title provenance. Never infer
287
287
  // ownership of historical titles, rewrite frozen profiles, or fill defaults.
288
288
  sql: ""
289
+ }, {
290
+ fromVersion: "1.25",
291
+ toVersion: "1.26",
292
+ name: "codex-workspace-temp-exclusions",
293
+ introducedIn: "next",
294
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
295
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
296
+ // Optional workspace-write permission fields; preserve all existing payloads.
297
+ sql: ""
298
+ }, {
299
+ fromVersion: "1.26",
300
+ toVersion: "1.27",
301
+ name: "host-upgrade-lifecycle-signals",
302
+ introducedIn: "next",
303
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
304
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
305
+ // New versioned signal sources in the existing role-runtime mailbox. No
306
+ // historical requests are inferred and existing records remain unchanged.
307
+ sql: ""
308
+ }, {
309
+ fromVersion: "1.27",
310
+ toVersion: "1.28",
311
+ name: "native-title-effect-receipts",
312
+ introducedIn: "next",
313
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
314
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
315
+ // Additive exact-identity intent/confirmation files under runtime/native-titles.
316
+ // Never invent native ownership or replay history during migration.
317
+ sql: ""
289
318
  }]);
290
319
  export function storageMinorUpgradePlan(from) {
291
320
  if (from === CURRENT_STORAGE_VERSION)
@@ -1,4 +1,4 @@
1
- export const CURRENT_STORAGE_VERSION = "1.25";
1
+ export const CURRENT_STORAGE_VERSION = "1.28";
2
2
  export const MIN_SUPPORTED_STORAGE_VERSION = "1.0";
3
3
  export const STORAGE_FORMAT = "yui-home";
4
4
  export function isStorageVersion(value) {
@@ -62,7 +62,9 @@ export class TmuxManager {
62
62
  this.#yuiHome = options.yuiHome ?? process.env.YUI_HOME ?? process.cwd();
63
63
  this.#serverName = yuiTmuxServerName(this.#yuiHome);
64
64
  this.#onRoleTargetRecorded = options.onRoleTargetRecorded;
65
- this.#terminalInput = options.terminalInput ?? process.stdin;
65
+ // Creating Node's stdin stream can make piped fd 0 nonblocking. Commands
66
+ // may still need to read that pipe synchronously before any attach.
67
+ this.#terminalInput = options.terminalInput;
66
68
  this.#terminalType = usableInteractiveTerminal(options.terminalType ?? process.env.TERM);
67
69
  this.#closeInteractiveInput = options.closeInteractiveInput ?? (() => { });
68
70
  this.#readinessTimeoutMs = positiveInteger(options.readinessTimeoutMs, DEFAULT_READINESS_TIMEOUT_MS, "readiness timeout");
@@ -228,7 +230,7 @@ export class TmuxManager {
228
230
  }
229
231
  if (resolvedAccess === "read-write")
230
232
  options.revalidateWritableAttach?.();
231
- handoffTerminal(this.#terminalInput, this.#closeInteractiveInput);
233
+ handoffTerminal(this.#terminalInput ?? process.stdin, this.#closeInteractiveInput);
232
234
  this.run([
233
235
  "attach-session",
234
236
  ...(resolvedAccess === "read-only" ? ["-r"] : []),
@@ -276,6 +276,8 @@ const ROLE_FIELDS = {
276
276
  model: { set: "--model", clear: "--clear-model" }, effort: { set: "--effort", clear: "--clear-effort" },
277
277
  "permission.strategy": { set: "--permission-strategy" }, "permission.sandbox": { set: "--sandbox" },
278
278
  "permission.approval": { set: "--approval" }, "permission.mode": { set: "--permission-mode" },
279
+ "permission.excludeTmpdirEnvVar": { set: "--exclude-tmpdir-env-var", clear: "--clear-exclude-tmpdir-env-var" },
280
+ "permission.excludeSlashTmp": { set: "--exclude-slash-tmp", clear: "--clear-exclude-slash-tmp" },
279
281
  "permission.allowedTools": { set: "--allowed-tool", clear: "--clear-allowed-tools", many: true },
280
282
  "permission.disallowedTools": { set: "--disallowed-tool", clear: "--clear-disallowed-tools", many: true },
281
283
  search: { set: "--search", clear: "--clear-search" },
@@ -20,9 +20,24 @@ authorization for any external effect.
20
20
  | May it access an additional resource or irreversible effect? | `CapabilityGrant`, `grant/taskAuthorization.ts`, resource/plugin/release consumers | Task activation does not authorize arbitrary resources |
21
21
 
22
22
  The full command inventory is generated from the catalog, not maintained as a
23
- second per-command permissions table: use `yui --json help` and scoped
24
- `yui --json help task`, or traverse `describeCommandTree(discoveryCommandTree(env))`
25
- in offline tooling. Complete role hints retain an offline template even without
23
+ second per-command permissions table. `yui --json help` and scoped
24
+ `yui --json help task` return the selected command's full details and all its
25
+ immediate child **summaries**, not a recursively expanded tree. The `discovery`
26
+ field labels this boundary and gives the detail and full-tree commands. Follow
27
+ each child's `path` with `yui --json help <command ...>` for full usage, options,
28
+ values and examples, or use `yui --json help [command ...] --recursive` for the
29
+ complete caller-filtered tree in one call.
30
+
31
+ This changes the default JSON help shape: consumers that previously traversed
32
+ nested `children` must request `--recursive`; absence of child details in the
33
+ default view is not proof that the child has no options or descendants.
34
+ Focused leaf details are unchanged, with additive discovery metadata.
35
+ Text help still shows immediate children; `--recursive` requires `--json`.
36
+ `config describe --json` and the offline
37
+ `describeCommandTree(discoveryCommandTree(env))` API retain full recursive
38
+ descriptions. Native `yui agent` product help is a separate surface and unchanged.
39
+
40
+ Complete role hints retain an offline template even without
26
41
  a running Home; that is not authentication. Native-ID-only callers are resolved
27
42
  from read-only local storage for help/completion; if that read fails the CLI shows
28
43
  the restricted `unbound` template. No Controller or provider is started for that