ai-foreman 1.7.6 → 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 +26 -1
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +31 -6
- package/dist/adapters/claude.d.ts +61 -6
- package/dist/adapters/claude.js +544 -36
- package/dist/adapters/codex.d.ts +28 -1
- package/dist/adapters/codex.js +398 -27
- package/dist/adapters/recovering.d.ts +13 -2
- package/dist/adapters/recovering.js +32 -4
- package/dist/adapters/sessionFailure.d.ts +18 -0
- package/dist/adapters/sessionFailure.js +51 -0
- package/dist/adapters/types.d.ts +117 -9
- package/dist/agentRun.d.ts +4 -1
- package/dist/agentRun.js +96 -9
- package/dist/branch/currentGuard.d.ts +38 -0
- package/dist/branch/currentGuard.js +71 -0
- package/dist/branch/planner.d.ts +1 -1
- package/dist/branch/planner.js +2 -7
- package/dist/branch/prefix.d.ts +8 -0
- package/dist/branch/prefix.js +26 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +8 -0
- package/dist/branch/runner.d.ts +14 -2
- package/dist/branch/runner.js +77 -17
- package/dist/buildRuns.d.ts +13 -4
- package/dist/buildRuns.js +85 -19
- package/dist/cli/handoffs.d.ts +2 -0
- package/dist/cli/handoffs.js +74 -0
- package/dist/cli/start.js +1079 -43
- package/dist/cli/tickets.js +199 -3
- package/dist/continuity.d.ts +92 -0
- package/dist/continuity.js +415 -0
- package/dist/foreman.d.ts +5 -1
- package/dist/foreman.js +41 -6
- package/dist/handoffs.d.ts +69 -0
- package/dist/handoffs.js +546 -0
- package/dist/log.d.ts +1 -1
- package/dist/permissions/policy.d.ts +5 -1
- package/dist/permissions/policy.js +48 -1
- package/dist/qaReview.d.ts +6 -6
- package/dist/qaReview.js +24 -14
- package/dist/sessionAvailability.d.ts +15 -0
- package/dist/sessionAvailability.js +57 -0
- package/dist/sessionIdentity.d.ts +33 -0
- package/dist/sessionIdentity.js +132 -0
- package/dist/sessionLifecycle.d.ts +198 -5
- package/dist/sessionLifecycle.js +1373 -20
- package/dist/statusReporter.d.ts +84 -8
- package/dist/statusReporter.js +270 -14
- package/dist/ticketPlanning.js +8 -0
- package/dist/ticketPopulation.js +88 -12
- package/dist/tickets/commands.js +20 -12
- package/dist/tickets/groupReset.d.ts +25 -0
- package/dist/tickets/groupReset.js +279 -0
- package/dist/tickets/groups.d.ts +36 -0
- package/dist/tickets/groups.js +68 -0
- package/dist/tickets/importer.js +27 -3
- package/dist/tickets/recommendations.js +31 -7
- package/dist/tickets/reset.d.ts +1 -0
- package/dist/tickets/reset.js +1 -0
- package/dist/tickets/setupConfig.d.ts +1 -0
- package/dist/tickets/setupConfig.js +10 -2
- package/dist/tickets/stateDb.d.ts +34 -0
- package/dist/tickets/stateDb.js +207 -0
- package/dist/tickets/ticketLoader.js +5 -2
- package/dist/tickets/validate.js +4 -0
- package/dist/workflowDb.d.ts +145 -3
- package/dist/workflowDb.js +571 -10
- package/package.json +32 -4
package/dist/sessionLifecycle.js
CHANGED
|
@@ -1,27 +1,122 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { SessionUnavailableError, sessionUnavailableErrorFromFailure } from "./adapters/sessionFailure.js";
|
|
1
3
|
import { sanitizeDiagnostics } from "./runtimeReadiness.js";
|
|
4
|
+
import { WorkflowDb } from "./workflowDb.js";
|
|
5
|
+
import { providerSessionKey } from "./sessionIdentity.js";
|
|
6
|
+
import { AsyncQueue } from "./util/asyncQueue.js";
|
|
7
|
+
export class RoleSessionValidationError extends Error {
|
|
8
|
+
role;
|
|
9
|
+
availability;
|
|
10
|
+
constructor(role, availability) {
|
|
11
|
+
super(`exact ${role} session is ${availability.status} (${availability.reason ?? "validation failed"}): ${availability.detail ?? "no provider detail"}`);
|
|
12
|
+
this.role = role;
|
|
13
|
+
this.availability = availability;
|
|
14
|
+
this.name = "RoleSessionValidationError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
2
17
|
/** One lifecycle implementation shared by interviews, builds, QA, and recovery. */
|
|
3
18
|
export class RoleSessionController {
|
|
4
19
|
options;
|
|
5
20
|
adapter;
|
|
6
21
|
workSessions = 0;
|
|
22
|
+
thresholdController;
|
|
7
23
|
constructor(options) {
|
|
8
24
|
this.options = options;
|
|
9
25
|
}
|
|
10
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Production facade: the role owner authorizes scoped adapters and validated
|
|
28
|
+
* successors while the threshold controller remains delegated policy and
|
|
29
|
+
* accounting. Compatibility callers may still construct the threshold
|
|
30
|
+
* controller directly.
|
|
31
|
+
*/
|
|
32
|
+
static managed(options) {
|
|
33
|
+
const controller = new RoleSessionController({
|
|
34
|
+
role: options.role,
|
|
35
|
+
settings: options.initialSettings,
|
|
36
|
+
create: async () => { throw new Error("managed role sessions create adapters only through validated lifecycle callbacks"); },
|
|
37
|
+
});
|
|
38
|
+
const settingsBoundary = options.settingsBoundary
|
|
39
|
+
? async (input) => {
|
|
40
|
+
const predecessor = controller.captureManagedIdentity(input.adapter);
|
|
41
|
+
const successor = await options.settingsBoundary(input);
|
|
42
|
+
return controller.acceptManagedTransition(predecessor, successor, "settings");
|
|
43
|
+
}
|
|
44
|
+
: undefined;
|
|
45
|
+
const handoff = options.handoff
|
|
46
|
+
? async (input) => {
|
|
47
|
+
const predecessor = controller.captureManagedIdentity(input.adapter);
|
|
48
|
+
const successor = await options.handoff(input);
|
|
49
|
+
return controller.acceptManagedTransition(predecessor, successor, "handoff");
|
|
50
|
+
}
|
|
51
|
+
: undefined;
|
|
52
|
+
controller.thresholdController = new ThresholdCompactionController({ ...options, settingsBoundary, handoff });
|
|
53
|
+
return controller;
|
|
54
|
+
}
|
|
55
|
+
async atSafeBoundary(adapter, frozenAction) {
|
|
56
|
+
if (!this.thresholdController)
|
|
57
|
+
throw new Error("this role session controller does not own threshold lifecycle policy");
|
|
58
|
+
await this.adoptManagedAdapter(adapter);
|
|
59
|
+
const result = await this.thresholdController.atSafeBoundary(adapter, frozenAction);
|
|
60
|
+
this.adapter = result.adapter;
|
|
61
|
+
return result;
|
|
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(); }
|
|
72
|
+
async atWorkSessionBoundary(adapter, frozenAction, strategy) {
|
|
73
|
+
if (!this.thresholdController)
|
|
74
|
+
throw new Error("this role session controller does not own threshold lifecycle policy");
|
|
75
|
+
await this.adoptManagedAdapter(adapter);
|
|
76
|
+
const result = await this.thresholdController.atWorkSessionBoundary(adapter, frozenAction, strategy);
|
|
77
|
+
this.adapter = result.adapter;
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
effectiveSettings() {
|
|
81
|
+
return this.thresholdController?.effectiveSettings() ?? this.options.settings;
|
|
82
|
+
}
|
|
83
|
+
effectiveThreshold() {
|
|
84
|
+
if (!this.thresholdController)
|
|
85
|
+
return this.options.settings.auto_compact_threshold_percent ?? 50;
|
|
86
|
+
return this.thresholdController.effectiveThreshold();
|
|
87
|
+
}
|
|
88
|
+
async next(durableHandoff, nextSettings) {
|
|
11
89
|
this.workSessions += 1;
|
|
12
90
|
if (!this.adapter) {
|
|
13
91
|
if (nextSettings)
|
|
14
92
|
this.options.settings = nextSettings;
|
|
15
|
-
|
|
93
|
+
if (this.options.initialSessionId && !this.options.initialSessionRef) {
|
|
94
|
+
throw new RoleSessionValidationError(this.options.role, {
|
|
95
|
+
version: 1, status: "unknown", checkedAt: (this.options.now?.() ?? new Date()).toISOString(),
|
|
96
|
+
reason: "legacy-unscoped", detail: "a raw provider session ID has no verifiable location scope",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
this.adapter = await this.options.create(this.options.settings, this.options.initialSessionRef?.sessionId ?? this.options.initialSessionId, this.options.initialSessionRef);
|
|
100
|
+
if (this.options.initialSessionRef) {
|
|
101
|
+
const availability = await (this.adapter.validateSession?.() ?? Promise.resolve({
|
|
102
|
+
version: 1, status: "unknown", checkedAt: (this.options.now?.() ?? new Date()).toISOString(),
|
|
103
|
+
reason: "legacy-unscoped", detail: "provider adapter does not expose exact-session validation", sessionRef: this.options.initialSessionRef,
|
|
104
|
+
}));
|
|
105
|
+
if (availability.status !== "available" || !availability.sessionRef) {
|
|
106
|
+
await this.adapter.close().catch(() => { });
|
|
107
|
+
this.adapter = undefined;
|
|
108
|
+
throw new RoleSessionValidationError(this.options.role, availability);
|
|
109
|
+
}
|
|
110
|
+
this.adapter.adoptSessionRef?.(availability.sessionRef);
|
|
111
|
+
}
|
|
16
112
|
return this.boundary("initial", "started a workflow-scoped provider conversation");
|
|
17
113
|
}
|
|
18
114
|
if (nextSettings) {
|
|
19
115
|
const current = this.options.settings;
|
|
20
116
|
if (nextSettings.make !== current.make) {
|
|
21
|
-
await this.adapter.close();
|
|
22
117
|
this.options.settings = nextSettings;
|
|
23
|
-
|
|
24
|
-
return this.boundary("settings-fallback",
|
|
118
|
+
const validated = await this.moveFresh(durableHandoff, `provider changed from ${current.make} to ${nextSettings.make}`);
|
|
119
|
+
return this.boundary("settings-fallback", `${validated ? "validated cumulative handoff" : "fresh compatibility fallback"} after provider changed from ${current.make} to ${nextSettings.make}`);
|
|
25
120
|
}
|
|
26
121
|
const changed = nextSettings.model !== current.model || nextSettings.reasoning !== current.reasoning || nextSettings.fast !== current.fast;
|
|
27
122
|
this.options.settings = nextSettings;
|
|
@@ -30,55 +125,167 @@ export class RoleSessionController {
|
|
|
30
125
|
const switched = this.adapter.switchSettings
|
|
31
126
|
? await this.adapter.switchSettings({ model: nextSettings.model === "default" ? undefined : nextSettings.model, effort: effort(nextSettings.reasoning), fast: nextSettings.fast })
|
|
32
127
|
: { ok: false, error: "provider adapter does not support an in-conversation settings switch" };
|
|
128
|
+
if (switched.failure?.category === "session-unavailable")
|
|
129
|
+
throw sessionUnavailableErrorFromFailure(switched.failure);
|
|
33
130
|
if (!switched.ok) {
|
|
34
|
-
await this.adapter.close();
|
|
35
|
-
this.adapter = await this.options.create(nextSettings);
|
|
36
131
|
const detail = sanitize(switched.error ?? "settings switch failed");
|
|
37
|
-
const
|
|
38
|
-
|
|
132
|
+
const validated = await this.moveFresh(durableHandoff, `settings switch failed: ${detail}`);
|
|
133
|
+
const issue = this.issue("session_model_switch_failure", detail, validated ? "The role continued through a validated cumulative handoff." : "A validated handoff callback was unavailable; inspect continuity before proceeding.");
|
|
134
|
+
return this.boundary("settings-fallback", `settings switch failed (${detail}); ${validated ? "continued through a validated fresh successor" : "used the compatibility fresh fallback"}`, issue);
|
|
39
135
|
}
|
|
40
136
|
this.emit("settings-continued", "provider accepted the same-conversation settings switch");
|
|
41
137
|
}
|
|
42
138
|
}
|
|
43
139
|
if (this.options.settings.session_strategy === "fresh") {
|
|
44
|
-
await this.
|
|
45
|
-
|
|
46
|
-
const result = this.boundary("fresh", "started a fresh conversation with a durable handoff");
|
|
140
|
+
const validated = await this.moveFresh(durableHandoff, "ordinary session_strategy=fresh boundary");
|
|
141
|
+
const result = this.boundary("fresh", validated ? "transferred to a validated fresh successor with the durable handoff" : "started a compatibility fresh conversation without host validation");
|
|
47
142
|
return result;
|
|
48
143
|
}
|
|
49
144
|
if (!this.adapter.sessionId()) {
|
|
50
|
-
await this.
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const result = this.boundary("missing", "continuity unavailable; started fresh from the durable handoff", issue);
|
|
145
|
+
const validated = await this.moveFresh(durableHandoff, "exact provider session identity is unavailable");
|
|
146
|
+
const issue = this.issue("session_missing", "exact provider session is unavailable", validated ? "Continued through a validated cumulative handoff." : "Validated handoff support is unavailable; inspect continuity before proceeding.");
|
|
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);
|
|
54
148
|
return result;
|
|
55
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
|
+
}
|
|
56
157
|
let firstError = "";
|
|
57
158
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
58
159
|
try {
|
|
59
160
|
const compacted = this.adapter.compact
|
|
60
161
|
? await this.adapter.compact()
|
|
61
162
|
: { ok: false, error: "provider adapter does not expose native compaction" };
|
|
163
|
+
if (compacted.failure?.category === "session-unavailable")
|
|
164
|
+
throw sessionUnavailableErrorFromFailure(compacted.failure);
|
|
62
165
|
if (compacted.ok)
|
|
63
166
|
return this.boundary("compacted", attempt === 1 ? "compacted and continued the exact conversation" : "compaction retry succeeded; continued the exact conversation");
|
|
64
167
|
firstError = sanitize(compacted.error ?? "provider reported compaction failure");
|
|
65
168
|
}
|
|
66
169
|
catch (error) {
|
|
170
|
+
if (error instanceof SessionUnavailableError)
|
|
171
|
+
throw error;
|
|
67
172
|
firstError = sanitize(error);
|
|
68
173
|
}
|
|
69
174
|
if (attempt === 1)
|
|
70
175
|
this.emit("compaction-retry", `compaction failed (${firstError}); retrying once`);
|
|
71
176
|
}
|
|
72
|
-
await this.
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const result = this.boundary("compaction-fallback", `compaction failed twice (${firstError}); started fresh and continuity was lost`, issue);
|
|
177
|
+
const validated = await this.moveFresh(durableHandoff, `native compaction failed twice: ${firstError}`);
|
|
178
|
+
const issue = this.issue("session_compaction_failure", firstError, validated ? "Inspect provider health; the role continued through a validated cumulative handoff." : "Validated handoff support is unavailable; inspect continuity before proceeding.");
|
|
179
|
+
const result = this.boundary("compaction-fallback", `compaction failed twice (${firstError}); ${validated ? "continued through a validated fresh successor" : "used the compatibility fresh fallback"}`, issue);
|
|
76
180
|
return result;
|
|
77
181
|
}
|
|
78
182
|
current() { return this.adapter; }
|
|
79
183
|
count() { return this.workSessions; }
|
|
80
184
|
async usage() { return this.adapter?.contextUsage?.(); }
|
|
81
185
|
async close() { await this.adapter?.close(); this.adapter = undefined; }
|
|
186
|
+
async moveFresh(durableHandoff, reason) {
|
|
187
|
+
const predecessor = this.adapter;
|
|
188
|
+
if (this.options.handoff) {
|
|
189
|
+
const successor = await this.options.handoff({ predecessor, durableHandoff, reason, settings: this.options.settings });
|
|
190
|
+
const predecessorRef = predecessor.sessionRef?.();
|
|
191
|
+
const successorRef = successor.sessionRef?.();
|
|
192
|
+
if (predecessorRef && (!successorRef || providerSessionKey(predecessorRef) === providerSessionKey(successorRef))) {
|
|
193
|
+
if (successor !== predecessor)
|
|
194
|
+
await successor.close().catch(() => { });
|
|
195
|
+
throw new Error("validated handoff did not return a genuinely fresh location-scoped successor");
|
|
196
|
+
}
|
|
197
|
+
this.adapter = successor;
|
|
198
|
+
if (successor !== predecessor)
|
|
199
|
+
await predecessor.close().catch(() => { });
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
await predecessor.close();
|
|
203
|
+
this.adapter = await this.options.create(this.options.settings);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
async adoptManagedAdapter(adapter) {
|
|
207
|
+
const sessionId = adapter.sessionId();
|
|
208
|
+
let ref = adapter.sessionRef?.();
|
|
209
|
+
if (sessionId && !ref) {
|
|
210
|
+
throw new RoleSessionValidationError(this.options.role, {
|
|
211
|
+
version: 1,
|
|
212
|
+
status: "unknown",
|
|
213
|
+
checkedAt: (this.options.now?.() ?? new Date()).toISOString(),
|
|
214
|
+
reason: "legacy-unscoped",
|
|
215
|
+
detail: "a live provider session ID has no location-scoped reference",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (ref?.role !== undefined && ref.role !== this.options.role) {
|
|
219
|
+
throw new RoleSessionValidationError(this.options.role, {
|
|
220
|
+
version: 1,
|
|
221
|
+
status: "unavailable",
|
|
222
|
+
checkedAt: (this.options.now?.() ?? new Date()).toISOString(),
|
|
223
|
+
reason: "role-mismatch",
|
|
224
|
+
detail: `provider session belongs to role ${ref.role}, not ${this.options.role}`,
|
|
225
|
+
sessionRef: ref,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (ref?.source === "legacy-inferred" && !ref.validatedAt) {
|
|
229
|
+
const availability = await (adapter.validateSession?.() ?? Promise.resolve({
|
|
230
|
+
version: 1,
|
|
231
|
+
status: "unknown",
|
|
232
|
+
checkedAt: (this.options.now?.() ?? new Date()).toISOString(),
|
|
233
|
+
reason: "legacy-unscoped",
|
|
234
|
+
detail: "provider adapter does not expose exact-session validation",
|
|
235
|
+
sessionRef: ref,
|
|
236
|
+
}));
|
|
237
|
+
if (availability.status !== "available" || !availability.sessionRef)
|
|
238
|
+
throw new RoleSessionValidationError(this.options.role, availability);
|
|
239
|
+
ref = availability.sessionRef;
|
|
240
|
+
adapter.adoptSessionRef?.(ref);
|
|
241
|
+
}
|
|
242
|
+
this.adapter = adapter;
|
|
243
|
+
}
|
|
244
|
+
captureManagedIdentity(adapter) {
|
|
245
|
+
const sessionRef = adapter.sessionRef?.();
|
|
246
|
+
return {
|
|
247
|
+
...(adapter.sessionId() ? { sessionId: adapter.sessionId() } : {}),
|
|
248
|
+
...(sessionRef ? { sessionRef, sessionKey: providerSessionKey(sessionRef) } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
acceptManagedTransition(predecessor, successor, kind) {
|
|
252
|
+
const successorRef = successor.sessionRef?.();
|
|
253
|
+
const successorId = successor.sessionId();
|
|
254
|
+
if (!successorId || !successorRef) {
|
|
255
|
+
throw new RoleSessionValidationError(this.options.role, {
|
|
256
|
+
version: 1,
|
|
257
|
+
status: "unknown",
|
|
258
|
+
checkedAt: (this.options.now?.() ?? new Date()).toISOString(),
|
|
259
|
+
reason: "legacy-unscoped",
|
|
260
|
+
detail: `${kind} transition did not return a location-scoped successor`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (successorRef.role !== this.options.role) {
|
|
264
|
+
throw new RoleSessionValidationError(this.options.role, {
|
|
265
|
+
version: 1,
|
|
266
|
+
status: "unavailable",
|
|
267
|
+
checkedAt: (this.options.now?.() ?? new Date()).toISOString(),
|
|
268
|
+
reason: "role-mismatch",
|
|
269
|
+
detail: `${kind} successor belongs to role ${successorRef.role}`,
|
|
270
|
+
sessionRef: successorRef,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const successorKey = providerSessionKey(successorRef);
|
|
274
|
+
const changed = predecessor.sessionKey !== successorKey;
|
|
275
|
+
if (kind === "handoff" && !changed)
|
|
276
|
+
throw new Error("validated handoff reused the predecessor's scoped provider session");
|
|
277
|
+
if (changed) {
|
|
278
|
+
if (!successorRef.validatedAt)
|
|
279
|
+
throw new Error(`${kind} successor was not validated before adoption`);
|
|
280
|
+
}
|
|
281
|
+
if (changed && predecessor.sessionRef) {
|
|
282
|
+
if (successorRef.generation <= predecessor.sessionRef.generation) {
|
|
283
|
+
throw new Error(`${kind} successor generation did not advance after validated acceptance`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
this.adapter = successor;
|
|
287
|
+
return successor;
|
|
288
|
+
}
|
|
82
289
|
boundary(kind, message, issue) {
|
|
83
290
|
const transition = this.emit(kind, message);
|
|
84
291
|
return { adapter: this.adapter, transition, issue };
|
|
@@ -96,9 +303,1155 @@ export class RoleSessionController {
|
|
|
96
303
|
};
|
|
97
304
|
}
|
|
98
305
|
}
|
|
306
|
+
export class ContextCapabilityError extends Error {
|
|
307
|
+
runId;
|
|
308
|
+
role;
|
|
309
|
+
constructor(runId, role, message) {
|
|
310
|
+
super(`${message}; new ${role} work is paused because truthful context occupancy is required for run ${runId}`);
|
|
311
|
+
this.runId = runId;
|
|
312
|
+
this.role = role;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
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. */
|
|
406
|
+
export class ThresholdCompactionController {
|
|
407
|
+
options;
|
|
408
|
+
settings;
|
|
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;
|
|
419
|
+
constructor(options) {
|
|
420
|
+
this.options = options;
|
|
421
|
+
this.settings = normalizeContextSettings(options.initialSettings);
|
|
422
|
+
this.historicalCountUncertain = Boolean(options.historicalCountUncertain);
|
|
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
|
+
}
|
|
434
|
+
async atSafeBoundary(adapter, frozenAction) {
|
|
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;
|
|
538
|
+
}
|
|
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");
|
|
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();
|
|
599
|
+
const db = new WorkflowDb(this.options.projectDir);
|
|
600
|
+
try {
|
|
601
|
+
if (sessionKey)
|
|
602
|
+
db.clearRoleAdapterActive(this.options.runId, this.options.role, sessionKey);
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
db.close();
|
|
606
|
+
}
|
|
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}%`);
|
|
680
|
+
}
|
|
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;
|
|
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;
|
|
716
|
+
}
|
|
717
|
+
finally {
|
|
718
|
+
db.close();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
reconcileIncompleteCompaction(managed, state) {
|
|
722
|
+
const db = new WorkflowDb(this.options.projectDir);
|
|
723
|
+
try {
|
|
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
|
+
}
|
|
743
|
+
}
|
|
744
|
+
finally {
|
|
745
|
+
db.close();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
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;
|
|
760
|
+
try {
|
|
761
|
+
usage = await managed.underlying().contextUsage?.();
|
|
762
|
+
}
|
|
763
|
+
catch (error) {
|
|
764
|
+
lastError = sanitize(error);
|
|
765
|
+
}
|
|
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));
|
|
774
|
+
}
|
|
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;
|
|
780
|
+
try {
|
|
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");
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
state.capabilityFailure = `context measurement failed after provider action: ${sanitize(error)}`;
|
|
787
|
+
this.persist(state, frozenAction);
|
|
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");
|
|
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;
|
|
916
|
+
try {
|
|
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());
|
|
960
|
+
}
|
|
961
|
+
finally {
|
|
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");
|
|
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);
|
|
1006
|
+
}
|
|
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);
|
|
1025
|
+
try {
|
|
1026
|
+
db.linkCompactionProviderEvent(state.manualAttemptKey, event.providerEventId);
|
|
1027
|
+
}
|
|
1028
|
+
catch { /* a duplicate provider event is already durably linked */ }
|
|
1029
|
+
finally {
|
|
1030
|
+
db.close();
|
|
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;
|
|
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;
|
|
1063
|
+
}
|
|
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);
|
|
1093
|
+
try {
|
|
1094
|
+
exceeded.finishCompactionAttempt(attempt.idempotencyKey, { ok: true, status: "unverified", error: "provider completed a vetoed over-maximum compaction" });
|
|
1095
|
+
}
|
|
1096
|
+
finally {
|
|
1097
|
+
exceeded.close();
|
|
1098
|
+
}
|
|
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;
|
|
1103
|
+
}
|
|
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);
|
|
1117
|
+
try {
|
|
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());
|
|
1122
|
+
}
|
|
1123
|
+
finally {
|
|
1124
|
+
completed.close();
|
|
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");
|
|
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");
|
|
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);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
generationTerminalAttempts(managed, state) {
|
|
1148
|
+
const session = managed.sessionRef?.() ?? managed.sessionId();
|
|
1149
|
+
if (!session)
|
|
1150
|
+
return 0;
|
|
1151
|
+
const db = new WorkflowDb(this.options.projectDir);
|
|
1152
|
+
try {
|
|
1153
|
+
return db.compactionAttempts(this.options.runId, this.options.role, session)
|
|
1154
|
+
.filter((attempt) => attempt.thresholdGenerationId === state.generationId && attempt.status !== "started")
|
|
1155
|
+
.length;
|
|
1156
|
+
}
|
|
1157
|
+
finally {
|
|
1158
|
+
db.close();
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
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;
|
|
1170
|
+
const candidate = this.options.readSettings?.();
|
|
1171
|
+
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;
|
|
1176
|
+
const next = normalizeContextSettings(candidate);
|
|
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);
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
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
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
const adoptedState = this.stateFor(managed, false);
|
|
1228
|
+
if (adoptedState?.capabilityFailure) {
|
|
1229
|
+
adoptedState.capabilityFailure = undefined;
|
|
1230
|
+
this.persist(adoptedState, frozenAction);
|
|
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;
|
|
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) {
|
|
1304
|
+
const db = new WorkflowDb(this.options.projectDir);
|
|
1305
|
+
try {
|
|
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
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
finally {
|
|
1317
|
+
db.close();
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
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);
|
|
1328
|
+
}
|
|
1329
|
+
finally {
|
|
1330
|
+
db.close();
|
|
1331
|
+
}
|
|
1332
|
+
this.persist(state);
|
|
1333
|
+
}
|
|
1334
|
+
measuringSample(managed, key, generation) {
|
|
1335
|
+
const ref = managed.sessionRef?.();
|
|
1336
|
+
return {
|
|
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,
|
|
1341
|
+
};
|
|
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
|
+
}
|
|
1407
|
+
compactionCount(session) {
|
|
1408
|
+
if (!session)
|
|
1409
|
+
return 0;
|
|
1410
|
+
const db = new WorkflowDb(this.options.projectDir);
|
|
1411
|
+
try {
|
|
1412
|
+
return db.successfulCompactionCount(this.options.runId, this.options.role, session);
|
|
1413
|
+
}
|
|
1414
|
+
finally {
|
|
1415
|
+
db.close();
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
handoffGeneration() {
|
|
1419
|
+
const db = new WorkflowDb(this.options.projectDir);
|
|
1420
|
+
try {
|
|
1421
|
+
return db.handoffs(this.options.runId).at(-1)?.generation ?? 0;
|
|
1422
|
+
}
|
|
1423
|
+
finally {
|
|
1424
|
+
db.close();
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
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; }
|
|
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)); }
|
|
1434
|
+
}
|
|
1435
|
+
function normalizeContextSettings(settings) {
|
|
1436
|
+
const threshold = settings.auto_compact_threshold_percent ?? 50;
|
|
1437
|
+
const maximum = settings.compact_maximum ?? 10;
|
|
1438
|
+
if (!Number.isInteger(threshold) || threshold < 1 || threshold > 99)
|
|
1439
|
+
throw new Error("auto_compact_threshold_percent must be an integer from 1 to 99");
|
|
1440
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1)
|
|
1441
|
+
throw new Error("compact_maximum must be a positive safe integer");
|
|
1442
|
+
return { ...settings, display_session_cost: settings.display_session_cost ?? false, auto_compact_threshold_percent: threshold, compact_maximum: maximum };
|
|
1443
|
+
}
|
|
1444
|
+
function sha(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
99
1445
|
function effort(value) {
|
|
100
1446
|
return ["low", "medium", "high", "xhigh"].includes(value) ? value : undefined;
|
|
101
1447
|
}
|
|
102
1448
|
function sanitize(error) {
|
|
103
1449
|
return sanitizeDiagnostics(error instanceof Error ? error.message : String(error)).slice(0, 1000);
|
|
104
1450
|
}
|
|
1451
|
+
function adapterSessionKey(adapter) {
|
|
1452
|
+
const ref = adapter.sessionRef?.();
|
|
1453
|
+
return ref ? providerSessionKey(ref) : undefined;
|
|
1454
|
+
}
|
|
1455
|
+
function adapterStateKey(adapter) {
|
|
1456
|
+
return adapterSessionKey(adapter) ?? (adapter.sessionId() ? `${adapter.agent}:unscoped:${adapter.sessionId()}` : undefined);
|
|
1457
|
+
}
|