@zq-silk/yui 0.8.3 → 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 (73) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +66 -19
  3. package/dist/cli/commandCatalog.js +43 -14
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli/updatePorts.js +6 -0
  6. package/dist/cli.js +252 -37
  7. package/dist/commands/executionAuditCommands.js +30 -0
  8. package/dist/commands/globalRoleCommands.js +8 -4
  9. package/dist/commands/operatorCommands.js +42 -1
  10. package/dist/commands/taskCommands.js +527 -147
  11. package/dist/commands/taskCompletionGate.js +36 -24
  12. package/dist/commands/taskContextCommand.js +11 -4
  13. package/dist/commands/taskInputCommands.js +48 -10
  14. package/dist/commands/taskNextActionCommand.js +38 -3
  15. package/dist/commands/taskOverviewCommand.js +2 -1
  16. package/dist/commands/taskRoleRuntimeStatus.js +2 -1
  17. package/dist/context/runContextPack.js +9 -5
  18. package/dist/context/sessionBootstrapManifest.js +158 -11
  19. package/dist/context/wakeNotification.js +5 -3
  20. package/dist/controller/clientRuntime.js +15 -15
  21. package/dist/controller/controller.js +16 -8
  22. package/dist/controller/fileSchedulerStoreAdapter.js +67 -7
  23. package/dist/controller/handoverCandidate.js +10 -3
  24. package/dist/controller/sessionNotify.js +4 -22
  25. package/dist/executor/agentAdapter.js +2 -2
  26. package/dist/executor/agentExecutor.js +29 -9
  27. package/dist/executor/fileRoleLaunchPlanner.js +37 -45
  28. package/dist/integration/gitIntegrationService.js +50 -2
  29. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  30. package/dist/observability/executionAudit.js +47 -1
  31. package/dist/observability/faultClassification.js +6 -4
  32. package/dist/observability/orchestrationMetrics.js +196 -0
  33. package/dist/operator/operatorSessionHistory.js +36 -0
  34. package/dist/release/releaseHandover.js +7 -5
  35. package/dist/release/runtimeRelease.js +15 -0
  36. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  37. package/dist/review/deltaRecheck.js +3 -2
  38. package/dist/review/reviewFindingLedger.js +5 -4
  39. package/dist/review/reviewOutcomeClassifier.js +263 -54
  40. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  41. package/dist/review/taskFinalReviewContractRebind.js +367 -0
  42. package/dist/run/runIdentity.js +10 -70
  43. package/dist/runtime/agentHost.js +3 -4
  44. package/dist/runtime/codexAppServerRuntime.js +6 -0
  45. package/dist/runtime/exactControlPlane.js +47 -37
  46. package/dist/runtime/firstProgressStopLoss.js +54 -0
  47. package/dist/runtime/launchBroker.js +10 -2
  48. package/dist/runtime/runtimeDeadlines.js +14 -0
  49. package/dist/runtime/sessionTitle.js +24 -12
  50. package/dist/runtime/structuredProviderHost.js +7 -1
  51. package/dist/runtime/tmuxAdapters.js +10 -3
  52. package/dist/scheduler/actionability.js +4 -2
  53. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  54. package/dist/scheduler/activeTaskProgress.js +2 -1
  55. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  56. package/dist/scheduler/taskExecutionProjection.js +13 -4
  57. package/dist/scheduler/wakeReason.js +1 -0
  58. package/dist/storage/sqliteStore.js +18 -3
  59. package/dist/storage/taskStore.js +14 -3
  60. package/dist/task/completionReadiness.js +48 -19
  61. package/dist/task/deliveryGuard.js +3 -1
  62. package/dist/task/nextAction.js +153 -55
  63. package/dist/task/repairWave.js +14 -1
  64. package/dist/task/task.js +10 -0
  65. package/dist/task/taskRecordRetirement.js +72 -0
  66. package/dist/web/webSnapshot.js +7 -1
  67. package/dist/workItem/workItem.js +6 -4
  68. package/i18n/README.zh-CN.md +48 -9
  69. package/package.json +1 -1
  70. package/skills/yui-leader/SKILL.md +73 -31
  71. package/skills/yui-operator/SKILL.md +58 -10
  72. package/skills/yui-reviewer/SKILL.md +23 -0
  73. package/skills/yui-runtime/SKILL.md +6 -6
@@ -7,15 +7,12 @@ 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
+ import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
13
14
  const STARTUP_TIMEOUT_MS = 5_000;
14
- // A lifecycle RPC may legitimately occupy the Controller for 30 seconds.
15
- // Restart must allow that request to drain before deciding shutdown is stuck.
16
- const SHUTDOWN_TIMEOUT_MS = 45_000;
17
15
  const POLL_INTERVAL_MS = 50;
18
- const LIFECYCLE_REQUEST_TIMEOUT_MS = 30_000;
19
16
  const ENVIRONMENT_REFRESH_TIMEOUT_MS = 500;
20
17
  const CONFIGURATION_REFRESH_TIMEOUT_MS = 500;
21
18
  const CONTROLLER_OPERATIONAL_ENVIRONMENT = [
@@ -140,12 +137,8 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
140
137
  + "Run `yui controller restart` before writing new task records.");
141
138
  }
142
139
  const actualVersion = statusRecord.version;
143
- const expected = expectedVersion ?? YUI_VERSION;
144
- const versionMismatch = expectedVersion === undefined
145
- ? typeof actualVersion === "string" && actualVersion !== expected
146
- : actualVersion !== expected;
147
- if (versionMismatch) {
148
- 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"}). `
149
142
  + "Run `yui controller restart` before writing new task records.");
150
143
  }
151
144
  // Ordinary callers must authenticate the complete control-plane identity.
@@ -154,7 +147,11 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
154
147
  // path authenticates its executable, argv, and version immediately after
155
148
  // readiness in ensureFileTaskControllerIdentity.
156
149
  if (expectedVersion === undefined) {
157
- assertControllerStatusIdentity(status, yuiVersionIdentity());
150
+ const identity = yuiVersionIdentity();
151
+ assertControllerStatusIdentity(status, {
152
+ ...identity,
153
+ version: typeof actualVersion === "string" ? actualVersion : identity.version
154
+ });
158
155
  }
159
156
  }
160
157
  function spawnDetachedFileTaskController(home, environment) {
@@ -168,7 +165,7 @@ function spawnDetachedFileTaskController(home, environment) {
168
165
  /** Stops the per-home Controller and waits until its owned discovery is gone. */
169
166
  export async function stopFileTaskController(home, options = {}) {
170
167
  const call = options.call ?? callController;
171
- const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
168
+ const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
172
169
  const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
173
170
  const expectedPid = options.expectedPid === undefined
174
171
  ? undefined
@@ -224,7 +221,7 @@ export async function stopFileTaskController(home, options = {}) {
224
221
  /** Restarts only the per-home Controller process; managed tmux sessions remain untouched. */
225
222
  export async function restartFileTaskController(home, options = {}) {
226
223
  const call = options.call ?? callController;
227
- const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
224
+ const shutdownTimeoutMs = positive(options.shutdownTimeoutMs, CONTROLLER_SHUTDOWN_TIMEOUT_MS, "shutdownTimeoutMs");
228
225
  const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
229
226
  let current = null;
230
227
  let previousPid;
@@ -529,7 +526,10 @@ export class FileTaskWorkflowRuntime {
529
526
  && this.workspacePreparer !== undefined) {
530
527
  await this.workspacePreparer.prepareTaskWorkspace(taskId);
531
528
  }
532
- await callFileTaskController(this.home, "scheduler.scan", {}, this.clientOptions);
529
+ await callFileTaskController(this.home, "scheduler.scan", {}, {
530
+ ...this.clientOptions,
531
+ requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
532
+ });
533
533
  }
534
534
  }
535
535
  const MANAGED_RUNTIME_ENVIRONMENT = new Set(YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES);
@@ -10,6 +10,7 @@ import { processOperatorInputNotifications } from "../scheduler/operatorInputNot
10
10
  import { startControllerServer } from "../core/controllerServer.js";
11
11
  import { monotonicMilliseconds } from "../core/controllerTelemetry.js";
12
12
  import { isProjectMaintenanceFenced } from "../repository/projectMaintenanceLock.js";
13
+ import { isHandoverLockHeld } from "../release/runtimeRelease.js";
13
14
  import { KeyedWorkQueue } from "../coordination/keyedWorkQueue.js";
14
15
  import { MailboxScheduler } from "../coordination/mailboxScheduler.js";
15
16
  import { nearestDeadlineBatch } from "../coordination/deadlineScheduler.js";
@@ -45,7 +46,7 @@ const ZERO_DRAIN_METRICS = Object.freeze({
45
46
  * Runs one lean scheduler pass. Due native Turn completions are folded before
46
47
  * liveness, so a valid Hook boundary fences destructive process reconciliation.
47
48
  */
48
- export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer, scope = { kind: "full" }, includeOperator = true, runtimeCleanupOutcomes = [], lifecycleHost, stallWindowMs = DEFAULT_STALL_WINDOW_MS, maintenanceFence, onMaintenanceFenceDefer, blockedTaskIds = new Set(), inputDeliveryRecoveryCutoff, diagnosticAfterMs = DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS) {
49
+ export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer, scope = { kind: "full" }, includeOperator = true, runtimeCleanupOutcomes = [], lifecycleHost, stallWindowMs = DEFAULT_STALL_WINDOW_MS, maintenanceFence, onMaintenanceFenceDefer, blockedTaskIds = new Set(), inputDeliveryRecoveryCutoff, diagnosticAfterMs = DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS, leaderWakeFence) {
49
50
  const compiledSelection = compileReconcileSelection(scope);
50
51
  const selection = includeOperator
51
52
  ? { ...compiledSelection, blockedTaskIds }
@@ -58,7 +59,9 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
58
59
  await controlEventLoopTurn();
59
60
  const failedCleanupRoles = await processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, runtimeCleanupOutcomes, blockedTaskIds);
60
61
  const roleSelection = selectionWithoutFailedCleanupRoles(store, selection, failedCleanupRoles);
61
- const wakeupSelection = selectionWithoutFailedLeaderCleanupTasks(store, selection, failedCleanupRoles);
62
+ const availableWakeupSelection = () => (leaderWakeFence?.() === true
63
+ ? exactTaskSelection(new Set())
64
+ : selectionWithoutFailedLeaderCleanupTasks(store, selection, failedCleanupRoles));
62
65
  if (selection.full)
63
66
  repairOrphanedActiveTasks(store, now, selection);
64
67
  const claimedTaskMailboxes = claimSelectedTaskMailboxes(store, selection, now);
@@ -66,7 +69,7 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
66
69
  // Durable Leader work that already has a ready Task workspace belongs to
67
70
  // the control path. Dispatch it before any unrelated Task workspace I/O;
68
71
  // the processor itself retains the fail-closed workspace-ready guard.
69
- const initialWakeups = selectedPendingWakeups(store, wakeupSelection);
72
+ const initialWakeups = selectedPendingWakeups(store, availableWakeupSelection());
70
73
  const initialWakeupResults = await processLeaderWakeups(store, delivery, now, exactTaskSelection(new Set(initialWakeups.keys())));
71
74
  // Preserve ready-Leader-first ordering, then bound the repeated state
72
75
  // projections that follow it in this pass.
@@ -134,7 +137,8 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
134
137
  && workspacePreparation.ready.has(result.taskId)
135
138
  ? [result.taskId]
136
139
  : [])));
137
- const laterWakeupTaskIds = new Set([...selectedPendingWakeups(store, wakeupSelection)].flatMap(([taskId, wakeup]) => {
140
+ const laterWakeupTaskIds = new Set([...selectedPendingWakeups(store, availableWakeupSelection())]
141
+ .flatMap(([taskId, wakeup]) => {
138
142
  const initial = initialWakeups.get(taskId);
139
143
  return initial === undefined
140
144
  || !pendingWakeupsMatch(initial, wakeup)
@@ -823,6 +827,7 @@ export class FileTaskController {
823
827
  #onExpiredEphemeralDomain;
824
828
  #maintenanceFence;
825
829
  #onMaintenanceFenceDefer;
830
+ #leaderWakeFence;
826
831
  #jobSupervisor;
827
832
  #continuationReconciler;
828
833
  #current;
@@ -867,6 +872,7 @@ export class FileTaskController {
867
872
  this.#onExpiredEphemeralDomain = options.onExpiredEphemeralDomain;
868
873
  this.#maintenanceFence = options.maintenanceFence;
869
874
  this.#onMaintenanceFenceDefer = options.onMaintenanceFenceDefer;
875
+ this.#leaderWakeFence = options.leaderWakeFence;
870
876
  this.#jobSupervisor = options.jobSupervisor;
871
877
  this.#continuationReconciler = options.continuationReconciler;
872
878
  this.#signalScheduler = new MailboxScheduler(async (keys) => { await this.#requestPass({ kind: "dirty", keys }); }, {
@@ -1118,7 +1124,7 @@ export class FileTaskController {
1118
1124
  // processes Leader wakeups.
1119
1125
  this.#jobSupervisor?.reconcile(this.#now());
1120
1126
  if (scope.kind === "full") {
1121
- result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, scope, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, runtimeFailedTaskIds, this.#startedAt, this.#diagnosticAfterMs);
1127
+ result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, scope, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, runtimeFailedTaskIds, this.#startedAt, this.#diagnosticAfterMs, this.#leaderWakeFence);
1122
1128
  }
1123
1129
  else {
1124
1130
  const dirtyPass = await this.#runDirtySchedulerPass(scope, runtimeCleanupOutcomes, runtimeFailedTaskIds);
@@ -1216,7 +1222,7 @@ export class FileTaskController {
1216
1222
  const taskScopes = partition.taskScopes.filter((taskScope) => (!blockedTaskIds.has(taskScope.taskId)));
1217
1223
  const orderedResults = [];
1218
1224
  if (partition.globalKeys.length > 0) {
1219
- orderedResults.push(await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: partition.globalKeys }, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs));
1225
+ orderedResults.push(await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: partition.globalKeys }, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs, this.#leaderWakeFence));
1220
1226
  }
1221
1227
  if (taskScopes.length === 0
1222
1228
  || this.#stopped
@@ -1249,7 +1255,7 @@ export class FileTaskController {
1249
1255
  if (this.#stopped || this.#pendingFull)
1250
1256
  continue;
1251
1257
  try {
1252
- taskResults[selected.index] = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: selected.taskScope.keys }, false, taskCleanupOutcomes[selected.index], this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs);
1258
+ taskResults[selected.index] = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: selected.taskScope.keys }, false, taskCleanupOutcomes[selected.index], this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs, this.#leaderWakeFence);
1253
1259
  this.#clearTaskPassRetry(selected.taskScope.taskId);
1254
1260
  }
1255
1261
  catch (error) {
@@ -1676,7 +1682,9 @@ export async function startFileTaskController(home, store, delivery, dispatcher,
1676
1682
  const runtime = new FileTaskController(store, delivery, {
1677
1683
  ...options,
1678
1684
  maintenanceFence: options.maintenanceFence
1679
- ?? ((projectId) => isProjectMaintenanceFenced(home, projectId))
1685
+ ?? ((projectId) => isProjectMaintenanceFenced(home, projectId)),
1686
+ leaderWakeFence: options.leaderWakeFence
1687
+ ?? (() => isHandoverLockHeld(home))
1680
1688
  });
1681
1689
  let stopping = false;
1682
1690
  const lifecycleRequests = new Set();
@@ -1,10 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
- import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
3
+ import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleProviderRuntimeForCleanup, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
4
4
  import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, markProviderTurnDeliveryUnknown, rejectProviderTurn, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
5
5
  import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
6
+ import { boundProviderRetryBeforeFirstProgress, projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
6
7
  import { hasRecentTurnId } from "../executor/turnCompletion.js";
7
8
  import { createTaskEvent } from "../event/taskEvent.js";
9
+ import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
8
10
  import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
9
11
  import { createTaskWake, fallbackWakeCursor, latestTaskWake } from "../scheduler/taskWake.js";
10
12
  import { rolloverTaskRoleSessionForContextBudget } from "../lifecycle/contextBudgetRollover.js";
@@ -595,7 +597,7 @@ export class FileSchedulerStoreAdapter {
595
597
  const latest = latestTaskWake(reader.listTaskWakes(taskId));
596
598
  const fromCursor = latest?.toCursor ?? fallbackWakeCursor({
597
599
  taskCreatedAt: task.createdAt,
598
- leaderRunCreatedAt: reader.listAgentRuns(taskId)
600
+ leaderRunCreatedAt: operationalTaskRecords(reader.listAgentRuns(taskId), reader.listEvents(taskId), "agent-run")
599
601
  .filter((run) => run.roleName === "leader")
600
602
  .at(-1)?.createdAt
601
603
  });
@@ -674,6 +676,9 @@ export class FileSchedulerStoreAdapter {
674
676
  : sessions?.sessions[agentId];
675
677
  return session === undefined ? null : mapSession(session);
676
678
  }
679
+ getTaskRoleSessionSet(taskId, roleName) {
680
+ return this.store.getTaskRoleSessionSet(taskId, roleName);
681
+ }
677
682
  listEvents(taskId) {
678
683
  return this.#taskReadProjection(taskId).events;
679
684
  }
@@ -1222,7 +1227,14 @@ export class FileSchedulerStoreAdapter {
1222
1227
  startedAt: now.toISOString()
1223
1228
  }), batchId);
1224
1229
  }
1225
- markRuntimeOwnerSessionStopped(store, runtimeOwnerFromTarget(target), now);
1230
+ const owner = runtimeOwnerFromTarget(target);
1231
+ markRuntimeOwnerSessionStopped(store, owner, now);
1232
+ if (owner.scope === "task") {
1233
+ const sessions = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
1234
+ if (sessions !== null && sessions.providerBinding !== null) {
1235
+ store.saveTaskRoleSessionSet(clearTaskRoleProviderRuntimeForCleanup(sessions, now));
1236
+ }
1237
+ }
1226
1238
  saveRuntimeLifecycleMailbox(store, mailbox);
1227
1239
  return true;
1228
1240
  });
@@ -1288,6 +1300,40 @@ export class FileSchedulerStoreAdapter {
1288
1300
  clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
1289
1301
  getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
1290
1302
  getOperatorNotification(taskId) { return this.store.getOperatorNotification(taskId); }
1303
+ saveLeaderFirstProgressStopLoss(input) {
1304
+ return this.store.transaction((store) => {
1305
+ const task = store.getTask(input.taskId);
1306
+ const role = store.getRole(input.taskId, input.roleName);
1307
+ if (task === null || task.status !== "active" || role === null
1308
+ || store.getLeaderFailure(input.taskId) !== null) {
1309
+ return "state-changed";
1310
+ }
1311
+ const stopLoss = projectFirstProgressStopLoss({
1312
+ sessions: store.getTaskRoleSessionSet(input.taskId, input.roleName),
1313
+ events: store.listEvents(input.taskId),
1314
+ workItems: store.listWorkItems(input.taskId),
1315
+ reviewRounds: store.listReviewRounds(input.taskId),
1316
+ integrations: store.listIntegrationAttempts(input.taskId)
1317
+ });
1318
+ if (!stopLoss.exhausted || stopLoss.fingerprint !== input.expectedFingerprint) {
1319
+ return "state-changed";
1320
+ }
1321
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
1322
+ const lastSession = sessions === null
1323
+ ? undefined
1324
+ : [...(sessions.history ?? []), ...Object.values(sessions.sessions)]
1325
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt))
1326
+ .at(-1);
1327
+ const message = `Leader first-progress stop-loss: ${stopLoss.reason}`;
1328
+ store.saveRole(input.taskId, updateRoleStatus(role, "failed", input.now));
1329
+ store.saveLeaderFailure(recordLeaderFailure(input.taskId, lastSession?.nativeSessionId ?? "(unregistered)", message, input.now, null));
1330
+ store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, message, input.now, store.getOperatorNotification(input.taskId)));
1331
+ enqueueWork(store, { kind: "operator" }, "leader-first-progress-stop-loss", input.now, [
1332
+ { type: "task", id: input.taskId }
1333
+ ]);
1334
+ return "recorded";
1335
+ });
1336
+ }
1291
1337
  saveLeaderDispatch(input) {
1292
1338
  return this.store.transaction((store) => {
1293
1339
  const task = store.getTask(input.task.id);
@@ -2241,7 +2287,7 @@ export class FileSchedulerStoreAdapter {
2241
2287
  failedNativeTurnId: input.nativeTurnId,
2242
2288
  lastErrorSummary: summary,
2243
2289
  ...(input.retryAfterMs === undefined ? {} : { retryAfterMs: input.retryAfterMs })
2244
- }, now, configuredProviderRetryPolicy(store));
2290
+ }, now, configuredProviderRetryPolicy(store, input.taskId, input.roleName));
2245
2291
  if (retryDecision.outcome === "exhausted") {
2246
2292
  this.recordProviderRetryClassified(store, input, classification.errorClass, {
2247
2293
  wouldRetry: "false",
@@ -2253,7 +2299,7 @@ export class FileSchedulerStoreAdapter {
2253
2299
  if (run.providerRetry === undefined)
2254
2300
  return null;
2255
2301
  const finalized = finalizeProviderRetryDeadline(store, run, retryDecision.reason === "attempts"
2256
- ? `Provider retry failed after all ${config.delaysMs.length} in-Session continuation attempts.`
2302
+ ? `Provider retry failed after all ${run.providerRetry.maxRetries} in-Session continuation attempts.`
2257
2303
  : retryDecision.reason === "retry-after-window"
2258
2304
  ? `Provider Retry-After exceeded the bounded ${config.maxWindowMs / 1_000}-second episode window.`
2259
2305
  : `Provider retry did not recover within the bounded ${config.maxWindowMs / 1_000}-second episode window.`, retryDecision.reason === "attempts" ? "attempts-exhausted" : "episode-window-exhausted", now);
@@ -3616,6 +3662,7 @@ function mapSession(session) {
3616
3662
  adapterId: session.adapterId,
3617
3663
  nativeSessionId: session.nativeSessionId,
3618
3664
  ...(session.launchId === undefined ? {} : { launchId: session.launchId }),
3665
+ ...(session.title === undefined ? {} : { title: session.title }),
3619
3666
  status: session.status,
3620
3667
  effective: session.effective,
3621
3668
  updatedAt: session.updatedAt
@@ -3629,6 +3676,7 @@ function saveTaskSession(store, role, session, status, now, launchId) {
3629
3676
  adapterId: session.adapterId,
3630
3677
  nativeSessionId: session.nativeSessionId,
3631
3678
  ...(launchId === undefined ? {} : { launchId }),
3679
+ ...(session.title === undefined ? {} : { title: session.title }),
3632
3680
  policy: "fixed",
3633
3681
  status,
3634
3682
  effective: session.effective
@@ -3659,7 +3707,7 @@ function bindTaskRoleRunInFlight(store, role, run, now) {
3659
3707
  agentId,
3660
3708
  runId: run.id,
3661
3709
  receiptId: agentRunDeliveryReceiptId(run)
3662
- }, now);
3710
+ }, now, run.mode);
3663
3711
  store.saveRoleSessionSet(updated);
3664
3712
  }
3665
3713
  function markTaskRoleRunPushedInFlight(store, role, run, now) {
@@ -4021,7 +4069,19 @@ function compareCanonicalObservationOrder(left, right) {
4021
4069
  || (left.ordinal ?? -1) - (right.ordinal ?? -1)
4022
4070
  || left.eventId.localeCompare(right.eventId);
4023
4071
  }
4024
- function configuredProviderRetryPolicy(store) {
4072
+ function configuredProviderRetryPolicy(store, taskId, roleName) {
4025
4073
  const config = providerRetryConfig(store.getConfig());
4074
+ if (taskId !== undefined && roleName === "leader") {
4075
+ const progress = projectFirstProgressStopLoss({
4076
+ sessions: store.getTaskRoleSessionSet(taskId, roleName),
4077
+ events: store.listEvents(taskId),
4078
+ workItems: store.listWorkItems(taskId),
4079
+ reviewRounds: store.listReviewRounds(taskId),
4080
+ integrations: store.listIntegrationAttempts(taskId)
4081
+ });
4082
+ if (progress.firstProgressAt === undefined) {
4083
+ return boundProviderRetryBeforeFirstProgress(config, progress);
4084
+ }
4085
+ }
4026
4086
  return { delaysMs: config.delaysMs, maxWindowMs: config.maxWindowMs };
4027
4087
  }
@@ -19,6 +19,7 @@ import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
19
19
  import { detectRunningRelease, isOwnerLive, readActiveReleasePointer, readHandoverFence, removeCandidateDiscovery, removeHandoverFence, writeCandidateDiscovery, writeHandoverFence, writeHandoverReceipt, writeRuntimeIdentity } from "../release/runtimeRelease.js";
20
20
  import { startFileTaskControllerRuntime } from "./runtime.js";
21
21
  import { readLinuxProcessStartIdentity } from "./domainIdentity.js";
22
+ import { RELEASE_HANDOVER_OLD_OWNER_GRACE_MS } from "../runtime/runtimeDeadlines.js";
22
23
  export const CONTROLLER_CANDIDATE_ENV = "YUI_CONTROLLER_CANDIDATE";
23
24
  export const CONTROLLER_HANDOVER_ID_ENV = "YUI_CONTROLLER_HANDOVER_ID";
24
25
  const DEFAULT_POLL_INTERVAL_MS = 100;
@@ -29,7 +30,7 @@ const DEFAULT_POLL_INTERVAL_MS = 100;
29
30
  * a second independent grace (see `DEFAULT_DUAL_OWNER_GRACE_MS` in
30
31
  * `releaseHandover.ts`, which is only an optional confirmation debounce).
31
32
  */
32
- export const DEFAULT_DUAL_OWNER_GRACE_MS = 30_000;
33
+ export const DEFAULT_DUAL_OWNER_GRACE_MS = RELEASE_HANDOVER_OLD_OWNER_GRACE_MS;
33
34
  /** Reads the candidate configuration from the process environment. */
34
35
  export function handoverCandidateFromEnvironment(environment) {
35
36
  if (environment[CONTROLLER_CANDIDATE_ENV] !== "1")
@@ -85,7 +86,6 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
85
86
  dualOwner: false
86
87
  }));
87
88
  advanceFence(home, fence, "candidate-ready", now().toISOString());
88
- const committedAt = Date.now();
89
89
  let dualOwnerReported = false;
90
90
  for (;;) {
91
91
  const current = readHandoverFence(home);
@@ -111,7 +111,7 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
111
111
  }
112
112
  if (current.phase === "committed"
113
113
  && !oldDead
114
- && Date.now() - committedAt > dualOwnerGraceMs
114
+ && committedFenceAgeMs(current, now()) > dualOwnerGraceMs
115
115
  && !dualOwnerReported) {
116
116
  // The old Controller was told to exit but is still live. Stay read-only
117
117
  // and make the dual-owner condition visible; never write concurrently.
@@ -129,6 +129,13 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
129
129
  await delay(pollIntervalMs);
130
130
  }
131
131
  }
132
+ function committedFenceAgeMs(fence, observedAt) {
133
+ const committedAt = Date.parse(fence.updatedAt);
134
+ if (!Number.isFinite(committedAt)) {
135
+ throw new Error("Committed handover fence timestamp is invalid.");
136
+ }
137
+ return observedAt.getTime() - committedAt;
138
+ }
132
139
  async function promote(home, fence, environment, now) {
133
140
  removeCandidateDiscovery(home);
134
141
  const controller = await startFileTaskControllerRuntime(home, { environment });
@@ -1,6 +1,5 @@
1
1
  import { callController } from "../core/controllerClient.js";
2
2
  import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
3
- import { yuiRunBodyFromInputMessage, yuiRunIdFromInputMessages } from "../run/runIdentity.js";
4
3
  import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
5
4
  import { setCodexThreadName } from "../execution/codexThreadNaming.js";
6
5
  import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
@@ -50,9 +49,11 @@ export function parseCodexSessionNotification(payloadArgument, environment) {
50
49
  const nativeSessionId = requireText(payload["thread-id"], "Codex thread-id");
51
50
  const turnId = requireText(payload["turn-id"], "Codex turn-id");
52
51
  const lastAssistantMessage = requireAssistantMessage(payload["last-assistant-message"]);
53
- const runId = yuiRunIdFromInputMessages(payload["input-messages"]);
52
+ const runId = environment.YUI_RUN_ID === undefined
53
+ ? undefined
54
+ : requireText(environment.YUI_RUN_ID, "YUI_RUN_ID");
54
55
  const title = environment.YUI_SESSION_TITLE === undefined
55
- ? sessionTitleFromInputMessages(payload["input-messages"])
56
+ ? undefined
56
57
  : requireText(environment.YUI_SESSION_TITLE, "YUI_SESSION_TITLE");
57
58
  const scope = environment.YUI_SESSION_SCOPE;
58
59
  if (scope !== "task" && scope !== "global") {
@@ -117,19 +118,6 @@ function requireAssistantMessage(value) {
117
118
  }
118
119
  return text;
119
120
  }
120
- function sessionTitleFromInputMessages(value) {
121
- if (!Array.isArray(value))
122
- return undefined;
123
- for (const entry of value) {
124
- if (typeof entry !== "string")
125
- continue;
126
- const body = yuiRunBodyFromInputMessage(entry);
127
- const normalized = body.trim().replaceAll(/\s+/g, " ");
128
- if (normalized.length > 0)
129
- return truncateSessionText(normalized);
130
- }
131
- return undefined;
132
- }
133
121
  function shouldSetThreadName(home, params) {
134
122
  if (params.scope !== "task"
135
123
  || params.title === undefined
@@ -168,12 +156,6 @@ function threadNameRequest(params, environment) {
168
156
  return null;
169
157
  }
170
158
  }
171
- function truncateSessionText(value) {
172
- const truncated = value.slice(0, 1_024);
173
- return /[\uD800-\uDBFF]$/.test(truncated)
174
- ? truncated.slice(0, -1)
175
- : truncated;
176
- }
177
159
  function isObject(value) {
178
160
  return typeof value === "object" && value !== null && !Array.isArray(value);
179
161
  }
@@ -276,7 +276,7 @@ class ClaudeAdapter extends BaseAdapter {
276
276
  return ["--append-system-prompt-file", input.managedContextFile];
277
277
  }
278
278
  compileResume(input) {
279
- const launch = super.compileNew(input);
279
+ const launch = this.compileNew(input);
280
280
  return { ...launch, argv: [...launch.argv, "--resume", nativeId(input.nativeSessionId)] };
281
281
  }
282
282
  compileManagedControl(input, mode, nativeSessionId) {
@@ -285,7 +285,7 @@ class ClaudeAdapter extends BaseAdapter {
285
285
  }
286
286
  const sessionId = nativeId(nativeSessionId);
287
287
  const launch = mode === "new"
288
- ? super.compileNew(input)
288
+ ? this.compileNew(input)
289
289
  : this.compileResume({ ...input, nativeSessionId: sessionId });
290
290
  return {
291
291
  ...launch,
@@ -173,9 +173,29 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
173
173
  // receive a fresh identity after the Role workspace changes.
174
174
  history: [...(set.history ?? []), ...Object.values(set.sessions)],
175
175
  sessions: {},
176
+ providerBinding: null,
176
177
  updatedAt: timestamp
177
178
  });
178
179
  }
180
+ /**
181
+ * Clears the Provider transport identity after its physical runtime is proven
182
+ * stopped, without retiring workspace-bound Session records. Workspace
183
+ * retirement remains a separate, stricter transaction after every supported
184
+ * placeholder has been terminalized.
185
+ */
186
+ export function clearTaskRoleProviderRuntimeForCleanup(set, now) {
187
+ validateRoleSessionSet(set);
188
+ if (set.inFlight !== null) {
189
+ throw new Error("Cannot clear a Task Role Provider runtime with unsettled Run state.");
190
+ }
191
+ if (set.providerBinding === null)
192
+ return set;
193
+ return validateRoleSessionSet({
194
+ ...set,
195
+ providerBinding: null,
196
+ updatedAt: requireDate(now, "Provider Runtime cleanup timestamp")
197
+ });
198
+ }
179
199
  /**
180
200
  * Terminalizes only the aggregate-16 Claude placeholder shape after the
181
201
  * caller has fenced the Task store and proved that the exact Role has no live
@@ -261,18 +281,18 @@ export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
261
281
  }
262
282
  return "new";
263
283
  }
284
+ if (session.status === "stopped" || session.status === "broken") {
285
+ return "new";
286
+ }
264
287
  const compatible = set.owner.scope === "task"
265
288
  ? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, desired, workspace)
266
289
  : effectiveLaunchSnapshotsCompatible(session.effective, desired);
267
290
  if (compatible)
268
291
  return "resume";
269
- if (session.status !== "stopped" && session.status !== "broken") {
270
- throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
271
- + "Stop the existing native process before starting a fresh Session.");
272
- }
273
- return "new";
292
+ throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
293
+ + "Stop the existing native process before starting a fresh Session.");
274
294
  }
275
- export function bindTaskRoleRun(set, fence, preparedAt) {
295
+ export function bindTaskRoleRun(set, fence, preparedAt, mode) {
276
296
  validateRoleSessionSet(set);
277
297
  assertTaskRoleSessionSet(set);
278
298
  const normalized = normalizeTaskRoleRunFence(fence);
@@ -285,9 +305,9 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
285
305
  throw new Error("Task Role session set already has an in-flight Run.");
286
306
  }
287
307
  const timestamp = requireDate(preparedAt, "Task Role Run preparedAt");
288
- const providerBinding = set.providerBinding === null
289
- ? null
290
- : rebindProviderRuntimeRun(set.providerBinding, normalized.runId);
308
+ const providerBinding = mode === "resume" && set.providerBinding !== null
309
+ ? rebindProviderRuntimeRun(set.providerBinding, normalized.runId)
310
+ : null;
291
311
  const updated = {
292
312
  ...set,
293
313
  inFlight: { ...normalized, preparedAt: timestamp },