ai-foreman 1.7.8 → 1.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -11
- package/dist/activity.js +2 -2
- package/dist/adapters/claude.d.ts +13 -29
- package/dist/adapters/claude.js +76 -334
- package/dist/adapters/codex.d.ts +13 -18
- package/dist/adapters/codex.js +103 -195
- package/dist/adapters/recovering.d.ts +5 -6
- package/dist/adapters/recovering.js +5 -9
- package/dist/adapters/types.d.ts +24 -66
- package/dist/branch/currentGuard.d.ts +5 -5
- package/dist/branch/currentGuard.js +4 -6
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +2 -1
- package/dist/cli/start.js +90 -165
- package/dist/continuity.d.ts +6 -8
- package/dist/continuity.js +26 -72
- package/dist/foreman.d.ts +7 -1
- package/dist/foreman.js +25 -1
- package/dist/handoffs.js +3 -223
- package/dist/qaReview.d.ts +2 -0
- package/dist/qaReview.js +1 -4
- package/dist/sessionLifecycle.d.ts +19 -108
- package/dist/sessionLifecycle.js +310 -1024
- package/dist/statusReporter.d.ts +5 -27
- package/dist/statusReporter.js +20 -76
- package/dist/workflowDb.d.ts +10 -47
- package/dist/workflowDb.js +41 -146
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -115,9 +115,7 @@ What each part does:
|
|
|
115
115
|
| `--branch-prefix <prefix>` | `feature` / `rafi` / another valid Git prefix | `feature` | Prefix for generated ticket branches. Explicit prefixes are preserved verbatim. |
|
|
116
116
|
| `--max-branch-depth <n>` | positive integer | `5` | Maximum selected branch/PR stack depth (root PR is depth 1). |
|
|
117
117
|
| `--show-session-cost` / `--hide-session-cost` | flags | saved role preferences | Show authoritative provider cost or trustworthy cumulative tokens for Builder and QA; does not change context occupancy display. |
|
|
118
|
-
| `--auto-compact-threshold <percent>` | integer `1`–`99` | saved Builder value, then `50` | Initial
|
|
119
|
-
|
|
120
|
-
Builder and QA context ceilings are configured independently (default `50%`, maximum `10` successful compactions per scoped provider session). Claude and Codex install the ceiling as a native in-turn auto-compaction limit before real work begins; Rafi verifies the post-compaction occupancy and counts provider-native and host-requested compactions together. An ineffective or failed compact receives one bounded retry, followed by a validated cumulative handoff. Status output reports the configured and installed ceilings, lifecycle state, scoped count/maximum, and settings revision without substituting cumulative session tokens for active context occupancy.
|
|
118
|
+
| `--auto-compact-threshold <percent>` | integer `1`–`99` | saved Builder value, then `50` | Initial run threshold override. A newer persistent settings revision wins at the next safe boundary. |
|
|
121
119
|
|
|
122
120
|
Auto-detected tracker files:
|
|
123
121
|
|
|
@@ -609,14 +607,6 @@ pnpm build
|
|
|
609
607
|
pnpm dev -- start ../../examples/dummy-project --steps 2
|
|
610
608
|
```
|
|
611
609
|
|
|
612
|
-
Changes to provider adapters or context lifecycle enforcement also require the opt-in authenticated gate:
|
|
613
|
-
|
|
614
|
-
```bash
|
|
615
|
-
pnpm test:live-providers
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
It exercises Claude and Codex as both Builder and QA with a low native ceiling, verifies in-turn compaction and same-session continuation, then verifies a maximum-one boundary interruption and validated fresh handoff. Set `RAFI_LIVE_CONTEXT_THRESHOLD_PERCENT` to an integer from `1` through `10` to override its default `10%` ceiling. Very low Claude ceilings can intentionally fail the capability check when they fall below the SDK's minimum native window. The gate uses locally authenticated provider CLIs and is intentionally skipped by the ordinary test suite.
|
|
619
|
-
|
|
620
610
|
Package output:
|
|
621
611
|
|
|
622
612
|
- The package publishes an `ai-foreman` binary from `dist/index.js`.
|
package/dist/activity.js
CHANGED
|
@@ -236,8 +236,8 @@ export function reportBuilderEvent(event) {
|
|
|
236
236
|
reporter.update("processing agent response");
|
|
237
237
|
else if (event.kind === "session-transition")
|
|
238
238
|
reporter.update(`session ${event.transition}`, event.detail);
|
|
239
|
-
else if (event.kind === "context-
|
|
240
|
-
reporter.
|
|
239
|
+
else if (event.kind === "context-usage")
|
|
240
|
+
reporter.pulse(event.percentage === undefined ? undefined : `context ${event.percentage.toFixed(0)}%`);
|
|
241
241
|
else if (event.kind === "turn-complete")
|
|
242
242
|
reporter.update(event.result.isError ? "agent turn failed" : "agent turn complete");
|
|
243
243
|
else if (event.kind === "error")
|
|
@@ -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,
|
|
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()`.
|
|
@@ -43,14 +43,11 @@ export declare function claudeApiRetryEvent(message: {
|
|
|
43
43
|
export declare class ClaudeAdapter implements BuilderAdapter {
|
|
44
44
|
private readonly opts;
|
|
45
45
|
readonly agent: "claude";
|
|
46
|
-
private inbox;
|
|
46
|
+
private readonly inbox;
|
|
47
47
|
private readonly eventQueue;
|
|
48
|
-
private query;
|
|
49
|
-
private abort;
|
|
50
|
-
private pumpDone;
|
|
51
|
-
private readonly queryFactory;
|
|
52
|
-
private transportGeneration;
|
|
53
|
-
private transportObservedSessionId?;
|
|
48
|
+
private readonly query;
|
|
49
|
+
private readonly abort;
|
|
50
|
+
private readonly pumpDone;
|
|
54
51
|
private _sessionId?;
|
|
55
52
|
private _sessionRef?;
|
|
56
53
|
private readonly stderrChunks;
|
|
@@ -58,19 +55,11 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
58
55
|
private structuredError?;
|
|
59
56
|
private apiErrorStatus?;
|
|
60
57
|
private compactResult?;
|
|
61
|
-
private
|
|
62
|
-
private
|
|
63
|
-
private
|
|
64
|
-
private
|
|
65
|
-
private
|
|
66
|
-
private boundaryInterrupt?;
|
|
67
|
-
private compactionTerminalObserved;
|
|
68
|
-
private lastContextUsage?;
|
|
69
|
-
/** Full provider context capacity, before Rafi narrows Claude's auto-compact window. */
|
|
70
|
-
private modelContextWindow?;
|
|
71
|
-
/** SDK-owned space between autoCompactWindow and the actual used-token trigger. */
|
|
72
|
-
private nativeAutoCompactReserve?;
|
|
73
|
-
private contextPrepared;
|
|
58
|
+
private autoCompactionPrepared;
|
|
59
|
+
private preparedAutoCompactThreshold?;
|
|
60
|
+
private manualCompactionInFlight;
|
|
61
|
+
private nativeCompactions;
|
|
62
|
+
private nativeCompactionSequence;
|
|
74
63
|
private cumulativeUsage;
|
|
75
64
|
private pending?;
|
|
76
65
|
private terminalResult?;
|
|
@@ -78,7 +67,6 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
78
67
|
private closed;
|
|
79
68
|
static create(opts: BuilderAdapterOptions): Promise<ClaudeAdapter>;
|
|
80
69
|
private constructor();
|
|
81
|
-
private openQuery;
|
|
82
70
|
/** Background loop: consume the SDK message stream until it ends. */
|
|
83
71
|
private pump;
|
|
84
72
|
private handle;
|
|
@@ -90,19 +78,15 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
90
78
|
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
91
79
|
validateSession(): Promise<SessionAvailabilityV1>;
|
|
92
80
|
compact(): Promise<CompactResult>;
|
|
81
|
+
prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<void>;
|
|
82
|
+
drainNativeCompactions(): NativeCompaction[];
|
|
83
|
+
restoreNativeCompactions(compactions: NativeCompaction[]): void;
|
|
93
84
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
94
|
-
prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
95
|
-
updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
96
|
-
interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
|
|
97
85
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
98
86
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
99
87
|
events(): AsyncIterable<BuilderEvent>;
|
|
100
88
|
close(): Promise<void>;
|
|
101
89
|
private observeSession;
|
|
102
|
-
private makeContextUsage;
|
|
103
|
-
private configureContextManagement;
|
|
104
|
-
private restartTransport;
|
|
105
|
-
private completeCompaction;
|
|
106
90
|
private settlePending;
|
|
107
91
|
private streamFailureResult;
|
|
108
92
|
}
|
package/dist/adapters/claude.js
CHANGED
|
@@ -145,10 +145,6 @@ export class ClaudeAdapter {
|
|
|
145
145
|
query;
|
|
146
146
|
abort = new AbortController();
|
|
147
147
|
pumpDone;
|
|
148
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
-
queryFactory;
|
|
150
|
-
transportGeneration = 0;
|
|
151
|
-
transportObservedSessionId;
|
|
152
148
|
_sessionId;
|
|
153
149
|
_sessionRef;
|
|
154
150
|
stderrChunks = [];
|
|
@@ -156,19 +152,11 @@ export class ClaudeAdapter {
|
|
|
156
152
|
structuredError;
|
|
157
153
|
apiErrorStatus;
|
|
158
154
|
compactResult;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
boundaryInterrupt;
|
|
165
|
-
compactionTerminalObserved = false;
|
|
166
|
-
lastContextUsage;
|
|
167
|
-
/** Full provider context capacity, before Rafi narrows Claude's auto-compact window. */
|
|
168
|
-
modelContextWindow;
|
|
169
|
-
/** SDK-owned space between autoCompactWindow and the actual used-token trigger. */
|
|
170
|
-
nativeAutoCompactReserve;
|
|
171
|
-
contextPrepared = false;
|
|
155
|
+
autoCompactionPrepared = false;
|
|
156
|
+
preparedAutoCompactThreshold;
|
|
157
|
+
manualCompactionInFlight = false;
|
|
158
|
+
nativeCompactions = [];
|
|
159
|
+
nativeCompactionSequence = 0;
|
|
172
160
|
cumulativeUsage = { observedAt: new Date(0).toISOString(), source: "provider" };
|
|
173
161
|
pending;
|
|
174
162
|
terminalResult;
|
|
@@ -212,30 +200,18 @@ export class ClaudeAdapter {
|
|
|
212
200
|
}
|
|
213
201
|
}
|
|
214
202
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
215
|
-
constructor(opts,
|
|
203
|
+
constructor(opts, query) {
|
|
216
204
|
this.opts = opts;
|
|
217
205
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
218
206
|
this._sessionRef = opts.resumeSessionRef;
|
|
219
|
-
this.
|
|
220
|
-
this.query = this.openQuery();
|
|
221
|
-
const generation = ++this.transportGeneration;
|
|
222
|
-
this.pumpDone = this.pump(this.query, generation);
|
|
223
|
-
}
|
|
224
|
-
openQuery(nativeAutoCompactWindow) {
|
|
225
|
-
const scopedOpts = this._sessionRef
|
|
226
|
-
? { ...this.opts, resumeSessionId: this._sessionRef.sessionId, resumeSessionRef: this._sessionRef }
|
|
227
|
-
: this.opts;
|
|
228
|
-
return this.queryFactory({
|
|
207
|
+
this.query = query({
|
|
229
208
|
prompt: this.inbox,
|
|
230
209
|
options: {
|
|
231
|
-
...buildClaudeQueryOptions(
|
|
232
|
-
...(nativeAutoCompactWindow === undefined ? {} : {
|
|
233
|
-
settings: { autoCompactEnabled: true, autoCompactWindow: nativeAutoCompactWindow },
|
|
234
|
-
}),
|
|
210
|
+
...buildClaudeQueryOptions(opts),
|
|
235
211
|
abortController: this.abort,
|
|
236
212
|
stderr: (data) => this.captureStderr(data),
|
|
237
213
|
canUseTool: async (toolName, input, requestOptions = {}) => {
|
|
238
|
-
const decision = await
|
|
214
|
+
const decision = await opts.permission({
|
|
239
215
|
toolName,
|
|
240
216
|
input,
|
|
241
217
|
signal: requestOptions.signal,
|
|
@@ -250,22 +226,16 @@ export class ClaudeAdapter {
|
|
|
250
226
|
},
|
|
251
227
|
},
|
|
252
228
|
});
|
|
229
|
+
this.pumpDone = this.pump();
|
|
253
230
|
}
|
|
254
231
|
/** Background loop: consume the SDK message stream until it ends. */
|
|
255
|
-
async pump(
|
|
232
|
+
async pump() {
|
|
256
233
|
try {
|
|
257
|
-
for await (const msg of query) {
|
|
258
|
-
|
|
259
|
-
return;
|
|
260
|
-
// Keep provider messages ordered through post-compaction measurement. In
|
|
261
|
-
// particular, a result message must not overtake the fresh context sample
|
|
262
|
-
// produced for the preceding compact_result/compact_boundary message.
|
|
263
|
-
await this.handle(msg);
|
|
234
|
+
for await (const msg of this.query) {
|
|
235
|
+
this.handle(msg);
|
|
264
236
|
}
|
|
265
237
|
}
|
|
266
238
|
catch (err) {
|
|
267
|
-
if (generation !== this.transportGeneration)
|
|
268
|
-
return;
|
|
269
239
|
// Suppress the AbortError that fires when close() aborts the stream.
|
|
270
240
|
const isShutdownAbort = this.closed &&
|
|
271
241
|
(err instanceof Error &&
|
|
@@ -280,8 +250,6 @@ export class ClaudeAdapter {
|
|
|
280
250
|
}
|
|
281
251
|
}
|
|
282
252
|
finally {
|
|
283
|
-
if (generation !== this.transportGeneration)
|
|
284
|
-
return;
|
|
285
253
|
this.streamEnded = true;
|
|
286
254
|
if (!this.closed && this.pending) {
|
|
287
255
|
const result = this.streamFailureResult("Claude stream ended without a result", Boolean(this.opts.resumeSessionRef ?? this.opts.resumeSessionId));
|
|
@@ -294,9 +262,8 @@ export class ClaudeAdapter {
|
|
|
294
262
|
this.eventQueue.close();
|
|
295
263
|
}
|
|
296
264
|
}
|
|
297
|
-
|
|
265
|
+
handle(msg) {
|
|
298
266
|
if ("session_id" in msg && typeof msg.session_id === "string") {
|
|
299
|
-
this.transportObservedSessionId = msg.session_id;
|
|
300
267
|
this._sessionId = msg.session_id;
|
|
301
268
|
this.observeSession(msg.session_id);
|
|
302
269
|
}
|
|
@@ -325,43 +292,16 @@ export class ClaudeAdapter {
|
|
|
325
292
|
this.turnSignals.push(...msg.output.map((line) => `auth: ${line}`));
|
|
326
293
|
}
|
|
327
294
|
else if (msg.type === "system" && msg.subtype === "status") {
|
|
328
|
-
if (msg.status === "compacting")
|
|
329
|
-
this.compactionTerminalObserved = false;
|
|
295
|
+
if (msg.status === "compacting")
|
|
330
296
|
this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
|
|
331
|
-
const providerSequence = ++this.compactionSequence;
|
|
332
|
-
const origin = this.manualCompactionPending ? "rafi-manual" : "provider-auto";
|
|
333
|
-
this.activeCompaction = { providerEventId: msg.uuid, providerSequence, origin };
|
|
334
|
-
this.eventQueue.push({
|
|
335
|
-
kind: "context-compaction", phase: "started", origin, providerEventId: msg.uuid,
|
|
336
|
-
providerSequence, provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef,
|
|
337
|
-
observedAt: new Date().toISOString(),
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
297
|
if (msg.compact_result === "success") {
|
|
341
298
|
this.compactResult = { ok: true };
|
|
342
299
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
|
|
343
|
-
if (!this.
|
|
344
|
-
|
|
300
|
+
if (!this.manualCompactionInFlight)
|
|
301
|
+
this.nativeCompactions.push({ id: `claude:${this._sessionId ?? "unknown"}:${++this.nativeCompactionSequence}`, occurredAt: new Date().toISOString(), provider: "claude" });
|
|
345
302
|
}
|
|
346
303
|
else if (msg.compact_result === "failed") {
|
|
347
304
|
this.compactResult = { ok: false, error: sanitizeDiagnostics(msg.compact_error ?? "Claude native compaction failed") };
|
|
348
|
-
if (!this.compactionTerminalObserved)
|
|
349
|
-
await this.completeCompaction(false, this.compactResult.error);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
else if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
353
|
-
if (!this.compactionTerminalObserved) {
|
|
354
|
-
// The boundary is itself a successful native compact signal. Query the
|
|
355
|
-
// SDK immediately and use its post_tokens only as a fallback if that
|
|
356
|
-
// authoritative query is unavailable.
|
|
357
|
-
if (!this.activeCompaction) {
|
|
358
|
-
this.activeCompaction = {
|
|
359
|
-
providerEventId: msg.uuid,
|
|
360
|
-
providerSequence: ++this.compactionSequence,
|
|
361
|
-
origin: msg.compact_metadata.trigger === "manual" ? "rafi-manual" : "provider-auto",
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
await this.completeCompaction(true, undefined, msg.compact_metadata.post_tokens);
|
|
365
305
|
}
|
|
366
306
|
}
|
|
367
307
|
else if (msg.type === "system" && msg.subtype === "api_retry") {
|
|
@@ -423,9 +363,7 @@ export class ClaudeAdapter {
|
|
|
423
363
|
costUsd: msg.total_cost_usd,
|
|
424
364
|
costAuthoritative: Number.isFinite(msg.total_cost_usd),
|
|
425
365
|
failure,
|
|
426
|
-
...(this.boundaryInterrupt ? { interrupted: { reason: "compaction-boundary", ...(this.boundaryInterrupt.providerEventId ? { providerEventId: this.boundaryInterrupt.providerEventId } : {}) } } : {}),
|
|
427
366
|
};
|
|
428
|
-
this.boundaryInterrupt = undefined;
|
|
429
367
|
const rawResult = msg;
|
|
430
368
|
const mergedUsage = mergeClaudeProviderSessionUsage(this.cumulativeUsage, rawResult);
|
|
431
369
|
result.inputTokens = mergedUsage.inputTokens;
|
|
@@ -440,11 +378,7 @@ export class ClaudeAdapter {
|
|
|
440
378
|
}
|
|
441
379
|
}
|
|
442
380
|
sendTurn(text) {
|
|
443
|
-
return withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`,
|
|
444
|
-
if (this.opts.contextManagementPolicy && !this.contextPrepared)
|
|
445
|
-
await this.prepareContextManagement(this.opts.contextManagementPolicy);
|
|
446
|
-
return this.sendTurnInternal(text);
|
|
447
|
-
});
|
|
381
|
+
return withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(text));
|
|
448
382
|
}
|
|
449
383
|
sendTurnInternal(text) {
|
|
450
384
|
if (this.closed)
|
|
@@ -496,25 +430,62 @@ export class ClaudeAdapter {
|
|
|
496
430
|
}
|
|
497
431
|
async compact() {
|
|
498
432
|
this.compactResult = undefined;
|
|
499
|
-
this.
|
|
433
|
+
this.manualCompactionInFlight = true;
|
|
434
|
+
let result;
|
|
500
435
|
try {
|
|
501
|
-
|
|
502
|
-
if (result.failure?.category === "session-unavailable") {
|
|
503
|
-
return { ok: false, error: result.text || result.failure.diagnostics, failure: result.failure };
|
|
504
|
-
}
|
|
505
|
-
if (this.compactResult)
|
|
506
|
-
return this.compactResult;
|
|
507
|
-
return { ok: false, error: sanitizeDiagnostics(result.text || "Claude did not emit an explicit compact status") };
|
|
436
|
+
result = await this.sendTurn("/compact");
|
|
508
437
|
}
|
|
509
438
|
finally {
|
|
510
|
-
this.
|
|
439
|
+
this.manualCompactionInFlight = false;
|
|
440
|
+
}
|
|
441
|
+
if (result.failure?.category === "session-unavailable") {
|
|
442
|
+
return { ok: false, error: result.text || result.failure.diagnostics, failure: result.failure };
|
|
511
443
|
}
|
|
444
|
+
if (this.compactResult)
|
|
445
|
+
return this.compactResult;
|
|
446
|
+
return { ok: false, error: sanitizeDiagnostics(result.text || "Claude did not emit an explicit compact status") };
|
|
512
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); }
|
|
513
485
|
async contextUsage() {
|
|
514
486
|
try {
|
|
515
487
|
const usage = await this.query.getContextUsage();
|
|
516
|
-
const result =
|
|
517
|
-
this.lastContextUsage = result;
|
|
488
|
+
const result = { used: usage.totalTokens, maximum: usage.maxTokens, percentage: usage.percentage, observedAt: new Date().toISOString(), source: "provider-query" };
|
|
518
489
|
this.eventQueue.push({ kind: "context-usage", ...result });
|
|
519
490
|
return result;
|
|
520
491
|
}
|
|
@@ -522,32 +493,6 @@ export class ClaudeAdapter {
|
|
|
522
493
|
return undefined;
|
|
523
494
|
}
|
|
524
495
|
}
|
|
525
|
-
async prepareContextManagement(policy) {
|
|
526
|
-
validateContextPolicy(policy);
|
|
527
|
-
if (this.contextPrepared)
|
|
528
|
-
return this.configureContextManagement(policy, false);
|
|
529
|
-
await this.query.initializationResult();
|
|
530
|
-
const prepared = await this.configureContextManagement(policy, true);
|
|
531
|
-
this.contextPrepared = true;
|
|
532
|
-
return prepared;
|
|
533
|
-
}
|
|
534
|
-
async updateContextManagement(policy) {
|
|
535
|
-
validateContextPolicy(policy);
|
|
536
|
-
return this.configureContextManagement(policy, false);
|
|
537
|
-
}
|
|
538
|
-
async interruptTurnAtCompactionBoundary(providerEventId) {
|
|
539
|
-
if (!this.pending)
|
|
540
|
-
return { ok: false, error: "no active Claude turn exists at the compaction boundary", providerEventId };
|
|
541
|
-
try {
|
|
542
|
-
this.boundaryInterrupt = { providerEventId };
|
|
543
|
-
await this.query.interrupt();
|
|
544
|
-
return { ok: true, providerEventId };
|
|
545
|
-
}
|
|
546
|
-
catch (error) {
|
|
547
|
-
this.boundaryInterrupt = undefined;
|
|
548
|
-
return { ok: false, error: error instanceof Error ? error.message : String(error), providerEventId };
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
496
|
async sessionUsage() {
|
|
552
497
|
return this.cumulativeUsage.observedAt === new Date(0).toISOString() ? undefined : { ...this.cumulativeUsage };
|
|
553
498
|
}
|
|
@@ -556,14 +501,11 @@ export class ClaudeAdapter {
|
|
|
556
501
|
return { ok: false, error: "Claude SDK cannot change reasoning/fast controls on an existing transport" };
|
|
557
502
|
if (settings.model === this.opts.model)
|
|
558
503
|
return { ok: true };
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
catch (error) {
|
|
565
|
-
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
566
|
-
}
|
|
504
|
+
const result = await this.sendTurn(`/model ${settings.model ?? "default"}`);
|
|
505
|
+
if (result.isError)
|
|
506
|
+
return { ok: false, error: result.text, ...(result.failure ? { failure: result.failure } : {}) };
|
|
507
|
+
this.opts.model = settings.model;
|
|
508
|
+
return { ok: true };
|
|
567
509
|
}
|
|
568
510
|
events() {
|
|
569
511
|
return this.eventQueue;
|
|
@@ -592,200 +534,6 @@ export class ClaudeAdapter {
|
|
|
592
534
|
source: "observed",
|
|
593
535
|
});
|
|
594
536
|
}
|
|
595
|
-
makeContextUsage(usage) {
|
|
596
|
-
const maximum = this.modelContextWindow
|
|
597
|
-
?? (Number.isFinite(usage.maxTokens) && usage.maxTokens > 0 ? usage.maxTokens : this.lastContextUsage?.maximum);
|
|
598
|
-
// Once Rafi installs a smaller autoCompactWindow, the SDK's percentage and
|
|
599
|
-
// maxTokens describe that artificial window. Enforcement and display stay
|
|
600
|
-
// relative to the full model capacity discovered before reconfiguration.
|
|
601
|
-
const percentage = maximum ? usage.totalTokens / maximum * 100
|
|
602
|
-
: Number.isFinite(usage.percentage) ? usage.percentage : undefined;
|
|
603
|
-
return {
|
|
604
|
-
used: usage.totalTokens,
|
|
605
|
-
...(maximum ? { maximum } : {}),
|
|
606
|
-
...(percentage !== undefined ? { percentage } : {}),
|
|
607
|
-
observedAt: new Date().toISOString(),
|
|
608
|
-
source: "provider-query",
|
|
609
|
-
sequence: ++this.contextSequence,
|
|
610
|
-
...(this._sessionId ? { sessionId: this._sessionId } : {}),
|
|
611
|
-
...(this.opts.model ?? usage.model ? { model: this.opts.model ?? usage.model } : {}),
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
async configureContextManagement(policy, initial) {
|
|
615
|
-
const modelChanged = this.contextPolicy !== undefined && this.contextPolicy.model !== policy.model;
|
|
616
|
-
if (modelChanged) {
|
|
617
|
-
this.modelContextWindow = undefined;
|
|
618
|
-
this.nativeAutoCompactReserve = undefined;
|
|
619
|
-
await this.restartTransport();
|
|
620
|
-
}
|
|
621
|
-
this.contextPolicy = { ...policy };
|
|
622
|
-
this.compactionSequence = Math.max(this.compactionSequence, policy.providerSequenceStart ?? 0);
|
|
623
|
-
this.contextSequence = Math.max(this.contextSequence, policy.providerSequenceStart ?? 0);
|
|
624
|
-
if (initial || modelChanged || !this.modelContextWindow || !this.nativeAutoCompactReserve) {
|
|
625
|
-
// Enable first so getContextUsage exposes the provider's real trigger.
|
|
626
|
-
await this.query.applyFlagSettings({ autoCompactEnabled: true });
|
|
627
|
-
const baseline = await this.query.getContextUsage();
|
|
628
|
-
const baselineWindow = positiveNumber(baseline.maxTokens);
|
|
629
|
-
const baselineTrigger = positiveNumber(baseline.autoCompactThreshold);
|
|
630
|
-
if (!baselineWindow || !baselineTrigger || baselineTrigger >= baselineWindow) {
|
|
631
|
-
throw new Error("Claude did not expose a usable maxTokens/autoCompactThreshold pair after enabling native auto-compaction");
|
|
632
|
-
}
|
|
633
|
-
this.modelContextWindow = policy.knownModelContextWindow ?? baselineWindow;
|
|
634
|
-
this.nativeAutoCompactReserve = baselineWindow - baselineTrigger;
|
|
635
|
-
}
|
|
636
|
-
const maximum = this.modelContextWindow;
|
|
637
|
-
const reserve = this.nativeAutoCompactReserve;
|
|
638
|
-
const configuredTokenLimit = tokenCeiling(maximum, policy.configuredThresholdPercent);
|
|
639
|
-
// Claude's setting is a total window, not the used-token trigger. Preserve
|
|
640
|
-
// the SDK-reported output/compaction reserve so its resulting trigger lands
|
|
641
|
-
// on (or earlier than) Rafi's configured absolute ceiling.
|
|
642
|
-
const requestedNativeWindow = configuredTokenLimit + reserve;
|
|
643
|
-
await this.query.applyFlagSettings({ autoCompactEnabled: true, autoCompactWindow: requestedNativeWindow });
|
|
644
|
-
let verified = await this.query.getContextUsage();
|
|
645
|
-
// The SDK applies flag-layer changes through an internal settings watcher;
|
|
646
|
-
// give that watcher a short bounded interval before declaring the native
|
|
647
|
-
// control unavailable.
|
|
648
|
-
for (let attempt = 0; attempt < 4
|
|
649
|
-
&& positiveNumber(verified.autoCompactThreshold) !== undefined
|
|
650
|
-
&& positiveNumber(verified.autoCompactThreshold) > configuredTokenLimit; attempt += 1) {
|
|
651
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
652
|
-
verified = await this.query.getContextUsage();
|
|
653
|
-
}
|
|
654
|
-
if (initial && positiveNumber(verified.autoCompactThreshold) > configuredTokenLimit) {
|
|
655
|
-
// Some SDK builds commit flag-layer changes to the active query state at
|
|
656
|
-
// the next streaming-input boundary. Use the same approved, tool-free
|
|
657
|
-
// setup exchange that the handoff monitor already recognizes, then
|
|
658
|
-
// verify again before any real role work is dispatched.
|
|
659
|
-
const setup = await this.sendTurnInternal("Rafi context setup only. Do not call tools or change files. Reply with exactly CONTEXT_READY.");
|
|
660
|
-
if (setup.isError || setup.text.trim() !== "CONTEXT_READY") {
|
|
661
|
-
throw new Error(`Claude context setup did not complete safely: ${setup.text.slice(0, 240)}`);
|
|
662
|
-
}
|
|
663
|
-
verified = await this.query.getContextUsage();
|
|
664
|
-
}
|
|
665
|
-
if (positiveNumber(verified.autoCompactThreshold) > configuredTokenLimit) {
|
|
666
|
-
// Current SDK releases accept applyFlagSettings but some noninteractive
|
|
667
|
-
// transports do not project autoCompactWindow into their already-created
|
|
668
|
-
// query state. Restart only that transport with equivalent inline flag
|
|
669
|
-
// settings and resume the exact scoped conversation.
|
|
670
|
-
await this.restartTransport(requestedNativeWindow);
|
|
671
|
-
verified = await this.query.getContextUsage();
|
|
672
|
-
}
|
|
673
|
-
if (!verified.isAutoCompactEnabled)
|
|
674
|
-
throw new Error("Claude disabled native auto-compaction during context configuration");
|
|
675
|
-
const installed = positiveNumber(verified.autoCompactThreshold);
|
|
676
|
-
if (!installed)
|
|
677
|
-
throw new Error("Claude did not report the installed native auto-compaction trigger");
|
|
678
|
-
if (installed > configuredTokenLimit) {
|
|
679
|
-
throw new Error(`Claude installed native compaction at ${installed} tokens, later than the configured ${configuredTokenLimit}-token ceiling `
|
|
680
|
-
+ `(requested SDK window ${requestedNativeWindow}; this ceiling may be below the provider's supported minimum)`);
|
|
681
|
-
}
|
|
682
|
-
const sample = this.makeContextUsage(verified);
|
|
683
|
-
this.lastContextUsage = sample;
|
|
684
|
-
this.eventQueue.push({ kind: "context-usage", ...sample });
|
|
685
|
-
return {
|
|
686
|
-
modelContextWindow: maximum,
|
|
687
|
-
configuredTokenLimit,
|
|
688
|
-
installedNativeTokenLimit: installed,
|
|
689
|
-
installedNativePercent: installed / maximum * 100,
|
|
690
|
-
sample,
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
async restartTransport(nativeAutoCompactWindow) {
|
|
694
|
-
if (this.pending)
|
|
695
|
-
throw new Error("Claude context transport cannot restart while a provider turn is active");
|
|
696
|
-
const expectedSessionId = this._sessionId;
|
|
697
|
-
const expectedSessionRef = this._sessionRef;
|
|
698
|
-
if (!expectedSessionId || !expectedSessionRef) {
|
|
699
|
-
throw new Error("Claude context transport restart requires an established location-scoped session");
|
|
700
|
-
}
|
|
701
|
-
const priorQuery = this.query;
|
|
702
|
-
const priorInbox = this.inbox;
|
|
703
|
-
const priorPump = this.pumpDone;
|
|
704
|
-
const generation = ++this.transportGeneration;
|
|
705
|
-
priorInbox.close();
|
|
706
|
-
priorQuery.close();
|
|
707
|
-
await priorPump.catch(() => { });
|
|
708
|
-
this.inbox = new AsyncQueue();
|
|
709
|
-
this.abort = new AbortController();
|
|
710
|
-
this.streamEnded = false;
|
|
711
|
-
this.terminalResult = undefined;
|
|
712
|
-
this.transportObservedSessionId = undefined;
|
|
713
|
-
this.query = this.openQuery(nativeAutoCompactWindow);
|
|
714
|
-
this.pumpDone = this.pump(this.query, generation);
|
|
715
|
-
await this.query.initializationResult();
|
|
716
|
-
for (let attempt = 0; attempt < 10 && !this.transportObservedSessionId; attempt += 1) {
|
|
717
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
718
|
-
}
|
|
719
|
-
if (this.transportObservedSessionId && this.transportObservedSessionId !== expectedSessionId) {
|
|
720
|
-
this.query.close();
|
|
721
|
-
throw new Error(`Claude context transport did not resume the exact scoped session ${expectedSessionId} `
|
|
722
|
-
+ `(observed ${this.transportObservedSessionId})`);
|
|
723
|
-
}
|
|
724
|
-
if (!this.transportObservedSessionId) {
|
|
725
|
-
// Claude does not always repeat the session-start frame until the next
|
|
726
|
-
// user message. Validate the exact persisted, location-scoped target;
|
|
727
|
-
// the immediately following context query then proves this transport is
|
|
728
|
-
// usable before any real role action is dispatched.
|
|
729
|
-
const availability = await probeClaudeSession(expectedSessionRef, {
|
|
730
|
-
cwd: this.opts.cwd,
|
|
731
|
-
configRoot: this.opts.configRoot,
|
|
732
|
-
workspaceIdentity: this.opts.workspaceIdentity,
|
|
733
|
-
role: this.opts.sessionRole,
|
|
734
|
-
stream: this.opts.sessionStream,
|
|
735
|
-
ticketId: this.opts.ticketId,
|
|
736
|
-
deliveryUnitId: this.opts.deliveryUnitId,
|
|
737
|
-
});
|
|
738
|
-
if (availability.status !== "available") {
|
|
739
|
-
this.query.close();
|
|
740
|
-
throw new Error(`Claude context transport could not validate scoped session ${expectedSessionId}: `
|
|
741
|
-
+ `${availability.reason ?? availability.status} (${availability.detail ?? "no provider detail"})`);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
async completeCompaction(ok, reason, fallbackPostTokens) {
|
|
746
|
-
this.compactionTerminalObserved = true;
|
|
747
|
-
const active = this.activeCompaction;
|
|
748
|
-
const providerSequence = active?.providerSequence ?? ++this.compactionSequence;
|
|
749
|
-
const providerEventId = active?.providerEventId ?? `claude:${this._sessionId ?? "pending"}:${providerSequence}`;
|
|
750
|
-
const origin = active?.origin ?? (this.manualCompactionPending ? "rafi-manual" : "provider-auto");
|
|
751
|
-
this.activeCompaction = undefined;
|
|
752
|
-
if (!ok) {
|
|
753
|
-
this.eventQueue.push({
|
|
754
|
-
kind: "context-compaction", phase: "failed", origin, providerEventId, providerSequence,
|
|
755
|
-
provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef,
|
|
756
|
-
observedAt: new Date().toISOString(), reason,
|
|
757
|
-
});
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
let postCompactSample;
|
|
761
|
-
try {
|
|
762
|
-
const usage = await this.query.getContextUsage();
|
|
763
|
-
postCompactSample = { ...this.makeContextUsage(usage), source: "post-compact" };
|
|
764
|
-
this.lastContextUsage = postCompactSample;
|
|
765
|
-
this.eventQueue.push({ kind: "context-usage", ...postCompactSample });
|
|
766
|
-
}
|
|
767
|
-
catch {
|
|
768
|
-
if (fallbackPostTokens !== undefined && this.lastContextUsage?.maximum) {
|
|
769
|
-
postCompactSample = {
|
|
770
|
-
...this.makeContextUsage({
|
|
771
|
-
totalTokens: fallbackPostTokens,
|
|
772
|
-
maxTokens: this.lastContextUsage.maximum,
|
|
773
|
-
model: this.opts.model,
|
|
774
|
-
}),
|
|
775
|
-
source: "post-compact",
|
|
776
|
-
};
|
|
777
|
-
this.lastContextUsage = postCompactSample;
|
|
778
|
-
this.eventQueue.push({ kind: "context-usage", ...postCompactSample });
|
|
779
|
-
}
|
|
780
|
-
// Otherwise the shared coordinator records this success as unverified
|
|
781
|
-
// and hands off after its own bounded query attempts.
|
|
782
|
-
}
|
|
783
|
-
this.eventQueue.push({
|
|
784
|
-
kind: "context-compaction", phase: "succeeded", origin, providerEventId, providerSequence,
|
|
785
|
-
provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef,
|
|
786
|
-
observedAt: new Date().toISOString(), ...(postCompactSample ? { postCompactSample } : {}),
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
537
|
settlePending(result, emit = true) {
|
|
790
538
|
const pending = this.pending;
|
|
791
539
|
if (!pending)
|
|
@@ -813,20 +561,14 @@ export class ClaudeAdapter {
|
|
|
813
561
|
}
|
|
814
562
|
}
|
|
815
563
|
function finiteNumber(value) { const parsed = Number(value); return value !== null && value !== undefined && value !== "" && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; }
|
|
816
|
-
function
|
|
817
|
-
|
|
818
|
-
|
|
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;
|
|
819
568
|
}
|
|
820
|
-
function
|
|
569
|
+
function tokenLimit(maximum, percentage) {
|
|
821
570
|
return Math.max(1, Math.floor(maximum * percentage / 100));
|
|
822
571
|
}
|
|
823
|
-
function validateContextPolicy(policy) {
|
|
824
|
-
if (!Number.isInteger(policy.configuredThresholdPercent) || policy.configuredThresholdPercent < 1 || policy.configuredThresholdPercent > 99) {
|
|
825
|
-
throw new Error("configured context threshold must be an integer from 1 to 99");
|
|
826
|
-
}
|
|
827
|
-
if (!Number.isSafeInteger(policy.compactMaximum) || policy.compactMaximum < 1)
|
|
828
|
-
throw new Error("compact maximum must be a positive safe integer");
|
|
829
|
-
}
|
|
830
572
|
function isMissingClaudeSession(message) {
|
|
831
573
|
return /no conversation found with session id|session .* not found|conversation .* not found/i.test(message);
|
|
832
574
|
}
|