@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.
Files changed (33) hide show
  1. package/README.md +20 -3
  2. package/dist/cli/commandCatalog.js +20 -3
  3. package/dist/cli/updatePorts.js +6 -0
  4. package/dist/cli.js +120 -43
  5. package/dist/commands/globalRoleCommands.js +8 -4
  6. package/dist/commands/taskCommands.js +141 -9
  7. package/dist/commands/taskContextCommand.js +5 -3
  8. package/dist/commands/taskNextActionCommand.js +4 -2
  9. package/dist/commands/taskOverviewCommand.js +2 -1
  10. package/dist/commands/taskRoleRuntimeStatus.js +2 -1
  11. package/dist/context/runContextPack.js +9 -5
  12. package/dist/context/sessionBootstrapManifest.js +77 -11
  13. package/dist/context/wakeNotification.js +5 -3
  14. package/dist/controller/clientRuntime.js +8 -8
  15. package/dist/controller/fileSchedulerStoreAdapter.js +3 -2
  16. package/dist/executor/agentExecutor.js +4 -4
  17. package/dist/executor/fileRoleLaunchPlanner.js +21 -34
  18. package/dist/review/reviewOutcomeClassifier.js +15 -4
  19. package/dist/review/taskFinalReviewContractRebind.js +28 -11
  20. package/dist/runtime/exactControlPlane.js +47 -37
  21. package/dist/runtime/firstProgressStopLoss.js +3 -1
  22. package/dist/scheduler/actionability.js +4 -2
  23. package/dist/scheduler/activeTaskProgress.js +2 -1
  24. package/dist/scheduler/taskExecutionProjection.js +13 -4
  25. package/dist/storage/sqliteStore.js +12 -4
  26. package/dist/storage/taskStore.js +6 -4
  27. package/dist/task/nextAction.js +8 -3
  28. package/dist/task/taskRecordRetirement.js +72 -0
  29. package/dist/workItem/workItem.js +6 -4
  30. package/i18n/README.zh-CN.md +20 -1
  31. package/package.json +1 -1
  32. package/skills/yui-operator/SKILL.md +7 -0
  33. 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 legacyObservations = firstRebindAt === undefined
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
- // Issue 02: bind the descriptor to the active release. A descriptor that
178
- // carries a build ID must match the Home's active release pointer; a
179
- // descriptor without a build ID is rejected once a Home has an active
180
- // release (old Sessions cannot mutate a released control plane). Homes
181
- // without an active release pointer keep the legacy continuity contract.
182
- const readActiveRelease = options.readActiveRelease ?? defaultReadActiveRelease;
183
- const activeRelease = readActiveRelease(descriptor.yuiHome);
184
- if (descriptor.buildId !== undefined) {
185
- if (activeRelease === null) {
186
- throw new Error("Exact control-plane descriptor names a release build but the Home has "
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
- if (activeRelease.buildId !== descriptor.buildId) {
190
- throw new Error("Exact control-plane release changed since the descriptor was frozen "
191
- + `(expected ${descriptor.buildId}, active ${activeRelease.buildId}).`);
187
+ catch (error) {
188
+ if (!isDefinitelyNotRunning(error))
189
+ throw error;
192
190
  }
193
- if (descriptor.activeReleaseDigest !== undefined
194
- && activeRelease.packageDigest !== descriptor.activeReleaseDigest) {
195
- throw new Error("Exact control-plane release digest does not match the active release pointer.");
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
- else if (activeRelease !== null) {
199
- throw new Error("Exact control-plane descriptor predates the active release; old Sessions "
200
- + "cannot mutate a released control plane. Re-launch through the current release.");
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(descriptor.yuiHome, "controller.status", {});
206
- assertControllerContinuityIdentity(status, descriptor.identity);
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 descriptor;
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.map((item) => ({ at: item.createdAt, ref: `work-item:${item.id}` })),
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
- for (const run of store.listAgentRuns(taskId).filter((candidate) => candidate.status === "active")) {
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 runs = store.listAgentRuns?.(taskId) ?? [];
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: store.listEvents(taskId) }),
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({ ...facts, executionGroups });
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) => byId.get(id)?.status !== "completed")) {
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 runs = this.#sortById(this.#listPayload("agent_runs", "task_id = ? AND (status = 'active' OR role_name = 'leader')", [taskId]), (run) => run.id);
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: this.#sortById(this.#listPayload("events", "task_id = ? AND type = ?", [taskId, TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT]), (event) => event.id),
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: this.listEvents(taskId).filter((event) => event.type === "review.completed")
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 agentRuns = values(aggregate.agentRuns, "id");
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: values(aggregate.events, "id")
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: values(aggregate.events, "id").filter((event) => (event.type === "review.completed"))
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"),
@@ -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) => byId.get(dependencyId)?.status === "completed")));
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
- .find((dependencyId) => byId.get(dependencyId)?.status !== "completed");
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("Only the Task Leader may retire a Work Item.");
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: "leader",
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");
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -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 exact authorized context for every Yui-managed Leader, Worker, Reviewer, Operator, or custom Role Run, and complete that Run through its bounded control-plane protocol.
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 exact Session CLI:
15
+ 2. Before acting, load its authorized pack with the ordinary Yui CLI command:
16
16
 
17
17
  ```sh
18
- "$YUI_SESSION_CLI" task run context "$YUI_TASK_ID/<run-id>" --json
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
- "$YUI_SESSION_CLI" task run context expand "$YUI_TASK_ID/<run-id>" <ref-id> --store <store> --mode full --json
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 exact view
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
- "$YUI_SESSION_CLI" session context "$YUI_ROLE" --json
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