ai-foreman 1.7.7 → 1.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude.d.ts +9 -1
- package/dist/adapters/claude.js +60 -1
- package/dist/adapters/codex.d.ts +13 -1
- package/dist/adapters/codex.js +113 -9
- package/dist/adapters/recovering.d.ts +5 -1
- package/dist/adapters/recovering.js +4 -0
- package/dist/adapters/types.d.ts +23 -0
- package/dist/branch/currentGuard.d.ts +5 -1
- package/dist/branch/currentGuard.js +4 -0
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +2 -1
- package/dist/cli/start.js +77 -14
- package/dist/continuity.d.ts +5 -1
- package/dist/continuity.js +4 -0
- package/dist/foreman.d.ts +7 -1
- package/dist/foreman.js +25 -1
- package/dist/qaReview.d.ts +2 -0
- package/dist/qaReview.js +1 -0
- package/dist/sessionLifecycle.d.ts +5 -0
- package/dist/sessionLifecycle.js +83 -2
- package/dist/statusReporter.d.ts +5 -1
- package/dist/statusReporter.js +4 -0
- package/dist/workflowDb.d.ts +8 -0
- package/dist/workflowDb.js +30 -0
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SDKSessionInfo, PermissionResult } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
/** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
|
|
3
3
|
export declare function requireClaudeSDK(): Promise<typeof import("@anthropic-ai/claude-agent-sdk")>;
|
|
4
|
-
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, PermissionDecision, ProviderSettingSwitch, ProviderSessionUsage, TurnResult } from "./types.js";
|
|
4
|
+
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, NativeCompaction, CompactResult, ContextUsage, PermissionDecision, ProviderSettingSwitch, ProviderSessionUsage, TurnResult } from "./types.js";
|
|
5
5
|
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
6
6
|
/**
|
|
7
7
|
* Pure function: build the `options` object passed to `query()`.
|
|
@@ -55,6 +55,11 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
55
55
|
private structuredError?;
|
|
56
56
|
private apiErrorStatus?;
|
|
57
57
|
private compactResult?;
|
|
58
|
+
private autoCompactionPrepared;
|
|
59
|
+
private preparedAutoCompactThreshold?;
|
|
60
|
+
private manualCompactionInFlight;
|
|
61
|
+
private nativeCompactions;
|
|
62
|
+
private nativeCompactionSequence;
|
|
58
63
|
private cumulativeUsage;
|
|
59
64
|
private pending?;
|
|
60
65
|
private terminalResult?;
|
|
@@ -73,6 +78,9 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
73
78
|
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
74
79
|
validateSession(): Promise<SessionAvailabilityV1>;
|
|
75
80
|
compact(): Promise<CompactResult>;
|
|
81
|
+
prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<void>;
|
|
82
|
+
drainNativeCompactions(): NativeCompaction[];
|
|
83
|
+
restoreNativeCompactions(compactions: NativeCompaction[]): void;
|
|
76
84
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
77
85
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
78
86
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
package/dist/adapters/claude.js
CHANGED
|
@@ -152,6 +152,11 @@ export class ClaudeAdapter {
|
|
|
152
152
|
structuredError;
|
|
153
153
|
apiErrorStatus;
|
|
154
154
|
compactResult;
|
|
155
|
+
autoCompactionPrepared = false;
|
|
156
|
+
preparedAutoCompactThreshold;
|
|
157
|
+
manualCompactionInFlight = false;
|
|
158
|
+
nativeCompactions = [];
|
|
159
|
+
nativeCompactionSequence = 0;
|
|
155
160
|
cumulativeUsage = { observedAt: new Date(0).toISOString(), source: "provider" };
|
|
156
161
|
pending;
|
|
157
162
|
terminalResult;
|
|
@@ -292,6 +297,8 @@ export class ClaudeAdapter {
|
|
|
292
297
|
if (msg.compact_result === "success") {
|
|
293
298
|
this.compactResult = { ok: true };
|
|
294
299
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
|
|
300
|
+
if (!this.manualCompactionInFlight)
|
|
301
|
+
this.nativeCompactions.push({ id: `claude:${this._sessionId ?? "unknown"}:${++this.nativeCompactionSequence}`, occurredAt: new Date().toISOString(), provider: "claude" });
|
|
295
302
|
}
|
|
296
303
|
else if (msg.compact_result === "failed") {
|
|
297
304
|
this.compactResult = { ok: false, error: sanitizeDiagnostics(msg.compact_error ?? "Claude native compaction failed") };
|
|
@@ -423,7 +430,14 @@ export class ClaudeAdapter {
|
|
|
423
430
|
}
|
|
424
431
|
async compact() {
|
|
425
432
|
this.compactResult = undefined;
|
|
426
|
-
|
|
433
|
+
this.manualCompactionInFlight = true;
|
|
434
|
+
let result;
|
|
435
|
+
try {
|
|
436
|
+
result = await this.sendTurn("/compact");
|
|
437
|
+
}
|
|
438
|
+
finally {
|
|
439
|
+
this.manualCompactionInFlight = false;
|
|
440
|
+
}
|
|
427
441
|
if (result.failure?.category === "session-unavailable") {
|
|
428
442
|
return { ok: false, error: result.text || result.failure.diagnostics, failure: result.failure };
|
|
429
443
|
}
|
|
@@ -431,6 +445,43 @@ export class ClaudeAdapter {
|
|
|
431
445
|
return this.compactResult;
|
|
432
446
|
return { ok: false, error: sanitizeDiagnostics(result.text || "Claude did not emit an explicit compact status") };
|
|
433
447
|
}
|
|
448
|
+
async prepareAutoCompaction(thresholdPercent = this.opts.autoCompactThresholdPercent) {
|
|
449
|
+
if (thresholdPercent === undefined)
|
|
450
|
+
return;
|
|
451
|
+
const threshold = validThreshold(thresholdPercent);
|
|
452
|
+
if (this.autoCompactionPrepared && this.preparedAutoCompactThreshold === threshold)
|
|
453
|
+
return;
|
|
454
|
+
this.opts.autoCompactThresholdPercent = threshold;
|
|
455
|
+
await this.query.initializationResult();
|
|
456
|
+
await this.query.applyFlagSettings({ autoCompactEnabled: true });
|
|
457
|
+
const baseline = await this.query.getContextUsage();
|
|
458
|
+
if (!Number.isFinite(baseline.maxTokens) || baseline.maxTokens <= 0
|
|
459
|
+
|| !Number.isFinite(baseline.autoCompactThreshold) || baseline.autoCompactThreshold === undefined
|
|
460
|
+
|| !baseline.isAutoCompactEnabled) {
|
|
461
|
+
throw new Error("Claude did not expose an enabled native automatic-compaction threshold");
|
|
462
|
+
}
|
|
463
|
+
// Claude's setting is the total window. Preserve the provider's own
|
|
464
|
+
// response/compaction reserve, so the resulting used-token trigger is at
|
|
465
|
+
// or before Rafi's configured percentage.
|
|
466
|
+
const reserve = baseline.maxTokens - baseline.autoCompactThreshold;
|
|
467
|
+
if (reserve < 0)
|
|
468
|
+
throw new Error("Claude reported an invalid automatic-compaction reserve");
|
|
469
|
+
const requestedWindow = tokenLimit(baseline.maxTokens, threshold) + reserve;
|
|
470
|
+
await this.query.applyFlagSettings({ autoCompactEnabled: true, autoCompactWindow: requestedWindow });
|
|
471
|
+
const installed = await this.query.getContextUsage();
|
|
472
|
+
if (!installed.isAutoCompactEnabled || installed.autoCompactThreshold === undefined
|
|
473
|
+
|| installed.autoCompactThreshold > tokenLimit(baseline.maxTokens, threshold)) {
|
|
474
|
+
throw new Error("Claude did not accept the requested native automatic-compaction ceiling");
|
|
475
|
+
}
|
|
476
|
+
this.autoCompactionPrepared = true;
|
|
477
|
+
this.preparedAutoCompactThreshold = threshold;
|
|
478
|
+
}
|
|
479
|
+
drainNativeCompactions() {
|
|
480
|
+
const pending = this.nativeCompactions;
|
|
481
|
+
this.nativeCompactions = [];
|
|
482
|
+
return pending;
|
|
483
|
+
}
|
|
484
|
+
restoreNativeCompactions(compactions) { this.nativeCompactions.unshift(...compactions); }
|
|
434
485
|
async contextUsage() {
|
|
435
486
|
try {
|
|
436
487
|
const usage = await this.query.getContextUsage();
|
|
@@ -510,6 +561,14 @@ export class ClaudeAdapter {
|
|
|
510
561
|
}
|
|
511
562
|
}
|
|
512
563
|
function finiteNumber(value) { const parsed = Number(value); return value !== null && value !== undefined && value !== "" && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; }
|
|
564
|
+
function validThreshold(value) {
|
|
565
|
+
if (!Number.isInteger(value) || value < 1 || value > 99)
|
|
566
|
+
throw new Error("automatic compaction threshold must be an integer from 1 to 99");
|
|
567
|
+
return value;
|
|
568
|
+
}
|
|
569
|
+
function tokenLimit(maximum, percentage) {
|
|
570
|
+
return Math.max(1, Math.floor(maximum * percentage / 100));
|
|
571
|
+
}
|
|
513
572
|
function isMissingClaudeSession(message) {
|
|
514
573
|
return /no conversation found with session id|session .* not found|conversation .* not found/i.test(message);
|
|
515
574
|
}
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
|
|
1
|
+
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
|
|
2
2
|
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
3
3
|
export interface CodexLineResult {
|
|
4
4
|
events: BuilderEvent[];
|
|
@@ -27,6 +27,13 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
27
27
|
private providerSessionUsage?;
|
|
28
28
|
private lastTurnTokens?;
|
|
29
29
|
private stderr;
|
|
30
|
+
private nativeAutoCompactTokenLimit?;
|
|
31
|
+
private autoCompactionPrepared;
|
|
32
|
+
private preparedAutoCompactThreshold?;
|
|
33
|
+
private nativeCompactions;
|
|
34
|
+
private nativeCompactionSequence;
|
|
35
|
+
private manualCompactionInFlight;
|
|
36
|
+
private observedToolCalls;
|
|
30
37
|
constructor(opts: BuilderAdapterOptions);
|
|
31
38
|
buildInstruction(instruction: string): string;
|
|
32
39
|
buildAppServerArgs(): string[];
|
|
@@ -35,7 +42,11 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
35
42
|
sendTurn(instruction: string): Promise<TurnResult>;
|
|
36
43
|
private sendTurnInternal;
|
|
37
44
|
compact(): Promise<CompactResult>;
|
|
45
|
+
prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<void>;
|
|
46
|
+
drainNativeCompactions(): NativeCompaction[];
|
|
47
|
+
restoreNativeCompactions(compactions: NativeCompaction[]): void;
|
|
38
48
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
49
|
+
contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
|
|
39
50
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
40
51
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
41
52
|
sessionId(): string | undefined;
|
|
@@ -52,5 +63,6 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
52
63
|
private handle;
|
|
53
64
|
private waitFor;
|
|
54
65
|
private disconnect;
|
|
66
|
+
private restartForAutoCompaction;
|
|
55
67
|
private buildSkillsAppendix;
|
|
56
68
|
}
|
package/dist/adapters/codex.js
CHANGED
|
@@ -43,6 +43,13 @@ export class CodexAdapter {
|
|
|
43
43
|
providerSessionUsage;
|
|
44
44
|
lastTurnTokens;
|
|
45
45
|
stderr = "";
|
|
46
|
+
nativeAutoCompactTokenLimit;
|
|
47
|
+
autoCompactionPrepared = false;
|
|
48
|
+
preparedAutoCompactThreshold;
|
|
49
|
+
nativeCompactions = [];
|
|
50
|
+
nativeCompactionSequence = 0;
|
|
51
|
+
manualCompactionInFlight = false;
|
|
52
|
+
observedToolCalls = 0;
|
|
46
53
|
constructor(opts) {
|
|
47
54
|
this.opts = opts;
|
|
48
55
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
@@ -52,7 +59,13 @@ export class CodexAdapter {
|
|
|
52
59
|
return [this.opts.systemPromptAppend, this.buildSkillsAppendix(), instruction]
|
|
53
60
|
.filter((part) => Boolean(part)).join("\n\n");
|
|
54
61
|
}
|
|
55
|
-
buildAppServerArgs() {
|
|
62
|
+
buildAppServerArgs() {
|
|
63
|
+
const args = ["app-server", "--listen", "stdio://"];
|
|
64
|
+
if (this.nativeAutoCompactTokenLimit !== undefined) {
|
|
65
|
+
args.push("-c", `model_auto_compact_token_limit=${this.nativeAutoCompactTokenLimit}`, "-c", 'model_auto_compact_token_limit_scope="total"');
|
|
66
|
+
}
|
|
67
|
+
return args;
|
|
68
|
+
}
|
|
56
69
|
/** Deprecated fixture helper. Runtime execution uses only app-server. */
|
|
57
70
|
buildArgs(instruction) {
|
|
58
71
|
const args = ["exec", "--json", "--skip-git-repo-check", "--sandbox", this.opts.sandboxMode ?? "workspace-write", "-C", this.opts.cwd];
|
|
@@ -143,11 +156,17 @@ export class CodexAdapter {
|
|
|
143
156
|
&& this.usageRevision > usageRevision
|
|
144
157
|
&& this.usage !== undefined;
|
|
145
158
|
}, 30_000);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
this.manualCompactionInFlight = true;
|
|
160
|
+
try {
|
|
161
|
+
await Promise.all([
|
|
162
|
+
this.request("thread/compact/start", { threadId: this._sessionId }),
|
|
163
|
+
done,
|
|
164
|
+
postCompactUsage,
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
this.manualCompactionInFlight = false;
|
|
169
|
+
}
|
|
151
170
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
|
|
152
171
|
return { ok: true };
|
|
153
172
|
}
|
|
@@ -157,7 +176,54 @@ export class CodexAdapter {
|
|
|
157
176
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
158
177
|
}
|
|
159
178
|
}
|
|
179
|
+
async prepareAutoCompaction(thresholdPercent = this.opts.autoCompactThresholdPercent) {
|
|
180
|
+
if (thresholdPercent === undefined)
|
|
181
|
+
return;
|
|
182
|
+
const threshold = validThreshold(thresholdPercent);
|
|
183
|
+
if (this.autoCompactionPrepared && this.preparedAutoCompactThreshold === threshold)
|
|
184
|
+
return;
|
|
185
|
+
this.opts.autoCompactThresholdPercent = threshold;
|
|
186
|
+
await this.ensureThread();
|
|
187
|
+
// Codex reports its model context window in token-usage notifications, not
|
|
188
|
+
// in thread/start. Establish the otherwise idle thread with a constrained
|
|
189
|
+
// setup turn, then restart the app server with the provider-native ceiling
|
|
190
|
+
// before any Builder or QA work is sent.
|
|
191
|
+
if (!this.usage?.maximum) {
|
|
192
|
+
const toolsBefore = this.observedToolCalls;
|
|
193
|
+
const setup = await this.sendTurnInternal("Rafi internal initialization only. Do not call tools or modify files. Reply briefly that the context is ready.");
|
|
194
|
+
if (setup.isError)
|
|
195
|
+
throw new Error(`Codex automatic-compaction setup failed: ${setup.text.slice(0, 240)}`);
|
|
196
|
+
if (this.observedToolCalls !== toolsBefore)
|
|
197
|
+
throw new Error("Codex automatic-compaction setup unexpectedly called a tool");
|
|
198
|
+
}
|
|
199
|
+
const maximum = this.usage?.maximum;
|
|
200
|
+
if (!maximum || !Number.isFinite(maximum) || maximum <= 0) {
|
|
201
|
+
throw new Error("Codex did not report a model context window during automatic-compaction setup");
|
|
202
|
+
}
|
|
203
|
+
this.nativeAutoCompactTokenLimit = tokenLimit(maximum, threshold);
|
|
204
|
+
await this.restartForAutoCompaction();
|
|
205
|
+
await this.ensureThread();
|
|
206
|
+
this.autoCompactionPrepared = true;
|
|
207
|
+
this.preparedAutoCompactThreshold = threshold;
|
|
208
|
+
}
|
|
209
|
+
drainNativeCompactions() {
|
|
210
|
+
const pending = this.nativeCompactions;
|
|
211
|
+
this.nativeCompactions = [];
|
|
212
|
+
return pending;
|
|
213
|
+
}
|
|
214
|
+
restoreNativeCompactions(compactions) { this.nativeCompactions.unshift(...compactions); }
|
|
160
215
|
async contextUsage() { return this.usage; }
|
|
216
|
+
async contextUsageAfterNativeCompaction(compaction) {
|
|
217
|
+
if (compaction.usageRevision === undefined || this.usageRevision > compaction.usageRevision)
|
|
218
|
+
return this.usage;
|
|
219
|
+
try {
|
|
220
|
+
await this.waitFor("thread/tokenUsage/updated", () => this.usageRevision > compaction.usageRevision, 5_000);
|
|
221
|
+
return this.usage;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
161
227
|
async sessionUsage() { return this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined; }
|
|
162
228
|
async switchSettings(settings) {
|
|
163
229
|
this.opts.model = settings.model;
|
|
@@ -303,8 +369,10 @@ export class CodexAdapter {
|
|
|
303
369
|
}
|
|
304
370
|
catch { /* ignore non-protocol stdout */ } });
|
|
305
371
|
child.stderr.on("data", (chunk) => { this.stderr = `${this.stderr}${chunk.toString()}`.slice(-8192); });
|
|
306
|
-
child.on("error", (error) => this.
|
|
307
|
-
|
|
372
|
+
child.on("error", (error) => { if (this.process === child)
|
|
373
|
+
this.disconnect(error); });
|
|
374
|
+
child.on("close", (code) => { if (this.process === child)
|
|
375
|
+
this.disconnect(new Error(`Codex app-server exited with code ${code ?? "unknown"}`)); });
|
|
308
376
|
await this.request("initialize", { clientInfo: { name: "rafi", title: "Rafi", version: "1" }, capabilities: { experimentalApi: true, requestAttestation: false } });
|
|
309
377
|
this.write({ method: "initialized", params: {} });
|
|
310
378
|
this.initialized = true;
|
|
@@ -341,6 +409,8 @@ export class CodexAdapter {
|
|
|
341
409
|
const params = message.params ?? {};
|
|
342
410
|
if (message.method === "item/started") {
|
|
343
411
|
const item = params.item;
|
|
412
|
+
if (isCodexToolItem(item))
|
|
413
|
+
this.observedToolCalls += 1;
|
|
344
414
|
const activity = codexItemActivity(item);
|
|
345
415
|
if (activity)
|
|
346
416
|
this.eventQueue.push({ kind: "activity", provider: "codex", ...activity });
|
|
@@ -351,8 +421,21 @@ export class CodexAdapter {
|
|
|
351
421
|
this.activeText.push(item.text);
|
|
352
422
|
this.eventQueue.push({ kind: "text", text: item.text });
|
|
353
423
|
}
|
|
354
|
-
else if (item?.type === "commandExecution")
|
|
424
|
+
else if (item?.type === "commandExecution") {
|
|
355
425
|
this.eventQueue.push({ kind: "tool", name: "command_execution", input: { command: item.command } });
|
|
426
|
+
}
|
|
427
|
+
else if (item?.type === "contextCompaction" && !this.manualCompactionInFlight) {
|
|
428
|
+
// Completion is the provider's authoritative confirmation. Token-usage
|
|
429
|
+
// notifications may arrive before or after it, so do not make durable
|
|
430
|
+
// compaction accounting depend on their delivery order.
|
|
431
|
+
this.nativeCompactions.push({
|
|
432
|
+
id: `codex:${this._sessionId ?? "unknown"}:${Date.now()}:${++this.nativeCompactionSequence}`,
|
|
433
|
+
occurredAt: new Date().toISOString(),
|
|
434
|
+
provider: "codex",
|
|
435
|
+
usageRevision: this.usageRevision,
|
|
436
|
+
});
|
|
437
|
+
this.eventQueue.push({ kind: "session-transition", transition: "compacted", detail: "provider-native automatic compaction" });
|
|
438
|
+
}
|
|
356
439
|
else {
|
|
357
440
|
const activity = codexItemActivity(item, true);
|
|
358
441
|
if (activity)
|
|
@@ -441,6 +524,16 @@ export class CodexAdapter {
|
|
|
441
524
|
waiter.reject(error);
|
|
442
525
|
this.notificationWaiters.clear();
|
|
443
526
|
}
|
|
527
|
+
async restartForAutoCompaction() {
|
|
528
|
+
const child = this.process;
|
|
529
|
+
if (!child)
|
|
530
|
+
return;
|
|
531
|
+
this.process = undefined;
|
|
532
|
+
this.initialized = false;
|
|
533
|
+
this.threadAttached = false;
|
|
534
|
+
if (child.exitCode === null)
|
|
535
|
+
child.kill("SIGTERM");
|
|
536
|
+
}
|
|
444
537
|
buildSkillsAppendix() {
|
|
445
538
|
if (!this.opts.skills?.length)
|
|
446
539
|
return undefined;
|
|
@@ -452,6 +545,14 @@ function optionalNonNegative(value) {
|
|
|
452
545
|
const number = Number(value);
|
|
453
546
|
return Number.isFinite(number) && number >= 0 ? number : undefined;
|
|
454
547
|
}
|
|
548
|
+
function validThreshold(value) {
|
|
549
|
+
if (!Number.isInteger(value) || value < 1 || value > 99)
|
|
550
|
+
throw new Error("automatic compaction threshold must be an integer from 1 to 99");
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
function tokenLimit(maximum, percentage) {
|
|
554
|
+
return Math.max(1, Math.floor(maximum * percentage / 100));
|
|
555
|
+
}
|
|
455
556
|
function activityPhase(phase) {
|
|
456
557
|
if (phase === "planning")
|
|
457
558
|
return "planning";
|
|
@@ -483,6 +584,9 @@ function codexItemActivity(item, completed = false) {
|
|
|
483
584
|
return { state: completed ? "received response" : "writing response" };
|
|
484
585
|
return undefined;
|
|
485
586
|
}
|
|
587
|
+
function isCodexToolItem(item) {
|
|
588
|
+
return item?.type === "commandExecution" || item?.type === "mcpToolCall" || item?.type === "dynamicToolCall" || item?.type === "webSearch" || item?.type === "fileChange";
|
|
589
|
+
}
|
|
486
590
|
function loadSkillMarkdown(cwd, skill) {
|
|
487
591
|
const projectPath = join(cwd, ".agents", "skills", skill, "SKILL.md");
|
|
488
592
|
if (existsSync(projectPath))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentRuntime } from "../runtimeAuth.js";
|
|
2
|
-
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, ProviderSessionUsage, TurnResult } from "./types.js";
|
|
2
|
+
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, NativeCompaction, ProviderSessionUsage, TurnResult } from "./types.js";
|
|
3
3
|
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
4
4
|
export type TurnRecoveryChoice = "retry" | "switch" | "cancel";
|
|
5
5
|
export interface RecoveringAdapterOptions {
|
|
@@ -29,6 +29,10 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
29
29
|
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
30
30
|
validateSession(): Promise<SessionAvailabilityV1>;
|
|
31
31
|
compact(): Promise<CompactResult>;
|
|
32
|
+
prepareAutoCompaction(thresholdPercent?: number): Promise<void>;
|
|
33
|
+
drainNativeCompactions(): import("./types.js").NativeCompaction[];
|
|
34
|
+
restoreNativeCompactions(compactions: NativeCompaction[]): void;
|
|
35
|
+
contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
|
|
32
36
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
33
37
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
34
38
|
events(): AsyncIterable<BuilderEvent>;
|
|
@@ -67,6 +67,10 @@ export class RecoveringAdapter {
|
|
|
67
67
|
return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped", detail: "wrapped adapter does not expose scoped session validation" });
|
|
68
68
|
}
|
|
69
69
|
compact() { return this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" }); }
|
|
70
|
+
prepareAutoCompaction(thresholdPercent) { return this.adapter.prepareAutoCompaction?.(thresholdPercent) ?? Promise.resolve(); }
|
|
71
|
+
drainNativeCompactions() { return this.adapter.drainNativeCompactions?.() ?? []; }
|
|
72
|
+
restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }
|
|
73
|
+
contextUsageAfterNativeCompaction(compaction) { return this.adapter.contextUsageAfterNativeCompaction?.(compaction) ?? Promise.resolve(undefined); }
|
|
70
74
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
71
75
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
72
76
|
events() {
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -122,6 +122,15 @@ export interface CompactResult {
|
|
|
122
122
|
/** Preserve exact-session loss so lifecycle policy never treats it as an ordinary retryable compaction failure. */
|
|
123
123
|
failure?: RuntimeFailure;
|
|
124
124
|
}
|
|
125
|
+
/** A provider-confirmed compaction that happened without Rafi issuing /compact. */
|
|
126
|
+
export interface NativeCompaction {
|
|
127
|
+
/** Stable for the lifetime of an adapter; used for durable idempotency. */
|
|
128
|
+
id: string;
|
|
129
|
+
occurredAt: string;
|
|
130
|
+
provider: "claude" | "codex";
|
|
131
|
+
/** Adapter-local usage generation captured with the provider confirmation. */
|
|
132
|
+
usageRevision?: number;
|
|
133
|
+
}
|
|
125
134
|
export interface ProviderSettingSwitch {
|
|
126
135
|
model?: string;
|
|
127
136
|
effort?: EffortLevel;
|
|
@@ -163,6 +172,8 @@ export interface BuilderAdapterOptions {
|
|
|
163
172
|
systemPromptAppend?: string;
|
|
164
173
|
/** Skill names to preload for this session (Claude: lazy-loaded; Codex: flattened). */
|
|
165
174
|
skills?: string[];
|
|
175
|
+
/** Provider-native context ceiling, as a percentage of that provider's model window. */
|
|
176
|
+
autoCompactThresholdPercent?: number;
|
|
166
177
|
}
|
|
167
178
|
export interface BuilderAdapter {
|
|
168
179
|
readonly agent: "claude" | "codex";
|
|
@@ -178,6 +189,18 @@ export interface BuilderAdapter {
|
|
|
178
189
|
validateSession?(): Promise<SessionAvailabilityV1>;
|
|
179
190
|
/** Provider-native compaction on the exact live conversation. */
|
|
180
191
|
compact?(): Promise<CompactResult>;
|
|
192
|
+
/**
|
|
193
|
+
* Install and verify provider-native automatic compaction before role work is
|
|
194
|
+
* dispatched. This intentionally happens outside a work turn so a long,
|
|
195
|
+
* tool-heavy first turn is protected too.
|
|
196
|
+
*/
|
|
197
|
+
prepareAutoCompaction?(thresholdPercent?: number): Promise<void>;
|
|
198
|
+
/** Consume provider-native compactions observed since the prior drain. */
|
|
199
|
+
drainNativeCompactions?(): NativeCompaction[];
|
|
200
|
+
/** Return a drained batch to the adapter when durable receipt failed. */
|
|
201
|
+
restoreNativeCompactions?(compactions: NativeCompaction[]): void;
|
|
202
|
+
/** Return occupancy known to postdate a native-compaction event, if available. */
|
|
203
|
+
contextUsageAfterNativeCompaction?(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
|
|
181
204
|
/** Truthful provider context occupancy, when exposed by the provider. */
|
|
182
205
|
contextUsage?(): Promise<ContextUsage | undefined>;
|
|
183
206
|
/** Provider/session cumulative totals; distinct from live context occupancy. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "../adapters/types.js";
|
|
1
|
+
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "../adapters/types.js";
|
|
2
2
|
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
3
3
|
export interface CurrentWorkflowIdentity {
|
|
4
4
|
worktree: string;
|
|
@@ -26,6 +26,10 @@ export declare class CurrentWorkflowGuardAdapter implements BuilderAdapter {
|
|
|
26
26
|
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
27
27
|
validateSession(): Promise<SessionAvailabilityV1>;
|
|
28
28
|
compact(): Promise<CompactResult>;
|
|
29
|
+
prepareAutoCompaction(thresholdPercent?: number): Promise<void>;
|
|
30
|
+
drainNativeCompactions(): import("../adapters/types.js").NativeCompaction[];
|
|
31
|
+
restoreNativeCompactions(compactions: NativeCompaction[]): void;
|
|
32
|
+
contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
|
|
29
33
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
30
34
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
31
35
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
@@ -48,6 +48,10 @@ export class CurrentWorkflowGuardAdapter {
|
|
|
48
48
|
adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
|
|
49
49
|
validateSession() { assertCurrentWorkflowIdentity(this.cwd, this.expected); return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped" }); }
|
|
50
50
|
async compact() { assertCurrentWorkflowIdentity(this.cwd, this.expected); const result = await (this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" })); assertCurrentWorkflowIdentity(this.cwd, this.expected); return result; }
|
|
51
|
+
prepareAutoCompaction(thresholdPercent) { return this.adapter.prepareAutoCompaction?.(thresholdPercent) ?? Promise.resolve(); }
|
|
52
|
+
drainNativeCompactions() { return this.adapter.drainNativeCompactions?.() ?? []; }
|
|
53
|
+
restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }
|
|
54
|
+
contextUsageAfterNativeCompaction(compaction) { return this.adapter.contextUsageAfterNativeCompaction?.(compaction) ?? Promise.resolve(undefined); }
|
|
51
55
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
52
56
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
53
57
|
switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
|
package/dist/branch/runner.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export interface BranchRunnerOptions {
|
|
|
56
56
|
builderSessionStrategy?: SessionStrategy;
|
|
57
57
|
qaSessionStrategy?: SessionStrategy;
|
|
58
58
|
observeBuilder?: (builder: BuilderAdapter) => Promise<void>;
|
|
59
|
+
observeBuilderNativeCompactions?: (builder: BuilderAdapter, cwd: string) => Promise<void>;
|
|
60
|
+
observeQaNativeCompactions?: (adapter: BuilderAdapter) => Promise<void>;
|
|
59
61
|
qaNonconvergence?: (context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>;
|
|
60
62
|
beforeBuilderTurn?: (adapter: BuilderAdapter, frozenAction: string, cwd: string) => Promise<BuilderAdapter>;
|
|
61
63
|
builderSessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>;
|
package/dist/branch/runner.js
CHANGED
|
@@ -173,7 +173,7 @@ export async function runBranchPlan(opts) {
|
|
|
173
173
|
}
|
|
174
174
|
builderWorkSessions += 1;
|
|
175
175
|
viewer = opts.observeBuilder?.(builder);
|
|
176
|
-
const foreman = new Foreman(builder, opts.log, opts.notificationsEnabled, opts.qaEnabled, 3, undefined, undefined, undefined, opts.qaSessionStrategy ?? "compact", undefined, opts.builderSessionStrategy ?? "compact", undefined, opts.beforeBuilderTurn ? (adapter, action) => opts.beforeBuilderTurn(adapter, action, worktreePath) : undefined);
|
|
176
|
+
const foreman = new Foreman(builder, opts.log, opts.notificationsEnabled, opts.qaEnabled, 3, undefined, undefined, undefined, opts.qaSessionStrategy ?? "compact", undefined, opts.builderSessionStrategy ?? "compact", undefined, opts.beforeBuilderTurn ? (adapter, action) => opts.beforeBuilderTurn(adapter, action, worktreePath) : undefined, undefined, undefined, async (adapter) => opts.observeBuilderNativeCompactions?.(adapter, worktreePath));
|
|
177
177
|
const { result, status } = await foreman.runInstruction(ticketInstruction);
|
|
178
178
|
builder = foreman.builderAdapter();
|
|
179
179
|
workflowCheckpoint(opts.projectDir, opts.runId, "builder-after", node.ticket.id, { status: status.kind, sessionId: builder.sessionId(), worktree: worktreePath });
|
|
@@ -242,6 +242,7 @@ export async function runBranchPlan(opts) {
|
|
|
242
242
|
ticket: node.ticket, builderWorktree: worktreePath, builderSummary: status.summary ?? result.text,
|
|
243
243
|
qaStrategy: opts.qaSessionStrategy ?? "compact", state: qaStream, createQa: opts.createQa, maxCycles: 3,
|
|
244
244
|
sessionBoundary: opts.qaSessionBoundary,
|
|
245
|
+
observeNativeCompactions: opts.observeQaNativeCompactions,
|
|
245
246
|
evidence: (entry) => opts.log.write("qa-evidence", { ticket: node.ticket.id, ...entry }),
|
|
246
247
|
fix: async (issues) => {
|
|
247
248
|
if (!builder)
|
package/dist/cli/start.js
CHANGED
|
@@ -37,7 +37,7 @@ import { eligibleTickets, evaluateTicketEligibility } from "../tickets/eligibili
|
|
|
37
37
|
import { BUILTIN_BRANCH_PREFIX, validateBranchPrefix } from "../branch/prefix.js";
|
|
38
38
|
import { captureCurrentWorkflowSessionIdentity, CurrentWorkflowChangedError, CurrentWorkflowGuardAdapter } from "../branch/currentGuard.js";
|
|
39
39
|
import { ContinuityAdapter, ContinuityRecoveryRequiredError, SessionUnavailableContinuityError } from "../continuity.js";
|
|
40
|
-
import { ContextCapabilityError, RoleSessionController } from "../sessionLifecycle.js";
|
|
40
|
+
import { ContextCapabilityError, RoleSessionController, ThresholdCompactionController } from "../sessionLifecycle.js";
|
|
41
41
|
import { HandoffLoopError, HandoffService, parseBuilderHandoffRequest } from "../handoffs.js";
|
|
42
42
|
import { captureWorkspaceIdentity, createProviderSessionRef, resolveUniqueSessionBinding } from "../sessionIdentity.js";
|
|
43
43
|
import { SessionUnavailableError } from "../adapters/sessionFailure.js";
|
|
@@ -625,6 +625,7 @@ export function buildStartCommand() {
|
|
|
625
625
|
fast: builderFast,
|
|
626
626
|
systemPromptAppend: roleBundle.system || undefined,
|
|
627
627
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
628
|
+
autoCompactThresholdPercent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
|
|
628
629
|
};
|
|
629
630
|
const adapter = agent === "codex"
|
|
630
631
|
? new CodexAdapter(adapterOpts)
|
|
@@ -636,6 +637,7 @@ export function buildStartCommand() {
|
|
|
636
637
|
throw new SessionUnavailableError({ runtime: "codex", phase: "preflight", dispatchState: "not-sent", executable: agentExecutable ?? "codex", cwd: builderCwd, diagnostics: availability.detail ?? `Codex session ${sessionRef.sessionId} is ${availability.status}`, availability });
|
|
637
638
|
}
|
|
638
639
|
}
|
|
640
|
+
await prepareNativeAutoCompaction(adapter);
|
|
639
641
|
return !branchMode ? new CurrentWorkflowGuardAdapter(adapter, builderCwd) : adapter;
|
|
640
642
|
};
|
|
641
643
|
let activeContinuityRunId;
|
|
@@ -651,7 +653,8 @@ export function buildStartCommand() {
|
|
|
651
653
|
role, source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
|
|
652
654
|
reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact",
|
|
653
655
|
settings_revision: settingsRevision, display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false,
|
|
654
|
-
auto_compact_threshold_percent:
|
|
656
|
+
auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
|
|
657
|
+
compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
|
|
655
658
|
};
|
|
656
659
|
const liveRoleSettings = (role, base) => {
|
|
657
660
|
const live = readAgentDefaults(cwd);
|
|
@@ -696,8 +699,10 @@ export function buildStartCommand() {
|
|
|
696
699
|
fast: settings.fast,
|
|
697
700
|
systemPromptAppend: roleBundle.system || undefined,
|
|
698
701
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
702
|
+
autoCompactThresholdPercent: settings.auto_compact_threshold_percent,
|
|
699
703
|
};
|
|
700
704
|
const created = settings.make === "codex" ? new CodexAdapter(adapterOptions) : await ClaudeAdapter.create(adapterOptions);
|
|
705
|
+
await prepareNativeAutoCompaction(created);
|
|
701
706
|
return !branchMode ? new CurrentWorkflowGuardAdapter(created, builderCwd) : created;
|
|
702
707
|
};
|
|
703
708
|
const createQaForSettings = async (qaCwd, settings) => {
|
|
@@ -722,8 +727,11 @@ export function buildStartCommand() {
|
|
|
722
727
|
fast: settings.fast,
|
|
723
728
|
systemPromptAppend: `${roleBundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`,
|
|
724
729
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
730
|
+
autoCompactThresholdPercent: settings.auto_compact_threshold_percent,
|
|
725
731
|
};
|
|
726
|
-
|
|
732
|
+
const created = settings.make === "codex" ? new CodexAdapter(adapterOptions) : await ClaudeAdapter.create(adapterOptions);
|
|
733
|
+
await prepareNativeAutoCompaction(created);
|
|
734
|
+
return created;
|
|
727
735
|
};
|
|
728
736
|
const createRecoveringBuilder = async (builderCwd, sessionId, sessionRef) => {
|
|
729
737
|
if (sessionId && !sessionRef) {
|
|
@@ -910,6 +918,16 @@ export function buildStartCommand() {
|
|
|
910
918
|
return continuousBuilder(transfer.successor, builderCwd, runId, input.next);
|
|
911
919
|
};
|
|
912
920
|
const createRawQa = async (qaCwd, sessionRef) => {
|
|
921
|
+
// Isolated QA conversations are intentionally disposable. Resolve live
|
|
922
|
+
// QA settings for each one so a threshold-only update applies to the
|
|
923
|
+
// very next review instead of waiting for a nonexistent session reuse.
|
|
924
|
+
const qaSettings = liveRoleSettings("qa", resolvedContinuitySettings("qa"));
|
|
925
|
+
const ready = await ensureRuntimeReadyForCommand(qaCwd, qaSettings.make, {
|
|
926
|
+
label: "live QA settings",
|
|
927
|
+
yes: true,
|
|
928
|
+
allowSwitch: false,
|
|
929
|
+
model: qaSettings.model === "default" ? undefined : qaSettings.model,
|
|
930
|
+
});
|
|
913
931
|
const qaPolicy = new PermissionPolicy(config.permissions, qaCwd);
|
|
914
932
|
const roleBundle = loadRoleBundle("qa", { projectDir: qaCwd });
|
|
915
933
|
const adapterOpts = {
|
|
@@ -917,21 +935,24 @@ export function buildStartCommand() {
|
|
|
917
935
|
configRoot: cwd,
|
|
918
936
|
sessionRole: "qa",
|
|
919
937
|
sessionStream: "qa",
|
|
920
|
-
runtimeExecutable:
|
|
938
|
+
runtimeExecutable: ready.executable,
|
|
921
939
|
runtimePhase: "qa",
|
|
922
|
-
model:
|
|
940
|
+
model: qaSettings.model === "default" ? undefined : qaSettings.model,
|
|
923
941
|
...(sessionRef ? { resumeSessionRef: sessionRef } : {}),
|
|
924
942
|
sessionGeneration: sessionRef?.generation ?? 0,
|
|
925
943
|
workspaceIdentity: captureWorkspaceIdentity(qaCwd),
|
|
926
944
|
permission: createPermissionHandler(qaPolicy, log),
|
|
927
|
-
effort:
|
|
928
|
-
fast:
|
|
945
|
+
effort: explicitEffort(qaSettings.reasoning),
|
|
946
|
+
fast: qaSettings.fast,
|
|
929
947
|
systemPromptAppend: `${roleBundle.system}\n\nYou are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.`,
|
|
930
948
|
skills: roleBundle.skills.length > 0 ? roleBundle.skills : undefined,
|
|
949
|
+
autoCompactThresholdPercent: qaSettings.auto_compact_threshold_percent,
|
|
931
950
|
};
|
|
932
|
-
|
|
951
|
+
const created = qaSettings.make === "codex"
|
|
933
952
|
? new CodexAdapter(adapterOpts)
|
|
934
953
|
: await ClaudeAdapter.create(adapterOpts);
|
|
954
|
+
await prepareNativeAutoCompaction(created);
|
|
955
|
+
return created;
|
|
935
956
|
};
|
|
936
957
|
const createRecoveringQa = async (qaCwd, sessionRef) => {
|
|
937
958
|
const initial = await createRawQa(qaCwd, sessionRef);
|
|
@@ -1152,7 +1173,7 @@ export function buildStartCommand() {
|
|
|
1152
1173
|
runId: activeContinuityRunId,
|
|
1153
1174
|
role: "qa",
|
|
1154
1175
|
initialSettings: qaSettings,
|
|
1155
|
-
historicalCountUncertain:
|
|
1176
|
+
historicalCountUncertain: recoveryCompactionHistoryUncertain(cwd, recoveryRecord, activeContinuityRunId, "qa"),
|
|
1156
1177
|
readSettings: () => liveRoleSettings("qa", qaSettings),
|
|
1157
1178
|
settingsBoundary: (input) => applyQaSettingsBoundary(activeContinuityRunId, qaCwd, input),
|
|
1158
1179
|
settingsAdopted: (settings, active) => liveStatusReporter?.updateState({
|
|
@@ -1185,6 +1206,19 @@ export function buildStartCommand() {
|
|
|
1185
1206
|
});
|
|
1186
1207
|
return (await controller.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter;
|
|
1187
1208
|
};
|
|
1209
|
+
const observeQaNativeCompactions = async (adapter) => {
|
|
1210
|
+
if (!activeContinuityRunId)
|
|
1211
|
+
return;
|
|
1212
|
+
const qaSettings = resolvedContinuitySettings("qa");
|
|
1213
|
+
const controller = new ThresholdCompactionController({
|
|
1214
|
+
projectDir: cwd,
|
|
1215
|
+
runId: activeContinuityRunId,
|
|
1216
|
+
role: "qa",
|
|
1217
|
+
initialSettings: qaSettings,
|
|
1218
|
+
historicalCountUncertain: recoveryCompactionHistoryUncertain(cwd, recoveryRecord, activeContinuityRunId, "qa"),
|
|
1219
|
+
});
|
|
1220
|
+
await controller.observeNativeCompactions(adapter);
|
|
1221
|
+
};
|
|
1188
1222
|
const qaNonconvergence = createQaNonconvergenceHandler(cwd, Boolean(opts.yes), roleDefaults.planner);
|
|
1189
1223
|
if (branchMode) {
|
|
1190
1224
|
const ticketsConfig = loadTicketsConfig(cwd);
|
|
@@ -1416,7 +1450,8 @@ export function buildStartCommand() {
|
|
|
1416
1450
|
role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default",
|
|
1417
1451
|
reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", settings_revision: settingsRevision,
|
|
1418
1452
|
display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false,
|
|
1419
|
-
auto_compact_threshold_percent:
|
|
1453
|
+
auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50,
|
|
1454
|
+
compact_maximum: roleDefaults.qa?.compact_maximum ?? 10,
|
|
1420
1455
|
};
|
|
1421
1456
|
const firstResumeRef = plan.nodes[0] ? resumeSessionByTicket.get(plan.nodes[0].ticket.id)?.sessionRef : undefined;
|
|
1422
1457
|
let masterRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBranchBuilder, qa: capturedBranchQa, builderSessionId: firstResumeRef?.sessionId ?? null, builderSessionRef: firstResumeRef ?? null }) : createBuildRun({
|
|
@@ -1432,7 +1467,7 @@ export function buildStartCommand() {
|
|
|
1432
1467
|
return existing;
|
|
1433
1468
|
const controller = RoleSessionController.managed({
|
|
1434
1469
|
projectDir: cwd, runId: masterRun.runId, role: "builder", initialSettings: capturedBranchBuilder,
|
|
1435
|
-
historicalCountUncertain:
|
|
1470
|
+
historicalCountUncertain: recoveryCompactionHistoryUncertain(cwd, recoveryRecord, masterRun.runId, "builder"),
|
|
1436
1471
|
readSettings: () => liveRoleSettings("builder", capturedBranchBuilder),
|
|
1437
1472
|
settingsBoundary: (input) => applyBuilderSettingsBoundary(masterRun.runId, worktreePath, input),
|
|
1438
1473
|
settingsAdopted: (settings, active) => liveStatusReporter?.updateState({
|
|
@@ -1547,8 +1582,10 @@ export function buildStartCommand() {
|
|
|
1547
1582
|
},
|
|
1548
1583
|
qaNonconvergence,
|
|
1549
1584
|
beforeBuilderTurn: async (adapter, frozenAction, worktreePath) => (await branchContextController(worktreePath).atSafeBoundary(adapter, frozenAction)).adapter,
|
|
1585
|
+
observeBuilderNativeCompactions: async (adapter, worktreePath) => { await branchContextController(worktreePath).observeNativeCompactions(adapter); },
|
|
1550
1586
|
builderSessionBoundary: async (adapter, frozenAction, strategy, worktreePath) => (await branchContextController(worktreePath).atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter,
|
|
1551
1587
|
qaSessionBoundary,
|
|
1588
|
+
observeQaNativeCompactions,
|
|
1552
1589
|
});
|
|
1553
1590
|
if (branchSessionFailure) {
|
|
1554
1591
|
const failure = branchSessionFailure;
|
|
@@ -1653,7 +1690,7 @@ export function buildStartCommand() {
|
|
|
1653
1690
|
auto_compact_threshold_percent: thresholdOverride ?? roleDefaults.builder?.auto_compact_threshold_percent ?? 50,
|
|
1654
1691
|
compact_maximum: roleDefaults.builder?.compact_maximum ?? 10,
|
|
1655
1692
|
};
|
|
1656
|
-
const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false, auto_compact_threshold_percent: 50, compact_maximum: 10, settings_revision: settingsRevision };
|
|
1693
|
+
const capturedQa = { role: "qa", source: roleDefaults.qa ? "project" : "provider", make: qaAgent, model: qaModel ?? "default", reasoning: qaEffort ?? "default", fast: qaFast, session_strategy: roleDefaults.qa?.session_strategy ?? "compact", display_session_cost: sessionCostOverride ?? roleDefaults.qa?.display_session_cost ?? false, auto_compact_threshold_percent: roleDefaults.qa?.auto_compact_threshold_percent ?? 50, compact_maximum: roleDefaults.qa?.compact_maximum ?? 10, settings_revision: settingsRevision };
|
|
1657
1694
|
let buildRun = recoveryRecord ? resumeBuildRun(cwd, recoveryRecord.runId, { builder: capturedBuilder, qa: capturedQa, builderSessionId: resumeSessionId ?? null, builderSessionRef: resumeSessionRef ?? null }) : createBuildRun({
|
|
1658
1695
|
tickets: [], repositoryRoot: cwd, branchMode: "current", baseRef: opts.base ?? loadTicketSetupConfig(cwd)?.build.base_branch, builder: capturedBuilder, qa: capturedQa,
|
|
1659
1696
|
runDecisions: { workMode: "current", workModeSource, branchPrefix, branchPrefixSource: resolvedPrefix.source, autoCompactThresholdPercent: capturedBuilder.auto_compact_threshold_percent ?? 50, thresholdSource: thresholdOverride === undefined ? "project" : "cli" },
|
|
@@ -1692,7 +1729,7 @@ export function buildStartCommand() {
|
|
|
1692
1729
|
runId: buildRun.runId,
|
|
1693
1730
|
role: "builder",
|
|
1694
1731
|
initialSettings: capturedBuilder,
|
|
1695
|
-
historicalCountUncertain:
|
|
1732
|
+
historicalCountUncertain: recoveryCompactionHistoryUncertain(cwd, recoveryRecord, buildRun.runId, "builder"),
|
|
1696
1733
|
readSettings: () => liveRoleSettings("builder", capturedBuilder),
|
|
1697
1734
|
settingsBoundary: (input) => applyBuilderSettingsBoundary(buildRun.runId, cwd, input),
|
|
1698
1735
|
settingsAdopted: (settings, active) => liveStatusReporter?.updateState({
|
|
@@ -1732,7 +1769,7 @@ export function buildStartCommand() {
|
|
|
1732
1769
|
return continuousBuilder(transfer.successor, cwd, buildRun.runId, settings);
|
|
1733
1770
|
},
|
|
1734
1771
|
});
|
|
1735
|
-
const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd, undefined, qaEnabled ? createQa : undefined, capturedQa.session_strategy, createBuilder, capturedBuilder.session_strategy, qaNonconvergence, async (adapter, frozenAction) => (await contextController.atSafeBoundary(adapter, frozenAction)).adapter, async (adapter, frozenAction, strategy) => (await contextController.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter, qaSessionBoundary);
|
|
1772
|
+
const foreman = new Foreman(builder, log, config.notifications.enabled, qaEnabled, 3, cwd, undefined, qaEnabled ? createQa : undefined, capturedQa.session_strategy, createBuilder, capturedBuilder.session_strategy, qaNonconvergence, async (adapter, frozenAction) => (await contextController.atSafeBoundary(adapter, frozenAction)).adapter, async (adapter, frozenAction, strategy) => (await contextController.atWorkSessionBoundary(adapter, frozenAction, strategy)).adapter, qaSessionBoundary, observeQaNativeCompactions, async (adapter) => { await contextController.observeNativeCompactions(adapter); });
|
|
1736
1773
|
activeBuilderForStatus = () => foreman.builderAdapter();
|
|
1737
1774
|
const statusReporter = liveStatusReporter = new AgentStatusReporter({
|
|
1738
1775
|
runId: buildRun.runId,
|
|
@@ -1916,6 +1953,19 @@ export function buildStartCommand() {
|
|
|
1916
1953
|
}
|
|
1917
1954
|
});
|
|
1918
1955
|
}
|
|
1956
|
+
async function prepareNativeAutoCompaction(adapter) {
|
|
1957
|
+
if (!adapter.prepareAutoCompaction) {
|
|
1958
|
+
await adapter.close().catch(() => { });
|
|
1959
|
+
throw new Error(`${adapter.agent} does not support provider-native automatic compaction`);
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
await adapter.prepareAutoCompaction();
|
|
1963
|
+
}
|
|
1964
|
+
catch (error) {
|
|
1965
|
+
await adapter.close().catch(() => { });
|
|
1966
|
+
throw error;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1919
1969
|
function createQaNonconvergenceHandler(projectDir, noninteractive, planner) {
|
|
1920
1970
|
return async (context) => {
|
|
1921
1971
|
if (noninteractive || !process.stdin.isTTY || !process.stdout.isTTY)
|
|
@@ -2018,6 +2068,19 @@ function readAgentDefaults(projectDir) {
|
|
|
2018
2068
|
function explicitDefaultValue(value) {
|
|
2019
2069
|
return value && value !== "default" ? value : undefined;
|
|
2020
2070
|
}
|
|
2071
|
+
function recoveryCompactionHistoryUncertain(projectDir, recovery, runId, role) {
|
|
2072
|
+
if (!recovery)
|
|
2073
|
+
return false;
|
|
2074
|
+
if (recovery.legacy || !recovery.runDecisions || !runId)
|
|
2075
|
+
return true;
|
|
2076
|
+
const db = new WorkflowDb(projectDir);
|
|
2077
|
+
try {
|
|
2078
|
+
return db.hasUncheckpointedRoleTurn(runId, role);
|
|
2079
|
+
}
|
|
2080
|
+
finally {
|
|
2081
|
+
db.close();
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2021
2084
|
/** Make the executable branch plan exactly match each explicit root-to-tip stack. */
|
|
2022
2085
|
export function applyExplicitStackTopology(plan, stacks, delivery, branchPrefix) {
|
|
2023
2086
|
const nodeByTicket = new Map(plan.nodes.map((node) => [node.ticket.id, node]));
|
package/dist/continuity.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContinuityDelta, ResolvedAgentSettings } from "rafi-spec";
|
|
2
|
-
import type { BuilderAdapter, CompactResult, ContextUsage, ProviderSessionUsage, ProviderSettingSwitch, RuntimeFailure, TurnResult } from "./adapters/types.js";
|
|
2
|
+
import type { BuilderAdapter, CompactResult, ContextUsage, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, RuntimeFailure, TurnResult } from "./adapters/types.js";
|
|
3
3
|
export declare const CONTINUITY_MARKER = "RAFI_CONTINUITY_DELTA:";
|
|
4
4
|
export declare class ContinuityValidationError extends Error {
|
|
5
5
|
readonly problems: string[];
|
|
@@ -70,6 +70,10 @@ export declare class ContinuityAdapter implements BuilderAdapter {
|
|
|
70
70
|
adoptSessionRef(ref: import("rafi-spec").ProviderSessionRefV1): void;
|
|
71
71
|
validateSession(): Promise<import("rafi-spec").SessionAvailabilityV1>;
|
|
72
72
|
compact(): Promise<CompactResult>;
|
|
73
|
+
prepareAutoCompaction(thresholdPercent?: number): Promise<void>;
|
|
74
|
+
drainNativeCompactions(): import("./adapters/types.js").NativeCompaction[];
|
|
75
|
+
restoreNativeCompactions(compactions: NativeCompaction[]): void;
|
|
76
|
+
contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
|
|
73
77
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
74
78
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
75
79
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
package/dist/continuity.js
CHANGED
|
@@ -232,6 +232,10 @@ export class ContinuityAdapter {
|
|
|
232
232
|
adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
|
|
233
233
|
validateSession() { return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped" }); }
|
|
234
234
|
compact() { return this.adapter.compact ? this.adapter.compact() : Promise.resolve({ ok: false, error: "provider adapter does not expose native compaction" }); }
|
|
235
|
+
prepareAutoCompaction(thresholdPercent) { return this.adapter.prepareAutoCompaction?.(thresholdPercent) ?? Promise.resolve(); }
|
|
236
|
+
drainNativeCompactions() { return this.adapter.drainNativeCompactions?.() ?? []; }
|
|
237
|
+
restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }
|
|
238
|
+
contextUsageAfterNativeCompaction(compaction) { return this.adapter.contextUsageAfterNativeCompaction?.(compaction) ?? Promise.resolve(undefined); }
|
|
235
239
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
236
240
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
237
241
|
switchSettings(settings) { return this.adapter.switchSettings ? this.adapter.switchSettings(settings) : Promise.resolve({ ok: false, error: "provider adapter does not support settings changes" }); }
|
package/dist/foreman.d.ts
CHANGED
|
@@ -65,10 +65,15 @@ export declare class Foreman {
|
|
|
65
65
|
private readonly beforeBuilderTurn?;
|
|
66
66
|
private readonly builderSessionBoundary?;
|
|
67
67
|
private readonly qaSessionBoundary?;
|
|
68
|
+
private readonly observeQaNativeCompactions?;
|
|
69
|
+
/** Persist a Builder's provider-native event immediately after every turn. */
|
|
70
|
+
private readonly observeBuilderNativeCompactions?;
|
|
68
71
|
private readonly ticketsEnabled;
|
|
69
72
|
private readonly qaStream;
|
|
70
73
|
private builderWorkSessions;
|
|
71
|
-
constructor(builder: BuilderAdapter, log: Log, notificationsEnabled?: boolean, qaEnabled?: boolean, qaMaxCycles?: number, projectDir?: string | undefined, qaReviewer?: BuilderAdapter | undefined, qaFactory?: ((cwd: string, sessionId?: string) => Promise<BuilderAdapter>) | undefined, qaSessionStrategy?: SessionStrategy, builderFactory?: ((cwd: string, sessionId?: string) => Promise<BuilderAdapter>) | undefined, builderSessionStrategy?: SessionStrategy, qaNonconvergence?: ((context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>) | undefined, beforeBuilderTurn?: ((adapter: BuilderAdapter, frozenAction: string) => Promise<BuilderAdapter>) | undefined, builderSessionBoundary?: ((adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy) => Promise<BuilderAdapter>) | undefined, qaSessionBoundary?: ((adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>) | undefined)
|
|
74
|
+
constructor(builder: BuilderAdapter, log: Log, notificationsEnabled?: boolean, qaEnabled?: boolean, qaMaxCycles?: number, projectDir?: string | undefined, qaReviewer?: BuilderAdapter | undefined, qaFactory?: ((cwd: string, sessionId?: string) => Promise<BuilderAdapter>) | undefined, qaSessionStrategy?: SessionStrategy, builderFactory?: ((cwd: string, sessionId?: string) => Promise<BuilderAdapter>) | undefined, builderSessionStrategy?: SessionStrategy, qaNonconvergence?: ((context: QaNonconvergenceContext) => Promise<QaNonconvergenceDecision>) | undefined, beforeBuilderTurn?: ((adapter: BuilderAdapter, frozenAction: string) => Promise<BuilderAdapter>) | undefined, builderSessionBoundary?: ((adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy) => Promise<BuilderAdapter>) | undefined, qaSessionBoundary?: ((adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>) | undefined, observeQaNativeCompactions?: ((adapter: BuilderAdapter) => Promise<void>) | undefined,
|
|
75
|
+
/** Persist a Builder's provider-native event immediately after every turn. */
|
|
76
|
+
observeBuilderNativeCompactions?: ((adapter: BuilderAdapter) => Promise<void>) | undefined);
|
|
72
77
|
/**
|
|
73
78
|
* Send one instruction and resolve any needs_input exchanges before returning.
|
|
74
79
|
* The returned result and status always reflect the final (non-needs_input) turn.
|
|
@@ -85,6 +90,7 @@ export declare class Foreman {
|
|
|
85
90
|
result: TurnResult;
|
|
86
91
|
status: StepStatus;
|
|
87
92
|
}>;
|
|
93
|
+
private observeBuilderNative;
|
|
88
94
|
/**
|
|
89
95
|
* Run a QA review pass on the ticket the builder just completed.
|
|
90
96
|
* Loops on qa_fail → fix → re-QA until qa_pass or the cycle cap is reached.
|
package/dist/foreman.js
CHANGED
|
@@ -229,10 +229,14 @@ export class Foreman {
|
|
|
229
229
|
beforeBuilderTurn;
|
|
230
230
|
builderSessionBoundary;
|
|
231
231
|
qaSessionBoundary;
|
|
232
|
+
observeQaNativeCompactions;
|
|
233
|
+
observeBuilderNativeCompactions;
|
|
232
234
|
ticketsEnabled;
|
|
233
235
|
qaStream = { reviews: 0, modificationViolations: 0 };
|
|
234
236
|
builderWorkSessions = 0;
|
|
235
|
-
constructor(builder, log, notificationsEnabled = false, qaEnabled = true, qaMaxCycles = 3, projectDir, qaReviewer, qaFactory, qaSessionStrategy = "compact", builderFactory, builderSessionStrategy = "compact", qaNonconvergence, beforeBuilderTurn, builderSessionBoundary, qaSessionBoundary
|
|
237
|
+
constructor(builder, log, notificationsEnabled = false, qaEnabled = true, qaMaxCycles = 3, projectDir, qaReviewer, qaFactory, qaSessionStrategy = "compact", builderFactory, builderSessionStrategy = "compact", qaNonconvergence, beforeBuilderTurn, builderSessionBoundary, qaSessionBoundary, observeQaNativeCompactions,
|
|
238
|
+
/** Persist a Builder's provider-native event immediately after every turn. */
|
|
239
|
+
observeBuilderNativeCompactions) {
|
|
236
240
|
this.builder = builder;
|
|
237
241
|
this.log = log;
|
|
238
242
|
this.notificationsEnabled = notificationsEnabled;
|
|
@@ -248,6 +252,8 @@ export class Foreman {
|
|
|
248
252
|
this.beforeBuilderTurn = beforeBuilderTurn;
|
|
249
253
|
this.builderSessionBoundary = builderSessionBoundary;
|
|
250
254
|
this.qaSessionBoundary = qaSessionBoundary;
|
|
255
|
+
this.observeQaNativeCompactions = observeQaNativeCompactions;
|
|
256
|
+
this.observeBuilderNativeCompactions = observeBuilderNativeCompactions;
|
|
251
257
|
this.ticketsEnabled = !!(projectDir && isTicketsInitialized(projectDir));
|
|
252
258
|
}
|
|
253
259
|
/**
|
|
@@ -309,6 +315,7 @@ export class Foreman {
|
|
|
309
315
|
adapter = this.builder;
|
|
310
316
|
}
|
|
311
317
|
let result = await adapter.sendTurn(instruction);
|
|
318
|
+
await this.observeBuilderNative(adapter);
|
|
312
319
|
let status = parseStepStatus(result.text);
|
|
313
320
|
if (result.failure?.category === "session-unavailable")
|
|
314
321
|
return { result, status };
|
|
@@ -318,9 +325,14 @@ export class Foreman {
|
|
|
318
325
|
}
|
|
319
326
|
else {
|
|
320
327
|
const qa = /\bQA\b|qa_pass|qa_fail/i.test(instruction);
|
|
328
|
+
if (adapter === this.builder && this.beforeBuilderTurn) {
|
|
329
|
+
this.builder = await this.beforeBuilderTurn(this.builder, instruction);
|
|
330
|
+
adapter = this.builder;
|
|
331
|
+
}
|
|
321
332
|
result = await adapter.sendTurn(qa
|
|
322
333
|
? "Protocol correction only: based on the review already completed, return exactly one final STEP_STATUS: qa_pass or STEP_STATUS: qa_fail marker. Do not repeat QA, tests, tools, or compaction."
|
|
323
334
|
: "Protocol correction only: based on the work already completed, return exactly one final STEP_STATUS: done, plan_complete, blocked, or needs_input marker. Do not repeat implementation, tools, or compaction.");
|
|
335
|
+
await this.observeBuilderNative(adapter);
|
|
324
336
|
status = parseStepStatus(result.text);
|
|
325
337
|
if (status.kind === "unknown")
|
|
326
338
|
status = { ...status, error: `protocol correction exhausted: ${status.error ?? "missing final marker"}` };
|
|
@@ -346,7 +358,12 @@ export class Foreman {
|
|
|
346
358
|
status = { kind: "blocked", reason: "user cancelled at input prompt" };
|
|
347
359
|
break;
|
|
348
360
|
}
|
|
361
|
+
if (adapter === this.builder && this.beforeBuilderTurn) {
|
|
362
|
+
this.builder = await this.beforeBuilderTurn(this.builder, String(answer));
|
|
363
|
+
adapter = this.builder;
|
|
364
|
+
}
|
|
349
365
|
result = await adapter.sendTurn(String(answer));
|
|
366
|
+
await this.observeBuilderNative(adapter);
|
|
350
367
|
status = parseStepStatus(result.text);
|
|
351
368
|
}
|
|
352
369
|
return { result, status };
|
|
@@ -357,6 +374,7 @@ export class Foreman {
|
|
|
357
374
|
if (this.beforeBuilderTurn)
|
|
358
375
|
this.builder = await this.beforeBuilderTurn(this.builder, instruction);
|
|
359
376
|
const result = await this.builder.sendTurn(instruction);
|
|
377
|
+
await this.observeBuilderNative(this.builder);
|
|
360
378
|
this.log.write("preflight", {
|
|
361
379
|
ticketsProvided: ticketsContent !== undefined,
|
|
362
380
|
costUsd: result.costUsd,
|
|
@@ -371,6 +389,7 @@ export class Foreman {
|
|
|
371
389
|
if (this.beforeBuilderTurn)
|
|
372
390
|
this.builder = await this.beforeBuilderTurn(this.builder, feedback);
|
|
373
391
|
const result = await this.builder.sendTurn(feedback);
|
|
392
|
+
await this.observeBuilderNative(this.builder);
|
|
374
393
|
this.log.write("preflight", { feedback: true, costUsd: result.costUsd, isError: result.isError });
|
|
375
394
|
if (result.isError)
|
|
376
395
|
throw new Error(result.text);
|
|
@@ -379,6 +398,10 @@ export class Foreman {
|
|
|
379
398
|
async runInstruction(instruction) {
|
|
380
399
|
return this.doTurn(instruction);
|
|
381
400
|
}
|
|
401
|
+
async observeBuilderNative(adapter) {
|
|
402
|
+
if (adapter === this.builder)
|
|
403
|
+
await this.observeBuilderNativeCompactions?.(adapter);
|
|
404
|
+
}
|
|
382
405
|
/**
|
|
383
406
|
* Run a QA review pass on the ticket the builder just completed.
|
|
384
407
|
* Loops on qa_fail → fix → re-QA until qa_pass or the cycle cap is reached.
|
|
@@ -394,6 +417,7 @@ export class Foreman {
|
|
|
394
417
|
state: this.qaStream,
|
|
395
418
|
createQa: this.qaFactory,
|
|
396
419
|
sessionBoundary: this.qaSessionBoundary,
|
|
420
|
+
observeNativeCompactions: this.observeQaNativeCompactions,
|
|
397
421
|
maxCycles: this.qaMaxCycles,
|
|
398
422
|
fix: async (issues) => {
|
|
399
423
|
const instruction = buildQaFixInstruction(issues);
|
package/dist/qaReview.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface IsolatedQaOptions {
|
|
|
29
29
|
createQa: (cwd: string, sessionId?: string) => Promise<BuilderAdapter>;
|
|
30
30
|
/** Host-owned ordinary QA boundary. Fresh strategies must return a validated successor. */
|
|
31
31
|
sessionBoundary?: (adapter: BuilderAdapter, frozenAction: string, strategy: SessionStrategy, cwd: string) => Promise<BuilderAdapter>;
|
|
32
|
+
/** Persist provider-native automatic compactions without imposing an ordinary QA boundary. */
|
|
33
|
+
observeNativeCompactions?: (adapter: BuilderAdapter) => Promise<void>;
|
|
32
34
|
fix: (issues: string) => Promise<{
|
|
33
35
|
ok: boolean;
|
|
34
36
|
detail?: string;
|
package/dist/qaReview.js
CHANGED
|
@@ -51,6 +51,7 @@ async function oneReview(opts, cycle) {
|
|
|
51
51
|
qa = await opts.createQa(snapshot.path);
|
|
52
52
|
const turn = await qa.sendTurn(handoff);
|
|
53
53
|
const status = parseStepStatus(turn.text);
|
|
54
|
+
await opts.observeNativeCompactions?.(qa);
|
|
54
55
|
opts.state.reviews += 1;
|
|
55
56
|
opts.state.sessionId = qa.sessionId();
|
|
56
57
|
opts.state.sessionRef = qa.sessionRef?.();
|
|
@@ -49,6 +49,8 @@ export declare class RoleSessionController {
|
|
|
49
49
|
static managed(options: ThresholdCompactionOptions): RoleSessionController;
|
|
50
50
|
atSafeBoundary(adapter: BuilderAdapter, frozenAction: string): Promise<ContextBoundaryResult>;
|
|
51
51
|
atWorkSessionBoundary(adapter: BuilderAdapter, frozenAction: string, strategy?: SessionStrategy): Promise<ContextBoundaryResult>;
|
|
52
|
+
/** Record provider-native automatic compaction immediately after a role turn. */
|
|
53
|
+
observeNativeCompactions(adapter: BuilderAdapter): Promise<number | undefined>;
|
|
52
54
|
effectiveSettings(): ResolvedAgentSettings;
|
|
53
55
|
effectiveThreshold(): number;
|
|
54
56
|
next(durableHandoff: string, nextSettings?: ResolvedAgentSettings): Promise<SessionBoundary>;
|
|
@@ -125,10 +127,13 @@ export declare class ThresholdCompactionController {
|
|
|
125
127
|
atWorkSessionBoundary(adapter: BuilderAdapter, frozenAction: string, strategy?: SessionStrategy): Promise<ContextBoundaryResult>;
|
|
126
128
|
effectiveSettings(): ResolvedAgentSettings;
|
|
127
129
|
effectiveThreshold(): number;
|
|
130
|
+
/** Persist provider-triggered compactions at a host-owned boundary. */
|
|
131
|
+
observeNativeCompactions(adapter: BuilderAdapter, afterSample?: ContextSample): Promise<number | undefined>;
|
|
128
132
|
private compactSession;
|
|
129
133
|
private performHandoff;
|
|
130
134
|
private adoptSettings;
|
|
131
135
|
private measure;
|
|
136
|
+
private sampleFromUsage;
|
|
132
137
|
private compactionCount;
|
|
133
138
|
private handoffGeneration;
|
|
134
139
|
private threshold;
|
package/dist/sessionLifecycle.js
CHANGED
|
@@ -67,6 +67,13 @@ export class RoleSessionController {
|
|
|
67
67
|
this.adapter = result.adapter;
|
|
68
68
|
return result;
|
|
69
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
|
+
}
|
|
70
77
|
effectiveSettings() {
|
|
71
78
|
return this.thresholdController?.effectiveSettings() ?? this.options.settings;
|
|
72
79
|
}
|
|
@@ -329,6 +336,9 @@ export class ThresholdCompactionController {
|
|
|
329
336
|
}
|
|
330
337
|
return { adapter, action: "continued", sample, effectiveThreshold: this.threshold(), compactionCount: 0 };
|
|
331
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);
|
|
332
342
|
let sample = await this.measure(adapter, "provider-query");
|
|
333
343
|
const threshold = this.threshold();
|
|
334
344
|
let session = adapter.sessionRef?.() ?? adapter.sessionId();
|
|
@@ -340,12 +350,20 @@ export class ThresholdCompactionController {
|
|
|
340
350
|
let count = 0;
|
|
341
351
|
try {
|
|
342
352
|
db.recordContextSample(sample);
|
|
343
|
-
count = db.successfulCompactionCount(this.options.runId, this.options.role, session);
|
|
353
|
+
count = nativeCount ?? db.successfulCompactionCount(this.options.runId, this.options.role, session);
|
|
344
354
|
}
|
|
345
355
|
finally {
|
|
346
356
|
db.close();
|
|
347
357
|
}
|
|
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
|
+
}
|
|
348
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`);
|
|
349
367
|
this.armed = true;
|
|
350
368
|
return { adapter, action: "below-threshold", sample, effectiveThreshold: threshold, compactionCount: count };
|
|
351
369
|
}
|
|
@@ -385,12 +403,13 @@ export class ThresholdCompactionController {
|
|
|
385
403
|
/** Apply the configured ordinary work-session boundary without bypassing handoff or compaction accounting. */
|
|
386
404
|
async atWorkSessionBoundary(adapter, frozenAction, strategy = this.settings.session_strategy) {
|
|
387
405
|
adapter = await this.adoptSettings(adapter, frozenAction);
|
|
406
|
+
const nativeCount = await this.observeNativeCompactions(adapter);
|
|
388
407
|
const sample = await this.measure(adapter, "provider-query");
|
|
389
408
|
const db = new WorkflowDb(this.options.projectDir);
|
|
390
409
|
let count = 0;
|
|
391
410
|
try {
|
|
392
411
|
db.recordContextSample(sample);
|
|
393
|
-
count = db.successfulCompactionCount(this.options.runId, this.options.role, adapter.sessionRef?.() ?? adapter.sessionId());
|
|
412
|
+
count = nativeCount ?? db.successfulCompactionCount(this.options.runId, this.options.role, adapter.sessionRef?.() ?? adapter.sessionId());
|
|
394
413
|
}
|
|
395
414
|
finally {
|
|
396
415
|
db.close();
|
|
@@ -403,6 +422,54 @@ export class ThresholdCompactionController {
|
|
|
403
422
|
}
|
|
404
423
|
effectiveSettings() { return this.settings; }
|
|
405
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);
|
|
437
|
+
const db = new WorkflowDb(this.options.projectDir);
|
|
438
|
+
try {
|
|
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 });
|
|
453
|
+
}
|
|
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);
|
|
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;
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
db.close();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
406
473
|
async compactSession(adapter, sample, count, frozenAction, crossingKey, thresholdCrossing, freshBootstrap) {
|
|
407
474
|
const threshold = this.threshold();
|
|
408
475
|
const sessionId = adapter.sessionId();
|
|
@@ -558,6 +625,7 @@ export class ThresholdCompactionController {
|
|
|
558
625
|
|| next.model !== this.settings.model
|
|
559
626
|
|| next.reasoning !== this.settings.reasoning
|
|
560
627
|
|| next.fast !== this.settings.fast;
|
|
628
|
+
const nativeCompactionChanged = next.auto_compact_threshold_percent !== this.settings.auto_compact_threshold_percent;
|
|
561
629
|
let adoptedAdapter = adapter;
|
|
562
630
|
if (providerControlsChanged) {
|
|
563
631
|
if (this.options.settingsBoundary) {
|
|
@@ -577,6 +645,16 @@ export class ThresholdCompactionController {
|
|
|
577
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}`);
|
|
578
646
|
}
|
|
579
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`);
|
|
651
|
+
try {
|
|
652
|
+
await adapter.prepareAutoCompaction(next.auto_compact_threshold_percent);
|
|
653
|
+
}
|
|
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)}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
580
658
|
this.settings = next;
|
|
581
659
|
this.runOnlyThreshold = undefined;
|
|
582
660
|
const db = new WorkflowDb(this.options.projectDir);
|
|
@@ -606,6 +684,9 @@ export class ThresholdCompactionController {
|
|
|
606
684
|
}
|
|
607
685
|
if (!usage || !Number.isFinite(usage.used))
|
|
608
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);
|
|
688
|
+
}
|
|
689
|
+
sampleFromUsage(adapter, usage, source) {
|
|
609
690
|
const percentage = usage.percentage ?? (usage.maximum ? usage.used / usage.maximum * 100 : undefined);
|
|
610
691
|
if (percentage === undefined || !Number.isFinite(percentage))
|
|
611
692
|
throw new ContextCapabilityError(this.options.runId, this.options.role, "provider context occupancy lacks a usable percentage and maximum");
|
package/dist/statusReporter.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContextSample, SessionUsageSample } from "rafi-spec";
|
|
2
|
-
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./adapters/types.js";
|
|
2
|
+
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./adapters/types.js";
|
|
3
3
|
export interface AgentStatusSnapshot {
|
|
4
4
|
role: "builder" | "qa";
|
|
5
5
|
provider: "claude" | "codex";
|
|
@@ -82,6 +82,10 @@ export declare class RoleStatusAdapter implements BuilderAdapter {
|
|
|
82
82
|
adoptSessionRef(ref: import("rafi-spec").ProviderSessionRefV1): void;
|
|
83
83
|
validateSession(): Promise<import("rafi-spec").SessionAvailabilityV1>;
|
|
84
84
|
compact(): Promise<CompactResult>;
|
|
85
|
+
prepareAutoCompaction(thresholdPercent?: number): Promise<void>;
|
|
86
|
+
drainNativeCompactions(): import("./adapters/types.js").NativeCompaction[];
|
|
87
|
+
restoreNativeCompactions(compactions: NativeCompaction[]): void;
|
|
88
|
+
contextUsageAfterNativeCompaction(compaction: NativeCompaction): Promise<ContextUsage | undefined>;
|
|
85
89
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
86
90
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
87
91
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
package/dist/statusReporter.js
CHANGED
|
@@ -177,6 +177,10 @@ export class RoleStatusAdapter {
|
|
|
177
177
|
adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
|
|
178
178
|
validateSession() { return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped" }); }
|
|
179
179
|
compact() { this.onActive(this.adapter); return this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" }); }
|
|
180
|
+
prepareAutoCompaction(thresholdPercent) { return this.adapter.prepareAutoCompaction?.(thresholdPercent) ?? Promise.resolve(); }
|
|
181
|
+
drainNativeCompactions() { return this.adapter.drainNativeCompactions?.() ?? []; }
|
|
182
|
+
restoreNativeCompactions(compactions) { this.adapter.restoreNativeCompactions?.(compactions); }
|
|
183
|
+
contextUsageAfterNativeCompaction(compaction) { return this.adapter.contextUsageAfterNativeCompaction?.(compaction) ?? Promise.resolve(undefined); }
|
|
180
184
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
181
185
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
182
186
|
switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
|
package/dist/workflowDb.d.ts
CHANGED
|
@@ -192,10 +192,18 @@ export declare class WorkflowDb {
|
|
|
192
192
|
afterSample?: ContextSample;
|
|
193
193
|
error?: string;
|
|
194
194
|
}, now?: Date): CompactionAttemptRecord;
|
|
195
|
+
/** Attach an eventually available measurement without changing confirmed accounting. */
|
|
196
|
+
recordCompactionAfterSample(idempotencyKey: string, afterSample: ContextSample, now?: Date): CompactionAttemptRecord;
|
|
195
197
|
compactionAttempt(idempotencyKey: string): CompactionAttemptRecord | undefined;
|
|
196
198
|
successfulCompactionCount(runId: string, role: "builder" | "qa", providerSession?: string | ProviderSessionRefV1): number;
|
|
197
199
|
recordRecoveryDecision(receipt: BuildRecoveryDecisionReceipt): void;
|
|
198
200
|
recoveryDecisions(runId: string): BuildRecoveryDecisionReceipt[];
|
|
201
|
+
/**
|
|
202
|
+
* A process may die after dispatching a provider turn but before its durable
|
|
203
|
+
* continuity checkpoint. The provider may have compacted during that gap, so
|
|
204
|
+
* recovery must not treat that role session's compaction history as complete.
|
|
205
|
+
*/
|
|
206
|
+
hasUncheckpointedRoleTurn(runId: string, role: "builder" | "qa"): boolean;
|
|
199
207
|
beginPublication(runId: string, intent: unknown, previousDigests: unknown, now?: Date): PublicationTransaction;
|
|
200
208
|
updatePublication(transactionId: string, status: PublicationTransaction["status"], now?: Date): PublicationTransaction;
|
|
201
209
|
publication(transactionId: string): PublicationTransaction | undefined;
|
package/dist/workflowDb.js
CHANGED
|
@@ -455,6 +455,17 @@ export class WorkflowDb {
|
|
|
455
455
|
this.insertEvent(prior.runId, input.ok ? "compaction_succeeded" : "compaction_failed", `context:${prior.role}`, { idempotencyKey, providerSessionId: prior.providerSessionId, sessionKey: prior.sessionKey, crossingKey: prior.crossingKey }, now.toISOString());
|
|
456
456
|
return this.compactionAttempt(idempotencyKey);
|
|
457
457
|
}
|
|
458
|
+
/** Attach an eventually available measurement without changing confirmed accounting. */
|
|
459
|
+
recordCompactionAfterSample(idempotencyKey, afterSample, now = new Date()) {
|
|
460
|
+
const prior = this.compactionAttempt(idempotencyKey);
|
|
461
|
+
if (!prior)
|
|
462
|
+
throw new Error(`compaction attempt not found: ${idempotencyKey}`);
|
|
463
|
+
if (prior.status !== "succeeded")
|
|
464
|
+
return prior;
|
|
465
|
+
this.db.prepare("UPDATE compaction_attempts SET after_sample_json=?,updated_at=? WHERE idempotency_key=?")
|
|
466
|
+
.run(json(afterSample), now.toISOString(), idempotencyKey);
|
|
467
|
+
return this.compactionAttempt(idempotencyKey);
|
|
468
|
+
}
|
|
458
469
|
compactionAttempt(idempotencyKey) {
|
|
459
470
|
const row = this.db.prepare("SELECT * FROM compaction_attempts WHERE idempotency_key=?").get(idempotencyKey);
|
|
460
471
|
return row ? compactionAttemptFromRow(row) : undefined;
|
|
@@ -481,6 +492,25 @@ export class WorkflowDb {
|
|
|
481
492
|
recoveryDecisions(runId) {
|
|
482
493
|
return this.db.prepare("SELECT receipt_json FROM recovery_decisions WHERE run_id=? ORDER BY decision_id").all(runId).map((row) => parseJson(row.receipt_json));
|
|
483
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* A process may die after dispatching a provider turn but before its durable
|
|
497
|
+
* continuity checkpoint. The provider may have compacted during that gap, so
|
|
498
|
+
* recovery must not treat that role session's compaction history as complete.
|
|
499
|
+
*/
|
|
500
|
+
hasUncheckpointedRoleTurn(runId, role) {
|
|
501
|
+
const events = this.continuityEvents(runId);
|
|
502
|
+
let startSequence = 0;
|
|
503
|
+
for (const event of events) {
|
|
504
|
+
if (event.kind === "turn_started" && event.role === "host" && event.payload.role === role) {
|
|
505
|
+
startSequence = event.sequence;
|
|
506
|
+
}
|
|
507
|
+
else if (startSequence && event.sequence > startSequence
|
|
508
|
+
&& (event.kind === "turn_completed" || event.kind === "turn_completed_after_repair" || event.kind === "fresh_successor_accepted")) {
|
|
509
|
+
startSequence = 0;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return startSequence > 0;
|
|
513
|
+
}
|
|
484
514
|
beginPublication(runId, intent, previousDigests, now = new Date()) {
|
|
485
515
|
const transactionId = randomUUID();
|
|
486
516
|
const at = now.toISOString();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-foreman",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.9",
|
|
4
4
|
"description": "An orchestrator that keeps Codex CLI / Claude Code builders moving through their step list unattended. Part of Rafi.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -158,8 +158,8 @@
|
|
|
158
158
|
"@anthropic-ai/claude-agent-sdk": "^0.3.148"
|
|
159
159
|
},
|
|
160
160
|
"dependencies": {
|
|
161
|
-
"special-agents": "0.8.
|
|
162
|
-
"rafi-spec": "0.8.
|
|
161
|
+
"special-agents": "0.8.7",
|
|
162
|
+
"rafi-spec": "0.8.7",
|
|
163
163
|
"@clack/prompts": "^1.4.0",
|
|
164
164
|
"ajv": "^8.20.0",
|
|
165
165
|
"ajv-formats": "^3.0.1",
|