@zq-silk/yui 0.8.6 → 0.8.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/README.md +20 -3
- package/dist/cli/commandCatalog.js +20 -3
- package/dist/cli/updatePorts.js +6 -0
- package/dist/cli.js +147 -44
- package/dist/commands/globalRoleCommands.js +8 -4
- package/dist/commands/releaseCommands.js +44 -8
- 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/release/releaseHandover.js +2 -2
- package/dist/release/runtimeRelease.js +4 -3
- 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
|
@@ -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 {
|
|
@@ -135,8 +135,8 @@ async function activateLocked(ports, locked) {
|
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
// 6) Switch the active release pointer. This is the atomic commit point:
|
|
138
|
-
// after it succeeds,
|
|
139
|
-
// release.
|
|
138
|
+
// after it succeeds, Controller lifecycle operations resolve the new
|
|
139
|
+
// release. The ordinary CLI and managed Session wrappers stay on `yui`.
|
|
140
140
|
const pointer = Object.freeze({
|
|
141
141
|
schemaVersion: 1,
|
|
142
142
|
releaseId,
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A release is a self-contained, content-addressed runtime package unpacked
|
|
6
6
|
* below `runtime/releases/<version>-<package-sha256>/`. The Home's active
|
|
7
|
-
* release pointer (`runtime/active-release.json`) names exactly one
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* release pointer (`runtime/active-release.json`) names exactly one Controller
|
|
8
|
+
* release. The ordinary CLI remains the globally installed package; only an
|
|
9
|
+
* explicit target activation delegates to the verified target release CLI so
|
|
10
|
+
* that release owns its handover protocol and deadlines.
|
|
10
11
|
*
|
|
11
12
|
* The Controller writes its provenance to `runtime/runtime-identity.json` on
|
|
12
13
|
* every start, and a versioned handover fence (`runtime/handover-fence.json`)
|
|
@@ -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];
|
|
@@ -23,6 +23,13 @@ export function resolveRecordedTaskFinalReviewContract(taskId, workItems, review
|
|
|
23
23
|
source: `Candidate ${item.id}/${candidate.id}`
|
|
24
24
|
}];
|
|
25
25
|
});
|
|
26
|
+
const historicalCandidateObservations = workItems.flatMap((item) => (item.candidates
|
|
27
|
+
.filter((candidate) => candidate.taskFinalReviewContract !== undefined)
|
|
28
|
+
.map((candidate) => ({
|
|
29
|
+
contract: candidate.taskFinalReviewContract,
|
|
30
|
+
createdAt: candidate.createdAt,
|
|
31
|
+
source: `Historical Candidate ${item.id}/${candidate.id}`
|
|
32
|
+
}))));
|
|
26
33
|
const reviewObservations = reviewRounds.flatMap((round) => ((round.scope ?? "work-item") !== "task"
|
|
27
34
|
|| round.taskFinalReviewContract === undefined
|
|
28
35
|
? []
|
|
@@ -31,7 +38,7 @@ export function resolveRecordedTaskFinalReviewContract(taskId, workItems, review
|
|
|
31
38
|
createdAt: round.createdAt,
|
|
32
39
|
source: `ReviewRound ${round.id}`
|
|
33
40
|
}]));
|
|
34
|
-
return resolveTaskFinalReviewContract(taskId, [...candidateObservations, ...reviewObservations], events);
|
|
41
|
+
return resolveTaskFinalReviewContract(taskId, [...candidateObservations, ...reviewObservations], events, historicalCandidateObservations);
|
|
35
42
|
}
|
|
36
43
|
export function createTaskFinalReviewContractRebind(input) {
|
|
37
44
|
const taskId = requireIdentity(input.taskId, "Task final-review rebind Task id");
|
|
@@ -140,7 +147,7 @@ export function taskFinalReviewContractRebindFromEvent(event) {
|
|
|
140
147
|
* as a forward-only sequence, then require one strict append-only rebind chain.
|
|
141
148
|
* Any Reviewer change, reversion, fork, or post-rebind drift fails closed.
|
|
142
149
|
*/
|
|
143
|
-
export function resolveTaskFinalReviewContract(taskId, observations, events) {
|
|
150
|
+
export function resolveTaskFinalReviewContract(taskId, observations, events, historicalObservations = []) {
|
|
144
151
|
const normalizedTaskId = requireIdentity(taskId, "Task final-review contract Task id");
|
|
145
152
|
const orderedObservations = [...observations]
|
|
146
153
|
.map((observation) => ({
|
|
@@ -149,6 +156,13 @@ export function resolveTaskFinalReviewContract(taskId, observations, events) {
|
|
|
149
156
|
source: requireText(observation.source, "Task final-review observation source")
|
|
150
157
|
}))
|
|
151
158
|
.sort(compareCreatedAt);
|
|
159
|
+
const orderedHistoricalObservations = [...historicalObservations]
|
|
160
|
+
.map((observation) => ({
|
|
161
|
+
contract: validateTaskFinalReviewContract(observation.contract),
|
|
162
|
+
createdAt: requireTimestamp(observation.createdAt, "Task final-review historical observation createdAt"),
|
|
163
|
+
source: requireText(observation.source, "Task final-review historical observation source")
|
|
164
|
+
}))
|
|
165
|
+
.sort(compareCreatedAt);
|
|
152
166
|
const orderedEvents = events
|
|
153
167
|
.filter(({ type }) => type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT)
|
|
154
168
|
.map((event) => ({
|
|
@@ -158,26 +172,29 @@ export function resolveTaskFinalReviewContract(taskId, observations, events) {
|
|
|
158
172
|
}))
|
|
159
173
|
.sort((left, right) => compareCreatedAt(left, right)
|
|
160
174
|
|| left.event.id.localeCompare(right.event.id, undefined, { numeric: true }));
|
|
161
|
-
if (orderedObservations.length === 0)
|
|
162
|
-
if (orderedEvents.length > 0) {
|
|
163
|
-
throw new Error(`Task ${normalizedTaskId} has a final-review rebind without a stored contract.`);
|
|
164
|
-
}
|
|
175
|
+
if (orderedObservations.length === 0 && orderedEvents.length === 0)
|
|
165
176
|
return undefined;
|
|
166
|
-
|
|
167
|
-
for (const observation of orderedObservations) {
|
|
177
|
+
for (const observation of [...orderedObservations, ...orderedHistoricalObservations]) {
|
|
168
178
|
if (observation.contract.taskId !== normalizedTaskId) {
|
|
169
179
|
throw new Error(`${observation.source} carries a final-review contract for another Task.`);
|
|
170
180
|
}
|
|
171
181
|
}
|
|
172
|
-
const initial = orderedObservations[0].contract;
|
|
173
|
-
const reviewerRoleName = initial.reviewerRoleName;
|
|
174
182
|
const firstRebindAt = orderedEvents[0]?.createdAt;
|
|
175
|
-
const
|
|
183
|
+
const primaryLegacyObservations = firstRebindAt === undefined
|
|
176
184
|
? orderedObservations
|
|
177
185
|
: orderedObservations.filter(({ createdAt }) => createdAt < firstRebindAt);
|
|
186
|
+
const historicalRebindSource = orderedEvents[0] === undefined
|
|
187
|
+
? undefined
|
|
188
|
+
: orderedHistoricalObservations.find((observation) => (observation.createdAt < orderedEvents[0].createdAt
|
|
189
|
+
&& sameTaskFinalReviewContract(observation.contract, orderedEvents[0].rebind.fromContract)));
|
|
190
|
+
const legacyObservations = primaryLegacyObservations.length > 0
|
|
191
|
+
? primaryLegacyObservations
|
|
192
|
+
: historicalRebindSource === undefined ? [] : [historicalRebindSource];
|
|
178
193
|
if (legacyObservations.length === 0) {
|
|
179
194
|
throw new Error(`Task ${normalizedTaskId} has a final-review rebind without an established source contract.`);
|
|
180
195
|
}
|
|
196
|
+
const initial = legacyObservations[0].contract;
|
|
197
|
+
const reviewerRoleName = initial.reviewerRoleName;
|
|
181
198
|
let effective = initial;
|
|
182
199
|
const legacyContractDigests = new Set([initial.digest]);
|
|
183
200
|
for (const observation of legacyObservations) {
|
|
@@ -9,7 +9,6 @@ import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecyc
|
|
|
9
9
|
import { nativeSessionIdForLaunch } from "./preallocatedNativeSession.js";
|
|
10
10
|
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
11
11
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
12
|
-
import { readActiveReleasePointer } from "../release/runtimeRelease.js";
|
|
13
12
|
export const EXACT_CONTROL_ARGUMENT = "--yui-control";
|
|
14
13
|
export const YUI_CONTROL_PLANE_DESCRIPTOR = "YUI_CONTROL_PLANE_DESCRIPTOR";
|
|
15
14
|
export const YUI_TASK_RUNTIME_DESCRIPTOR = "YUI_TASK_RUNTIME_DESCRIPTOR";
|
|
@@ -174,43 +173,67 @@ export async function assertExactControlPlanePreflight(input, options = {}) {
|
|
|
174
173
|
+ `(expected ${descriptor.identity.aggregateSchemaVersion}, found `
|
|
175
174
|
+ `${storage.currentAggregateSchemaVersion ?? "unknown"}).`);
|
|
176
175
|
}
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
+ "no active release pointer.");
|
|
176
|
+
// A frozen descriptor authenticates the command that created it; it no
|
|
177
|
+
// longer pins the Home's deployment pointer for the lifetime of a Session.
|
|
178
|
+
// Continuity is the protocol/storage contract checked above and the durable
|
|
179
|
+
// Task/Role/Run identity checked below. This lets a compatible Controller or
|
|
180
|
+
// active release advance without invalidating a still-current Session.
|
|
181
|
+
if (options.checkController !== false) {
|
|
182
|
+
const call = options.callController ?? defaultCallController;
|
|
183
|
+
try {
|
|
184
|
+
const status = await call(descriptor.yuiHome, "controller.status", {});
|
|
185
|
+
assertControllerContinuityIdentity(status, descriptor.identity);
|
|
188
186
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (!isDefinitelyNotRunning(error))
|
|
189
|
+
throw error;
|
|
192
190
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
191
|
+
}
|
|
192
|
+
return descriptor;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Compatibility gate for an ordinary `yui` invocation inside a managed
|
|
196
|
+
* Session. The Session Manifest and durable runtime state authenticate the
|
|
197
|
+
* actor separately; this gate proves that the current CLI can safely share the
|
|
198
|
+
* Home with its storage and Controller without pinning package/build identity.
|
|
199
|
+
*/
|
|
200
|
+
export async function assertCompatibleControlPlanePreflight(input, options = {}) {
|
|
201
|
+
const home = canonicalPath(input.actualHome);
|
|
202
|
+
const identity = validateVersionIdentity(options.identity ?? yuiVersionIdentity());
|
|
203
|
+
const storage = (options.inspectStorage ?? inspectStorageSchema)(home);
|
|
204
|
+
if (storage.status !== "current") {
|
|
205
|
+
const compatibleRecordOnlyOlder = storage.status === "unsupported"
|
|
206
|
+
&& storage.incompatibleComponent === "record"
|
|
207
|
+
&& storage.direction === "older"
|
|
208
|
+
&& storage.currentLayoutVersion === identity.storageLayoutVersion
|
|
209
|
+
&& storage.currentAggregateSchemaVersion === identity.aggregateSchemaVersion;
|
|
210
|
+
if (!compatibleRecordOnlyOlder) {
|
|
211
|
+
throw new Error(`Managed control-plane storage is not current: ${storage.status}.`);
|
|
196
212
|
}
|
|
213
|
+
(options.openCompatibleStore ?? openCompatibleFileTaskStore)(home).getConfig();
|
|
197
214
|
}
|
|
198
|
-
|
|
199
|
-
throw new Error("
|
|
200
|
-
+
|
|
215
|
+
if (storage.currentLayoutVersion !== identity.storageLayoutVersion) {
|
|
216
|
+
throw new Error("Managed control-plane storage layout is incompatible "
|
|
217
|
+
+ `(expected ${identity.storageLayoutVersion}, found `
|
|
218
|
+
+ `${storage.currentLayoutVersion ?? "unknown"}).`);
|
|
219
|
+
}
|
|
220
|
+
if (storage.currentAggregateSchemaVersion !== identity.aggregateSchemaVersion) {
|
|
221
|
+
throw new Error("Managed control-plane aggregate schema is incompatible "
|
|
222
|
+
+ `(expected ${identity.aggregateSchemaVersion}, found `
|
|
223
|
+
+ `${storage.currentAggregateSchemaVersion ?? "unknown"}).`);
|
|
201
224
|
}
|
|
202
225
|
if (options.checkController !== false) {
|
|
203
226
|
const call = options.callController ?? defaultCallController;
|
|
204
227
|
try {
|
|
205
|
-
const status = await call(
|
|
206
|
-
assertControllerContinuityIdentity(status,
|
|
228
|
+
const status = await call(home, "controller.status", {});
|
|
229
|
+
assertControllerContinuityIdentity(status, identity);
|
|
207
230
|
}
|
|
208
231
|
catch (error) {
|
|
209
232
|
if (!isDefinitelyNotRunning(error))
|
|
210
233
|
throw error;
|
|
211
234
|
}
|
|
212
235
|
}
|
|
213
|
-
return
|
|
236
|
+
return identity;
|
|
214
237
|
}
|
|
215
238
|
export function assertControllerStatusIdentity(status, expected = yuiVersionIdentity()) {
|
|
216
239
|
if (!isRecord(status) || status.running !== true) {
|
|
@@ -539,16 +562,3 @@ function isRecord(value) {
|
|
|
539
562
|
function isDefinitelyNotRunning(error) {
|
|
540
563
|
return isRecord(error) && error.code === "CONTROLLER_NOT_RUNNING";
|
|
541
564
|
}
|
|
542
|
-
function defaultReadActiveRelease(home) {
|
|
543
|
-
try {
|
|
544
|
-
const pointer = readActiveReleasePointer(home);
|
|
545
|
-
return pointer === null
|
|
546
|
-
? null
|
|
547
|
-
: { buildId: pointer.buildId, packageDigest: pointer.packageDigest };
|
|
548
|
-
}
|
|
549
|
-
catch {
|
|
550
|
-
// A damaged pointer fails closed: treat it as an active release the
|
|
551
|
-
// descriptor cannot match, rather than silently skipping the gate.
|
|
552
|
-
return { buildId: "unknown", packageDigest: "unknown" };
|
|
553
|
-
}
|
|
554
|
-
}
|
|
@@ -21,7 +21,9 @@ export function projectFirstProgressStopLoss(input) {
|
|
|
21
21
|
...input.events
|
|
22
22
|
.filter((event) => typeof event.payload.leaderRunId === "string")
|
|
23
23
|
.map((event) => ({ at: event.createdAt, ref: `event:${event.id}` })),
|
|
24
|
-
...input.workItems
|
|
24
|
+
...input.workItems
|
|
25
|
+
.filter((item) => item.status !== "retired")
|
|
26
|
+
.map((item) => ({ at: item.createdAt, ref: `work-item:${item.id}` })),
|
|
25
27
|
...input.reviewRounds.map((round) => ({ at: round.createdAt, ref: `review-round:${round.id}` })),
|
|
26
28
|
...input.integrations.map((attempt) => ({ at: attempt.createdAt, ref: `integration-attempt:${attempt.id}` }))
|
|
27
29
|
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isDurableJobTerminal } from "../job/durableJob.js";
|
|
3
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
3
4
|
/**
|
|
4
5
|
* Canonical SHA-256 digest over the normalized actionable facts. Pure and
|
|
5
6
|
* deterministic: the same facts always produce the same digest regardless of
|
|
@@ -46,7 +47,8 @@ export function collectTaskActionability(store, taskId) {
|
|
|
46
47
|
throw new Error(`Task not found for actionability projection: ${taskId}.`);
|
|
47
48
|
}
|
|
48
49
|
const facts = [];
|
|
49
|
-
|
|
50
|
+
const events = store.listEvents?.(taskId) ?? [];
|
|
51
|
+
for (const run of operationalTaskRecords(store.listAgentRuns(taskId), events, "agent-run").filter((candidate) => candidate.status === "active")) {
|
|
50
52
|
facts.push({
|
|
51
53
|
key: `active-run:${run.id}`,
|
|
52
54
|
value: [
|
|
@@ -95,7 +97,7 @@ export function collectTaskActionability(store, taskId) {
|
|
|
95
97
|
value: `${request.status}|${request.updatedAt}`
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
|
-
for (const message of store.listMessages?.(taskId) ?? []) {
|
|
100
|
+
for (const message of operationalTaskRecords(store.listMessages?.(taskId) ?? [], events, "message")) {
|
|
99
101
|
if (message.wakePolicy !== "leader")
|
|
100
102
|
continue;
|
|
101
103
|
facts.push({
|
|
@@ -3,6 +3,7 @@ import { queueLeaderWakeup } from "./wakeupQueue.js";
|
|
|
3
3
|
import { wakeReason } from "./wakeReason.js";
|
|
4
4
|
import { projectTaskExecution } from "./taskExecutionProjection.js";
|
|
5
5
|
import { collectTaskActionability, computeActionabilityDigest, decideOrphanWake } from "./actionability.js";
|
|
6
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
6
7
|
/**
|
|
7
8
|
* Repairs an active Task that has no durable owner capable of advancing it.
|
|
8
9
|
* This is a low-frequency safety net; normal transitions enqueue their own
|
|
@@ -96,7 +97,7 @@ function admitOrphanWake(store, taskId) {
|
|
|
96
97
|
* so the admission check never suppresses while a Leader is still running.
|
|
97
98
|
*/
|
|
98
99
|
function findLastLeaderRun(store, taskId) {
|
|
99
|
-
const runs = store.listAgentRuns?.(taskId) ?? [];
|
|
100
|
+
const runs = operationalTaskRecords(store.listAgentRuns?.(taskId) ?? [], store.listEvents?.(taskId) ?? [], "agent-run");
|
|
100
101
|
let latest = null;
|
|
101
102
|
for (const run of runs) {
|
|
102
103
|
if (run.roleName !== "leader")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
1
2
|
import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
2
3
|
import { mailboxBatches } from "../coordination/workMailbox.js";
|
|
3
4
|
import { summarizeExecutionGroup } from "../execution/executionGroup.js";
|
|
@@ -12,7 +13,8 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride) {
|
|
|
12
13
|
if (task === null)
|
|
13
14
|
return null;
|
|
14
15
|
const roles = store.listRoles?.(taskId) ?? [];
|
|
15
|
-
const
|
|
16
|
+
const events = store.listEvents?.(taskId) ?? [];
|
|
17
|
+
const runs = operationalTaskRecords(store.listAgentRuns?.(taskId) ?? [], events, "agent-run");
|
|
16
18
|
const leaderMailbox = store.getWorkMailbox?.({
|
|
17
19
|
kind: "role",
|
|
18
20
|
taskId,
|
|
@@ -42,7 +44,7 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride) {
|
|
|
42
44
|
...(store.listIntegrationAttempts === undefined
|
|
43
45
|
? {}
|
|
44
46
|
: { integrations: store.listIntegrationAttempts(taskId) }),
|
|
45
|
-
...(store.listEvents === undefined ? {} : { events
|
|
47
|
+
...(store.listEvents === undefined ? {} : { events }),
|
|
46
48
|
...(store.getTaskBrief === undefined ? {} : { brief: store.getTaskBrief(taskId) }),
|
|
47
49
|
pendingWakeup: store.getPendingWakeup?.(taskId) ?? null,
|
|
48
50
|
leaderMailbox,
|
|
@@ -60,7 +62,11 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride) {
|
|
|
60
62
|
export function projectTaskExecutionFromFacts(facts) {
|
|
61
63
|
const executionGroups = facts.executionGroups
|
|
62
64
|
?? collectExecutionGroups(facts.workItems ?? [], facts.reviewRounds ?? []);
|
|
63
|
-
return projectTaskExecution({
|
|
65
|
+
return projectTaskExecution({
|
|
66
|
+
...facts,
|
|
67
|
+
runs: operationalTaskRecords(facts.runs, facts.events ?? [], "agent-run"),
|
|
68
|
+
executionGroups
|
|
69
|
+
});
|
|
64
70
|
}
|
|
65
71
|
/** Alias kept intentionally small for scheduler callers and external read models. */
|
|
66
72
|
export const deriveTaskExecutionProjection = projectTaskExecution;
|
|
@@ -471,7 +477,10 @@ function collectBlockers(workItems, reviewRounds, integrations, openInputs, task
|
|
|
471
477
|
summary: item.outcome ?? `WorkItem ${item.id} is ${item.status}.`
|
|
472
478
|
});
|
|
473
479
|
}
|
|
474
|
-
if (item.status === "pending" && (item.dependsOn ?? []).some((id) =>
|
|
480
|
+
if (item.status === "pending" && (item.dependsOn ?? []).some((id) => {
|
|
481
|
+
const status = byId.get(id)?.status;
|
|
482
|
+
return status !== "completed" && status !== "retired";
|
|
483
|
+
})) {
|
|
475
484
|
blockers.push({
|
|
476
485
|
kind: "work",
|
|
477
486
|
id: item.id,
|
|
@@ -48,6 +48,7 @@ import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeId
|
|
|
48
48
|
import { validateIntegrationQueueEntry } from "../integration/integrationQueueEntry.js";
|
|
49
49
|
import { validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
|
|
50
50
|
import { validateTaskWake } from "../scheduler/taskWake.js";
|
|
51
|
+
import { operationalTaskRecords, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
|
|
51
52
|
import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
|
|
52
53
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
53
54
|
import { assertHomeWritable } from "./upgradeFence.js";
|
|
@@ -709,7 +710,13 @@ export class SqliteTaskStore {
|
|
|
709
710
|
return null;
|
|
710
711
|
// One indexed query (idx_agent_runs_role_status) covers both the active
|
|
711
712
|
// Runs the projection waits on and the Leader Runs the budget consumes.
|
|
712
|
-
const
|
|
713
|
+
const events = this.#sortById(this.#listPayload("events", "task_id = ? AND type IN (?, ?, ?)", [
|
|
714
|
+
taskId,
|
|
715
|
+
TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT,
|
|
716
|
+
TASK_RECORD_RETIRED_EVENT,
|
|
717
|
+
"review.completed"
|
|
718
|
+
]), (event) => event.id);
|
|
719
|
+
const runs = operationalTaskRecords(this.#sortById(this.#listPayload("agent_runs", "task_id = ? AND (status = 'active' OR role_name = 'leader')", [taskId]), (run) => run.id), events, "agent-run");
|
|
713
720
|
return {
|
|
714
721
|
task: {
|
|
715
722
|
id: task.id,
|
|
@@ -721,7 +728,8 @@ export class SqliteTaskStore {
|
|
|
721
728
|
changeSets: this.#sortById(this.#listPayload("change_sets", "task_id = ?", [taskId]), (changeSet) => changeSet.id),
|
|
722
729
|
integrations: this.#sortById(this.#listPayload("integration_attempts", "task_id = ?", [taskId]), (attempt) => attempt.id),
|
|
723
730
|
reviewRounds: this.#sortById(this.#listPayload("review_rounds", "task_id = ?", [taskId]), (round) => round.id),
|
|
724
|
-
taskFinalReviewContractEvents:
|
|
731
|
+
taskFinalReviewContractEvents: events
|
|
732
|
+
.filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
|
|
725
733
|
reviewConfig: this.getReviewConfig(),
|
|
726
734
|
openInputRequests: this.#sortById(this.#listPayload("input_requests", "task_id = ? AND status = 'open'", [taskId]), (request) => request.id),
|
|
727
735
|
activeRuns: runs.filter((run) => run.status === "active"),
|
|
@@ -729,7 +737,7 @@ export class SqliteTaskStore {
|
|
|
729
737
|
reviewOutcomeEvidence: {
|
|
730
738
|
agentRuns: this.#sortById(this.#listPayload("agent_runs", "task_id = ?", [taskId]).filter((run) => run.purpose === "review"), (run) => run.id),
|
|
731
739
|
reviewFindings: this.listReviewFindings(taskId),
|
|
732
|
-
events:
|
|
740
|
+
events: events.filter((event) => event.type === "review.completed")
|
|
733
741
|
}
|
|
734
742
|
};
|
|
735
743
|
}
|
|
@@ -739,7 +747,7 @@ export class SqliteTaskStore {
|
|
|
739
747
|
return null;
|
|
740
748
|
return {
|
|
741
749
|
...base,
|
|
742
|
-
agentRuns: this.listAgentRuns(taskId),
|
|
750
|
+
agentRuns: operationalTaskRecords(this.listAgentRuns(taskId), this.listEvents(taskId), "agent-run"),
|
|
743
751
|
roleSessionSets: this.listRoleSessionSets(taskId),
|
|
744
752
|
managedWorkspaces: this.#sortById(this.#listPayload("managed_workspaces", "task_id = ?", [taskId]), (workspace) => managedWorkspaceKey(workspace.owner)),
|
|
745
753
|
durableJobs: this.#sortById(this.#listPayload("durable_jobs", "task_id = ?", [taskId]), (job) => job.id),
|
|
@@ -34,6 +34,7 @@ import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../scheduler/leaderFailur
|
|
|
34
34
|
import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../scheduler/operatorNotification.js";
|
|
35
35
|
import { CURRENT_TASK_WAKE_SCHEMA_VERSION, validateTaskWake } from "../scheduler/taskWake.js";
|
|
36
36
|
import { validateTask } from "../task/task.js";
|
|
37
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
37
38
|
import { TASK_RECORD_ID_PREFIXES, validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
38
39
|
import { workItemExecutionGroupById, validateWorkItem } from "../workItem/workItem.js";
|
|
39
40
|
import { isExecutionGroupTransition, validateExecutionGroup } from "../execution/executionGroup.js";
|
|
@@ -409,7 +410,8 @@ export class FileTaskStore {
|
|
|
409
410
|
const aggregate = this.#state().tasks[taskId];
|
|
410
411
|
if (aggregate === undefined)
|
|
411
412
|
return null;
|
|
412
|
-
const
|
|
413
|
+
const events = values(aggregate.events, "id");
|
|
414
|
+
const agentRuns = operationalTaskRecords(values(aggregate.agentRuns, "id"), events, "agent-run");
|
|
413
415
|
return {
|
|
414
416
|
task: {
|
|
415
417
|
id: aggregate.task.id,
|
|
@@ -421,7 +423,7 @@ export class FileTaskStore {
|
|
|
421
423
|
changeSets: values(aggregate.changeSets, "id"),
|
|
422
424
|
integrations: values(aggregate.integrationAttempts, "id"),
|
|
423
425
|
reviewRounds: values(aggregate.reviewRounds, "id"),
|
|
424
|
-
taskFinalReviewContractEvents:
|
|
426
|
+
taskFinalReviewContractEvents: events
|
|
425
427
|
.filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
|
|
426
428
|
reviewConfig: this.getReviewConfig(),
|
|
427
429
|
openInputRequests: values(aggregate.inputRequests, "id")
|
|
@@ -432,7 +434,7 @@ export class FileTaskStore {
|
|
|
432
434
|
agentRuns: agentRuns.filter((run) => run.purpose === "review"),
|
|
433
435
|
// The rollback file backend has no finding-ledger records.
|
|
434
436
|
reviewFindings: [],
|
|
435
|
-
events:
|
|
437
|
+
events: events.filter((event) => (event.type === "review.completed"))
|
|
436
438
|
}
|
|
437
439
|
};
|
|
438
440
|
}
|
|
@@ -451,7 +453,7 @@ export class FileTaskStore {
|
|
|
451
453
|
}
|
|
452
454
|
return {
|
|
453
455
|
...base,
|
|
454
|
-
agentRuns: this.listAgentRuns(taskId),
|
|
456
|
+
agentRuns: operationalTaskRecords(this.listAgentRuns(taskId), values(aggregate.events, "id"), "agent-run"),
|
|
455
457
|
roleSessionSets: this.listRoleSessionSets(taskId),
|
|
456
458
|
managedWorkspaces: values(aggregate.managedWorkspaces, (workspace) => managedWorkspaceKey(workspace.owner)),
|
|
457
459
|
durableJobs: values(aggregate.durableJobs, "id"),
|