ai-foreman 1.7.7 → 1.7.8

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.
@@ -3,6 +3,7 @@ import { SessionUnavailableError, sessionUnavailableErrorFromFailure } from "./a
3
3
  import { sanitizeDiagnostics } from "./runtimeReadiness.js";
4
4
  import { WorkflowDb } from "./workflowDb.js";
5
5
  import { providerSessionKey } from "./sessionIdentity.js";
6
+ import { AsyncQueue } from "./util/asyncQueue.js";
6
7
  export class RoleSessionValidationError extends Error {
7
8
  role;
8
9
  availability;
@@ -59,6 +60,15 @@ export class RoleSessionController {
59
60
  this.adapter = result.adapter;
60
61
  return result;
61
62
  }
63
+ /** Return the stable role facade whose sendTurn() contains the lifecycle gate. */
64
+ manage(adapter) {
65
+ if (!this.thresholdController)
66
+ throw new Error("this role session controller does not own threshold lifecycle policy");
67
+ const managed = this.thresholdController.manage(adapter);
68
+ this.adapter = managed;
69
+ return managed;
70
+ }
71
+ contextSnapshot() { return this.thresholdController?.snapshot(); }
62
72
  async atWorkSessionBoundary(adapter, frozenAction, strategy) {
63
73
  if (!this.thresholdController)
64
74
  throw new Error("this role session controller does not own threshold lifecycle policy");
@@ -137,6 +147,13 @@ export class RoleSessionController {
137
147
  const result = this.boundary("missing", validated ? "exact session unavailable; transferred to a validated fresh successor" : "exact session unavailable; used the compatibility fresh fallback", issue);
138
148
  return result;
139
149
  }
150
+ if ((this.options.role === "builder" || this.options.role === "qa") && this.options.settings.session_strategy === "compact") {
151
+ const usage = await this.adapter.contextUsage?.().catch(() => undefined);
152
+ const percentage = usage?.percentage ?? (usage?.maximum ? usage.used / usage.maximum * 100 : undefined);
153
+ if (percentage !== undefined && percentage < (this.options.settings.auto_compact_threshold_percent ?? 50)) {
154
+ return this.boundary("continued", `context ${percentage.toFixed(1)}% remains below the configured ${this.options.settings.auto_compact_threshold_percent ?? 50}% ceiling`);
155
+ }
156
+ }
140
157
  let firstError = "";
141
158
  for (let attempt = 1; attempt <= 2; attempt++) {
142
159
  try {
@@ -295,331 +312,1101 @@ export class ContextCapabilityError extends Error {
295
312
  this.role = role;
296
313
  }
297
314
  }
298
- /** Threshold-triggered native compaction at host-owned safe boundaries. */
315
+ /** Stable facade: every Builder/QA provider turn enters the coordinator here. */
316
+ export class ManagedRoleAdapter {
317
+ coordinator;
318
+ adapter;
319
+ queue = new AsyncQueue();
320
+ eventPump;
321
+ processing = Promise.resolve();
322
+ closed = false;
323
+ activeSessionKeyValue;
324
+ constructor(coordinator, adapter) {
325
+ this.coordinator = coordinator;
326
+ this.adapter = adapter;
327
+ this.attachDispatcher(adapter);
328
+ this.pump(adapter);
329
+ }
330
+ get agent() { return this.adapter.agent; }
331
+ sendTurn(text) { return this.coordinator.dispatch(this, text); }
332
+ sessionId() { return this.adapter.sessionId(); }
333
+ sessionRef() { return this.adapter.sessionRef?.(); }
334
+ adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
335
+ validateSession() { return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped" }); }
336
+ compact() { return this.coordinator.compactExplicit(this); }
337
+ prepareContextManagement(policy) { if (!this.adapter.prepareContextManagement)
338
+ return Promise.reject(new Error("native context management unavailable")); return this.adapter.prepareContextManagement(policy); }
339
+ updateContextManagement(policy) { if (!this.adapter.updateContextManagement)
340
+ return Promise.reject(new Error("native context reconfiguration unavailable")); return this.adapter.updateContextManagement(policy); }
341
+ interruptTurnAtCompactionBoundary(providerEventId) { return this.adapter.interruptTurnAtCompactionBoundary?.(providerEventId) ?? Promise.resolve({ ok: false, error: "compaction-boundary interruption unavailable", providerEventId }); }
342
+ contextUsage() { return Promise.resolve(this.coordinator.contextUsage(this)); }
343
+ sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
344
+ switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
345
+ events() { return this.queue; }
346
+ underlying() { return this.adapter; }
347
+ noteActiveSession(key) {
348
+ const previous = this.activeSessionKeyValue;
349
+ this.activeSessionKeyValue = key;
350
+ return previous !== key ? previous : undefined;
351
+ }
352
+ activeSessionKey() { return this.activeSessionKeyValue; }
353
+ isClosed() { return this.closed; }
354
+ replace(adapter) {
355
+ if (adapter === this.adapter)
356
+ return;
357
+ this.adapter = adapter;
358
+ this.attachDispatcher(adapter);
359
+ this.pump(adapter);
360
+ }
361
+ async settleEvents() {
362
+ // A provider may resolve sendTurn() in the same microtask that enqueues its
363
+ // final usage/compaction event. Wait until both the event pump and its
364
+ // serialized coordinator work reach a stable point.
365
+ for (let attempt = 0; attempt < 10; attempt++) {
366
+ const processing = this.processing;
367
+ await Promise.resolve();
368
+ await processing;
369
+ await Promise.resolve();
370
+ if (processing === this.processing)
371
+ return;
372
+ }
373
+ await this.processing;
374
+ }
375
+ async close() {
376
+ if (this.closed)
377
+ return;
378
+ this.closed = true;
379
+ this.coordinator.unregister(this);
380
+ await this.adapter.close().catch(() => { });
381
+ await this.eventPump?.catch(() => { });
382
+ this.queue.close();
383
+ }
384
+ pump(source) {
385
+ this.eventPump = (async () => {
386
+ try {
387
+ for await (const event of source.events()) {
388
+ if (source !== this.adapter && event.kind !== "turn-complete")
389
+ continue;
390
+ this.queue.push(event);
391
+ this.processing = this.processing.then(() => this.coordinator.observe(this, source, event));
392
+ await this.processing;
393
+ }
394
+ }
395
+ catch (error) {
396
+ if (!this.closed)
397
+ this.queue.push({ kind: "error", message: error instanceof Error ? error.message : String(error) });
398
+ }
399
+ })();
400
+ }
401
+ attachDispatcher(adapter) {
402
+ adapter.installManagedTurnDispatcher?.((text, invoke) => this.coordinator.dispatchFollowup(this, text, invoke));
403
+ }
404
+ }
405
+ /** Per-scoped-session context state machine and the sole occupancy sample store. */
299
406
  export class ThresholdCompactionController {
300
407
  options;
301
408
  settings;
302
- armed = true;
303
- crossing = 0;
304
- seenSessions = new Set();
305
- runOnlyThreshold;
306
- bootstrapScheduled = false;
307
- bootstrapRecoveryDepth = 0;
308
409
  historicalCountUncertain;
410
+ managed = new WeakMap();
411
+ states = new Map();
412
+ nativeAttempts = new Map();
413
+ observedCompactionPhases = new Set();
414
+ settingsWatchers = new WeakMap();
415
+ active;
416
+ provisional = 0;
417
+ refreshingSettings;
418
+ dispatchDepth = 0;
309
419
  constructor(options) {
310
420
  this.options = options;
311
421
  this.settings = normalizeContextSettings(options.initialSettings);
312
422
  this.historicalCountUncertain = Boolean(options.historicalCountUncertain);
313
423
  }
424
+ manage(adapter) {
425
+ if (adapter instanceof ManagedRoleAdapter && adapter.coordinator === this)
426
+ return adapter;
427
+ const existing = this.managed.get(adapter);
428
+ if (existing)
429
+ return existing;
430
+ const wrapper = new ManagedRoleAdapter(this, adapter);
431
+ this.managed.set(adapter, wrapper);
432
+ return wrapper;
433
+ }
314
434
  async atSafeBoundary(adapter, frozenAction) {
315
- adapter = await this.adoptSettings(adapter, frozenAction);
316
- if (!adapter.sessionId() && !this.bootstrapScheduled) {
317
- this.bootstrapScheduled = true;
318
- const sample = {
319
- version: 1, runId: this.options.runId, role: this.options.role, provider: adapter.agent,
320
- model: this.settings.model, observedAt: this.now().toISOString(), source: "provider-query", freshness: "measuring",
321
- settingsRevision: this.settings.settings_revision, compactionCount: 0, handoffGeneration: this.handoffGeneration(),
322
- };
323
- const db = new WorkflowDb(this.options.projectDir);
324
- try {
325
- db.recordContextSample(sample);
326
- }
327
- finally {
328
- db.close();
435
+ const managed = this.manage(adapter);
436
+ this.active = managed;
437
+ await this.refreshSettings(managed, frozenAction, true);
438
+ let state = await this.ensureInitialized(managed, frozenAction);
439
+ state = await this.recoverMeasurementFailure(managed, state, frozenAction);
440
+ if (state.capabilityFailure)
441
+ throw this.capability(state.capabilityFailure);
442
+ state = await this.measureIntoState(managed, state, "provider-query").catch((error) => { throw this.capability(error); });
443
+ if ((state.latest.percentage ?? 100) >= this.threshold()) {
444
+ return this.enforceThreshold(managed, state, frozenAction, true);
445
+ }
446
+ this.transition(state, "armed");
447
+ return this.result(managed, state, "below-threshold");
448
+ }
449
+ async atWorkSessionBoundary(adapter, frozenAction, strategy = this.settings.session_strategy) {
450
+ const managed = this.manage(adapter);
451
+ this.active = managed;
452
+ await this.refreshSettings(managed, frozenAction, true);
453
+ let state = await this.ensureInitialized(managed, frozenAction);
454
+ state = await this.recoverMeasurementFailure(managed, state, frozenAction);
455
+ if (state.capabilityFailure)
456
+ throw this.capability(state.capabilityFailure);
457
+ state = await this.measureIntoState(managed, state, "provider-query");
458
+ if (strategy === "fresh") {
459
+ return this.performHandoff(managed, state, frozenAction, "ordinary session_strategy=fresh boundary requires a validated fresh successor", false);
460
+ }
461
+ // `compact` is a threshold policy, not an unconditional work-boundary compact.
462
+ if ((state.latest.percentage ?? 100) < this.threshold()) {
463
+ this.transition(state, "armed");
464
+ return this.result(managed, state, "below-threshold");
465
+ }
466
+ return this.enforceThreshold(managed, state, frozenAction, true);
467
+ }
468
+ effectiveSettings() { return this.settings; }
469
+ effectiveThreshold() { return this.threshold(); }
470
+ snapshot() {
471
+ if (!this.active)
472
+ return undefined;
473
+ const state = this.stateFor(this.active, false);
474
+ if (!state)
475
+ return undefined;
476
+ return {
477
+ role: this.options.role, provider: this.active.agent, model: this.settings.model,
478
+ lifecycleState: state.state, contextSample: state.latest,
479
+ configuredCeilingPercent: this.threshold(),
480
+ ...(state.installedNativeTokenLimit === undefined ? {} : { installedNativeTokenLimit: state.installedNativeTokenLimit }),
481
+ ...(state.installedNativePercent === undefined ? {} : { installedNativePercent: state.installedNativePercent }),
482
+ compactionCount: state.compactionCount, compactMaximum: this.maximum(), settingsRevision: this.settings.settings_revision,
483
+ };
484
+ }
485
+ contextUsage(adapter) {
486
+ const state = this.stateFor(adapter, false);
487
+ if (!state || state.latest.freshness !== "fresh" || state.latest.used === undefined)
488
+ return undefined;
489
+ return {
490
+ used: state.latest.used, maximum: state.latest.maximum, percentage: state.latest.percentage,
491
+ observedAt: state.latest.observedAt, source: state.latest.source, sequence: state.latest.sequence,
492
+ sessionId: state.latest.providerSessionId, model: state.latest.model,
493
+ };
494
+ }
495
+ async dispatch(managed, frozenAction, resumed = false) {
496
+ if (this.dispatchDepth > 0 && this.active === managed) {
497
+ // Nested role wrappers (for example continuity repair) remain inside the
498
+ // already-established gate and native policy.
499
+ return managed.underlying().sendTurn(frozenAction);
500
+ }
501
+ this.active = managed;
502
+ await this.refreshSettings(managed, frozenAction, true);
503
+ let state = await this.ensureInitialized(managed, frozenAction);
504
+ state = await this.recoverMeasurementFailure(managed, state, frozenAction);
505
+ if (state.capabilityFailure)
506
+ throw this.capability(state.capabilityFailure);
507
+ if (state.state === "handoff_required") {
508
+ const moved = await this.performHandoff(managed, state, frozenAction, state.handoffAfterTurn ?? "durable lifecycle recovery requires a validated successor", false);
509
+ state = this.stateFor(moved.adapter, true);
510
+ }
511
+ if ((state.latest.percentage ?? -1) >= this.threshold() || state.state === "threshold_pending" || state.state === "retrying" || state.state === "compacted_unverified") {
512
+ const enforced = await this.enforceThreshold(managed, state, frozenAction, true);
513
+ state = this.stateFor(enforced.adapter, true);
514
+ }
515
+ let result;
516
+ this.dispatchDepth += 1;
517
+ try {
518
+ result = await managed.underlying().sendTurn(frozenAction);
519
+ }
520
+ finally {
521
+ this.dispatchDepth -= 1;
522
+ }
523
+ await managed.settleEvents();
524
+ // A continuity-owned validated handoff may adopt its successor while the
525
+ // outer action is still running. Establish independent successor state
526
+ // before interpreting any post-turn lifecycle flags.
527
+ state = this.stateFor(managed, false) ?? await this.ensureInitialized(managed, frozenAction);
528
+ if (state.capabilityFailure) {
529
+ // The in-flight action settled. Preserve its result, then fence the next turn.
530
+ this.options.report?.({ kind: "capability-failure", detail: state.capabilityFailure, sample: state.latest });
531
+ return result;
532
+ }
533
+ if (state.boundaryHandoff) {
534
+ if (!result.interrupted) {
535
+ state.capabilityFailure = "the provider completed a turn after the compact maximum boundary instead of returning a distinct interrupted result";
536
+ this.persist(state);
537
+ return result;
329
538
  }
330
- return { adapter, action: "continued", sample, effectiveThreshold: this.threshold(), compactionCount: 0 };
539
+ if (resumed)
540
+ throw this.capability("a fresh successor immediately repeated a maximum-triggered compaction boundary");
541
+ await this.performHandoff(managed, state, frozenAction, state.boundaryHandoff.reason, true);
542
+ return this.dispatch(managed, frozenAction, true);
543
+ }
544
+ try {
545
+ await this.refreshSettings(managed, frozenAction, true);
546
+ }
547
+ catch (error) {
548
+ state.capabilityFailure = sanitize(error);
549
+ this.persist(state);
550
+ return result;
551
+ }
552
+ try {
553
+ state = await this.measureIntoState(managed, this.stateFor(managed, true), "provider-query");
554
+ }
555
+ catch (error) {
556
+ state.capabilityFailure = `context measurement failed after provider action: ${sanitize(error)}`;
557
+ this.persist(state);
558
+ return result;
559
+ }
560
+ if ((state.latest.percentage ?? 100) >= this.threshold() || state.retryAfterTurn || state.handoffAfterTurn) {
561
+ await this.enforceThreshold(managed, state, frozenAction, true);
562
+ }
563
+ return result;
564
+ }
565
+ /** Same gate for continuity repair, recursive continuation, and runtime retry. */
566
+ async dispatchFollowup(managed, frozenAction, invoke) {
567
+ if (this.dispatchDepth === 0 || this.active !== managed) {
568
+ throw this.capability("a wrapper attempted a provider follow-up outside the managed role dispatcher");
331
569
  }
332
- let sample = await this.measure(adapter, "provider-query");
333
- const threshold = this.threshold();
334
- let session = adapter.sessionRef?.() ?? adapter.sessionId();
335
- let sessionKey = adapterSessionKey(adapter);
336
- const firstObservedSession = Boolean(sessionKey && !this.seenSessions.has(sessionKey));
337
- if (sessionKey)
338
- this.seenSessions.add(sessionKey);
570
+ let state = this.stateFor(managed, false) ?? await this.ensureInitialized(managed, frozenAction);
571
+ if (state.capabilityFailure)
572
+ throw this.capability(state.capabilityFailure);
573
+ if (state.state === "handoff_required") {
574
+ await this.performHandoff(managed, state, frozenAction, state.handoffAfterTurn ?? "durable lifecycle recovery requires a validated successor", false);
575
+ state = this.stateFor(managed, true);
576
+ }
577
+ if ((state.latest.percentage ?? -1) >= this.threshold()
578
+ || state.state === "threshold_pending" || state.state === "retrying" || state.state === "compacted_unverified") {
579
+ await this.enforceThreshold(managed, state, frozenAction, true);
580
+ }
581
+ return invoke();
582
+ }
583
+ async compactExplicit(managed) {
584
+ try {
585
+ const state = await this.ensureInitialized(managed, "explicit native compaction");
586
+ const result = await this.compactAndVerify(managed, state, "explicit native compaction", true);
587
+ return { ok: result.action === "compacted" || result.action === "handed-off" };
588
+ }
589
+ catch (error) {
590
+ if (error instanceof SessionUnavailableError)
591
+ return { ok: false, error: error.message, failure: error.failure };
592
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
593
+ }
594
+ }
595
+ unregister(managed) {
596
+ this.settingsWatchers.get(managed)?.stop();
597
+ this.settingsWatchers.delete(managed);
598
+ const sessionKey = managed.activeSessionKey();
339
599
  const db = new WorkflowDb(this.options.projectDir);
340
- let count = 0;
341
600
  try {
342
- db.recordContextSample(sample);
343
- count = db.successfulCompactionCount(this.options.runId, this.options.role, session);
601
+ if (sessionKey)
602
+ db.clearRoleAdapterActive(this.options.runId, this.options.role, sessionKey);
344
603
  }
345
604
  finally {
346
605
  db.close();
347
606
  }
348
- if ((sample.percentage ?? -1) < threshold) {
349
- this.armed = true;
350
- return { adapter, action: "below-threshold", sample, effectiveThreshold: threshold, compactionCount: count };
351
- }
352
- if (!this.armed)
353
- return { adapter, action: "continued", sample, effectiveThreshold: threshold, compactionCount: count };
354
- this.armed = false;
355
- this.crossing += 1;
356
- let freshBootstrap = firstObservedSession;
357
- // A settings command published while the action settled is newer intent.
358
- const revisionBeforeSettle = this.settings.settings_revision;
359
- const sessionBeforeSettle = adapterSessionKey(adapter);
360
- adapter = await this.adoptSettings(adapter, frozenAction);
361
- if (this.settings.settings_revision !== revisionBeforeSettle || adapterSessionKey(adapter) !== sessionBeforeSettle) {
362
- sample = await this.measure(adapter, "provider-query");
363
- session = adapter.sessionRef?.() ?? adapter.sessionId();
364
- sessionKey = adapterSessionKey(adapter);
365
- freshBootstrap = Boolean(sessionKey && !this.seenSessions.has(sessionKey));
366
- if (sessionKey)
367
- this.seenSessions.add(sessionKey);
368
- const refreshed = new WorkflowDb(this.options.projectDir);
369
- try {
370
- refreshed.recordContextSample(sample);
371
- count = refreshed.successfulCompactionCount(this.options.runId, this.options.role, adapter.sessionRef?.() ?? adapter.sessionId());
607
+ if (this.active === managed)
608
+ this.active = undefined;
609
+ }
610
+ async observe(managed, source, event) {
611
+ if (source !== managed.underlying())
612
+ return;
613
+ if (event.kind === "context-usage") {
614
+ const state = this.stateFor(managed, false);
615
+ if (!state)
616
+ return;
617
+ this.acceptUsage(managed, state, event, event.source ?? "provider-event");
618
+ return;
619
+ }
620
+ if (event.kind === "context-compaction")
621
+ await this.observeCompaction(managed, event);
622
+ }
623
+ async ensureInitialized(managed, frozenAction) {
624
+ let existing = this.stateFor(managed, false);
625
+ if (existing && existing.settingsRevision === this.settings.settings_revision && existing.model === this.settings.model) {
626
+ this.markActive(managed, existing);
627
+ this.ensureSettingsWatcher(managed, frozenAction);
628
+ return existing;
629
+ }
630
+ const raw = managed.underlying();
631
+ const prepared = await this.installNative(raw, this.settings, !existing);
632
+ const key = adapterStateKey(raw) ?? `pending:${this.options.role}:${++this.provisional}`;
633
+ if (this.options.requireNativeContextManagement && key.includes(":unscoped:")) {
634
+ throw this.capability("provider initialized context management without a location-scoped session reference");
635
+ }
636
+ existing = this.states.get(key);
637
+ if (!existing)
638
+ existing = this.recoverOrCreateState(managed, key, frozenAction);
639
+ existing.installedNativeTokenLimit = prepared.installedNativeTokenLimit;
640
+ existing.installedNativePercent = prepared.installedNativePercent;
641
+ existing.settingsRevision = this.settings.settings_revision;
642
+ existing.model = this.settings.model;
643
+ if (prepared.sample)
644
+ this.acceptUsage(managed, existing, prepared.sample, prepared.sample.source ?? "provider-query");
645
+ if (existing.latest.freshness !== "fresh")
646
+ existing = await this.measureIntoState(managed, existing, "provider-query");
647
+ if (existing.state === "native_compacting" || existing.state === "host_compacting") {
648
+ this.reconcileIncompleteCompaction(managed, existing);
649
+ }
650
+ else if (existing.state === "compacted_unverified") {
651
+ if ((existing.latest.percentage ?? 100) < this.threshold())
652
+ this.transition(existing, "armed");
653
+ else
654
+ existing.handoffAfterTurn = "recovered compaction remained unverified at or above the configured ceiling";
655
+ }
656
+ else if (existing.state !== "handoff_required") {
657
+ this.transition(existing, (existing.latest.percentage ?? 100) < this.threshold() ? "armed" : "threshold_pending");
658
+ }
659
+ this.markActive(managed, existing);
660
+ this.ensureSettingsWatcher(managed, frozenAction);
661
+ return existing;
662
+ }
663
+ async installNative(adapter, settings, initial) {
664
+ const strict = Boolean(this.options.requireNativeContextManagement);
665
+ if (strict && (!adapter.prepareContextManagement || !adapter.updateContextManagement || !adapter.interruptTurnAtCompactionBoundary)) {
666
+ throw this.capability(`${adapter.agent} does not expose prepare, live update, and compaction-boundary interruption capabilities`);
667
+ }
668
+ const known = this.persistedMaximum(adapter, settings.model);
669
+ const policy = {
670
+ role: this.options.role, configuredThresholdPercent: settings.auto_compact_threshold_percent,
671
+ compactMaximum: settings.compact_maximum, settingsRevision: settings.settings_revision, model: settings.model,
672
+ providerSequenceStart: this.providerSequence(adapter), ...(known ? { knownModelContextWindow: known } : {}),
673
+ nativeCompactionEnabled: true,
674
+ };
675
+ const method = initial ? adapter.prepareContextManagement : adapter.updateContextManagement;
676
+ if (method) {
677
+ const prepared = await method.call(adapter, policy);
678
+ if (prepared.installedNativePercent > settings.auto_compact_threshold_percent + 0.01) {
679
+ throw this.capability(`installed native ceiling ${prepared.installedNativePercent.toFixed(2)}% is later than configured ${settings.auto_compact_threshold_percent}%`);
372
680
  }
373
- finally {
374
- refreshed.close();
681
+ return prepared;
682
+ }
683
+ const usage = await adapter.contextUsage?.();
684
+ if (!usage?.maximum)
685
+ throw this.capability("provider context occupancy and model maximum are unavailable");
686
+ const configuredTokenLimit = Math.max(1, Math.floor(usage.maximum * settings.auto_compact_threshold_percent / 100));
687
+ return { modelContextWindow: usage.maximum, configuredTokenLimit, installedNativeTokenLimit: configuredTokenLimit, installedNativePercent: settings.auto_compact_threshold_percent, sample: usage };
688
+ }
689
+ recoverOrCreateState(managed, key, frozenAction) {
690
+ const db = new WorkflowDb(this.options.projectDir);
691
+ try {
692
+ const prior = db.thresholdGenerations(this.options.runId, this.options.role, key).at(-1);
693
+ if (prior && prior.model === this.settings.model && prior.settingsRevision === this.settings.settings_revision) {
694
+ const state = {
695
+ key, generationId: prior.generationId, generation: prior.generation, state: prior.state,
696
+ settingsRevision: prior.settingsRevision, model: prior.model,
697
+ latest: prior.latestSample ?? this.measuringSample(managed, key, prior.generation),
698
+ latestProviderSequence: prior.latestSample?.sequence ?? 0,
699
+ installedNativeTokenLimit: prior.installedNativeTokenLimit,
700
+ installedNativePercent: prior.installedNativePercent,
701
+ compactionCount: db.successfulCompactionCount(this.options.runId, this.options.role, managed.sessionRef?.() ?? managed.sessionId()),
702
+ };
703
+ this.states.set(key, state);
704
+ return state;
375
705
  }
706
+ const generation = (prior?.generation ?? 0) + 1;
707
+ const state = {
708
+ key, generation, generationId: this.generationId(key, generation), state: "initializing",
709
+ settingsRevision: this.settings.settings_revision, model: this.settings.model,
710
+ latest: this.measuringSample(managed, key, generation), latestProviderSequence: 0,
711
+ compactionCount: db.successfulCompactionCount(this.options.runId, this.options.role, managed.sessionRef?.() ?? managed.sessionId()),
712
+ };
713
+ this.states.set(key, state);
714
+ this.persist(state, frozenAction);
715
+ return state;
376
716
  }
377
- const revisedThreshold = this.threshold();
378
- if ((sample.percentage ?? -1) < revisedThreshold) {
379
- this.armed = true;
380
- return { adapter, action: "continued", sample, effectiveThreshold: revisedThreshold, compactionCount: count };
717
+ finally {
718
+ db.close();
381
719
  }
382
- const crossingKey = `${sessionKey ?? "missing"}:${this.crossing}:${revisedThreshold}`;
383
- return this.compactSession(adapter, sample, count, frozenAction, crossingKey, true, freshBootstrap);
384
720
  }
385
- /** Apply the configured ordinary work-session boundary without bypassing handoff or compaction accounting. */
386
- async atWorkSessionBoundary(adapter, frozenAction, strategy = this.settings.session_strategy) {
387
- adapter = await this.adoptSettings(adapter, frozenAction);
388
- const sample = await this.measure(adapter, "provider-query");
721
+ reconcileIncompleteCompaction(managed, state) {
389
722
  const db = new WorkflowDb(this.options.projectDir);
390
- let count = 0;
391
723
  try {
392
- db.recordContextSample(sample);
393
- count = db.successfulCompactionCount(this.options.runId, this.options.role, adapter.sessionRef?.() ?? adapter.sessionId());
724
+ const incomplete = db.compactionAttempts(this.options.runId, this.options.role, managed.sessionRef?.() ?? managed.sessionId())
725
+ .filter((attempt) => attempt.status === "started" && attempt.thresholdGenerationId === state.generationId);
726
+ if (!incomplete.length) {
727
+ this.transition(state, (state.latest.percentage ?? 100) < this.threshold() ? "armed" : "retrying");
728
+ return;
729
+ }
730
+ if ((state.latest.percentage ?? 100) < this.threshold()) {
731
+ for (const attempt of incomplete)
732
+ db.finishCompactionAttempt(attempt.idempotencyKey, { ok: true, afterSample: state.latest });
733
+ state.compactionCount = db.successfulCompactionCount(this.options.runId, this.options.role, managed.sessionRef?.() ?? managed.sessionId());
734
+ this.transition(state, "armed");
735
+ }
736
+ else {
737
+ for (const attempt of incomplete)
738
+ db.finishCompactionAttempt(attempt.idempotencyKey, { ok: true, status: "unverified", error: "process restarted before provider completion could be reconciled" });
739
+ state.compactionCount = db.successfulCompactionCount(this.options.runId, this.options.role, managed.sessionRef?.() ?? managed.sessionId());
740
+ state.handoffAfterTurn = "an in-flight compaction could not be reconciled after recovery";
741
+ this.transition(state, "compacted_unverified");
742
+ }
394
743
  }
395
744
  finally {
396
745
  db.close();
397
746
  }
398
- if (strategy === "fresh") {
399
- return this.performHandoff(adapter, sample, count, frozenAction, "ordinary session_strategy=fresh boundary requires a validated fresh successor");
400
- }
401
- const crossingKey = `${adapterSessionKey(adapter) ?? "missing"}:ordinary:${count + 1}:${sha(frozenAction).slice(0, 20)}`;
402
- return this.compactSession(adapter, sample, count, frozenAction, crossingKey, false, false);
403
747
  }
404
- effectiveSettings() { return this.settings; }
405
- effectiveThreshold() { return this.threshold(); }
406
- async compactSession(adapter, sample, count, frozenAction, crossingKey, thresholdCrossing, freshBootstrap) {
407
- const threshold = this.threshold();
408
- const sessionId = adapter.sessionId();
409
- const sessionRef = adapter.sessionRef?.();
410
- const sessionKey = sessionRef ? providerSessionKey(sessionRef) : undefined;
411
- if (this.historicalCountUncertain)
412
- return this.performHandoff(adapter, sample, count, frozenAction, "historical compaction count is uncertain; handing off rather than risking the configured maximum");
413
- if (!sessionId)
414
- return this.performHandoff(adapter, sample, count, frozenAction, "the provider session identity is unavailable");
415
- if (!sessionRef)
416
- return this.performHandoff(adapter, sample, count, frozenAction, "the provider session has no validated location-scoped identity");
417
- if (count >= (this.settings.compact_maximum ?? 10))
418
- return this.performHandoff(adapter, sample, count, frozenAction, `compact maximum ${this.settings.compact_maximum ?? 10} reached`);
419
- if (!adapter.compact)
420
- return this.performHandoff(adapter, sample, count, frozenAction, "native compaction is unavailable for this provider session");
421
- const idempotencyKey = `compact:${this.options.runId}:${this.options.role}:${sha(crossingKey).slice(0, 24)}`;
422
- const attemptDb = new WorkflowDb(this.options.projectDir);
423
- const prior = attemptDb.startCompactionAttempt({ idempotencyKey, runId: this.options.runId, role: this.options.role, providerSessionId: sessionId, sessionRef, sessionKey, crossingKey, beforeSample: sample });
424
- attemptDb.close();
425
- if (prior.status === "succeeded" && prior.afterSample) {
426
- const replayDb = new WorkflowDb(this.options.projectDir);
748
+ stateFor(managed, required) {
749
+ const key = adapterStateKey(managed.underlying());
750
+ const state = key ? this.states.get(key) : undefined;
751
+ if (!state && required)
752
+ throw this.capability("managed role adapter has no initialized scoped-session state");
753
+ return state;
754
+ }
755
+ async measureIntoState(managed, state, source, requireNewerThan) {
756
+ const attempts = Math.max(1, this.options.contextRetries ?? 3);
757
+ let lastError = "truthful context occupancy is unavailable";
758
+ for (let attempt = 0; attempt < attempts; attempt++) {
759
+ let usage;
427
760
  try {
428
- count = replayDb.successfulCompactionCount(this.options.runId, this.options.role, sessionRef ?? sessionId);
761
+ usage = await managed.underlying().contextUsage?.();
429
762
  }
430
- finally {
431
- replayDb.close();
763
+ catch (error) {
764
+ lastError = sanitize(error);
432
765
  }
433
- return { adapter, action: "compacted", sample: prior.afterSample, effectiveThreshold: threshold, compactionCount: count };
766
+ if (usage && Number.isFinite(usage.used) && (usage.percentage !== undefined || usage.maximum !== undefined)) {
767
+ const accepted = this.acceptUsage(managed, state, usage, source, requireNewerThan);
768
+ if (accepted)
769
+ return state;
770
+ lastError = "provider returned only a stale or out-of-order context sample";
771
+ }
772
+ if (attempt + 1 < attempts)
773
+ await new Promise((resolve) => setTimeout(resolve, 100));
434
774
  }
435
- this.options.report?.({ kind: "compacting", detail: thresholdCrossing
436
- ? `context ${sample.percentage?.toFixed(1)}% reached ${threshold}%`
437
- : `ordinary compact session boundary (${count}/${this.settings.compact_maximum ?? 10})`, sample });
438
- let compacted;
775
+ throw this.capability(`${lastError} after ${attempts} bounded provider query attempt(s)`);
776
+ }
777
+ async recoverMeasurementFailure(managed, state, frozenAction) {
778
+ if (!state.capabilityFailure?.startsWith("context measurement failed after provider action:"))
779
+ return state;
439
780
  try {
440
- compacted = await adapter.compact();
781
+ state = await this.measureIntoState(managed, state, "provider-query");
782
+ state.capabilityFailure = undefined;
783
+ this.transition(state, (state.latest.percentage ?? 100) < this.threshold() ? "armed" : "threshold_pending");
441
784
  }
442
785
  catch (error) {
443
- compacted = error instanceof SessionUnavailableError
444
- ? { ok: false, error: error.message, failure: error.failure }
445
- : { ok: false, error: error instanceof Error ? error.message : String(error) };
786
+ state.capabilityFailure = `context measurement failed after provider action: ${sanitize(error)}`;
787
+ this.persist(state, frozenAction);
446
788
  }
447
- if (compacted.failure?.category === "session-unavailable") {
448
- const failedDb = new WorkflowDb(this.options.projectDir);
789
+ return state;
790
+ }
791
+ acceptUsage(managed, state, usage, source, requireNewerThan) {
792
+ if (!Number.isFinite(usage.used))
793
+ return false;
794
+ const percentage = usage.percentage ?? (usage.maximum ? usage.used / usage.maximum * 100 : undefined);
795
+ if (percentage === undefined || !Number.isFinite(percentage))
796
+ return false;
797
+ if (usage.sessionId && managed.sessionId() && usage.sessionId !== managed.sessionId())
798
+ return false;
799
+ if (usage.model && this.settings.model !== "default" && usage.model !== this.settings.model)
800
+ return false;
801
+ const sequence = usage.sequence;
802
+ if (sequence !== undefined && sequence === state.latestProviderSequence && requireNewerThan === undefined && state.latest.freshness === "fresh")
803
+ return true;
804
+ if (sequence !== undefined && sequence < state.latestProviderSequence)
805
+ return false;
806
+ if (requireNewerThan !== undefined && sequence !== undefined && sequence <= requireNewerThan)
807
+ return false;
808
+ if (requireNewerThan !== undefined && sequence === undefined
809
+ && state.latest.used === usage.used && state.latest.maximum === usage.maximum && state.latest.percentage === percentage)
810
+ return false;
811
+ if (sequence !== undefined)
812
+ state.latestProviderSequence = sequence;
813
+ if (state.capabilityFailure?.startsWith("context measurement failed after provider action:"))
814
+ state.capabilityFailure = undefined;
815
+ const ref = managed.sessionRef?.();
816
+ state.compactionCount = this.compactionCount(ref ?? managed.sessionId());
817
+ state.latest = {
818
+ version: 1, runId: this.options.runId, role: this.options.role, provider: managed.agent,
819
+ providerSessionId: managed.sessionId(), ...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : { sessionKey: state.key }),
820
+ model: this.settings.model, observedAt: usage.observedAt ?? this.now().toISOString(), source, freshness: "fresh",
821
+ used: usage.used, maximum: usage.maximum, percentage, settingsRevision: this.settings.settings_revision,
822
+ compactionCount: state.compactionCount, handoffGeneration: this.handoffGeneration(),
823
+ ...(sequence === undefined ? {} : { sequence }), thresholdGeneration: state.generation,
824
+ };
825
+ const db = new WorkflowDb(this.options.projectDir);
826
+ try {
827
+ db.recordContextSample(state.latest);
828
+ }
829
+ finally {
830
+ db.close();
831
+ }
832
+ this.persist(state);
833
+ this.options.report?.({
834
+ kind: "context-sample",
835
+ detail: `authoritative context ${percentage.toFixed(1)}% (${usage.used}${usage.maximum === undefined ? "" : `/${usage.maximum}`})`,
836
+ sample: state.latest,
837
+ });
838
+ if (percentage >= this.threshold() && state.state === "armed")
839
+ this.beginGeneration(state, "threshold_pending");
840
+ return true;
841
+ }
842
+ async enforceThreshold(managed, state, frozenAction, allowHandoff) {
843
+ if (state.handoffAfterTurn)
844
+ return this.performHandoff(managed, state, frozenAction, state.handoffAfterTurn, false);
845
+ if (this.historicalCountUncertain)
846
+ return this.performHandoff(managed, state, frozenAction, "historical compaction count is uncertain; handing off rather than risking the configured maximum", false);
847
+ if ((state.latest.percentage ?? 100) < this.threshold() && !state.retryAfterTurn) {
848
+ this.transition(state, "armed");
849
+ return this.result(managed, state, "below-threshold");
850
+ }
851
+ if (state.compactionCount >= this.maximum())
852
+ return this.performHandoff(managed, state, frozenAction, `compact maximum ${this.maximum()} reached`, false);
853
+ return this.compactAndVerify(managed, state, frozenAction, allowHandoff);
854
+ }
855
+ async compactAndVerify(managed, state, frozenAction, allowHandoff) {
856
+ const raw = managed.underlying();
857
+ if (!raw.compact) {
858
+ if (allowHandoff)
859
+ return this.performHandoff(managed, state, frozenAction, "native compaction is unavailable", false);
860
+ throw this.capability("fresh successor does not expose native compaction");
861
+ }
862
+ let failure = "native compaction failed";
863
+ for (let attempt = 1; attempt <= 2; attempt++) {
864
+ const terminalAttempts = this.generationTerminalAttempts(managed, state);
865
+ if (terminalAttempts >= 2) {
866
+ failure = "the threshold generation exhausted its one bounded compaction retry";
867
+ break;
868
+ }
869
+ state.compactionCount = this.compactionCount(managed.sessionRef?.() ?? managed.sessionId());
870
+ if (state.compactionCount >= this.maximum())
871
+ break;
872
+ if (attempt > 1)
873
+ this.transition(state, "retrying");
874
+ else if (state.state === "armed")
875
+ this.beginGeneration(state, "threshold_pending");
876
+ this.transition(state, "host_compacting");
877
+ const beforeSample = state.latest;
878
+ const beforeProviderSequence = state.latestProviderSequence;
879
+ this.invalidateSample(managed, state);
880
+ const crossingKey = `${state.key}:${state.generation}:${attempt}:${sha(frozenAction).slice(0, 20)}`;
881
+ const idempotencyKey = `compact:${this.options.runId}:${this.options.role}:${sha(crossingKey).slice(0, 24)}`;
882
+ const db = new WorkflowDb(this.options.projectDir);
883
+ const prior = db.startCompactionAttempt({
884
+ idempotencyKey, runId: this.options.runId, role: this.options.role,
885
+ providerSessionId: managed.sessionId(), sessionRef: managed.sessionRef?.(), sessionKey: state.key,
886
+ crossingKey, beforeSample, origin: "rafi-manual", thresholdGenerationId: state.generationId,
887
+ });
888
+ db.close();
889
+ if (prior.status !== "started") {
890
+ const recoveredSample = prior.afterSample ?? prior.beforeSample;
891
+ if (recoveredSample) {
892
+ state.latest = recoveredSample;
893
+ state.latestProviderSequence = Math.max(state.latestProviderSequence, recoveredSample.sequence ?? 0);
894
+ }
895
+ state.compactionCount = this.compactionCount(managed.sessionRef?.() ?? managed.sessionId());
896
+ if (prior.status === "succeeded" && prior.afterSample && (prior.afterSample.percentage ?? 100) < this.threshold()) {
897
+ this.transition(state, "armed");
898
+ return this.result(managed, state, "compacted");
899
+ }
900
+ if (prior.status === "unverified" || (prior.status === "succeeded" && !prior.afterSample)) {
901
+ this.transition(state, "compacted_unverified");
902
+ if (allowHandoff)
903
+ return this.performHandoff(managed, state, frozenAction, "a recovered compaction has no authoritative post-compact sample", false);
904
+ throw this.capability("fresh successor compaction could not be verified after recovery");
905
+ }
906
+ failure = prior.status === "failed"
907
+ ? sanitize(prior.error ?? "the prior compaction attempt failed")
908
+ : `the prior compaction remained at ${prior.afterSample?.percentage?.toFixed(1) ?? "an unknown"}%`;
909
+ // The durable terminal attempt already consumed this ordinal. Move to
910
+ // the one remaining retry rather than issuing the same compaction again.
911
+ continue;
912
+ }
913
+ state.manualAttemptKey = idempotencyKey;
914
+ this.options.report?.({ kind: attempt === 1 ? "compacting" : "retrying", detail: `context reached ${this.threshold()}%; compaction ${state.compactionCount + 1}/${this.maximum()}`, sample: state.latest });
915
+ let compacted;
449
916
  try {
450
- failedDb.finishCompactionAttempt(idempotencyKey, { ok: false, error: compacted.error ?? compacted.failure.diagnostics });
451
- failedDb.appendContinuityEvent({
452
- runId: this.options.runId,
453
- role: "host",
454
- kind: "session_unavailable",
455
- payload: {
456
- role: this.options.role,
457
- operation: "native-compaction",
458
- phase: compacted.failure.phase,
459
- dispatchState: compacted.failure.dispatchState ?? "unknown",
460
- reason: compacted.failure.availability?.reason,
461
- detail: compacted.failure.diagnostics,
462
- },
463
- authoritativeStateRevision: failedDb.continuityHead(this.options.runId, this.options.role)?.authoritativeStateRevision ?? this.settings.settings_revision,
464
- sessionRef,
465
- });
466
- if (compacted.failure.dispatchState === "unknown")
467
- failedDb.setContinuityHeadState(this.options.runId, this.options.role, "degraded");
917
+ compacted = await raw.compact();
918
+ }
919
+ catch (error) {
920
+ compacted = error instanceof SessionUnavailableError ? { ok: false, error: error.message, failure: error.failure } : { ok: false, error: sanitize(error) };
921
+ }
922
+ await managed.settleEvents();
923
+ state.manualAttemptKey = undefined;
924
+ if (compacted.failure?.category === "session-unavailable") {
925
+ const failed = new WorkflowDb(this.options.projectDir);
926
+ try {
927
+ failed.finishCompactionAttempt(idempotencyKey, { ok: false, error: compacted.error ?? compacted.failure.diagnostics });
928
+ this.recordCompactionSessionUnavailable(failed, managed, compacted.failure);
929
+ }
930
+ finally {
931
+ failed.close();
932
+ }
933
+ throw sessionUnavailableErrorFromFailure(compacted.failure);
934
+ }
935
+ if (!compacted.ok) {
936
+ failure = sanitize(compacted.error ?? "provider did not explicitly complete compaction");
937
+ const failed = new WorkflowDb(this.options.projectDir);
938
+ try {
939
+ failed.finishCompactionAttempt(idempotencyKey, { ok: false, error: failure });
940
+ }
941
+ finally {
942
+ failed.close();
943
+ }
944
+ continue; // one bounded retry; failures do not consume compact slots
945
+ }
946
+ let after;
947
+ try {
948
+ state = await this.measureIntoState(managed, state, "post-compact", beforeProviderSequence);
949
+ after = state.latest;
950
+ }
951
+ catch (error) {
952
+ failure = sanitize(error);
953
+ }
954
+ const completed = new WorkflowDb(this.options.projectDir);
955
+ try {
956
+ completed.finishCompactionAttempt(idempotencyKey, after
957
+ ? { ok: true, afterSample: after }
958
+ : { ok: true, status: "unverified", error: failure });
959
+ state.compactionCount = completed.successfulCompactionCount(this.options.runId, this.options.role, managed.sessionRef?.() ?? managed.sessionId());
468
960
  }
469
961
  finally {
470
- failedDb.close();
962
+ completed.close();
963
+ }
964
+ if (!after) {
965
+ this.transition(state, "compacted_unverified");
966
+ if (allowHandoff)
967
+ return this.performHandoff(managed, state, frozenAction, "provider reported compaction but no newer authoritative post-compact sample was available", false);
968
+ throw this.capability("fresh successor compaction could not be verified");
471
969
  }
472
- throw sessionUnavailableErrorFromFailure(compacted.failure);
970
+ this.options.report?.({ kind: "compacted", detail: `context reduced to ${after.percentage?.toFixed(1)}%`, sample: after });
971
+ if ((after.percentage ?? 100) < this.threshold()) {
972
+ state.retryAfterTurn = undefined;
973
+ this.transition(state, "armed");
974
+ return this.result(managed, state, "compacted");
975
+ }
976
+ failure = `compaction remained at ${after.percentage?.toFixed(1)}%, at or above the configured ${this.threshold()}% ceiling`;
977
+ if (this.generationTerminalAttempts(managed, state) >= 2 || state.compactionCount >= this.maximum())
978
+ break;
979
+ }
980
+ if (allowHandoff)
981
+ return this.performHandoff(managed, state, frozenAction, failure, false);
982
+ throw this.capability(`fresh successor could not get below the configured ceiling after bounded compaction: ${failure}`);
983
+ }
984
+ async performHandoff(managed, state, frozenAction, reason, resumeFrozenAction) {
985
+ if (!this.options.handoff)
986
+ throw this.capability(`${reason}; validated handoff is unavailable`);
987
+ this.transition(state, "handoff_required", frozenAction);
988
+ this.options.report?.({ kind: "handoff", detail: reason, sample: state.latest });
989
+ const predecessorKey = state.key;
990
+ const successor = await this.options.handoff({
991
+ reason, adapter: managed.underlying(), sample: state.latest, settings: this.settings,
992
+ compactionCount: state.compactionCount, frozenAction,
993
+ });
994
+ const successorRaw = successor instanceof ManagedRoleAdapter ? successor.underlying() : successor;
995
+ const successorKey = adapterStateKey(successorRaw);
996
+ if (!successorKey || successorKey === predecessorKey)
997
+ throw this.capability("validated handoff did not produce a genuinely fresh location-scoped successor");
998
+ managed.replace(successorRaw);
999
+ this.historicalCountUncertain = false;
1000
+ let next = await this.ensureInitialized(managed, frozenAction);
1001
+ this.transition(next, "resuming", frozenAction);
1002
+ next = await this.measureIntoState(managed, next, "provider-query");
1003
+ if ((next.latest.percentage ?? 100) >= this.threshold()) {
1004
+ await this.compactAndVerify(managed, next, frozenAction, false);
1005
+ next = this.stateFor(managed, true);
473
1006
  }
474
- if (!compacted.ok) {
475
- const failedDb = new WorkflowDb(this.options.projectDir);
1007
+ this.transition(next, "armed");
1008
+ if (resumeFrozenAction)
1009
+ next.boundaryHandoff = undefined;
1010
+ return this.result(managed, next, "handed-off");
1011
+ }
1012
+ async observeCompaction(managed, event) {
1013
+ const state = this.stateFor(managed, false);
1014
+ if (!state)
1015
+ return;
1016
+ if (event.sessionId && managed.sessionId() && event.sessionId !== managed.sessionId())
1017
+ return;
1018
+ const eventKey = `${state.key}:${event.providerEventId}`;
1019
+ const phaseKey = `${eventKey}:${event.phase}`;
1020
+ if (this.observedCompactionPhases.has(phaseKey))
1021
+ return;
1022
+ this.observedCompactionPhases.add(phaseKey);
1023
+ if (event.origin === "rafi-manual" && state.manualAttemptKey) {
1024
+ const db = new WorkflowDb(this.options.projectDir);
476
1025
  try {
477
- failedDb.finishCompactionAttempt(idempotencyKey, { ok: false, error: compacted.error ?? "provider did not explicitly complete compaction" });
1026
+ db.linkCompactionProviderEvent(state.manualAttemptKey, event.providerEventId);
478
1027
  }
1028
+ catch { /* a duplicate provider event is already durably linked */ }
479
1029
  finally {
480
- failedDb.close();
1030
+ db.close();
481
1031
  }
482
- return this.performHandoff(adapter, sample, count, frozenAction, `native compaction failed: ${sanitize(compacted.error ?? "no explicit completion")}`);
1032
+ this.nativeAttempts.set(eventKey, state.manualAttemptKey);
1033
+ return; // the host compact path owns success verification and accounting
483
1034
  }
484
- let after;
485
- try {
486
- after = await this.measure(adapter, "post-compact");
1035
+ if (event.origin === "rafi-manual" && this.nativeAttempts.has(eventKey))
1036
+ return;
1037
+ if (event.phase === "started") {
1038
+ if (state.compactionCount >= this.maximum()) {
1039
+ await this.interruptForHandoff(managed, state, event.providerEventId, `compact maximum ${this.maximum()} reached at native boundary ${event.providerEventId}`);
1040
+ return;
1041
+ }
1042
+ if (state.state === "armed")
1043
+ this.beginGeneration(state, "threshold_pending");
1044
+ if (state.handoffAfterTurn || this.generationTerminalAttempts(managed, state) >= 2) {
1045
+ await this.interruptForHandoff(managed, state, event.providerEventId, state.handoffAfterTurn
1046
+ ?? `bounded compaction retry exhausted at native boundary ${event.providerEventId}`);
1047
+ return;
1048
+ }
1049
+ this.transition(state, "native_compacting", undefined, event.providerEventId);
1050
+ const beforeSample = state.latest;
1051
+ this.invalidateSample(managed, state);
1052
+ const idempotencyKey = `native:${this.options.runId}:${this.options.role}:${sha(eventKey).slice(0, 24)}`;
1053
+ const db = new WorkflowDb(this.options.projectDir);
1054
+ const attempt = db.startCompactionAttempt({
1055
+ idempotencyKey, runId: this.options.runId, role: this.options.role,
1056
+ providerSessionId: managed.sessionId(), sessionRef: managed.sessionRef?.(), sessionKey: state.key,
1057
+ crossingKey: eventKey, beforeSample, origin: event.origin,
1058
+ providerEventId: event.providerEventId, thresholdGenerationId: state.generationId,
1059
+ });
1060
+ db.close();
1061
+ this.nativeAttempts.set(eventKey, attempt.idempotencyKey);
1062
+ return;
487
1063
  }
488
- catch (error) {
489
- const failedDb = new WorkflowDb(this.options.projectDir);
1064
+ const idempotencyKey = this.nativeAttempts.get(eventKey) ?? `native:${this.options.runId}:${this.options.role}:${sha(eventKey).slice(0, 24)}`;
1065
+ const observedStart = this.nativeAttempts.has(eventKey);
1066
+ const db = new WorkflowDb(this.options.projectDir);
1067
+ let attempt = db.compactionAttempt(idempotencyKey);
1068
+ if (!attempt)
1069
+ attempt = db.startCompactionAttempt({
1070
+ idempotencyKey, runId: this.options.runId, role: this.options.role,
1071
+ providerSessionId: managed.sessionId(), sessionRef: managed.sessionRef?.(), sessionKey: state.key,
1072
+ crossingKey: eventKey, beforeSample: state.latest, origin: event.origin,
1073
+ providerEventId: event.providerEventId, thresholdGenerationId: state.generationId,
1074
+ });
1075
+ if (attempt.status !== "started") {
1076
+ db.close();
1077
+ return;
1078
+ }
1079
+ if (event.phase === "failed") {
1080
+ db.finishCompactionAttempt(attempt.idempotencyKey, { ok: false, error: event.reason ?? "provider-native compaction failed" });
1081
+ db.close();
1082
+ state.retryAfterTurn = event.reason ?? "provider-native compaction failed";
1083
+ this.transition(state, "retrying");
1084
+ return;
1085
+ }
1086
+ db.close();
1087
+ if (!observedStart && event.origin !== "rafi-manual") {
1088
+ state.capabilityFailure = `provider completed native compaction ${event.providerEventId} without an observable start boundary; compact maximum ${this.maximum()} cannot be enforced`;
1089
+ this.persist(state, undefined, event.providerEventId);
1090
+ }
1091
+ if (state.boundaryHandoff?.providerEventId === event.providerEventId) {
1092
+ const exceeded = new WorkflowDb(this.options.projectDir);
490
1093
  try {
491
- failedDb.finishCompactionAttempt(idempotencyKey, { ok: false, error: error instanceof Error ? error.message : String(error) });
1094
+ exceeded.finishCompactionAttempt(attempt.idempotencyKey, { ok: true, status: "unverified", error: "provider completed a vetoed over-maximum compaction" });
492
1095
  }
493
1096
  finally {
494
- failedDb.close();
1097
+ exceeded.close();
495
1098
  }
496
- return this.performHandoff(adapter, sample, count, frozenAction, "provider reported compaction but no authoritative post-compact occupancy sample was available");
1099
+ state.compactionCount = this.compactionCount(managed.sessionRef?.() ?? managed.sessionId());
1100
+ state.capabilityFailure = `provider completed native compaction ${event.providerEventId} beyond compact maximum ${this.maximum()}`;
1101
+ this.persist(state);
1102
+ return;
497
1103
  }
498
- const completedDb = new WorkflowDb(this.options.projectDir);
1104
+ if (event.postCompactSample)
1105
+ this.acceptUsage(managed, state, event.postCompactSample, "post-compact");
1106
+ let verified = state.latest.freshness === "fresh" && state.latest.sequence !== undefined && state.latest.sequence > (attempt.beforeSample?.sequence ?? -1);
1107
+ if (!verified) {
1108
+ try {
1109
+ await this.measureIntoState(managed, state, "post-compact", attempt.beforeSample?.sequence);
1110
+ verified = true;
1111
+ }
1112
+ catch {
1113
+ verified = false;
1114
+ }
1115
+ }
1116
+ const completed = new WorkflowDb(this.options.projectDir);
499
1117
  try {
500
- completedDb.recordContextSample(after);
501
- completedDb.finishCompactionAttempt(idempotencyKey, { ok: true, afterSample: after });
502
- count = completedDb.successfulCompactionCount(this.options.runId, this.options.role, sessionRef ?? sessionId);
1118
+ completed.finishCompactionAttempt(attempt.idempotencyKey, verified
1119
+ ? { ok: true, afterSample: state.latest }
1120
+ : { ok: true, status: "unverified", error: "no newer authoritative post-compact sample" });
1121
+ state.compactionCount = completed.successfulCompactionCount(this.options.runId, this.options.role, managed.sessionRef?.() ?? managed.sessionId());
503
1122
  }
504
1123
  finally {
505
- completedDb.close();
1124
+ completed.close();
506
1125
  }
507
- this.options.report?.({ kind: "compacted", detail: `context reduced to ${after.percentage?.toFixed(1)}%`, sample: after });
508
- if (freshBootstrap && (after.percentage ?? -1) >= threshold) {
509
- const adjusted = Math.min(99, Math.ceil((after.percentage ?? 0) + 10));
510
- if (adjusted <= (after.percentage ?? 0))
511
- throw new ContextCapabilityError(this.options.runId, this.options.role, `fresh bootstrap remained at ${after.percentage?.toFixed(1)}% after compaction and no safe run-only threshold exists`);
512
- this.runOnlyThreshold = adjusted;
513
- this.options.report?.({ kind: "threshold-adjusted", detail: `fresh bootstrap remained high; run-only threshold is now ${adjusted}%`, sample: after });
1126
+ if (!verified) {
1127
+ state.handoffAfterTurn = "provider-native compaction succeeded but post-compact occupancy could not be verified";
1128
+ this.transition(state, "compacted_unverified");
1129
+ }
1130
+ else if ((state.latest.percentage ?? 100) < this.threshold()) {
1131
+ this.transition(state, "armed");
1132
+ }
1133
+ else {
1134
+ state.retryAfterTurn = `provider-native compaction remained at ${state.latest.percentage?.toFixed(1)}%`;
1135
+ this.transition(state, "retrying");
514
1136
  }
515
- this.armed = thresholdCrossing ? (after.percentage ?? 100) < this.threshold() : true;
516
- return { adapter, action: "compacted", sample: after, effectiveThreshold: this.threshold(), compactionCount: count };
517
1137
  }
518
- async performHandoff(adapter, sample, count, frozenAction, reason) {
519
- if (!this.options.handoff)
520
- throw new ContextCapabilityError(this.options.runId, this.options.role, `${reason}; validated handoff is unavailable`);
521
- this.options.report?.({ kind: "handoff", detail: reason, sample });
522
- const successor = await this.options.handoff({ reason, adapter, sample, settings: this.settings, compactionCount: count, frozenAction });
523
- const post = await this.measure(successor, "provider-query");
524
- this.historicalCountUncertain = false;
525
- const successorSessionId = successor.sessionId();
526
- const successorSessionKey = adapterSessionKey(successor);
527
- if (successorSessionKey)
528
- this.seenSessions.add(successorSessionKey);
1138
+ async interruptForHandoff(managed, state, providerEventId, reason) {
1139
+ state.boundaryHandoff = { providerEventId, reason };
1140
+ this.transition(state, "handoff_required", undefined, providerEventId);
1141
+ const interrupted = await managed.underlying().interruptTurnAtCompactionBoundary?.(providerEventId);
1142
+ if (!interrupted?.ok) {
1143
+ state.capabilityFailure = `provider could not stop native compaction ${providerEventId}: ${interrupted?.error ?? "interrupt unavailable"}`;
1144
+ this.persist(state);
1145
+ }
1146
+ }
1147
+ generationTerminalAttempts(managed, state) {
1148
+ const session = managed.sessionRef?.() ?? managed.sessionId();
1149
+ if (!session)
1150
+ return 0;
529
1151
  const db = new WorkflowDb(this.options.projectDir);
530
1152
  try {
531
- db.recordContextSample(post);
1153
+ return db.compactionAttempts(this.options.runId, this.options.role, session)
1154
+ .filter((attempt) => attempt.thresholdGenerationId === state.generationId && attempt.status !== "started")
1155
+ .length;
532
1156
  }
533
1157
  finally {
534
1158
  db.close();
535
1159
  }
536
- if ((post.percentage ?? 100) >= this.threshold()) {
537
- if (this.bootstrapRecoveryDepth >= 1) {
538
- throw new ContextCapabilityError(this.options.runId, this.options.role, `fresh successor bootstrap remained at ${post.percentage?.toFixed(1)}% after compaction-failure recovery`);
539
- }
540
- this.bootstrapRecoveryDepth += 1;
541
- try {
542
- const bootstrapped = await this.compactSession(successor, post, 0, frozenAction, `${successorSessionKey ?? successorSessionId ?? "missing"}:fresh-bootstrap:${sha(frozenAction).slice(0, 20)}`, true, true);
543
- return { ...bootstrapped, action: "handed-off" };
544
- }
545
- finally {
546
- this.bootstrapRecoveryDepth -= 1;
547
- }
548
- }
549
- this.armed = true;
550
- return { adapter: successor, action: "handed-off", sample: post, effectiveThreshold: this.threshold(), compactionCount: 0 };
551
1160
  }
552
- async adoptSettings(adapter, frozenAction) {
1161
+ async refreshSettings(managed, frozenAction, allowTransition) {
1162
+ // Timer-driven and post-turn refreshes can meet at the same provider
1163
+ // boundary. Wait for the active installation, then re-read the revision so
1164
+ // neither caller can proceed on partially applied settings or miss a newer
1165
+ // revision that arrived during the first installation.
1166
+ while (this.refreshingSettings)
1167
+ await this.refreshingSettings;
1168
+ if (managed.isClosed())
1169
+ return;
553
1170
  const candidate = this.options.readSettings?.();
554
1171
  if (!candidate || candidate.settings_revision <= this.settings.settings_revision)
555
- return adapter;
1172
+ return;
1173
+ const lifecycle = this.stateFor(managed, false)?.state;
1174
+ if (lifecycle === "native_compacting" || lifecycle === "host_compacting")
1175
+ return;
556
1176
  const next = normalizeContextSettings(candidate);
557
- const providerControlsChanged = next.make !== this.settings.make
558
- || next.model !== this.settings.model
559
- || next.reasoning !== this.settings.reasoning
560
- || next.fast !== this.settings.fast;
561
- let adoptedAdapter = adapter;
562
- if (providerControlsChanged) {
563
- if (this.options.settingsBoundary) {
564
- adoptedAdapter = await this.options.settingsBoundary({ adapter, current: this.settings, next, frozenAction });
565
- }
566
- else if (next.make === this.settings.make && adapter.switchSettings) {
567
- const switched = await adapter.switchSettings({ model: next.model === "default" ? undefined : next.model, effort: effort(next.reasoning), fast: next.fast });
568
- if (switched.failure?.category === "session-unavailable")
569
- throw sessionUnavailableErrorFromFailure(switched.failure);
570
- if (!switched.ok)
571
- throw new ContextCapabilityError(this.options.runId, this.options.role, `settings revision ${next.settings_revision} could not be applied safely: ${sanitize(switched.error ?? "provider rejected the settings switch")}`);
1177
+ const providerControlsChanged = next.make !== this.settings.make || next.model !== this.settings.model || next.reasoning !== this.settings.reasoning || next.fast !== this.settings.fast;
1178
+ if (providerControlsChanged && !allowTransition)
1179
+ return;
1180
+ let releaseRefresh;
1181
+ const refresh = new Promise((resolve) => { releaseRefresh = resolve; });
1182
+ this.refreshingSettings = refresh;
1183
+ try {
1184
+ if (providerControlsChanged) {
1185
+ let adopted;
1186
+ if (this.options.settingsBoundary) {
1187
+ adopted = await this.options.settingsBoundary({ adapter: managed.underlying(), current: this.settings, next, frozenAction });
1188
+ }
1189
+ else if (next.make === this.settings.make && managed.underlying().switchSettings) {
1190
+ const switched = await managed.underlying().switchSettings({ model: next.model === "default" ? undefined : next.model, effort: effort(next.reasoning), fast: next.fast });
1191
+ if (switched.failure?.category === "session-unavailable")
1192
+ throw sessionUnavailableErrorFromFailure(switched.failure);
1193
+ if (!switched.ok)
1194
+ throw this.capability(`settings revision ${next.settings_revision} could not be applied safely: ${sanitize(switched.error ?? "provider rejected settings")}`);
1195
+ adopted = managed.underlying();
1196
+ }
1197
+ else
1198
+ throw this.capability(`settings revision ${next.settings_revision} requires a validated provider/model handoff`);
1199
+ const raw = adopted instanceof ManagedRoleAdapter ? adopted.underlying() : adopted;
1200
+ if (raw.agent !== next.make)
1201
+ throw this.capability(`settings revision ${next.settings_revision} requested ${next.make}, but successor reports ${raw.agent}`);
1202
+ managed.replace(raw);
1203
+ this.settings = next;
1204
+ await this.ensureInitialized(managed, frozenAction);
572
1205
  }
573
1206
  else {
574
- throw new ContextCapabilityError(this.options.runId, this.options.role, `settings revision ${next.settings_revision} requires a validated provider/model handoff, but no settings boundary is available`);
1207
+ // Threshold-only revisions are acknowledged only after native control is installed.
1208
+ const priorState = this.stateFor(managed, false);
1209
+ if (!priorState) {
1210
+ this.settings = next;
1211
+ await this.ensureInitialized(managed, frozenAction);
1212
+ }
1213
+ else {
1214
+ const prepared = await this.installNative(managed.underlying(), next, false);
1215
+ this.settings = next;
1216
+ const state = this.stateFor(managed, true);
1217
+ this.beginGeneration(state, (state.latest.percentage ?? 100) < this.threshold() ? "armed" : "threshold_pending");
1218
+ state.settingsRevision = next.settings_revision;
1219
+ state.model = next.model;
1220
+ state.installedNativeTokenLimit = prepared.installedNativeTokenLimit;
1221
+ state.installedNativePercent = prepared.installedNativePercent;
1222
+ if (prepared.sample)
1223
+ this.acceptUsage(managed, state, prepared.sample, prepared.sample.source ?? "provider-query");
1224
+ this.persist(state, frozenAction);
1225
+ }
575
1226
  }
576
- if (adoptedAdapter.agent !== next.make) {
577
- throw new ContextCapabilityError(this.options.runId, this.options.role, `settings revision ${next.settings_revision} requested ${next.make}, but the adopted session reports ${adoptedAdapter.agent}`);
1227
+ const adoptedState = this.stateFor(managed, false);
1228
+ if (adoptedState?.capabilityFailure) {
1229
+ adoptedState.capabilityFailure = undefined;
1230
+ this.persist(adoptedState, frozenAction);
578
1231
  }
1232
+ const db = new WorkflowDb(this.options.projectDir);
1233
+ try {
1234
+ const ref = managed.sessionRef?.();
1235
+ db.acknowledgeSettings({ runId: this.options.runId, role: this.options.role, providerSessionId: managed.sessionId(), ...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : {}), revision: next.settings_revision, acknowledgedAt: this.now().toISOString() });
1236
+ }
1237
+ finally {
1238
+ db.close();
1239
+ }
1240
+ this.options.settingsAdopted?.(next, managed);
1241
+ this.options.report?.({ kind: "settings-adopted", detail: `adopted settings revision ${next.settings_revision}` });
1242
+ }
1243
+ finally {
1244
+ releaseRefresh();
1245
+ if (this.refreshingSettings === refresh)
1246
+ this.refreshingSettings = undefined;
1247
+ }
1248
+ }
1249
+ ensureSettingsWatcher(managed, frozenAction) {
1250
+ if (managed.isClosed())
1251
+ return;
1252
+ const existing = this.settingsWatchers.get(managed);
1253
+ if (existing) {
1254
+ existing.frozenAction = frozenAction;
1255
+ return;
579
1256
  }
580
- this.settings = next;
581
- this.runOnlyThreshold = undefined;
1257
+ if (!this.options.readSettings)
1258
+ return;
1259
+ const watcher = { frozenAction, stop: () => { } };
1260
+ const timer = setInterval(() => {
1261
+ // Provider/model transitions may hand off only while no role turn is in
1262
+ // flight. Threshold-only changes may be attempted during a turn; an
1263
+ // adapter that cannot safely reconfigure then is retried while idle.
1264
+ void this.refreshSettings(managed, watcher.frozenAction, this.dispatchDepth === 0).catch((error) => {
1265
+ const state = this.stateFor(managed, false);
1266
+ if (state && this.dispatchDepth > 0) {
1267
+ this.options.report?.({ kind: "settings-pending", detail: sanitize(error), sample: state.latest });
1268
+ this.transition(state, "threshold_pending", watcher.frozenAction);
1269
+ return;
1270
+ }
1271
+ if (state) {
1272
+ state.capabilityFailure = `live settings revision could not be installed: ${sanitize(error)}`;
1273
+ this.options.report?.({ kind: "capability-failure", detail: state.capabilityFailure, sample: state.latest });
1274
+ this.transition(state, "threshold_pending", watcher.frozenAction);
1275
+ }
1276
+ watcher.stop();
1277
+ this.settingsWatchers.delete(managed);
1278
+ });
1279
+ const state = this.stateFor(managed, false);
1280
+ if (state)
1281
+ this.markActive(managed, state);
1282
+ }, Math.max(50, this.options.settingsPollMs ?? 250));
1283
+ timer.unref();
1284
+ watcher.stop = () => clearInterval(timer);
1285
+ this.settingsWatchers.set(managed, watcher);
1286
+ }
1287
+ beginGeneration(state, lifecycle) {
1288
+ state.generation += 1;
1289
+ state.generationId = this.generationId(state.key, state.generation);
1290
+ state.state = lifecycle;
1291
+ state.latest = { ...state.latest, thresholdGeneration: state.generation, settingsRevision: this.settings.settings_revision };
1292
+ state.retryAfterTurn = undefined;
1293
+ state.handoffAfterTurn = undefined;
1294
+ this.persist(state);
1295
+ }
1296
+ transition(state, lifecycle, frozenAction, providerEventId) {
1297
+ const changed = state.state !== lifecycle;
1298
+ state.state = lifecycle;
1299
+ this.persist(state, frozenAction, providerEventId);
1300
+ if (changed)
1301
+ this.options.report?.({ kind: "lifecycle", detail: lifecycle, sample: state.latest });
1302
+ }
1303
+ persist(state, frozenAction, providerEventId) {
582
1304
  const db = new WorkflowDb(this.options.projectDir);
583
1305
  try {
584
- const ref = adoptedAdapter.sessionRef?.();
585
- db.acknowledgeSettings({ runId: this.options.runId, role: this.options.role, providerSessionId: adoptedAdapter.sessionId(), ...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : {}), revision: next.settings_revision, acknowledgedAt: this.now().toISOString() });
1306
+ const ref = state.latest.sessionRef;
1307
+ db.upsertThresholdGeneration({
1308
+ generationId: state.generationId, runId: this.options.runId, role: this.options.role,
1309
+ providerSessionId: state.latest.providerSessionId, ...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : { sessionKey: state.key }),
1310
+ generation: state.generation, state: state.state, configuredCeilingPercent: this.threshold(),
1311
+ installedNativeTokenLimit: state.installedNativeTokenLimit, installedNativePercent: state.installedNativePercent,
1312
+ settingsRevision: this.settings.settings_revision, model: this.settings.model, latestSample: state.latest,
1313
+ ...(frozenAction ? { frozenActionDigest: sha(frozenAction) } : {}), ...(providerEventId ? { providerEventId } : {}),
1314
+ });
586
1315
  }
587
1316
  finally {
588
1317
  db.close();
589
1318
  }
590
- this.options.settingsAdopted?.(next, adoptedAdapter);
591
- this.options.report?.({ kind: "settings-adopted", detail: `adopted settings revision ${next.settings_revision}` });
592
- return adoptedAdapter;
593
1319
  }
594
- async measure(adapter, source) {
595
- const attempts = Math.max(1, this.options.contextRetries ?? 3);
596
- let usage;
597
- for (let attempt = 0; attempt < attempts; attempt++) {
598
- try {
599
- usage = await adapter.contextUsage?.();
600
- }
601
- catch {
602
- usage = undefined;
603
- }
604
- if (usage && Number.isFinite(usage.used) && (usage.percentage !== undefined || usage.maximum !== undefined))
605
- break;
1320
+ invalidateSample(managed, state) {
1321
+ state.latest = {
1322
+ ...this.measuringSample(managed, state.key, state.generation), source: "post-compact",
1323
+ observedAt: this.now().toISOString(), freshness: "measuring", compactionCount: state.compactionCount,
1324
+ };
1325
+ const db = new WorkflowDb(this.options.projectDir);
1326
+ try {
1327
+ db.recordContextSample(state.latest);
606
1328
  }
607
- if (!usage || !Number.isFinite(usage.used))
608
- throw new ContextCapabilityError(this.options.runId, this.options.role, `truthful context occupancy was unavailable after ${attempts} provider attempt(s)`);
609
- const percentage = usage.percentage ?? (usage.maximum ? usage.used / usage.maximum * 100 : undefined);
610
- if (percentage === undefined || !Number.isFinite(percentage))
611
- throw new ContextCapabilityError(this.options.runId, this.options.role, "provider context occupancy lacks a usable percentage and maximum");
612
- const ref = adapter.sessionRef?.();
1329
+ finally {
1330
+ db.close();
1331
+ }
1332
+ this.persist(state);
1333
+ }
1334
+ measuringSample(managed, key, generation) {
1335
+ const ref = managed.sessionRef?.();
613
1336
  return {
614
- version: 1, runId: this.options.runId, role: this.options.role, provider: adapter.agent,
615
- providerSessionId: adapter.sessionId(), ...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : {}), model: this.settings.model, observedAt: usage.observedAt ?? this.now().toISOString(),
616
- source, freshness: "fresh", used: usage.used, maximum: usage.maximum, percentage,
617
- settingsRevision: this.settings.settings_revision,
618
- compactionCount: adapter.sessionId() ? this.compactionCount(ref ?? adapter.sessionId()) : 0,
619
- handoffGeneration: this.handoffGeneration(),
1337
+ version: 1, runId: this.options.runId, role: this.options.role, provider: managed.agent,
1338
+ providerSessionId: managed.sessionId(), ...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : { sessionKey: key }),
1339
+ model: this.settings.model, observedAt: this.now().toISOString(), source: "provider-query", freshness: "measuring",
1340
+ settingsRevision: this.settings.settings_revision, compactionCount: 0, handoffGeneration: this.handoffGeneration(), thresholdGeneration: generation,
620
1341
  };
621
1342
  }
1343
+ result(managed, state, action) {
1344
+ return { adapter: managed, action, sample: state.latest, effectiveThreshold: this.threshold(), compactionCount: state.compactionCount };
1345
+ }
1346
+ markActive(managed, state) {
1347
+ if (managed.isClosed())
1348
+ return;
1349
+ const db = new WorkflowDb(this.options.projectDir);
1350
+ try {
1351
+ const previous = managed.noteActiveSession(state.key);
1352
+ if (previous)
1353
+ db.clearRoleAdapterActive(this.options.runId, this.options.role, previous);
1354
+ db.markRoleAdapterActive({ runId: this.options.runId, role: this.options.role, providerSessionId: managed.sessionId(), sessionKey: state.key, settingsRevision: this.settings.settings_revision, observedAt: this.now().toISOString() });
1355
+ }
1356
+ finally {
1357
+ db.close();
1358
+ }
1359
+ }
1360
+ providerSequence(adapter, state) {
1361
+ const key = adapterStateKey(adapter);
1362
+ if (state)
1363
+ return state.latestProviderSequence;
1364
+ if (!key)
1365
+ return 0;
1366
+ const db = new WorkflowDb(this.options.projectDir);
1367
+ try {
1368
+ return Math.max(0, ...db.thresholdGenerations(this.options.runId, this.options.role, key)
1369
+ .map((generation) => generation.latestSample?.sequence ?? 0));
1370
+ }
1371
+ finally {
1372
+ db.close();
1373
+ }
1374
+ }
1375
+ recordCompactionSessionUnavailable(db, managed, failure) {
1376
+ const sessionRef = managed.sessionRef?.();
1377
+ db.appendContinuityEvent({
1378
+ runId: this.options.runId,
1379
+ role: "host",
1380
+ kind: "session_unavailable",
1381
+ payload: {
1382
+ role: this.options.role,
1383
+ operation: "native-compaction",
1384
+ phase: failure.phase,
1385
+ dispatchState: failure.dispatchState ?? "unknown",
1386
+ reason: failure.availability?.reason,
1387
+ detail: failure.diagnostics,
1388
+ },
1389
+ authoritativeStateRevision: db.continuityHead(this.options.runId, this.options.role)?.authoritativeStateRevision ?? this.settings.settings_revision,
1390
+ sessionRef,
1391
+ });
1392
+ if (failure.dispatchState === "unknown")
1393
+ db.setContinuityHeadState(this.options.runId, this.options.role, "degraded");
1394
+ }
1395
+ persistedMaximum(adapter, model) {
1396
+ const key = adapterStateKey(adapter);
1397
+ if (!key)
1398
+ return undefined;
1399
+ const db = new WorkflowDb(this.options.projectDir);
1400
+ try {
1401
+ return db.thresholdGenerations(this.options.runId, this.options.role, key).filter((item) => item.model === model).at(-1)?.latestSample?.maximum;
1402
+ }
1403
+ finally {
1404
+ db.close();
1405
+ }
1406
+ }
622
1407
  compactionCount(session) {
1408
+ if (!session)
1409
+ return 0;
623
1410
  const db = new WorkflowDb(this.options.projectDir);
624
1411
  try {
625
1412
  return db.successfulCompactionCount(this.options.runId, this.options.role, session);
@@ -637,8 +1424,13 @@ export class ThresholdCompactionController {
637
1424
  db.close();
638
1425
  }
639
1426
  }
640
- threshold() { return this.runOnlyThreshold ?? this.settings.auto_compact_threshold_percent ?? 50; }
1427
+ generationId(key, generation) {
1428
+ return `threshold:${this.options.runId}:${this.options.role}:${sha(`${key}:${generation}`).slice(0, 24)}`;
1429
+ }
1430
+ threshold() { return this.settings.auto_compact_threshold_percent; }
1431
+ maximum() { return this.settings.compact_maximum; }
641
1432
  now() { return this.options.now?.() ?? new Date(); }
1433
+ capability(error) { return error instanceof ContextCapabilityError ? error : new ContextCapabilityError(this.options.runId, this.options.role, sanitize(error)); }
642
1434
  }
643
1435
  function normalizeContextSettings(settings) {
644
1436
  const threshold = settings.auto_compact_threshold_percent ?? 50;
@@ -660,3 +1452,6 @@ function adapterSessionKey(adapter) {
660
1452
  const ref = adapter.sessionRef?.();
661
1453
  return ref ? providerSessionKey(ref) : undefined;
662
1454
  }
1455
+ function adapterStateKey(adapter) {
1456
+ return adapterSessionKey(adapter) ?? (adapter.sessionId() ? `${adapter.agent}:unscoped:${adapter.sessionId()}` : undefined);
1457
+ }