@zq-silk/yui 2.21.0 → 2.22.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 (125) hide show
  1. package/README.md +5 -0
  2. package/dist/cli/commandCatalog.js +85 -25
  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/commands/globalRoleCommands.js +33 -8
  9. package/dist/commands/inboxCommands.js +39 -12
  10. package/dist/commands/operatorCommands.js +2 -5
  11. package/dist/commands/releaseCommands.js +16 -2
  12. package/dist/commands/roleConfiguration.js +25 -2
  13. package/dist/commands/sessionCommands.js +5 -2
  14. package/dist/commands/taskCommands.js +217 -64
  15. package/dist/commands/taskContextCommand.js +26 -4
  16. package/dist/commands/taskFactCommands.js +15 -2
  17. package/dist/commands/taskIntegrationCommands.js +3 -3
  18. package/dist/commands/taskRoleRuntimeStatus.js +15 -5
  19. package/dist/commands/textInput.js +2 -2
  20. package/dist/context/contextFamilies.js +10 -0
  21. package/dist/context/currentSkills.js +6 -4
  22. package/dist/context/runContextPack.js +44 -5
  23. package/dist/context/runInputContract.js +1 -1
  24. package/dist/context/sessionBootstrapManifest.js +7 -5
  25. package/dist/context/sessionSkillNotice.js +53 -0
  26. package/dist/context/taskContext.js +100 -16
  27. package/dist/controlPlaneCli.js +17 -7
  28. package/dist/controller/agentRuntimeObserver.js +57 -12
  29. package/dist/controller/clientRuntime.js +37 -8
  30. package/dist/controller/controller.js +3 -1
  31. package/dist/controller/diagnosticJournal.js +10 -2
  32. package/dist/controller/fileSchedulerStoreAdapter.js +72 -24
  33. package/dist/controller/messageHealth.js +4 -3
  34. package/dist/controller/providerRetryAdmission.js +15 -1
  35. package/dist/controller/providerRetryDelivery.js +18 -2
  36. package/dist/controller/runtime.js +15 -5
  37. package/dist/controller/runtimeEventInbox.js +107 -13
  38. package/dist/controller/runtimeEventProcessor.js +3 -3
  39. package/dist/controller/taskRunRecovery.js +153 -0
  40. package/dist/core/controllerClient.js +16 -2
  41. package/dist/core/controllerServer.js +14 -5
  42. package/dist/core/controllerTelemetry.js +51 -5
  43. package/dist/errors/cliFailure.js +14 -5
  44. package/dist/executor/agentAdapter.js +32 -4
  45. package/dist/executor/agentConfigurationFields.js +2 -0
  46. package/dist/integration/gitIntegrationService.js +3 -2
  47. package/dist/integration/integrationSourceApplication.js +12 -1
  48. package/dist/interaction/notificationPresentation.js +11 -16
  49. package/dist/interaction/operatorPresentation.js +1 -1
  50. package/dist/message/messageContinuation.js +3 -1
  51. package/dist/output/boundedRead.js +36 -7
  52. package/dist/output/rolePresentation.js +3 -1
  53. package/dist/runtime/agentHost.js +12 -3
  54. package/dist/runtime/codexAppServerRuntime.js +110 -24
  55. package/dist/runtime/codexInteractiveHost.js +46 -0
  56. package/dist/runtime/providerRetry.js +5 -2
  57. package/dist/runtime/providerRuntimeIdentity.js +1 -1
  58. package/dist/runtime/structuredProviderHost.js +32 -34
  59. package/dist/scheduler/wakeupQueue.js +10 -4
  60. package/dist/storage/sqliteSchema.js +9 -0
  61. package/dist/storage/storageVersions.js +1 -1
  62. package/dist/tmux/tmuxManager.js +4 -2
  63. package/dist/web/webSettings.js +2 -0
  64. package/docs/authorization.md +18 -3
  65. package/docs/cli-information-contract.md +228 -6
  66. package/docs/cli-information-contract.zh-CN.md +95 -5
  67. package/docs/managed-turn-and-session-runtime.md +82 -21
  68. package/docs/managed-turn-and-session-runtime.zh-CN.md +56 -13
  69. package/docs/provider-retry.md +11 -0
  70. package/docs/release-workflow.md +37 -0
  71. package/docs/release-workflow.zh-CN.md +28 -0
  72. package/docs/roles-and-configuration.md +28 -0
  73. package/docs/roles-and-configuration.zh-CN.md +21 -0
  74. package/docs/storage-baseline.md +4 -1
  75. package/docs/storage-baseline.zh-CN.md +1 -1
  76. package/docs/task-delivery.md +7 -0
  77. package/docs/task-delivery.zh-CN.md +5 -0
  78. package/docs/testing/review-evaluation.md +7 -7
  79. package/docs/testing/review-evaluation.zh-CN.md +7 -7
  80. package/docs/testing/verification-levels.md +45 -2
  81. package/docs/testing/verification-levels.zh-CN.md +21 -2
  82. package/i18n/README.zh-CN.md +6 -0
  83. package/package.json +1 -1
  84. package/skills/yui-leader/SKILL.md +98 -193
  85. package/skills/yui-leader/VERSION +1 -1
  86. package/skills/yui-leader/references/artifacts.md +41 -0
  87. package/skills/yui-leader/references/assignment-execution.md +140 -0
  88. package/skills/yui-leader/references/completion.md +79 -0
  89. package/skills/yui-leader/references/delivery-scope.md +26 -0
  90. package/skills/yui-leader/references/execution.md +16 -487
  91. package/skills/yui-leader/references/implementation.md +81 -0
  92. package/skills/yui-leader/references/planning.md +7 -45
  93. package/skills/yui-leader/references/review.md +90 -0
  94. package/skills/yui-leader/references/task-progress.md +23 -0
  95. package/skills/yui-leader/references/work-allocation.md +118 -0
  96. package/skills/yui-operator/SKILL.md +77 -333
  97. package/skills/yui-operator/VERSION +1 -1
  98. package/skills/yui-operator/references/body-transport.md +62 -0
  99. package/skills/yui-operator/references/configuration.md +47 -0
  100. package/skills/yui-operator/references/lifecycle.md +41 -0
  101. package/skills/yui-operator/references/progress.md +39 -0
  102. package/skills/yui-operator/references/routing.md +57 -0
  103. package/skills/yui-operator/references/submission.md +56 -0
  104. package/skills/yui-operator/references/task-delivery.md +1 -1
  105. package/skills/yui-operator/references/web-lifecycle.md +19 -0
  106. package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
  107. package/skills/yui-runtime/SKILL.md +177 -531
  108. package/skills/yui-runtime/VERSION +1 -1
  109. package/skills/yui-runtime/references/archive.md +51 -0
  110. package/skills/yui-runtime/references/command-discovery.md +41 -0
  111. package/skills/yui-runtime/references/completed-followup.md +65 -0
  112. package/skills/yui-runtime/references/context-ordering.md +3 -34
  113. package/skills/yui-runtime/references/context-reads.md +61 -0
  114. package/skills/yui-runtime/references/dispatched-run.md +61 -0
  115. package/skills/yui-runtime/references/execution-failure.md +105 -0
  116. package/skills/yui-runtime/references/global-context.md +18 -0
  117. package/skills/yui-runtime/references/global-input-controls.md +44 -0
  118. package/skills/yui-runtime/references/host-custody.md +30 -0
  119. package/skills/yui-runtime/references/inbox-recovery.md +77 -0
  120. package/skills/yui-runtime/references/incremental-context.md +40 -0
  121. package/skills/yui-runtime/references/publication.md +4 -111
  122. package/skills/yui-runtime/references/recovery.md +32 -192
  123. package/skills/yui-runtime/references/session-identity.md +18 -0
  124. package/skills/yui-runtime/references/session-replacement.md +45 -0
  125. package/skills/yui-runtime/references/uncertain-execution.md +29 -0
@@ -87,7 +87,7 @@ class CodexAdapter extends BaseAdapter {
87
87
  throw new Error("Codex permission strategy is required.");
88
88
  }
89
89
  if (config.permission.strategy === "configured") {
90
- exact(config.permission, ["strategy", "sandbox", "approval"], "Codex permission config");
90
+ exact(config.permission, ["strategy", "sandbox", "approval", "excludeTmpdirEnvVar", "excludeSlashTmp"], "Codex permission config");
91
91
  if (config.permission.sandbox !== undefined
92
92
  && !SANDBOXES.includes(config.permission.sandbox)) {
93
93
  throw new Error("Codex sandbox is invalid.");
@@ -96,6 +96,16 @@ class CodexAdapter extends BaseAdapter {
96
96
  && !APPROVALS.includes(config.permission.approval)) {
97
97
  throw new Error("Codex approval is invalid.");
98
98
  }
99
+ for (const key of ["excludeTmpdirEnvVar", "excludeSlashTmp"]) {
100
+ const value = config.permission[key];
101
+ if (value === undefined)
102
+ continue;
103
+ if (typeof value !== "boolean")
104
+ throw new Error(`Codex ${key} must be boolean.`);
105
+ if (config.permission.sandbox !== "workspace-write") {
106
+ throw new Error(`Codex ${key} requires an explicit workspace-write sandbox.`);
107
+ }
108
+ }
99
109
  requireConfiguredPermissionOption(config.permission, "Codex");
100
110
  }
101
111
  else {
@@ -118,7 +128,8 @@ class CodexAdapter extends BaseAdapter {
118
128
  ...(config.permission.sandbox === undefined
119
129
  ? [] : ["--sandbox", config.permission.sandbox]),
120
130
  ...(config.permission.approval === undefined
121
- ? [] : ["--ask-for-approval", config.permission.approval])
131
+ ? [] : ["--ask-for-approval", config.permission.approval]),
132
+ ...Object.entries(codexWorkspaceWriteOptions(config)).flatMap(([key, value]) => ["--config", `sandbox_workspace_write.${key}=${value}`])
122
133
  ]
123
134
  : []),
124
135
  ...(config.search === true ? ["--search"] : []),
@@ -158,7 +169,12 @@ class CodexAdapter extends BaseAdapter {
158
169
  // A managed Codex thread is an ordinary user thread. Its Yui guidance is
159
170
  // part of the durable Task message, not a developer_instructions override
160
171
  // that would shadow the user's native project/developer configuration.
161
- const argv = withoutCodexConfigOverride(launch.argv, "developer_instructions");
172
+ let argv = withoutCodexConfigOverride(launch.argv, "developer_instructions");
173
+ // Managed exclusions belong to thread/start and thread/resume configuration,
174
+ // never the shared daemon/proxy startup defaults.
175
+ for (const key of Object.keys(codexWorkspaceWriteOptions(config))) {
176
+ argv = withoutCodexConfigOverride(argv, `sandbox_workspace_write.${key}`);
177
+ }
162
178
  if (config.model !== undefined) {
163
179
  const modelFlag = argv.findIndex((value, index) => (value === "--model" && argv[index + 1] === config.model));
164
180
  if (modelFlag < 0)
@@ -475,7 +491,10 @@ function codexThreadOptions(input, config) {
475
491
  // Keep the trust override on this thread instead of mutating global config.
476
492
  projects: { [resolve(input.workspace)]: { trust_level: "trusted" } },
477
493
  ...(config.effort === undefined ? {} : { model_reasoning_effort: config.effort }),
478
- ...(config.search === true ? { web_search: "live" } : {})
494
+ ...(config.search === true ? { web_search: "live" } : {}),
495
+ ...(Object.keys(codexWorkspaceWriteOptions(config)).length === 0 ? {} : {
496
+ sandbox_workspace_write: codexWorkspaceWriteOptions(config)
497
+ })
479
498
  };
480
499
  const permission = config.permission.strategy === "bypass"
481
500
  ? { approvalPolicy: "never", sandbox: "danger-full-access" }
@@ -496,6 +515,15 @@ function codexThreadOptions(input, config) {
496
515
  ...(Object.keys(threadConfig).length === 0 ? {} : { config: threadConfig })
497
516
  };
498
517
  }
518
+ function codexWorkspaceWriteOptions(config) {
519
+ if (config.permission.strategy !== "configured")
520
+ return {};
521
+ const { excludeTmpdirEnvVar, excludeSlashTmp } = config.permission;
522
+ return {
523
+ ...(excludeTmpdirEnvVar === undefined ? {} : { exclude_tmpdir_env_var: excludeTmpdirEnvVar }),
524
+ ...(excludeSlashTmp === undefined ? {} : { exclude_slash_tmp: excludeSlashTmp })
525
+ };
526
+ }
499
527
  function codexSessionInstructions(input) {
500
528
  if (input.sessionManifestPath !== undefined) {
501
529
  return sessionContextInstructions(input.sessionManifestPath, input.sessionManifestDigest);
@@ -15,6 +15,8 @@ export function staticAgentConfigurationFields(id) {
15
15
  ...fields,
16
16
  staticField("permission.sandbox", CODEX_SANDBOXES, false),
17
17
  staticField("permission.approval", CODEX_APPROVALS, false, "Native versions may retire values accepted by the adapter."),
18
+ staticField("permission.excludeTmpdirEnvVar", ["true", "false"], false, "Requires explicit workspace-write; omission follows the native default."),
19
+ staticField("permission.excludeSlashTmp", ["true", "false"], false, "Requires explicit workspace-write; omission follows the native default."),
18
20
  staticField("search", ["true"], false),
19
21
  staticField("profile", [], true),
20
22
  staticField("additionalDirectories", [], true)
@@ -79,7 +79,7 @@ export class GitIntegrationService {
79
79
  #targetCheckout(attempt) {
80
80
  return taskIntegrationTargetCheckout(this.store, this.environment, attempt.taskId, attempt.projectId, attempt.targetRef);
81
81
  }
82
- async integrate(taskId, integrationId, signal) {
82
+ async integrate(taskId, integrationId, signal, intent) {
83
83
  let initial = requireIntegration(this.store, taskId, integrationId);
84
84
  this.#targetCheckout(initial);
85
85
  if (!["running", "conflicted", "blocked", "validating"].includes(initial.status)) {
@@ -185,7 +185,8 @@ export class GitIntegrationService {
185
185
  try {
186
186
  current = await applyIntegrationSource({
187
187
  attempt: current, workspace, remoteUrl: project.remoteUrl,
188
- git: this.git, store: this.store, now: this.now
188
+ git: this.git, store: this.store, now: this.now,
189
+ continueOwnedOperation: intent === "continue"
189
190
  });
190
191
  if (current.status === "conflicted" || current.status === "blocked") {
191
192
  return { status: current.status, attempt: current, workspace };
@@ -135,7 +135,18 @@ export async function applyIntegrationSource(input) {
135
135
  return pause(unresolved, kind);
136
136
  }
137
137
  if (!fresh && current.status !== "conflicted") {
138
- throw new Error("Git operation is present but no paused conflict is recorded; inspect its execution before continuing.");
138
+ if (input.continueOwnedOperation !== true) {
139
+ throw new Error("Git operation is present but no paused conflict is recorded; inspect its execution before continuing.");
140
+ }
141
+ // Git can have stopped before the transaction recording its conflict
142
+ // failed. Existing progress and assertOperation above bind this exact
143
+ // operation; explicit intent may continue only a staged resolution.
144
+ // Do not manufacture a conflict record or restart the source command.
145
+ if (!await succeeds(path, ["diff", "--quiet"])
146
+ || await succeeds(path, ["diff", "--cached", "--quiet"])
147
+ || (await command(path, ["ls-files", "--others", "--exclude-standard"])).trim() !== "") {
148
+ throw new Error("Owned Git recovery requires a fully staged resolution with no unstaged or untracked files.");
149
+ }
139
150
  }
140
151
  try {
141
152
  if (operation !== undefined) {
@@ -63,32 +63,27 @@ export function taskNotificationPresentation(wake, store) {
63
63
  `Changes: ${labels.slice(0, 4).join("; ")}${labels.length > 4 ? "; more in wake" : ""}.`,
64
64
  ...messages.slice(0, 4).map(message => {
65
65
  const item = taskMessagePresentation(message, (task, run) => store.getRun(task, run));
66
- return `- ${message.id} [${message.author.type}${message.author.type === "role" ? `:${message.author.roleName}` : ""}]: ${item.fact}.`
67
- + (item.summary === undefined ? "" : ` Author summary (${item.summarySource}): ${JSON.stringify(item.summary)}`);
66
+ // Keep failure/disposition facts here. Authored summaries remain in the
67
+ // mandatory wake/original reads, not a second copy in every delivery.
68
+ return `- ${message.id} [${message.author.type}${message.author.type === "role" ? `:${message.author.roleName}` : ""}]: ${item.fact}.`;
68
69
  }),
69
70
  ...(messages.length > 4 ? [`${messages.length - 4} more messages in the fixed wake.`] : []),
70
71
  `Refresh: yui task context ${wake.taskId} --json`,
71
- `Read inputs/results: yui task wake show ${wake.taskId} ${wake.id} --json`,
72
- `Handling: claim exact inbox entry ${wake.id}; after disposition, ack it with a concrete reason before ending this turn.`,
73
- "Use yui inbox list --view current --json for current inputs. Historical missing receipts are not new work; never replay effects to repair an ACK.",
72
+ // Match Runtime's receiving-entry -> claim -> originals order. The claim
73
+ // returns the exact focused read; do not suggest reading it once beforehand.
74
+ `Read receiving entry: yui inbox show ${wake.id} --json`,
75
+ `Inbox: ${wake.id}; follow Runtime claim → original → disposition → ACK.`,
74
76
  ...(wake.reasons.includes("session-replaced") ? [
75
- "Session replacement is initialization, not replay or completion of earlier input.",
76
- "Read yui inbox list --view history --json; inspect retained originals, prior effects and current authority before explicit settlement.",
77
- "Preserve original claims and delivery targets. Unknown external effects require evidence, not another execution."
78
- ] : []),
79
- "Check current Skills each turn: yui session skills --json. Apply current yui-leader; read relevant originals in full and advance authorized work."
77
+ "Session replaced: yui inbox list --view history --json; follow Runtime inbox recovery before settling retained claims. Do not replay unknown effects."
78
+ ] : [])
80
79
  ].join("\n");
81
80
  }
82
81
  export function globalMessagePresentation(roleName, messageId, message) {
83
- const summary = authoredNotificationSummary(message?.body);
84
82
  return [
85
83
  `Yui Global Message: role=${roleName} message=${messageId}.`,
86
84
  `Changes: ${message?.author.type ?? "Recorded"} input received.`,
87
- ...(summary === undefined ? [] : [`Author summary (message-body): ${JSON.stringify(summary)}`]),
88
85
  "Refresh Session Context using the current Manifest's loadCommand.",
89
- `Read original: yui role message show ${roleName} ${messageId} --json`,
90
- `Handling: claim exact inbox entry ${messageId}; after disposition, ack it with a concrete reason before ending this turn.`,
91
- "Routing to a Task or asking a necessary user question can settle this input without completing the Task. Missing ACK is not permission to repeat effects.",
92
- `The Global loadCommand checks current Skill versions each turn. Apply current ${roleName === "operator" ? "yui-operator" : "yui-runtime"} within existing authority.`
86
+ `Original: reuse the complete matching currentOriginals record from Context; if absent, yui inbox show ${messageId} --json.`,
87
+ `Inbox: ${messageId}; follow Runtime claim → original → disposition → ACK.`
93
88
  ].join("\n");
94
89
  }
@@ -10,7 +10,7 @@ export function createOperatorBatchPresentation(batchId, items) {
10
10
  receiptId: `operator-batch:${batchId}`,
11
11
  text: [
12
12
  "[Yui updates]",
13
- "Check current Skills each turn via the Global Manifest's loadCommand. Apply current yui-operator; inspect these originals and present material changes or decisions.",
13
+ "Apply current yui-operator and refresh Global Context; inspect these originals and present material changes or decisions.",
14
14
  ...items.flatMap(renderItem)
15
15
  ].join("\n")
16
16
  };
@@ -12,6 +12,7 @@ import { sourceRunContextValue } from "../context/sourceRunContext.js";
12
12
  import { requireManagedTaskCaller } from "../runtime/managedCaller.js";
13
13
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
14
14
  import { isTaskRecordRetired, operationalTaskRecords } from "../task/taskRecordRetirement.js";
15
+ import { recoveredMessageContinuation } from "../controller/taskRunRecovery.js";
15
16
  export function resolveMessageRecipient(store, taskId, roleName, scope) {
16
17
  if (scope.reviewRoundId !== undefined) {
17
18
  const round = store.getReviewRound(taskId, scope.reviewRoundId);
@@ -80,7 +81,8 @@ export function messageAssignmentBlocker(store, message) {
80
81
  const latest = store.listRuns(message.taskId).filter((run) => run.workItemId === owner.workItemId && run.reviewRoundId === owner.reviewRoundId
81
82
  && run.roleName === owner.roleName).at(-1);
82
83
  if (latest !== undefined && latest.id !== owner.id
83
- && latest.inputs[0]?.input.source.channel !== "message-continuation")
84
+ && latest.inputs[0]?.input.source.channel !== "message-continuation"
85
+ && !recoveredMessageContinuation(store, message, latest))
84
86
  return "assignment-changed";
85
87
  if (target.reviewRoundId !== undefined) {
86
88
  const validation = validateExactRunReviewRound(store, latest ?? owner, { allowTerminal: true });
@@ -35,10 +35,17 @@ export function readDigest(value) {
35
35
  }
36
36
  /** A save receipt is not another read of the submitted body. Keep the exact
37
37
  * delivery/control facts and identity; body/history are read on demand.
38
+ * bodySha256 hashes the saved string's UTF-8 bytes, not its JSON encoding or
39
+ * record metadata. It permits transport comparison, not semantic read credit.
38
40
  */
39
41
  export function messageReceipt(message) {
40
42
  const { body: _body, ...facts } = message;
41
- return { ...facts, digest: readDigest(message), bodyBytes: Buffer.byteLength(message.body) };
43
+ return {
44
+ ...facts,
45
+ digest: readDigest(message),
46
+ bodyBytes: Buffer.byteLength(message.body, "utf8"),
47
+ bodySha256: createHash("sha256").update(message.body, "utf8").digest("hex")
48
+ };
42
49
  }
43
50
  export function encodeReadCursor(cursor) {
44
51
  return Buffer.from(JSON.stringify(cursor)).toString("base64url");
@@ -117,15 +124,37 @@ export function boundedDocument(value, source, cursor) {
117
124
  const offset = cursor === undefined ? 0 : decodeReadCursor(cursor, source, digest);
118
125
  if (offset >= text.length && offset !== 0)
119
126
  throw usageError("Read cursor exceeds this document.");
120
- let end = Math.min(offset + 4096, text.length);
121
- // Never cut between UTF-16 surrogate halves.
122
- if (end < text.length && /[\uD800-\uDBFF]/u.test(text[end - 1]))
123
- end--;
124
- return { contentPage: {
127
+ const boundary = (end) => end < text.length && /[\uD800-\uDBFF]/u.test(text[end - 1])
128
+ ? end - 1 : end;
129
+ const page = (end) => ({ contentPage: {
125
130
  source, digest, encoding: "json", offset, totalCharacters: text.length,
126
131
  totalBytes, text: text.slice(offset, end), complete: end === text.length,
127
132
  nextCursor: end < text.length ? encodeReadCursor({ source, digest, offset: end }) : null
128
- } };
133
+ } });
134
+ // A code unit costs at least one encoded byte. Bound each trial independently
135
+ // of document size, and budget the entire response including escaped identity.
136
+ const upper = boundary(Math.min(offset + READ_PAGE_BYTES, text.length));
137
+ const largest = page(upper);
138
+ if (fitsReadBudget(largest))
139
+ return largest;
140
+ // The terminal page drops its cursor: test it above, separately from the
141
+ // monotone non-terminal search. Raw indices advance even at surrogate pairs.
142
+ let low = offset + 1;
143
+ let high = upper - 1;
144
+ let end = offset;
145
+ while (low <= high) {
146
+ const middle = Math.floor((low + high) / 2);
147
+ const candidate = boundary(middle);
148
+ if (fitsReadBudget(page(candidate))) {
149
+ end = candidate;
150
+ low = middle + 1;
151
+ }
152
+ else
153
+ high = middle - 1;
154
+ }
155
+ if (end === offset)
156
+ throw usageError("Read identity and text exceed the response budget.");
157
+ return page(end);
129
158
  }
130
159
  /** Shared parser for list/detail command tails; no options reach mutations. */
131
160
  export function readOptions(args, flags = ["--cursor", "--limit"]) {
@@ -122,7 +122,9 @@ function permission(binding) {
122
122
  return "bypass";
123
123
  return [
124
124
  permission.sandbox === undefined ? undefined : `sandbox=${permission.sandbox}`,
125
- permission.approval === undefined ? undefined : `approval=${permission.approval}`
125
+ permission.approval === undefined ? undefined : `approval=${permission.approval}`,
126
+ permission.excludeTmpdirEnvVar === undefined ? undefined : `excludeTmpdirEnvVar=${permission.excludeTmpdirEnvVar}`,
127
+ permission.excludeSlashTmp === undefined ? undefined : `excludeSlashTmp=${permission.excludeSlashTmp}`
126
128
  ].filter((value) => value !== undefined).join("; ");
127
129
  }
128
130
  // ACP's strategy decides the Session mode Yui requests over the protocol. It
@@ -4,7 +4,7 @@ import { createConnection, createServer } from "node:net";
4
4
  import { homedir, tmpdir } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { createInterface } from "node:readline";
7
- import { withSessionContextPointer } from "../context/sessionBootstrapManifest.js";
7
+ import { createSessionSkillNotice } from "../context/sessionSkillNotice.js";
8
8
  import { publishStructuredConversationRecoverability, publishStructuredProviderAccepted, publishStructuredProviderActivity, publishStructuredProviderAttachmentExit, publishStructuredProviderConnection, publishStructuredProviderDiagnostic, publishStructuredProviderGoal, publishStructuredProviderInputObserved, publishStructuredProviderInputSettlement, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, replayStructuredProviderPersistence, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
9
9
  import { callController, controllerCallMayHaveApplied, ControllerClientError } from "../core/controllerClient.js";
10
10
  import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
@@ -82,6 +82,7 @@ export async function runAgentHost(input) {
82
82
  return runCodexInteractiveHost(input.home, payload, openAgentHostControl);
83
83
  }
84
84
  let session;
85
+ const skillNotice = createSessionSkillNotice(input.home);
85
86
  // This process really holds the Endpoint code its Session runs on. The lease
86
87
  // spans the Session's Turns, so installing a newer generation does not change
87
88
  // the implementation under a Session that is still using this one.
@@ -405,6 +406,7 @@ export async function runAgentHost(input) {
405
406
  throw new Error("Codex client reconnect lost its Endpoint implementation lease.");
406
407
  }
407
408
  const started = await endpointLease.resume(reconnectPayload);
409
+ skillNotice.invalidate();
408
410
  session = started.session;
409
411
  sessionPayload = currentPayload;
410
412
  started.session.events((event) => {
@@ -660,6 +662,7 @@ export async function runAgentHost(input) {
660
662
  session = started.session;
661
663
  sessionPayload = next;
662
664
  await recordProviderConnection(input.home, next, started.session);
665
+ skillNotice.invalidate();
663
666
  lastTerminalPayload = undefined;
664
667
  lastTerminal = undefined;
665
668
  directObservation = undefined;
@@ -909,9 +912,10 @@ export async function runAgentHost(input) {
909
912
  try {
910
913
  // beginDurableProviderTurn validated current execution-environment,
911
914
  // authority and Session identity before this native write.
915
+ const notice = skillNotice.prepare(recoveryInput?.text ?? request.run.boundedText, activeRunPayload.environment, session.nativeSessionId, { recovery: recoveryInput !== undefined });
912
916
  const receipt = endpointReceipt(await session.submit({
913
917
  ...request.run,
914
- boundedText: withSessionContextPointer(recoveryInput?.text ?? request.run.boundedText, activeRunPayload.environment),
918
+ boundedText: notice.text,
915
919
  ...(recoveryInput === undefined ? {} : { requireQuiescent: true }),
916
920
  ...(recoveryInput?.expectedFailedNativeTurnId === undefined ? {} : {
917
921
  expectedFailedNativeTurnId: recoveryInput.expectedFailedNativeTurnId
@@ -920,6 +924,8 @@ export async function runAgentHost(input) {
920
924
  }), request.run.attemptId);
921
925
  transportAccepted = true;
922
926
  providerAccepted = receipt.acceptance === "provider";
927
+ if (providerAccepted)
928
+ notice.commit();
923
929
  activeNativeTurnId = receipt.nativeTurnId;
924
930
  try {
925
931
  await publishStructuredProviderAccepted({
@@ -1029,14 +1035,17 @@ export async function runAgentHost(input) {
1029
1035
  }
1030
1036
  try {
1031
1037
  await assertHostExecutionEnvironment(input.home, sessionPayload, session.nativeSessionId);
1038
+ const notice = skillNotice.prepare(request.run.boundedText, sessionPayload.environment, session.nativeSessionId);
1032
1039
  const receipt = endpointReceipt(await session.steer({
1033
1040
  ...request.run,
1034
- boundedText: withSessionContextPointer(request.run.boundedText, sessionPayload.environment),
1041
+ boundedText: notice.text,
1035
1042
  inputRef: request.run.attemptId
1036
1043
  }), request.run.attemptId);
1037
1044
  if (receipt.nativeTurnId !== request.nativeTurnId) {
1038
1045
  throw new ProviderDeliveryUnknownError("Provider accepted steer against an unexpected native Turn.", request.run.attemptId);
1039
1046
  }
1047
+ if (receipt.acceptance === "provider")
1048
+ notice.commit();
1040
1049
  }
1041
1050
  catch (error) {
1042
1051
  const unknown = error instanceof ProviderDeliveryUnknownError;
@@ -49,9 +49,10 @@ export class CodexAppServerRuntime {
49
49
  const result = await this.transport.request("thread/resume", {
50
50
  threadId: id,
51
51
  ...(options.cwd === undefined ? {} : { cwd: text(options.cwd, "Codex thread cwd") }),
52
- ...threadOptions(options)
52
+ ...threadOptions(options),
53
+ excludeTurns: true
53
54
  });
54
- return parseThreadSnapshot(result, id, true);
55
+ return this.withLatestTurn(parseThreadSnapshot(result, id, true));
55
56
  }
56
57
  async setConversationName(input) {
57
58
  await this.transport.request("thread/name/set", {
@@ -63,9 +64,81 @@ export class CodexAppServerRuntime {
63
64
  const id = text(conversationId, "Codex thread id");
64
65
  const result = await this.transport.request("thread/read", {
65
66
  threadId: id,
66
- includeTurns: options.includeTurns ?? true
67
+ includeTurns: false
67
68
  });
68
- return parseThreadSnapshot(result, id, "unknown");
69
+ const snapshot = parseThreadSnapshot(result, id, "unknown");
70
+ return options.includeTurns === false ? snapshot : this.withLatestTurn(snapshot);
71
+ }
72
+ /** Current execution metadata, never an unbounded transcript hydration. */
73
+ async withLatestTurn(snapshot) {
74
+ const page = await this.transport.request("thread/turns/list", {
75
+ threadId: snapshot.threadId, limit: 1, sortDirection: "desc", itemsView: "notLoaded"
76
+ });
77
+ const records = codexPage(page, new Set()).data;
78
+ if (records.length > 1)
79
+ throw new Error("Codex latest Turn page exceeded its requested bound.");
80
+ const turns = records.map(parseTurnSnapshot);
81
+ const latest = turns[0];
82
+ return {
83
+ ...snapshot,
84
+ // The metadata response may contain no Turns. Do not carry an old active
85
+ // identity over a separately observed latest Turn.
86
+ activeTurnId: latest?.status === "inProgress" ? latest.turnId : undefined,
87
+ latestTurnStatus: latest?.status,
88
+ turns
89
+ };
90
+ }
91
+ /** Recover only the persisted owned Turn. Scan metadata, then its items,
92
+ * newest first, stopping at the last nonempty assistant message. A malformed
93
+ * page or oversized individual item remains an error, never guessed success. */
94
+ async readTurn(conversationId, turnId) {
95
+ const threadId = text(conversationId, "Codex thread id");
96
+ const expectedTurnId = text(turnId, "Codex owned Turn id");
97
+ const seen = new Set();
98
+ let cursor;
99
+ for (;;) {
100
+ const page = codexPage(await this.transport.request("thread/turns/list", {
101
+ threadId, limit: 50, sortDirection: "desc", itemsView: "notLoaded",
102
+ ...(cursor === undefined ? {} : { cursor })
103
+ }), seen);
104
+ if (page.data.length > 50)
105
+ throw new Error("Codex Turn page exceeded its requested bound.");
106
+ const found = page.data.map(parseTurnSnapshot).find(turn => turn.turnId === expectedTurnId);
107
+ if (found !== undefined) {
108
+ if (found.status === "inProgress")
109
+ return found;
110
+ const output = await this.readTurnOutput(threadId, expectedTurnId);
111
+ return { ...found, ...(output === undefined ? {} : { output }) };
112
+ }
113
+ if (page.nextCursor === null)
114
+ return undefined;
115
+ cursor = page.nextCursor;
116
+ }
117
+ }
118
+ async readTurnOutput(threadId, turnId) {
119
+ const seen = new Set();
120
+ let cursor;
121
+ for (;;) {
122
+ const page = codexPage(await this.transport.request("thread/items/list", {
123
+ threadId, turnId, limit: 1, sortDirection: "desc",
124
+ ...(cursor === undefined ? {} : { cursor })
125
+ }), seen);
126
+ if (page.data.length > 1)
127
+ throw new Error("Codex item page exceeded its requested bound.");
128
+ for (const value of page.data) {
129
+ const entry = object(value);
130
+ const item = object(entry?.item);
131
+ if (entry?.turnId !== turnId || item === null) {
132
+ throw new Error("Codex item page does not belong to the owned Turn.");
133
+ }
134
+ if (item.type === "agentMessage" && typeof item.text === "string" && item.text.trim().length > 0) {
135
+ return item.text;
136
+ }
137
+ }
138
+ if (page.nextCursor === null)
139
+ return undefined;
140
+ cursor = page.nextCursor;
141
+ }
69
142
  }
70
143
  async readGoal(conversationId) {
71
144
  const id = text(conversationId, "Codex thread id");
@@ -330,26 +403,7 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
330
403
  throw new Error("Codex thread status is missing or unsupported.");
331
404
  if (!Array.isArray(thread.turns))
332
405
  throw new Error("Codex thread turns are missing or invalid.");
333
- const turnSnapshots = thread.turns.map((value) => {
334
- const turn = object(value);
335
- if (turn === null)
336
- throw new Error("Codex thread contains an invalid Turn.");
337
- const turnId = text(optionalId(turn.id), "Codex response Turn id");
338
- const status = optionalTurnStatus(turn.status);
339
- if (status === undefined)
340
- throw new Error("Codex Turn status is missing or unsupported.");
341
- const output = codexTurnOutput(turn);
342
- const error = providerError(turn.error);
343
- return {
344
- turnId,
345
- status,
346
- ...(output === undefined ? {} : { output }),
347
- ...(error === undefined ? {} : { error }),
348
- ...(turn.error === undefined || turn.error === null ? {} : {
349
- rawError: serializeAgentErrorRaw(turn.error)
350
- })
351
- };
352
- });
406
+ const turnSnapshots = thread.turns.map(parseTurnSnapshot);
353
407
  const active = [...turnSnapshots].reverse().find((turn) => turn.status === "inProgress");
354
408
  const latestStatus = turnSnapshots.at(-1)?.status;
355
409
  return {
@@ -365,6 +419,38 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
365
419
  raw: result
366
420
  };
367
421
  }
422
+ function parseTurnSnapshot(value) {
423
+ const turn = object(value);
424
+ if (turn === null)
425
+ throw new Error("Codex thread contains an invalid Turn.");
426
+ const turnId = text(optionalId(turn.id), "Codex response Turn id");
427
+ const status = optionalTurnStatus(turn.status);
428
+ if (status === undefined)
429
+ throw new Error("Codex Turn status is missing or unsupported.");
430
+ const output = codexTurnOutput(turn);
431
+ const error = providerError(turn.error);
432
+ return {
433
+ turnId,
434
+ status,
435
+ ...(output === undefined ? {} : { output }),
436
+ ...(error === undefined ? {} : { error }),
437
+ ...(turn.error === undefined || turn.error === null ? {} : {
438
+ rawError: serializeAgentErrorRaw(turn.error)
439
+ })
440
+ };
441
+ }
442
+ function codexPage(page, seen) {
443
+ if (!Array.isArray(page.data)
444
+ || !(page.nextCursor === null || typeof page.nextCursor === "string" && page.nextCursor.length > 0)) {
445
+ throw new Error("Codex history page is missing data or its continuation boundary.");
446
+ }
447
+ if (typeof page.nextCursor === "string") {
448
+ if (seen.has(page.nextCursor))
449
+ throw new Error("Codex history cursor repeated.");
450
+ seen.add(page.nextCursor);
451
+ }
452
+ return { data: page.data, nextCursor: page.nextCursor };
453
+ }
368
454
  /** Extract the last assistant message from one native Codex Turn without rewriting it. */
369
455
  export function codexTurnOutput(turn) {
370
456
  for (const value of [...arrayMember(turn, "items")].reverse()) {
@@ -4,6 +4,7 @@ import { once } from "node:events";
4
4
  import WebSocket, { WebSocketServer } from "ws";
5
5
  import { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
6
6
  import { openCodexInteractiveConnection } from "./structuredProviderHost.js";
7
+ import { createSessionSkillNotice } from "../context/sessionSkillNotice.js";
7
8
  /**
8
9
  * Keep the native TUI and its transparent App Server attachment in one pane.
9
10
  * The existing Host acknowledgement carries the ID from that TUI's exact
@@ -15,6 +16,8 @@ import { openCodexInteractiveConnection } from "./structuredProviderHost.js";
15
16
  */
16
17
  export async function runCodexInteractiveHost(home, payload, openAgentHostControl) {
17
18
  const environment = payload.environment;
19
+ const skillNotice = createSessionSkillNotice(home);
20
+ const pendingNotices = new Map();
18
21
  if (environment.YUI_SESSION_SCOPE !== "global" || environment.YUI_ADAPTER_ID !== "codex"
19
22
  || payload.providerControl !== undefined) {
20
23
  throw new Error("Interactive Codex Host requires a global native TUI launch.");
@@ -106,6 +109,23 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
106
109
  startupRequestId = message.id;
107
110
  message.params = codexInteractiveStartupParameters(payload, params);
108
111
  }
112
+ if (message.method === "turn/start" || message.method === "turn/steer") {
113
+ if (snapshot.nativeSessionId === undefined)
114
+ throw new Error("Skill check requires the reserved native Thread.");
115
+ if (typeof message.id !== "string" && typeof message.id !== "number"
116
+ || pendingNotices.has(message.id))
117
+ throw new Error("Turn input requires a unique correlation id.");
118
+ const params = jsonObject(message.params);
119
+ if (params.threadId !== snapshot.nativeSessionId)
120
+ throw new Error("Turn input targets another Thread.");
121
+ const prepared = codexInteractiveSkillInput(skillNotice, environment, snapshot.nativeSessionId, params);
122
+ message.params = prepared.params;
123
+ pendingNotices.set(message.id, {
124
+ commit: prepared.commit,
125
+ ...(message.method === "turn/steer" && typeof params.expectedTurnId === "string"
126
+ ? { expectedTurnId: params.expectedTurnId } : {})
127
+ });
128
+ }
109
129
  void connection.send(message).catch(fail);
110
130
  }
111
131
  catch (error) {
@@ -115,6 +135,17 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
115
135
  });
116
136
  connection.onMessage((message) => {
117
137
  try {
138
+ if (message.method === undefined && (typeof message.id === "string" || typeof message.id === "number")) {
139
+ const pending = pendingNotices.get(message.id);
140
+ if (pending !== undefined) {
141
+ pendingNotices.delete(message.id);
142
+ const result = message.result;
143
+ const turnId = result?.turn?.id ?? result?.turnId;
144
+ if (message.error === undefined && typeof turnId === "string" && turnId.length > 0
145
+ && (pending.expectedTurnId === undefined || pending.expectedTurnId === turnId))
146
+ pending.commit();
147
+ }
148
+ }
118
149
  let nativeSessionId;
119
150
  if (startupRequestId !== undefined && message.id === startupRequestId
120
151
  && message.method === undefined) {
@@ -183,6 +214,21 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
183
214
  await control.close();
184
215
  }
185
216
  }
217
+ /** Preserve all native content parts (including images), adding only Host metadata. */
218
+ export function codexInteractiveSkillInput(notice, environment, nativeSessionId, params) {
219
+ if (!Array.isArray(params.input))
220
+ throw new Error("Turn input must be an array.");
221
+ const prepared = notice.prepare("", environment, nativeSessionId);
222
+ return {
223
+ params: {
224
+ ...params,
225
+ input: prepared.text.length === 0 ? params.input : [
226
+ { type: "text", text: prepared.text.trimEnd(), text_elements: [] }, ...params.input
227
+ ]
228
+ },
229
+ commit: prepared.commit
230
+ };
231
+ }
186
232
  /** The remote TUI can omit local launch configuration. Apply the reserved
187
233
  * workspace and managed identity on its actual startup, never by starting a
188
234
  * second Thread or changing daemon-wide configuration. */
@@ -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
  }