@zq-silk/yui 0.13.2 → 0.13.3

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.
package/ARCHITECTURE.md CHANGED
@@ -49,7 +49,8 @@ Session.
49
49
  - `WorkerProfile` is a versioned, provider-neutral behavior template containing
50
50
  instructions, Skills, a read/write behavior intent, and optional model/effort hints.
51
51
  - `TaskRole` is a mutable Worker instance inside one Task. Applying a Profile
52
- copies its portable behavior. Its versioned desired launch configuration is
52
+ copies its portable behavior and the optional model/effort selections into
53
+ the active Agent binding. Its versioned desired launch configuration is
53
54
  next-launch-only. The Role may bind multiple Agents; every binding retains
54
55
  independent runtime configuration.
55
56
  - `AgentRun` records one managed dispatch and an immutable effective snapshot:
@@ -240,33 +241,38 @@ context.
240
241
 
241
242
  ## Runtime ownership
242
243
 
243
- tmux owns the persistent Agent Host lifetime and observable output. The
244
+ Provider conversations remain user conversations. Yui adds the matching Role
245
+ Skill and Session Manifest pointer, then uses provider-native requests for
246
+ durable Task delivery; it does not own or mirror the full transcript. The
244
247
  Controller owns mailbox delivery, wakeups, Role liveness, recovery decisions,
245
- and a durable single-writer authority epoch. The Agent Host owns one structured
246
- Provider child process and mirrors its output to the pane. Managed prompts are
247
- never terminal bytes: both Controller and human takeover input become fenced
248
- Provider-native Turn requests.
248
+ and exact receipts. tmux keeps Yui's client attachment observable where the
249
+ provider path needs one.
249
250
 
250
251
  Run, Conversation, Activation, and Turn identities are independent. A
251
- Conversation can span Runs and Provider processes; one Activation identifies
252
- one live process; one Turn identifies one pre-recorded input attempt. A live
253
- Activation is retained when a later Run reuses the Conversation. Process exit
254
- ends that Activation, and a resumed process receives a new Activation and a
255
- higher authority epoch.
256
-
257
- Task observation uses `task role view`. Explicit takeover requires a live
258
- managed Run, atomically transfers authority to a human holder, synchronizes the
259
- same fence to the Host, and only then exposes the PTY input gateway. Detach
260
- releases authority; `task role release` is an idempotent repair path even after
261
- the Run has ended. Global interactive entry remains a native session-lifecycle
262
- operation outside this managed Provider contract.
263
-
264
- Codex uses a persistent App Server JSON-RPC transport. Claude uses a persistent
265
- stream-json transport with exact user-message replay acknowledgement. In both
266
- cases, Yui records Turn intent before writing, accepts only exact Provider
267
- evidence, and maps an uncertain write to `delivery-unknown` without automatic
268
- resubmission. Conversation replacement requires exact missing evidence and no
269
- unsettled input, Turn, Activation, or writer authority.
252
+ Conversation can span Runs and client attachments; one Activation identifies
253
+ Yui's current attachment, not exclusive ownership of the Provider thread. One
254
+ Turn identifies one provider-native execution. Yui's authority epoch fences
255
+ only Yui's own submissions and retries.
256
+
257
+ Codex Task threads can be opened and used directly in Desktop as ordinary
258
+ sessions; no Yui takeover is required. If a direct user Turn is active, Yui
259
+ keeps its pending Run/message and retries after that Turn settles. The explicit
260
+ tmux takeover gateway remains for providers whose managed conversation is
261
+ attached to an independent process. Global interactive entry remains a native
262
+ session-lifecycle operation outside the Task delivery contract.
263
+
264
+ Codex uses `app-server proxy` to create or resume a normal thread on the shared
265
+ App Server daemon. Role model, effort, permission, workspace, and shell settings are passed at
266
+ `thread/start`/`thread/resume`, while the ordinary Task message points to the
267
+ Session Manifest and matching Role Skill. Yui does not write either to global
268
+ Codex config and may only ensure that the daemon is running; it never restarts
269
+ or stops it for a thread error. A Codex native config profile is rejected for
270
+ Managed Codex because the protocol cannot isolate it to one thread; a Yui Agent
271
+ Profile supplies the per-Role behavior, Skills, model, and effort instead.
272
+ Claude uses a persistent stream-json
273
+ transport with exact user-message replay acknowledgement. In both cases, Yui
274
+ records Turn intent before writing, accepts only exact Provider evidence, and
275
+ maps an uncertain write to `delivery-unknown` without automatic resubmission.
270
276
 
271
277
  Role desired revisions and Run/Session effective snapshots keep configuration
272
278
  history explicit. Resume compares the complete effective snapshot and
package/README.md CHANGED
@@ -252,7 +252,7 @@ Yui provides four reusable Worker Profile definitions through
252
252
  worker explorer implementer reviewer
253
253
  ```
254
254
 
255
- Profiles are versioned, provider-neutral Worker behavior templates. They hold portable prompt instructions, Skills, access expectations, and optional model and effort hints, but do not bind an Agent or own a Session or workspace. A Task Role is the Task-bound Worker instance: applying a Profile copies its portable behavior into that mutable instance, while each Agent binding keeps its own runtime configuration.
255
+ Profiles are versioned, provider-neutral Worker behavior templates. They hold portable prompt instructions, Skills, access expectations, and optional model and effort selections, but do not bind an Agent or own a Session or workspace. A Task Role is the Task-bound Worker instance: applying a Yui Agent Profile copies the behavior plus model/effort into that Role's active Agent binding; explicit Role options may override the copied values. This is separate from a Codex native config profile selected with `--profile`.
256
256
 
257
257
  ## Quick start
258
258
 
@@ -871,15 +871,20 @@ Task lifecycle completion/selection only suggests valid source states: Draft for
871
871
  ## Sessions and tmux
872
872
 
873
873
  Managed Task Agents use the [hybrid Provider runtime](docs/provider-runtime.md).
874
- The Controller and Agent Host send Provider-native structured requests; tmux
875
- and PTY keep the Host alive, show output, and provide an explicit human input
876
- gateway. Managed prompts are never delivered as terminal bytes.
877
-
878
- Codex uses a persistent App Server JSON-RPC process. Claude Code uses a
879
- persistent stream-json process with exact user-message replay acknowledgement.
880
- Both follow the same Conversation, Activation, Turn, and authority-epoch
881
- contract. A timeout or uncertain write becomes `delivery-unknown` and is never
882
- automatically retried.
874
+ Provider conversations remain ordinary user conversations. Yui adds the Role
875
+ Skill and Session Manifest pointer, then sends Task work through provider-native
876
+ structured requests. Managed prompts are never delivered as terminal bytes.
877
+
878
+ Codex uses `app-server proxy` to create or resume a normal thread on the shared
879
+ App Server daemon. The thread is visible and directly usable in Desktop; Yui
880
+ does not require takeover or install global Hook/config state. It may
881
+ idempotently start an absent daemon, but never restarts or stops it for a thread
882
+ error. When a user Turn is active, Yui keeps pending Task work and retries
883
+ later. If Yui's proxy disconnects, the Host reattaches without ending the
884
+ logical Activation and reconciles the exact owned Turn from native history.
885
+ Claude Code keeps its independent stream-json process with exact
886
+ user-message replay acknowledgement. A timeout or uncertain write becomes
887
+ `delivery-unknown` and is never automatically retried.
883
888
 
884
889
  Task Role observation and takeover are explicit:
885
890
 
@@ -889,11 +894,10 @@ yui task role takeover <task-id> <role>
889
894
  yui task role release <task-id> <role>
890
895
  ```
891
896
 
892
- `view` is read-only. `takeover` transfers the durable writer authority to the
893
- human before enabling the Host PTY gateway and requires a live managed Run;
894
- detach returns it to the Controller. `release` remains available without a live
895
- Run and idempotently repairs a stranded or partially synchronized takeover.
896
- Authority cannot transfer while a Provider Turn or input delivery is unsettled.
897
+ For Codex, open the Role's ordinary thread in Desktop for direct interaction;
898
+ these commands are not required. `view`/`takeover`/`release` remain the explicit
899
+ PTY gateway for providers whose managed conversation uses an independent
900
+ process.
897
901
 
898
902
  Global Operator and global Role sessions remain native interactive CLIs:
899
903
 
@@ -941,8 +945,8 @@ thread identity from App Server responses. Managed Task Runs use structured
941
945
  Provider observations for both CLIs. Global interactive Codex sessions may
942
946
  still use its `notify` callback for conversation presentation.
943
947
 
944
- Automated lifecycle and delivery decisions use structured Hook payloads,
945
- persisted identities, usage snapshots, tmux process state, receipts, and pane
948
+ Automated lifecycle and delivery decisions use structured Provider events or
949
+ supported Hook payloads, persisted identities, usage snapshots, tmux process state, receipts, and pane
946
950
  fences. Yui never
947
951
  parses prompt glyphs, progress text, trust dialogs, or other Agent terminal
948
952
  output to infer readiness or success. `captureRole()` remains an explicit
@@ -956,9 +960,9 @@ not. A live tmux pane proves only that the host exists. Runtime activity and
956
960
  durable workflow progress use independent clocks, so token/tool/resource
957
961
  movement cannot conceal a workflow that is not advancing.
958
962
 
959
- Stable Role context is also launch metadata, never a bootstrap turn. Yui passes Role policy and `systemPrompt` through the Agent's native system/developer-instruction channel. Task execution Runs receive the generic Leader or Worker Skill, while review Runs receive the generic Reviewer Skill based on durable Run purpose rather than a configured Role name. These Yui-owned Role Skills define portable orchestration only. Project Skills remain ordinary versioned files in the Project and are discovered, selected, and loaded by the Agent through its native project mechanism; Yui does not scan, parse, copy, or inject them.
963
+ Stable Role context never creates a separate bootstrap Turn. Task execution Runs use the generic Leader or Worker Skill, while review Runs use the generic Reviewer Skill based on durable Run purpose rather than a configured Role name. The provider either carries the Skill through a safe additive native context channel or points to it from the ordinary Task delivery. These Yui-owned Role Skills define portable orchestration only. Project Skills remain ordinary versioned files in the Project and are discovered, selected, and loaded by the Agent through its native project mechanism; Yui does not scan, parse, copy, or inject them.
960
964
 
961
- Native Codex developer instructions carry compact absolute references only for Yui-owned Role Skills, which Codex reads on demand. Yui applies this scalar as an invocation-local override, so existing user, profile, project, and system values do not make the Session unusable and the underlying config file is never mutated. A higher-precedence managed `developer_instructions` value remains a bounded launch blocker because Codex will not let invocation flags replace it. Interactive Codex Sessions apply the same rule to Yui's structured `notify` callback; Doctor reports ordinary overridden sources as context and rejects an effective managed conflict. Managed Runs instead use invocation-local Agent Driver Hooks as their sole lifecycle authority and do not claim `notify`. `skills.config` is not misused because it only enables or disables already-discovered Skills. Claude receives the same Yui-owned Role Skill content from a private `0600` managed context file rather than a large or sensitive argv value; retries and resumes reuse the purpose-specific Role path. Non-Operator global Roles stay neutral and receive no Task orchestration Skill. Operator therefore opens at an empty native composer, so the user's text remains its first user message. Leader wakeups and Worker or Reviewer Run assignments remain real mailbox-delivered work messages. An adapter without a native instruction channel must reject this context rather than silently converting it into a first user prompt.
965
+ Managed Codex keeps the user's native developer instructions unchanged. The ordinary Task message includes a compact absolute Session Manifest pointer, and the manifest identifies the matching Yui-owned Role Skill for Codex to read on demand. Model, effort, permission, workspace, and shell settings selected for the Role are supplied as thread-scoped `thread/start` or `thread/resume` configuration through the shared App Server daemon. A Codex native config profile cannot be isolated to one shared-daemon thread, so Managed Codex rejects that setting and directs callers to a Yui Agent Profile; ordinary non-Yui and interactive Codex sessions remain unaffected. Unrelated Codex threads keep their existing user, profile, project, and system configuration and the underlying config file is never mutated. App Server notifications are the managed thread's lifecycle authority; Yui installs no managed Codex Hook and does not claim `notify`. Interactive Codex Sessions may still use Yui's structured `notify` callback, and Doctor reports any effective configuration conflict. `skills.config` is not misused because it only enables or disables already-discovered Skills. Claude receives the same Yui-owned Role Skill content from a private `0600` managed context file rather than a large or sensitive argv value; retries and resumes reuse the purpose-specific Role path. Non-Operator global Roles stay neutral and receive no Task orchestration Skill. Operator therefore opens at an empty native composer, so the user's text remains its first user message. Leader wakeups and Worker or Reviewer Run assignments remain real mailbox-delivered work messages.
962
966
 
963
967
  ## Controller and failure handling
964
968
 
@@ -994,7 +998,7 @@ Read-only commands and `upgrade --dry-run` do not start a Controller. `update`
994
998
  also replaces an already-running Controller only after the new binary passes its
995
999
  health checks.
996
1000
 
997
- Its recovery reconciliation runs every 120 seconds by default. Normal durable state changes enqueue a Task, Role, or Operator key and return immediately; keys received in the same fixed 100 ms window trigger one non-overlapping targeted pass. Operator presentation has an independent lane, so a blocked Task workspace operation cannot delay a user question. Periodic Git/worktree work is limited to Tasks with durable Task-mailbox work, while active Role liveness uses one tmux inventory. Agent Driver Hooks write exact-fenced observations to the durable runtime inbox without starting or waiting for the Controller. A terminal Turn observation gives a legal yield/input/completion two seconds to win before a forgotten Run fails its workflow contract. Durable mailboxes freeze the current batch while new signals merge into the next batch. Task-orchestration failures retain the exact Controller-owned processing batch for two bounded fast retries and later periodic recovery; a successful retry completes that batch before newer pending work is claimed. Recommended InputRequest and pending Turn deadlines share one nearest-deadline selector and therefore do not wait for the recovery interval. Explicit `task reconcile` still requests an immediate recovery pass. The retained loop is:
1001
+ Its recovery reconciliation runs every 120 seconds by default. Normal durable state changes enqueue a Task, Role, or Operator key and return immediately; keys received in the same fixed 100 ms window trigger one non-overlapping targeted pass. Operator presentation has an independent lane, so a blocked Task workspace operation cannot delay a user question. Periodic Git/worktree work is limited to Tasks with durable Task-mailbox work, while active Role liveness uses one tmux inventory. Structured Agent Driver observations, whether received from native provider events or supported Hooks, are exact-fenced before they reach the durable runtime inbox. A terminal Turn observation gives a legal yield/input/completion two seconds to win before a forgotten Run fails its workflow contract. Durable mailboxes freeze the current batch while new signals merge into the next batch. Task-orchestration failures retain the exact Controller-owned processing batch for two bounded fast retries and later periodic recovery; a successful retry completes that batch before newer pending work is claimed. Recommended InputRequest and pending Turn deadlines share one nearest-deadline selector and therefore do not wait for the recovery interval. Explicit `task reconcile` still requests an immediate recovery pass. The retained loop is:
998
1002
 
999
1003
  1. dispatch pending Leader wakes whose Task workspaces are already ready;
1000
1004
  2. prepare active Project Task main worktrees with durable orchestration work;
@@ -582,17 +582,17 @@ const taskChildren = [
582
582
  },
583
583
  {
584
584
  name: "view",
585
- summary: "Attach read-only to a managed Provider presentation surface.",
585
+ summary: "Attach read-only to an independent Provider presentation surface.",
586
586
  usage: "yui task role view <task> <role>"
587
587
  },
588
588
  {
589
589
  name: "takeover",
590
- summary: "Acquire Provider writer authority and enter the PTY input gateway.",
590
+ summary: "Enter the PTY input gateway for an independent Provider process.",
591
591
  usage: "yui task role takeover <task> <role>"
592
592
  },
593
593
  {
594
594
  name: "release",
595
- summary: "Return stranded human Provider authority to the Controller.",
595
+ summary: "Release an independent Provider PTY input gateway.",
596
596
  usage: "yui task role release <task> <role>"
597
597
  }
598
598
  ]
@@ -20,7 +20,7 @@ import { renderRoleDetails } from "../output/rolePresentation.js";
20
20
  import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js";
21
21
  import { cancelInputRequest } from "../input/inputRequest.js";
22
22
  import { recoverExactAgentRun, retireExactActiveAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
23
- import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
23
+ import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
24
24
  import { agentRunDeliveryReceiptId, createAgentRun, withAgentRunContextSnapshot } from "../run/agentRun.js";
25
25
  import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
26
26
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
@@ -1441,7 +1441,7 @@ function addTaskRole(args, store, options) {
1441
1441
  let created = createTaskRole(tx, task, roleName, agentId, now);
1442
1442
  const profileId = parsed.one("--profile");
1443
1443
  if (profileId !== undefined) {
1444
- created = updateRole(created, workerProfileRolePatch(requireAgentProfile(tx, profileId)), now);
1444
+ created = applyWorkerAgentProfile(created, requireAgentProfile(tx, profileId), now);
1445
1445
  }
1446
1446
  const profile = roleProfilePatch(parsed);
1447
1447
  if (Object.keys(profile).length > 0)
@@ -1558,20 +1558,20 @@ function updateTaskRole(args, store, options) {
1558
1558
  roleName: role.name
1559
1559
  }, "desired launch configuration update");
1560
1560
  }
1561
- let bindings = role.agentBindings;
1561
+ const profileId = parsed.one("--profile");
1562
+ const withProfile = profileId === undefined
1563
+ ? role
1564
+ : applyWorkerAgentProfile(role, requireAgentProfile(tx, profileId), now);
1565
+ let bindings = withProfile.agentBindings;
1562
1566
  if (changesAgentConfig) {
1563
- const agentId = parsed.one("--agent")?.trim() || role.activeAgentId;
1567
+ const agentId = parsed.one("--agent")?.trim() || withProfile.activeAgentId;
1564
1568
  const agent = requireAgent(tx, agentId);
1565
1569
  const binding = bindings[agentId]
1566
1570
  ?? createRoleAgentBinding({ id: agent.id, adapterId: agent.adapterId });
1567
1571
  bindings = { ...bindings, [agentId]: patchRoleAgentBinding(binding, parsed) };
1568
1572
  }
1569
- const profileId = parsed.one("--profile");
1570
- const withProfile = profileId === undefined
1571
- ? role
1572
- : updateRole(role, workerProfileRolePatch(requireAgentProfile(tx, profileId)), now);
1573
1573
  const next = updateRole(withProfile, {
1574
- ...(bindings === role.agentBindings ? {} : { agentBindings: bindings }),
1574
+ ...(bindings === withProfile.agentBindings ? {} : { agentBindings: bindings }),
1575
1575
  ...roleProfilePatch(parsed)
1576
1576
  }, now);
1577
1577
  if (changesLaunchContext) {
@@ -6463,6 +6463,29 @@ function workerProfileRolePatch(profile) {
6463
6463
  : undefined
6464
6464
  };
6465
6465
  }
6466
+ function applyWorkerAgentProfile(role, profile, now) {
6467
+ const binding = activeRoleAgentBinding(role);
6468
+ const config = structuredClone(binding.config);
6469
+ if (profile.model === undefined)
6470
+ delete config.model;
6471
+ else
6472
+ config.model = profile.model;
6473
+ if (profile.effort === undefined)
6474
+ delete config.effort;
6475
+ else
6476
+ config.effort = profile.effort;
6477
+ const profiledBinding = createRoleAgentBinding({
6478
+ id: binding.agentId,
6479
+ adapterId: binding.adapterId
6480
+ }, config);
6481
+ return updateRole(role, {
6482
+ ...workerProfileRolePatch(profile),
6483
+ agentBindings: {
6484
+ ...role.agentBindings,
6485
+ [binding.agentId]: profiledBinding
6486
+ }
6487
+ }, now);
6488
+ }
6466
6489
  function appendMessage(store, taskId, body, kind, author, now, context = {}) {
6467
6490
  const message = createTaskMessage(store.nextMessageId(taskId), taskId, body, kind, author, now, context);
6468
6491
  store.saveMessage(taskId, message);
@@ -1515,7 +1515,8 @@ export class FileTaskController {
1515
1515
  else if (wakeup.reason === "writer-attached") {
1516
1516
  writerBlocked.add(key);
1517
1517
  }
1518
- else if (wakeup.reason === "not-ready") {
1518
+ else if (wakeup.reason === "not-ready"
1519
+ || (wakeup.reason === "busy" && wakeup.runId !== undefined)) {
1519
1520
  retry.set(key, { identity: wakeup.runId ?? key });
1520
1521
  }
1521
1522
  else if (wakeup.status === "dispatched")
@@ -3588,7 +3588,7 @@ function nativeTransitionExisting(store, owner, existing, nativeSessionId, launc
3588
3588
  throw new Error(conflictMessage);
3589
3589
  }
3590
3590
  function globalCompletionNativeSessionId(existing, input) {
3591
- // Codex app-server and its notify Hook expose different native IDs for the
3591
+ // Codex structured event sources can expose different native IDs for the
3592
3592
  // same launch. The launch generation remains the authoritative fence.
3593
3593
  return input.adapterId === "codex"
3594
3594
  && input.launchId !== undefined
@@ -446,6 +446,10 @@ function requireMatchingRuntimeBinding(raw, request, launchId) {
446
446
  && binding.initialTurnDeliveryUnknownRunId !== request.runId) {
447
447
  throw new RuntimeBindingContractError(`Session host returned a delivery-unknown initial structured Turn for another Run: ${request.owner.roleName}.`);
448
448
  }
449
+ if (binding.initialTurnBusyRunId !== undefined
450
+ && binding.initialTurnBusyRunId !== request.runId) {
451
+ throw new RuntimeBindingContractError(`Session host returned a busy initial structured Turn for another Run: ${request.owner.roleName}.`);
452
+ }
449
453
  if (binding.initialTurnRejectedRunId !== undefined
450
454
  && binding.initialTurnRejectedRunId !== request.runId) {
451
455
  throw new RuntimeBindingContractError(`Session host returned a rejected initial structured Turn for another Run: ${request.owner.roleName}.`);
@@ -10,7 +10,7 @@ const SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
10
10
  const APPROVALS = ["untrusted", "on-request", "never"];
11
11
  const PROBE_TIMEOUT_MS = 2_000;
12
12
  const PROBE_MAX_BYTES = 1024 * 1024;
13
- const CODEX_TESTED_THROUGH_VERSION = "0.145.0";
13
+ const CODEX_TESTED_THROUGH_VERSION = "0.150.1";
14
14
  class BaseAdapter {
15
15
  launchContextArgs(_input) {
16
16
  return [];
@@ -51,7 +51,7 @@ class BaseAdapter {
51
51
  class CodexAdapter extends BaseAdapter {
52
52
  id = "codex";
53
53
  label = "Codex";
54
- supportedVersion = "0.144.1";
54
+ supportedVersion = "0.150.1";
55
55
  capabilities = {
56
56
  recover: true,
57
57
  interrupt: true,
@@ -149,21 +149,31 @@ class CodexAdapter extends BaseAdapter {
149
149
  return { ...launch, argv: [...launch.argv, "resume", nativeId(input.nativeSessionId)] };
150
150
  }
151
151
  compileManagedControl(input, _mode, _nativeSessionId) {
152
+ const config = this.canonicalizeConfig(input.config);
153
+ if (config.profile !== undefined) {
154
+ throw new Error("Managed Codex does not accept a Codex config profile because it cannot be scoped to one "
155
+ + "shared-daemon thread. Use a Yui Agent Profile for skills, model, and effort.");
156
+ }
152
157
  const launch = this.compileNew(input);
153
- const argv = [...launch.argv];
154
- if (input.config.model !== undefined) {
155
- const modelFlag = argv.findIndex((value, index) => (value === "--model" && argv[index + 1] === input.config.model));
158
+ // A managed Codex thread is an ordinary user thread. Its Yui guidance is
159
+ // part of the durable Task message, not a developer_instructions override
160
+ // that would shadow the user's native project/developer configuration.
161
+ const argv = withoutCodexConfigOverride(launch.argv, "developer_instructions");
162
+ if (config.model !== undefined) {
163
+ const modelFlag = argv.findIndex((value, index) => (value === "--model" && argv[index + 1] === config.model));
156
164
  if (modelFlag < 0)
157
165
  throw new Error("Managed Codex launch lost its selected model.");
158
166
  argv.splice(modelFlag, 2);
159
167
  // App Server thread/start reads the model from resolved configuration;
160
168
  // the interactive --model shortcut is not inherited by new threads.
161
- argv.push("--config", `model=${tomlString(input.config.model)}`);
169
+ argv.push("--config", `model=${tomlString(config.model)}`);
162
170
  }
163
171
  return {
164
172
  ...launch,
165
- argv: [...argv, "app-server", "--stdio"],
166
- transport: "codex-app-server-stdio"
173
+ argv: [...argv, "app-server", "proxy"],
174
+ transport: "codex-app-server-proxy",
175
+ codexThread: codexThreadOptions(input, config),
176
+ codexDaemonStartArgs: [...input.agent.baseArgs, "app-server", "daemon", "start"]
167
177
  };
168
178
  }
169
179
  }
@@ -314,6 +324,50 @@ function tomlString(value) {
314
324
  throw new Error("Agent launch context cannot contain NUL bytes.");
315
325
  return JSON.stringify(value);
316
326
  }
327
+ function codexThreadOptions(input, config) {
328
+ const roots = [...new Set([
329
+ resolve(input.workspace),
330
+ ...(config.additionalDirectories ?? []).map((path) => resolve(path))
331
+ ])];
332
+ const threadConfig = {
333
+ // Yui has already scoped and authorized this exact managed workspace.
334
+ // Keep the trust override on this thread instead of mutating global config.
335
+ projects: { [resolve(input.workspace)]: { trust_level: "trusted" } },
336
+ ...(config.effort === undefined ? {} : { model_reasoning_effort: config.effort }),
337
+ ...(config.search === true ? { web_search: "live" } : {})
338
+ };
339
+ const permission = config.permission.strategy === "bypass"
340
+ ? { approvalPolicy: "never", sandbox: "danger-full-access" }
341
+ : config.permission.strategy === "configured"
342
+ ? {
343
+ ...(config.permission.approval === undefined
344
+ ? {}
345
+ : { approvalPolicy: config.permission.approval }),
346
+ ...(config.permission.sandbox === undefined
347
+ ? {}
348
+ : { sandbox: config.permission.sandbox })
349
+ }
350
+ : {};
351
+ return {
352
+ ...(config.model === undefined ? {} : { model: config.model }),
353
+ ...permission,
354
+ runtimeWorkspaceRoots: roots,
355
+ ...(Object.keys(threadConfig).length === 0 ? {} : { config: threadConfig })
356
+ };
357
+ }
358
+ function withoutCodexConfigOverride(argv, key) {
359
+ const filtered = [];
360
+ for (let index = 0; index < argv.length; index += 1) {
361
+ const argument = argv[index];
362
+ if ((argument === "--config" || argument === "-c")
363
+ && argv[index + 1]?.startsWith(`${key}=`) === true) {
364
+ index += 1;
365
+ continue;
366
+ }
367
+ filtered.push(argument);
368
+ }
369
+ return filtered;
370
+ }
317
371
  function sessionTitle(value) {
318
372
  if (typeof value !== "string" || value.includes("\0")) {
319
373
  throw new Error("Agent session title is invalid.");
@@ -126,6 +126,10 @@ export class ExecutorRegistry {
126
126
  && binding?.initialTurnDeliveryUnknownRunId === input.runId
127
127
  ? { turnDeliveryUnknownDuringLaunch: true }
128
128
  : {}),
129
+ ...(input.runId !== undefined
130
+ && binding?.initialTurnBusyRunId === input.runId
131
+ ? { turnBusyDuringLaunch: true }
132
+ : {}),
129
133
  ...(input.runId !== undefined
130
134
  && binding?.initialTurnRejectedRunId === input.runId
131
135
  ? { turnRejectedDuringLaunch: true }
@@ -292,7 +292,7 @@ export class FileRoleLaunchPlanner {
292
292
  const adapter = resolveAgentAdapter(binding.adapterId);
293
293
  const effectiveWorkspace = effective.workspace.root;
294
294
  const agentWorkspace = nativeAgentWorkspace(effective.workspace);
295
- if (adapter.id === "codex") {
295
+ if (adapter.id === "codex" && (owner.scope !== "task" || input.runId === undefined)) {
296
296
  const codexConfig = inspectCodexLaunchConfig({
297
297
  environment: launchEnvironment,
298
298
  workspace: agentWorkspace,
@@ -301,9 +301,7 @@ export class FileRoleLaunchPlanner {
301
301
  : undefined,
302
302
  trustWorkspace: true
303
303
  });
304
- assertCodexLaunchOverridesAvailable(codexConfig, owner.scope !== "task" || input.runId === undefined
305
- ? ["developerInstructions", "notify"]
306
- : ["developerInstructions"]);
304
+ assertCodexLaunchOverridesAvailable(codexConfig, ["developerInstructions", "notify"]);
307
305
  }
308
306
  const runtimeIsolation = input.runtimeIsolation === undefined
309
307
  ? undefined
@@ -424,20 +422,17 @@ export class FileRoleLaunchPlanner {
424
422
  }
425
423
  if (binding.adapterId === "codex") {
426
424
  // Global/interactive Codex sessions still use notify for presentation.
427
- // Managed Runs use the structured Driver Hook as their sole lifecycle
428
- // authority, avoiding two terminal channels for the same Turn.
425
+ // Managed Runs receive lifecycle facts through their ordinary App Server
426
+ // subscription, avoiding a second Hook channel for the same Turn.
429
427
  if (owner.scope !== "task" || input.runId === undefined) {
430
428
  args = addCodexSessionNotify(args, launchMode, this.#cliPath);
431
429
  }
432
- // Managed Codex runs use App Server lifecycle hooks and structured Turn
433
- // submission. No Run prompt is placed in argv or written as terminal input.
430
+ // Managed Codex Runs are ordinary shared-daemon threads. Their Session
431
+ // Manifest points at the Yui Skills; no Yui Hook config is installed.
434
432
  if (owner.scope === "task" && input.runId !== undefined) {
435
433
  if (managedRun === null || managedRun.status !== "active") {
436
434
  throw new Error(`Managed Codex Run is no longer active: ${input.runId}.`);
437
435
  }
438
- args = managedControl
439
- ? addCodexManagedLifecycleHooks(args, this.#cliPath)
440
- : addCodexLifecycleHooks(args, launchMode, this.#cliPath);
441
436
  }
442
437
  session = launchMode === "resume"
443
438
  ? readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective)
@@ -502,13 +497,16 @@ export class FileRoleLaunchPlanner {
502
497
  const providerAuthority = managedControl
503
498
  ? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId, input.mode)
504
499
  : undefined;
500
+ const providerOwnedTurn = managedControl && !carriesInitialTurn
501
+ ? this.#providerOwnedTurnForLaunch(owner.taskId, role.name, input.runId)
502
+ : undefined;
505
503
  const providerNativeSessionId = binding.adapterId === "claude"
506
504
  ? preallocatedNativeSessionId
507
505
  : resumeNativeSessionId;
508
506
  const initialTurn = carriesInitialTurn
509
507
  ? {
510
508
  attemptId: agentRunDeliveryReceiptId(managedRun),
511
- boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionTitle)
509
+ boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionContext.sessionManifestPath, sessionTitle)
512
510
  }
513
511
  : undefined;
514
512
  const providerControl = !managedControl
@@ -522,6 +520,13 @@ export class FileRoleLaunchPlanner {
522
520
  mode: "resume",
523
521
  nativeSessionId: requireText(providerNativeSessionId, "Managed Provider ensure native session id"),
524
522
  ...(sessionTitle === undefined ? {} : { sessionTitle }),
523
+ ...(managedCompiled.codexThread === undefined
524
+ ? {}
525
+ : { codexThread: managedCompiled.codexThread }),
526
+ ...(managedCompiled.codexDaemonStartArgs === undefined
527
+ ? {}
528
+ : { codexDaemonStartArgs: managedCompiled.codexDaemonStartArgs }),
529
+ ...(providerOwnedTurn === undefined ? {} : { ownedTurn: providerOwnedTurn }),
525
530
  authority: providerAuthority
526
531
  }
527
532
  : resumeNativeSessionId === undefined
@@ -535,6 +540,12 @@ export class FileRoleLaunchPlanner {
535
540
  ? {}
536
541
  : { nativeSessionId: providerNativeSessionId }),
537
542
  ...(sessionTitle === undefined ? {} : { sessionTitle }),
543
+ ...(managedCompiled.codexThread === undefined
544
+ ? {}
545
+ : { codexThread: managedCompiled.codexThread }),
546
+ ...(managedCompiled.codexDaemonStartArgs === undefined
547
+ ? {}
548
+ : { codexDaemonStartArgs: managedCompiled.codexDaemonStartArgs }),
538
549
  authority: providerAuthority,
539
550
  initialTurn
540
551
  }
@@ -546,6 +557,12 @@ export class FileRoleLaunchPlanner {
546
557
  mode: "resume",
547
558
  nativeSessionId: requireText(providerNativeSessionId, "Managed Provider resume native session id"),
548
559
  ...(sessionTitle === undefined ? {} : { sessionTitle }),
560
+ ...(managedCompiled.codexThread === undefined
561
+ ? {}
562
+ : { codexThread: managedCompiled.codexThread }),
563
+ ...(managedCompiled.codexDaemonStartArgs === undefined
564
+ ? {}
565
+ : { codexDaemonStartArgs: managedCompiled.codexDaemonStartArgs }),
549
566
  authority: providerAuthority,
550
567
  initialTurn
551
568
  };
@@ -601,6 +618,7 @@ export class FileRoleLaunchPlanner {
601
618
  const scopedLaunch = owner.scope === "task"
602
619
  ? this.#applyWorkspaceScope(owner.taskId, role, launch, workspaceOverride)
603
620
  : launch;
621
+ const ordinaryConversationLaunch = withCodexThreadEnvironment(scopedLaunch);
604
622
  return {
605
623
  role: {
606
624
  name: role.name,
@@ -608,7 +626,7 @@ export class FileRoleLaunchPlanner {
608
626
  ...(agentWorkspace === effectiveWorkspace ? {} : { cwd: agentWorkspace }),
609
627
  ...(owner.scope === "task" ? { status: role.status } : {})
610
628
  },
611
- launch: scopedLaunch,
629
+ launch: ordinaryConversationLaunch,
612
630
  session,
613
631
  ...(sessionTitle === undefined ? {} : { sessionTitle }),
614
632
  ...(carriesInitialTurn && input.runId !== undefined
@@ -654,6 +672,18 @@ export class FileRoleLaunchPlanner {
654
672
  }
655
673
  throw new Error(`Provider writer authority is unknown: ${taskId}/${roleName}.`);
656
674
  }
675
+ #providerOwnedTurnForLaunch(taskId, roleName, runId) {
676
+ const binding = this.store.getTaskRoleSessionSet(taskId, roleName)?.providerBinding;
677
+ if (binding === null || binding === undefined || binding.runId !== runId)
678
+ return undefined;
679
+ const turn = binding.turn;
680
+ if (turn === null || !["accepted", "running"].includes(turn.status))
681
+ return undefined;
682
+ if (turn.turnId === undefined) {
683
+ throw new Error(`Active Provider Turn has no native identity: ${taskId}/${roleName}.`);
684
+ }
685
+ return { attemptId: turn.attemptId, turnId: turn.turnId };
686
+ }
657
687
  #applyWorkspaceScope(taskId, role, launch, workspaceOverride) {
658
688
  const workspace = workspaceOverride
659
689
  ?? (role.name === "leader"
@@ -691,7 +721,7 @@ export class FileRoleLaunchPlanner {
691
721
  return selectEnvironment(source, names);
692
722
  }
693
723
  }
694
- function managedRunLaunchEnvelope(run, mode, title) {
724
+ function managedRunLaunchEnvelope(run, mode, sessionManifestPath, title) {
695
725
  const body = run.providerRetry?.state === "dispatching"
696
726
  ? serializeProviderRetryEnvelope({
697
727
  taskId: run.taskId,
@@ -709,9 +739,14 @@ function managedRunLaunchEnvelope(run, mode, title) {
709
739
  : mode === "resume" && run.pushedAt !== undefined
710
740
  ? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
711
741
  : serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
742
+ const guided = [
743
+ `Yui Session Manifest: ${sessionManifestPath}`,
744
+ "Read the manifest and its matching Role Skill before using the yui CLI for this Task.",
745
+ body
746
+ ].join("\n");
712
747
  return title === undefined
713
- ? body
714
- : prefixYuiTitleInput(body, title);
748
+ ? guided
749
+ : prefixYuiTitleInput(guided, title);
715
750
  }
716
751
  export function nativeAgentWorkspace(workspace) {
717
752
  return workspace.entries.length === 1
@@ -916,17 +951,26 @@ function addCodexLifecycleHooks(args, mode, cliPath) {
916
951
  }
917
952
  return [...args.slice(0, -2), ...managed, ...args.slice(-2)];
918
953
  }
919
- function addCodexManagedLifecycleHooks(args, cliPath) {
920
- if (args.length < 2 || args.at(-2) !== "app-server" || args.at(-1) !== "--stdio") {
921
- throw new Error("Managed Codex App Server launch shape is invalid.");
922
- }
923
- return [
924
- ...args.slice(0, -2),
925
- "--enable", "hooks",
926
- "--config", codexLifecycleHooksConfig(cliPath),
927
- "--dangerously-bypass-hook-trust",
928
- ...args.slice(-2)
929
- ];
954
+ function withCodexThreadEnvironment(launch) {
955
+ const control = launch.providerControl;
956
+ if (control?.adapterId !== "codex" || control.codexThread === undefined)
957
+ return launch;
958
+ const set = Object.fromEntries(Object.entries(launch.env).filter(([key]) => (key.startsWith("YUI_")
959
+ || ["TMPDIR", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_RUNTIME_DIR"]
960
+ .includes(key))));
961
+ return {
962
+ ...launch,
963
+ providerControl: {
964
+ ...control,
965
+ codexThread: {
966
+ ...control.codexThread,
967
+ config: {
968
+ ...(control.codexThread.config ?? {}),
969
+ shell_environment_policy: { set }
970
+ }
971
+ }
972
+ }
973
+ };
930
974
  }
931
975
  function readySession(agentId, adapterId, nativeSessionId, effective) {
932
976
  return {