@urateam/core 0.1.49 → 0.1.51

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 (122) hide show
  1. package/dist/__tests__/agentic-deep-review-provider.test.js +6 -1
  2. package/dist/__tests__/agentic-deep-review-provider.test.js.map +1 -1
  3. package/dist/__tests__/audit-session-events.test.d.ts +2 -0
  4. package/dist/__tests__/audit-session-events.test.d.ts.map +1 -0
  5. package/dist/__tests__/audit-session-events.test.js +39 -0
  6. package/dist/__tests__/audit-session-events.test.js.map +1 -0
  7. package/dist/__tests__/db-migration-agent-session-id.test.d.ts +2 -0
  8. package/dist/__tests__/db-migration-agent-session-id.test.d.ts.map +1 -0
  9. package/dist/__tests__/db-migration-agent-session-id.test.js +35 -0
  10. package/dist/__tests__/db-migration-agent-session-id.test.js.map +1 -0
  11. package/dist/__tests__/deep-review-resume.test.d.ts +18 -0
  12. package/dist/__tests__/deep-review-resume.test.d.ts.map +1 -0
  13. package/dist/__tests__/deep-review-resume.test.js +90 -0
  14. package/dist/__tests__/deep-review-resume.test.js.map +1 -0
  15. package/dist/__tests__/execute-stage-session-opts.test.d.ts +17 -0
  16. package/dist/__tests__/execute-stage-session-opts.test.d.ts.map +1 -0
  17. package/dist/__tests__/execute-stage-session-opts.test.js +179 -0
  18. package/dist/__tests__/execute-stage-session-opts.test.js.map +1 -0
  19. package/dist/__tests__/ralph-handoff-suppression.test.d.ts +2 -0
  20. package/dist/__tests__/ralph-handoff-suppression.test.d.ts.map +1 -0
  21. package/dist/__tests__/ralph-handoff-suppression.test.js +37 -0
  22. package/dist/__tests__/ralph-handoff-suppression.test.js.map +1 -0
  23. package/dist/__tests__/session-policy.test.d.ts +2 -0
  24. package/dist/__tests__/session-policy.test.d.ts.map +1 -0
  25. package/dist/__tests__/session-policy.test.js +44 -0
  26. package/dist/__tests__/session-policy.test.js.map +1 -0
  27. package/dist/__tests__/session-resume-fallback.test.d.ts +18 -0
  28. package/dist/__tests__/session-resume-fallback.test.d.ts.map +1 -0
  29. package/dist/__tests__/session-resume-fallback.test.js +137 -0
  30. package/dist/__tests__/session-resume-fallback.test.js.map +1 -0
  31. package/dist/__tests__/session-resume-flag.test.d.ts +2 -0
  32. package/dist/__tests__/session-resume-flag.test.d.ts.map +1 -0
  33. package/dist/__tests__/session-resume-flag.test.js +87 -0
  34. package/dist/__tests__/session-resume-flag.test.js.map +1 -0
  35. package/dist/__tests__/session-store.test.d.ts +2 -0
  36. package/dist/__tests__/session-store.test.d.ts.map +1 -0
  37. package/dist/__tests__/session-store.test.js +44 -0
  38. package/dist/__tests__/session-store.test.js.map +1 -0
  39. package/dist/__tests__/session-volume-check.test.d.ts +2 -0
  40. package/dist/__tests__/session-volume-check.test.d.ts.map +1 -0
  41. package/dist/__tests__/session-volume-check.test.js +28 -0
  42. package/dist/__tests__/session-volume-check.test.js.map +1 -0
  43. package/dist/__tests__/validate-run-mode.test.d.ts +2 -0
  44. package/dist/__tests__/validate-run-mode.test.d.ts.map +1 -0
  45. package/dist/__tests__/validate-run-mode.test.js +102 -0
  46. package/dist/__tests__/validate-run-mode.test.js.map +1 -0
  47. package/dist/__tests__/zombie-age-default.test.d.ts +2 -0
  48. package/dist/__tests__/zombie-age-default.test.d.ts.map +1 -0
  49. package/dist/__tests__/zombie-age-default.test.js +24 -0
  50. package/dist/__tests__/zombie-age-default.test.js.map +1 -0
  51. package/dist/audit/events.d.ts +45 -0
  52. package/dist/audit/events.d.ts.map +1 -1
  53. package/dist/audit/events.js +79 -0
  54. package/dist/audit/events.js.map +1 -1
  55. package/dist/db/client.d.ts.map +1 -1
  56. package/dist/db/client.js +4 -1
  57. package/dist/db/client.js.map +1 -1
  58. package/dist/db/schema.d.ts +19 -0
  59. package/dist/db/schema.d.ts.map +1 -1
  60. package/dist/db/schema.js +2 -0
  61. package/dist/db/schema.js.map +1 -1
  62. package/dist/executor/deep-review.d.ts +28 -1
  63. package/dist/executor/deep-review.d.ts.map +1 -1
  64. package/dist/executor/deep-review.js +113 -9
  65. package/dist/executor/deep-review.js.map +1 -1
  66. package/dist/executor/executor.d.ts +33 -0
  67. package/dist/executor/executor.d.ts.map +1 -1
  68. package/dist/executor/executor.js +94 -4
  69. package/dist/executor/executor.js.map +1 -1
  70. package/dist/executor/index.d.ts +7 -0
  71. package/dist/executor/index.d.ts.map +1 -1
  72. package/dist/executor/index.js +7 -0
  73. package/dist/executor/index.js.map +1 -1
  74. package/dist/executor/prompt/assembler.d.ts +7 -1
  75. package/dist/executor/prompt/assembler.d.ts.map +1 -1
  76. package/dist/executor/prompt/assembler.js +9 -3
  77. package/dist/executor/prompt/assembler.js.map +1 -1
  78. package/dist/executor/prompt/templates.d.ts +25 -6
  79. package/dist/executor/prompt/templates.d.ts.map +1 -1
  80. package/dist/executor/prompt/templates.js +21 -12
  81. package/dist/executor/prompt/templates.js.map +1 -1
  82. package/dist/executor/review/agentic-deep-review.d.ts.map +1 -1
  83. package/dist/executor/review/agentic-deep-review.js +15 -1
  84. package/dist/executor/review/agentic-deep-review.js.map +1 -1
  85. package/dist/executor/review/review-provider.d.ts +21 -0
  86. package/dist/executor/review/review-provider.d.ts.map +1 -1
  87. package/dist/executor/review/review-provider.js.map +1 -1
  88. package/dist/executor/session-policy.d.ts +38 -0
  89. package/dist/executor/session-policy.d.ts.map +1 -0
  90. package/dist/executor/session-policy.js +66 -0
  91. package/dist/executor/session-policy.js.map +1 -0
  92. package/dist/executor/session-store.d.ts +32 -0
  93. package/dist/executor/session-store.d.ts.map +1 -0
  94. package/dist/executor/session-store.js +38 -0
  95. package/dist/executor/session-store.js.map +1 -0
  96. package/dist/executor/validate.d.ts +20 -1
  97. package/dist/executor/validate.d.ts.map +1 -1
  98. package/dist/executor/validate.js +16 -1
  99. package/dist/executor/validate.js.map +1 -1
  100. package/dist/index.d.ts +1 -0
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +1 -0
  103. package/dist/index.js.map +1 -1
  104. package/dist/pipeline/runner.d.ts.map +1 -1
  105. package/dist/pipeline/runner.js +139 -6
  106. package/dist/pipeline/runner.js.map +1 -1
  107. package/dist/pipeline/session-volume-check.d.ts +38 -0
  108. package/dist/pipeline/session-volume-check.d.ts.map +1 -0
  109. package/dist/pipeline/session-volume-check.js +45 -0
  110. package/dist/pipeline/session-volume-check.js.map +1 -0
  111. package/dist/pm/scheduler.d.ts +14 -0
  112. package/dist/pm/scheduler.d.ts.map +1 -1
  113. package/dist/pm/scheduler.js +9 -5
  114. package/dist/pm/scheduler.js.map +1 -1
  115. package/dist/server.d.ts.map +1 -1
  116. package/dist/server.js +16 -0
  117. package/dist/server.js.map +1 -1
  118. package/dist/types.d.ts +8 -0
  119. package/dist/types.d.ts.map +1 -1
  120. package/dist/types.js +19 -0
  121. package/dist/types.js.map +1 -1
  122. package/package.json +1 -1
@@ -33,11 +33,13 @@ import { withBranchLock, createBranchLockAdapter, } from "./distributed-lock.js"
33
33
  import { upsertActiveWork, removeActiveWork, checkFileOverlap, getModifiedFiles, } from "../pm/coordination.js";
34
34
  import { eq, and, or, sql, gte, lt, inArray } from "drizzle-orm";
35
35
  import { nanoid } from "nanoid";
36
+ import { randomUUID } from "node:crypto";
37
+ import { isAgentSessionResumeEnabled, isAlwaysFreshStage } from "../executor/session-policy.js";
36
38
  import { createLogger, runWithLogContext } from "../logger.js";
37
39
  import { isTransientError, MAX_TRANSIENT_RETRIES } from "./error-classifier.js";
38
40
  import { evaluatePolicyGates } from "../policy/evaluate.js";
39
41
  import { buildReviewerRequest, verifyApprovalsReceived } from "../policy/index.js";
40
- import { logAuditEvent, policyReviewersRequestedEvent, reviewFanoutFallbackUsedEvent, pipelineScratchFilesBlockedEvent, pipelineTypecheckFailedEvent, pipelineSpecVsImplFailedEvent, pipelineAutoDeepReviewBumpedEvent, pmTriageQualityScoreEvent, } from "../audit/index.js";
42
+ import { logAuditEvent, policyReviewersRequestedEvent, reviewFanoutFallbackUsedEvent, pipelineScratchFilesBlockedEvent, pipelineTypecheckFailedEvent, pipelineSpecVsImplFailedEvent, pipelineAutoDeepReviewBumpedEvent, pmTriageQualityScoreEvent, agentSessionCreatedEvent, } from "../audit/index.js";
41
43
  import { computeAffectedFilesPredictionQuality, } from "../pm/triage-prediction-quality.js";
42
44
  import { getTriageResult } from "../pm/triage-results-store.js";
43
45
  import { matchesAnyPattern } from "../util/glob.js";
@@ -113,6 +115,11 @@ export class PipelineRunner {
113
115
  return;
114
116
  }
115
117
  const runId = nanoid();
118
+ // BEC-227: mint a per-run agent session UUID when the flag is on. The
119
+ // first resumable stage opens its SDK session with this id; downstream
120
+ // stages reuse it via `resume:`. Read env at call time so flipping the
121
+ // var takes effect on the next pipeline run without a daemon restart.
122
+ const agentSessionId = isAgentSessionResumeEnabled() ? randomUUID() : null;
116
123
  const branch = branchName(issue.identifier, sanitizedIssue.slug);
117
124
  const db = this.db;
118
125
  const runLog = createLogger({ component: "PipelineRunner", runId, issueId: issue.identifier });
@@ -128,8 +135,16 @@ export class PipelineRunner {
128
135
  branch,
129
136
  status: "queued",
130
137
  linearTeamId,
138
+ agentSessionId, // null when flag is off; UUID when BEC-227 is enabled
131
139
  });
132
140
  runLog.info({ branch }, "run queued");
141
+ if (agentSessionId !== null) {
142
+ void logAuditEvent(db, agentSessionCreatedEvent({
143
+ runId,
144
+ issueId: issue.identifier,
145
+ sessionId: agentSessionId,
146
+ }));
147
+ }
133
148
  const run = this.buildPipelineRun(runId, issue, pipelineKey, repoConfig, branch);
134
149
  // Set activeRuns BEFORE enqueue so abort() can cancel while queued
135
150
  this.activeRuns.set(issue.identifier, runId);
@@ -139,7 +154,7 @@ export class PipelineRunner {
139
154
  return;
140
155
  runLog.info("executing pipeline");
141
156
  try {
142
- await runWithLogContext({ runId, issueId: issue.identifier }, () => this.executePipeline(runId, run, pipelineConfig, repoConfig, sanitizedIssue, branch));
157
+ await runWithLogContext({ runId, issueId: issue.identifier }, () => this.executePipeline(runId, run, pipelineConfig, repoConfig, sanitizedIssue, branch, undefined, agentSessionId));
143
158
  }
144
159
  catch (err) {
145
160
  runLog.error({ err }, "pipeline execution failed");
@@ -300,6 +315,10 @@ export class PipelineRunner {
300
315
  startStageIndex: pausedRun.currentStageIndex,
301
316
  worktreePath,
302
317
  initialHandoff: handoff ?? undefined,
318
+ // BEC-227 — carry the per-run SDK session id across the
319
+ // await-approval pause so the post-resume stages keep
320
+ // talking to the same transcript.
321
+ agentSessionId: pausedRun.agentSessionId ?? null,
303
322
  }));
304
323
  }
305
324
  catch (err) {
@@ -468,9 +487,45 @@ export class PipelineRunner {
468
487
  log.warn({ err: e }, "failed to inject CLAUDE.md — agent will run without it");
469
488
  }
470
489
  }
471
- async executePipeline(runId, run, config, repoConfig, sanitizedIssue, branch, resumeOptions) {
490
+ async executePipeline(runId, run, config, repoConfig, sanitizedIssue, branch, resumeOptions,
491
+ /**
492
+ * BEC-227 — per-run SDK session UUID minted at start(). Null when the
493
+ * `URATEAM_ENABLE_AGENT_SESSION_RESUME` flag is off. On the resume path
494
+ * this is ignored in favour of `resumeOptions.agentSessionId`.
495
+ */
496
+ agentSessionId = null) {
472
497
  const db = this.db;
473
498
  const runLog = createLogger({ component: "PipelineRunner", runId, issueId: run.issueId });
499
+ // BEC-227 — resolve the session id from the resume path (preferred) or
500
+ // the start() path. Tracks whether the first resumable stage in this run
501
+ // has already opened the SDK session so subsequent stages switch from
502
+ // `sessionId` (create) to `resume` (reuse). The flag stays scoped to a
503
+ // single executePipeline() invocation: on resume after await-approval the
504
+ // SDK session created in the pre-pause run has already been initiated, so
505
+ // we start this second invocation with `hasInitiatedSession = true` —
506
+ // every resumable stage uses the `resume:` shape, never re-creates the
507
+ // session.
508
+ const runAgentSessionId = resumeOptions
509
+ ? resumeOptions.agentSessionId
510
+ : agentSessionId;
511
+ let hasInitiatedSession = !!resumeOptions;
512
+ /**
513
+ * Returns `true` for the first non-fresh stage in this run, flips the
514
+ * `hasInitiatedSession` flag as a side effect. Always-fresh stages
515
+ * (validate, ralph-check) never count as the first resumable stage —
516
+ * they don't take a session id. Returns false when the flag is off
517
+ * (`runAgentSessionId === null`).
518
+ */
519
+ const claimFirstResumableStage = (stage) => {
520
+ if (runAgentSessionId === null)
521
+ return false;
522
+ if (hasInitiatedSession)
523
+ return false;
524
+ if (isAlwaysFreshStage(stage))
525
+ return false;
526
+ hasInitiatedSession = true;
527
+ return true;
528
+ };
474
529
  let handoff;
475
530
  let worktreePath;
476
531
  let devcontainerSession;
@@ -655,6 +710,7 @@ export class PipelineRunner {
655
710
  }, "file overlap detected with other active runs — proceeding with awareness");
656
711
  }
657
712
  }
713
+ const isFirstResumableStageForMain = claimFirstResumableStage(stageType);
658
714
  let result = await executeStage({
659
715
  runId,
660
716
  issueId: sanitizedIssue.id,
@@ -667,6 +723,8 @@ export class PipelineRunner {
667
723
  techStack,
668
724
  devcontainerSession,
669
725
  stageModels: config.stageModels,
726
+ agentSessionId: runAgentSessionId,
727
+ isFirstResumableStage: isFirstResumableStageForMain,
670
728
  });
671
729
  // Operator stop check (cancel path) — the AbortController inside the
672
730
  // executor surfaces as result.status === "cancelled". Don't try to use
@@ -735,6 +793,17 @@ export class PipelineRunner {
735
793
  }
736
794
  runLog.info({ iteration, gaps: check.gaps.length, suggestions: check.suggestions.length }, "RALPH: gaps found, re-running implement");
737
795
  const ralphContext = buildRalphContext(iteration, check, handoffResult.artifact);
796
+ // BEC-227 — RALPH re-implement runs inside the implement stage's
797
+ // main invocation, which already claimed the first-resumable slot
798
+ // if eligible. Re-claim is a no-op (returns false) so this call
799
+ // takes the `resume` shape when the flag is on.
800
+ const isFirstResumableStageForRalph = claimFirstResumableStage(stageType);
801
+ // BEC-227 — when this RALPH iteration is a resumed call (session
802
+ // active AND not the first resumable stage), the prior handoff is
803
+ // already in the agent's resumed SDK transcript. Suppress the
804
+ // `<previous-stage-context>` block to avoid duplicating that
805
+ // context as prompt input tokens.
806
+ const suppressRalphHandoff = runAgentSessionId !== null && !isFirstResumableStageForRalph;
738
807
  result = await executeStage({
739
808
  runId,
740
809
  issueId: sanitizedIssue.id,
@@ -748,6 +817,9 @@ export class PipelineRunner {
748
817
  devcontainerSession,
749
818
  ralphContext,
750
819
  stageModels: config.stageModels,
820
+ agentSessionId: runAgentSessionId,
821
+ isFirstResumableStage: isFirstResumableStageForRalph,
822
+ suppressHandoff: suppressRalphHandoff,
751
823
  });
752
824
  // Accumulate each RALPH iteration's tokens
753
825
  run.totalInputTokens += result.inputTokens;
@@ -778,6 +850,9 @@ export class PipelineRunner {
778
850
  }, "stage failed — restarting (urateam#121)");
779
851
  run.stageRetries ??= {};
780
852
  run.stageRetries[stageType] = (run.stageRetries[stageType] ?? 0) + 1;
853
+ // BEC-227 — retry of an already-attempted stage. If the main
854
+ // invocation above claimed the session, this is a no-op.
855
+ const isFirstResumableStageForRetry = claimFirstResumableStage(stageType);
781
856
  result = await executeStage({
782
857
  runId,
783
858
  issueId: sanitizedIssue.id,
@@ -790,6 +865,8 @@ export class PipelineRunner {
790
865
  techStack,
791
866
  devcontainerSession,
792
867
  stageModels: config.stageModels,
868
+ agentSessionId: runAgentSessionId,
869
+ isFirstResumableStage: isFirstResumableStageForRetry,
793
870
  });
794
871
  if (result.status === "completed")
795
872
  break;
@@ -844,10 +921,19 @@ export class PipelineRunner {
844
921
  !isLastStage) {
845
922
  runLog.info({ stage }, "validating handoff");
846
923
  let validationPassed = false;
924
+ // BEC-227 — runMode tells the validator whether this stage is the
925
+ // first resumable stage of the run (paranoia check still runs),
926
+ // a subsequent resumable stage (skip — agent inherits context),
927
+ // or a non-session run (validate as before).
928
+ const mainStageRunMode = runAgentSessionId === null
929
+ ? "fallback"
930
+ : isFirstResumableStageForMain
931
+ ? "first-resumed"
932
+ : "resumed";
847
933
  const validation = await validateHandoff(stage, {
848
934
  artifact: result.handoffArtifact,
849
935
  structured: result.handoffIsStructured ?? false,
850
- }, sanitizedIssue, repoConfig, worktreePath);
936
+ }, sanitizedIssue, repoConfig, worktreePath, mainStageRunMode);
851
937
  validationPassed = validation.valid;
852
938
  let lastValidationIssues = validation.issues;
853
939
  if (!validationPassed) {
@@ -855,6 +941,9 @@ export class PipelineRunner {
855
941
  // Retry with the last known-good handoff (not the failed artifact)
856
942
  if (config.retry.strategy === "fix-and-retry") {
857
943
  for (let attempt = 0; attempt < config.retry.maxAttempts; attempt++) {
944
+ // BEC-227 — validation-failed retry. Same stage as the main
945
+ // invocation; claim is a no-op when the session is open.
946
+ const isFirstResumableStageForValRetry = claimFirstResumableStage(stageType);
858
947
  result = await executeStage({
859
948
  runId,
860
949
  issueId: sanitizedIssue.id,
@@ -867,12 +956,22 @@ export class PipelineRunner {
867
956
  techStack,
868
957
  devcontainerSession,
869
958
  stageModels: config.stageModels,
959
+ agentSessionId: runAgentSessionId,
960
+ isFirstResumableStage: isFirstResumableStageForValRetry,
870
961
  });
871
962
  if (result.status === "completed" && result.handoffArtifact) {
963
+ // BEC-227 — `isFirstResumableStageForValRetry` reflects the
964
+ // retry execution that just produced `result`. Same formula
965
+ // as the main-loop validation.
966
+ const valRetryRunMode = runAgentSessionId === null
967
+ ? "fallback"
968
+ : isFirstResumableStageForValRetry
969
+ ? "first-resumed"
970
+ : "resumed";
872
971
  const retryValidation = await validateHandoff(stage, {
873
972
  artifact: result.handoffArtifact,
874
973
  structured: result.handoffIsStructured ?? false,
875
- }, sanitizedIssue, repoConfig, worktreePath);
974
+ }, sanitizedIssue, repoConfig, worktreePath, valRetryRunMode);
876
975
  if (retryValidation.valid) {
877
976
  validationPassed = true;
878
977
  break;
@@ -971,6 +1070,7 @@ export class PipelineRunner {
971
1070
  runLog.info({ rfIteration, maxIterations: reviewFixIterations, blockingFindings: blockingCount }, "review-fix loop: re-running stages to address blocking findings");
972
1071
  for (const fixStage of fixStages) {
973
1072
  runLog.info({ stage: fixStage, rfIteration }, "review-fix: executing stage");
1073
+ const isFirstResumableStageForFix = claimFirstResumableStage(fixStage);
974
1074
  const fixResult = await executeStage({
975
1075
  runId,
976
1076
  issueId: sanitizedIssue.id,
@@ -983,6 +1083,8 @@ export class PipelineRunner {
983
1083
  techStack,
984
1084
  devcontainerSession,
985
1085
  stageModels: config.stageModels,
1086
+ agentSessionId: runAgentSessionId,
1087
+ isFirstResumableStage: isFirstResumableStageForFix,
986
1088
  });
987
1089
  // BEC-134: track latest review stage_run id for fanout persistence.
988
1090
  if (fixStage === "review") {
@@ -1007,7 +1109,16 @@ export class PipelineRunner {
1007
1109
  }
1008
1110
  // Validate handoff (same as main stage loop)
1009
1111
  if (fixResult.handoffArtifact && config.validateHandoffs === true) {
1010
- const validation = await validateHandoff(fixStage, { artifact: fixResult.handoffArtifact, structured: fixResult.handoffIsStructured ?? false }, sanitizedIssue, repoConfig, worktreePath);
1112
+ // BEC-227 runMode derived from the review-fix executeStage
1113
+ // claim. The review-fix loop runs after the main stage loop,
1114
+ // so `isFirstResumableStageForFix` is virtually always false
1115
+ // (session already initiated). Formula is the same.
1116
+ const fixStageRunMode = runAgentSessionId === null
1117
+ ? "fallback"
1118
+ : isFirstResumableStageForFix
1119
+ ? "first-resumed"
1120
+ : "resumed";
1121
+ const validation = await validateHandoff(fixStage, { artifact: fixResult.handoffArtifact, structured: fixResult.handoffIsStructured ?? false }, sanitizedIssue, repoConfig, worktreePath, fixStageRunMode);
1011
1122
  if (!validation.valid) {
1012
1123
  runLog.warn({ stage: fixStage, rfIteration, issues: validation.issues }, "review-fix: handoff validation failed");
1013
1124
  }
@@ -1144,13 +1255,26 @@ export class PipelineRunner {
1144
1255
  // review stage_run row (from the main stage loop or review-fix loop).
1145
1256
  // PR doesn't exist yet here, so prNumber stays null; the runner posts
1146
1257
  // fanout PR comments after PR creation using `pendingFanoutRuns`.
1258
+ //
1259
+ // BEC-227 Task 11 — thread agent-session info through so the
1260
+ // agentic deep-review provider can resume the per-run SDK session
1261
+ // in its 3 parallel sub-agents. `claimFirstResumableStage` flips
1262
+ // the runner-level latch exactly once across the whole run; deep
1263
+ // review may be the first resumable consumer (e.g. when the main
1264
+ // review stage was skipped or used a fresh model).
1265
+ const isFirstResumableStageForDeepReview = claimFirstResumableStage("review");
1147
1266
  const reviewCtx = {
1148
1267
  runId,
1268
+ issueId: sanitizedIssue.id,
1149
1269
  stageRunId: lastReviewStageRunId,
1150
1270
  workdir: worktreePath,
1151
1271
  handoff,
1152
1272
  baseRef: repoConfig.defaultBranch ?? "main",
1153
1273
  prNumber: null,
1274
+ agentSessionId: runAgentSessionId,
1275
+ isFirstResumableStage: isFirstResumableStageForDeepReview,
1276
+ reviewModel: config.stageModels?.["review"],
1277
+ db: this.db,
1154
1278
  };
1155
1279
  const reviewResult = await runReviewProviders(reviewCtx, {
1156
1280
  env: drPass === 1 ? process.env : {},
@@ -1203,6 +1327,7 @@ export class PipelineRunner {
1203
1327
  });
1204
1328
  const deepReviewContext = buildDeepReviewContext(drPass, deepFindingsForContext, handoff);
1205
1329
  runLog.info({ drPass }, "deep review: re-running implement stage");
1330
+ const isFirstResumableStageForDrImpl = claimFirstResumableStage("implement");
1206
1331
  const drImplementResult = await executeStage({
1207
1332
  runId,
1208
1333
  issueId: sanitizedIssue.id,
@@ -1216,6 +1341,8 @@ export class PipelineRunner {
1216
1341
  devcontainerSession,
1217
1342
  ralphContext: deepReviewContext,
1218
1343
  stageModels: config.stageModels,
1344
+ agentSessionId: runAgentSessionId,
1345
+ isFirstResumableStage: isFirstResumableStageForDrImpl,
1219
1346
  });
1220
1347
  run.totalInputTokens += drImplementResult.inputTokens;
1221
1348
  run.totalOutputTokens += drImplementResult.outputTokens;
@@ -1237,6 +1364,7 @@ export class PipelineRunner {
1237
1364
  handoff = drImplementResult.handoffArtifact;
1238
1365
  // Re-run review stage to verify fixes
1239
1366
  runLog.info({ drPass }, "deep review: re-running review stage");
1367
+ const isFirstResumableStageForDrReview = claimFirstResumableStage("review");
1240
1368
  const drReviewResult = await executeStage({
1241
1369
  runId,
1242
1370
  issueId: sanitizedIssue.id,
@@ -1249,6 +1377,8 @@ export class PipelineRunner {
1249
1377
  techStack,
1250
1378
  devcontainerSession,
1251
1379
  stageModels: config.stageModels,
1380
+ agentSessionId: runAgentSessionId,
1381
+ isFirstResumableStage: isFirstResumableStageForDrReview,
1252
1382
  });
1253
1383
  // BEC-134: refresh latest review stage_run id for any subsequent
1254
1384
  // fanout persistence inside this loop.
@@ -1586,6 +1716,7 @@ export class PipelineRunner {
1586
1716
  }
1587
1717
  else {
1588
1718
  runLog.warn("push queue: rebase conflicts detected, running implement pass to resolve");
1719
+ const isFirstResumableStageForResolve = claimFirstResumableStage("implement");
1589
1720
  const resolveResult = await executeStage({
1590
1721
  runId,
1591
1722
  issueId: sanitizedIssue.id,
@@ -1599,6 +1730,8 @@ export class PipelineRunner {
1599
1730
  devcontainerSession,
1600
1731
  mergeConflictContext: { defaultBranch: repoConfig.defaultBranch },
1601
1732
  stageModels: config.stageModels,
1733
+ agentSessionId: runAgentSessionId,
1734
+ isFirstResumableStage: isFirstResumableStageForResolve,
1602
1735
  });
1603
1736
  run.totalInputTokens += resolveResult.inputTokens;
1604
1737
  run.totalOutputTokens += resolveResult.outputTokens;