@zq-silk/yui 0.8.6 → 0.8.7
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/README.md +20 -3
- package/dist/cli/commandCatalog.js +20 -3
- package/dist/cli/updatePorts.js +6 -0
- package/dist/cli.js +120 -43
- package/dist/commands/globalRoleCommands.js +8 -4
- package/dist/commands/taskCommands.js +141 -9
- package/dist/commands/taskContextCommand.js +5 -3
- package/dist/commands/taskNextActionCommand.js +4 -2
- package/dist/commands/taskOverviewCommand.js +2 -1
- package/dist/commands/taskRoleRuntimeStatus.js +2 -1
- package/dist/context/runContextPack.js +9 -5
- package/dist/context/sessionBootstrapManifest.js +77 -11
- package/dist/context/wakeNotification.js +5 -3
- package/dist/controller/clientRuntime.js +8 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +3 -2
- package/dist/executor/agentExecutor.js +4 -4
- package/dist/executor/fileRoleLaunchPlanner.js +21 -34
- package/dist/review/reviewOutcomeClassifier.js +15 -4
- package/dist/review/taskFinalReviewContractRebind.js +28 -11
- package/dist/runtime/exactControlPlane.js +47 -37
- package/dist/runtime/firstProgressStopLoss.js +3 -1
- package/dist/scheduler/actionability.js +4 -2
- package/dist/scheduler/activeTaskProgress.js +2 -1
- package/dist/scheduler/taskExecutionProjection.js +13 -4
- package/dist/storage/sqliteStore.js +12 -4
- package/dist/storage/taskStore.js +6 -4
- package/dist/task/nextAction.js +8 -3
- package/dist/task/taskRecordRetirement.js +72 -0
- package/dist/workItem/workItem.js +6 -4
- package/i18n/README.zh-CN.md +20 -1
- package/package.json +1 -1
- package/skills/yui-operator/SKILL.md +7 -0
- package/skills/yui-runtime/SKILL.md +6 -6
|
@@ -8,6 +8,7 @@ import { taskDeliveryPath } from "../task/task.js";
|
|
|
8
8
|
import { inspectTaskRoleSessionRecovery } from "./taskRoleRuntimeStatus.js";
|
|
9
9
|
import { summarizeExecutionGroup } from "../execution/executionGroup.js";
|
|
10
10
|
import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
11
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
11
12
|
const RECENT_RECORD_LIMIT = 5;
|
|
12
13
|
const RELATED_RECORD_LIMIT = 5;
|
|
13
14
|
const SUMMARY_TEXT_LIMIT = 400;
|
|
@@ -41,7 +42,8 @@ export function runTaskContextCommand(args, store) {
|
|
|
41
42
|
roleName: role.name
|
|
42
43
|
}))
|
|
43
44
|
].filter((mailbox) => mailbox !== null);
|
|
44
|
-
const
|
|
45
|
+
const events = reader.listEvents(task.id);
|
|
46
|
+
const agentRuns = chronological(operationalTaskRecords(reader.listAgentRuns(task.id), events, "agent-run"));
|
|
45
47
|
const reviewRounds = chronological(reader.listReviewRounds(task.id));
|
|
46
48
|
const changeSets = chronological(reader.listChangeSets(task.id));
|
|
47
49
|
const integrations = chronological(reader.listIntegrationAttempts(task.id));
|
|
@@ -70,10 +72,10 @@ export function runTaskContextCommand(args, store) {
|
|
|
70
72
|
changeSets,
|
|
71
73
|
integrations,
|
|
72
74
|
publications,
|
|
73
|
-
messages: reader.listMessages(task.id),
|
|
75
|
+
messages: operationalTaskRecords(reader.listMessages(task.id), events, "message"),
|
|
74
76
|
openInputRequests: inputRequests.filter((request) => request.status === "open"),
|
|
75
77
|
resolvedInputRequests: inputRequests.filter((request) => request.status !== "open"),
|
|
76
|
-
events
|
|
78
|
+
events,
|
|
77
79
|
nextAction: projectNextAction(nextActionFacts)
|
|
78
80
|
};
|
|
79
81
|
});
|
|
@@ -4,6 +4,7 @@ import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
|
4
4
|
import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
|
|
5
5
|
import { taskDeliveryPath } from "../task/task.js";
|
|
6
6
|
import { projectTaskOrchestration } from "../observability/orchestrationMetrics.js";
|
|
7
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
7
8
|
/**
|
|
8
9
|
* Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
|
|
9
10
|
* Folds the existing durable records into exactly one protocol-level next
|
|
@@ -57,9 +58,10 @@ export function runTaskNextActionCommand(args, store) {
|
|
|
57
58
|
throw taskNotFound(taskId);
|
|
58
59
|
completionReadiness = projectCompletionReadiness(readinessFacts);
|
|
59
60
|
}
|
|
61
|
+
const events = reader.listEvents(taskId);
|
|
60
62
|
const orchestration = projectTaskOrchestration({
|
|
61
63
|
task: reader.getTask(taskId),
|
|
62
|
-
runs: reader.listAgentRuns(taskId),
|
|
64
|
+
runs: operationalTaskRecords(reader.listAgentRuns(taskId), events, "agent-run"),
|
|
63
65
|
roleSessionSets: reader.listRoleSessionSets(taskId),
|
|
64
66
|
workItems: reader.listWorkItems(taskId),
|
|
65
67
|
changeSets: reader.listChangeSets(taskId),
|
|
@@ -69,7 +71,7 @@ export function runTaskNextActionCommand(args, store) {
|
|
|
69
71
|
durableJobs: reader.listDurableJobs(taskId),
|
|
70
72
|
publications: reader.listPublicationReferences(taskId),
|
|
71
73
|
decisions: reader.listDecisions(taskId),
|
|
72
|
-
events
|
|
74
|
+
events,
|
|
73
75
|
managedWorkspaces: reader.listManagedWorkspaces(taskId)
|
|
74
76
|
});
|
|
75
77
|
return {
|
|
@@ -226,7 +226,8 @@ function collectBlockers(workItems, openInputRequests, attention) {
|
|
|
226
226
|
}
|
|
227
227
|
if (item.status !== "pending")
|
|
228
228
|
continue;
|
|
229
|
-
const dependencies = item.dependsOn.filter((dependency) => (workById.get(dependency)?.status !== "completed"
|
|
229
|
+
const dependencies = item.dependsOn.filter((dependency) => (workById.get(dependency)?.status !== "completed"
|
|
230
|
+
&& workById.get(dependency)?.status !== "retired"));
|
|
230
231
|
if (dependencies.length === 0)
|
|
231
232
|
continue;
|
|
232
233
|
blockers.push({
|
|
@@ -7,6 +7,7 @@ import { classifyRuntimeHealth, projectRuntimeMailbox, projectRuntimeObservation
|
|
|
7
7
|
import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
|
|
8
8
|
import { resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
9
9
|
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
10
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
10
11
|
export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
|
|
11
12
|
const taskOpenInputRequestCount = store.listInputRequests(taskId)
|
|
12
13
|
.filter((request) => request.status === "open").length;
|
|
@@ -140,7 +141,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
|
|
|
140
141
|
// Issue 09: the last Run outcome is a separate axis from the Session
|
|
141
142
|
// lifecycle. A Session that stops after its Run yielded must not retroactively
|
|
142
143
|
// turn that Run into a failure; the status display keeps both visible.
|
|
143
|
-
const lastRun = store.listAgentRuns(taskId)
|
|
144
|
+
const lastRun = operationalTaskRecords(store.listAgentRuns(taskId), store.listEvents(taskId), "agent-run")
|
|
144
145
|
.filter((candidate) => candidate.roleName === role.name)
|
|
145
146
|
.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt))[0]
|
|
146
147
|
?? null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
1
2
|
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
2
3
|
import { RUN_BOOTSTRAP_MAX_DELTAS } from "./runContextContract.js";
|
|
3
4
|
import { contextContentDigest, contextSnapshotRef, createContextSnapshot, validateContextSnapshot } from "./contextSnapshot.js";
|
|
@@ -213,9 +214,12 @@ function collectAuthorizedContext(store, run) {
|
|
|
213
214
|
if (view === "worker") {
|
|
214
215
|
for (const dependencyId of item.dependsOn) {
|
|
215
216
|
const dependency = store.getWorkItem(task.id, dependencyId);
|
|
216
|
-
if (dependency === null
|
|
217
|
+
if (dependency === null
|
|
218
|
+
|| (dependency.status !== "completed" && dependency.status !== "retired")) {
|
|
217
219
|
throw new Error(`Run WorkItem dependency is not accepted: ${dependencyId}.`);
|
|
218
220
|
}
|
|
221
|
+
if (dependency.status === "retired")
|
|
222
|
+
continue;
|
|
219
223
|
result.push(materialize("L3", "accepted-work-item", dependency.id, dependency));
|
|
220
224
|
}
|
|
221
225
|
}
|
|
@@ -242,7 +246,8 @@ function collectAuthorizedContext(store, run) {
|
|
|
242
246
|
}
|
|
243
247
|
}
|
|
244
248
|
if (view === "leader") {
|
|
245
|
-
|
|
249
|
+
const events = store.listEvents(task.id);
|
|
250
|
+
for (const item of store.listWorkItems(task.id).filter(({ status }) => status !== "retired")) {
|
|
246
251
|
result.push(materialize("L3", "work-item", item.id, item));
|
|
247
252
|
}
|
|
248
253
|
for (const decision of store.listDecisions(task.id)) {
|
|
@@ -257,14 +262,13 @@ function collectAuthorizedContext(store, run) {
|
|
|
257
262
|
for (const finding of store.listReviewFindings(task.id)) {
|
|
258
263
|
result.push(materialize("L3", "review-finding", finding.id, finding));
|
|
259
264
|
}
|
|
260
|
-
for (const agentRun of store.listAgentRuns(task.id).slice(-24)) {
|
|
265
|
+
for (const agentRun of operationalTaskRecords(store.listAgentRuns(task.id), events, "agent-run").slice(-24)) {
|
|
261
266
|
result.push(materialize("L4", "agent-run", agentRun.id, agentRun));
|
|
262
267
|
}
|
|
263
|
-
for (const message of store.listMessages(task.id).slice(-16)) {
|
|
268
|
+
for (const message of operationalTaskRecords(store.listMessages(task.id), events, "message").slice(-16)) {
|
|
264
269
|
result.push(materialize("L4", "task-message", message.id, message));
|
|
265
270
|
}
|
|
266
271
|
const publishedTreeAuthorizations = [];
|
|
267
|
-
const events = store.listEvents(task.id);
|
|
268
272
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
269
273
|
const event = events[index];
|
|
270
274
|
if (event.type === "task.completed" || event.type === "task.reopened")
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { chmodSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
2
|
+
import { chmodSync, existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
|
|
5
5
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
6
6
|
import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
|
|
7
7
|
export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
|
|
8
|
+
const ORDINARY_SESSION_CLI = [
|
|
9
|
+
"#!/bin/sh",
|
|
10
|
+
"exec yui \"$@\"",
|
|
11
|
+
""
|
|
12
|
+
].join("\n");
|
|
8
13
|
/** Read back one immutable Session Manifest and verify its content digest. */
|
|
9
14
|
export function readSessionBootstrapManifest(path) {
|
|
10
15
|
const source = resolve(path);
|
|
@@ -79,11 +84,10 @@ export function materializeSessionBootstrap(input) {
|
|
|
79
84
|
const controlDigest = exactControlPlaneDigest(input.controlPlane);
|
|
80
85
|
const descriptorPath = resolve(join(home, "runtime", "control-plane", `${controlDigest}.json`));
|
|
81
86
|
writeImmutableText(descriptorPath, `${serializeExactDescriptor(input.controlPlane)}\n`);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
].join("\n");
|
|
87
|
+
// Session identity is carried by the immutable Manifest and durable Role/
|
|
88
|
+
// Run fences. Resolve the ordinary CLI on every invocation so package or
|
|
89
|
+
// release upgrades do not invalidate a still-current native Session.
|
|
90
|
+
const sessionCliContent = ORDINARY_SESSION_CLI;
|
|
87
91
|
const sessionCliDigest = digest(sessionCliContent);
|
|
88
92
|
const sessionCliPath = resolve(join(home, "runtime", "session-cli", `yui-${sessionCliDigest}.sh`));
|
|
89
93
|
writeImmutableText(sessionCliPath, sessionCliContent);
|
|
@@ -122,11 +126,11 @@ export function materializeSessionBootstrap(input) {
|
|
|
122
126
|
roleProfileRef: { digest: profileDigest, path: roleProfilePath },
|
|
123
127
|
contextProtocol: input.owner.scope === "global"
|
|
124
128
|
? {
|
|
125
|
-
loadCommand:
|
|
129
|
+
loadCommand: "yui session context \"$YUI_ROLE\" --json"
|
|
126
130
|
}
|
|
127
131
|
: {
|
|
128
|
-
loadCommand:
|
|
129
|
-
expandCommand:
|
|
132
|
+
loadCommand: "yui task run context \"$YUI_TASK_ID/<run-id>\" --json",
|
|
133
|
+
expandCommand: "yui task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json"
|
|
130
134
|
}
|
|
131
135
|
};
|
|
132
136
|
const manifest = Object.freeze({ ...body, digest: digest(body) });
|
|
@@ -140,6 +144,68 @@ export function materializeSessionBootstrap(input) {
|
|
|
140
144
|
descriptorPath
|
|
141
145
|
});
|
|
142
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Converts wrappers produced before the protocol-compatible Session CLI to an
|
|
149
|
+
* ordinary `yui` invocation. Only a valid Session Manifest may nominate a
|
|
150
|
+
* wrapper, and only the exact legacy two-line wrapper shape is changed. The
|
|
151
|
+
* Manifest and its frozen descriptor stay immutable and continue to
|
|
152
|
+
* authenticate the Session; repeated refreshes are no-ops.
|
|
153
|
+
*/
|
|
154
|
+
export function refreshManagedSessionCliWrappers(homeInput) {
|
|
155
|
+
const home = resolve(homeInput);
|
|
156
|
+
const manifestDirectory = resolve(join(home, "runtime", "session-manifests"));
|
|
157
|
+
const sessionCliDirectory = resolve(join(home, "runtime", "session-cli"));
|
|
158
|
+
if (!existsSync(manifestDirectory)) {
|
|
159
|
+
return Object.freeze({ refreshed: 0, current: 0, skipped: 0 });
|
|
160
|
+
}
|
|
161
|
+
const wrapperPaths = new Set();
|
|
162
|
+
let skipped = 0;
|
|
163
|
+
for (const name of readdirSync(manifestDirectory).filter((entry) => entry.endsWith(".json"))) {
|
|
164
|
+
const manifestPath = resolve(join(manifestDirectory, name));
|
|
165
|
+
try {
|
|
166
|
+
const manifest = readSessionBootstrapManifest(manifestPath);
|
|
167
|
+
if (manifestPath !== resolve(join(manifestDirectory, `${manifest.digest}.json`))) {
|
|
168
|
+
skipped += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const wrapperPath = resolve(manifest.controlPlane.sessionCliPath);
|
|
172
|
+
if (dirname(wrapperPath) !== sessionCliDirectory) {
|
|
173
|
+
skipped += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
wrapperPaths.add(wrapperPath);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Historical or incomplete manifests are audit material. They must not
|
|
180
|
+
// block current Sessions or an otherwise compatible package update.
|
|
181
|
+
skipped += 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let refreshed = 0;
|
|
185
|
+
let current = 0;
|
|
186
|
+
for (const wrapperPath of wrapperPaths) {
|
|
187
|
+
if (!existsSync(wrapperPath)) {
|
|
188
|
+
skipped += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const content = readFileSync(wrapperPath, "utf8");
|
|
192
|
+
if (content === ORDINARY_SESSION_CLI) {
|
|
193
|
+
current += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!isLegacyExactSessionCli(content)) {
|
|
197
|
+
skipped += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
writeTextFileAtomically(wrapperPath, ORDINARY_SESSION_CLI);
|
|
201
|
+
chmodSync(wrapperPath, 0o700);
|
|
202
|
+
refreshed += 1;
|
|
203
|
+
}
|
|
204
|
+
return Object.freeze({ refreshed, current, skipped });
|
|
205
|
+
}
|
|
206
|
+
function isLegacyExactSessionCli(content) {
|
|
207
|
+
return /^#!\/bin\/sh\nexec [^\n]+ '--yui-control' '[a-f0-9]{64}' "\$@"\n$/u.test(content);
|
|
208
|
+
}
|
|
143
209
|
function writeImmutableText(path, content) {
|
|
144
210
|
writeTextFileAtomically(path, content);
|
|
145
211
|
chmodSync(path, 0o600);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderWakeReason } from "../scheduler/wakeReason.js";
|
|
2
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
3
|
/**
|
|
3
4
|
* Issue 04 (context token budget) — long-term design:
|
|
4
5
|
*
|
|
@@ -25,12 +26,13 @@ export function buildTaskWakeEnvelope(reader, request) {
|
|
|
25
26
|
throw new Error(`Wake envelope ${request.wakeId} must carry at least one reason.`);
|
|
26
27
|
}
|
|
27
28
|
const fromTime = Date.parse(request.fromCursor);
|
|
29
|
+
const events = reader.listEvents(request.taskId);
|
|
28
30
|
const counts = {
|
|
29
|
-
events:
|
|
31
|
+
events: events
|
|
30
32
|
.filter((record) => Date.parse(record.createdAt) > fromTime).length,
|
|
31
|
-
messages: reader.listMessages(request.taskId)
|
|
33
|
+
messages: operationalTaskRecords(reader.listMessages(request.taskId), events, "message")
|
|
32
34
|
.filter((record) => Date.parse(record.createdAt) > fromTime).length,
|
|
33
|
-
runs: reader.listAgentRuns(request.taskId)
|
|
35
|
+
runs: operationalTaskRecords(reader.listAgentRuns(request.taskId), events, "agent-run")
|
|
34
36
|
.filter((record) => Date.parse(record.createdAt) > fromTime).length
|
|
35
37
|
};
|
|
36
38
|
const lines = [
|
|
@@ -7,7 +7,7 @@ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
|
7
7
|
import { hasRuntimeLifecycleWork } from "../runtime/lifecycleReservation.js";
|
|
8
8
|
import { assertControllerStatusIdentity } from "../runtime/exactControlPlane.js";
|
|
9
9
|
import { EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES } from "./domainIdentity.js";
|
|
10
|
-
import {
|
|
10
|
+
import { yuiVersionIdentity } from "../version.js";
|
|
11
11
|
import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
12
12
|
import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
|
|
13
13
|
import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
|
|
@@ -137,12 +137,8 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
|
|
|
137
137
|
+ "Run `yui controller restart` before writing new task records.");
|
|
138
138
|
}
|
|
139
139
|
const actualVersion = statusRecord.version;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
? typeof actualVersion === "string" && actualVersion !== expected
|
|
143
|
-
: actualVersion !== expected;
|
|
144
|
-
if (versionMismatch) {
|
|
145
|
-
throw new Error(`Controller version is incompatible (expected ${expected}, found ${typeof actualVersion === "string" ? actualVersion : "unknown"}). `
|
|
140
|
+
if (expectedVersion !== undefined && actualVersion !== expectedVersion) {
|
|
141
|
+
throw new Error(`Controller version is incompatible (expected ${expectedVersion}, found ${typeof actualVersion === "string" ? actualVersion : "unknown"}). `
|
|
146
142
|
+ "Run `yui controller restart` before writing new task records.");
|
|
147
143
|
}
|
|
148
144
|
// Ordinary callers must authenticate the complete control-plane identity.
|
|
@@ -151,7 +147,11 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
|
|
|
151
147
|
// path authenticates its executable, argv, and version immediately after
|
|
152
148
|
// readiness in ensureFileTaskControllerIdentity.
|
|
153
149
|
if (expectedVersion === undefined) {
|
|
154
|
-
|
|
150
|
+
const identity = yuiVersionIdentity();
|
|
151
|
+
assertControllerStatusIdentity(status, {
|
|
152
|
+
...identity,
|
|
153
|
+
version: typeof actualVersion === "string" ? actualVersion : identity.version
|
|
154
|
+
});
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
function spawnDetachedFileTaskController(home, environment) {
|
|
@@ -6,6 +6,7 @@ import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
|
|
|
6
6
|
import { boundProviderRetryBeforeFirstProgress, projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
|
|
7
7
|
import { hasRecentTurnId } from "../executor/turnCompletion.js";
|
|
8
8
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
9
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
9
10
|
import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
|
|
10
11
|
import { createTaskWake, fallbackWakeCursor, latestTaskWake } from "../scheduler/taskWake.js";
|
|
11
12
|
import { rolloverTaskRoleSessionForContextBudget } from "../lifecycle/contextBudgetRollover.js";
|
|
@@ -596,7 +597,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
596
597
|
const latest = latestTaskWake(reader.listTaskWakes(taskId));
|
|
597
598
|
const fromCursor = latest?.toCursor ?? fallbackWakeCursor({
|
|
598
599
|
taskCreatedAt: task.createdAt,
|
|
599
|
-
leaderRunCreatedAt: reader.listAgentRuns(taskId)
|
|
600
|
+
leaderRunCreatedAt: operationalTaskRecords(reader.listAgentRuns(taskId), reader.listEvents(taskId), "agent-run")
|
|
600
601
|
.filter((run) => run.roleName === "leader")
|
|
601
602
|
.at(-1)?.createdAt
|
|
602
603
|
});
|
|
@@ -3706,7 +3707,7 @@ function bindTaskRoleRunInFlight(store, role, run, now) {
|
|
|
3706
3707
|
agentId,
|
|
3707
3708
|
runId: run.id,
|
|
3708
3709
|
receiptId: agentRunDeliveryReceiptId(run)
|
|
3709
|
-
}, now);
|
|
3710
|
+
}, now, run.mode);
|
|
3710
3711
|
store.saveRoleSessionSet(updated);
|
|
3711
3712
|
}
|
|
3712
3713
|
function markTaskRoleRunPushedInFlight(store, role, run, now) {
|
|
@@ -292,7 +292,7 @@ export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
|
|
|
292
292
|
throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
|
|
293
293
|
+ "Stop the existing native process before starting a fresh Session.");
|
|
294
294
|
}
|
|
295
|
-
export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
295
|
+
export function bindTaskRoleRun(set, fence, preparedAt, mode) {
|
|
296
296
|
validateRoleSessionSet(set);
|
|
297
297
|
assertTaskRoleSessionSet(set);
|
|
298
298
|
const normalized = normalizeTaskRoleRunFence(fence);
|
|
@@ -305,9 +305,9 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
|
305
305
|
throw new Error("Task Role session set already has an in-flight Run.");
|
|
306
306
|
}
|
|
307
307
|
const timestamp = requireDate(preparedAt, "Task Role Run preparedAt");
|
|
308
|
-
const providerBinding = set.providerBinding
|
|
309
|
-
?
|
|
310
|
-
:
|
|
308
|
+
const providerBinding = mode === "resume" && set.providerBinding !== null
|
|
309
|
+
? rebindProviderRuntimeRun(set.providerBinding, normalized.runId)
|
|
310
|
+
: null;
|
|
311
311
|
const updated = {
|
|
312
312
|
...set,
|
|
313
313
|
inFlight: { ...normalized, preparedAt: timestamp },
|
|
@@ -19,8 +19,8 @@ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.j
|
|
|
19
19
|
import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
|
|
20
20
|
import { activeLiveRoleAgentSession } from "./agentExecutor.js";
|
|
21
21
|
import { effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
|
|
22
|
-
import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor,
|
|
23
|
-
import {
|
|
22
|
+
import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor, exactControlPlaneDigest, exactTaskRuntimeDescriptorPath, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
|
|
23
|
+
import { detectRunningRelease } from "../release/runtimeRelease.js";
|
|
24
24
|
import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
|
|
25
25
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
26
26
|
import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
@@ -53,21 +53,19 @@ export class FileRoleLaunchPlanner {
|
|
|
53
53
|
this.#createNativeSessionId = options.createNativeSessionId ?? randomUUID;
|
|
54
54
|
this.#cliPath = canonicalPath(options.cliPath
|
|
55
55
|
?? fileURLToPath(new URL("../cli.js", import.meta.url)));
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
// handover cannot mutate a different control plane.
|
|
61
|
-
const activeRelease = readActiveReleasePointer(this.home);
|
|
56
|
+
// Internal callbacks retain one exact command identity for receipt fencing.
|
|
57
|
+
// Interactive Role commands use ordinary `yui`; their continuity is the
|
|
58
|
+
// Session Manifest plus protocol/storage and durable runtime identity.
|
|
59
|
+
const runningRelease = detectRunningRelease(this.#cliPath);
|
|
62
60
|
this.#controlPlane = createExactControlPlaneDescriptor({
|
|
63
61
|
executable: process.execPath,
|
|
64
62
|
cliEntry: this.#cliPath,
|
|
65
63
|
yuiHome: this.home,
|
|
66
|
-
...(
|
|
64
|
+
...(runningRelease === null
|
|
67
65
|
? {}
|
|
68
66
|
: {
|
|
69
|
-
buildId:
|
|
70
|
-
activeReleaseDigest:
|
|
67
|
+
buildId: runningRelease.manifest.buildId,
|
|
68
|
+
activeReleaseDigest: runningRelease.manifest.packageDigest
|
|
71
69
|
})
|
|
72
70
|
});
|
|
73
71
|
}
|
|
@@ -247,7 +245,7 @@ export class FileRoleLaunchPlanner {
|
|
|
247
245
|
if (input.mode === "resume" && !compatibleExisting) {
|
|
248
246
|
throw new Error(`Global Role resume effective snapshot drifted: ${role.name}.`);
|
|
249
247
|
}
|
|
250
|
-
return this.#compile(role, input, { scope: "global" }, undefined, compatibleExisting ? existing.nativeSessionId : undefined, undefined, effective, { purpose: "execution" });
|
|
248
|
+
return this.#compile(role, input, { scope: "global" }, undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, undefined, effective, { purpose: "execution" });
|
|
251
249
|
}
|
|
252
250
|
#compile(role, input, owner, sessionTitle, knownNativeSessionId, workspaceOverride, effective, sessionPolicy) {
|
|
253
251
|
const launchRole = effectiveRoleForLaunch(role, effective);
|
|
@@ -347,7 +345,7 @@ export class FileRoleLaunchPlanner {
|
|
|
347
345
|
const roleConfig = binding.config.adapterId === "claude"
|
|
348
346
|
&& owner.scope === "task"
|
|
349
347
|
&& input.runId !== undefined
|
|
350
|
-
? managedClaudeControlPlaneConfig(binding.config, owner.taskId, managedRun?.workItemId, input.runId
|
|
348
|
+
? managedClaudeControlPlaneConfig(binding.config, owner.taskId, managedRun?.workItemId, input.runId)
|
|
351
349
|
: binding.config;
|
|
352
350
|
const effectiveConfig = withNativeProjectDirectories(roleConfig, nativeAdditionalDirectories(effective.workspace, agentWorkspace));
|
|
353
351
|
const compileInput = {
|
|
@@ -383,15 +381,14 @@ export class FileRoleLaunchPlanner {
|
|
|
383
381
|
CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "codex_exec"
|
|
384
382
|
}
|
|
385
383
|
: {};
|
|
386
|
-
const
|
|
387
|
-
&& binding.adapterId === "claude"
|
|
384
|
+
const preallocatedNativeSessionId = binding.adapterId === "claude"
|
|
388
385
|
&& resumeNativeSessionId === undefined
|
|
389
386
|
? requireText(input.launchId === undefined
|
|
390
387
|
? this.#createNativeSessionId()
|
|
391
388
|
: nativeSessionIdForLaunch(this.home, input.launchId, input.agentId, input.adapterId), "Native session id")
|
|
392
389
|
: resumeNativeSessionId;
|
|
393
390
|
const managedCompiled = managedControl
|
|
394
|
-
? adapter.compileManagedControl(compileInput, launchMode,
|
|
391
|
+
? adapter.compileManagedControl(compileInput, launchMode, preallocatedNativeSessionId)
|
|
395
392
|
: undefined;
|
|
396
393
|
const compiled = managedCompiled !== undefined
|
|
397
394
|
? managedCompiled
|
|
@@ -440,7 +437,7 @@ export class FileRoleLaunchPlanner {
|
|
|
440
437
|
else if (launchMode === "new") {
|
|
441
438
|
if (managedControl)
|
|
442
439
|
args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
|
|
443
|
-
const nativeSessionId = requireText(
|
|
440
|
+
const nativeSessionId = requireText(preallocatedNativeSessionId, "Native session id");
|
|
444
441
|
if (!managedControl)
|
|
445
442
|
args.push("--session-id", nativeSessionId);
|
|
446
443
|
else if (!args.includes("--session-id"))
|
|
@@ -496,7 +493,7 @@ export class FileRoleLaunchPlanner {
|
|
|
496
493
|
? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId)
|
|
497
494
|
: undefined;
|
|
498
495
|
const providerNativeSessionId = binding.adapterId === "claude"
|
|
499
|
-
?
|
|
496
|
+
? preallocatedNativeSessionId
|
|
500
497
|
: resumeNativeSessionId;
|
|
501
498
|
const providerControl = managedControl
|
|
502
499
|
? {
|
|
@@ -745,18 +742,17 @@ function ensureManagedClaudeLifecyclePlugin(home, cliPath) {
|
|
|
745
742
|
}, null, 2)}\n`);
|
|
746
743
|
return root;
|
|
747
744
|
}
|
|
748
|
-
function managedClaudeControlPlaneConfig(config, taskId, workItemId, runId
|
|
745
|
+
function managedClaudeControlPlaneConfig(config, taskId, workItemId, runId) {
|
|
749
746
|
if (config.permission.strategy !== "configured")
|
|
750
747
|
return config;
|
|
751
|
-
const exact = exactControlPlaneCommandPrefix(controlPlane);
|
|
752
748
|
const managed = [
|
|
753
|
-
`Bash(
|
|
754
|
-
`Bash(
|
|
755
|
-
`Bash(
|
|
749
|
+
`Bash(yui task run context ${taskId}/${runId}:*)`,
|
|
750
|
+
`Bash(yui --json task context ${taskId})`,
|
|
751
|
+
`Bash(yui --json task work list ${taskId})`,
|
|
756
752
|
...(workItemId === undefined
|
|
757
753
|
? []
|
|
758
|
-
: [`Bash(
|
|
759
|
-
`Bash(
|
|
754
|
+
: [`Bash(yui --json task work show ${workItemId})`]),
|
|
755
|
+
`Bash(yui task run yield ${runId} --summary-file -:*)`
|
|
760
756
|
];
|
|
761
757
|
const existing = (config.permission.allowedTools ?? [])
|
|
762
758
|
.filter((rule) => !isManagedYuiBashRule(rule));
|
|
@@ -773,15 +769,6 @@ function isManagedYuiBashRule(rule) {
|
|
|
773
769
|
return /^Bash\(yui(?:\s|:\*|\*|\))/u.test(normalized)
|
|
774
770
|
|| /^Bash\(.*\s--yui-control\s/u.test(normalized);
|
|
775
771
|
}
|
|
776
|
-
function renderExactControlPlaneInstructions(descriptor) {
|
|
777
|
-
const command = exactControlPlaneCommandPrefix(descriptor);
|
|
778
|
-
return [
|
|
779
|
-
"Exact Yui control-plane command prefix for this managed Task session:",
|
|
780
|
-
`\`${command}\``,
|
|
781
|
-
"Replace the portable bare `yui` token in Yui Role Skills and dispatch instructions with this exact prefix.",
|
|
782
|
-
"Bare `yui`, a PATH launcher, another checkout CLI, another YUI_HOME, or a changed schema/Controller identity is invalid and fails before Task state is read or written."
|
|
783
|
-
].join("\n");
|
|
784
|
-
}
|
|
785
772
|
function canonicalPath(path) {
|
|
786
773
|
const absolute = resolve(path);
|
|
787
774
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { matchYieldReceipt } from "../run/yieldReceipt.js";
|
|
2
|
+
import { isTaskRecordRetired, operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
3
|
const INFRA_SIGNATURES = [
|
|
3
4
|
{ kind: "session-not-stopped", pattern: /session must be stopped before workspace migration/iu },
|
|
4
5
|
{ kind: "run-start", pattern: /role run could not start|could not start (?:the )?(?:reviewer|role) run/iu },
|
|
@@ -15,6 +16,14 @@ const INFRA_SIGNATURES = [
|
|
|
15
16
|
export function classifyReviewRoundOutcome(round, evidence) {
|
|
16
17
|
if (round.status !== "completed" && round.status !== "failed")
|
|
17
18
|
return null;
|
|
19
|
+
if (evidence !== undefined && round.reviewerRunId !== undefined
|
|
20
|
+
&& isTaskRecordRetired(evidence.listEvents(round.taskId), "agent-run", round.reviewerRunId)) {
|
|
21
|
+
return {
|
|
22
|
+
kind: "non-semantic",
|
|
23
|
+
infraKind: "run-identity",
|
|
24
|
+
reason: `Reviewer Run ${round.reviewerRunId} was retired from operational evidence.`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
18
27
|
const infraKind = classifyInfraKind(`${round.summary ?? ""}\n${round.report ?? ""}`);
|
|
19
28
|
if (round.status === "failed") {
|
|
20
29
|
const semanticEvidence = failedRoundSemanticEvidence(round, evidence);
|
|
@@ -86,7 +95,8 @@ function failedRoundSemanticEvidence(round, evidence) {
|
|
|
86
95
|
if (semanticLane !== undefined)
|
|
87
96
|
return `Reviewer Lane ${semanticLane.id} delivered semantic evidence.`;
|
|
88
97
|
if (evidence !== undefined) {
|
|
89
|
-
const
|
|
98
|
+
const events = evidence.listEvents(round.taskId);
|
|
99
|
+
const reviewRun = operationalTaskRecords(evidence.listAgentRuns(round.taskId), events, "agent-run").find((run) => (run.purpose === "review"
|
|
90
100
|
&& run.reviewRoundId === round.id
|
|
91
101
|
&& (run.status === "yielded" || runtimeFailureSummaryHasReviewerOutput(run.summary ?? ""))));
|
|
92
102
|
if (reviewRun !== undefined)
|
|
@@ -94,7 +104,7 @@ function failedRoundSemanticEvidence(round, evidence) {
|
|
|
94
104
|
const finding = evidence.listReviewFindings(round.taskId).find((entry) => (entry.firstReviewRoundId === round.id || entry.lastReviewRoundId === round.id));
|
|
95
105
|
if (finding !== undefined)
|
|
96
106
|
return `Review finding ${finding.id} references the Round.`;
|
|
97
|
-
const completion =
|
|
107
|
+
const completion = events.find((event) => (event.type === "review.completed" && event.payload.reviewRoundId === round.id));
|
|
98
108
|
if (completion !== undefined)
|
|
99
109
|
return `Review completion Event ${completion.id} exists.`;
|
|
100
110
|
}
|
|
@@ -142,7 +152,8 @@ function completedInfrastructureCorroborationFailure(round, store) {
|
|
|
142
152
|
return `Completed Round has non-completed Reviewer Lane ${lane.id}/${lane.status}.`;
|
|
143
153
|
}
|
|
144
154
|
}
|
|
145
|
-
const
|
|
155
|
+
const allEvents = store.listEvents(round.taskId);
|
|
156
|
+
const runs = operationalTaskRecords(store.listAgentRuns(round.taskId), allEvents, "agent-run").filter((run) => (run.purpose === "review" && run.reviewRoundId === round.id));
|
|
146
157
|
const active = runs.find(({ status }) => status === "active");
|
|
147
158
|
if (active !== undefined)
|
|
148
159
|
return `Reviewer Run ${active.id} is still active.`;
|
|
@@ -176,7 +187,7 @@ function completedInfrastructureCorroborationFailure(round, store) {
|
|
|
176
187
|
const finding = store.listReviewFindings(round.taskId).find((entry) => (entry.firstReviewRoundId === round.id || entry.lastReviewRoundId === round.id));
|
|
177
188
|
if (finding !== undefined)
|
|
178
189
|
return `Review finding ${finding.id} references the Round.`;
|
|
179
|
-
const events =
|
|
190
|
+
const events = allEvents.filter((event) => (event.type === "review.completed" && event.payload.reviewRoundId === round.id));
|
|
180
191
|
if (events.length !== 1)
|
|
181
192
|
return "Completed Round lacks one exact completion Event.";
|
|
182
193
|
const event = events[0];
|