@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 +31 -25
- package/README.md +24 -20
- package/dist/cli/commandCatalog.js +3 -3
- package/dist/commands/taskCommands.js +32 -9
- package/dist/controller/controller.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +1 -1
- package/dist/controller/runtimeLaunchCoordinator.js +4 -0
- package/dist/executor/agentAdapter.js +62 -8
- package/dist/executor/executorRegistry.js +4 -0
- package/dist/executor/fileRoleLaunchPlanner.js +71 -27
- package/dist/runtime/agentHost.js +226 -14
- package/dist/runtime/builtinAgentDrivers.js +9 -0
- package/dist/runtime/codexAppServerRuntime.js +63 -12
- package/dist/runtime/launchBroker.js +51 -1
- package/dist/runtime/runtimeBinding.js +10 -1
- package/dist/runtime/structuredProviderHost.js +214 -54
- package/dist/runtime/tmuxAdapters.js +13 -2
- package/dist/scheduler/activeRoleRunDelivery.js +24 -3
- package/dist/scheduler/leaderWakeupProcessor.js +20 -0
- package/i18n/README.zh-CN.md +7 -7
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
246
|
-
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
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
|
-
|
|
875
|
-
and
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
Codex uses
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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: "
|
|
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: "
|
|
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 =
|
|
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
|
-
|
|
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() ||
|
|
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 ===
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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(
|
|
169
|
+
argv.push("--config", `model=${tomlString(config.model)}`);
|
|
162
170
|
}
|
|
163
171
|
return {
|
|
164
172
|
...launch,
|
|
165
|
-
argv: [...argv, "app-server", "
|
|
166
|
-
transport: "codex-app-server-
|
|
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,
|
|
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
|
|
428
|
-
//
|
|
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
|
|
433
|
-
//
|
|
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:
|
|
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
|
-
?
|
|
714
|
-
: prefixYuiTitleInput(
|
|
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
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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 {
|