@zq-silk/yui 0.13.4 → 0.13.5

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 (48) hide show
  1. package/README.md +8 -8
  2. package/dist/cli/commandCatalog.js +22 -21
  3. package/dist/cli/interactionPolicy.js +0 -14
  4. package/dist/cli.js +42 -0
  5. package/dist/commands/executionAuditCommands.js +1 -1
  6. package/dist/commands/taskCommands.js +60 -326
  7. package/dist/commands/taskContextCommand.js +3 -14
  8. package/dist/commands/taskExecutionCommands.js +254 -0
  9. package/dist/commands/taskNextActionCommand.js +1 -3
  10. package/dist/commands/taskOverviewCommand.js +9 -2
  11. package/dist/commands/taskRoleRuntimeStatus.js +2 -25
  12. package/dist/controller/agentRuntimeObserver.js +4 -2
  13. package/dist/controller/clientRuntime.js +45 -2
  14. package/dist/controller/controller.js +6 -3
  15. package/dist/controller/fileSchedulerStoreAdapter.js +59 -188
  16. package/dist/controller/jobControl.js +3 -2
  17. package/dist/controller/runtime.js +6 -33
  18. package/dist/controller/runtimeEventProcessor.js +8 -4
  19. package/dist/controller/runtimeHookRunFence.js +4 -10
  20. package/dist/execution/executionHealth.js +8 -16
  21. package/dist/executor/agentExecutor.js +13 -14
  22. package/dist/executor/fileRoleLaunchPlanner.js +9 -16
  23. package/dist/lifecycle/exactRunTerminalization.js +24 -322
  24. package/dist/repository/taskWorkspaceCoordinator.js +0 -9
  25. package/dist/runtime/agentHost.js +20 -80
  26. package/dist/runtime/exactControlPlane.js +15 -9
  27. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  28. package/dist/runtime/providerRecoveryDecision.js +1 -1
  29. package/dist/runtime/providerRuntimeIdentity.js +25 -15
  30. package/dist/scheduler/activeRoleRunDelivery.js +1 -19
  31. package/dist/scheduler/leaderWakeupProcessor.js +12 -63
  32. package/dist/scheduler/ports.js +3 -2
  33. package/dist/scheduler/roleRunLiveness.js +4 -1
  34. package/dist/scheduler/roleRunStall.js +0 -2
  35. package/dist/scheduler/taskExecutionProjection.js +18 -1
  36. package/dist/scheduler/wakeupQueue.js +2 -1
  37. package/dist/storage/migration/productionRegistry.js +65 -0
  38. package/dist/storage/sqliteStore.js +10 -2
  39. package/dist/storage/taskStore.js +11 -3
  40. package/dist/task/completionReadiness.js +0 -67
  41. package/dist/task/nextAction.js +16 -32
  42. package/dist/task/task.js +38 -3
  43. package/dist/web/assets/client/i18n.js +0 -4
  44. package/dist/web/assets/client/view.js +0 -18
  45. package/dist/web/webSnapshot.js +7 -13
  46. package/package.json +1 -1
  47. package/dist/run/recoveryProjection.js +0 -252
  48. package/dist/runtime/conversationSwitch.js +0 -277
@@ -10,19 +10,17 @@ import { isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../sc
10
10
  import { routeRoleEvent } from "../scheduler/operatorEvent.js";
11
11
  import { readCommandText } from "./textInput.js";
12
12
  import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
13
- import { createRoleSessionSet, retireTaskRoleSessionsForWorkspace, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
13
+ import { createRoleSessionSet, roleAgentSessionResumeMode, retireTaskRoleSessionsForWorkspace, terminalizeTaskRoleRunSession, updateTaskRoleProviderRuntime, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
14
14
  import { currentProviderActivation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
15
- import { CONVERSATION_SWITCH_REQUESTED_EVENT, CONVERSATION_SWITCH_RESOLVED_EVENT, projectConversationSwitch, providerConversationGeneration, roleSessionDispatchModeWithConversationSwitch } from "../runtime/conversationSwitch.js";
16
15
  import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
17
16
  import { defaultTableWidth, renderTable } from "../output/table.js";
18
17
  import { formatTimestamp } from "../output/timePresentation.js";
19
18
  import { renderRoleDetails } from "../output/rolePresentation.js";
20
19
  import { createTaskMessage, taskMessageAuthorLabel } from "../message/message.js";
21
20
  import { cancelInputRequest } from "../input/inputRequest.js";
22
- import { recoverExactAgentRun, retireExactActiveAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
21
+ import { retireExactActiveAgentRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
23
22
  import { activeRoleAgentBinding, copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole, updateRoleStatus } from "../role/role.js";
24
- import { agentRunDeliveryReceiptId, createAgentRun, withAgentRunContextSnapshot } from "../run/agentRun.js";
25
- import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProjection.js";
23
+ import { agentRunDeliveryReceiptId, createAgentRun, failAgentRun, withAgentRunContextSnapshot, yieldAgentRun } from "../run/agentRun.js";
26
24
  import { matchYieldReceipt } from "../run/yieldReceipt.js";
27
25
  import { createRejectedYieldAttempt, rejectedYieldAttemptEventPayload, rejectedYieldAttemptFromTaskEvent, RUN_YIELD_REJECTED_EVENT } from "../run/rejectedYieldAttempt.js";
28
26
  import { createReviewRound, createTaskReviewRound, createTaskDeltaReviewRound, attachReviewExecutionGroup, deltaRecheckBlocksAcceptance, finishReviewRound, parseReviewYieldReport, recordReviewWorkspaceDisposition, retryRunningReviewExecutionLane, retryTaskReviewRound, startReviewRound, updateReviewExecutionGroup, validateTaskReviewCandidate } from "../review/reviewRound.js";
@@ -38,7 +36,7 @@ import { runPublicationCommand } from "./taskPublicationCommands.js";
38
36
  import { enqueueWork, settleExactWorkExecution } from "../coordination/workMailboxQueue.js";
39
37
  import { mailboxHasWork as workMailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
40
38
  import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
41
- import { blockingProviderContinuations, projectProviderContinuations, runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
39
+ import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
42
40
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
43
41
  import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, updateTaskMetadata } from "../task/task.js";
44
42
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
@@ -196,19 +194,6 @@ export function preflightTaskCompletion(taskId, store, options = {}, request = {
196
194
  if (blockers.length > 0) {
197
195
  throw usageError(formatCompletionBlockers(task.id, blockers));
198
196
  }
199
- const roles = store.listRoles(task.id);
200
- const activeRuns = roles
201
- .map((role) => ({ role, run: store.getActiveAgentRun(task.id, role.name) }))
202
- .filter((entry) => entry.run !== null);
203
- const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
204
- if (leaderEntry !== undefined) {
205
- if (leaderEntry.run.workItemId !== undefined) {
206
- throw usageError(`Task ${task.id} has running work: ${leaderEntry.run.workItemId}.`);
207
- }
208
- if (leaderEntry.run.pushedAt === undefined) {
209
- throw usageError(`Task ${task.id} Leader delivery is still pending.`);
210
- }
211
- }
212
197
  return {
213
198
  task,
214
199
  actor,
@@ -728,33 +713,6 @@ function completeTaskCommand(args, store, options) {
728
713
  ? undefined
729
714
  : assertTaskCompletionPublishedTreeProof(tx, task, request.acceptedPublishedTreePublicationId, options.completionPublishedTreeProof, actualTaskCandidate);
730
715
  const roles = tx.listRoles(task.id);
731
- const activeRuns = roles
732
- .map((role) => ({ role, run: tx.getActiveAgentRun(task.id, role.name) }))
733
- .filter((entry) => entry.run !== null);
734
- // The preflight (above, same transaction) already projected the full
735
- // readiness via projectCompletionReadiness. The leader-Run check is
736
- // actor-dependent and stays here; every other blocker is re-validated by
737
- // the post-Review readiness fence below.
738
- const leaderEntry = activeRuns.find(({ role }) => role.name === LEADER_ROLE);
739
- if (leaderEntry !== undefined) {
740
- if (leaderEntry.run.workItemId !== undefined) {
741
- throw usageError(`Task ${task.id} has running work: ${leaderEntry.run.workItemId}.`);
742
- }
743
- if (leaderEntry.run.pushedAt === undefined) {
744
- throw usageError(`Task ${task.id} Leader delivery is still pending.`);
745
- }
746
- const terminal = terminalizeExactTaskRun(tx, {
747
- taskId: task.id,
748
- roleName: LEADER_ROLE,
749
- agentId: leaderEntry.run.effective.agentId,
750
- runId: leaderEntry.run.id,
751
- receiptId: agentRunDeliveryReceiptId(leaderEntry.run),
752
- outcome: { status: "yielded", summary }
753
- }, now);
754
- if (terminal.disposition !== "applied") {
755
- throw usageError(`Task Leader Run changed during completion: ${leaderEntry.run.id}/${terminal.reason}.`);
756
- }
757
- }
758
716
  // A final (Task-scoped) review is the Task delivery policy: it reviews the
759
717
  // complete frozen Task heads, not a single WorkItem Candidate. If the
760
718
  // review config requests a final review, create the Task ReviewRound here
@@ -769,6 +727,10 @@ function completeTaskCommand(args, store, options) {
769
727
  finalReview,
770
728
  };
771
729
  }
730
+ // Completion is a semantic boundary. Current Runs and Sessions are
731
+ // disposable execution attempts; settle them before deriving readiness so
732
+ // only durable Task results decide whether the Task can complete.
733
+ const runtimeCleanupTargets = settleTaskExecutionForCompletion(tx, task.id, roles, summary, now);
772
734
  // Issue 06: re-validate the full completion readiness inside the
773
735
  // transaction (the CAS fence) after final-review preparation. This is the
774
736
  // same pure projection `task next-action` displays, now with the finding
@@ -818,13 +780,12 @@ function completeTaskCommand(args, store, options) {
818
780
  // The durable records remain intact; only the derived mailbox work is
819
781
  // discarded at this lifecycle boundary.
820
782
  tx.removeWorkMailbox(taskMailbox(task.id));
821
- // Role mailboxes are also derived wake state. A Worker result or recovery
783
+ // Role mailboxes are also derived wake state. A Worker result or runtime
822
784
  // signal queued while the final Run was being settled must not survive a
823
785
  // completed Task and become actionable after a later explicit reopen.
824
786
  for (const role of roles) {
825
787
  tx.removeWorkMailbox(roleMailbox(task.id, role.name));
826
788
  }
827
- const runtimeCleanupTargets = queueCompletedTaskRuntimeCleanups(tx, task.id, roles, now);
828
789
  return {
829
790
  task: completed,
830
791
  changed: true,
@@ -930,11 +891,6 @@ function archiveTaskCommand(args, store, options) {
930
891
  if (activeArchiveJob !== undefined) {
931
892
  throw usageError(`Task ${task.id} has an active DurableJob: ${activeArchiveJob.id}/${activeArchiveJob.status}.`);
932
893
  }
933
- const continuationBlockers = blockingProviderContinuations(tx.listEvents(task.id));
934
- if (continuationBlockers.length > 0) {
935
- throw usageError(`Task ${task.id} still owns unsettled Provider continuation(s); `
936
- + "reconcile or cancel them before archiving.");
937
- }
938
894
  if (task.cwd !== undefined || tx.listManagedWorkspaces(task.id).length > 0) {
939
895
  throw usageError(`Task ${task.id} still has managed worktrees; clean them before archiving.`);
940
896
  }
@@ -1325,8 +1281,6 @@ function taskRoleCommand(args, store, options) {
1325
1281
  return output(bindTaskRole(rest, store, options));
1326
1282
  if (command === "unbind")
1327
1283
  return output(unbindTaskRole(rest, store, options));
1328
- if (command === "session")
1329
- return taskRoleSessionCommand(rest, store, options);
1330
1284
  if (command === "view")
1331
1285
  return viewTaskRole(rest, store);
1332
1286
  if (command === "takeover")
@@ -1337,79 +1291,6 @@ function taskRoleCommand(args, store, options) {
1337
1291
  ? "Task role command is required."
1338
1292
  : `Unknown command: task role ${command}`);
1339
1293
  }
1340
- function taskRoleSessionCommand(args, store, options) {
1341
- const [command, ...rest] = args;
1342
- if (command === "switch")
1343
- return switchTaskRoleSession(rest, store, options);
1344
- throw usageError(command === undefined
1345
- ? "Task role session command is required."
1346
- : `Unknown command: task role session ${command}`);
1347
- }
1348
- function switchTaskRoleSession(args, store, options) {
1349
- const usage = "Task role session switch usage: yui task role session switch <task> <role> --reason <text>.";
1350
- const parsed = parseTail(args, new Set(["--reason"]), usage);
1351
- exactPositionals(parsed.positionals, 2, usage);
1352
- const reason = truncateEventNote(requiredOption(parsed.options, "--reason"));
1353
- const now = clock(options);
1354
- const result = store.transaction((tx) => {
1355
- const task = requireTask(tx, parsed.positionals[0]);
1356
- if (task.status !== "active") {
1357
- throw usageError(inactiveTaskMessage(task, "switching a Provider Conversation"), usage);
1358
- }
1359
- const role = requireRole(tx, task.id, parsed.positionals[1]);
1360
- const requestedBy = taskActor(tx, options, task.id);
1361
- const leaderRunId = requestedBy === "leader"
1362
- ? taskLeaderActionRunId(tx, task.id, options.environment, options.yuiHome)
1363
- : undefined;
1364
- if (requestedBy === "leader" && leaderRunId === undefined) {
1365
- throw usageError("Leader Conversation switching requires the exact current-Turn assertion.", usage);
1366
- }
1367
- const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
1368
- const generation = providerConversationGeneration(sessions);
1369
- if (sessions === null || generation === null) {
1370
- throw usageError(`Task Role has no current Provider Conversation: ${task.id}/${role.name}.`, usage);
1371
- }
1372
- const events = tx.listEvents(task.id);
1373
- const previous = projectConversationSwitch(events, role.name, sessions);
1374
- if (previous?.status === "pending"
1375
- && previous.generation === generation
1376
- && previous.requestedBy === requestedBy
1377
- && previous.reason === reason) {
1378
- return { taskId: task.id, roleName: role.name, request: previous, replayed: true };
1379
- }
1380
- if (previous !== null && previous.resolvedAt === undefined) {
1381
- tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, CONVERSATION_SWITCH_RESOLVED_EVENT, {
1382
- requestId: previous.requestId,
1383
- roleName: role.name,
1384
- generation: previous.generation,
1385
- status: "obsolete"
1386
- }, now));
1387
- }
1388
- const requestId = tx.nextEventId(task.id);
1389
- const event = createTaskEvent(requestId, task.id, CONVERSATION_SWITCH_REQUESTED_EVENT, {
1390
- requestId,
1391
- roleName: role.name,
1392
- generation,
1393
- requestedBy,
1394
- reason,
1395
- ...(leaderRunId === undefined ? {} : { leaderRunId })
1396
- }, now);
1397
- tx.saveEvent(task.id, event);
1398
- return {
1399
- taskId: task.id,
1400
- roleName: role.name,
1401
- request: projectConversationSwitch([...events, event], role.name, sessions),
1402
- replayed: false
1403
- };
1404
- });
1405
- options.runtime?.notifyStateChanged(result.taskId);
1406
- options.runtime?.notifyMailboxChanged?.({
1407
- kind: "role",
1408
- taskId: result.taskId,
1409
- roleName: result.roleName
1410
- });
1411
- return output(`${result.replayed ? "Reused" : "Recorded"} pending Provider Conversation switch ${result.request.requestId} for ${result.taskId}/${result.roleName}. The current Conversation remains authoritative until safe replacement binding.\n`, result);
1412
- }
1413
1294
  function addTaskRole(args, store, options) {
1414
1295
  const usage = "Task role add usage: yui task role add <task> <name> [Role and Agent settings].";
1415
1296
  const [taskId, roleName, ...tail] = args;
@@ -2099,6 +1980,7 @@ function dispatchWork(args, store, options) {
2099
1980
  if (task.status !== "active") {
2100
1981
  throw usageError(inactiveTaskMessage(task, "dispatch"));
2101
1982
  }
1983
+ assertTaskExecutionEnabled(task, "dispatching work");
2102
1984
  taskActor(tx, options, task.id);
2103
1985
  const currentGroup = currentWorkItemExecutionGroup(item);
2104
1986
  const existingGroup = currentGroup?.resolution === undefined
@@ -2342,7 +2224,7 @@ function dispatchWork(args, store, options) {
2342
2224
  workItemWriteProjectIds: item.writeProjectIds
2343
2225
  });
2344
2226
  const sessions = tx.getTaskRoleSessionSet(task.id, plan.role.name);
2345
- const dispatchMode = taskRoleDispatchMode(tx, task.id, plan.role.name, sessions, effective.agentId, effective);
2227
+ const dispatchMode = roleAgentSessionResumeMode(sessions, effective.agentId, effective);
2346
2228
  const laneWorkspace = laneManagedWorkspace === undefined
2347
2229
  ? undefined
2348
2230
  : {
@@ -4041,8 +3923,6 @@ function taskRunCommand(args, store, options) {
4041
3923
  return retryRun(rest, store, options);
4042
3924
  if (command === "settle")
4043
3925
  return settleStaleFinalReviewRun(rest, store, options);
4044
- if (command === "recover")
4045
- return output(recoverRun(rest, store, options));
4046
3926
  if (command === "yield")
4047
3927
  return yieldRun(rest, store, options);
4048
3928
  if (command === "yield-status")
@@ -4413,15 +4293,7 @@ function retryRun(args, store, options) {
4413
4293
  const task = requireTask(tx, previous.taskId);
4414
4294
  if (task.status !== "active")
4415
4295
  throw usageError(`Task is not active: ${task.id}.`);
4416
- if (runOwnsBlockingProviderContinuation(tx.listEvents(task.id), {
4417
- taskId: previous.taskId,
4418
- roleName: previous.roleName,
4419
- runId: previous.id,
4420
- agentId: previous.effective.agentId
4421
- })) {
4422
- throw usageError(`Run ${previous.id} still owns an unsettled Provider continuation; `
4423
- + "reconcile it before retrying the Lane.");
4424
- }
4296
+ assertTaskExecutionEnabled(task, "retrying a Run");
4425
4297
  const role = requireRole(tx, task.id, previous.roleName);
4426
4298
  if (tx.getActiveAgentRun(task.id, role.name) !== null) {
4427
4299
  throw usageError(`${task.id}/${role.name} already has an active run.`);
@@ -4616,7 +4488,7 @@ function retryRun(args, store, options) {
4616
4488
  contextSnapshotRef: contextSnapshotRef(retrySnapshot),
4617
4489
  deltaRefIds: contextSnapshotDeltaRefIds(tx, retrySnapshot)
4618
4490
  });
4619
- const created = createAgentRun(runId, task.id, role.name, taskRoleDispatchMode(tx, task.id, role.name, sessions, effective.agentId, effective), assignment, now, {
4491
+ const created = createAgentRun(runId, task.id, role.name, roleAgentSessionResumeMode(sessions, effective.agentId, effective), assignment, now, {
4620
4492
  ...(previous.workItemId === undefined ? {} : { workItemId: previous.workItemId }),
4621
4493
  ...(runningGroup === undefined ? {} : {
4622
4494
  executionGroupId: runningGroup.id,
@@ -4960,15 +4832,6 @@ function retryFailedReviewRun(previous, store, options, now) {
4960
4832
  const task = requireTask(tx, run.taskId);
4961
4833
  if (task.status !== "active")
4962
4834
  throw usageError(`Task is not active: ${task.id}.`);
4963
- if (runOwnsBlockingProviderContinuation(tx.listEvents(task.id), {
4964
- taskId: run.taskId,
4965
- roleName: run.roleName,
4966
- runId: run.id,
4967
- agentId: run.effective.agentId
4968
- })) {
4969
- throw usageError(`Review Run ${run.id} still owns an unsettled Provider continuation; `
4970
- + "reconcile it before retrying the Lane.");
4971
- }
4972
4835
  const requestedBy = taskActor(tx, options, task.id);
4973
4836
  const round = tx.getReviewRound(task.id, run.reviewRoundId);
4974
4837
  if (round === null) {
@@ -5161,127 +5024,7 @@ function retryFailedReviewRun(previous, store, options, now) {
5161
5024
  ? `Review retry requested as ${result.round.id}\n`
5162
5025
  : `Review retry already requested as ${result.round.id} (${result.round.status})\n`, { reviewRound: result.round });
5163
5026
  }
5164
- /**
5165
- * Records one explicit Leader recovery decision against an exact live Run.
5166
- * Diagnose and retry remain same-Run requests: this command never changes a
5167
- * native Conversation generation.
5168
- */
5169
- function recoverRun(args, store, options) {
5170
- const usage = "Task run recover usage: yui task run recover <task>/<run> --action <diagnose|retry|terminate> (--expected-progress-at <timestamp>|--from-next-action <fingerprint>) --provider-acceptance <accepted|rejected|ambiguous> --reason <text> [--agent-id <id>] [--adapter-id <id>] [--native-session-id <id>] [--launch-id <id>].";
5171
- const parsed = parseTail(args, new Set([
5172
- "--action",
5173
- "--expected-progress-at",
5174
- "--progress-at",
5175
- "--from-next-action",
5176
- "--provider-acceptance",
5177
- "--reason",
5178
- "--role",
5179
- "--agent-id",
5180
- "--adapter-id",
5181
- "--native-session-id",
5182
- "--launch-id"
5183
- ]), usage);
5184
- exactPositionals(parsed.positionals, 1, usage);
5185
- const now = clock(options);
5186
- const fingerprint = parsed.options.get("--from-next-action");
5187
- const explicitFence = parsed.options.get("--expected-progress-at")
5188
- ?? parsed.options.get("--progress-at");
5189
- if (fingerprint !== undefined && explicitFence !== undefined) {
5190
- throw usageError("--from-next-action and --expected-progress-at/--progress-at are mutually exclusive.", usage);
5191
- }
5192
- const input = store.transaction((tx) => {
5193
- const active = requireRun(tx, parsed.positionals[0], options);
5194
- const task = requireTask(tx, active.taskId);
5195
- if (task.status !== "active")
5196
- throw usageError(inactiveTaskMessage(task, "recovering a run"));
5197
- taskActor(tx, options, task.id);
5198
- const roleName = parsed.options.get("--role") ?? active.roleName;
5199
- if (roleName !== active.roleName) {
5200
- throw usageError(`Recovery Role does not match Run ${active.id}: ${roleName}.`);
5201
- }
5202
- const role = requireRole(tx, task.id, roleName);
5203
- const sessions = tx.getTaskRoleSessionSet(task.id, role.name);
5204
- const session = sessions?.sessions[sessions.activeAgentId];
5205
- // Issue 08: a fingerprint copied from `task run show` resolves the
5206
- // canonical fence server-side. The projection is recomputed here, so a
5207
- // fingerprint from stale observations matches nothing and fails closed.
5208
- let plan = null;
5209
- if (fingerprint !== undefined) {
5210
- const facts = readRunRecoveryFacts(tx, task.id, active.id);
5211
- if (facts === null)
5212
- throw usageError(`Agent Run not found: ${task.id}/${active.id}.`, usage);
5213
- const projection = projectRunRecovery(facts);
5214
- plan = projection.actions.find((entry) => entry.fingerprint === fingerprint) ?? null;
5215
- if (plan === null) {
5216
- throw usageError(runRecoveryStaleDiagnosis(task.id, active.id, projection.canonicalProgressAt ?? null, "recovery action fingerprint is stale or unknown"), usage);
5217
- }
5218
- }
5219
- const action = parseRecoveryAction(parsed.options.get("--action") ?? plan?.action, usage);
5220
- if (plan !== null && plan.action !== action) {
5221
- throw usageError(`--action ${action} does not match recovery fingerprint action ${plan.action}.`, usage);
5222
- }
5223
- const expectedProgressAt = explicitFence ?? plan?.expectedProgressAt;
5224
- if (expectedProgressAt === undefined) {
5225
- throw usageError("--expected-progress-at is required.", usage);
5226
- }
5227
- const providerAcceptance = parseProviderAcceptance(parsed.options.get("--provider-acceptance"), usage);
5228
- const agentId = parsed.options.get("--agent-id")
5229
- ?? plan?.agentId
5230
- ?? active.effective.agentId;
5231
- const adapterId = parsed.options.get("--adapter-id")
5232
- ?? plan?.adapterId
5233
- ?? active.effective.adapterId;
5234
- const nativeSessionId = parsed.options.get("--native-session-id")
5235
- ?? plan?.nativeSessionId
5236
- ?? session?.nativeSessionId;
5237
- const launchId = parsed.options.get("--launch-id")
5238
- ?? plan?.launchId
5239
- ?? session?.launchId;
5240
- return {
5241
- taskId: task.id,
5242
- roleName: role.name,
5243
- runId: active.id,
5244
- agentId,
5245
- adapterId,
5246
- ...(nativeSessionId === undefined ? {} : { nativeSessionId }),
5247
- ...(launchId === undefined ? {} : { launchId }),
5248
- expectedProgressAt,
5249
- providerAcceptance,
5250
- action,
5251
- reason: requiredOption(parsed.options, "--reason"),
5252
- now
5253
- };
5254
- });
5255
- const result = recoverExactAgentRun(store, input);
5256
- if (result.disposition !== "applied") {
5257
- throw usageError(runRecoveryStaleDiagnosis(input.taskId, input.runId, result.progressAt ?? null, `exact Run recovery ${result.disposition}: ${result.reason ?? "state changed"}`), usage);
5258
- }
5259
- // The durable recovery request is committed before asking the Controller to
5260
- // wake the owning Leader; a failed transaction must not leak a signal.
5261
- if (result.run !== null && result.run.roleName !== LEADER_ROLE) {
5262
- notifyMailbox(options.runtime, leaderMailbox(result.run.taskId), result.run.taskId);
5263
- }
5264
- const followup = result.requiresExplicitFollowup === true
5265
- ? " Leader follow-up is required; no provider input or Session action was performed."
5266
- : "";
5267
- return `Recorded exact ${result.action} recovery for ${result.run?.id ?? "unknown Run"}.${followup}\n`;
5268
- }
5269
- /**
5270
- * Issue 08: every recovery rejection carries the current canonical fence and
5271
- * points at the single read-only command that projects it. The caller's
5272
- * side-effecting action is never retried automatically.
5273
- */
5274
- function runRecoveryStaleDiagnosis(taskId, runId, canonicalProgressAt, detail) {
5275
- const fence = canonicalProgressAt === null
5276
- ? "no durable progress timestamp is available for this Run"
5277
- : `the canonical durable fence is now ${canonicalProgressAt}`;
5278
- return `${detail}; ${fence}. Re-read the recovery plan: yui task run show ${taskId}/${runId}.`;
5279
- }
5280
- /**
5281
- * Issue 08: read-only Run detail with the canonical recovery fence, Provider
5282
- * evidence, and every exact recovery action. Never mutates state and never
5283
- * selects an action or Provider acceptance for the Leader.
5284
- */
5027
+ /** Run details are retained audit evidence; continuation uses durable Task state. */
5285
5028
  function showRun(args, store, options) {
5286
5029
  const usage = "Task run show usage: yui task run show <task>/<run> [--json].";
5287
5030
  const asJson = args.includes("--json");
@@ -5289,9 +5032,6 @@ function showRun(args, store, options) {
5289
5032
  exactPositionals(positionals, 1, usage);
5290
5033
  const data = store.transaction((tx) => {
5291
5034
  const run = requireRun(tx, positionals[0], options);
5292
- const facts = readRunRecoveryFacts(tx, run.taskId, run.id);
5293
- if (facts === null)
5294
- throw usageError(`Agent Run not found: ${run.taskId}/${run.id}.`, usage);
5295
5035
  const retirement = tx.listEvents(run.taskId)
5296
5036
  .map(taskRecordRetirement)
5297
5037
  .find((entry) => entry?.recordKind === "agent-run" && entry.recordId === run.id) ?? null;
@@ -5300,7 +5040,6 @@ function showRun(args, store, options) {
5300
5040
  .filter((attempt) => (attempt !== null && attempt.runId === run.id));
5301
5041
  return {
5302
5042
  run,
5303
- recovery: projectRunRecovery(facts),
5304
5043
  retirement,
5305
5044
  rejectedYieldAttemptCount: allRejectedYieldAttempts.length,
5306
5045
  rejectedYieldAttempts: allRejectedYieldAttempts.slice(-16)
@@ -5311,11 +5050,11 @@ function showRun(args, store, options) {
5311
5050
  }
5312
5051
  return {
5313
5052
  kind: "output",
5314
- output: renderRunShow(data.run, data.recovery, data.retirement, data.rejectedYieldAttemptCount, data.rejectedYieldAttempts),
5053
+ output: renderRunShow(data.run, data.retirement, data.rejectedYieldAttemptCount, data.rejectedYieldAttempts),
5315
5054
  data
5316
5055
  };
5317
5056
  }
5318
- function renderRunShow(run, recovery, retirement, rejectedYieldAttemptCount, rejectedYieldAttempts) {
5057
+ function renderRunShow(run, retirement, rejectedYieldAttemptCount, rejectedYieldAttempts) {
5319
5058
  const lines = [
5320
5059
  `Run: ${run.id}`,
5321
5060
  `Task: ${run.taskId}`,
@@ -5329,6 +5068,7 @@ function renderRunShow(run, recovery, retirement, rejectedYieldAttemptCount, rej
5329
5068
  ]),
5330
5069
  `Effective: ${run.effective.agentId}/${run.effective.adapterId} r${run.effective.sourceDesiredRevision}`,
5331
5070
  `Created: ${run.createdAt}`,
5071
+ ...(run.endedAt === undefined ? [] : [`Ended: ${run.endedAt}`]),
5332
5072
  ...(run.pushedAt === undefined ? [] : [`Pushed: ${run.pushedAt}`]),
5333
5073
  ...(run.deliveredAt === undefined
5334
5074
  ? []
@@ -5349,33 +5089,6 @@ function renderRunShow(run, recovery, retirement, rejectedYieldAttemptCount, rej
5349
5089
  ])
5350
5090
  ])
5351
5091
  ];
5352
- if (recovery.canonicalProgressAt !== null) {
5353
- lines.push(`Canonical recovery fence (Yui durable CAS): ${recovery.canonicalProgressAt}`, ...(recovery.canonicalProgressEvidence === undefined
5354
- ? []
5355
- : [`Fence evidence: ${recovery.canonicalProgressEvidence}`]));
5356
- }
5357
- if (recovery.provider.observedAt !== null) {
5358
- lines.push(`Provider observation (evidence only, not a fence): `
5359
- + `${recovery.provider.observationKind} at ${recovery.provider.observedAt}`);
5360
- }
5361
- if (recovery.session !== null) {
5362
- const session = recovery.session;
5363
- lines.push(`Session: ${session.status}`
5364
- + `${session.nativeSessionId === undefined ? "" : ` ${session.nativeSessionId}`}`
5365
- + `${session.launchId === undefined ? "" : ` launch ${session.launchId}`}`);
5366
- }
5367
- if (recovery.recoverable) {
5368
- lines.push(`Provider acceptance options: ${recovery.providerAcceptance.options.join(", ")}`, "Recovery actions (copy one; the fence is already canonical):");
5369
- for (const plan of recovery.actions) {
5370
- lines.push(` [${plan.action}] ${plan.reason}`, ` ${plan.command}`);
5371
- }
5372
- if (recovery.judgmentRequired !== undefined) {
5373
- lines.push(`Judgment: ${recovery.judgmentRequired}`);
5374
- }
5375
- }
5376
- else {
5377
- lines.push(`Not recoverable: ${recovery.reason ?? "unknown"}`);
5378
- }
5379
5092
  return `${lines.join("\n")}\n`;
5380
5093
  }
5381
5094
  function truncateRunShowText(value) {
@@ -6027,6 +5740,9 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
6027
5740
  }
6028
5741
  }
6029
5742
  const task = requireTask(tx, taskId);
5743
+ if (task.status !== "active")
5744
+ throw usageError(`Task is not active: ${task.id}.`);
5745
+ assertTaskExecutionEnabled(task, "dispatching a Review");
6030
5746
  if ((round.scope ?? "work-item") === "task") {
6031
5747
  let taskFinalContract;
6032
5748
  try {
@@ -6308,7 +6024,7 @@ export function dispatchPreparedReviewRound(taskId, reviewRoundId, store, option
6308
6024
  effective,
6309
6025
  workspace: laneWorkspace
6310
6026
  }, now);
6311
- createdRuns.push(createAgentRun(runId, taskId, laneReviewer.name, taskRoleDispatchMode(tx, taskId, laneReviewer.name, sessions, effective.agentId, effective), assignment, now, {
6027
+ createdRuns.push(createAgentRun(runId, taskId, laneReviewer.name, roleAgentSessionResumeMode(sessions, effective.agentId, effective), assignment, now, {
6312
6028
  ...(item === undefined ? {} : { workItemId: item.id }),
6313
6029
  purpose: "review",
6314
6030
  reviewRoundId: round.id,
@@ -6650,12 +6366,14 @@ function assertTaskOpen(task) {
6650
6366
  if (task.status === "retired")
6651
6367
  throw usageError(`Task is retired: ${task.id}.`);
6652
6368
  }
6369
+ function assertTaskExecutionEnabled(task, action) {
6370
+ if (task.executionGate.state === "enabled")
6371
+ return;
6372
+ throw usageError(`Task execution is stopped: ${task.id}. Run "yui task execution start ${task.id}" before ${action}.`);
6373
+ }
6653
6374
  function taskActor(store, options, taskId) {
6654
6375
  return resolveTaskLocalActor(store, options.environment, taskId, options.yuiHome);
6655
6376
  }
6656
- function taskRoleDispatchMode(store, taskId, roleName, sessions, agentId, effective) {
6657
- return roleSessionDispatchModeWithConversationSwitch(sessions, store.listEvents(taskId), store.getWorkMailbox({ kind: "role", taskId, roleName }), roleName, agentId, effective);
6658
- }
6659
6377
  function inactiveTaskMessage(task, action) {
6660
6378
  if (task.status === "draft") {
6661
6379
  return `Task ${task.id} is a Draft; activate it before ${action}.`;
@@ -6683,18 +6401,6 @@ function parseWorkStatus(value) {
6683
6401
  return "failed";
6684
6402
  throw usageError(`Invalid work item status: ${value}.`);
6685
6403
  }
6686
- function parseRecoveryAction(value, usage) {
6687
- if (value === "diagnose"
6688
- || value === "retry"
6689
- || value === "terminate")
6690
- return value;
6691
- throw usageError("--action must be diagnose, retry, or terminate.", usage);
6692
- }
6693
- function parseProviderAcceptance(value, usage) {
6694
- if (value === "accepted" || value === "rejected" || value === "ambiguous")
6695
- return value;
6696
- throw usageError("--provider-acceptance must be accepted, rejected, or ambiguous.", usage);
6697
- }
6698
6404
  function presentWorkStatus(status) {
6699
6405
  if (status === "pending")
6700
6406
  return "todo";
@@ -7437,14 +7143,42 @@ function messageOf(error) {
7437
7143
  function taskMailbox(taskId) {
7438
7144
  return { kind: "task", taskId };
7439
7145
  }
7440
- function queueCompletedTaskRuntimeCleanups(store, taskId, roles, now) {
7146
+ function settleTaskExecutionForCompletion(store, taskId, roles, summary, now) {
7147
+ const activeRuns = store.listAgentRuns(taskId).filter((run) => run.status === "active");
7148
+ for (const run of activeRuns) {
7149
+ store.saveAgentRun(run.roleName === LEADER_ROLE && run.pushedAt !== undefined
7150
+ ? yieldAgentRun(run, summary, now)
7151
+ : failAgentRun(run, `Task completed: ${summary}`, now));
7152
+ if (run.executionGroupId !== undefined && run.executionLaneId !== undefined) {
7153
+ store.clearActiveExecutionLaneRun(taskId, run.executionGroupId, run.executionLaneId);
7154
+ }
7155
+ }
7156
+ // Historical pointer projections are execution state, not Task progress.
7157
+ for (const run of store.listAgentRuns(taskId)) {
7158
+ if (run.executionGroupId !== undefined && run.executionLaneId !== undefined) {
7159
+ store.clearActiveExecutionLaneRun(taskId, run.executionGroupId, run.executionLaneId);
7160
+ }
7161
+ }
7441
7162
  const targets = [];
7442
7163
  for (const role of roles) {
7443
- if (store.getActiveAgentRun(taskId, role.name) !== null)
7444
- continue;
7445
- const sessions = store.getTaskRoleSessionSet(taskId, role.name);
7446
- const active = sessions?.sessions[sessions.activeAgentId];
7447
- if (!ownedRuntimeSessionRequiresCleanup(active))
7164
+ store.clearActiveAgentRun(taskId, role.name);
7165
+ let sessions = store.getTaskRoleSessionSet(taskId, role.name);
7166
+ const activeSession = sessions?.sessions[sessions.activeAgentId];
7167
+ const cleanupRequired = ownedRuntimeSessionRequiresCleanup(activeSession);
7168
+ if (sessions !== null) {
7169
+ if (sessions.inFlight !== null) {
7170
+ sessions = terminalizeTaskRoleRunSession(sessions, sessions.inFlight, now);
7171
+ }
7172
+ const current = sessions.sessions[sessions.activeAgentId];
7173
+ if (current !== undefined && current.status !== "stopped" && current.status !== "broken") {
7174
+ sessions = updateRoleAgentSessionStatus(sessions, sessions.activeAgentId, "stopped", now);
7175
+ }
7176
+ store.saveTaskRoleSessionSet(sessions);
7177
+ }
7178
+ if (role.status !== "idle") {
7179
+ store.saveRole(taskId, updateRoleStatus(role, "idle", now));
7180
+ }
7181
+ if (!cleanupRequired)
7448
7182
  continue;
7449
7183
  const target = runtimeLifecycleTarget({
7450
7184
  scope: "task",
@@ -4,11 +4,9 @@ import { formatTimestamp } from "../output/timePresentation.js";
4
4
  import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunStall.js";
5
5
  import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
6
6
  import { projectNextAction } from "../task/nextAction.js";
7
- import { inspectTaskRoleSessionRecovery } from "./taskRoleRuntimeStatus.js";
8
7
  import { summarizeExecutionGroup } from "../execution/executionGroup.js";
9
8
  import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
10
9
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
11
- import { projectExecutionLaneRunRecoveries } from "../run/recoveryProjection.js";
12
10
  import { projectReviewDecision } from "../review/reviewDecision.js";
13
11
  const RECENT_RECORD_LIMIT = 5;
14
12
  const RELATED_RECORD_LIMIT = 5;
@@ -76,7 +74,6 @@ export function runTaskContextCommand(args, store, currentTaskReviewCandidate =
76
74
  managedWorkspaces: reader.listManagedWorkspaces(task.id),
77
75
  roleSessionSets,
78
76
  coordinationMailboxes,
79
- roleSessionRecoveries: roles.map((role) => (inspectTaskRoleSessionRecovery(task.id, role.name, reader))),
80
77
  workItems,
81
78
  agentRuns,
82
79
  reviewRounds,
@@ -90,12 +87,11 @@ export function runTaskContextCommand(args, store, currentTaskReviewCandidate =
90
87
  nextAction: projectNextAction({
91
88
  ...nextActionFacts,
92
89
  currentTaskReviewCandidate,
93
- executionGroups: execution.executionGroups,
94
- runRecoveries: projectExecutionLaneRunRecoveries(reader, task.id, execution.executionGroups)
90
+ executionGroups: execution.executionGroups
95
91
  })
96
92
  };
97
93
  });
98
- const { task, execution, reviewConfig, reviewDecision, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, coordinationMailboxes, roleSessionRecoveries, workItems, agentRuns, reviewRounds, changeSets, integrations, publications, messages, openInputRequests, resolvedInputRequests, events, nextAction } = data;
94
+ const { task, execution, reviewConfig, reviewDecision, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, coordinationMailboxes, workItems, agentRuns, reviewRounds, changeSets, integrations, publications, messages, openInputRequests, resolvedInputRequests, events, nextAction } = data;
99
95
  const timeZone = store.getConfig().timeZone;
100
96
  const displayedActiveDecisions = activeDecisions.slice(-RECENT_RECORD_LIMIT);
101
97
  const displayedWorkItems = currentAndRecentWorkItems(workItems);
@@ -251,7 +247,6 @@ export function runTaskContextCommand(args, store, currentTaskReviewCandidate =
251
247
  const activeSession = sessions?.sessions[sessions.activeAgentId];
252
248
  const effective = activeRun?.effective ?? activeSession?.effective;
253
249
  const effectiveSource = activeRun === undefined ? "Session" : "Run";
254
- const recovery = roleSessionRecoveries.find((entry) => entry.roleName === role.name);
255
250
  const creation = [...events].reverse().find((event) => (event.type === "role.added" && event.payload.role === role.name));
256
251
  return [
257
252
  ` ${role.name} [${role.status}]: ${role.activeAgentId}/${binding.adapterId}`,
@@ -266,13 +261,7 @@ export function runTaskContextCommand(args, store, currentTaskReviewCandidate =
266
261
  : "pending next launch"}`,
267
262
  ...(creation?.payload.runtimeSource === undefined
268
263
  ? []
269
- : [` Runtime source at creation: ${creation.payload.runtimeSource}`]),
270
- ...(recovery === undefined ? [] : [
271
- ` Runtime cleanup: ${recovery.runtimeCleanupPending ? "pending" : "none"}`,
272
- ` Fresh launch: ${recovery.freshLaunchAllowed
273
- ? "allowed"
274
- : `blocked (${recovery.freshLaunchBlockers.join(", ")})`}`
275
- ])
264
+ : [` Runtime source at creation: ${creation.payload.runtimeSource}`])
276
265
  ];
277
266
  })),
278
267
  "",