@zq-silk/yui 0.6.0 → 0.6.2

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 (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
@@ -1,9 +1,22 @@
1
+ import { realpathSync } from "node:fs";
1
2
  import { resolve } from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { resolveYuiHome } from "../storage/taskStore.js";
5
+ import { handoverCandidateFromEnvironment, runHandoverCandidate } from "./handoverCandidate.js";
4
6
  import { startFileTaskControllerRuntime } from "./runtime.js";
5
7
  export async function runFileTaskControllerProcess(environment = process.env) {
6
8
  const home = resolveYuiHome(environment);
9
+ // Issue 02: a handover candidate runs the read-only prelude and promotes
10
+ // itself once the old owner exits. It never binds the main socket first.
11
+ const candidate = handoverCandidateFromEnvironment(environment);
12
+ if (candidate !== null) {
13
+ const result = await runHandoverCandidate(home, candidate.handoverId, { environment });
14
+ if (result.outcome === "aborted") {
15
+ process.stderr.write(`Controller handover candidate aborted: ${result.reason}\n`);
16
+ process.exitCode = 5;
17
+ }
18
+ return;
19
+ }
7
20
  const controller = await startFileTaskControllerRuntime(home, {
8
21
  environment,
9
22
  onError: (error) => {
@@ -25,8 +38,18 @@ export async function runFileTaskControllerProcess(environment = process.env) {
25
38
  }
26
39
  }
27
40
  function isEntrypoint() {
28
- return process.argv[1] !== undefined
29
- && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
41
+ // Node's ESM loader resolves symlinks for import.meta.url, while
42
+ // process.argv[1] keeps the path as invoked. A release installed under a
43
+ // symlinked Home (e.g. /home -> /data00/home) must still detect itself as
44
+ // the entrypoint, so compare realpaths on both sides.
45
+ if (process.argv[1] === undefined)
46
+ return false;
47
+ try {
48
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
49
+ }
50
+ catch {
51
+ return resolve(process.argv[1]) === fileURLToPath(import.meta.url);
52
+ }
30
53
  }
31
54
  if (isEntrypoint()) {
32
55
  void runFileTaskControllerProcess().catch((error) => {
@@ -297,18 +297,26 @@ export function ephemeralDomainFingerprint(identity, fileFingerprint) {
297
297
  fileFingerprint ?? ""
298
298
  ].join(":");
299
299
  }
300
+ /**
301
+ * Parses the process start identity (field 20, `starttime`) from the content
302
+ * of a Linux `/proc/<pid>/stat` file. Shared by every reader so the parsing
303
+ * (including the `comm` field's embedded spaces/parentheses) is defined once.
304
+ * Returns undefined when the content is not a recognizable stat line.
305
+ */
306
+ export function parseLinuxProcessStartIdentity(statContent) {
307
+ const closing = statContent.lastIndexOf(")");
308
+ if (closing < 0)
309
+ return undefined;
310
+ const identity = statContent.slice(closing + 1).trim().split(/\s+/u)[19];
311
+ return identity !== undefined && /^[0-9]{1,32}$/u.test(identity)
312
+ ? identity
313
+ : undefined;
314
+ }
300
315
  export function readLinuxProcessStartIdentity(pid) {
301
316
  if (!Number.isSafeInteger(pid) || pid <= 0)
302
317
  return undefined;
303
318
  try {
304
- const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
305
- const closing = stat.lastIndexOf(")");
306
- if (closing < 0)
307
- return undefined;
308
- const identity = stat.slice(closing + 1).trim().split(/\s+/u)[19];
309
- return identity !== undefined && /^[0-9]{1,32}$/u.test(identity)
310
- ? identity
311
- : undefined;
319
+ return parseLinuxProcessStartIdentity(readFileSync(`/proc/${pid}/stat`, "utf8"));
312
320
  }
313
321
  catch {
314
322
  return undefined;
@@ -1,12 +1,17 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
+ import { routeProviderProgress } from "../telemetry/telemetryRouter.js";
3
+ import { NullTelemetrySink } from "../telemetry/telemetryStore.js";
2
4
  import { activeLiveRoleAgentSession, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, recordObservedTaskRoleCompletion, recordRoleAgentSession, rememberRoleAgentCompletedTurn, settleTaskRoleCompletion, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
3
5
  import { createPendingTurnCompletion, hasRecentTurnId } from "../executor/turnCompletion.js";
4
6
  import { createTaskEvent } from "../event/taskEvent.js";
5
7
  import { answerInputRequest } from "../input/inputRequest.js";
6
8
  import { activeRoleAgentBinding, updateRoleStatus } from "../role/role.js";
7
- import { effectiveLaunchSnapshotsCompatible, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
9
+ import { effectiveLaunchWithTaskMainWorkspace, effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain, resolveEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
8
10
  import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
9
- import { failAgentRun, markAgentRunDelivered, markAgentRunPushed } from "../run/agentRun.js";
11
+ import { failAgentRun, markAgentRunDelivered, markAgentRunPushed, reopenRunForProviderRetry, withProviderRetry } from "../run/agentRun.js";
12
+ import { classifyProviderError, isRetryableProviderErrorClass } from "../lifecycle/providerErrorClass.js";
13
+ import { providerRetryIsDue, scheduleProviderRetry } from "../run/providerRetry.js";
14
+ import { providerRetryConfig } from "../run/providerRetryConfig.js";
10
15
  import { terminalizeExactTaskRun } from "../lifecycle/exactRunTerminalization.js";
11
16
  import { foldCanonicalLifecycleEvent } from "../lifecycle/canonicalLifecycleEvent.js";
12
17
  import { mapNativeLifecycleSignal } from "../lifecycle/providerLifecycleMapping.js";
@@ -14,7 +19,7 @@ import { recordLeaderFailure } from "../scheduler/leaderFailure.js";
14
19
  import { createLeaderRecoveryNotification, createLeaderStallNotification } from "../scheduler/operatorNotification.js";
15
20
  import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
16
21
  import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
17
- import { latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, isRoleRunStalled, clearMatchingLeaderStallAttention, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT, RUN_RESOURCE_SUPPRESSED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
22
+ import { foldRunProgressFacts, latestRunDurableProgressAt, latestRunEventTime, latestStallEvidenceKey, isRoleRunStalled, clearMatchingLeaderStallAttention, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT, RUN_RESOURCE_SUPPRESSED_EVENT, RUN_STALLED_EVENT } from "../scheduler/roleRunStall.js";
18
23
  import { currentWorkItemExecutionGroup, updateWorkItemExecutionGroup, updateWorkItemStatus } from "../workItem/workItem.js";
19
24
  import { recordExecutionLaneResult } from "../execution/executionGroup.js";
20
25
  import { formatAgentRunReceiptId, formatTaskRecordReference } from "../task/taskRecordReference.js";
@@ -22,11 +27,68 @@ import { bindExecution, claimPending, completeProcessing, releaseProcessing } fr
22
27
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
23
28
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_LAUNCH_RESERVED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
24
29
  import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
30
+ const NULL_TELEMETRY_SINK = new NullTelemetrySink();
25
31
  /** Maps the authoritative FileTaskStore records to the scheduler's narrow port. */
26
32
  export class FileSchedulerStoreAdapter {
27
33
  store;
28
- constructor(store) {
34
+ telemetry;
35
+ /**
36
+ * @param telemetry Optional sidecar wiring (Issue 09). Absent or `legacy`
37
+ * keeps master behavior (semantic progress events only); `dual` also
38
+ * upserts progress to the sidecar; `bounded` routes progress to the
39
+ * sidecar only and feeds the scheduler projection synthesized latest
40
+ * progress from the reader.
41
+ */
42
+ constructor(store, telemetry = null) {
29
43
  this.store = store;
44
+ this.telemetry = telemetry;
45
+ }
46
+ /**
47
+ * Revision-scoped read projection. Keyed by the store's durable revision;
48
+ * any committed mutation (ours or an external writer's) advances it and the
49
+ * next read rebuilds. A store without getStateRevision disables caching.
50
+ */
51
+ #readProjection = null;
52
+ #taskReadProjection(taskId) {
53
+ const revision = typeof this.store.getStateRevision === "function"
54
+ ? this.store.getStateRevision()
55
+ : Number.NaN;
56
+ const telemetryRevision = this.telemetry?.mode === "bounded"
57
+ ? this.telemetry.reader.revision()
58
+ : 0;
59
+ if (this.#readProjection === null
60
+ || this.#readProjection.revision !== revision
61
+ || this.#readProjection.telemetryRevision !== telemetryRevision) {
62
+ this.#readProjection = { revision, telemetryRevision, tasks: new Map() };
63
+ }
64
+ const tasks = this.#readProjection.tasks;
65
+ let task = tasks.get(taskId);
66
+ if (task === undefined) {
67
+ const semanticEvents = this.store.listEvents(taskId);
68
+ // In bounded mode progress lives only in the sidecar; merge the latest
69
+ // observation per Run so liveness/stall folds keep working. In legacy
70
+ // and dual modes semantic history already carries progress.
71
+ const events = this.telemetry?.mode === "bounded"
72
+ ? [...semanticEvents, ...this.telemetry.reader.latestProgressEvents(taskId)]
73
+ : semanticEvents;
74
+ task = {
75
+ events,
76
+ runFacts: foldRunProgressFacts(events),
77
+ agentRuns: this.store.listAgentRuns(taskId),
78
+ workItems: this.store.listWorkItems(taskId),
79
+ reviewRounds: this.store.listReviewRounds(taskId),
80
+ changeSets: this.store.listChangeSets(taskId),
81
+ integrationAttempts: this.store.listIntegrationAttempts(taskId),
82
+ inputRequests: this.store.listInputRequests(taskId),
83
+ durableJobs: this.store.listDurableJobs(taskId),
84
+ messages: this.store.listMessages(taskId)
85
+ };
86
+ tasks.set(taskId, task);
87
+ }
88
+ return task;
89
+ }
90
+ withRuntimeEventTransaction(execute) {
91
+ return this.store.withRuntimeEventTransaction(execute);
30
92
  }
31
93
  getPresentationContext() {
32
94
  return { timeZone: this.store.getConfig().timeZone };
@@ -98,10 +160,51 @@ export class FileSchedulerStoreAdapter {
98
160
  return session === undefined ? null : mapSession(session);
99
161
  }
100
162
  listEvents(taskId) {
101
- return this.store.listEvents(taskId);
163
+ return this.#taskReadProjection(taskId).events;
164
+ }
165
+ listAgentRuns(taskId) {
166
+ return this.#taskReadProjection(taskId).agentRuns;
167
+ }
168
+ listWorkItems(taskId) {
169
+ return this.#taskReadProjection(taskId).workItems;
170
+ }
171
+ listReviewRounds(taskId) {
172
+ return this.#taskReadProjection(taskId).reviewRounds;
173
+ }
174
+ listIntegrationAttempts(taskId) {
175
+ return this.#taskReadProjection(taskId).integrationAttempts;
176
+ }
177
+ listDurableJobs(taskId) {
178
+ return this.#taskReadProjection(taskId).durableJobs;
179
+ }
180
+ listInputRequests(taskId) {
181
+ return this.#taskReadProjection(taskId).inputRequests;
182
+ }
183
+ listMessages(taskId) {
184
+ return this.#taskReadProjection(taskId).messages;
185
+ }
186
+ getRunProgressFacts(taskId, runId) {
187
+ return this.#taskReadProjection(taskId).runFacts.get(runId);
102
188
  }
103
189
  getRunDurableProgress(taskId, roleName, runId) {
104
- return latestRunDurableProgressAt(this.store, taskId, roleName, runId);
190
+ const projected = this.#taskReadProjection(taskId);
191
+ // Serve the related-record fold from the same revision's projection: the
192
+ // event history and the WorkItem/Review/ChangeSet/Integration/Input lists
193
+ // are read once per Task per revision, and the per-Run checkpoint/activity
194
+ // facts come from the one-pass fold instead of per-candidate scans.
195
+ const view = {
196
+ getAgentRun: (id, agentRunId) => this.store.getAgentRun(id, agentRunId),
197
+ listEvents: () => projected.events,
198
+ getWorkItem: (id, workItemId) => this.store.getWorkItem(id, workItemId),
199
+ listReviewRounds: () => projected.reviewRounds,
200
+ listChangeSets: () => projected.changeSets,
201
+ listIntegrationAttempts: () => projected.integrationAttempts,
202
+ listInputRequests: () => projected.inputRequests
203
+ };
204
+ // A missing fold entry is an authoritative empty fold, not a signal to
205
+ // re-scan the whole history. Pass {} so latestRunDurableProgressAt treats
206
+ // the fold as present and skips the per-candidate fallback scans.
207
+ return latestRunDurableProgressAt(view, taskId, roleName, runId, projected.runFacts.get(runId) ?? {});
105
208
  }
106
209
  recordRoleRunStall(input) {
107
210
  return this.store.transaction((store) => {
@@ -241,8 +344,9 @@ export class FileSchedulerStoreAdapter {
241
344
  // A provider-ready fact for this exact generation is proven by a folded
242
345
  // runtime.provider-session-lifecycle event with preInputReady true and a
243
346
  // matching agent/launch/nativeSession fence. This reads durable folded truth
244
- // only — no liveness, screen, or pane/PID inference.
245
- return this.store.listEvents(input.taskId).some((event) => (event.type === "runtime.provider-session-lifecycle"
347
+ // only — no liveness, screen, or pane/PID inference. Served from the
348
+ // revision projection: a stale miss only repeats an idempotent first push.
349
+ return this.listEvents(input.taskId).some((event) => (event.type === "runtime.provider-session-lifecycle"
246
350
  && event.payload.roleName === input.roleName
247
351
  && event.payload.agentId === input.agentId
248
352
  && event.payload.preInputReady === "true"
@@ -265,6 +369,32 @@ export class FileSchedulerStoreAdapter {
265
369
  enqueueLeaderWakeup(taskId, reason, now) {
266
370
  return this.store.transaction((store) => (queueLeaderWakeup(store, taskId, reason, now)));
267
371
  }
372
+ listAllDurableJobs() {
373
+ return this.store.listAllDurableJobs();
374
+ }
375
+ /**
376
+ * Apply one durable-job transition and, in the SAME transaction, enqueue
377
+ * the Leader wakeup. A terminal job without its wakeup enqueued is a lost
378
+ * wakeup, so the two writes commit together or not at all.
379
+ *
380
+ * f6: The wakeup targets the Leader role mailbox (not the Task mailbox)
381
+ * and also queues a pending wakeup so processLeaderWakeups dispatches
382
+ * the Leader Run with the exact job-finished reason.
383
+ */
384
+ transitionDurableJob(taskId, jobId, transition, now, wakeup) {
385
+ return this.store.transaction((store) => {
386
+ const current = store.getDurableJob(taskId, jobId);
387
+ if (current === null)
388
+ return null;
389
+ const next = transition(current);
390
+ store.saveDurableJob(taskId, next);
391
+ if (wakeup !== undefined) {
392
+ enqueueWork(store, { kind: "role", taskId, roleName: "leader" }, wakeup.reason, now, [...wakeup.refs]);
393
+ queueLeaderWakeup(store, taskId, wakeup.reason, now);
394
+ }
395
+ return next;
396
+ });
397
+ }
268
398
  releaseLeaderWakeupAndEnqueue(taskId, batchId, reason, now) {
269
399
  const target = { kind: "role", taskId, roleName: "leader" };
270
400
  return this.store.transaction((store) => {
@@ -1383,6 +1513,11 @@ export class FileSchedulerStoreAdapter {
1383
1513
  return { disposition: "obsolete", runId: input.runId };
1384
1514
  }
1385
1515
  const summary = claudeStopFailureSummary(input);
1516
+ // Issue 04: a classified transient Provider failure keeps the exact Run
1517
+ // and Native Session and is retried in place instead of terminalizing.
1518
+ const retryDecision = this.providerRetryDecision(store, input, summary, now);
1519
+ if (retryDecision !== null)
1520
+ return retryDecision;
1386
1521
  const result = terminalizeExactTaskRun(store, {
1387
1522
  taskId: input.taskId,
1388
1523
  roleName: input.roleName,
@@ -1442,6 +1577,218 @@ export class FileSchedulerStoreAdapter {
1442
1577
  return { disposition: "applied", runId: input.runId };
1443
1578
  });
1444
1579
  }
1580
+ /**
1581
+ * Issue 04 in-place retry decision for one classified Provider failure.
1582
+ * Returns an observation when the failure was handled without terminalizing
1583
+ * the Run, or `null` to fall through to the existing terminalization path.
1584
+ */
1585
+ providerRetryDecision(store, input, summary, now) {
1586
+ const config = providerRetryConfig();
1587
+ if (config.mode === "off")
1588
+ return null;
1589
+ const classification = classifyProviderError({
1590
+ adapterId: input.adapterId,
1591
+ error: input.error,
1592
+ errorDetails: input.errorDetails,
1593
+ summary: input.lastAssistantMessage
1594
+ });
1595
+ const adapterEnabled = config.adapters.includes(input.adapterId);
1596
+ const retryable = adapterEnabled
1597
+ && isRetryableProviderErrorClass(classification.errorClass);
1598
+ const shadow = config.mode === "shadow";
1599
+ // Shadow mode records the classification and "would retry" fact without
1600
+ // changing any behavior; the Run still terminalizes below.
1601
+ if (shadow || !retryable) {
1602
+ if (adapterEnabled) {
1603
+ this.recordProviderRetryClassified(store, input, classification.errorClass, {
1604
+ wouldRetry: String(retryable),
1605
+ shadow: String(shadow)
1606
+ }, now);
1607
+ }
1608
+ if (!shadow && classification.errorClass === "policy-denied") {
1609
+ const blocked = this.applyProviderPolicyBlock(store, input, summary, now);
1610
+ if (blocked !== null)
1611
+ return blocked;
1612
+ }
1613
+ return null;
1614
+ }
1615
+ // transport-uncertain: the Provider may have accepted the turn. If a
1616
+ // native completion is already durable, the completion path owns the
1617
+ // yield; do not resend.
1618
+ if (classification.errorClass === "transport-uncertain") {
1619
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
1620
+ const pending = sessions?.pendingTurnCompletion;
1621
+ if (pending !== null
1622
+ && pending !== undefined
1623
+ && pending.runId === input.runId
1624
+ && pending.agentId === input.agentId) {
1625
+ this.recordProviderRetryClassified(store, input, classification.errorClass, {
1626
+ wouldRetry: "false",
1627
+ shadow: "false",
1628
+ note: "native-turn-completion-durable"
1629
+ }, now);
1630
+ return { disposition: "applied", runId: input.runId };
1631
+ }
1632
+ }
1633
+ const run = store.getAgentRun(input.taskId, input.runId);
1634
+ if (run === null || run.status !== "active")
1635
+ return null;
1636
+ const retry = scheduleProviderRetry(run.providerRetry, {
1637
+ errorClass: classification.errorClass,
1638
+ launchId: input.launchId,
1639
+ nativeSessionId: input.nativeSessionId,
1640
+ lastErrorSummary: summary
1641
+ }, now);
1642
+ store.saveAgentRun(withProviderRetry(run, retry));
1643
+ // Clear the in-flight fence so the original Session can be re-bound and
1644
+ // re-pushed by the delivery path; the Session itself stays alive.
1645
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
1646
+ if (sessions !== null) {
1647
+ store.saveTaskRoleSessionSet(clearTaskRoleRun(sessions, {
1648
+ agentId: input.agentId,
1649
+ runId: input.runId,
1650
+ receiptId: `agent-run:${input.taskId}/${input.runId}`
1651
+ }, now));
1652
+ }
1653
+ // Settle the delivery claim so the retry push can re-claim the mailbox.
1654
+ const target = runtimeLifecycleTarget({
1655
+ scope: "task",
1656
+ taskId: input.taskId,
1657
+ roleName: input.roleName
1658
+ });
1659
+ const mailbox = store.getWorkMailbox(target);
1660
+ if (mailbox !== null && mailbox.processing !== null) {
1661
+ const settled = completeProcessing(mailbox, mailbox.processing.batchId);
1662
+ if (settled.processing === null && settled.pending === null) {
1663
+ store.removeWorkMailbox(target);
1664
+ }
1665
+ else {
1666
+ store.saveWorkMailbox(settled);
1667
+ }
1668
+ }
1669
+ this.recordProviderRetryClassified(store, input, classification.errorClass, {
1670
+ wouldRetry: "true",
1671
+ shadow: "false",
1672
+ attempt: String(retry.attempt),
1673
+ nextAttemptAt: retry.nextAttemptAt
1674
+ }, now);
1675
+ return { disposition: "applied", runId: input.runId };
1676
+ }
1677
+ /**
1678
+ * Issue 04 policy-denied handling: the Run stays active on its original
1679
+ * Session, no retry is scheduled, and the Leader receives a bounded blocker.
1680
+ * Yui never works around a policy denial by switching Session or widening
1681
+ * permission.
1682
+ */
1683
+ applyProviderPolicyBlock(store, input, summary, now) {
1684
+ const run = store.getAgentRun(input.taskId, input.runId);
1685
+ if (run === null || run.status !== "active")
1686
+ return null;
1687
+ const retry = scheduleProviderRetry(run.providerRetry, {
1688
+ errorClass: "policy-denied",
1689
+ launchId: input.launchId,
1690
+ nativeSessionId: input.nativeSessionId,
1691
+ lastErrorSummary: summary,
1692
+ scheduleNextAttempt: false
1693
+ }, now);
1694
+ store.saveAgentRun(withProviderRetry(run, retry));
1695
+ this.recordProviderRetryClassified(store, input, "policy-denied", {
1696
+ wouldRetry: "false",
1697
+ shadow: "false",
1698
+ note: "policy-denied-blocker"
1699
+ }, now);
1700
+ if (input.roleName !== "leader") {
1701
+ enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, "provider-policy-blocked", now, [{ type: "run", taskId: input.taskId, id: input.runId }]);
1702
+ }
1703
+ return { disposition: "applied", runId: input.runId };
1704
+ }
1705
+ recordProviderRetryClassified(store, input, errorClass, extra, now) {
1706
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-retry-classified", {
1707
+ eventId: input.eventId,
1708
+ runId: input.runId,
1709
+ roleName: input.roleName,
1710
+ errorClass,
1711
+ ...extra
1712
+ }, now));
1713
+ }
1714
+ /**
1715
+ * Issue 04 durable retry timer: lists active Runs whose in-place retry is
1716
+ * due. The Controller arms its deadline timer from this projection, so a
1717
+ * Controller restart resumes the same attempt lineage.
1718
+ */
1719
+ listPendingProviderRetries() {
1720
+ return this.store.listPendingProviderRetries();
1721
+ }
1722
+ /**
1723
+ * Issue 04: reopens each due retry on its original Native Session. A Run
1724
+ * whose Session is proven dead terminalizes with an exact replacement
1725
+ * blocker; a live Session is reset for the existing delivery path, which
1726
+ * re-pushes the exact same input in the same pass.
1727
+ */
1728
+ resolveDueProviderRetries(now, taskIds) {
1729
+ const due = this.listPendingProviderRetries().filter((entry) => (Date.parse(entry.nextAttemptAt) <= now.getTime()
1730
+ && (taskIds === undefined || taskIds.has(entry.taskId))));
1731
+ const reopened = [];
1732
+ for (const entry of due) {
1733
+ this.store.transaction((store) => {
1734
+ const run = store.getAgentRun(entry.taskId, entry.runId);
1735
+ if (run === null
1736
+ || run.status !== "active"
1737
+ || run.providerRetry === undefined
1738
+ || run.providerRetry.nextAttemptAt === undefined
1739
+ || !providerRetryIsDue(run.providerRetry, now)) {
1740
+ return;
1741
+ }
1742
+ const sessions = store.getTaskRoleSessionSet(entry.taskId, entry.roleName);
1743
+ const session = sessions?.sessions[run.effective.agentId];
1744
+ const identityMatches = session !== undefined
1745
+ && session.adapterId === run.effective.adapterId
1746
+ && (run.providerRetry.nativeSessionId === undefined
1747
+ || session.nativeSessionId === run.providerRetry.nativeSessionId)
1748
+ && (run.providerRetry.launchId === undefined
1749
+ || session.launchId === run.providerRetry.launchId);
1750
+ const sessionAlive = identityMatches
1751
+ && session.status !== "stopped"
1752
+ && session.status !== "broken";
1753
+ if (!sessionAlive) {
1754
+ // Session-dead: stop in-place retry and raise a precise, actionable
1755
+ // replacement blocker for the Leader.
1756
+ const summary = `Provider retry stopped: the original Session is gone `
1757
+ + `(run ${run.id}, attempt ${run.providerRetry.attempt}, `
1758
+ + `last error: ${run.providerRetry.lastErrorSummary}). `
1759
+ + `A replacement Session requires an explicit Leader recovery decision.`;
1760
+ const result = terminalizeExactTaskRun(store, {
1761
+ taskId: entry.taskId,
1762
+ roleName: entry.roleName,
1763
+ agentId: run.effective.agentId,
1764
+ runId: run.id,
1765
+ receiptId: formatAgentRunReceiptId(entry.taskId, run.id),
1766
+ ...(run.providerRetry.nativeSessionId === undefined
1767
+ ? {}
1768
+ : { nativeSessionId: run.providerRetry.nativeSessionId }),
1769
+ ...(run.providerRetry.launchId === undefined
1770
+ ? {}
1771
+ : { launchId: run.providerRetry.launchId }),
1772
+ outcome: { status: "failed", summary }
1773
+ }, now);
1774
+ if (result.disposition !== "applied")
1775
+ return;
1776
+ store.saveEvent(entry.taskId, createTaskEvent(store.nextEventId(entry.taskId), entry.taskId, "runtime.provider-retry-session-dead", { runId: run.id, attempt: String(run.providerRetry.attempt) }, now));
1777
+ enqueueWork(store, { kind: "role", taskId: entry.taskId, roleName: "leader" }, "role-run-failed", now, [{ type: "run", taskId: entry.taskId, id: run.id }]);
1778
+ return;
1779
+ }
1780
+ // Reopen the Run on its original Session: reset transport markers,
1781
+ // switch to resume, and mark the projection in-flight. The delivery
1782
+ // pass re-pushes the exact same input in this same pass.
1783
+ store.saveAgentRun(reopenRunForProviderRetry(run, now));
1784
+ // The retry decision settled the original delivery claim; re-queue the
1785
+ // Role mailbox so the delivery pass can claim and re-push the Run.
1786
+ enqueueWork(store, { kind: "role", taskId: entry.taskId, roleName: entry.roleName }, "provider-retry-repush", now, [{ type: "run", taskId: entry.taskId, id: run.id }]);
1787
+ reopened.push(formatTaskRecordReference(entry.taskId, run.id, "agentRun"));
1788
+ });
1789
+ }
1790
+ return reopened;
1791
+ }
1445
1792
  /**
1446
1793
  * Folds a provider SessionStart lifecycle fact
1447
1794
  * through the canonical contract. Session-started/ready never advance Run
@@ -1596,22 +1943,29 @@ export class FileSchedulerStoreAdapter {
1596
1943
  case "apply":
1597
1944
  if (decision.outcome.outcome !== "advance-progress")
1598
1945
  return "obsolete";
1599
- if (store.listEvents(input.taskId).some((event) => (event.type === "runtime.provider-turn-progress"
1600
- && event.payload.eventId === input.eventId)))
1601
- return "applied";
1602
- store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-turn-progress", {
1603
- eventId: input.eventId,
1604
- roleName: input.roleName,
1605
- agentId: input.agentId,
1606
- adapterId: input.adapterId,
1607
- launchId: input.launchId,
1608
- nativeSessionId: input.nativeSessionId,
1609
- runId: input.runId,
1610
- progressId: input.progressId,
1611
- progressAt: input.receivedAt,
1612
- ...(input.sequence === undefined ? {} : { sequence: String(input.sequence) })
1613
- }, now));
1614
- return "applied";
1946
+ return routeProviderProgress({
1947
+ mode: this.telemetry?.mode ?? "legacy",
1948
+ entry: toTelemetryEntry(input),
1949
+ semanticExists: this.telemetry?.mode === "bounded"
1950
+ ? false
1951
+ : store.listEvents(input.taskId).some((event) => (event.type === "runtime.provider-turn-progress"
1952
+ && event.payload.eventId === input.eventId)),
1953
+ writeSemantic: () => {
1954
+ store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "runtime.provider-turn-progress", {
1955
+ eventId: input.eventId,
1956
+ roleName: input.roleName,
1957
+ agentId: input.agentId,
1958
+ adapterId: input.adapterId,
1959
+ launchId: input.launchId,
1960
+ nativeSessionId: input.nativeSessionId,
1961
+ runId: input.runId,
1962
+ progressId: input.progressId,
1963
+ progressAt: input.receivedAt,
1964
+ ...(input.sequence === undefined ? {} : { sequence: String(input.sequence) })
1965
+ }, now));
1966
+ },
1967
+ sink: this.telemetry?.sink ?? NULL_TELEMETRY_SINK
1968
+ });
1615
1969
  }
1616
1970
  });
1617
1971
  }
@@ -2102,7 +2456,7 @@ function recordTaskRuntimeNativeSession(store, input, now) {
2102
2456
  const effective = input.effective === undefined
2103
2457
  ? resolvedEffective
2104
2458
  : validateEffectiveLaunchSnapshot(input.effective);
2105
- if (!effectiveLaunchSnapshotsCompatible(resolvedEffective, effective)) {
2459
+ if (!effectiveLaunchSnapshotsCompatibleForTaskMain(resolvedEffective, effective, store.getTaskWorkspace(input.taskId))) {
2106
2460
  throw new Error("Reserved native Session effective launch changed before persistence.");
2107
2461
  }
2108
2462
  if (effective.agentId !== input.agentId || effective.adapterId !== input.adapterId) {
@@ -2115,7 +2469,7 @@ function recordTaskRuntimeNativeSession(store, input, now) {
2115
2469
  ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
2116
2470
  policy: "fixed",
2117
2471
  status: "running",
2118
- effective
2472
+ effective: effectiveExisting?.effective ?? effective
2119
2473
  }, now);
2120
2474
  store.saveRoleSessionSet(updated);
2121
2475
  return updated.sessions[input.agentId];
@@ -2174,17 +2528,20 @@ function mapRole(store, role) {
2174
2528
  ?? undefined;
2175
2529
  const reopened = role.name === "leader"
2176
2530
  && store.getPendingWakeup(role.taskId)?.reasons.includes("task-reopened") === true;
2531
+ const liveSession = activeLiveRoleAgentSession(store.getTaskRoleSessionSet(role.taskId, role.name));
2177
2532
  const effective = reopened
2178
2533
  ? resolveEffectiveLaunch({
2179
2534
  role,
2180
2535
  purpose: "execution",
2181
2536
  ...(workspace === undefined ? {} : { workspace })
2182
2537
  })
2183
- : activeLiveRoleAgentSession(store.getTaskRoleSessionSet(role.taskId, role.name))?.effective ?? resolveEffectiveLaunch({
2184
- role,
2185
- purpose: "execution",
2186
- ...(workspace === undefined ? {} : { workspace })
2187
- });
2538
+ : liveSession !== null && workspace?.owner.type === "task"
2539
+ ? effectiveLaunchWithTaskMainWorkspace(liveSession.effective, workspace)
2540
+ : liveSession?.effective ?? resolveEffectiveLaunch({
2541
+ role,
2542
+ purpose: "execution",
2543
+ ...(workspace === undefined ? {} : { workspace })
2544
+ });
2188
2545
  return {
2189
2546
  taskId: role.taskId,
2190
2547
  name: role.name,
@@ -2204,6 +2561,13 @@ function taskSessionEffective(store, taskId, roleName, agentId, existing) {
2204
2561
  if (active.effective.agentId !== agentId) {
2205
2562
  throw new Error(`Native Session registration does not match the effective Run Agent: ${taskId}/${roleName}.`);
2206
2563
  }
2564
+ if (existing !== undefined) {
2565
+ const workspace = active.workspace ?? store.getTaskWorkspace(taskId);
2566
+ if (!effectiveLaunchSnapshotsCompatibleForTaskMain(existing.effective, active.effective, workspace)) {
2567
+ throw new Error(`Native Session effective launch does not match the active Run: ${taskId}/${roleName}.`);
2568
+ }
2569
+ return existing.effective;
2570
+ }
2207
2571
  return active.effective;
2208
2572
  }
2209
2573
  if (existing !== undefined)
@@ -2370,3 +2734,31 @@ function runLaunchEventPayload(run) {
2370
2734
  writeProjectIds: run.effective.writeProjectIds.join(",") || "none"
2371
2735
  };
2372
2736
  }
2737
+ /**
2738
+ * Maps a provider progress observation to the telemetry sidecar entry. The
2739
+ * payload mirrors the semantic event payload so sidecar rows and legacy
2740
+ * events carry identical evidence.
2741
+ */
2742
+ function toTelemetryEntry(input) {
2743
+ return {
2744
+ taskId: input.taskId,
2745
+ roleName: input.roleName,
2746
+ runId: input.runId,
2747
+ generation: input.launchId !== "" ? input.launchId : input.nativeSessionId,
2748
+ progressId: input.progressId,
2749
+ ...(input.sequence === undefined ? {} : { sequence: input.sequence }),
2750
+ payload: {
2751
+ eventId: input.eventId,
2752
+ roleName: input.roleName,
2753
+ agentId: input.agentId,
2754
+ adapterId: input.adapterId,
2755
+ launchId: input.launchId,
2756
+ nativeSessionId: input.nativeSessionId,
2757
+ runId: input.runId,
2758
+ progressId: input.progressId,
2759
+ progressAt: input.receivedAt,
2760
+ ...(input.sequence === undefined ? {} : { sequence: String(input.sequence) })
2761
+ },
2762
+ receivedAt: input.receivedAt
2763
+ };
2764
+ }