@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.
Files changed (36) 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 +147 -44
  5. package/dist/commands/globalRoleCommands.js +8 -4
  6. package/dist/commands/releaseCommands.js +44 -8
  7. package/dist/commands/taskCommands.js +141 -9
  8. package/dist/commands/taskContextCommand.js +5 -3
  9. package/dist/commands/taskNextActionCommand.js +4 -2
  10. package/dist/commands/taskOverviewCommand.js +2 -1
  11. package/dist/commands/taskRoleRuntimeStatus.js +2 -1
  12. package/dist/context/runContextPack.js +9 -5
  13. package/dist/context/sessionBootstrapManifest.js +77 -11
  14. package/dist/context/wakeNotification.js +5 -3
  15. package/dist/controller/clientRuntime.js +8 -8
  16. package/dist/controller/fileSchedulerStoreAdapter.js +3 -2
  17. package/dist/executor/agentExecutor.js +4 -4
  18. package/dist/executor/fileRoleLaunchPlanner.js +21 -34
  19. package/dist/release/releaseHandover.js +2 -2
  20. package/dist/release/runtimeRelease.js +4 -3
  21. package/dist/review/reviewOutcomeClassifier.js +15 -4
  22. package/dist/review/taskFinalReviewContractRebind.js +28 -11
  23. package/dist/runtime/exactControlPlane.js +47 -37
  24. package/dist/runtime/firstProgressStopLoss.js +3 -1
  25. package/dist/scheduler/actionability.js +4 -2
  26. package/dist/scheduler/activeTaskProgress.js +2 -1
  27. package/dist/scheduler/taskExecutionProjection.js +13 -4
  28. package/dist/storage/sqliteStore.js +12 -4
  29. package/dist/storage/taskStore.js +6 -4
  30. package/dist/task/nextAction.js +8 -3
  31. package/dist/task/taskRecordRetirement.js +72 -0
  32. package/dist/workItem/workItem.js +6 -4
  33. package/i18n/README.zh-CN.md +20 -1
  34. package/package.json +1 -1
  35. package/skills/yui-operator/SKILL.md +7 -0
  36. 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, exactControlPlaneCommandPrefix, exactControlPlaneDigest, exactTaskRuntimeDescriptorPath, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
23
- import { readActiveReleasePointer } from "../release/runtimeRelease.js";
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
- // Freeze the complete control identity before any native process exists.
57
- // This value is immutable for the planner/Controller lifetime and is never
58
- // rewritten through a shared PATH launcher.
59
- // Issue 02: bind the active release build ID so a Session created after a
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
- ...(activeRelease === null
64
+ ...(runningRelease === null
67
65
  ? {}
68
66
  : {
69
- buildId: activeRelease.buildId,
70
- activeReleaseDigest: activeRelease.packageDigest
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, this.#controlPlane, sessionContext.sessionCliPath)
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 preallocatedManagedNativeSessionId = managedControl
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, preallocatedManagedNativeSessionId)
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(preallocatedManagedNativeSessionId, "Native session id");
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
- ? preallocatedManagedNativeSessionId
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, controlPlane, sessionCliPath) {
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(${sessionCliPath} task run context ${taskId}/${runId}:*)`,
754
- `Bash(${exact} --json task context ${taskId})`,
755
- `Bash(${exact} --json task work list ${taskId})`,
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(${exact} --json task work show ${workItemId})`]),
759
- `Bash(${exact} task run yield ${runId} --summary-file -:*)`
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, the stable launcher and new Sessions resolve the new
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 release;
8
- * the stable launcher shim resolves the CLI through that pointer instead of
9
- * symlinking a development checkout.
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 reviewRun = evidence.listAgentRuns(round.taskId).find((run) => (run.purpose === "review"
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 = evidence.listEvents(round.taskId).find((event) => (event.type === "review.completed" && event.payload.reviewRoundId === round.id));
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 runs = store.listAgentRuns(round.taskId).filter((run) => (run.purpose === "review" && run.reviewRoundId === round.id));
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 = store.listEvents(round.taskId).filter((event) => (event.type === "review.completed" && event.payload.reviewRoundId === round.id));
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 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"),