ai-foreman 1.7.8 → 1.7.9

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.
package/dist/cli/start.js CHANGED
@@ -37,7 +37,7 @@ import { eligibleTickets, evaluateTicketEligibility } from "../tickets/eligibili
37
37
  import { BUILTIN_BRANCH_PREFIX, validateBranchPrefix } from "../branch/prefix.js";
38
38
  import { captureCurrentWorkflowSessionIdentity, CurrentWorkflowChangedError, CurrentWorkflowGuardAdapter } from "../branch/currentGuard.js";
39
39
  import { ContinuityAdapter, ContinuityRecoveryRequiredError, SessionUnavailableContinuityError } from "../continuity.js";
40
- import { ContextCapabilityError, RoleSessionController } from "../sessionLifecycle.js";
40
+ import { ContextCapabilityError, RoleSessionController, ThresholdCompactionController } from "../sessionLifecycle.js";
41
41
  import { HandoffLoopError, HandoffService, parseBuilderHandoffRequest } from "../handoffs.js";
42
42
  import { captureWorkspaceIdentity, createProviderSessionRef, resolveUniqueSessionBinding } from "../sessionIdentity.js";
43
43
  import { SessionUnavailableError } from "../adapters/sessionFailure.js";
@@ -625,13 +625,7 @@ export function buildStartCommand() {
625
625
  fast: builderFast,
626
626
  systemPromptAppend: roleBundle.system || undefined,
627
627
  skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
628
- contextManagementPolicy: {
629
- role: "builder",
630
- configuredThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
631
- compactMaximum: roleDefaults.builder?.compact_maximum ?? 10,
632
- settingsRevision,
633
- model: model ?? "default",
634
- },
628
+ autoCompactThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
635
629
  };
636
630
  const adapter = agent === "codex"
637
631
  ? new CodexAdapter(adapterOpts)
@@ -643,12 +637,12 @@ export function buildStartCommand() {
643
637
  throw new SessionUnavailableError({ runtime: "codex", phase: "preflight", dispatchState: "not-sent", executable: agentExecutable ?? "codex", cwd: builderCwd, diagnostics: availability.detail ?? `Codex session ${sessionRef.sessionId} is ${availability.status}`, availability });
644
638
  }
645
639
  }
640
+ await prepareNativeAutoCompaction(adapter);
646
641
  return !branchMode ? new CurrentWorkflowGuardAdapter(adapter, builderCwd) : adapter;
647
642
  };
648
643
  let activeContinuityRunId;
649
644
  let liveStatusReporter;
650
645
  let activeBuilderForStatus;
651
- let activeBuilderContextSnapshot;
652
646
  const resolvedContinuitySettings = (role) => role === "builder" ? {
653
647
  role, source: roleDefaults.builder ? "project" : "provider", make: agent, model: model ?? "default",
654
648
  reasoning: builderEffort ?? "default", fast: Boolean(builderFast), session_strategy: roleDefaults.builder?.session_strategy ?? "compact",
@@ -662,67 +656,25 @@ export function buildStartCommand() {
662
656
  auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
663
657
  compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
664
658
  };
665
- const liveRoleFields = [
666
- "make", "model", "reasoning", "fast", "session_strategy", "display_session_cost",
667
- "auto_compact_threshold_percent", "compact_maximum",
668
- ];
669
- const observedRoleDefaults = {
670
- builder: { ...(roleDefaults.builder ?? {}) },
671
- qa: { ...(roleDefaults.qa ?? {}) },
672
- };
673
- const observedProjectRevision = { builder: settingsRevision, qa: settingsRevision };
674
- const effectiveRoleRevision = { builder: settingsRevision, qa: settingsRevision };
675
- const liveRoleOverrides = {
676
- builder: new Map(),
677
- qa: new Map(),
678
- };
679
659
  const liveRoleSettings = (role, base) => {
680
660
  const live = readAgentDefaults(cwd);
681
661
  const revision = live.revision ?? 0;
682
- const defaults = role === "builder" ? live.builder : live.qa;
683
- if (revision > observedProjectRevision[role]) {
684
- const prior = observedRoleDefaults[role];
685
- let roleChanged = false;
686
- for (const field of liveRoleFields) {
687
- const value = defaults?.[field];
688
- if (!Object.is(value, prior[field])) {
689
- liveRoleOverrides[role].set(field, value);
690
- roleChanged = true;
691
- }
692
- }
693
- const revisionDb = new WorkflowDb(cwd);
694
- let targetedFields;
695
- try {
696
- targetedFields = revisionDb.projectSettingsRevisionTargets(revision)[role] ?? [];
697
- }
698
- finally {
699
- revisionDb.close();
700
- }
701
- for (const field of targetedFields) {
702
- if (liveRoleFields.includes(field)) {
703
- const typedField = field;
704
- liveRoleOverrides[role].set(typedField, defaults?.[typedField]);
705
- }
706
- }
707
- observedRoleDefaults[role] = { ...(defaults ?? {}) };
708
- observedProjectRevision[role] = revision;
709
- if (roleChanged || targetedFields.length)
710
- effectiveRoleRevision[role] = revision;
711
- }
712
- if (effectiveRoleRevision[role] <= base.settings_revision)
662
+ if (revision <= base.settings_revision)
713
663
  return base;
714
- const merged = {
664
+ const defaults = role === "builder" ? live.builder : live.qa;
665
+ return {
715
666
  ...base,
716
667
  source: defaults ? "project" : base.source,
717
- settings_revision: effectiveRoleRevision[role],
668
+ make: defaults?.make ?? base.make,
669
+ model: defaults?.model ?? base.model,
670
+ reasoning: defaults?.reasoning ?? base.reasoning,
671
+ fast: defaults?.fast ?? base.fast,
672
+ session_strategy: defaults?.session_strategy ?? base.session_strategy,
673
+ display_session_cost: defaults?.display_session_cost ?? base.display_session_cost,
674
+ auto_compact_threshold_percent: defaults?.auto_compact_threshold_percent ?? base.auto_compact_threshold_percent,
675
+ compact_maximum: defaults?.compact_maximum ?? base.compact_maximum,
676
+ settings_revision: revision,
718
677
  };
719
- const mutable = merged;
720
- for (const [field, value] of liveRoleOverrides[role]) {
721
- if (value === undefined)
722
- continue;
723
- mutable[field] = value;
724
- }
725
- return merged;
726
678
  };
727
679
  const createBuilderForSettings = async (builderCwd, settings) => {
728
680
  const ready = await ensureRuntimeReadyForCommand(builderCwd, settings.make, {
@@ -747,12 +699,10 @@ export function buildStartCommand() {
747
699
  fast: settings.fast,
748
700
  systemPromptAppend: roleBundle.system || undefined,
749
701
  skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
750
- contextManagementPolicy: {
751
- role: "builder", configuredThresholdPercent: settings.auto_compact_threshold_percent,
752
- compactMaximum: settings.compact_maximum, settingsRevision: settings.settings_revision, model: settings.model,
753
- },
702
+ autoCompactThresholdPercent: settings.auto_compact_threshold_percent,
754
703
  };
755
704
  const created = settings.make === "codex" ? new CodexAdapter(adapterOptions) : await ClaudeAdapter.create(adapterOptions);
705
+ await prepareNativeAutoCompaction(created);
756
706
  return !branchMode ? new CurrentWorkflowGuardAdapter(created, builderCwd) : created;
757
707
  };
758
708
  const createQaForSettings = async (qaCwd, settings) => {
@@ -777,12 +727,11 @@ export function buildStartCommand() {
777
727
  fast: settings.fast,
778
728
  systemPromptAppend: `${roleBundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`,
779
729
  skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
780
- contextManagementPolicy: {
781
- role: "qa", configuredThresholdPercent: settings.auto_compact_threshold_percent,
782
- compactMaximum: settings.compact_maximum, settingsRevision: settings.settings_revision, model: settings.model,
783
- },
730
+ autoCompactThresholdPercent: settings.auto_compact_threshold_percent,
784
731
  };
785
- return settings.make === "codex" ? new CodexAdapter(adapterOptions) : await ClaudeAdapter.create(adapterOptions);
732
+ const created = settings.make === "codex" ? new CodexAdapter(adapterOptions) : await ClaudeAdapter.create(adapterOptions);
733
+ await prepareNativeAutoCompaction(created);
734
+ return created;
786
735
  };
787
736
  const createRecoveringBuilder = async (builderCwd, sessionId, sessionRef) => {
788
737
  if (sessionId && !sessionRef) {
@@ -969,6 +918,16 @@ export function buildStartCommand() {
969
918
  return continuousBuilder(transfer.successor, builderCwd, runId, input.next);
970
919
  };
971
920
  const createRawQa = async (qaCwd, sessionRef) => {
921
+ // Isolated QA conversations are intentionally disposable. Resolve live
922
+ // QA settings for each one so a threshold-only update applies to the
923
+ // very next review instead of waiting for a nonexistent session reuse.
924
+ const qaSettings = liveRoleSettings("qa", resolvedContinuitySettings("qa"));
925
+ const ready = await ensureRuntimeReadyForCommand(qaCwd, qaSettings.make, {
926
+ label: "live QA settings",
927
+ yes: true,
928
+ allowSwitch: false,
929
+ model: qaSettings.model === "default" ? undefined : qaSettings.model,
930
+ });
972
931
  const qaPolicy = new PermissionPolicy(config.permissions, qaCwd);
973
932
  const roleBundle = loadRoleBundle("qa", { projectDir: qaCwd });
974
933
  const adapterOpts = {
@@ -976,28 +935,24 @@ export function buildStartCommand() {
976
935
  configRoot: cwd,
977
936
  sessionRole: "qa",
978
937
  sessionStream: "qa",
979
- runtimeExecutable: qaExecutable,
938
+ runtimeExecutable: ready.executable,
980
939
  runtimePhase: "qa",
981
- model: qaModel,
940
+ model: qaSettings.model === "default" ? undefined : qaSettings.model,
982
941
  ...(sessionRef ? { resumeSessionRef: sessionRef } : {}),
983
942
  sessionGeneration: sessionRef?.generation ?? 0,
984
943
  workspaceIdentity: captureWorkspaceIdentity(qaCwd),
985
944
  permission: createPermissionHandler(qaPolicy, log),
986
- effort: qaEffort,
987
- fast: qaFast,
945
+ effort: explicitEffort(qaSettings.reasoning),
946
+ fast: qaSettings.fast,
988
947
  systemPromptAppend: `${roleBundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`,
989
948
  skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
990
- contextManagementPolicy: {
991
- role: "qa",
992
- configuredThresholdPercent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
993
- compactMaximum: roleDefaults.qa?.compact_maximum ?? 10,
994
- settingsRevision,
995
- model: qaModel ?? "default",
996
- },
949
+ autoCompactThresholdPercent: qaSettings.auto_compact_threshold_percent,
997
950
  };
998
- return qaAgent === "codex"
951
+ const created = qaSettings.make === "codex"
999
952
  ? new CodexAdapter(adapterOpts)
1000
953
  : await ClaudeAdapter.create(adapterOpts);
954
+ await prepareNativeAutoCompaction(created);
955
+ return created;
1001
956
  };
1002
957
  const createRecoveringQa = async (qaCwd, sessionRef) => {
1003
958
  const initial = await createRawQa(qaCwd, sessionRef);
@@ -1114,7 +1069,6 @@ export function buildStartCommand() {
1114
1069
  finally {
1115
1070
  db.close();
1116
1071
  } },
1117
- contextSnapshot: activeBuilderContextSnapshot,
1118
1072
  });
1119
1073
  });
1120
1074
  };
@@ -1218,9 +1172,8 @@ export function buildStartCommand() {
1218
1172
  projectDir: cwd,
1219
1173
  runId: activeContinuityRunId,
1220
1174
  role: "qa",
1221
- requireNativeContextManagement: true,
1222
1175
  initialSettings: qaSettings,
1223
- historicalCountUncertain: Boolean(recoveryRecord && (recoveryRecord.legacy || !recoveryRecord.runDecisions)),
1176
+ historicalCountUncertain: recoveryCompactionHistoryUncertain(cwd, recoveryRecord, activeContinuityRunId, "qa"),
1224
1177
  readSettings: () => liveRoleSettings("qa", qaSettings),
1225
1178
  settingsBoundary: (input) => applyQaSettingsBoundary(activeContinuityRunId, qaCwd, input),
1226
1179
  settingsAdopted: (settings, active) => liveStatusReporter?.updateState({
@@ -1231,7 +1184,6 @@ export function buildStartCommand() {
1231
1184
  report: (event) => {
1232
1185
  currentActivity()?.update(event.kind, event.detail, { provider: qaSettings.make, model: qaSettings.model });
1233
1186
  log.write("context-lifecycle", { role: "qa", ...event, sample: event.sample });
1234
- void liveStatusReporter?.tick();
1235
1187
  },
1236
1188
  handoff: async ({ reason, adapter: predecessor, sample, settings, compactionCount }) => {
1237
1189
  const nativeUsage = await predecessor.sessionUsage?.().catch(() => undefined);
@@ -1252,9 +1204,21 @@ export function buildStartCommand() {
1252
1204
  return decorateQa(transfer.successor, qaCwd, undefined, settings);
1253
1205
  },
1254
1206
  });
1255
- liveStatusReporter?.updateState({ contextSnapshot: () => controller.contextSnapshot() });
1256
1207
  return (await controller.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter;
1257
1208
  };
1209
+ const observeQaNativeCompactions = async (adapter) => {
1210
+ if (!activeContinuityRunId)
1211
+ return;
1212
+ const qaSettings = resolvedContinuitySettings("qa");
1213
+ const controller = new ThresholdCompactionController({
1214
+ projectDir: cwd,
1215
+ runId: activeContinuityRunId,
1216
+ role: "qa",
1217
+ initialSettings: qaSettings,
1218
+ historicalCountUncertain: recoveryCompactionHistoryUncertain(cwd, recoveryRecord, activeContinuityRunId, "qa"),
1219
+ });
1220
+ await controller.observeNativeCompactions(adapter);
1221
+ };
1258
1222
  const qaNonconvergence = createQaNonconvergenceHandler(cwd, Boolean(opts.yes), roleDefaults.planner);
1259
1223
  if (branchMode) {
1260
1224
  const ticketsConfig = loadTicketsConfig(cwd);
@@ -1356,68 +1320,12 @@ export function buildStartCommand() {
1356
1320
  });
1357
1321
  let auditBuilder;
1358
1322
  let auditViewer;
1359
- let auditCompleted = false;
1360
1323
  const auditRunId = `audit-${stamp}`;
1361
1324
  activeContinuityRunId = auditRunId;
1362
1325
  try {
1363
- const auditSettings = resolvedContinuitySettings("builder");
1364
- const auditInstruction = buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket));
1365
- const auditDb = new WorkflowDb(cwd);
1366
- try {
1367
- auditDb.ensureRun(auditRunId);
1368
- auditDb.appendContinuityEvent({
1369
- runId: auditRunId, role: "host", kind: "branch_audit_initialized",
1370
- payload: { frozenActionDigest: createHash("sha256").update(auditInstruction).digest("hex") },
1371
- authoritativeStateRevision: auditSettings.settings_revision,
1372
- });
1373
- auditDb.publishContinuityCheckpoint({
1374
- runId: auditRunId, role: "builder", authoritativeStateRevision: auditSettings.settings_revision,
1375
- delta: {
1376
- version: 1, decisions: [], constraints: [], discoveries: [], completedActions: [], evidence: [], failures: [], blockers: [],
1377
- openWork: ["complete the selected-ticket dependency audit"], nextAction: "complete the selected-ticket dependency audit",
1378
- },
1379
- });
1380
- }
1381
- finally {
1382
- auditDb.close();
1383
- }
1384
1326
  auditBuilder = await createBuilder(cwd);
1385
- const auditController = RoleSessionController.managed({
1386
- projectDir: cwd, runId: auditRunId, role: "builder", initialSettings: auditSettings,
1387
- requireNativeContextManagement: true,
1388
- readSettings: () => liveRoleSettings("builder", auditSettings),
1389
- settingsBoundary: (input) => applyBuilderSettingsBoundary(auditRunId, cwd, input),
1390
- report: (event) => log.write("context-lifecycle", { phase: "branch-audit", ...event, sample: event.sample }),
1391
- handoff: async ({ reason, adapter, sample, settings, compactionCount, frozenAction }) => {
1392
- const nativeUsage = await adapter.sessionUsage?.().catch(() => undefined);
1393
- const transfer = await new HandoffService(cwd).transfer({
1394
- runId: auditRunId, role: "builder", reason, predecessorSessionId: adapter.sessionId(), predecessorSessionRef: adapter.sessionRef?.(),
1395
- roleState: { phase: "branch-audit", frozenActionDigest: createHash("sha256").update(frozenAction).digest("hex"), contextSample: sample },
1396
- ...(nativeUsage ? { sessionUsage: {
1397
- version: 1, runId: auditRunId, role: "builder", provider: adapter.agent,
1398
- providerSessionId: adapter.sessionId(), observedAt: nativeUsage.observedAt, source: nativeUsage.source,
1399
- cumulativeInputTokens: nativeUsage.inputTokens, cumulativeOutputTokens: nativeUsage.outputTokens,
1400
- cumulativeTotalTokens: nativeUsage.totalTokens, authoritativeCostUsd: nativeUsage.authoritativeCostUsd,
1401
- } } : {}),
1402
- compactionCount, compactMaximum: settings.compact_maximum,
1403
- resources: [{ label: "frozen-branch-audit", content: frozenAction, authoritative: true }],
1404
- }, () => createBuilderForSettings(cwd, settings));
1405
- log.write("handoff-transfer", {
1406
- phase: "branch-audit", reason, generation: transfer.manifest.generation,
1407
- predecessorSessionId: transfer.manifest.predecessorSessionId, successorSessionId: transfer.successorSessionId,
1408
- acceptanceCheckpointDigest: transfer.acceptanceCheckpointDigest,
1409
- });
1410
- if (adapter instanceof ContinuityAdapter) {
1411
- await adapter.adoptValidatedSuccessor(transfer.successor);
1412
- return adapter;
1413
- }
1414
- await adapter.close().catch(() => { });
1415
- return continuousBuilder(transfer.successor, cwd, auditRunId, settings);
1416
- },
1417
- });
1418
- auditBuilder = (await auditController.atSafeBoundary(auditBuilder, auditInstruction)).adapter;
1419
1327
  auditViewer = printEvents(auditBuilder.events());
1420
- const audit = await auditBuilder.sendTurn(auditInstruction);
1328
+ const audit = await auditBuilder.sendTurn(buildBranchAuditInstruction(plan.nodes.map((node) => node.ticket)));
1421
1329
  if (audit.isError)
1422
1330
  throw new Error(audit.text);
1423
1331
  const auditDependencies = parseAuditDependencies(audit.text);
@@ -1431,7 +1339,6 @@ export function buildStartCommand() {
1431
1339
  ticketIds: selectedNewTicket ? [selectedNewTicket] : selectedStackTickets ?? deliveryRun?.remaining.slice(0, steps) ?? recoveryRecord?.tickets,
1432
1340
  });
1433
1341
  auditDependencyCount = auditDependencies.length;
1434
- auditCompleted = true;
1435
1342
  }
1436
1343
  finally {
1437
1344
  await auditBuilder?.close().catch(() => { });
@@ -1439,9 +1346,7 @@ export function buildStartCommand() {
1439
1346
  const auditDb = new WorkflowDb(cwd);
1440
1347
  try {
1441
1348
  if (auditDb.getRun(auditRunId))
1442
- auditDb.transition(auditRunId, auditCompleted
1443
- ? { status: "completed", checkpoint: "branch-audit-complete", remainingWork: {}, event: "audit_completed" }
1444
- : { status: "paused", checkpoint: "branch-audit-interrupted", remainingWork: { action: "complete the selected-ticket dependency audit" }, event: "audit_interrupted" });
1349
+ auditDb.transition(auditRunId, { status: "completed", checkpoint: "branch-audit-complete", remainingWork: {}, event: "audit_completed" });
1445
1350
  }
1446
1351
  finally {
1447
1352
  auditDb.close();
@@ -1562,8 +1467,7 @@ export function buildStartCommand() {
1562
1467
  return existing;
1563
1468
  const controller = RoleSessionController.managed({
1564
1469
  projectDir: cwd, runId: masterRun.runId, role: "builder", initialSettings: capturedBranchBuilder,
1565
- requireNativeContextManagement: true,
1566
- historicalCountUncertain: Boolean(recoveryRecord && (recoveryRecord.legacy || !recoveryRecord.runDecisions)),
1470
+ historicalCountUncertain: recoveryCompactionHistoryUncertain(cwd, recoveryRecord, masterRun.runId, "builder"),
1567
1471
  readSettings: () => liveRoleSettings("builder", capturedBranchBuilder),
1568
1472
  settingsBoundary: (input) => applyBuilderSettingsBoundary(masterRun.runId, worktreePath, input),
1569
1473
  settingsAdopted: (settings, active) => liveStatusReporter?.updateState({
@@ -1571,7 +1475,7 @@ export function buildStartCommand() {
1571
1475
  adapter: active, settingsRevision: settings.settings_revision, displaySessionCost: settings.display_session_cost,
1572
1476
  sessionTransition: "adopted live settings",
1573
1477
  }),
1574
- report: (event) => { currentActivity()?.update(event.kind, event.detail, { provider: capturedBranchBuilder.make, model: capturedBranchBuilder.model }); log.write("context-lifecycle", { worktreePath, ...event, sample: event.sample }); void liveStatusReporter?.tick(); },
1478
+ report: (event) => { currentActivity()?.update(event.kind, event.detail, { provider: capturedBranchBuilder.make, model: capturedBranchBuilder.model }); log.write("context-lifecycle", { worktreePath, ...event, sample: event.sample }); },
1575
1479
  handoff: async ({ reason, adapter, sample, settings, compactionCount, frozenAction }) => {
1576
1480
  const nativeUsage = await adapter.sessionUsage?.().catch(() => undefined);
1577
1481
  const transfer = await new HandoffService(cwd).transfer({
@@ -1623,7 +1527,7 @@ export function buildStartCommand() {
1623
1527
  archiveDoc: ticketsConfig.paths.archiveDoc,
1624
1528
  },
1625
1529
  resumeSessions: resumeSessionByTicket,
1626
- createBuilder: async (builderCwd, sessionId, sessionRef) => branchContextController(builderCwd).manage(await createBuilder(builderCwd, sessionId, sessionRef)),
1530
+ createBuilder: (builderCwd, sessionId, sessionRef) => createBuilder(builderCwd, sessionId, sessionRef),
1627
1531
  recordBuilderSession: (session, ticketId) => { masterRun = persistBuildSession(cwd, { ...masterRun, currentTicket: ticketId }, "builder", session); },
1628
1532
  recordQaSession: (session, ticketId) => { masterRun = persistBuildSession(cwd, { ...masterRun, currentTicket: ticketId }, "qa", session); },
1629
1533
  onSessionUnavailable: (error) => { branchSessionFailure = error; },
@@ -1633,9 +1537,6 @@ export function buildStartCommand() {
1633
1537
  observeBuilder: async (builder) => {
1634
1538
  liveStatusReporter?.stop();
1635
1539
  activeBuilderForStatus = () => builder;
1636
- activeBuilderContextSnapshot = () => [...branchContextControllers.values()]
1637
- .map((controller) => controller.contextSnapshot())
1638
- .find((snapshot) => snapshot?.contextSample.providerSessionId === builder.sessionId());
1639
1540
  const reporter = liveStatusReporter = new AgentStatusReporter({
1640
1541
  runId: masterRun.runId, role: "builder", provider: builder.agent, model: capturedBranchBuilder.model,
1641
1542
  reasoning: capturedBranchBuilder.reasoning, fast: capturedBranchBuilder.fast, step: Math.min(plan.nodes.length, branchContextControllers.size + 1), total: plan.nodes.length,
@@ -1653,7 +1554,6 @@ export function buildStartCommand() {
1653
1554
  finally {
1654
1555
  db.close();
1655
1556
  } },
1656
- contextSnapshot: activeBuilderContextSnapshot,
1657
1557
  }, (line, snapshot) => {
1658
1558
  const activity = currentActivity();
1659
1559
  if (activity && process.stdout.isTTY)
@@ -1664,6 +1564,7 @@ export function buildStartCommand() {
1664
1564
  const db = new WorkflowDb(cwd);
1665
1565
  try {
1666
1566
  db.recordTelemetry(masterRun.runId, snapshot);
1567
+ db.recordContextSample(snapshot.contextSample);
1667
1568
  if (snapshot.sessionUsage)
1668
1569
  db.recordSessionUsage(snapshot.sessionUsage);
1669
1570
  }
@@ -1681,8 +1582,10 @@ export function buildStartCommand() {
1681
1582
  },
1682
1583
  qaNonconvergence,
1683
1584
  beforeBuilderTurn: async (adapter, frozenAction, worktreePath) => (await branchContextController(worktreePath).atSafeBoundary(adapter, frozenAction)).adapter,
1585
+ observeBuilderNativeCompactions: async (adapter, worktreePath) => { await branchContextController(worktreePath).observeNativeCompactions(adapter); },
1684
1586
  builderSessionBoundary: async (adapter, frozenAction, strategy, worktreePath) => (await branchContextController(worktreePath).atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter,
1685
1587
  qaSessionBoundary,
1588
+ observeQaNativeCompactions,
1686
1589
  });
1687
1590
  if (branchSessionFailure) {
1688
1591
  const failure = branchSessionFailure;
@@ -1825,9 +1728,8 @@ export function buildStartCommand() {
1825
1728
  projectDir: cwd,
1826
1729
  runId: buildRun.runId,
1827
1730
  role: "builder",
1828
- requireNativeContextManagement: true,
1829
1731
  initialSettings: capturedBuilder,
1830
- historicalCountUncertain: Boolean(recoveryRecord && (recoveryRecord.legacy || !recoveryRecord.runDecisions)),
1732
+ historicalCountUncertain: recoveryCompactionHistoryUncertain(cwd, recoveryRecord, buildRun.runId, "builder"),
1831
1733
  readSettings: () => liveRoleSettings("builder", capturedBuilder),
1832
1734
  settingsBoundary: (input) => applyBuilderSettingsBoundary(buildRun.runId, cwd, input),
1833
1735
  settingsAdopted: (settings, active) => liveStatusReporter?.updateState({
@@ -1838,7 +1740,6 @@ export function buildStartCommand() {
1838
1740
  report: (event) => {
1839
1741
  currentActivity()?.update(event.kind, event.detail, { provider: capturedBuilder.make, model: capturedBuilder.model });
1840
1742
  log.write("context-lifecycle", { ...event, sample: event.sample });
1841
- void liveStatusReporter?.tick();
1842
1743
  },
1843
1744
  handoff: async ({ reason, adapter, sample, settings, compactionCount, frozenAction }) => {
1844
1745
  const nativeUsage = await adapter.sessionUsage?.().catch(() => undefined);
@@ -1868,9 +1769,7 @@ export function buildStartCommand() {
1868
1769
  return continuousBuilder(transfer.successor, cwd, buildRun.runId, settings);
1869
1770
  },
1870
1771
  });
1871
- builder = contextController.manage(builder);
1872
- activeBuilderContextSnapshot = () => contextController.contextSnapshot();
1873
- const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd, undefined, qaEnabled ? createQa : undefined, capturedQa.session_strategy, createBuilder, capturedBuilder.session_strategy, qaNonconvergence, async (adapter, frozenAction) => (await contextController.atSafeBoundary(adapter, frozenAction)).adapter, async (adapter, frozenAction, strategy) => (await contextController.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter, qaSessionBoundary);
1772
+ const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd, undefined, qaEnabled ? createQa : undefined, capturedQa.session_strategy, createBuilder, capturedBuilder.session_strategy, qaNonconvergence, async (adapter, frozenAction) => (await contextController.atSafeBoundary(adapter, frozenAction)).adapter, async (adapter, frozenAction, strategy) => (await contextController.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter, qaSessionBoundary, observeQaNativeCompactions, async (adapter) => { await contextController.observeNativeCompactions(adapter); });
1874
1773
  activeBuilderForStatus = () => foreman.builderAdapter();
1875
1774
  const statusReporter = liveStatusReporter = new AgentStatusReporter({
1876
1775
  runId: buildRun.runId,
@@ -1891,7 +1790,6 @@ export function buildStartCommand() {
1891
1790
  finally {
1892
1791
  db.close();
1893
1792
  } },
1894
- contextSnapshot: () => contextController.contextSnapshot(),
1895
1793
  }, (line, snapshot) => {
1896
1794
  const activity = currentActivity();
1897
1795
  if (activity && process.stdout.isTTY)
@@ -1902,6 +1800,7 @@ export function buildStartCommand() {
1902
1800
  const db = new WorkflowDb(cwd);
1903
1801
  try {
1904
1802
  db.recordTelemetry(buildRun.runId, snapshot);
1803
+ db.recordContextSample(snapshot.contextSample);
1905
1804
  if (snapshot.sessionUsage)
1906
1805
  db.recordSessionUsage(snapshot.sessionUsage);
1907
1806
  }
@@ -2054,6 +1953,19 @@ export function buildStartCommand() {
2054
1953
  }
2055
1954
  });
2056
1955
  }
1956
+ async function prepareNativeAutoCompaction(adapter) {
1957
+ if (!adapter.prepareAutoCompaction) {
1958
+ await adapter.close().catch(() => { });
1959
+ throw new Error(`${adapter.agent} does not support provider-native automatic compaction`);
1960
+ }
1961
+ try {
1962
+ await adapter.prepareAutoCompaction();
1963
+ }
1964
+ catch (error) {
1965
+ await adapter.close().catch(() => { });
1966
+ throw error;
1967
+ }
1968
+ }
2057
1969
  function createQaNonconvergenceHandler(projectDir, noninteractive, planner) {
2058
1970
  return async (context) => {
2059
1971
  if (noninteractive || !process.stdin.isTTY || !process.stdout.isTTY)
@@ -2156,6 +2068,19 @@ function readAgentDefaults(projectDir) {
2156
2068
  function explicitDefaultValue(value) {
2157
2069
  return value && value !== "default" ? value : undefined;
2158
2070
  }
2071
+ function recoveryCompactionHistoryUncertain(projectDir, recovery, runId, role) {
2072
+ if (!recovery)
2073
+ return false;
2074
+ if (recovery.legacy || !recovery.runDecisions || !runId)
2075
+ return true;
2076
+ const db = new WorkflowDb(projectDir);
2077
+ try {
2078
+ return db.hasUncheckpointedRoleTurn(runId, role);
2079
+ }
2080
+ finally {
2081
+ db.close();
2082
+ }
2083
+ }
2159
2084
  /** Make the executable branch plan exactly match each explicit root-to-tip stack. */
2160
2085
  export function applyExplicitStackTopology(plan, stacks, delivery, branchPrefix) {
2161
2086
  const nodeByTicket = new Map(plan.nodes.map((node) => [node.ticket.id, node]));
@@ -1,5 +1,5 @@
1
1
  import type { ContinuityDelta, ResolvedAgentSettings } from "rafi-spec";
2
- import type { BuilderAdapter, CompactResult, ContextManagementPolicy, ContextUsage, InterruptResult, ManagedTurnDispatcher, PreparedContextManagement, ProviderSessionUsage, ProviderSettingSwitch, RuntimeFailure, TurnResult } from "./adapters/types.js";
2
+ import type { BuilderAdapter, CompactResult, ContextUsage, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, RuntimeFailure, TurnResult } from "./adapters/types.js";
3
3
  export declare const CONTINUITY_MARKER = "RAFI_CONTINUITY_DELTA:";
4
4
  export declare class ContinuityValidationError extends Error {
5
5
  readonly problems: string[];
@@ -38,7 +38,7 @@ export interface ContinuityAdapterOptions {
38
38
  role: "builder" | "qa";
39
39
  settings: ResolvedAgentSettings;
40
40
  authoritativeStateRevision?: () => number;
41
- /** @deprecated Use recoverWithHandoff so successor acceptance is lifecycle-managed and durable. */
41
+ /** Fresh successor creation after the same-session repair also fails. */
42
42
  createSuccessor?: (handoff: string) => Promise<BuilderAdapter>;
43
43
  /**
44
44
  * Production recovery path for a double-invalid delta. The callback must
@@ -62,7 +62,6 @@ export declare class ContinuityAdapter implements BuilderAdapter {
62
62
  private readonly queue;
63
63
  private sourcePump?;
64
64
  private recoveryLeasePending;
65
- private turnDispatcher?;
66
65
  constructor(options: ContinuityAdapterOptions);
67
66
  get agent(): "claude" | "codex";
68
67
  sendTurn(instruction: string): Promise<TurnResult>;
@@ -71,10 +70,10 @@ export declare class ContinuityAdapter implements BuilderAdapter {
71
70
  adoptSessionRef(ref: import("rafi-spec").ProviderSessionRefV1): void;
72
71
  validateSession(): Promise<import("rafi-spec").SessionAvailabilityV1>;
73
72
  compact(): Promise<CompactResult>;
74
- prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
75
- updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
76
- interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
77
- installManagedTurnDispatcher(dispatcher: ManagedTurnDispatcher): void;
73
+ prepareAutoCompaction(thresholdPercent?: number): Promise<void>;
74
+ drainNativeCompactions(): import("./adapters/types.js").NativeCompaction[];
75
+ restoreNativeCompactions(compactions: NativeCompaction[]): void;
76
+ contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
78
77
  contextUsage(): Promise<ContextUsage | undefined>;
79
78
  sessionUsage(): Promise<ProviderSessionUsage | undefined>;
80
79
  switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
@@ -82,7 +81,6 @@ export declare class ContinuityAdapter implements BuilderAdapter {
82
81
  close(): Promise<void>;
83
82
  /** Keep the observable wrapper and event stream stable while moving to an accepted successor. */
84
83
  adoptValidatedSuccessor(successor: BuilderAdapter): Promise<void>;
85
- private providerTurn;
86
84
  private publish;
87
85
  private moveRecoveryLeaseAfterCheckpoint;
88
86
  private recordSessionUnavailable;