@zq-silk/yui 0.8.1 → 0.8.2

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 (38) hide show
  1. package/ARCHITECTURE.md +27 -28
  2. package/README.md +35 -46
  3. package/dist/cli/commandCatalog.js +49 -14
  4. package/dist/cli/interactionPolicy.js +4 -10
  5. package/dist/cli/invocationRouter.js +2 -1
  6. package/dist/cli.js +73 -21
  7. package/dist/commands/taskCommands.js +108 -53
  8. package/dist/controller/fileSchedulerStoreAdapter.js +389 -70
  9. package/dist/controller/resourceInventory.js +9 -5
  10. package/dist/controller/runtime.js +80 -7
  11. package/dist/controller/runtimeLaunchCoordinator.js +18 -78
  12. package/dist/controller/structuredProviderObservation.js +273 -0
  13. package/dist/executor/agentAdapter.js +40 -0
  14. package/dist/executor/agentExecutor.js +31 -7
  15. package/dist/executor/executorRegistry.js +11 -49
  16. package/dist/executor/fileRoleLaunchPlanner.js +115 -37
  17. package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
  18. package/dist/run/agentRun.js +2 -2
  19. package/dist/runtime/agentHost.js +767 -158
  20. package/dist/runtime/builtinAgentDrivers.js +1 -5
  21. package/dist/runtime/codexAppServerRuntime.js +67 -60
  22. package/dist/runtime/exactControlPlane.js +7 -2
  23. package/dist/runtime/index.js +6 -2
  24. package/dist/runtime/launchBroker.js +30 -8
  25. package/dist/runtime/providerAuthorityFence.js +24 -0
  26. package/dist/runtime/providerControl.js +63 -0
  27. package/dist/runtime/providerRecoveryDecision.js +55 -0
  28. package/dist/runtime/providerRuntimeIdentity.js +269 -19
  29. package/dist/runtime/runtimeBinding.js +20 -11
  30. package/dist/runtime/structuredProviderHost.js +476 -0
  31. package/dist/runtime/tmuxAdapters.js +143 -42
  32. package/dist/scheduler/activeRoleRunDelivery.js +206 -120
  33. package/dist/scheduler/leaderWakeupProcessor.js +141 -16
  34. package/dist/storage/migration/productionRegistry.js +111 -0
  35. package/dist/storage/taskStore.js +1 -1
  36. package/dist/tmux/tmuxManager.js +1 -1
  37. package/i18n/README.zh-CN.md +11 -8
  38. package/package.json +1 -1
package/ARCHITECTURE.md CHANGED
@@ -201,34 +201,33 @@ context.
201
201
 
202
202
  ## Runtime ownership
203
203
 
204
- tmux owns Agent process lifetimes and observable output. The Controller owns
205
- mailbox delivery, wakeups, Role liveness, and reconciliation. Task attachment
206
- surfaces only attach to an existing pane and cannot create, resume, wake, or
207
- deliver to a managed runtime. Global interactive entry remains an explicit
208
- session-lifecycle operation.
209
-
210
- Task observation is read-only by default. Explicit write access publishes a
211
- Role-scoped tmux lease before revalidating durable Run state. The managed host
212
- also checks that lease before planning or process creation, so either the Run
213
- claim or the writer lease wins and they never share a pane. Writer contention
214
- is transient backpressure rather than a delivery failure: it does not consume
215
- bounded delivery retries, and lease release signals only existing durable work.
216
- Global interactive entry uses the same mechanism at tmux-host scope and
217
- automatically falls back to read-only when another writer already exists.
218
-
219
- Managed Task Claude execution is process-per-Run: the exact Run input is a
220
- stream-json stdin frame submitted at process launch, while native session IDs
221
- carry conversation continuity across processes. The lifecycle binding records
222
- the exact Run submitted at launch. A Controller restart may recover only the
223
- same reserved launch/Run as uncertain until its Provider Hook arrives; a newly
224
- reserved Run cannot reuse an older live Role pane, so its provisional launch
225
- is released, the old owner is fenced through the durable cleanup lane, and the
226
- same Run is retried only after cleanup; pending cleanup prevents a successor
227
- generation from starting early.
228
- Terminal key injection is therefore an interactive compatibility mechanism,
229
- not a managed Claude delivery protocol. Operator and Leader Sessions remain
230
- fixed Task/global Roles; Task Worker Sessions are selected through Role Agent
231
- bindings.
204
+ tmux owns the persistent Agent Host lifetime and observable output. The
205
+ Controller owns mailbox delivery, wakeups, Role liveness, recovery decisions,
206
+ and a durable single-writer authority epoch. The Agent Host owns one structured
207
+ Provider child process and mirrors its output to the pane. Managed prompts are
208
+ never terminal bytes: both Controller and human takeover input become fenced
209
+ Provider-native Turn requests.
210
+
211
+ Run, Conversation, Activation, and Turn identities are independent. A
212
+ Conversation can span Runs and Provider processes; one Activation identifies
213
+ one live process; one Turn identifies one pre-recorded input attempt. A live
214
+ Activation is retained when a later Run reuses the Conversation. Process exit
215
+ ends that Activation, and a resumed process receives a new Activation and a
216
+ higher authority epoch.
217
+
218
+ Task observation uses `task role view`. Explicit takeover requires a live
219
+ managed Run, atomically transfers authority to a human holder, synchronizes the
220
+ same fence to the Host, and only then exposes the PTY input gateway. Detach
221
+ releases authority; `task role release` is an idempotent repair path even after
222
+ the Run has ended. Global interactive entry remains a native session-lifecycle
223
+ operation outside this managed Provider contract.
224
+
225
+ Codex uses a persistent App Server JSON-RPC transport. Claude uses a persistent
226
+ stream-json transport with exact user-message replay acknowledgement. In both
227
+ cases, Yui records Turn intent before writing, accepts only exact Provider
228
+ evidence, and maps an uncertain write to `delivery-unknown` without automatic
229
+ resubmission. Conversation replacement requires exact missing evidence and no
230
+ unsettled input, Turn, Activation, or writer authority.
232
231
 
233
232
  Role desired revisions and Run/Session effective snapshots keep configuration
234
233
  history explicit. Resume compares the complete effective snapshot and
package/README.md CHANGED
@@ -659,55 +659,46 @@ yui task complete <task-id> --summary-file delivery.txt --refresh-remote
659
659
  yui task reopen <task-id>
660
660
  ```
661
661
 
662
- Completed Tasks reject messages, dispatch, enter, retry, and late yields until explicitly reopened, while retaining Task main for inspection or integration. Every isolated WorkItem worktree must be explicitly cleaned as integrated or abandoned before archive; that cleanup also removes its managed branch. Archive requires `--integrated` or `--abandon` to state the Task main outcome and is allowed only after Task main is clean. It removes managed worktrees but retains Task and WorkItem records. The Task main branch is retained as a recovery artifact instead of being silently deleted.
662
+ Completed Tasks reject messages, dispatch, Provider authority changes, retry, and late yields until explicitly reopened, while retaining Task main for inspection or integration. Every isolated WorkItem worktree must be explicitly cleaned as integrated or abandoned before archive; that cleanup also removes its managed branch. Archive requires `--integrated` or `--abandon` to state the Task main outcome and is allowed only after Task main is clean. It removes managed worktrees but retains Task and WorkItem records. The Task main branch is retained as a recovery artifact instead of being silently deleted.
663
663
  Task lifecycle completion/selection only suggests valid source states: Draft for activate, active for complete, and completed for reopen.
664
664
 
665
665
  ## Sessions and tmux
666
666
 
667
- tmux owns Agent process lifetimes and their observable output. Global Operator
668
- and global Role sessions remain native interactive CLIs. A managed Task Claude
669
- Run instead starts one finite Claude process with `--print`, stream-json input
670
- and stream-json output. Yui writes the exact Run prompt as one newline-delimited
671
- JSON user frame on stdin, drains output concurrently, and carries native
672
- continuity with Claude's session ID. Startup and delivery therefore never
673
- depend on a TUI composer, readiness glyph, paste delay, or a synthetic Enter
674
- key. Codex keeps its adapter-native launch-prompt and structured callback path.
675
-
676
- `task enter` and `task role enter` are pure attachments to an existing Task
677
- Role pane. They do not start the Controller, prepare a workspace, create or
678
- resume an Agent, wake a Role, or deliver input. Task attachments default to
679
- `--read-only`; `--read-write` is explicit and is rejected while that Role owns
680
- an active managed Run, a managed Claude process is still exiting, or another
681
- writer owns the same pane. A read-write attach first publishes a Role-scoped
682
- tmux writer lease and then revalidates durable Run state, closing the race with
683
- Controller launch. While the lease exists, managed delivery for that Role is
684
- paused without consuming its bounded delivery retries; detach releases the
685
- lease and signals only already-durable Role work for reconsideration. Other
686
- Roles in the same Task continue independently. Before any attach Yui closes
687
- readline, leaves raw mode, pauses its stdin, and synchronously hands the terminal
688
- to tmux. The attach uses the real outer terminal capabilities and a clean alternate
689
- screen; mouse scrolling stays in the Agent pane's
690
- 100,000-line tmux history instead of mixing with earlier shell or IDE terminal
691
- history. A read-write attachment exposes whatever native interaction the
692
- existing pane supports, but it is never part of managed startup or delivery.
693
-
694
- tmux fixes a pane's history capacity when that pane is created. Roles created
695
- before this limit was configured keep their earlier capacity; Yui warns on
696
- Terminal attach and in Web so the user can exit and re-enter that Role once to
697
- create a 100,000-line pane while retaining the native Agent conversation.
698
-
699
- Global interactive entry remains writable when no writer exists and
700
- automatically downgrades to read-only when another writer is present; global
701
- Web keeps one writer per tmux session. Task Web is always read-only. Task CLI
702
- entry is read-only unless `--read-write` is requested, preventing observation
703
- from changing Agent execution.
667
+ Managed Task Agents use the [hybrid Provider runtime](docs/provider-runtime.md).
668
+ The Controller and Agent Host send Provider-native structured requests; tmux
669
+ and PTY keep the Host alive, show output, and provide an explicit human input
670
+ gateway. Managed prompts are never delivered as terminal bytes.
671
+
672
+ Codex uses a persistent App Server JSON-RPC process. Claude Code uses a
673
+ persistent stream-json process with exact user-message replay acknowledgement.
674
+ Both follow the same Conversation, Activation, Turn, and authority-epoch
675
+ contract. A timeout or uncertain write becomes `delivery-unknown` and is never
676
+ automatically retried.
677
+
678
+ Task Role observation and takeover are explicit:
679
+
680
+ ```sh
681
+ yui task role view <task-id> <role>
682
+ yui task role takeover <task-id> <role>
683
+ yui task role release <task-id> <role>
684
+ ```
685
+
686
+ `view` is read-only. `takeover` transfers the durable writer authority to the
687
+ human before enabling the Host PTY gateway and requires a live managed Run;
688
+ detach returns it to the Controller. `release` remains available without a live
689
+ Run and idempotently repairs a stranded or partially synchronized takeover.
690
+ Authority cannot transfer while a Provider Turn or input delivery is unsettled.
691
+
692
+ Global Operator and global Role sessions remain native interactive CLIs:
704
693
 
705
694
  ```sh
706
695
  yui session enter <global-role>
707
- yui task enter <task-id> [role] [--read-only | --read-write]
708
- yui task role enter <task-id> <role> [--read-only | --read-write]
709
696
  ```
710
697
 
698
+ tmux fixes a pane's history capacity when that pane is created. Existing panes
699
+ retain their configured capacity; managed runtime output remains observable in
700
+ the Agent Host pane without becoming lifecycle or acknowledgement evidence.
701
+
711
702
  Each Role, including a Task-bound Worker instance, can bind multiple configured Agents, has one active Agent, and keeps
712
703
  a separate native session per Agent binding. Operator narrows this to at most
713
704
  one Agent per adapter—for example, one Codex and one Claude—so its bindings are
@@ -729,12 +720,10 @@ snapshot instead of applying desired drift as a hot change.
729
720
 
730
721
  Use `yui config role unbind <global-role> <agent-id>` or `yui task role unbind <task-id> <role> <agent-id>` to retire a dormant binding. The active binding and any non-stopped native session are rejected; a stopped session record is removed atomically with the binding.
731
722
 
732
- Claude session IDs are preallocated at launch. Every managed Task Claude Run
733
- uses a new finite process; resume starts a new process against the fixed native
734
- session instead of reusing an interactive pane. Codex discovers its native
735
- thread identity from structured lifecycle events. Managed Task Runs use one
736
- Agent Driver Hook ingress for both CLIs. Global interactive Codex sessions may
737
- still use its structured `notify` callback for conversation presentation.
723
+ Claude session IDs are preallocated at launch. Codex discovers its native
724
+ thread identity from App Server responses. Managed Task Runs use structured
725
+ Provider observations for both CLIs. Global interactive Codex sessions may
726
+ still use its `notify` callback for conversation presentation.
738
727
 
739
728
  Automated lifecycle and delivery decisions use structured Hook payloads,
740
729
  persisted identities, usage snapshots, tmux process state, receipts, and pane
@@ -513,7 +513,8 @@ const taskChildren = [
513
513
  name: "role",
514
514
  summary: "Manage Roles within a Task.",
515
515
  sections: [{ id: "manage", title: "Commands", entries: [
516
- "add", "list", "status", "show", "update", "remove", "bind", "unbind", "reset", "enter"
516
+ "add", "list", "status", "show", "update", "remove", "bind", "unbind", "reset",
517
+ "view", "takeover", "release"
517
518
  ] }],
518
519
  children: [
519
520
  {
@@ -548,10 +549,19 @@ const taskChildren = [
548
549
  options: ["--reason"]
549
550
  },
550
551
  {
551
- name: "enter",
552
- summary: "Attach to an existing Task Role session without starting it.",
553
- usage: "yui task role enter <task> <role> [--read-only | --read-write]",
554
- options: ["--read-only", "--read-write"]
552
+ name: "view",
553
+ summary: "Attach read-only to a managed Provider presentation surface.",
554
+ usage: "yui task role view <task> <role>"
555
+ },
556
+ {
557
+ name: "takeover",
558
+ summary: "Acquire Provider writer authority and enter the PTY input gateway.",
559
+ usage: "yui task role takeover <task> <role>"
560
+ },
561
+ {
562
+ name: "release",
563
+ summary: "Return stranded human Provider authority to the Controller.",
564
+ usage: "yui task role release <task> <role>"
555
565
  }
556
566
  ]
557
567
  },
@@ -675,7 +685,7 @@ const taskChildren = [
675
685
  {
676
686
  name: "run",
677
687
  summary: "Inspect and control Task Role Agent Runs.",
678
- sections: [{ id: "manage", title: "Commands", entries: ["list", "show", "retry", "settle", "recover", "yield", "checkpoint"] }],
688
+ sections: [{ id: "manage", title: "Commands", entries: ["list", "show", "retry", "settle", "recover", "yield", "context", "checkpoint"] }],
679
689
  children: [
680
690
  { name: "list", summary: "List Runs for a work item.", usage: "yui task run list <task>/<work>" },
681
691
  {
@@ -706,6 +716,28 @@ const taskChildren = [
706
716
  options: ["--summary", "--summary-file"],
707
717
  fileOptions: ["--summary-file"]
708
718
  },
719
+ {
720
+ name: "context",
721
+ summary: "Load the exact authorized Run context.",
722
+ usage: "yui task run context <task>/<run> [--json]",
723
+ executable: true,
724
+ hidden: true,
725
+ sections: [{ id: "load", title: "Commands", entries: ["expand", "delta"] }],
726
+ children: [
727
+ {
728
+ name: "expand",
729
+ summary: "Expand one authorized Run context reference.",
730
+ usage: "yui task run context expand <task>/<run> <ref-id> [--mode full]",
731
+ options: ["--mode"]
732
+ },
733
+ {
734
+ name: "delta",
735
+ summary: "Load authorized Run context changes after a cursor.",
736
+ usage: "yui task run context delta <task>/<run> --after <cursor>",
737
+ options: ["--after"]
738
+ }
739
+ ]
740
+ },
709
741
  {
710
742
  name: "checkpoint",
711
743
  summary: "Record durable progress for a long-running Agent Run.",
@@ -940,12 +972,6 @@ const taskChildren = [
940
972
  { name: "show", summary: "Show one ChangeSet.", usage: "yui task change-set show <task>/<change-set>" }
941
973
  ]
942
974
  },
943
- {
944
- name: "enter",
945
- summary: "Attach to an existing Task Role, defaulting to Leader and read-only.",
946
- usage: "yui task enter <task> [role] [--read-only | --read-write]",
947
- options: ["--read-only", "--read-write"]
948
- }
949
975
  ];
950
976
  export const ROOT_COMMAND = buildNode({
951
977
  name: "yui",
@@ -1319,7 +1345,7 @@ export const ROOT_COMMAND = buildNode({
1319
1345
  summary: "Manage Tasks, WorkItems, Agent Runs, and integration.",
1320
1346
  sections: [
1321
1347
  { id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "base", "update", "activate", "complete", "reopen", "retire", "list", "show", "context", "next-action", "archive", "rebuild", "history", "replace", "reconcile"] },
1322
- { id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "publication", "work", "run", "review", "integration", "role", "enter", "overlap", "change-set"] },
1348
+ { id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "publication", "work", "run", "review", "integration", "role", "overlap", "change-set"] },
1323
1349
  { id: "knowledge", title: "Task Knowledge", entries: ["brief", "decision", "milestone", "event", "continuation", "wake"] }
1324
1350
  ],
1325
1351
  children: taskChildren
@@ -1359,8 +1385,17 @@ export const ROOT_COMMAND = buildNode({
1359
1385
  name: "internal",
1360
1386
  summary: "Internal Yui callbacks.",
1361
1387
  hidden: true,
1362
- sections: [{ id: "callbacks", title: "Callbacks", entries: ["session-notify", "runtime-hook"] }],
1388
+ sections: [{
1389
+ id: "callbacks",
1390
+ title: "Callbacks",
1391
+ entries: ["session-notify", "runtime-hook", "agent-host"]
1392
+ }],
1363
1393
  children: [
1394
+ {
1395
+ name: "agent-host",
1396
+ summary: "Run the persistent structured Provider host.",
1397
+ usage: "yui internal agent-host <launch-id> <ticket>"
1398
+ },
1364
1399
  {
1365
1400
  name: "session-notify",
1366
1401
  summary: "Record a structured native session notification.",
@@ -361,8 +361,8 @@ export const INTERACTION_POLICIES = Object.freeze([
361
361
  trailingOptions: { "--reason": "value" },
362
362
  confirmation: { action: "Reset Task Role Session", targetArgumentIndex: 4 }
363
363
  },
364
- {
365
- commandPath: ["task", "role", "enter"],
364
+ ...["view", "takeover", "release"].map((command) => ({
365
+ commandPath: ["task", "role", command],
366
366
  selectors: [
367
367
  { argumentIndex: 3, entity: "task", provider: "tasks", actionTarget: true },
368
368
  {
@@ -372,9 +372,8 @@ export const INTERACTION_POLICIES = Object.freeze([
372
372
  dependsOn: 3,
373
373
  actionTarget: true
374
374
  }
375
- ],
376
- trailingOptions: { "--read-only": "flag", "--read-write": "flag" }
377
- },
375
+ ]
376
+ })),
378
377
  {
379
378
  commandPath: ["task", "work", "create"],
380
379
  selectors: [
@@ -534,11 +533,6 @@ export const INTERACTION_POLICIES = Object.freeze([
534
533
  ? { trailingOptions: { "--reason": "value" } }
535
534
  : {})
536
535
  })),
537
- {
538
- commandPath: ["task", "enter"],
539
- selectors: [{ argumentIndex: 2, entity: "task", provider: "tasks", actionTarget: true }],
540
- trailingOptions: { "--read-only": "flag", "--read-write": "flag" }
541
- },
542
536
  {
543
537
  commandPath: ["jobs", "retry"],
544
538
  selectors: [{ argumentIndex: 2, entity: "job", provider: "jobs", actionTarget: true }]
@@ -32,7 +32,8 @@ function resolveExecutionPath(args) {
32
32
  const child = findChild(node, args[index] ?? "");
33
33
  const internalExecutable = child !== undefined && ((node === ROOT_COMMAND && child.name === "internal")
34
34
  || (node.path.join(" ") === "yui config completion" && child.name === "candidates")
35
- || (node.path.join(" ") === "yui task run" && child.name === "checkpoint")
35
+ || (node.path.join(" ") === "yui task run"
36
+ && (child.name === "checkpoint" || child.name === "context"))
36
37
  || (node.path.join(" ") === "yui controller"
37
38
  && (child.name === "identity" || child.name === "live-identity")));
38
39
  if (child === undefined || (child.hidden && !internalExecutable)) {
package/dist/cli.js CHANGED
@@ -36,7 +36,7 @@ import { runResourcesCommand } from "./commands/resourcesCommands.js";
36
36
  import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
37
37
  import { runProjectCommand } from "./commands/projectCommands.js";
38
38
  import { runProfileCommand } from "./commands/profileCommands.js";
39
- import { dispatchPreparedReviewRound, failPendingReviewRound, assertTaskRoleWritableAttachAvailable, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, validateTaskArchiveRequest } from "./commands/taskCommands.js";
39
+ import { dispatchPreparedReviewRound, failPendingReviewRound, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, validateTaskArchiveRequest } from "./commands/taskCommands.js";
40
40
  import { taskActor } from "./commands/taskActor.js";
41
41
  import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
42
42
  import { runTaskChangeSetCommand } from "./commands/taskChangeSetCommands.js";
@@ -61,9 +61,9 @@ import { runSessionNotifyCommand } from "./controller/sessionNotify.js";
61
61
  import { openSchedulerTelemetry } from "./telemetry/telemetryWiring.js";
62
62
  import { runRuntimeObservationHookCommand } from "./controller/runtimeObservationHook.js";
63
63
  import { buildDoctorReport, renderDoctor, runDoctorCommand } from "./doctor/doctor.js";
64
- import { agentNotFound, CliError, usageError } from "./errors/cliError.js";
64
+ import { agentNotFound, CliError, runtimeError, usageError } from "./errors/cliError.js";
65
65
  import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
66
- import { runAgentHost } from "./runtime/agentHost.js";
66
+ import { AGENT_HOST_CONTROL_PROTOCOL, runAgentHost, sendAgentHostAuthorityControl } from "./runtime/agentHost.js";
67
67
  import { TaskWorkspaceCoordinator, WorkspaceCleanupBlockedError } from "./repository/taskWorkspaceCoordinator.js";
68
68
  import { FileTaskWorkspacePreparer, ReviewRoundWorkspaceEvidenceError } from "./repository/taskWorkspacePreparer.js";
69
69
  import { inspectStorageSchema } from "./storage/storageSchema.js";
@@ -1148,30 +1148,82 @@ export async function main() {
1148
1148
  : { command: result.data, ...reviewData });
1149
1149
  return;
1150
1150
  }
1151
- if (result.output !== undefined)
1152
- emit(result.output);
1153
- try {
1154
- tmux.attachRole(result.taskId, result.roleName, result.access, {
1155
- ...(result.access === "read-write"
1156
- ? {
1157
- revalidateWritableAttach: () => {
1158
- assertTaskRoleWritableAttachAvailable(store, result.taskId, result.roleName, {
1159
- isManagedProcessRunning: () => (tmux.probeRoleStatus(result.taskId, result.roleName) === "running")
1160
- });
1161
- }
1151
+ if (jsonOutput) {
1152
+ throw usageError("Task Role view/takeover requires an interactive terminal.");
1153
+ }
1154
+ if (result.kind === "view") {
1155
+ if (result.output !== undefined)
1156
+ emit(result.output);
1157
+ tmux.attachRole(result.taskId, result.roleName, "read-only");
1158
+ return;
1159
+ }
1160
+ const syncAuthority = async (authorityResult) => {
1161
+ let control;
1162
+ try {
1163
+ control = await sendAgentHostAuthorityControl({
1164
+ home,
1165
+ scope: "task",
1166
+ taskId: authorityResult.taskId,
1167
+ roleName: authorityResult.roleName,
1168
+ control: {
1169
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
1170
+ type: "set-authority",
1171
+ nativeSessionId: authorityResult.nativeSessionId,
1172
+ authority: authorityResult.authority
1162
1173
  }
1163
- : {})
1174
+ });
1175
+ }
1176
+ catch (error) {
1177
+ throw runtimeError(`Agent Host authority synchronization failed at epoch ${authorityResult.authority.epoch}: `
1178
+ + `${error instanceof Error ? error.message : String(error)}. `
1179
+ + `Durable authority is ${authorityResult.authority.owner}-owned; retry `
1180
+ + `'yui task role release ${authorityResult.taskId} ${authorityResult.roleName}' `
1181
+ + "to reconcile the Host.");
1182
+ }
1183
+ if (control.outcome !== "accepted"
1184
+ || control.snapshot.nativeSessionId !== authorityResult.nativeSessionId
1185
+ || control.snapshot.authorityEpoch !== authorityResult.authority.epoch
1186
+ || control.snapshot.authorityOwner !== authorityResult.authority.owner
1187
+ || control.snapshot.authorityHolderId !== authorityResult.authority.holderId) {
1188
+ throw runtimeError(`Agent Host did not accept Provider authority epoch ${authorityResult.authority.epoch}: `
1189
+ + (control.snapshot.detail ?? control.outcome)
1190
+ + `. Durable authority is ${authorityResult.authority.owner}-owned; `
1191
+ + "retry 'yui task role release "
1192
+ + `${authorityResult.taskId} ${authorityResult.roleName}' to reconcile the Host.`);
1193
+ }
1194
+ return control;
1195
+ };
1196
+ await syncAuthority(result);
1197
+ emit(result.output);
1198
+ if (result.action === "release") {
1199
+ runtime.notifyMailboxChanged({
1200
+ kind: "role",
1201
+ taskId: result.taskId,
1202
+ roleName: result.roleName
1164
1203
  });
1204
+ return;
1205
+ }
1206
+ process.stdout.write("Provider input is now routed through the Agent Host PTY gateway. "
1207
+ + "Use tmux detach (Ctrl-b d) to return authority to the Controller.\n");
1208
+ try {
1209
+ tmux.attachRole(result.taskId, result.roleName, "read-write");
1165
1210
  }
1166
1211
  finally {
1167
- if (result.access === "read-write") {
1168
- // A Run claimed while the writer lease was visible is intentionally
1169
- // paused. Releasing the lease only signals that existing durable
1170
- // work may be reconsidered; it never creates or wakes a Run.
1212
+ const currentTask = store.getTask(result.taskId);
1213
+ // Completing or retiring the Task from inside the takeover Turn owns
1214
+ // Provider shutdown and clears the live binding. Do not turn that
1215
+ // successful terminal transition into a failing best-effort release.
1216
+ if (currentTask?.status === "active") {
1217
+ const released = runTaskCommand(["role", "release", result.taskId, result.roleName], store, { runtime, environment: process.env, yuiHome: home });
1218
+ if (released.kind !== "authority" || released.action !== "release") {
1219
+ throw runtimeError("Provider authority release returned an invalid result.");
1220
+ }
1221
+ await syncAuthority(released);
1222
+ emit(released.output);
1171
1223
  runtime.notifyMailboxChanged({
1172
1224
  kind: "role",
1173
- taskId: result.taskId,
1174
- roleName: result.roleName
1225
+ taskId: released.taskId,
1226
+ roleName: released.roleName
1175
1227
  });
1176
1228
  }
1177
1229
  }