@zq-silk/yui 2.22.0 → 2.25.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 (89) hide show
  1. package/README.md +9 -4
  2. package/dist/artifacts/artifactCapability.js +6 -6
  3. package/dist/artifacts/gitArtifactRef.js +10 -10
  4. package/dist/artifacts/jobArtifact.js +5 -5
  5. package/dist/artifacts/managedGit.js +15 -12
  6. package/dist/artifacts/taskArtifactRepository.js +35 -33
  7. package/dist/cli/commandCatalog.js +2 -2
  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/cli.js +9 -2
  12. package/dist/commands/configCommands.js +64 -1
  13. package/dist/commands/projectCommands.js +9 -9
  14. package/dist/commands/taskCompletionGate.js +1 -1
  15. package/dist/commands/taskIntegrationCommands.js +1 -1
  16. package/dist/commands/taskPublicationAdoptCommand.js +1 -1
  17. package/dist/commands/taskRoleRuntimeStatus.js +4 -0
  18. package/dist/commands/taskUpstreamCommands.js +1 -1
  19. package/dist/config/configCatalog.js +3 -0
  20. package/dist/config/gitConfig.js +20 -0
  21. package/dist/config/yuiConfig.js +1 -0
  22. package/dist/context/currentSkills.js +32 -1
  23. package/dist/context/sessionBootstrapManifest.js +17 -4
  24. package/dist/context/sessionSkillNotice.js +33 -12
  25. package/dist/controlPlaneCli.js +4 -4
  26. package/dist/controller/clientRuntime.js +37 -18
  27. package/dist/controller/controller.js +16 -1
  28. package/dist/controller/fileSchedulerStoreAdapter.js +2 -0
  29. package/dist/controller/hostLifecycle.js +153 -0
  30. package/dist/controller/hostLifecycleChecks.js +27 -0
  31. package/dist/controller/hostUpgrade.js +344 -0
  32. package/dist/controller/jobArtifactControl.js +3 -3
  33. package/dist/controller/jobDefinitionControl.js +1 -1
  34. package/dist/controller/runtime.js +67 -16
  35. package/dist/controller/sessionNotify.js +5 -46
  36. package/dist/execution/codexThreadNaming.js +54 -43
  37. package/dist/executor/fileRoleLaunchPlanner.js +18 -3
  38. package/dist/integration/gitIntegrationService.js +1 -1
  39. package/dist/kernel/builtinCapabilities.js +3 -3
  40. package/dist/repository/gitCommand.js +153 -0
  41. package/dist/repository/gitCompatibility.js +3 -2
  42. package/dist/repository/gitWorkspace.js +579 -563
  43. package/dist/repository/gitWorktreeInventory.js +15 -19
  44. package/dist/repository/projectRefresh.js +1 -1
  45. package/dist/repository/taskBaseFreshness.js +1 -4
  46. package/dist/repository/taskWorkspacePreparer.js +1 -1
  47. package/dist/runtime/agentEndpoint.js +11 -1
  48. package/dist/runtime/agentEndpointIdentity.js +2 -0
  49. package/dist/runtime/agentHost.js +167 -42
  50. package/dist/runtime/agentHostImplementation.js +3 -1
  51. package/dist/runtime/codexAppServerRuntime.js +3 -0
  52. package/dist/runtime/codexInteractiveHost.js +62 -17
  53. package/dist/runtime/codexNativeAccess.js +1 -1
  54. package/dist/runtime/contextRecovery.js +15 -0
  55. package/dist/runtime/hostSessionLifecycle.js +83 -0
  56. package/dist/runtime/hostUpgradeGate.js +43 -0
  57. package/dist/runtime/lifecycleChecks.js +47 -0
  58. package/dist/runtime/lifecycleReservation.js +9 -1
  59. package/dist/runtime/nativeExecutable.js +5 -2
  60. package/dist/runtime/nativeTitleReceipt.js +31 -0
  61. package/dist/runtime/nativeTitleSync.js +192 -0
  62. package/dist/runtime/platform.js +11 -0
  63. package/dist/runtime/providerRetry.js +4 -4
  64. package/dist/runtime/structuredProviderHost.js +42 -10
  65. package/dist/storage/sqliteSchema.js +30 -0
  66. package/dist/storage/storageVersions.js +1 -1
  67. package/dist/storage/taskStore.js +7 -1
  68. package/dist/web/webConversation.js +2 -2
  69. package/dist/web/webMaterials.js +19 -19
  70. package/dist/web/webTaskSurface.js +3 -3
  71. package/dist/workspace/workItemChangeSetManager.js +3 -3
  72. package/docs/managed-turn-and-session-runtime.md +96 -1
  73. package/docs/managed-turn-and-session-runtime.zh-CN.md +70 -1
  74. package/docs/project-refresh.md +3 -2
  75. package/docs/project-refresh.zh-CN.md +2 -1
  76. package/docs/release-workflow.md +86 -2
  77. package/docs/release-workflow.zh-CN.md +65 -1
  78. package/docs/roles-and-configuration.md +34 -0
  79. package/docs/roles-and-configuration.zh-CN.md +27 -0
  80. package/docs/storage-baseline.md +14 -1
  81. package/docs/storage-baseline.zh-CN.md +7 -1
  82. package/docs/testing/verification-levels.md +7 -7
  83. package/docs/testing/verification-levels.zh-CN.md +6 -6
  84. package/i18n/README.zh-CN.md +8 -4
  85. package/package.json +1 -1
  86. package/skills/yui-runtime/SKILL.md +7 -2
  87. package/skills/yui-runtime/VERSION +1 -1
  88. package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
  89. package/dist/runtime/native/darwin-x64/process-identity +0 -0
@@ -27,7 +27,7 @@ import { launchBrokerForHome } from "../runtime/launchBroker.js";
27
27
  import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
28
28
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
29
29
  import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
30
- import { providerRetryPrompt } from "../runtime/providerRetry.js";
30
+ import { providerRetryContext } from "../runtime/providerRetry.js";
31
31
  import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
32
32
  import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
33
33
  import { managedRuntimeRoot } from "../storage/homeLayout.js";
@@ -60,6 +60,13 @@ import { ephemeralDomainFromEnvironment, recordEphemeralTmuxTarget } from "./dom
60
60
  import { createEphemeralResourceReaper } from "./ephemeralResourceReaper.js";
61
61
  import { AgentHostProviderSessionBusyError, AgentHostProviderTurnFenceError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
62
62
  import { deliverGlobalInputs } from "./globalInputDelivery.js";
63
+ import { HostUpgradeCoordinator } from "./hostUpgrade.js";
64
+ import { createHostLifecycleChecks } from "./hostLifecycleChecks.js";
65
+ import { createControllerHostLifecycle } from "./hostLifecycle.js";
66
+ import { createNonBlockingChecks } from "../runtime/lifecycleChecks.js";
67
+ import { AGENT_HOST_IMPLEMENTATION } from "../runtime/agentHostImplementation.js";
68
+ import { builtinAgentEndpointImplementation } from "../runtime/agentEndpointIdentity.js";
69
+ import { sendAgentHostUpgradeControl } from "../runtime/agentHost.js";
63
70
  import { authorizeJobStart } from "./jobControl.js";
64
71
  import { authorizeGlobalJobStart, createJobDefinitionControl } from "./jobDefinitionControl.js";
65
72
  import { reconcileTaskDelegation } from "./jobTaskDelegationControl.js";
@@ -198,7 +205,7 @@ async function createFileTaskControllerRuntime(home, options) {
198
205
  ?? new AgentConfigurationCatalogService(home, {
199
206
  environment: options.environment ?? process.env
200
207
  });
201
- const sessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
208
+ const rawSessionHost = options.sessionHost ?? new TmuxSessionHost(planner, tmux, {
202
209
  validateLaunch: async (request) => {
203
210
  const agent = store.getConfiguredAgent(request.agentId);
204
211
  if (agent === null)
@@ -267,13 +274,13 @@ async function createFileTaskControllerRuntime(home, options) {
267
274
  globalInstallPaths: [process.execPath]
268
275
  }
269
276
  });
270
- const lifecycleHost = {
271
- inspectOwner: (owner) => (sessionHost.inspectOwner(owner)),
272
- ...(sessionHost.inspectOwners === undefined
273
- ? {}
274
- : {
275
- inspectOwners: (owners) => sessionHost.inspectOwners(owners)
276
- }),
277
+ const hostStateChecks = createNonBlockingChecks([{
278
+ id: "host-version-hint",
279
+ run: (_transition, signal) => { if (!signal.aborted)
280
+ hostLifecycleChecks.notify("host-observed"); }
281
+ }], options.onError);
282
+ const controllerHosts = createControllerHostLifecycle(rawSessionHost, {
283
+ after: hostStateChecks,
277
284
  stopOwner: (owner) => {
278
285
  // Issue 03: the durable `stopped` transition is gated on physical
279
286
  // exit proof. A blocked result keeps the Session non-terminal and
@@ -287,7 +294,9 @@ async function createFileTaskControllerRuntime(home, options) {
287
294
  return result.outcome === "stop-confirmed";
288
295
  });
289
296
  },
290
- };
297
+ });
298
+ const sessionHost = controllerHosts.port;
299
+ const lifecycleHost = sessionHost;
291
300
  const resourceActivity = createRuntimeResourceActivityTracker();
292
301
  let runningRuntime;
293
302
  let runningController;
@@ -298,6 +307,32 @@ async function createFileTaskControllerRuntime(home, options) {
298
307
  },
299
308
  runtimeIsolation
300
309
  });
310
+ const hostUpgrades = new HostUpgradeCoordinator(store, AGENT_HOST_IMPLEMENTATION, {
311
+ inspect: owner => inspectAgentHost({ home, ...owner }),
312
+ control: async (owner, type, request) => {
313
+ const send = async () => {
314
+ const result = await sendAgentHostUpgradeControl({
315
+ home, ...owner, control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type, ...request }
316
+ });
317
+ if (result.outcome === "rejected")
318
+ throw new Error(result.failure?.detail ?? "Host upgrade control rejected.");
319
+ return result.snapshot;
320
+ };
321
+ return type === "finish-upgrade" ? controllerHosts.requestExit(owner, send) : send();
322
+ },
323
+ hostState: async (owner) => (await sessionHost.inspectOwner(owner)).state,
324
+ endpoint: builtinAgentEndpointImplementation,
325
+ restore: async (owner, request, effective, assertCurrent) => {
326
+ const workspace = owner.scope === "task" ? currentDesiredManagedWorkspace(store, owner.taskId, owner.roleName) : undefined;
327
+ await launchCoordinator.prepare({
328
+ owner, agentId: request.agentId, adapterId: request.adapterId, effective,
329
+ workspace: effective.workspace.root, mode: "resume", nativeSessionId: request.nativeSessionId,
330
+ ...(workspace === undefined ? {} : { managedWorkspace: workspace }),
331
+ ...(effective.executionAuthority === "planning" ? { workspaceFree: true } : {})
332
+ }, assertCurrent);
333
+ }
334
+ });
335
+ const hostLifecycleChecks = createHostLifecycleChecks(hostUpgrades, options.onError);
301
336
  // One inventory scan per scheduler pass. When the inventory worker is active
302
337
  // the blocking /proc scan runs there; otherwise it runs on the main thread.
303
338
  const scanInventory = (panes) => inventoryClient !== undefined
@@ -588,10 +623,22 @@ async function createFileTaskControllerRuntime(home, options) {
588
623
  let messageHealthHosts = [];
589
624
  const running = await startFileTaskController(home, workerMaintenance
590
625
  ? workerSchedulerStore(schedulerStore, asyncStoreClient)
591
- : schedulerStore, delivery, (method, params) => method === "task.role-capabilities"
592
- ? readTaskAgentCapabilities(store, catalogs, params)
593
- : method === "web.start" || method === "web.stop" || method === "web.status"
594
- ? web.dispatch(method, params) : lifecycleDispatcher(method, params), {
626
+ : schedulerStore, delivery, (method, params) => method === "runtime.host-upgrade-status"
627
+ ? hostUpgrades.status().then(hosts => ({ hosts }))
628
+ : method === "runtime.upgrade-hosts"
629
+ ? (params.generation === AGENT_HOST_IMPLEMENTATION.generation
630
+ ? hostUpgrades.request().then(hosts => ({ hosts }))
631
+ : Promise.reject(new Error("Host upgrade target is not this Controller's verified loaded generation.")))
632
+ : method === "task.role-capabilities"
633
+ ? readTaskAgentCapabilities(store, catalogs, params)
634
+ : method === "web.start" || method === "web.stop" || method === "web.status"
635
+ ? web.dispatch(method, params) : Promise.resolve(lifecycleDispatcher(method, params)).then(result => {
636
+ if (["runtime.ensure-role-session", "runtime.process-exit-observe",
637
+ "runtime.host-observation-apply", "runtime.observation-apply"].includes(method)) {
638
+ hostLifecycleChecks.notify("host-observed");
639
+ }
640
+ return result;
641
+ }), {
595
642
  intervalMs: options.intervalMs
596
643
  ?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
597
644
  signalWindowMs: options.signalWindowMs,
@@ -610,6 +657,7 @@ async function createFileTaskControllerRuntime(home, options) {
610
657
  diagnosticMetrics: options.diagnosticMetrics,
611
658
  onRequestDiagnostic: options.onRequestDiagnostic,
612
659
  lifecycleHost,
660
+ lifecycleChecks: hostLifecycleChecks,
613
661
  providerRetry: createProviderRetryHooks(home, store, {
614
662
  now: options.now,
615
663
  onError: options.onError,
@@ -704,7 +752,9 @@ async function createFileTaskControllerRuntime(home, options) {
704
752
  }
705
753
  let resourceClose;
706
754
  const closeResources = () => {
707
- resourceClose ??= closeTelemetry().finally(() => Promise.all([
755
+ hostStateChecks.close();
756
+ hostLifecycleChecks.close();
757
+ resourceClose ??= hostUpgrades.close().then(() => closeTelemetry()).finally(() => Promise.all([
708
758
  web.close(),
709
759
  kernel.close(),
710
760
  asyncStoreClient?.close() ?? Promise.resolve(),
@@ -845,7 +895,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
845
895
  const retry = binding?.retry;
846
896
  return { recorded: true, ...(retry?.currentAttemptId === value.attemptId && value.boundedText !== undefined
847
897
  ? { retryInput: {
848
- text: providerRetryPrompt(retry, value.boundedText),
898
+ text: value.boundedText,
899
+ ...(retry.failedNativeTurnId === undefined ? {} : { context: providerRetryContext(retry) }),
849
900
  ...(retry.failedNativeTurnId === undefined ? {} : { expectedFailedNativeTurnId: retry.failedNativeTurnId })
850
901
  } } : {}) };
851
902
  }
@@ -2,10 +2,9 @@ import { callController } from "../core/controllerClient.js";
2
2
  import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
3
3
  import { transportAgentResult } from "../domain/agentResultTransport.js";
4
4
  import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
5
- import { setCodexThreadName } from "../execution/codexThreadNaming.js";
6
5
  import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
7
6
  /** Hidden CLI entrypoint used by Codex's structured notify hook. */
8
- export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call, setThreadName = setCodexThreadName) {
7
+ export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call) {
9
8
  const params = parseCodexSessionNotification(payloadArgument, environment);
10
9
  // Old global TUIs can still emit their invocation-local hook. It is not
11
10
  // evidence of a shared-daemon Thread's identity or process lifecycle.
@@ -15,7 +14,7 @@ export async function runSessionNotifyCommand(payloadArgument, environment = pro
15
14
  const home = requireText(environment.YUI_HOME, "YUI_HOME");
16
15
  // The caller reports native identity; durable state supplies the current AgentRun.
17
16
  const current = currentNotifyRun(home, params);
18
- const enqueued = new FileRuntimeEventInbox(home).enqueueRunTerminal({
17
+ new FileRuntimeEventInbox(home).enqueueRunTerminal({
19
18
  scope: params.scope,
20
19
  ...(params.scope === "task" ? { taskId: params.taskId } : {}),
21
20
  roleName: params.roleName,
@@ -42,11 +41,9 @@ export async function runSessionNotifyCommand(payloadArgument, environment = pro
42
41
  roleName: params.roleName
43
42
  })
44
43
  }, { timeoutMs: 100 }).catch(() => { });
45
- if (enqueued.created && shouldSetThreadName(home, params, current.runId)) {
46
- const request = threadNameRequest(params, environment);
47
- if (request !== null)
48
- await setThreadName(request).catch(() => { });
49
- }
44
+ // Title synchronization belongs to the Host's separately fenced metadata
45
+ // lane. A terminal notification is neither a rename receipt nor permission
46
+ // to replay an unknown write or overwrite a native user's name.
50
47
  }
51
48
  /**
52
49
  * Resolve the active AgentRun only when the notifying native Session matches.
@@ -130,44 +127,6 @@ function requireText(value, label) {
130
127
  throw new Error(`${label} is invalid.`);
131
128
  return text;
132
129
  }
133
- function shouldSetThreadName(home, params, runId) {
134
- if (params.scope !== "task"
135
- || params.title === undefined
136
- || runId === undefined)
137
- return false;
138
- try {
139
- const store = openCurrentTaskStore(home);
140
- return store.getRun(params.taskId, runId)?.mode === "new";
141
- }
142
- catch {
143
- return false;
144
- }
145
- }
146
- function threadNameRequest(params, environment) {
147
- if (params.title === undefined)
148
- return null;
149
- const command = environment.YUI_AGENT_COMMAND;
150
- const encodedBaseArgs = environment.YUI_AGENT_BASE_ARGS;
151
- if (command === undefined || encodedBaseArgs === undefined)
152
- return null;
153
- try {
154
- const parsed = JSON.parse(encodedBaseArgs);
155
- if (!Array.isArray(parsed)
156
- || parsed.some((value) => typeof value !== "string" || value.includes("\0"))) {
157
- return null;
158
- }
159
- return {
160
- command: requireText(command, "YUI_AGENT_COMMAND"),
161
- baseArgs: parsed,
162
- environment,
163
- threadId: params.nativeSessionId,
164
- name: params.title
165
- };
166
- }
167
- catch {
168
- return null;
169
- }
170
- }
171
130
  function isObject(value) {
172
131
  return typeof value === "object" && value !== null && !Array.isArray(value);
173
132
  }
@@ -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;
@@ -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
  : {}),
@@ -64,7 +64,7 @@ export class GitIntegrationService {
64
64
  environment;
65
65
  runtimeIsolation;
66
66
  #resourceRegistrarValue;
67
- constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home, store.getHomeIdentity().homeId), jobPort) {
67
+ constructor(home, store, git = new NodeGitWorkspace(() => store.getConfig()), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home, store.getHomeIdentity().homeId), jobPort) {
68
68
  this.store = store;
69
69
  this.git = git;
70
70
  this.now = now;
@@ -324,14 +324,14 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
324
324
  ...(params.encoding === undefined ? {} : { encoding: params.encoding }),
325
325
  ...(params.message === undefined ? {} : { message: params.message }),
326
326
  ...(params.expectedHead === undefined ? {} : { expectedHead: params.expectedHead })
327
- });
327
+ }, () => store.getConfig());
328
328
  if (name === "artifact.read")
329
329
  return readArtifactCapability(store.rootDirectory(), taskId, {
330
330
  relativePath: params.relativePath,
331
331
  ...(params.commit === undefined ? {} : { commit: params.commit })
332
- });
332
+ }, () => store.getConfig());
333
333
  if (name === "artifact.list")
334
- return listArtifactsCapability(store.rootDirectory(), taskId);
334
+ return listArtifactsCapability(store.rootDirectory(), taskId, () => store.getConfig());
335
335
  if (name === "environment.prepare")
336
336
  return resources.prepare(taskId, params.plan);
337
337
  if (name === "environment.adopt")
@@ -0,0 +1,153 @@
1
+ import { spawn } from "node:child_process";
2
+ import { performance } from "node:perf_hooks";
3
+ import { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS } from "../config/gitConfig.js";
4
+ export const LOCAL_GIT_TIMEOUT_MS = DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS * 1_000;
5
+ /** Retain process facts independently of Git's last progress/error output. */
6
+ export class GitCommandError extends Error {
7
+ timeoutKind;
8
+ budget;
9
+ elapsedMs;
10
+ code;
11
+ exitCode;
12
+ signal;
13
+ stdout;
14
+ stderr;
15
+ stderrTruncated;
16
+ constructor(cause, timeoutKind, budget, elapsedMs, output) {
17
+ const failure = cause;
18
+ const code = failure?.code ?? output.exitCode;
19
+ const exitCode = output.exitCode;
20
+ const stderr = output.stderr.toString("utf8").trim();
21
+ const reasons = [
22
+ ...(timeoutKind === undefined ? [] : [
23
+ `${timeoutKind === "inactivity" ? "no-progress timeout" : "total timeout"} after `
24
+ + `${timeoutKind === "inactivity" ? budget.inactivityMs : budget.totalMs} ms`
25
+ ]),
26
+ ...(exitCode === null ? [] : [`exit code ${exitCode}`]),
27
+ ...(output.signal === null ? [] : [`signal ${output.signal}`]),
28
+ ...(typeof code === "string" ? [`${code}: ${failure?.message ?? "process error"}`] : [])
29
+ ];
30
+ if (reasons.length === 0)
31
+ reasons.push(failure?.message ?? "unknown process failure");
32
+ super(`Git command failed (${reasons.join("; ")}; elapsed ${elapsedMs} ms)`
33
+ + (stderr.length === 0 ? "." : `: ${output.stderrTruncated ? "[stderr tail; earlier output omitted] " : ""}${stderr}`), { cause });
34
+ this.timeoutKind = timeoutKind;
35
+ this.budget = budget;
36
+ this.elapsedMs = elapsedMs;
37
+ this.name = "GitCommandError";
38
+ this.code = code;
39
+ this.exitCode = exitCode;
40
+ this.signal = output.signal;
41
+ this.stdout = output.stdout;
42
+ this.stderr = stderr;
43
+ this.stderrTruncated = output.stderrTruncated;
44
+ }
45
+ }
46
+ /**
47
+ * One buffered invocation, never retried. Output refreshes only the inactivity
48
+ * clock; the total deadline never moves. At the hard deadline, kill the owned
49
+ * process group (including transport helpers) before reporting completion.
50
+ */
51
+ export async function executeGitCommand(program, args, options = {}) {
52
+ const budget = options.budget ?? { totalMs: LOCAL_GIT_TIMEOUT_MS };
53
+ for (const value of [budget.totalMs, ...(budget.inactivityMs === undefined ? [] : [budget.inactivityMs])]) {
54
+ if (!Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647) {
55
+ throw new TypeError("Git timeout budgets must be positive integer milliseconds within the timer range.");
56
+ }
57
+ }
58
+ const started = performance.now();
59
+ const child = spawn(program, args, {
60
+ ...(options.environment === undefined ? {} : { env: options.environment }),
61
+ detached: process.platform !== "win32",
62
+ windowsHide: true
63
+ });
64
+ const maxBuffer = options.maxBuffer ?? 1024 * 1024;
65
+ const chunks = { stdout: [], stderr: [] };
66
+ const sizes = { stdout: 0, stderr: 0 };
67
+ let stderrTruncated = false;
68
+ let failure;
69
+ let timeoutKind;
70
+ let terminationError;
71
+ const terminate = () => {
72
+ try {
73
+ if (child.pid !== undefined && process.platform !== "win32") {
74
+ try {
75
+ process.kill(-child.pid, "SIGKILL");
76
+ }
77
+ catch (error) {
78
+ if (error.code !== "ESRCH")
79
+ throw error;
80
+ }
81
+ }
82
+ else {
83
+ child.kill("SIGKILL");
84
+ }
85
+ }
86
+ catch (error) {
87
+ terminationError = error;
88
+ child.kill("SIGKILL");
89
+ }
90
+ };
91
+ const expire = (kind) => {
92
+ if (timeoutKind !== undefined || failure !== undefined)
93
+ return;
94
+ timeoutKind = kind;
95
+ terminate();
96
+ };
97
+ child.once("error", error => { failure ??= error; });
98
+ const completion = new Promise(settle => {
99
+ child.once("close", (exitCode, signal) => settle({
100
+ // A negative close code is an OS spawn error, not a process exit.
101
+ exitCode: exitCode !== null && exitCode >= 0 ? exitCode : null, signal
102
+ }));
103
+ });
104
+ const totalTimer = setTimeout(() => expire("total"), budget.totalMs);
105
+ const inactivityTimer = budget.inactivityMs === undefined
106
+ ? undefined : setTimeout(() => expire("inactivity"), budget.inactivityMs);
107
+ const collect = (stream, chunk) => {
108
+ if (chunk.length === 0)
109
+ return;
110
+ if (timeoutKind === undefined && failure === undefined)
111
+ inactivityTimer?.refresh();
112
+ if (stream === "stderr" && options.stderrTail) {
113
+ const limit = Math.min(maxBuffer, 64 * 1024);
114
+ stderrTruncated ||= sizes.stderr + chunk.length > limit;
115
+ const tail = Buffer.concat([...chunks.stderr, chunk]);
116
+ chunks.stderr = [tail.subarray(Math.max(0, tail.length - limit))];
117
+ sizes.stderr = Math.min(limit, tail.length);
118
+ return;
119
+ }
120
+ const remaining = maxBuffer - sizes[stream];
121
+ if (remaining > 0) {
122
+ chunks[stream].push(chunk.subarray(0, remaining));
123
+ sizes[stream] += Math.min(chunk.length, remaining);
124
+ }
125
+ if (chunk.length > remaining && failure === undefined) {
126
+ failure = Object.assign(new Error(`${stream} maxBuffer length exceeded (${maxBuffer} bytes).`), { code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" });
127
+ terminate();
128
+ }
129
+ };
130
+ child.stdout.on("data", chunk => collect("stdout", chunk));
131
+ child.stderr.on("data", chunk => collect("stderr", chunk));
132
+ child.stdin.on("error", error => {
133
+ failure ??= error;
134
+ terminate();
135
+ });
136
+ try {
137
+ child.stdin.end(options.input);
138
+ const status = await completion;
139
+ const result = { stdout: Buffer.concat(chunks.stdout), stderr: Buffer.concat(chunks.stderr), stderrTruncated };
140
+ if (failure !== undefined || timeoutKind !== undefined || status.exitCode !== 0 || status.signal !== null) {
141
+ const error = new GitCommandError(failure, timeoutKind, budget, Math.round(performance.now() - started), { ...result, ...status });
142
+ if (terminationError !== undefined) {
143
+ throw new AggregateError([error, terminationError], `${error.message} Process-group termination failed; helper effects are unconfirmed.`, { cause: error });
144
+ }
145
+ throw error;
146
+ }
147
+ return result;
148
+ }
149
+ finally {
150
+ clearTimeout(totalTimer);
151
+ clearTimeout(inactivityTimer);
152
+ }
153
+ }
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { realpathSync, statSync } from "node:fs";
3
3
  import { resolveExecutable } from "../external/pinnedCommandRunner.js";
4
+ import { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS } from "../config/gitConfig.js";
4
5
  // The fetch isolation boundary needs --no-write-fetch-head. Older Git cannot
5
6
  // implement it by saving/restoring FETCH_HEAD without racing another writer.
6
7
  export const MINIMUM_GIT_VERSION = "2.29.0";
@@ -17,7 +18,7 @@ export function gitVersionSupport(output) {
17
18
  }
18
19
  let cached;
19
20
  /** Pin the executable checked, reusing only an unchanged on-disk identity. */
20
- export function supportedGit() {
21
+ export function supportedGit(timeoutMs = DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS * 1_000) {
21
22
  const selected = resolveExecutable("git", process.env.PATH);
22
23
  if (selected === undefined)
23
24
  throw new Error("Git executable not found in PATH; install a supported Git before retrying. No Git operation was started.");
@@ -27,7 +28,7 @@ export function supportedGit() {
27
28
  if (cached?.identity === identity)
28
29
  return cached;
29
30
  const output = execFileSync(path, ["--version"], {
30
- encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"]
31
+ encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"]
31
32
  });
32
33
  const support = gitVersionSupport(output);
33
34
  if (!support.supported) {