@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
@@ -79,8 +79,17 @@ export function describeCliFailure(error, context = {}) {
79
79
  ? "The Controller returned an error response; it does not prove rollback of earlier effects. " + RECONCILE
80
80
  : RECONCILE;
81
81
  const help = error instanceof CliError && error.helpText !== undefined ? clean(error.helpText.trimEnd()) : undefined;
82
- const message = [
83
- ...chain.map((item, index) => `${index === 0 ? "" : "Caused by: "}${item.code === undefined ? "" : `[${item.code}] `}${item.message}`),
82
+ const causes = chain.map((item, index) => `${index === 0 ? "" : "Caused by: "}${item.code === undefined ? "" : `[${item.code}] `}${item.message}`);
83
+ const lines = (parts) => parts.filter((part) => part !== undefined && part.length > 0).join("\n");
84
+ const jsonMessage = lines([
85
+ causes[0],
86
+ effects,
87
+ guidance,
88
+ "Use the same Home and CLI in details.diagnostic; its causes retain the full error chain. Recovery commands are not run automatically.",
89
+ help
90
+ ]);
91
+ const message = lines([
92
+ ...causes,
84
93
  diagnostic.operation === undefined ? undefined : `Operation: ${diagnostic.operation}`,
85
94
  diagnostic.home === undefined ? undefined : `YUI_HOME: ${diagnostic.home}`,
86
95
  diagnostic.cli === undefined ? undefined : `CLI entry: ${diagnostic.cli}`,
@@ -92,9 +101,9 @@ export function describeCliFailure(error, context = {}) {
92
101
  guidance,
93
102
  "Recovery commands shown as `yui` must use the same Home and matching CLI entry above; they are not run automatically.",
94
103
  help
95
- ].filter((part) => part !== undefined && part.length > 0).join("\n");
104
+ ]);
96
105
  return {
97
- code, exitCode, message,
106
+ code, exitCode, message, jsonMessage,
98
107
  details: {
99
108
  ...(error instanceof CliError ? sanitizeFailureDetails(error.details, context) : {}),
100
109
  diagnostic
@@ -103,7 +112,7 @@ export function describeCliFailure(error, context = {}) {
103
112
  }
104
113
  export function renderCliFailure(failure, json, jsonCode = failure.code) {
105
114
  return json
106
- ? JSON.stringify({ ok: false, code: jsonCode, message: failure.message, details: failure.details })
115
+ ? JSON.stringify({ ok: false, code: jsonCode, message: failure.jsonMessage, details: failure.details })
107
116
  : `${failure.code}: ${failure.message}`;
108
117
  }
109
118
  function errorCode(error) {
@@ -2,52 +2,64 @@ import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
3
  import { codexClientInitialization } from "../runtime/codexAppServerRuntime.js";
4
4
  const DEFAULT_TIMEOUT_MS = 1_000;
5
- export async function setCodexThreadName(request) {
6
- const timeoutMs = threadNameTimeout(request.timeoutMs);
7
- const client = await CodexNamingClient.start(request.command, request.environment, request.baseArgs ?? [], timeoutMs);
8
- try {
9
- await withTimeout(client.request("thread/name/set", {
10
- threadId: requiredText(request.threadId, "Codex thread id"),
11
- name: requiredThreadName(request.name)
12
- }), timeoutMs, "Timed out setting Codex thread name.");
13
- }
14
- finally {
15
- client.close();
16
- }
17
- }
18
- class CodexNamingClient {
5
+ export class CodexNamingClient {
19
6
  child;
7
+ accountHome;
8
+ exited;
20
9
  #pending = new Map();
21
10
  #stderr = [];
22
11
  #nextId = 1;
23
12
  #closed = false;
24
- constructor(child) {
13
+ #deadline;
14
+ constructor(child, timeoutMs) {
25
15
  this.child = child;
16
+ this.exited = new Promise(resolve => child.once("close", () => resolve()));
17
+ this.#deadline = setTimeout(() => {
18
+ this.#failAll(new Error("Native title metadata process deadline exceeded; effect may be unknown."));
19
+ this.close("SIGKILL");
20
+ }, timeoutMs);
26
21
  const output = createInterface({ input: child.stdout });
27
- output.on("line", (line) => { this.#receive(line); });
22
+ output.on("line", (line) => {
23
+ try {
24
+ if (Buffer.byteLength(line) > 1_048_576)
25
+ throw new Error("Native title reply exceeds its bound.");
26
+ this.#receive(line);
27
+ }
28
+ catch (error) {
29
+ this.#failAll(error instanceof Error ? error : new Error(String(error)));
30
+ this.close();
31
+ }
32
+ });
33
+ child.stdout.on("error", error => { this.#failAll(error); this.close(); });
28
34
  child.stderr.setEncoding("utf8");
29
35
  child.stderr.on("data", (chunk) => {
30
36
  this.#stderr.push(chunk);
31
37
  if (this.#stderr.join("").length > 16_384)
32
38
  this.#stderr.shift();
33
39
  });
34
- child.on("error", (error) => { this.#failAll(error); });
40
+ child.on("error", (error) => { this.#failAll(error); this.close(); });
41
+ child.stdin.on("error", (error) => { this.#failAll(error); this.close(); });
35
42
  child.on("exit", (code, signal) => {
43
+ clearTimeout(this.#deadline);
36
44
  if (this.#closed)
37
45
  return;
46
+ this.#closed = true;
38
47
  const detail = this.#stderr.join("").trim();
39
48
  this.#failAll(new Error(`Codex App Server exited (${code ?? signal ?? "unknown"})${detail.length === 0 ? "" : `: ${detail}`}`));
40
49
  });
41
50
  }
42
51
  static async start(command, environment, baseArgs, timeoutMs) {
43
- const client = new CodexNamingClient(spawn(command, [...baseArgs, "app-server", "--stdio"], { env: environment, stdio: ["pipe", "pipe", "pipe"] }));
52
+ const deadlineMs = threadNameTimeout(timeoutMs);
53
+ const client = new CodexNamingClient(spawn(command, [...baseArgs, "app-server", "--listen", "stdio://"], { env: environment, stdio: ["pipe", "pipe", "pipe"] }), deadlineMs);
44
54
  try {
45
- await withTimeout(client.request("initialize", codexClientInitialization()), timeoutMs, "Timed out initializing Codex App Server for thread naming.");
55
+ const initialized = await withTimeout(client.request("initialize", codexClientInitialization()), timeoutMs, "Timed out initializing Codex App Server for thread naming.");
56
+ client.accountHome = typeof initialized.codexHome === "string" ? initialized.codexHome : undefined;
46
57
  client.notify("initialized", {});
47
58
  return client;
48
59
  }
49
60
  catch (error) {
50
61
  client.close();
62
+ await client.exited;
51
63
  throw error;
52
64
  }
53
65
  }
@@ -63,11 +75,19 @@ class CodexNamingClient {
63
75
  notify(method, params) {
64
76
  this.#write({ method, params });
65
77
  }
66
- close() {
78
+ close(signal = "SIGTERM") {
67
79
  if (this.#closed)
68
80
  return;
69
81
  this.#closed = true;
70
- this.child.kill("SIGTERM");
82
+ clearTimeout(this.#deadline);
83
+ this.child.kill(signal);
84
+ // An owned metadata process must not survive a lost reply or ignored TERM.
85
+ const reap = setTimeout(() => {
86
+ if (this.child.exitCode === null && this.child.signalCode === null)
87
+ this.child.kill("SIGKILL");
88
+ }, 250);
89
+ reap.unref();
90
+ this.child.once("exit", () => clearTimeout(reap));
71
91
  this.#failAll(new Error("Codex App Server closed."));
72
92
  }
73
93
  #write(value) {
@@ -94,13 +114,20 @@ class CodexNamingClient {
94
114
  }
95
115
  return;
96
116
  }
97
- this.#pending.delete(message.id);
98
- if (message.error !== undefined) {
99
- const error = object(message.error, "Codex App Server error");
100
- pending.reject(new Error(typeof error.message === "string" ? error.message : "Codex App Server request failed."));
117
+ try {
118
+ if (message.error !== undefined) {
119
+ const error = object(message.error, "Codex App Server error");
120
+ pending.reject(new Error(typeof error.message === "string" ? error.message : "Codex App Server request failed."));
121
+ }
122
+ else {
123
+ pending.resolve(object(message.result, "Codex App Server result"));
124
+ }
125
+ }
126
+ catch (error) {
127
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
101
128
  }
102
- else {
103
- pending.resolve(object(message.result, "Codex App Server result"));
129
+ finally {
130
+ this.#pending.delete(message.id);
104
131
  }
105
132
  }
106
133
  #failAll(error) {
@@ -115,22 +142,6 @@ function object(value, label) {
115
142
  }
116
143
  return value;
117
144
  }
118
- function requiredText(value, label) {
119
- if (typeof value !== "string" || value.trim().length === 0) {
120
- throw new Error(`${label} is invalid.`);
121
- }
122
- return value;
123
- }
124
- function requiredThreadName(value) {
125
- if (typeof value !== "string" || value.includes("\0")) {
126
- throw new Error("Codex thread name is invalid.");
127
- }
128
- const normalized = value.trim();
129
- if (normalized.length === 0 || normalized.length > 1_024) {
130
- throw new Error("Codex thread name is invalid.");
131
- }
132
- return normalized;
133
- }
134
145
  function threadNameTimeout(value) {
135
146
  if (value === undefined)
136
147
  return DEFAULT_TIMEOUT_MS;
@@ -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)
@@ -17,7 +17,7 @@ import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
17
17
  import { builtinAgentEndpointImplementation, validateAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
18
18
  import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
19
19
  import { assertProviderConversationReplaceable } from "../runtime/providerRuntimeIdentity.js";
20
- import { globalRoleSessionTitle, resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
20
+ import { globalRoleSessionTitle, taskRoleSessionTitle, resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
21
21
  import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
22
22
  import { planningRuntimeCwd } from "../storage/homeLayout.js";
23
23
  import { resolveAgentAdapter } from "./agentAdapter.js";
@@ -225,7 +225,11 @@ export class FileRoleLaunchPlanner {
225
225
  instanceCode: role.instanceCode,
226
226
  generatedTitle: existing?.generatedTitle
227
227
  }), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, {
228
- purpose
228
+ purpose,
229
+ nativeGeneratedTitle: taskRoleSessionTitle(task, role.name, {
230
+ executionAuthority: (input.mode === "resume" ? existing?.effective : effective)?.executionAuthority,
231
+ instanceCode: role.instanceCode
232
+ })
229
233
  });
230
234
  }
231
235
  planGlobalRole(input) {
@@ -258,7 +262,10 @@ export class FileRoleLaunchPlanner {
258
262
  : existing?.generatedTitle !== undefined && existing.title === existing.generatedTitle
259
263
  ? globalRoleSessionTitle(role.name, new Date(existing.createdAt), this.store.getConfig().timeZone, role.instanceCode)
260
264
  ?? `Yui ${role.name}`
261
- : undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, { purpose: "execution" });
265
+ : undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, { purpose: "execution",
266
+ nativeGeneratedTitle: globalRoleSessionTitle(role.name, input.mode === "resume" && existing !== undefined ? new Date(existing.createdAt) : this.#now(), this.store.getConfig().timeZone, role.instanceCode)
267
+ ?? (input.mode === "resume" ? `Yui ${role.name}` : undefined)
268
+ });
262
269
  }
263
270
  #compile(role, input, owner, sessionTitle, knownNativeSessionId, effective, sessionPolicy) {
264
271
  const launchRole = effectiveRoleForLaunch(role, effective);
@@ -570,6 +577,14 @@ export class FileRoleLaunchPlanner {
570
577
  YUI_DRIVER_ID: driver.id,
571
578
  YUI_WORKSPACE: effectiveWorkspace,
572
579
  YUI_SESSION_MANIFEST: sessionContext.sessionManifestPath,
580
+ // A proposal only. The independent metadata lane must prove native
581
+ // ownership; planning never reads/waits on title effects.
582
+ ...(configured.adapterId !== "codex" || sessionPolicy.nativeGeneratedTitle === undefined ? {} : {
583
+ YUI_NATIVE_TITLE_PROPOSAL: sessionPolicy.nativeGeneratedTitle
584
+ }),
585
+ ...(configured.adapterId !== "codex" || input.mode !== "resume" || existingSession?.title === undefined ? {} : {
586
+ YUI_NATIVE_TITLE_LOCAL: existingSession.title
587
+ }),
573
588
  ...(configured.adapterId === "codex"
574
589
  ? { YUI_AGENT_BASE_ARGS: JSON.stringify(configured.baseArgs) }
575
590
  : {}),
@@ -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
@@ -1,3 +1,4 @@
1
+ import { isDeepStrictEqual } from "node:util";
1
2
  import { builtinAgentEndpointImplementation, requireBuiltinAgentEndpointImplementation } from "./agentEndpointIdentity.js";
2
3
  import { builtinAgentDriverRegistry } from "./builtinAgentDrivers.js";
3
4
  import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
@@ -80,6 +81,12 @@ function freezeConfiguration(value) {
80
81
  class BuiltinAgentEndpoint {
81
82
  driver;
82
83
  configuration;
84
+ get contextEpoch() { return this.driver.contextEpoch; }
85
+ async checkQuiescent() {
86
+ if (!this.driver.checkQuiescent)
87
+ throw new Error("This native Endpoint cannot prove background quiescence.");
88
+ return this.driver.checkQuiescent();
89
+ }
83
90
  get nativeAccess() { return this.driver.nativeAccess; }
84
91
  get appliedGeneratedTitle() { return this.driver.appliedGeneratedTitle; }
85
92
  get ownedProcessId() { return this.driver.ownedProcessId; }
@@ -147,6 +154,7 @@ class BuiltinAgentEndpoint {
147
154
  const previous = this.#attempts.get(input.attemptId);
148
155
  if (previous !== undefined) {
149
156
  if (previous.input.inputRef !== input.inputRef || previous.input.boundedText !== input.boundedText
157
+ || !isDeepStrictEqual(previous.input.additionalContext, input.additionalContext)
150
158
  || previous.operation !== operation) {
151
159
  throw new Error("Endpoint attempt identity cannot be reused for different input or operation.");
152
160
  }
@@ -160,7 +168,9 @@ class BuiltinAgentEndpoint {
160
168
  return { status: "not-submitted", error: new ProviderTurnRejectedError("Endpoint is detached.", input.attemptId) };
161
169
  }
162
170
  const attempt = {
163
- input: Object.freeze({ ...input }),
171
+ input: Object.freeze({ ...input, ...(input.additionalContext === undefined ? {} : {
172
+ additionalContext: structuredClone(input.additionalContext)
173
+ }) }),
164
174
  operation,
165
175
  disposition: { status: "pending", reason: "submitting" }
166
176
  };
@@ -35,6 +35,8 @@ const ENDPOINT_CHECKOUT_MODULES = Object.freeze([
35
35
  "acpProtocol.js",
36
36
  "acpSessionConfiguration.js",
37
37
  "agentRunConfiguration.js",
38
+ "contextRecovery.js",
39
+ "codexNativeAccess.js",
38
40
  "jsonLineChannel.js"
39
41
  ]);
40
42
  function resolveEndpointGeneration() {