@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
|
@@ -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"),
|
package/dist/task/nextAction.js
CHANGED
|
@@ -650,7 +650,10 @@ function selectOpenWorkItem(workItems) {
|
|
|
650
650
|
const openItems = workItems.filter((item) => OPEN_WORK_ITEM_STATUSES.has(item.status));
|
|
651
651
|
if (openItems.length === 0)
|
|
652
652
|
return { kind: "none" };
|
|
653
|
-
const eligible = openItems.find((item) => (item.dependsOn.every((dependencyId) =>
|
|
653
|
+
const eligible = openItems.find((item) => (item.dependsOn.every((dependencyId) => {
|
|
654
|
+
const status = byId.get(dependencyId)?.status;
|
|
655
|
+
return status === "completed" || status === "retired";
|
|
656
|
+
})));
|
|
654
657
|
if (eligible !== undefined)
|
|
655
658
|
return { kind: "ready", item: eligible };
|
|
656
659
|
let current = openItems[0];
|
|
@@ -660,8 +663,10 @@ function selectOpenWorkItem(workItems) {
|
|
|
660
663
|
return { kind: "blocked", itemId: current.id, blockedBy: current.id };
|
|
661
664
|
}
|
|
662
665
|
visited.add(current.id);
|
|
663
|
-
const blockedBy = current.dependsOn
|
|
664
|
-
|
|
666
|
+
const blockedBy = current.dependsOn.find((dependencyId) => {
|
|
667
|
+
const status = byId.get(dependencyId)?.status;
|
|
668
|
+
return status !== "completed" && status !== "retired";
|
|
669
|
+
});
|
|
665
670
|
if (blockedBy === undefined)
|
|
666
671
|
return { kind: "ready", item: current };
|
|
667
672
|
const dependency = byId.get(blockedBy);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
2
|
+
export const TASK_RECORD_RETIRED_EVENT = "task.record-retired";
|
|
3
|
+
export const RETIRABLE_TASK_RECORD_KINDS = [
|
|
4
|
+
"work-item",
|
|
5
|
+
"message",
|
|
6
|
+
"agent-run"
|
|
7
|
+
];
|
|
8
|
+
/**
|
|
9
|
+
* Appends a tombstone fact without rewriting the original record. Operational
|
|
10
|
+
* projections ignore the retired identity; audit and list surfaces can still
|
|
11
|
+
* render both the original bytes and this reasoned retirement event.
|
|
12
|
+
*/
|
|
13
|
+
export function createTaskRecordRetirement(input, now) {
|
|
14
|
+
return createTaskEvent(input.eventId, input.taskId, TASK_RECORD_RETIRED_EVENT, {
|
|
15
|
+
recordKind: requireRecordKind(input.recordKind),
|
|
16
|
+
recordId: requireText(input.recordId, "Task record id"),
|
|
17
|
+
reason: requireText(input.reason, "Task record retirement reason"),
|
|
18
|
+
retiredBy: requireRetiredBy(input.retiredBy)
|
|
19
|
+
}, now);
|
|
20
|
+
}
|
|
21
|
+
export function taskRecordRetirement(event) {
|
|
22
|
+
if (event.type !== TASK_RECORD_RETIRED_EVENT)
|
|
23
|
+
return null;
|
|
24
|
+
const recordKind = event.payload.recordKind;
|
|
25
|
+
const retiredBy = event.payload.retiredBy;
|
|
26
|
+
if (!RETIRABLE_TASK_RECORD_KINDS.includes(recordKind)
|
|
27
|
+
|| (retiredBy !== "user" && retiredBy !== "operator" && retiredBy !== "leader")
|
|
28
|
+
|| event.payload.recordId?.trim().length === 0
|
|
29
|
+
|| event.payload.reason?.trim().length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
recordKind: recordKind,
|
|
34
|
+
recordId: event.payload.recordId,
|
|
35
|
+
reason: event.payload.reason,
|
|
36
|
+
retiredBy
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function retiredTaskRecordIds(events, recordKind) {
|
|
40
|
+
const ids = new Set();
|
|
41
|
+
for (const event of events) {
|
|
42
|
+
const retirement = taskRecordRetirement(event);
|
|
43
|
+
if (retirement?.recordKind === recordKind)
|
|
44
|
+
ids.add(retirement.recordId);
|
|
45
|
+
}
|
|
46
|
+
return ids;
|
|
47
|
+
}
|
|
48
|
+
export function isTaskRecordRetired(events, recordKind, recordId) {
|
|
49
|
+
return retiredTaskRecordIds(events, recordKind).has(recordId);
|
|
50
|
+
}
|
|
51
|
+
export function operationalTaskRecords(records, events, recordKind) {
|
|
52
|
+
const retired = retiredTaskRecordIds(events, recordKind);
|
|
53
|
+
return retired.size === 0 ? [...records] : records.filter(({ id }) => !retired.has(id));
|
|
54
|
+
}
|
|
55
|
+
function requireRecordKind(value) {
|
|
56
|
+
if (!RETIRABLE_TASK_RECORD_KINDS.includes(value)) {
|
|
57
|
+
throw new Error(`Task record retirement kind is invalid: ${String(value)}.`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
function requireRetiredBy(value) {
|
|
62
|
+
if (value !== "user" && value !== "operator" && value !== "leader") {
|
|
63
|
+
throw new Error(`Task record retirement actor is invalid: ${String(value)}.`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function requireText(value, label) {
|
|
68
|
+
if (typeof value !== "string" || value.includes("\0") || value.trim().length === 0) {
|
|
69
|
+
throw new Error(`${label} is required.`);
|
|
70
|
+
}
|
|
71
|
+
return value.trim();
|
|
72
|
+
}
|
|
@@ -638,8 +638,8 @@ function normalizeProjectBaseRefs(baseRefs) {
|
|
|
638
638
|
});
|
|
639
639
|
}
|
|
640
640
|
function normalizeDisposition(input, now) {
|
|
641
|
-
if (input.by !== "leader") {
|
|
642
|
-
throw new Error("
|
|
641
|
+
if (input.by !== "leader" && input.by !== "operator" && input.by !== "user") {
|
|
642
|
+
throw new Error("Work Item retirement actor is invalid.");
|
|
643
643
|
}
|
|
644
644
|
const summary = requireText(input.summary, "Work item disposition summary");
|
|
645
645
|
const replacementWorkItemId = input.replacementWorkItemId;
|
|
@@ -648,7 +648,7 @@ function normalizeDisposition(input, now) {
|
|
|
648
648
|
}
|
|
649
649
|
const result = {
|
|
650
650
|
schemaVersion: 1,
|
|
651
|
-
by:
|
|
651
|
+
by: input.by,
|
|
652
652
|
summary,
|
|
653
653
|
retiredAt: now.toISOString(),
|
|
654
654
|
...(replacementWorkItemId === undefined ? {} : { replacementWorkItemId })
|
|
@@ -659,7 +659,9 @@ function validateDisposition(disposition) {
|
|
|
659
659
|
if (disposition.schemaVersion !== 1) {
|
|
660
660
|
throw new Error("Work Item disposition must use schemaVersion 1.");
|
|
661
661
|
}
|
|
662
|
-
if (disposition.by !== "leader"
|
|
662
|
+
if (disposition.by !== "leader"
|
|
663
|
+
&& disposition.by !== "operator"
|
|
664
|
+
&& disposition.by !== "user") {
|
|
663
665
|
throw new Error("Work Item disposition actor is invalid.");
|
|
664
666
|
}
|
|
665
667
|
requireText(disposition.summary, "Work item disposition summary");
|
package/i18n/README.zh-CN.md
CHANGED
|
@@ -404,6 +404,19 @@ yui task work accept <task-id>/<work-item-id> --summary "验收标准满足。"
|
|
|
404
404
|
replacement。WorkItem、Integration
|
|
405
405
|
worktree 与检查日志会作为证据保留,直到显式清理。
|
|
406
406
|
|
|
407
|
+
错误的历史指令或执行记录可以从运行投影中废弃,而不删除审计证据:
|
|
408
|
+
|
|
409
|
+
```sh
|
|
410
|
+
yui task message retire <task>/<message> --reason "已被新指令替代"
|
|
411
|
+
yui task run retire <task>/<agent-run> --reason "无效的启动记录"
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
这些命令追加 retirement 事实;列表和审计仍保留并标记原 Message、
|
|
415
|
+
WorkItem 或 AgentRun,而受管 Run 上下文、actionability、恢复、Review 证据和调度会忽略
|
|
416
|
+
它。活动 AgentRun 会先按精确身份终态化;重复废弃是幂等操作。Message 与
|
|
417
|
+
AgentRun 只能由用户或全局 Operator 废弃,WorkItem 也可由所属 Task Leader
|
|
418
|
+
废弃。
|
|
419
|
+
|
|
407
420
|
长期 Task 不依赖 native transcript 恢复。Leader 每次 yield 前更新 Brief
|
|
408
421
|
的 focus 和 leader summary;材料性技术选择写入 Decision;可独立汇报的
|
|
409
422
|
阶段成果写入 Milestone;只有跨 Task 稳定有效的信息才进入 Project
|
|
@@ -481,6 +494,12 @@ binding 是预先保存、可随时切换的配置,而不是并行身份。Ope
|
|
|
481
494
|
并切换。跨 Agent 切换默认复用已保存的 model/effort,只有用户明确选择
|
|
482
495
|
更新时才进入现有配置选择流程。
|
|
483
496
|
|
|
497
|
+
受管理 Session 的普通工作流命令统一调用 PATH 中的 `yui`。Session Manifest
|
|
498
|
+
与持久 Role/Run fence 负责身份认证,CLI 和 Controller 只需满足协议与存储兼容,
|
|
499
|
+
不会因包版本升级而使现有 Session 失效;Provider 回调等内部路径仍保留精确围栏。
|
|
500
|
+
`update` 会幂等刷新旧版本生成的精确 CLI wrapper,使历史 Session 也转为这一
|
|
501
|
+
兼容入口。
|
|
502
|
+
|
|
484
503
|
使用 `yui config role unbind <global-role> <agent-id>` 或 `yui task role unbind <task-id> <role> <agent-id>` 可移除休眠 binding。active binding 或任何未 stopped 的 native session 都会被拒绝;stopped session 记录会和 binding 在同一事务中删除。
|
|
485
504
|
|
|
486
505
|
Claude 的 session ID 在启动前分配,并由持久 stream-json Provider 进程承载多个 Turn;Codex 使用持久 App Server thread。两者都复用同一套 Conversation、Activation、Turn 与 authority fence,不再向模型对话注入 session-bind prompt。
|
|
@@ -502,7 +521,7 @@ yui controller stop
|
|
|
502
521
|
yui controller restart
|
|
503
522
|
```
|
|
504
523
|
|
|
505
|
-
`controller restart` 会用当前安装的 Yui 版本替换 Controller 进程及其调度循环、socket 服务,不会停止或重启已受管的 tmux/Agent
|
|
524
|
+
`controller restart` 会用当前安装的 Yui 版本替换 Controller 进程及其调度循环、socket 服务,不会停止或重启已受管的 tmux/Agent 会话;普通 Session 命令按协议与存储身份兼容,不要求 Controller 与 CLI 包版本完全相同。
|
|
506
525
|
|
|
507
526
|
成功的 `setup`、`upgrade` 和 `update` 都会确保当前 Home 有一个运行中的
|
|
508
527
|
Controller;如果之前没有运行,会在完成后启动。只读命令和
|
package/package.json
CHANGED
|
@@ -319,6 +319,13 @@ ChangeSet is integrated.
|
|
|
319
319
|
Role with `yui task enter <task-id> <role>`.
|
|
320
320
|
- Relay explicit Task information with
|
|
321
321
|
`yui task message send <task-id> "<body>"`.
|
|
322
|
+
- When a proven incorrect historical record is affecting current projections,
|
|
323
|
+
preserve it as audit evidence and append a reasoned retirement with
|
|
324
|
+
`yui task message retire <task>/<message> --reason "..."`,
|
|
325
|
+
`yui task run retire <task>/<run> --reason "..."`, or
|
|
326
|
+
`yui task work retire <task>/<work> --summary "..."`. Inspect the exact
|
|
327
|
+
record first; retirement is not a substitute for normal failure recovery or
|
|
328
|
+
for resolving a still-valid result.
|
|
322
329
|
- Inspect each InputRequest before presenting it. Present questions, choices,
|
|
323
330
|
recommendations, and deadlines exactly only when the request is a user-owned
|
|
324
331
|
boundary (a real choice, authorization, credential, unavailable external
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yui-runtime
|
|
3
|
-
description: Load and use the
|
|
3
|
+
description: Load and use the authorized context for every Yui-managed Leader, Worker, Reviewer, Operator, or custom Role Run, and complete that Run through its bounded control-plane protocol.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Yui Runtime
|
|
@@ -12,10 +12,10 @@ workspace layout, native transcript, or an earlier Run.
|
|
|
12
12
|
For every managed Task Run:
|
|
13
13
|
|
|
14
14
|
1. Read the exact Run identity from the newest Bootstrap Envelope.
|
|
15
|
-
2. Before acting, load its authorized pack with the
|
|
15
|
+
2. Before acting, load its authorized pack with the ordinary Yui CLI command:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
|
|
18
|
+
yui task run context "$YUI_TASK_ID/<run-id>" --json
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
3. Verify that the returned Task, Run, Role, purpose, Snapshot digest, workspace,
|
|
@@ -27,7 +27,7 @@ For every managed Task Run:
|
|
|
27
27
|
`refId`:
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
|
-
|
|
30
|
+
yui task run context expand "$YUI_TASK_ID/<run-id>" <ref-id> --store <store> --mode full --json
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
A bare `<ref-id>` remains supported only when it identifies exactly one
|
|
@@ -42,11 +42,11 @@ The pack's authority view and writable Project IDs are hard boundaries. A
|
|
|
42
42
|
native subagent inherits the parent Run's refs and authority; it does not gain a
|
|
43
43
|
new Yui actor, Run, Session, or cross-Task read permission.
|
|
44
44
|
|
|
45
|
-
For a global Operator or custom GlobalRole Session, load the stable
|
|
45
|
+
For a global Operator or custom GlobalRole Session, load the stable authorized view
|
|
46
46
|
before routing or acting:
|
|
47
47
|
|
|
48
48
|
```sh
|
|
49
|
-
|
|
49
|
+
yui session context "$YUI_ROLE" --json
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
Global context grants no Task implementation workspace. Read a Task only after
|