@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.
- package/ARCHITECTURE.md +52 -17
- package/README.md +32 -12
- package/dist/cli/updatePorts.js +4 -4
- package/dist/cli.js +1 -1
- package/dist/commands/taskCommands.js +1 -12
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskExecutionCommands.js +0 -5
- package/dist/commands/taskOverviewCommand.js +5 -1
- package/dist/commands/taskRoleRuntimeStatus.js +0 -13
- package/dist/context/sessionBootstrapManifest.js +26 -23
- package/dist/controller/controller.js +0 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +84 -78
- package/dist/executor/agentAdapter.js +6 -2
- package/dist/executor/agentExecutor.js +14 -28
- package/dist/executor/fileRoleLaunchPlanner.js +5 -5
- package/dist/lifecycle/exactRunTerminalization.js +1 -3
- package/dist/output/rolePresentation.js +0 -1
- package/dist/repository/taskWorkspacePreparer.js +0 -1
- package/dist/role/role.js +12 -20
- package/dist/runtime/agentHost.js +2 -2
- package/dist/runtime/builtinAgentDrivers.js +2 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/launchBroker.js +1 -1
- package/dist/runtime/providerRuntimeIdentity.js +8 -17
- package/dist/runtime/structuredProviderHost.js +172 -33
- package/dist/runtime/tmuxAdapters.js +9 -1
- package/dist/scheduler/activeRoleRunDelivery.js +2 -10
- package/dist/scheduler/activeTaskProgress.js +1 -4
- package/dist/scheduler/leaderWakeupProcessor.js +0 -5
- package/dist/scheduler/roleRunStall.js +3 -3
- package/dist/storage/migration/productionRegistry.js +143 -0
- package/dist/storage/taskStore.js +2 -2
- package/dist/web/webSnapshot.js +3 -0
- package/i18n/README.zh-CN.md +15 -7
- package/package.json +1 -1
- 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
|
|
4
|
-
runtimes. The user talks to one Operator. The Operator
|
|
5
|
-
the right Project and Task; that Task's Leader owns
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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.
|
|
270
|
-
|
|
271
|
-
the
|
|
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
|
|
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
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
disconnects, the Host may
|
|
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
|
|
898
|
-
human-control boundary.
|
|
899
|
-
|
|
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.
|
|
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
|
|
package/dist/cli/updatePorts.js
CHANGED
|
@@ -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
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
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
|
|
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} [${
|
|
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
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
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: "
|
|
133
|
+
loadCommand: "\"$YUI_SESSION_CLI\" session context \"$YUI_ROLE\" --json"
|
|
130
134
|
}
|
|
131
135
|
: {
|
|
132
|
-
loadCommand: "
|
|
133
|
-
expandCommand: "
|
|
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
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
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 ===
|
|
195
|
+
if (content === currentSessionCli) {
|
|
193
196
|
current += 1;
|
|
194
197
|
continue;
|
|
195
198
|
}
|
|
196
|
-
if (!
|
|
199
|
+
if (content !== ordinarySessionCli && !isExactSessionCli(content)) {
|
|
197
200
|
skipped += 1;
|
|
198
201
|
continue;
|
|
199
202
|
}
|
|
200
|
-
writeTextFileAtomically(wrapperPath,
|
|
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
|
|
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.
|