@zq-silk/yui 0.13.6 → 0.13.8

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 (36) hide show
  1. package/ARCHITECTURE.md +52 -17
  2. package/README.md +32 -12
  3. package/dist/cli/updatePorts.js +4 -4
  4. package/dist/cli.js +1 -1
  5. package/dist/commands/taskCommands.js +1 -12
  6. package/dist/commands/taskContextCommand.js +1 -1
  7. package/dist/commands/taskExecutionCommands.js +0 -5
  8. package/dist/commands/taskOverviewCommand.js +5 -1
  9. package/dist/commands/taskRoleRuntimeStatus.js +0 -13
  10. package/dist/context/sessionBootstrapManifest.js +26 -23
  11. package/dist/controller/controller.js +0 -2
  12. package/dist/controller/fileSchedulerStoreAdapter.js +84 -78
  13. package/dist/executor/agentAdapter.js +6 -2
  14. package/dist/executor/agentExecutor.js +14 -28
  15. package/dist/executor/fileRoleLaunchPlanner.js +5 -5
  16. package/dist/lifecycle/exactRunTerminalization.js +1 -3
  17. package/dist/output/rolePresentation.js +0 -1
  18. package/dist/repository/taskWorkspacePreparer.js +0 -1
  19. package/dist/role/role.js +12 -20
  20. package/dist/runtime/agentHost.js +2 -2
  21. package/dist/runtime/builtinAgentDrivers.js +2 -1
  22. package/dist/runtime/index.js +1 -1
  23. package/dist/runtime/launchBroker.js +1 -1
  24. package/dist/runtime/providerRuntimeIdentity.js +8 -17
  25. package/dist/runtime/structuredProviderHost.js +172 -33
  26. package/dist/runtime/tmuxAdapters.js +9 -1
  27. package/dist/scheduler/activeRoleRunDelivery.js +2 -10
  28. package/dist/scheduler/activeTaskProgress.js +1 -4
  29. package/dist/scheduler/leaderWakeupProcessor.js +0 -5
  30. package/dist/scheduler/roleRunStall.js +3 -3
  31. package/dist/storage/migration/productionRegistry.js +143 -0
  32. package/dist/storage/taskStore.js +2 -2
  33. package/dist/web/webSnapshot.js +3 -0
  34. package/i18n/README.zh-CN.md +15 -7
  35. package/package.json +1 -1
  36. package/skills/yui-runtime/SKILL.md +5 -4
package/ARCHITECTURE.md CHANGED
@@ -1,9 +1,33 @@
1
1
  # Yui Architecture
2
2
 
3
- Yui is a local control plane for durable work across Projects and native Agent
4
- runtimes. The user talks to one Operator. The Operator routes each request to
5
- the right Project and Task; that Task's Leader owns decomposition, execution
6
- choice, review, integration, and completion.
3
+ Yui is a local control plane for intelligent Agents doing durable work across
4
+ Projects and native runtimes. The user talks to one Operator. The Operator
5
+ routes each request to the right Project and Task; that Task's Leader owns
6
+ decomposition, execution choice, review, integration, and completion.
7
+
8
+ ## Design principles
9
+
10
+ - **Agents own judgment.** Yui exposes current durable context and atomic
11
+ capabilities; the Operator, Leader, and Workers choose plans, execution
12
+ topology, sequencing, retry, and recovery from that context.
13
+ - **Core provides primitives, not a prescribed workflow.** Reads, messages,
14
+ bounded record transitions, workspace ownership, Session lifecycle, and
15
+ acceptance are composable operations. Project Skills and Knowledge provide
16
+ project-specific policy without adding core branches.
17
+ - **Durable intent outranks runtime continuity.** Tasks, WorkItems, Messages,
18
+ Decisions, results, Project Knowledge, and managed workspaces are authority.
19
+ Provider Sessions, transcripts, processes, and observations are execution
20
+ aids that may be resumed or replaced.
21
+ - **Trust explicit Agent actions.** Once identity, authority, and scope are
22
+ established, a valid Agent command is a semantic declaration. Core should not
23
+ reconstruct the same judgment through another status protocol.
24
+ - **Fail visibly and let the Agent adapt.** Preserve pending intent and return
25
+ actionable state. Add automated retry, recovery, leases, or fallback only for
26
+ a normal product path, a hard safety or data-integrity boundary, or a proven
27
+ failure whose cost justifies the machinery.
28
+ - **One question has one authority.** Projections and indexes may summarize
29
+ state, but scheduling and lifecycle decisions must not depend on independently
30
+ writable copies of the same fact.
7
31
 
8
32
  ## One outcome, Leader-chosen execution topology
9
33
 
@@ -254,21 +278,32 @@ Yui's current attachment, not exclusive ownership of the Provider thread. One
254
278
  Turn identifies one provider-native execution. Yui's authority epoch fences
255
279
  only Yui's own submissions and retries.
256
280
 
257
- Codex Task threads remain ordinary native sessions and can be inspected in
258
- Desktop. While their managed Role runtime is live, writes go through Yui's
259
- view/takeover boundary; stop Task execution before resuming the thread from a
260
- different client. If Yui observes a pre-existing active Turn during resume, it
261
- keeps its pending Run/message until that Turn settles. Global interactive entry
262
- remains a native session-lifecycle operation outside the Task delivery contract.
263
-
264
- Codex uses a Yui-owned direct `app-server` child to create or resume a normal
265
- thread. The process loads the selected native config profile; Role model,
266
- effort, permission, workspace, and shell settings are passed at
281
+ `AgentRun` is the single durable scheduling authority for a Role. Provider
282
+ runtime persistence has no independently writable current-Run field; a Turn's
283
+ Run id is correlation evidence for receipts and terminal observations only.
284
+ `TaskRole` likewise stores configuration and identity, not a writable runtime
285
+ status. CLI and Web status views derive activity from the active AgentRun and
286
+ add Session/Driver facts only as lifecycle and diagnostic detail.
287
+ `AgentHost` is the serialized consumer: while a native Turn is active, the next
288
+ AgentRun and mailbox batch remain durable and unsubmitted. When that Turn ends,
289
+ the Host makes the Conversation ready and the retained delivery continues.
290
+ This remains true when the Agent declared the old Run's semantic outcome before
291
+ the Provider emitted its terminal event.
292
+
293
+ Codex Task threads remain ordinary native sessions and can be opened and used
294
+ directly in Desktop. If a direct user Turn is active, Yui keeps its pending
295
+ Run/message until that Turn settles. Global interactive entry remains a native
296
+ session-lifecycle operation outside the Task delivery contract.
297
+
298
+ Codex establishes an App Server WebSocket through the byte-forwarding
299
+ `app-server proxy` to create or resume a normal thread on the shared daemon.
300
+ Role model, effort, permission, workspace, and shell settings are passed at
267
301
  `thread/start`/`thread/resume`, while the ordinary Task message points to the
268
302
  Session Manifest and matching Role Skill. Yui does not write either to global
269
- Codex config. The Agent Host owns the Provider process group, so Task execution
270
- stop can end the complete runtime and start can rebuild it without depending on
271
- the shared Codex daemon.
303
+ Codex config. A native Codex profile is rejected because it cannot be isolated
304
+ to one shared-daemon thread. The Agent Host owns only its proxy and WebSocket:
305
+ Task execution stop discards the Yui attachment without waiting for or changing
306
+ the daemon or native thread, and start creates a new attachment.
272
307
  Claude uses a persistent stream-json
273
308
  transport with exact user-message replay acknowledgement. In both cases, Yui
274
309
  records Turn intent before writing, accepts only exact Provider evidence, and
package/README.md CHANGED
@@ -2,7 +2,16 @@
2
2
 
3
3
  # Yui
4
4
 
5
- Yui is a local control plane for durable Codex and Claude work. It keeps control state and Project knowledge in inspectable JSON, lets tmux own native Agent terminals, and combines reusable Worker Profiles, Leader-owned delegation, explicit acceptance, and isolated Git worktrees for Project-backed Tasks.
5
+ Yui is a local control plane for intelligent Codex and Claude Agents. It keeps
6
+ user intent, Project knowledge, Tasks, handoffs, and results durable and
7
+ inspectable, while exposing small atomic capabilities for context, messaging,
8
+ delegation, workspaces, Sessions, review, and integration. Agents compose those
9
+ capabilities and decide how to plan, sequence, delegate, retry, and recover.
10
+
11
+ Yui deliberately does not turn Agent judgment into a deterministic workflow
12
+ engine. Its core owns durable identity, user authority, workspace isolation,
13
+ and atomic state changes. Provider Sessions and runtime observations support
14
+ execution and continuity, but they are not competing sources of Task truth.
6
15
 
7
16
  The current implementation restores the useful Role/Agent/session and CLI framework without restoring the later data-maintenance, lease, schedule, and recovery-ledger systems.
8
17
 
@@ -875,13 +884,14 @@ Provider conversations remain ordinary user conversations. Yui adds the Role
875
884
  Skill and Session Manifest pointer, then sends Task work through provider-native
876
885
  structured requests. Managed prompts are never delivered as terminal bytes.
877
886
 
878
- Codex uses a direct, Yui-owned `app-server` child to create or resume a normal
879
- thread. The thread remains visible in Codex, while Yui's view/takeover boundary
880
- is the supported way to interact with a live managed Role. Task execution stop
881
- terminates the Agent Host and its Provider process group; start creates a fresh
882
- owned runtime without depending on the shared Codex daemon. If the child
883
- disconnects, the Host may start a bounded replacement process and reconcile the
884
- exact owned Turn from native history.
887
+ Codex establishes the App Server WebSocket protocol through `app-server proxy`
888
+ to create or resume an ordinary thread on the shared native daemon. The thread
889
+ remains visible and directly usable in Desktop. Task execution stop terminates
890
+ Yui's Agent Host and proxy while leaving the daemon and thread untouched; start
891
+ creates a fresh proxy attachment.
892
+ If the proxy disconnects, the Host may attach a bounded replacement client and
893
+ reconcile the exact owned Turn from native history. A failed fresh attachment
894
+ is released instead of becoming a cleanup prerequisite for later Runs.
885
895
  Claude Code keeps its independent stream-json process with exact
886
896
  user-message replay acknowledgement. A timeout or uncertain write becomes
887
897
  `delivery-unknown` and is never automatically retried.
@@ -894,9 +904,19 @@ yui task role takeover <task-id> <role>
894
904
  yui task role release <task-id> <role>
895
905
  ```
896
906
 
897
- For a live managed Role, including Codex, these commands are the supported
898
- human-control boundary. Its ordinary native thread remains inspectable in
899
- Codex; stop Task execution before resuming it from a different client.
907
+ For an independently hosted Provider such as Claude, these commands are the
908
+ supported human-control boundary. A Codex Role uses an ordinary shared thread
909
+ and may be operated directly in Desktop; an active Desktop Turn creates bounded
910
+ backpressure for Yui rather than a failed Run.
911
+
912
+ AgentRun is the only durable Role scheduling state. Conversation state does not
913
+ carry a second current-Run pointer; each Provider Turn records a Run id only to
914
+ correlate its receipt and terminal event. If an Agent finishes a Yui Run before
915
+ the native Turn terminal arrives, the next mailbox batch may already become a
916
+ new AgentRun, but Agent Host keeps its input pending and submits it only after
917
+ the old Turn settles. TaskRole itself stores identity and desired launch
918
+ configuration, not runtime status; Role status shown by CLI/Web is derived from
919
+ the active AgentRun plus Session/Driver lifecycle facts.
900
920
 
901
921
  Global Operator and global Role sessions remain native interactive CLIs:
902
922
 
@@ -961,7 +981,7 @@ movement cannot conceal a workflow that is not advancing.
961
981
 
962
982
  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.
963
983
 
964
- 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. Its Yui-owned App Server process loads any selected Codex native config profile; model, effort, permission, workspace, and shell settings are supplied to `thread/start` or `thread/resume`, and the underlying Codex 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.
984
+ 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 are supplied to `thread/start` or `thread/resume` through the shared App Server daemon; a Codex native config profile is rejected because it cannot be isolated to one shared-daemon thread. The underlying Codex 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.
965
985
 
966
986
  ## Controller and failure handling
967
987
 
@@ -226,10 +226,10 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
226
226
  + `but the staged/verified artifact was ${staged.version}. Refusing to trust a `
227
227
  + "different build than the one that passed preflight.");
228
228
  }
229
- // Existing managed Sessions may have been created by a release that
230
- // embedded an exact CLI path/version in its wrapper. Convert those
231
- // authenticated, Manifest-referenced wrappers before the replacement
232
- // Controller starts so the update cannot strand a live Session.
229
+ // Existing managed Sessions may have been created by an earlier release.
230
+ // Retarget those authenticated, Manifest-referenced wrappers to the
231
+ // activated control plane before the replacement Controller starts so
232
+ // the update cannot strand a live Session.
233
233
  const sessionCliRefresh = run(activeBinary, ["--json", "internal", "session-cli-refresh"], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
234
234
  assertSpawnOk(sessionCliRefresh, "refresh managed Session CLI wrappers");
235
235
  // Retain the exact path used by both doctor and version verification. The
package/dist/cli.js CHANGED
@@ -348,7 +348,7 @@ export async function main() {
348
348
  || (process.env.YUI_SESSION_SCOPE === "global" && process.env.YUI_ROLE !== "operator")) {
349
349
  throw usageError("Managed Session CLI refresh may be run only by the user or global Operator.");
350
350
  }
351
- const result = refreshManagedSessionCliWrappers(home);
351
+ const result = refreshManagedSessionCliWrappers(home, currentInvocationControlPlane(home));
352
352
  emit(`Refreshed ${result.refreshed} legacy Session CLI wrapper(s); `
353
353
  + `${result.current} already current, ${result.skipped} skipped.`, false, result);
354
354
  return;
@@ -19,7 +19,7 @@ import { renderRoleDetails } from "../output/rolePresentation.js";
19
19
  import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js";
20
20
  import { cancelInputRequest } from "../input/inputRequest.js";
21
21
  import { retireExactActiveAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
22
- import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
22
+ import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole } from "../role/role.js";
23
23
  import { agentRunDeliveryReceiptId, createAgentRun, failAgentRun, withAgentRunContextSnapshot, yieldAgentRun } from "../run/agentRun.js";
24
24
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
25
25
  import { createRejectedYieldAttempt, rejectedYieldAttemptEventPayload, rejectedYieldAttemptFromTaskEvent, RUN_YIELD_REJECTED_EVENT } from "../run/rejectedYieldAttempt.js";
@@ -987,11 +987,6 @@ function retireTaskCommand(args, store, options) {
987
987
  tx.saveInputRequest(task.id, cancelInputRequest(request, `Task retired: ${summary}`, now));
988
988
  }
989
989
  }
990
- for (const role of tx.listRoles(task.id)) {
991
- if (role.status !== "idle") {
992
- tx.saveRole(task.id, updateRoleStatus(role, "idle", now));
993
- }
994
- }
995
990
  for (const mailbox of tx.listWorkMailboxes()) {
996
991
  if ((mailbox.target.kind === "task"
997
992
  || mailbox.target.kind === "role")
@@ -2374,7 +2369,6 @@ function dispatchWork(args, store, options) {
2374
2369
  const role = requireRole(tx, task.id, unboundRun.roleName);
2375
2370
  tx.saveAgentRun(runWithLineage);
2376
2371
  tx.saveActiveAgentRun(runWithLineage);
2377
- tx.saveRole(task.id, updateRoleStatus(role, "running", now));
2378
2372
  enqueueWork(tx, roleMailbox(task.id, role.name), "run-dispatched", now, [
2379
2373
  runRef(task.id, runWithLineage.id),
2380
2374
  workItemRef(task.id, item.id)
@@ -4499,7 +4493,6 @@ function retryRun(args, store, options) {
4499
4493
  });
4500
4494
  tx.saveAgentRun(created);
4501
4495
  tx.saveActiveAgentRun(created);
4502
- tx.saveRole(task.id, updateRoleStatus(role, "running", now));
4503
4496
  if (previous.workItemId !== undefined && retriedItemWithGroup !== null) {
4504
4497
  const item = retriedItemWithGroup;
4505
4498
  const workspace = tx.getWorkItemWorkspace(task.id, item.id);
@@ -6072,7 +6065,6 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
6072
6065
  const laneReviewer = requireRole(tx, taskId, unboundRun.roleName);
6073
6066
  tx.saveAgentRun(created);
6074
6067
  tx.saveActiveAgentRun(created);
6075
- tx.saveRole(taskId, updateRoleStatus(laneReviewer, "running", now));
6076
6068
  enqueueWork(tx, roleMailbox(taskId, laneReviewer.name), "review-requested", now, [
6077
6069
  runRef(taskId, created.id),
6078
6070
  ...(item === undefined ? [] : [workItemRef(taskId, item.id)])
@@ -7175,9 +7167,6 @@ function settleTaskExecutionForCompletion(store, taskId, roles, summary, now) {
7175
7167
  }
7176
7168
  store.saveTaskRoleSessionSet(sessions);
7177
7169
  }
7178
- if (role.status !== "idle") {
7179
- store.saveRole(taskId, updateRoleStatus(role, "idle", now));
7180
- }
7181
7170
  if (!cleanupRequired)
7182
7171
  continue;
7183
7172
  const target = runtimeLifecycleTarget({
@@ -249,7 +249,7 @@ export function runTaskContextCommand(args, store, currentTaskReviewCandidate =
249
249
  const effectiveSource = activeRun === undefined ? "Session" : "Run";
250
250
  const creation = [...events].reverse().find((event) => (event.type === "role.added" && event.payload.role === role.name));
251
251
  return [
252
- ` ${role.name} [${role.status}]: ${role.activeAgentId}/${binding.adapterId}`,
252
+ ` ${role.name} [${activeRun === undefined ? "idle" : "running"}]: ${role.activeAgentId}/${binding.adapterId}`,
253
253
  ` Desired: r${role.launchRevision}; Profile intent: ${role.defaultAccess}; Model: ${binding.config.model ?? "default"}; effort: ${binding.config.effort ?? "default"}; permission: ${binding.config.permission.strategy}`,
254
254
  ` Effective: ${effective === undefined
255
255
  ? "not started"
@@ -4,7 +4,6 @@ import { recordExecutionLaneResult } from "../execution/executionGroup.js";
4
4
  import { usageError } from "../errors/cliError.js";
5
5
  import { requestDurableJobCancel } from "../job/durableJob.js";
6
6
  import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewRound.js";
7
- import { updateRoleStatus } from "../role/role.js";
8
7
  import { failAgentRun } from "../run/agentRun.js";
9
8
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
10
9
  import { startTaskExecution, stopTaskExecution } from "../task/task.js";
@@ -83,10 +82,6 @@ export function stopTaskExecutionCommand(request, store, options = {}) {
83
82
  if (sessions !== null && sessions.inFlight !== null) {
84
83
  tx.saveTaskRoleSessionSet(terminalizeCurrentRun(sessions, now));
85
84
  }
86
- const role = tx.getRole(task.id, roleName);
87
- if (role !== null && role.status !== "idle") {
88
- tx.saveRole(task.id, updateRoleStatus(role, "idle", now));
89
- }
90
85
  }
91
86
  // A stop is allowed to discard stale pointer projections even when their
92
87
  // historical Runs are already terminal.
@@ -78,7 +78,11 @@ function buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy) {
78
78
  const legacyNext = deriveNextAction(task, brief, workItems, openInputRequests, blockers, pendingWakeup);
79
79
  const leader = {
80
80
  role: "leader",
81
- roleStatus: leaderRole?.status ?? "missing",
81
+ roleStatus: leaderRole === null
82
+ ? "missing"
83
+ : agentRuns.some((run) => run.roleName === "leader" && run.status === "active")
84
+ ? "running"
85
+ : roleSessions.find((session) => session.roleName === "leader")?.status ?? "idle",
82
86
  summary: brief?.leaderSummary ?? null,
83
87
  currentFocus: brief?.currentFocus ?? null,
84
88
  updatedAt: brief?.updatedAt ?? null,
@@ -85,7 +85,6 @@ export function renderTaskRoleRuntimeStatus(status) {
85
85
  ? "-"
86
86
  : status.launchDrift ? "pending next launch" : "none"}`,
87
87
  ` Run/session ${status.runSessionDrift ? "snapshot mismatch" : "snapshot consistent"}`,
88
- ` Role state ${status.role.status}`,
89
88
  ` Active work ${activeWork}`,
90
89
  ` Active run ${activeRun}`,
91
90
  ...(lastRun === undefined ? [] : [` Last run ${lastRun}`]),
@@ -254,9 +253,6 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
254
253
  healthReason: "the native Session is unbound; verified runtime cleanup is pending"
255
254
  };
256
255
  }
257
- if (role.status === "failed" || role.status === "exited") {
258
- return { health: "failed", healthReason: `persisted Role state is ${role.status}` };
259
- }
260
256
  if (nativeSession?.status === "broken") {
261
257
  // Issue 09: a broken Session only fails a live Run. When the last Run
262
258
  // already yielded, the Session death is a lifecycle event, not a Run
@@ -287,12 +283,6 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
287
283
  };
288
284
  }
289
285
  if (activeRun !== null) {
290
- if (role.status !== "running") {
291
- return {
292
- health: "needs-attention",
293
- healthReason: `the active Run conflicts with persisted Role state ${role.status}`
294
- };
295
- }
296
286
  if (activeRun.deliveredAt !== undefined && tmux.state !== "running") {
297
287
  return { health: "needs-attention", healthReason: "the delivered active Run has no live tmux pane" };
298
288
  }
@@ -339,9 +329,6 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
339
329
  }
340
330
  }
341
331
  }
342
- if (activeRun === null && role.status === "running") {
343
- return { health: "needs-attention", healthReason: "the Role is running without an active Run" };
344
- }
345
332
  if (nativeSession?.status === "running" && tmux.state !== "running") {
346
333
  return { health: "needs-attention", healthReason: "the native session is running without a live tmux pane" };
347
334
  }
@@ -1,15 +1,18 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { chmodSync, existsSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
- import { exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
4
+ import { exactControlPlaneCommandPrefix, exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
5
5
  import { writeTextFileAtomically } from "../storage/durableFile.js";
6
6
  import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
7
7
  export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
8
- const ORDINARY_SESSION_CLI = [
9
- "#!/bin/sh",
10
- "exec yui \"$@\"",
11
- ""
12
- ].join("\n");
8
+ const ordinarySessionCli = ["#!/bin/sh", "exec yui \"$@\"", ""].join("\n");
9
+ function renderSessionCli(controlPlane) {
10
+ return [
11
+ "#!/bin/sh",
12
+ `exec ${exactControlPlaneCommandPrefix(controlPlane)} \"$@\"`,
13
+ ""
14
+ ].join("\n");
15
+ }
13
16
  /** Read back one immutable Session Manifest and verify its content digest. */
14
17
  export function readSessionBootstrapManifest(path) {
15
18
  const source = resolve(path);
@@ -84,10 +87,11 @@ export function materializeSessionBootstrap(input) {
84
87
  const controlDigest = exactControlPlaneDigest(input.controlPlane);
85
88
  const descriptorPath = resolve(join(home, "runtime", "control-plane", `${controlDigest}.json`));
86
89
  writeImmutableText(descriptorPath, `${serializeExactDescriptor(input.controlPlane)}\n`);
87
- // Session identity is carried by the immutable Manifest and durable Role/
88
- // Run fences. Resolve the ordinary CLI on every invocation so package or
89
- // release upgrades do not invalidate a still-current native Session.
90
- const sessionCliContent = ORDINARY_SESSION_CLI;
90
+ // Provider command runners may rebuild PATH independently of the managed
91
+ // process environment. Keep the Session entry point deterministic; the
92
+ // existing continuity preflight accepts compatible package replacement at
93
+ // this path without treating package version as Session identity.
94
+ const sessionCliContent = renderSessionCli(input.controlPlane);
91
95
  const sessionCliDigest = digest(sessionCliContent);
92
96
  const sessionCliPath = resolve(join(home, "runtime", "session-cli", `yui-${sessionCliDigest}.sh`));
93
97
  writeImmutableText(sessionCliPath, sessionCliContent);
@@ -126,11 +130,11 @@ export function materializeSessionBootstrap(input) {
126
130
  roleProfileRef: { digest: profileDigest, path: roleProfilePath },
127
131
  contextProtocol: input.owner.scope === "global"
128
132
  ? {
129
- loadCommand: "yui session context \"$YUI_ROLE\" --json"
133
+ loadCommand: "\"$YUI_SESSION_CLI\" session context \"$YUI_ROLE\" --json"
130
134
  }
131
135
  : {
132
- loadCommand: "yui task run context \"$YUI_TASK_ID/<run-id>\" --json",
133
- expandCommand: "yui task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json"
136
+ loadCommand: "\"$YUI_SESSION_CLI\" task run context \"$YUI_TASK_ID/<run-id>\" --json",
137
+ expandCommand: "\"$YUI_SESSION_CLI\" task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json"
134
138
  }
135
139
  };
136
140
  const manifest = Object.freeze({ ...body, digest: digest(body) });
@@ -145,14 +149,13 @@ export function materializeSessionBootstrap(input) {
145
149
  });
146
150
  }
147
151
  /**
148
- * Converts wrappers produced before the protocol-compatible Session CLI to an
149
- * ordinary `yui` invocation. Only a valid Session Manifest may nominate a
150
- * wrapper, and only the exact legacy two-line wrapper shape is changed. The
151
- * Manifest and its frozen descriptor stay immutable and continue to
152
- * authenticate the Session; repeated refreshes are no-ops.
152
+ * Retargets known managed wrappers to the current resolved control plane after
153
+ * a compatible update. Only a valid Session Manifest may nominate a wrapper,
154
+ * and only Yui's ordinary or exact two-line wrapper shapes are changed.
153
155
  */
154
- export function refreshManagedSessionCliWrappers(homeInput) {
156
+ export function refreshManagedSessionCliWrappers(homeInput, controlPlane) {
155
157
  const home = resolve(homeInput);
158
+ const currentSessionCli = renderSessionCli(controlPlane);
156
159
  const manifestDirectory = resolve(join(home, "runtime", "session-manifests"));
157
160
  const sessionCliDirectory = resolve(join(home, "runtime", "session-cli"));
158
161
  if (!existsSync(manifestDirectory)) {
@@ -189,21 +192,21 @@ export function refreshManagedSessionCliWrappers(homeInput) {
189
192
  continue;
190
193
  }
191
194
  const content = readFileSync(wrapperPath, "utf8");
192
- if (content === ORDINARY_SESSION_CLI) {
195
+ if (content === currentSessionCli) {
193
196
  current += 1;
194
197
  continue;
195
198
  }
196
- if (!isLegacyExactSessionCli(content)) {
199
+ if (content !== ordinarySessionCli && !isExactSessionCli(content)) {
197
200
  skipped += 1;
198
201
  continue;
199
202
  }
200
- writeTextFileAtomically(wrapperPath, ORDINARY_SESSION_CLI);
203
+ writeTextFileAtomically(wrapperPath, currentSessionCli);
201
204
  chmodSync(wrapperPath, 0o700);
202
205
  refreshed += 1;
203
206
  }
204
207
  return Object.freeze({ refreshed, current, skipped });
205
208
  }
206
- function isLegacyExactSessionCli(content) {
209
+ function isExactSessionCli(content) {
207
210
  return /^#!\/bin\/sh\nexec [^\n]+ '--yui-control' '[a-f0-9]{64}' "\$@"\n$/u.test(content);
208
211
  }
209
212
  function writeImmutableText(path, content) {
@@ -96,8 +96,6 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
96
96
  }
97
97
  const newlyIdleBusyTaskIds = new Set(initialWakeupResults.flatMap((result) => (result.reason === "busy"
98
98
  && store.getActiveAgentRun(result.taskId, "leader") === null
99
- && (typeof store.hasInFlightTurn !== "function"
100
- || !store.hasInFlightTurn(result.taskId, "leader"))
101
99
  ? [result.taskId]
102
100
  : [])));
103
101
  // Phase-one Leader Runs did not exist at the pass's liveness boundary.