@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
@@ -8,6 +8,7 @@ import { taskDeliveryPath } from "../task/task.js";
8
8
  import { inspectTaskRoleSessionRecovery } from "./taskRoleRuntimeStatus.js";
9
9
  import { summarizeExecutionGroup } from "../execution/executionGroup.js";
10
10
  import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
11
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
11
12
  const RECENT_RECORD_LIMIT = 5;
12
13
  const RELATED_RECORD_LIMIT = 5;
13
14
  const SUMMARY_TEXT_LIMIT = 400;
@@ -41,7 +42,8 @@ export function runTaskContextCommand(args, store) {
41
42
  roleName: role.name
42
43
  }))
43
44
  ].filter((mailbox) => mailbox !== null);
44
- const agentRuns = chronological(reader.listAgentRuns(task.id));
45
+ const events = reader.listEvents(task.id);
46
+ const agentRuns = chronological(operationalTaskRecords(reader.listAgentRuns(task.id), events, "agent-run"));
45
47
  const reviewRounds = chronological(reader.listReviewRounds(task.id));
46
48
  const changeSets = chronological(reader.listChangeSets(task.id));
47
49
  const integrations = chronological(reader.listIntegrationAttempts(task.id));
@@ -70,10 +72,10 @@ export function runTaskContextCommand(args, store) {
70
72
  changeSets,
71
73
  integrations,
72
74
  publications,
73
- messages: reader.listMessages(task.id),
75
+ messages: operationalTaskRecords(reader.listMessages(task.id), events, "message"),
74
76
  openInputRequests: inputRequests.filter((request) => request.status === "open"),
75
77
  resolvedInputRequests: inputRequests.filter((request) => request.status !== "open"),
76
- events: reader.listEvents(task.id),
78
+ events,
77
79
  nextAction: projectNextAction(nextActionFacts)
78
80
  };
79
81
  });
@@ -4,6 +4,7 @@ import { projectCompletionReadiness } from "../task/completionReadiness.js";
4
4
  import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
5
5
  import { taskDeliveryPath } from "../task/task.js";
6
6
  import { projectTaskOrchestration } from "../observability/orchestrationMetrics.js";
7
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
7
8
  /**
8
9
  * Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
9
10
  * Folds the existing durable records into exactly one protocol-level next
@@ -57,9 +58,10 @@ export function runTaskNextActionCommand(args, store) {
57
58
  throw taskNotFound(taskId);
58
59
  completionReadiness = projectCompletionReadiness(readinessFacts);
59
60
  }
61
+ const events = reader.listEvents(taskId);
60
62
  const orchestration = projectTaskOrchestration({
61
63
  task: reader.getTask(taskId),
62
- runs: reader.listAgentRuns(taskId),
64
+ runs: operationalTaskRecords(reader.listAgentRuns(taskId), events, "agent-run"),
63
65
  roleSessionSets: reader.listRoleSessionSets(taskId),
64
66
  workItems: reader.listWorkItems(taskId),
65
67
  changeSets: reader.listChangeSets(taskId),
@@ -69,7 +71,7 @@ export function runTaskNextActionCommand(args, store) {
69
71
  durableJobs: reader.listDurableJobs(taskId),
70
72
  publications: reader.listPublicationReferences(taskId),
71
73
  decisions: reader.listDecisions(taskId),
72
- events: reader.listEvents(taskId),
74
+ events,
73
75
  managedWorkspaces: reader.listManagedWorkspaces(taskId)
74
76
  });
75
77
  return {
@@ -226,7 +226,8 @@ function collectBlockers(workItems, openInputRequests, attention) {
226
226
  }
227
227
  if (item.status !== "pending")
228
228
  continue;
229
- const dependencies = item.dependsOn.filter((dependency) => (workById.get(dependency)?.status !== "completed"));
229
+ const dependencies = item.dependsOn.filter((dependency) => (workById.get(dependency)?.status !== "completed"
230
+ && workById.get(dependency)?.status !== "retired"));
230
231
  if (dependencies.length === 0)
231
232
  continue;
232
233
  blockers.push({
@@ -7,6 +7,7 @@ import { classifyRuntimeHealth, projectRuntimeMailbox, projectRuntimeObservation
7
7
  import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
8
8
  import { resolveRuntimeHealth } from "../config/yuiConfig.js";
9
9
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
10
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
10
11
  export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
11
12
  const taskOpenInputRequestCount = store.listInputRequests(taskId)
12
13
  .filter((request) => request.status === "open").length;
@@ -140,7 +141,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
140
141
  // Issue 09: the last Run outcome is a separate axis from the Session
141
142
  // lifecycle. A Session that stops after its Run yielded must not retroactively
142
143
  // turn that Run into a failure; the status display keeps both visible.
143
- const lastRun = store.listAgentRuns(taskId)
144
+ const lastRun = operationalTaskRecords(store.listAgentRuns(taskId), store.listEvents(taskId), "agent-run")
144
145
  .filter((candidate) => candidate.roleName === role.name)
145
146
  .sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt))[0]
146
147
  ?? null;
@@ -1,3 +1,4 @@
1
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
1
2
  import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
2
3
  import { RUN_BOOTSTRAP_MAX_DELTAS } from "./runContextContract.js";
3
4
  import { contextContentDigest, contextSnapshotRef, createContextSnapshot, validateContextSnapshot } from "./contextSnapshot.js";
@@ -213,9 +214,12 @@ function collectAuthorizedContext(store, run) {
213
214
  if (view === "worker") {
214
215
  for (const dependencyId of item.dependsOn) {
215
216
  const dependency = store.getWorkItem(task.id, dependencyId);
216
- if (dependency === null || dependency.status !== "completed") {
217
+ if (dependency === null
218
+ || (dependency.status !== "completed" && dependency.status !== "retired")) {
217
219
  throw new Error(`Run WorkItem dependency is not accepted: ${dependencyId}.`);
218
220
  }
221
+ if (dependency.status === "retired")
222
+ continue;
219
223
  result.push(materialize("L3", "accepted-work-item", dependency.id, dependency));
220
224
  }
221
225
  }
@@ -242,7 +246,8 @@ function collectAuthorizedContext(store, run) {
242
246
  }
243
247
  }
244
248
  if (view === "leader") {
245
- for (const item of store.listWorkItems(task.id)) {
249
+ const events = store.listEvents(task.id);
250
+ for (const item of store.listWorkItems(task.id).filter(({ status }) => status !== "retired")) {
246
251
  result.push(materialize("L3", "work-item", item.id, item));
247
252
  }
248
253
  for (const decision of store.listDecisions(task.id)) {
@@ -257,14 +262,13 @@ function collectAuthorizedContext(store, run) {
257
262
  for (const finding of store.listReviewFindings(task.id)) {
258
263
  result.push(materialize("L3", "review-finding", finding.id, finding));
259
264
  }
260
- for (const agentRun of store.listAgentRuns(task.id).slice(-24)) {
265
+ for (const agentRun of operationalTaskRecords(store.listAgentRuns(task.id), events, "agent-run").slice(-24)) {
261
266
  result.push(materialize("L4", "agent-run", agentRun.id, agentRun));
262
267
  }
263
- for (const message of store.listMessages(task.id).slice(-16)) {
268
+ for (const message of operationalTaskRecords(store.listMessages(task.id), events, "message").slice(-16)) {
264
269
  result.push(materialize("L4", "task-message", message.id, message));
265
270
  }
266
271
  const publishedTreeAuthorizations = [];
267
- const events = store.listEvents(task.id);
268
272
  for (let index = events.length - 1; index >= 0; index -= 1) {
269
273
  const event = events[index];
270
274
  if (event.type === "task.completed" || event.type === "task.reopened")
@@ -1,10 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
- import { chmodSync, readFileSync } from "node:fs";
3
- import { join, resolve } from "node:path";
4
- import { exactControlPlaneCommandPrefix, exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
2
+ import { chmodSync, existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
5
5
  import { writeTextFileAtomically } from "../storage/durableFile.js";
6
6
  import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
7
7
  export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
8
+ const ORDINARY_SESSION_CLI = [
9
+ "#!/bin/sh",
10
+ "exec yui \"$@\"",
11
+ ""
12
+ ].join("\n");
8
13
  /** Read back one immutable Session Manifest and verify its content digest. */
9
14
  export function readSessionBootstrapManifest(path) {
10
15
  const source = resolve(path);
@@ -79,11 +84,10 @@ export function materializeSessionBootstrap(input) {
79
84
  const controlDigest = exactControlPlaneDigest(input.controlPlane);
80
85
  const descriptorPath = resolve(join(home, "runtime", "control-plane", `${controlDigest}.json`));
81
86
  writeImmutableText(descriptorPath, `${serializeExactDescriptor(input.controlPlane)}\n`);
82
- const sessionCliContent = [
83
- "#!/bin/sh",
84
- `exec ${exactControlPlaneCommandPrefix(input.controlPlane)} \"$@\"`,
85
- ""
86
- ].join("\n");
87
+ // Session identity is carried by the immutable Manifest and durable Role/
88
+ // Run fences. Resolve the ordinary CLI on every invocation so package or
89
+ // release upgrades do not invalidate a still-current native Session.
90
+ const sessionCliContent = ORDINARY_SESSION_CLI;
87
91
  const sessionCliDigest = digest(sessionCliContent);
88
92
  const sessionCliPath = resolve(join(home, "runtime", "session-cli", `yui-${sessionCliDigest}.sh`));
89
93
  writeImmutableText(sessionCliPath, sessionCliContent);
@@ -122,11 +126,11 @@ export function materializeSessionBootstrap(input) {
122
126
  roleProfileRef: { digest: profileDigest, path: roleProfilePath },
123
127
  contextProtocol: input.owner.scope === "global"
124
128
  ? {
125
- loadCommand: `\"${sessionCliPath}\" session context \"$YUI_ROLE\" --json`
129
+ loadCommand: "yui session context \"$YUI_ROLE\" --json"
126
130
  }
127
131
  : {
128
- loadCommand: `\"${sessionCliPath}\" task run context \"$YUI_TASK_ID/<run-id>\" --json`,
129
- expandCommand: `\"${sessionCliPath}\" task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json`
132
+ loadCommand: "yui task run context \"$YUI_TASK_ID/<run-id>\" --json",
133
+ expandCommand: "yui task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json"
130
134
  }
131
135
  };
132
136
  const manifest = Object.freeze({ ...body, digest: digest(body) });
@@ -140,6 +144,68 @@ export function materializeSessionBootstrap(input) {
140
144
  descriptorPath
141
145
  });
142
146
  }
147
+ /**
148
+ * Converts wrappers produced before the protocol-compatible Session CLI to an
149
+ * ordinary `yui` invocation. Only a valid Session Manifest may nominate a
150
+ * wrapper, and only the exact legacy two-line wrapper shape is changed. The
151
+ * Manifest and its frozen descriptor stay immutable and continue to
152
+ * authenticate the Session; repeated refreshes are no-ops.
153
+ */
154
+ export function refreshManagedSessionCliWrappers(homeInput) {
155
+ const home = resolve(homeInput);
156
+ const manifestDirectory = resolve(join(home, "runtime", "session-manifests"));
157
+ const sessionCliDirectory = resolve(join(home, "runtime", "session-cli"));
158
+ if (!existsSync(manifestDirectory)) {
159
+ return Object.freeze({ refreshed: 0, current: 0, skipped: 0 });
160
+ }
161
+ const wrapperPaths = new Set();
162
+ let skipped = 0;
163
+ for (const name of readdirSync(manifestDirectory).filter((entry) => entry.endsWith(".json"))) {
164
+ const manifestPath = resolve(join(manifestDirectory, name));
165
+ try {
166
+ const manifest = readSessionBootstrapManifest(manifestPath);
167
+ if (manifestPath !== resolve(join(manifestDirectory, `${manifest.digest}.json`))) {
168
+ skipped += 1;
169
+ continue;
170
+ }
171
+ const wrapperPath = resolve(manifest.controlPlane.sessionCliPath);
172
+ if (dirname(wrapperPath) !== sessionCliDirectory) {
173
+ skipped += 1;
174
+ continue;
175
+ }
176
+ wrapperPaths.add(wrapperPath);
177
+ }
178
+ catch {
179
+ // Historical or incomplete manifests are audit material. They must not
180
+ // block current Sessions or an otherwise compatible package update.
181
+ skipped += 1;
182
+ }
183
+ }
184
+ let refreshed = 0;
185
+ let current = 0;
186
+ for (const wrapperPath of wrapperPaths) {
187
+ if (!existsSync(wrapperPath)) {
188
+ skipped += 1;
189
+ continue;
190
+ }
191
+ const content = readFileSync(wrapperPath, "utf8");
192
+ if (content === ORDINARY_SESSION_CLI) {
193
+ current += 1;
194
+ continue;
195
+ }
196
+ if (!isLegacyExactSessionCli(content)) {
197
+ skipped += 1;
198
+ continue;
199
+ }
200
+ writeTextFileAtomically(wrapperPath, ORDINARY_SESSION_CLI);
201
+ chmodSync(wrapperPath, 0o700);
202
+ refreshed += 1;
203
+ }
204
+ return Object.freeze({ refreshed, current, skipped });
205
+ }
206
+ function isLegacyExactSessionCli(content) {
207
+ return /^#!\/bin\/sh\nexec [^\n]+ '--yui-control' '[a-f0-9]{64}' "\$@"\n$/u.test(content);
208
+ }
143
209
  function writeImmutableText(path, content) {
144
210
  writeTextFileAtomically(path, content);
145
211
  chmodSync(path, 0o600);
@@ -1,4 +1,5 @@
1
1
  import { renderWakeReason } from "../scheduler/wakeReason.js";
2
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
2
3
  /**
3
4
  * Issue 04 (context token budget) — long-term design:
4
5
  *
@@ -25,12 +26,13 @@ export function buildTaskWakeEnvelope(reader, request) {
25
26
  throw new Error(`Wake envelope ${request.wakeId} must carry at least one reason.`);
26
27
  }
27
28
  const fromTime = Date.parse(request.fromCursor);
29
+ const events = reader.listEvents(request.taskId);
28
30
  const counts = {
29
- events: reader.listEvents(request.taskId)
31
+ events: events
30
32
  .filter((record) => Date.parse(record.createdAt) > fromTime).length,
31
- messages: reader.listMessages(request.taskId)
33
+ messages: operationalTaskRecords(reader.listMessages(request.taskId), events, "message")
32
34
  .filter((record) => Date.parse(record.createdAt) > fromTime).length,
33
- runs: reader.listAgentRuns(request.taskId)
35
+ runs: operationalTaskRecords(reader.listAgentRuns(request.taskId), events, "agent-run")
34
36
  .filter((record) => Date.parse(record.createdAt) > fromTime).length
35
37
  };
36
38
  const lines = [
@@ -7,7 +7,7 @@ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
7
7
  import { hasRuntimeLifecycleWork } from "../runtime/lifecycleReservation.js";
8
8
  import { assertControllerStatusIdentity } from "../runtime/exactControlPlane.js";
9
9
  import { EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES } from "./domainIdentity.js";
10
- import { YUI_VERSION, yuiVersionIdentity } from "../version.js";
10
+ import { yuiVersionIdentity } from "../version.js";
11
11
  import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
12
12
  import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
13
13
  import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
@@ -137,12 +137,8 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
137
137
  + "Run `yui controller restart` before writing new task records.");
138
138
  }
139
139
  const actualVersion = statusRecord.version;
140
- const expected = expectedVersion ?? YUI_VERSION;
141
- const versionMismatch = expectedVersion === undefined
142
- ? typeof actualVersion === "string" && actualVersion !== expected
143
- : actualVersion !== expected;
144
- if (versionMismatch) {
145
- throw new Error(`Controller version is incompatible (expected ${expected}, found ${typeof actualVersion === "string" ? actualVersion : "unknown"}). `
140
+ if (expectedVersion !== undefined && actualVersion !== expectedVersion) {
141
+ throw new Error(`Controller version is incompatible (expected ${expectedVersion}, found ${typeof actualVersion === "string" ? actualVersion : "unknown"}). `
146
142
  + "Run `yui controller restart` before writing new task records.");
147
143
  }
148
144
  // Ordinary callers must authenticate the complete control-plane identity.
@@ -151,7 +147,11 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
151
147
  // path authenticates its executable, argv, and version immediately after
152
148
  // readiness in ensureFileTaskControllerIdentity.
153
149
  if (expectedVersion === undefined) {
154
- assertControllerStatusIdentity(status, yuiVersionIdentity());
150
+ const identity = yuiVersionIdentity();
151
+ assertControllerStatusIdentity(status, {
152
+ ...identity,
153
+ version: typeof actualVersion === "string" ? actualVersion : identity.version
154
+ });
155
155
  }
156
156
  }
157
157
  function spawnDetachedFileTaskController(home, environment) {
@@ -6,6 +6,7 @@ import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
6
6
  import { boundProviderRetryBeforeFirstProgress, projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
7
7
  import { hasRecentTurnId } from "../executor/turnCompletion.js";
8
8
  import { createTaskEvent } from "../event/taskEvent.js";
9
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
9
10
  import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
10
11
  import { createTaskWake, fallbackWakeCursor, latestTaskWake } from "../scheduler/taskWake.js";
11
12
  import { rolloverTaskRoleSessionForContextBudget } from "../lifecycle/contextBudgetRollover.js";
@@ -596,7 +597,7 @@ export class FileSchedulerStoreAdapter {
596
597
  const latest = latestTaskWake(reader.listTaskWakes(taskId));
597
598
  const fromCursor = latest?.toCursor ?? fallbackWakeCursor({
598
599
  taskCreatedAt: task.createdAt,
599
- leaderRunCreatedAt: reader.listAgentRuns(taskId)
600
+ leaderRunCreatedAt: operationalTaskRecords(reader.listAgentRuns(taskId), reader.listEvents(taskId), "agent-run")
600
601
  .filter((run) => run.roleName === "leader")
601
602
  .at(-1)?.createdAt
602
603
  });
@@ -3706,7 +3707,7 @@ function bindTaskRoleRunInFlight(store, role, run, now) {
3706
3707
  agentId,
3707
3708
  runId: run.id,
3708
3709
  receiptId: agentRunDeliveryReceiptId(run)
3709
- }, now);
3710
+ }, now, run.mode);
3710
3711
  store.saveRoleSessionSet(updated);
3711
3712
  }
3712
3713
  function markTaskRoleRunPushedInFlight(store, role, run, now) {
@@ -292,7 +292,7 @@ export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
292
292
  throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
293
293
  + "Stop the existing native process before starting a fresh Session.");
294
294
  }
295
- export function bindTaskRoleRun(set, fence, preparedAt) {
295
+ export function bindTaskRoleRun(set, fence, preparedAt, mode) {
296
296
  validateRoleSessionSet(set);
297
297
  assertTaskRoleSessionSet(set);
298
298
  const normalized = normalizeTaskRoleRunFence(fence);
@@ -305,9 +305,9 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
305
305
  throw new Error("Task Role session set already has an in-flight Run.");
306
306
  }
307
307
  const timestamp = requireDate(preparedAt, "Task Role Run preparedAt");
308
- const providerBinding = set.providerBinding === null
309
- ? null
310
- : rebindProviderRuntimeRun(set.providerBinding, normalized.runId);
308
+ const providerBinding = mode === "resume" && set.providerBinding !== null
309
+ ? rebindProviderRuntimeRun(set.providerBinding, normalized.runId)
310
+ : null;
311
311
  const updated = {
312
312
  ...set,
313
313
  inFlight: { ...normalized, preparedAt: timestamp },
@@ -19,8 +19,8 @@ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.j
19
19
  import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
20
20
  import { activeLiveRoleAgentSession } from "./agentExecutor.js";
21
21
  import { effectiveLaunchSnapshotsCompatibleForTaskMain, effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
22
- import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor, 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 {
@@ -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];