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.
- package/README.md +11 -1
- package/dist/activity.js +2 -2
- package/dist/adapters/claude.d.ts +29 -5
- package/dist/adapters/claude.js +340 -23
- package/dist/adapters/codex.d.ts +18 -1
- package/dist/adapters/codex.js +207 -11
- package/dist/adapters/recovering.d.ts +6 -1
- package/dist/adapters/recovering.js +9 -1
- package/dist/adapters/types.d.ts +66 -1
- package/dist/branch/currentGuard.d.ts +5 -1
- package/dist/branch/currentGuard.js +6 -0
- package/dist/cli/start.js +159 -21
- package/dist/continuity.d.ts +8 -2
- package/dist/continuity.js +72 -22
- package/dist/handoffs.js +223 -3
- package/dist/qaReview.js +4 -0
- package/dist/sessionLifecycle.d.ts +108 -14
- package/dist/sessionLifecycle.js +1034 -239
- package/dist/statusReporter.d.ts +26 -0
- package/dist/statusReporter.js +76 -16
- package/dist/workflowDb.d.ts +47 -2
- package/dist/workflowDb.js +149 -14
- package/package.json +4 -4
package/dist/sessionLifecycle.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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 (
|
|
349
|
-
this.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
return
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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.
|
|
393
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
761
|
+
usage = await managed.underlying().contextUsage?.();
|
|
429
762
|
}
|
|
430
|
-
|
|
431
|
-
|
|
763
|
+
catch (error) {
|
|
764
|
+
lastError = sanitize(error);
|
|
432
765
|
}
|
|
433
|
-
|
|
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.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
475
|
-
|
|
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
|
-
|
|
1026
|
+
db.linkCompactionProviderEvent(state.manualAttemptKey, event.providerEventId);
|
|
478
1027
|
}
|
|
1028
|
+
catch { /* a duplicate provider event is already durably linked */ }
|
|
479
1029
|
finally {
|
|
480
|
-
|
|
1030
|
+
db.close();
|
|
481
1031
|
}
|
|
482
|
-
|
|
1032
|
+
this.nativeAttempts.set(eventKey, state.manualAttemptKey);
|
|
1033
|
+
return; // the host compact path owns success verification and accounting
|
|
483
1034
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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
|
-
|
|
1094
|
+
exceeded.finishCompactionAttempt(attempt.idempotencyKey, { ok: true, status: "unverified", error: "provider completed a vetoed over-maximum compaction" });
|
|
492
1095
|
}
|
|
493
1096
|
finally {
|
|
494
|
-
|
|
1097
|
+
exceeded.close();
|
|
495
1098
|
}
|
|
496
|
-
|
|
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
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
1124
|
+
completed.close();
|
|
506
1125
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
this.
|
|
513
|
-
|
|
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
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
|
|
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.
|
|
581
|
-
|
|
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 =
|
|
585
|
-
db.
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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:
|
|
615
|
-
providerSessionId:
|
|
616
|
-
|
|
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
|
-
|
|
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
|
+
}
|