ai-foreman 1.7.8 → 1.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -11
- package/dist/activity.js +2 -2
- package/dist/adapters/claude.d.ts +13 -29
- package/dist/adapters/claude.js +76 -334
- package/dist/adapters/codex.d.ts +13 -18
- package/dist/adapters/codex.js +103 -195
- package/dist/adapters/recovering.d.ts +5 -6
- package/dist/adapters/recovering.js +5 -9
- package/dist/adapters/types.d.ts +24 -66
- package/dist/branch/currentGuard.d.ts +5 -5
- package/dist/branch/currentGuard.js +4 -6
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +2 -1
- package/dist/cli/start.js +90 -165
- package/dist/continuity.d.ts +6 -8
- package/dist/continuity.js +26 -72
- package/dist/foreman.d.ts +7 -1
- package/dist/foreman.js +25 -1
- package/dist/handoffs.js +3 -223
- package/dist/qaReview.d.ts +2 -0
- package/dist/qaReview.js +1 -4
- package/dist/sessionLifecycle.d.ts +19 -108
- package/dist/sessionLifecycle.js +310 -1024
- package/dist/statusReporter.d.ts +5 -27
- package/dist/statusReporter.js +20 -76
- package/dist/workflowDb.d.ts +10 -47
- package/dist/workflowDb.js +41 -146
- package/package.json +4 -4
package/dist/sessionLifecycle.js
CHANGED
|
@@ -3,7 +3,6 @@ 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";
|
|
7
6
|
export class RoleSessionValidationError extends Error {
|
|
8
7
|
role;
|
|
9
8
|
availability;
|
|
@@ -60,15 +59,6 @@ export class RoleSessionController {
|
|
|
60
59
|
this.adapter = result.adapter;
|
|
61
60
|
return result;
|
|
62
61
|
}
|
|
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(); }
|
|
72
62
|
async atWorkSessionBoundary(adapter, frozenAction, strategy) {
|
|
73
63
|
if (!this.thresholdController)
|
|
74
64
|
throw new Error("this role session controller does not own threshold lifecycle policy");
|
|
@@ -77,6 +67,13 @@ export class RoleSessionController {
|
|
|
77
67
|
this.adapter = result.adapter;
|
|
78
68
|
return result;
|
|
79
69
|
}
|
|
70
|
+
/** Record provider-native automatic compaction immediately after a role turn. */
|
|
71
|
+
async observeNativeCompactions(adapter) {
|
|
72
|
+
if (!this.thresholdController)
|
|
73
|
+
throw new Error("this role session controller does not own threshold lifecycle policy");
|
|
74
|
+
await this.adoptManagedAdapter(adapter);
|
|
75
|
+
return this.thresholdController.observeNativeCompactions(adapter);
|
|
76
|
+
}
|
|
80
77
|
effectiveSettings() {
|
|
81
78
|
return this.thresholdController?.effectiveSettings() ?? this.options.settings;
|
|
82
79
|
}
|
|
@@ -147,13 +144,6 @@ export class RoleSessionController {
|
|
|
147
144
|
const result = this.boundary("missing", validated ? "exact session unavailable; transferred to a validated fresh successor" : "exact session unavailable; used the compatibility fresh fallback", issue);
|
|
148
145
|
return result;
|
|
149
146
|
}
|
|
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
|
-
}
|
|
157
147
|
let firstError = "";
|
|
158
148
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
159
149
|
try {
|
|
@@ -312,1101 +302,405 @@ export class ContextCapabilityError extends Error {
|
|
|
312
302
|
this.role = role;
|
|
313
303
|
}
|
|
314
304
|
}
|
|
315
|
-
/**
|
|
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. */
|
|
305
|
+
/** Threshold-triggered native compaction at host-owned safe boundaries. */
|
|
406
306
|
export class ThresholdCompactionController {
|
|
407
307
|
options;
|
|
408
308
|
settings;
|
|
309
|
+
armed = true;
|
|
310
|
+
crossing = 0;
|
|
311
|
+
seenSessions = new Set();
|
|
312
|
+
runOnlyThreshold;
|
|
313
|
+
bootstrapScheduled = false;
|
|
314
|
+
bootstrapRecoveryDepth = 0;
|
|
409
315
|
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;
|
|
419
316
|
constructor(options) {
|
|
420
317
|
this.options = options;
|
|
421
318
|
this.settings = normalizeContextSettings(options.initialSettings);
|
|
422
319
|
this.historicalCountUncertain = Boolean(options.historicalCountUncertain);
|
|
423
320
|
}
|
|
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
|
-
}
|
|
434
321
|
async atSafeBoundary(adapter, frozenAction) {
|
|
435
|
-
|
|
436
|
-
this.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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;
|
|
322
|
+
adapter = await this.adoptSettings(adapter, frozenAction);
|
|
323
|
+
if (!adapter.sessionId() && !this.bootstrapScheduled) {
|
|
324
|
+
this.bootstrapScheduled = true;
|
|
325
|
+
const sample = {
|
|
326
|
+
version: 1, runId: this.options.runId, role: this.options.role, provider: adapter.agent,
|
|
327
|
+
model: this.settings.model, observedAt: this.now().toISOString(), source: "provider-query", freshness: "measuring",
|
|
328
|
+
settingsRevision: this.settings.settings_revision, compactionCount: 0, handoffGeneration: this.handoffGeneration(),
|
|
329
|
+
};
|
|
330
|
+
const db = new WorkflowDb(this.options.projectDir);
|
|
331
|
+
try {
|
|
332
|
+
db.recordContextSample(sample);
|
|
538
333
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
return this.
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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");
|
|
569
|
-
}
|
|
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();
|
|
334
|
+
finally {
|
|
335
|
+
db.close();
|
|
336
|
+
}
|
|
337
|
+
return { adapter, action: "continued", sample, effectiveThreshold: this.threshold(), compactionCount: 0 };
|
|
338
|
+
}
|
|
339
|
+
// Drain first so a provider-confirmed compaction is persisted with a
|
|
340
|
+
// measurement taken after the event, regardless of provider event order.
|
|
341
|
+
const nativeCount = await this.observeNativeCompactions(adapter);
|
|
342
|
+
let sample = await this.measure(adapter, "provider-query");
|
|
343
|
+
const threshold = this.threshold();
|
|
344
|
+
let session = adapter.sessionRef?.() ?? adapter.sessionId();
|
|
345
|
+
let sessionKey = adapterSessionKey(adapter);
|
|
346
|
+
const firstObservedSession = Boolean(sessionKey && !this.seenSessions.has(sessionKey));
|
|
347
|
+
if (sessionKey)
|
|
348
|
+
this.seenSessions.add(sessionKey);
|
|
599
349
|
const db = new WorkflowDb(this.options.projectDir);
|
|
350
|
+
let count = 0;
|
|
600
351
|
try {
|
|
601
|
-
|
|
602
|
-
|
|
352
|
+
db.recordContextSample(sample);
|
|
353
|
+
count = nativeCount ?? db.successfulCompactionCount(this.options.runId, this.options.role, session);
|
|
603
354
|
}
|
|
604
355
|
finally {
|
|
605
356
|
db.close();
|
|
606
357
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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}%`);
|
|
358
|
+
// An interrupted dispatched turn leaves both conversation state and native
|
|
359
|
+
// compaction accounting unknowable. Do not continue that exact session,
|
|
360
|
+
// even if its next occupancy sample happens to be below threshold.
|
|
361
|
+
if (this.historicalCountUncertain) {
|
|
362
|
+
return this.performHandoff(adapter, sample, count, frozenAction, "a prior provider turn lacked a durable completion checkpoint; handing off rather than reusing an uncertain session");
|
|
363
|
+
}
|
|
364
|
+
if ((sample.percentage ?? -1) < threshold) {
|
|
365
|
+
if (nativeCount !== undefined && count >= (this.settings.compact_maximum ?? 10))
|
|
366
|
+
return this.performHandoff(adapter, sample, count, frozenAction, `compact maximum ${this.settings.compact_maximum ?? 10} reached after provider-native compaction`);
|
|
367
|
+
this.armed = true;
|
|
368
|
+
return { adapter, action: "below-threshold", sample, effectiveThreshold: threshold, compactionCount: count };
|
|
369
|
+
}
|
|
370
|
+
if (!this.armed)
|
|
371
|
+
return { adapter, action: "continued", sample, effectiveThreshold: threshold, compactionCount: count };
|
|
372
|
+
this.armed = false;
|
|
373
|
+
this.crossing += 1;
|
|
374
|
+
let freshBootstrap = firstObservedSession;
|
|
375
|
+
// A settings command published while the action settled is newer intent.
|
|
376
|
+
const revisionBeforeSettle = this.settings.settings_revision;
|
|
377
|
+
const sessionBeforeSettle = adapterSessionKey(adapter);
|
|
378
|
+
adapter = await this.adoptSettings(adapter, frozenAction);
|
|
379
|
+
if (this.settings.settings_revision !== revisionBeforeSettle || adapterSessionKey(adapter) !== sessionBeforeSettle) {
|
|
380
|
+
sample = await this.measure(adapter, "provider-query");
|
|
381
|
+
session = adapter.sessionRef?.() ?? adapter.sessionId();
|
|
382
|
+
sessionKey = adapterSessionKey(adapter);
|
|
383
|
+
freshBootstrap = Boolean(sessionKey && !this.seenSessions.has(sessionKey));
|
|
384
|
+
if (sessionKey)
|
|
385
|
+
this.seenSessions.add(sessionKey);
|
|
386
|
+
const refreshed = new WorkflowDb(this.options.projectDir);
|
|
387
|
+
try {
|
|
388
|
+
refreshed.recordContextSample(sample);
|
|
389
|
+
count = refreshed.successfulCompactionCount(this.options.runId, this.options.role, adapter.sessionRef?.() ?? adapter.sessionId());
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
refreshed.close();
|
|
680
393
|
}
|
|
681
|
-
return prepared;
|
|
682
394
|
}
|
|
683
|
-
const
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
395
|
+
const revisedThreshold = this.threshold();
|
|
396
|
+
if ((sample.percentage ?? -1) < revisedThreshold) {
|
|
397
|
+
this.armed = true;
|
|
398
|
+
return { adapter, action: "continued", sample, effectiveThreshold: revisedThreshold, compactionCount: count };
|
|
399
|
+
}
|
|
400
|
+
const crossingKey = `${sessionKey ?? "missing"}:${this.crossing}:${revisedThreshold}`;
|
|
401
|
+
return this.compactSession(adapter, sample, count, frozenAction, crossingKey, true, freshBootstrap);
|
|
688
402
|
}
|
|
689
|
-
|
|
403
|
+
/** Apply the configured ordinary work-session boundary without bypassing handoff or compaction accounting. */
|
|
404
|
+
async atWorkSessionBoundary(adapter, frozenAction, strategy = this.settings.session_strategy) {
|
|
405
|
+
adapter = await this.adoptSettings(adapter, frozenAction);
|
|
406
|
+
const nativeCount = await this.observeNativeCompactions(adapter);
|
|
407
|
+
const sample = await this.measure(adapter, "provider-query");
|
|
690
408
|
const db = new WorkflowDb(this.options.projectDir);
|
|
409
|
+
let count = 0;
|
|
691
410
|
try {
|
|
692
|
-
|
|
693
|
-
|
|
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;
|
|
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;
|
|
411
|
+
db.recordContextSample(sample);
|
|
412
|
+
count = nativeCount ?? db.successfulCompactionCount(this.options.runId, this.options.role, adapter.sessionRef?.() ?? adapter.sessionId());
|
|
716
413
|
}
|
|
717
414
|
finally {
|
|
718
415
|
db.close();
|
|
719
416
|
}
|
|
417
|
+
if (strategy === "fresh") {
|
|
418
|
+
return this.performHandoff(adapter, sample, count, frozenAction, "ordinary session_strategy=fresh boundary requires a validated fresh successor");
|
|
419
|
+
}
|
|
420
|
+
const crossingKey = `${adapterSessionKey(adapter) ?? "missing"}:ordinary:${count + 1}:${sha(frozenAction).slice(0, 20)}`;
|
|
421
|
+
return this.compactSession(adapter, sample, count, frozenAction, crossingKey, false, false);
|
|
720
422
|
}
|
|
721
|
-
|
|
423
|
+
effectiveSettings() { return this.settings; }
|
|
424
|
+
effectiveThreshold() { return this.threshold(); }
|
|
425
|
+
/** Persist provider-triggered compactions at a host-owned boundary. */
|
|
426
|
+
async observeNativeCompactions(adapter, afterSample) {
|
|
427
|
+
const observed = adapter.drainNativeCompactions?.() ?? [];
|
|
428
|
+
if (observed.length === 0)
|
|
429
|
+
return undefined;
|
|
430
|
+
const sessionId = adapter.sessionId();
|
|
431
|
+
const sessionRef = adapter.sessionRef?.();
|
|
432
|
+
if (!sessionId || !sessionRef) {
|
|
433
|
+
adapter.restoreNativeCompactions?.(observed);
|
|
434
|
+
throw new ContextCapabilityError(this.options.runId, this.options.role, "provider reported automatic compaction without a validated session identity");
|
|
435
|
+
}
|
|
436
|
+
const sessionKey = providerSessionKey(sessionRef);
|
|
722
437
|
const db = new WorkflowDb(this.options.projectDir);
|
|
723
438
|
try {
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
439
|
+
const keys = [];
|
|
440
|
+
for (const compaction of observed) {
|
|
441
|
+
const idempotencyKey = `native-compact:${this.options.runId}:${this.options.role}:${sha(`${sessionKey}:${compaction.id}`).slice(0, 32)}`;
|
|
442
|
+
keys.push(idempotencyKey);
|
|
443
|
+
const prior = db.startCompactionAttempt({
|
|
444
|
+
idempotencyKey, runId: this.options.runId, role: this.options.role,
|
|
445
|
+
providerSessionId: sessionId, sessionRef, sessionKey,
|
|
446
|
+
crossingKey: `provider-native:${compaction.provider}:${compaction.id}`,
|
|
447
|
+
});
|
|
448
|
+
// Confirmation, not occupancy telemetry, is authoritative for the
|
|
449
|
+
// count. Persist it first so an out-of-order or unavailable usage
|
|
450
|
+
// event cannot lose a completed provider compaction.
|
|
451
|
+
if (prior.status === "started")
|
|
452
|
+
db.finishCompactionAttempt(idempotencyKey, { ok: true });
|
|
729
453
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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");
|
|
454
|
+
const usage = afterSample ? undefined : await adapter.contextUsageAfterNativeCompaction?.(observed.at(-1));
|
|
455
|
+
const sample = afterSample ?? (usage ? this.sampleFromUsage(adapter, usage, "post-compact") : undefined);
|
|
456
|
+
if (sample) {
|
|
457
|
+
db.recordContextSample(sample);
|
|
458
|
+
for (const key of keys)
|
|
459
|
+
db.recordCompactionAfterSample(key, sample);
|
|
742
460
|
}
|
|
461
|
+
return db.successfulCompactionCount(this.options.runId, this.options.role, sessionRef);
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
// Do not lose a provider confirmation merely because its durable receipt
|
|
465
|
+
// encountered a transient database/measurement failure.
|
|
466
|
+
adapter.restoreNativeCompactions?.(observed);
|
|
467
|
+
throw error;
|
|
743
468
|
}
|
|
744
469
|
finally {
|
|
745
470
|
db.close();
|
|
746
471
|
}
|
|
747
472
|
}
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
473
|
+
async compactSession(adapter, sample, count, frozenAction, crossingKey, thresholdCrossing, freshBootstrap) {
|
|
474
|
+
const threshold = this.threshold();
|
|
475
|
+
const sessionId = adapter.sessionId();
|
|
476
|
+
const sessionRef = adapter.sessionRef?.();
|
|
477
|
+
const sessionKey = sessionRef ? providerSessionKey(sessionRef) : undefined;
|
|
478
|
+
if (this.historicalCountUncertain)
|
|
479
|
+
return this.performHandoff(adapter, sample, count, frozenAction, "historical compaction count is uncertain; handing off rather than risking the configured maximum");
|
|
480
|
+
if (!sessionId)
|
|
481
|
+
return this.performHandoff(adapter, sample, count, frozenAction, "the provider session identity is unavailable");
|
|
482
|
+
if (!sessionRef)
|
|
483
|
+
return this.performHandoff(adapter, sample, count, frozenAction, "the provider session has no validated location-scoped identity");
|
|
484
|
+
if (count >= (this.settings.compact_maximum ?? 10))
|
|
485
|
+
return this.performHandoff(adapter, sample, count, frozenAction, `compact maximum ${this.settings.compact_maximum ?? 10} reached`);
|
|
486
|
+
if (!adapter.compact)
|
|
487
|
+
return this.performHandoff(adapter, sample, count, frozenAction, "native compaction is unavailable for this provider session");
|
|
488
|
+
const idempotencyKey = `compact:${this.options.runId}:${this.options.role}:${sha(crossingKey).slice(0, 24)}`;
|
|
489
|
+
const attemptDb = new WorkflowDb(this.options.projectDir);
|
|
490
|
+
const prior = attemptDb.startCompactionAttempt({ idempotencyKey, runId: this.options.runId, role: this.options.role, providerSessionId: sessionId, sessionRef, sessionKey, crossingKey, beforeSample: sample });
|
|
491
|
+
attemptDb.close();
|
|
492
|
+
if (prior.status === "succeeded" && prior.afterSample) {
|
|
493
|
+
const replayDb = new WorkflowDb(this.options.projectDir);
|
|
760
494
|
try {
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
catch (error) {
|
|
764
|
-
lastError = sanitize(error);
|
|
495
|
+
count = replayDb.successfulCompactionCount(this.options.runId, this.options.role, sessionRef ?? sessionId);
|
|
765
496
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (accepted)
|
|
769
|
-
return state;
|
|
770
|
-
lastError = "provider returned only a stale or out-of-order context sample";
|
|
497
|
+
finally {
|
|
498
|
+
replayDb.close();
|
|
771
499
|
}
|
|
772
|
-
|
|
773
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
500
|
+
return { adapter, action: "compacted", sample: prior.afterSample, effectiveThreshold: threshold, compactionCount: count };
|
|
774
501
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
return state;
|
|
502
|
+
this.options.report?.({ kind: "compacting", detail: thresholdCrossing
|
|
503
|
+
? `context ${sample.percentage?.toFixed(1)}% reached ${threshold}%`
|
|
504
|
+
: `ordinary compact session boundary (${count}/${this.settings.compact_maximum ?? 10})`, sample });
|
|
505
|
+
let compacted;
|
|
780
506
|
try {
|
|
781
|
-
|
|
782
|
-
state.capabilityFailure = undefined;
|
|
783
|
-
this.transition(state, (state.latest.percentage ?? 100) < this.threshold() ? "armed" : "threshold_pending");
|
|
507
|
+
compacted = await adapter.compact();
|
|
784
508
|
}
|
|
785
509
|
catch (error) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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");
|
|
510
|
+
compacted = error instanceof SessionUnavailableError
|
|
511
|
+
? { ok: false, error: error.message, failure: error.failure }
|
|
512
|
+
: { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
861
513
|
}
|
|
862
|
-
|
|
863
|
-
|
|
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;
|
|
514
|
+
if (compacted.failure?.category === "session-unavailable") {
|
|
515
|
+
const failedDb = new WorkflowDb(this.options.projectDir);
|
|
916
516
|
try {
|
|
917
|
-
compacted
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
|
|
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());
|
|
517
|
+
failedDb.finishCompactionAttempt(idempotencyKey, { ok: false, error: compacted.error ?? compacted.failure.diagnostics });
|
|
518
|
+
failedDb.appendContinuityEvent({
|
|
519
|
+
runId: this.options.runId,
|
|
520
|
+
role: "host",
|
|
521
|
+
kind: "session_unavailable",
|
|
522
|
+
payload: {
|
|
523
|
+
role: this.options.role,
|
|
524
|
+
operation: "native-compaction",
|
|
525
|
+
phase: compacted.failure.phase,
|
|
526
|
+
dispatchState: compacted.failure.dispatchState ?? "unknown",
|
|
527
|
+
reason: compacted.failure.availability?.reason,
|
|
528
|
+
detail: compacted.failure.diagnostics,
|
|
529
|
+
},
|
|
530
|
+
authoritativeStateRevision: failedDb.continuityHead(this.options.runId, this.options.role)?.authoritativeStateRevision ?? this.settings.settings_revision,
|
|
531
|
+
sessionRef,
|
|
532
|
+
});
|
|
533
|
+
if (compacted.failure.dispatchState === "unknown")
|
|
534
|
+
failedDb.setContinuityHeadState(this.options.runId, this.options.role, "degraded");
|
|
960
535
|
}
|
|
961
536
|
finally {
|
|
962
|
-
|
|
537
|
+
failedDb.close();
|
|
963
538
|
}
|
|
964
|
-
|
|
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");
|
|
969
|
-
}
|
|
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);
|
|
539
|
+
throw sessionUnavailableErrorFromFailure(compacted.failure);
|
|
1006
540
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
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);
|
|
541
|
+
if (!compacted.ok) {
|
|
542
|
+
const failedDb = new WorkflowDb(this.options.projectDir);
|
|
1025
543
|
try {
|
|
1026
|
-
|
|
544
|
+
failedDb.finishCompactionAttempt(idempotencyKey, { ok: false, error: compacted.error ?? "provider did not explicitly complete compaction" });
|
|
1027
545
|
}
|
|
1028
|
-
catch { /* a duplicate provider event is already durably linked */ }
|
|
1029
546
|
finally {
|
|
1030
|
-
|
|
1031
|
-
}
|
|
1032
|
-
this.nativeAttempts.set(eventKey, state.manualAttemptKey);
|
|
1033
|
-
return; // the host compact path owns success verification and accounting
|
|
1034
|
-
}
|
|
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;
|
|
547
|
+
failedDb.close();
|
|
1041
548
|
}
|
|
1042
|
-
|
|
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;
|
|
549
|
+
return this.performHandoff(adapter, sample, count, frozenAction, `native compaction failed: ${sanitize(compacted.error ?? "no explicit completion")}`);
|
|
1063
550
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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);
|
|
551
|
+
let after;
|
|
552
|
+
try {
|
|
553
|
+
after = await this.measure(adapter, "post-compact");
|
|
1090
554
|
}
|
|
1091
|
-
|
|
1092
|
-
const
|
|
555
|
+
catch (error) {
|
|
556
|
+
const failedDb = new WorkflowDb(this.options.projectDir);
|
|
1093
557
|
try {
|
|
1094
|
-
|
|
558
|
+
failedDb.finishCompactionAttempt(idempotencyKey, { ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
1095
559
|
}
|
|
1096
560
|
finally {
|
|
1097
|
-
|
|
561
|
+
failedDb.close();
|
|
1098
562
|
}
|
|
1099
|
-
|
|
1100
|
-
state.capabilityFailure = `provider completed native compaction ${event.providerEventId} beyond compact maximum ${this.maximum()}`;
|
|
1101
|
-
this.persist(state);
|
|
1102
|
-
return;
|
|
563
|
+
return this.performHandoff(adapter, sample, count, frozenAction, "provider reported compaction but no authoritative post-compact occupancy sample was available");
|
|
1103
564
|
}
|
|
1104
|
-
|
|
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);
|
|
565
|
+
const completedDb = new WorkflowDb(this.options.projectDir);
|
|
1117
566
|
try {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
state.compactionCount = completed.successfulCompactionCount(this.options.runId, this.options.role, managed.sessionRef?.() ?? managed.sessionId());
|
|
567
|
+
completedDb.recordContextSample(after);
|
|
568
|
+
completedDb.finishCompactionAttempt(idempotencyKey, { ok: true, afterSample: after });
|
|
569
|
+
count = completedDb.successfulCompactionCount(this.options.runId, this.options.role, sessionRef ?? sessionId);
|
|
1122
570
|
}
|
|
1123
571
|
finally {
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1126
|
-
if (!verified) {
|
|
1127
|
-
state.handoffAfterTurn = "provider-native compaction succeeded but post-compact occupancy could not be verified";
|
|
1128
|
-
this.transition(state, "compacted_unverified");
|
|
572
|
+
completedDb.close();
|
|
1129
573
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
this.
|
|
1136
|
-
|
|
1137
|
-
}
|
|
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);
|
|
574
|
+
this.options.report?.({ kind: "compacted", detail: `context reduced to ${after.percentage?.toFixed(1)}%`, sample: after });
|
|
575
|
+
if (freshBootstrap && (after.percentage ?? -1) >= threshold) {
|
|
576
|
+
const adjusted = Math.min(99, Math.ceil((after.percentage ?? 0) + 10));
|
|
577
|
+
if (adjusted <= (after.percentage ?? 0))
|
|
578
|
+
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`);
|
|
579
|
+
this.runOnlyThreshold = adjusted;
|
|
580
|
+
this.options.report?.({ kind: "threshold-adjusted", detail: `fresh bootstrap remained high; run-only threshold is now ${adjusted}%`, sample: after });
|
|
1145
581
|
}
|
|
582
|
+
this.armed = thresholdCrossing ? (after.percentage ?? 100) < this.threshold() : true;
|
|
583
|
+
return { adapter, action: "compacted", sample: after, effectiveThreshold: this.threshold(), compactionCount: count };
|
|
1146
584
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
585
|
+
async performHandoff(adapter, sample, count, frozenAction, reason) {
|
|
586
|
+
if (!this.options.handoff)
|
|
587
|
+
throw new ContextCapabilityError(this.options.runId, this.options.role, `${reason}; validated handoff is unavailable`);
|
|
588
|
+
this.options.report?.({ kind: "handoff", detail: reason, sample });
|
|
589
|
+
const successor = await this.options.handoff({ reason, adapter, sample, settings: this.settings, compactionCount: count, frozenAction });
|
|
590
|
+
const post = await this.measure(successor, "provider-query");
|
|
591
|
+
this.historicalCountUncertain = false;
|
|
592
|
+
const successorSessionId = successor.sessionId();
|
|
593
|
+
const successorSessionKey = adapterSessionKey(successor);
|
|
594
|
+
if (successorSessionKey)
|
|
595
|
+
this.seenSessions.add(successorSessionKey);
|
|
1151
596
|
const db = new WorkflowDb(this.options.projectDir);
|
|
1152
597
|
try {
|
|
1153
|
-
|
|
1154
|
-
.filter((attempt) => attempt.thresholdGenerationId === state.generationId && attempt.status !== "started")
|
|
1155
|
-
.length;
|
|
598
|
+
db.recordContextSample(post);
|
|
1156
599
|
}
|
|
1157
600
|
finally {
|
|
1158
601
|
db.close();
|
|
1159
602
|
}
|
|
603
|
+
if ((post.percentage ?? 100) >= this.threshold()) {
|
|
604
|
+
if (this.bootstrapRecoveryDepth >= 1) {
|
|
605
|
+
throw new ContextCapabilityError(this.options.runId, this.options.role, `fresh successor bootstrap remained at ${post.percentage?.toFixed(1)}% after compaction-failure recovery`);
|
|
606
|
+
}
|
|
607
|
+
this.bootstrapRecoveryDepth += 1;
|
|
608
|
+
try {
|
|
609
|
+
const bootstrapped = await this.compactSession(successor, post, 0, frozenAction, `${successorSessionKey ?? successorSessionId ?? "missing"}:fresh-bootstrap:${sha(frozenAction).slice(0, 20)}`, true, true);
|
|
610
|
+
return { ...bootstrapped, action: "handed-off" };
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
this.bootstrapRecoveryDepth -= 1;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
this.armed = true;
|
|
617
|
+
return { adapter: successor, action: "handed-off", sample: post, effectiveThreshold: this.threshold(), compactionCount: 0 };
|
|
1160
618
|
}
|
|
1161
|
-
async
|
|
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;
|
|
619
|
+
async adoptSettings(adapter, frozenAction) {
|
|
1170
620
|
const candidate = this.options.readSettings?.();
|
|
1171
621
|
if (!candidate || candidate.settings_revision <= this.settings.settings_revision)
|
|
1172
|
-
return;
|
|
1173
|
-
const lifecycle = this.stateFor(managed, false)?.state;
|
|
1174
|
-
if (lifecycle === "native_compacting" || lifecycle === "host_compacting")
|
|
1175
|
-
return;
|
|
622
|
+
return adapter;
|
|
1176
623
|
const next = normalizeContextSettings(candidate);
|
|
1177
|
-
const providerControlsChanged = next.make !== this.settings.make
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
if (
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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);
|
|
624
|
+
const providerControlsChanged = next.make !== this.settings.make
|
|
625
|
+
|| next.model !== this.settings.model
|
|
626
|
+
|| next.reasoning !== this.settings.reasoning
|
|
627
|
+
|| next.fast !== this.settings.fast;
|
|
628
|
+
const nativeCompactionChanged = next.auto_compact_threshold_percent !== this.settings.auto_compact_threshold_percent;
|
|
629
|
+
let adoptedAdapter = adapter;
|
|
630
|
+
if (providerControlsChanged) {
|
|
631
|
+
if (this.options.settingsBoundary) {
|
|
632
|
+
adoptedAdapter = await this.options.settingsBoundary({ adapter, current: this.settings, next, frozenAction });
|
|
633
|
+
}
|
|
634
|
+
else if (next.make === this.settings.make && adapter.switchSettings) {
|
|
635
|
+
const switched = await adapter.switchSettings({ model: next.model === "default" ? undefined : next.model, effort: effort(next.reasoning), fast: next.fast });
|
|
636
|
+
if (switched.failure?.category === "session-unavailable")
|
|
637
|
+
throw sessionUnavailableErrorFromFailure(switched.failure);
|
|
638
|
+
if (!switched.ok)
|
|
639
|
+
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")}`);
|
|
1205
640
|
}
|
|
1206
641
|
else {
|
|
1207
|
-
|
|
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
|
-
}
|
|
642
|
+
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`);
|
|
1226
643
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
adoptedState.capabilityFailure = undefined;
|
|
1230
|
-
this.persist(adoptedState, frozenAction);
|
|
644
|
+
if (adoptedAdapter.agent !== next.make) {
|
|
645
|
+
throw new ContextCapabilityError(this.options.runId, this.options.role, `settings revision ${next.settings_revision} requested ${next.make}, but the adopted session reports ${adoptedAdapter.agent}`);
|
|
1231
646
|
}
|
|
1232
|
-
|
|
647
|
+
}
|
|
648
|
+
if (nativeCompactionChanged && !providerControlsChanged) {
|
|
649
|
+
if (!adapter.prepareAutoCompaction)
|
|
650
|
+
throw new ContextCapabilityError(this.options.runId, this.options.role, `settings revision ${next.settings_revision} cannot apply provider-native automatic compaction`);
|
|
1233
651
|
try {
|
|
1234
|
-
|
|
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() });
|
|
652
|
+
await adapter.prepareAutoCompaction(next.auto_compact_threshold_percent);
|
|
1236
653
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
654
|
+
catch (error) {
|
|
655
|
+
throw new ContextCapabilityError(this.options.runId, this.options.role, `settings revision ${next.settings_revision} could not apply the automatic-compaction threshold: ${sanitize(error)}`);
|
|
1239
656
|
}
|
|
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
657
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
if (managed.isClosed())
|
|
1251
|
-
return;
|
|
1252
|
-
const existing = this.settingsWatchers.get(managed);
|
|
1253
|
-
if (existing) {
|
|
1254
|
-
existing.frozenAction = frozenAction;
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
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) {
|
|
658
|
+
this.settings = next;
|
|
659
|
+
this.runOnlyThreshold = undefined;
|
|
1304
660
|
const db = new WorkflowDb(this.options.projectDir);
|
|
1305
661
|
try {
|
|
1306
|
-
const ref =
|
|
1307
|
-
db.
|
|
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
|
-
});
|
|
662
|
+
const ref = adoptedAdapter.sessionRef?.();
|
|
663
|
+
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() });
|
|
1315
664
|
}
|
|
1316
665
|
finally {
|
|
1317
666
|
db.close();
|
|
1318
667
|
}
|
|
668
|
+
this.options.settingsAdopted?.(next, adoptedAdapter);
|
|
669
|
+
this.options.report?.({ kind: "settings-adopted", detail: `adopted settings revision ${next.settings_revision}` });
|
|
670
|
+
return adoptedAdapter;
|
|
1319
671
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
672
|
+
async measure(adapter, source) {
|
|
673
|
+
const attempts = Math.max(1, this.options.contextRetries ?? 3);
|
|
674
|
+
let usage;
|
|
675
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
676
|
+
try {
|
|
677
|
+
usage = await adapter.contextUsage?.();
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
usage = undefined;
|
|
681
|
+
}
|
|
682
|
+
if (usage && Number.isFinite(usage.used) && (usage.percentage !== undefined || usage.maximum !== undefined))
|
|
683
|
+
break;
|
|
1331
684
|
}
|
|
1332
|
-
|
|
685
|
+
if (!usage || !Number.isFinite(usage.used))
|
|
686
|
+
throw new ContextCapabilityError(this.options.runId, this.options.role, `truthful context occupancy was unavailable after ${attempts} provider attempt(s)`);
|
|
687
|
+
return this.sampleFromUsage(adapter, usage, source);
|
|
1333
688
|
}
|
|
1334
|
-
|
|
1335
|
-
const
|
|
689
|
+
sampleFromUsage(adapter, usage, source) {
|
|
690
|
+
const percentage = usage.percentage ?? (usage.maximum ? usage.used / usage.maximum * 100 : undefined);
|
|
691
|
+
if (percentage === undefined || !Number.isFinite(percentage))
|
|
692
|
+
throw new ContextCapabilityError(this.options.runId, this.options.role, "provider context occupancy lacks a usable percentage and maximum");
|
|
693
|
+
const ref = adapter.sessionRef?.();
|
|
1336
694
|
return {
|
|
1337
|
-
version: 1, runId: this.options.runId, role: this.options.role, provider:
|
|
1338
|
-
providerSessionId:
|
|
1339
|
-
|
|
1340
|
-
settingsRevision: this.settings.settings_revision,
|
|
695
|
+
version: 1, runId: this.options.runId, role: this.options.role, provider: adapter.agent,
|
|
696
|
+
providerSessionId: adapter.sessionId(), ...(ref ? { sessionRef: ref, sessionKey: providerSessionKey(ref) } : {}), model: this.settings.model, observedAt: usage.observedAt ?? this.now().toISOString(),
|
|
697
|
+
source, freshness: "fresh", used: usage.used, maximum: usage.maximum, percentage,
|
|
698
|
+
settingsRevision: this.settings.settings_revision,
|
|
699
|
+
compactionCount: adapter.sessionId() ? this.compactionCount(ref ?? adapter.sessionId()) : 0,
|
|
700
|
+
handoffGeneration: this.handoffGeneration(),
|
|
1341
701
|
};
|
|
1342
702
|
}
|
|
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
|
-
}
|
|
1407
703
|
compactionCount(session) {
|
|
1408
|
-
if (!session)
|
|
1409
|
-
return 0;
|
|
1410
704
|
const db = new WorkflowDb(this.options.projectDir);
|
|
1411
705
|
try {
|
|
1412
706
|
return db.successfulCompactionCount(this.options.runId, this.options.role, session);
|
|
@@ -1424,13 +718,8 @@ export class ThresholdCompactionController {
|
|
|
1424
718
|
db.close();
|
|
1425
719
|
}
|
|
1426
720
|
}
|
|
1427
|
-
|
|
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; }
|
|
721
|
+
threshold() { return this.runOnlyThreshold ?? this.settings.auto_compact_threshold_percent ?? 50; }
|
|
1432
722
|
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)); }
|
|
1434
723
|
}
|
|
1435
724
|
function normalizeContextSettings(settings) {
|
|
1436
725
|
const threshold = settings.auto_compact_threshold_percent ?? 50;
|
|
@@ -1452,6 +741,3 @@ function adapterSessionKey(adapter) {
|
|
|
1452
741
|
const ref = adapter.sessionRef?.();
|
|
1453
742
|
return ref ? providerSessionKey(ref) : undefined;
|
|
1454
743
|
}
|
|
1455
|
-
function adapterStateKey(adapter) {
|
|
1456
|
-
return adapterSessionKey(adapter) ?? (adapter.sessionId() ? `${adapter.agent}:unscoped:${adapter.sessionId()}` : undefined);
|
|
1457
|
-
}
|