ai-foreman 1.7.7 → 1.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/activity.js +2 -2
- package/dist/adapters/claude.d.ts +29 -5
- package/dist/adapters/claude.js +340 -23
- package/dist/adapters/codex.d.ts +18 -1
- package/dist/adapters/codex.js +207 -11
- package/dist/adapters/recovering.d.ts +6 -1
- package/dist/adapters/recovering.js +9 -1
- package/dist/adapters/types.d.ts +66 -1
- package/dist/branch/currentGuard.d.ts +5 -1
- package/dist/branch/currentGuard.js +6 -0
- package/dist/cli/start.js +159 -21
- package/dist/continuity.d.ts +8 -2
- package/dist/continuity.js +72 -22
- package/dist/handoffs.js +223 -3
- package/dist/qaReview.js +4 -0
- package/dist/sessionLifecycle.d.ts +108 -14
- package/dist/sessionLifecycle.js +1034 -239
- package/dist/statusReporter.d.ts +26 -0
- package/dist/statusReporter.js +76 -16
- package/dist/workflowDb.d.ts +47 -2
- package/dist/workflowDb.js +149 -14
- package/package.json +4 -4
package/dist/adapters/codex.js
CHANGED
|
@@ -23,6 +23,7 @@ export function parseCodexLine(raw) {
|
|
|
23
23
|
return { events: [{ kind: "error", message: String(raw.error?.message ?? JSON.stringify(raw)) }] };
|
|
24
24
|
return { events: [] };
|
|
25
25
|
}
|
|
26
|
+
const CODEX_COMPACTION_TIMEOUT_MS = 120_000;
|
|
26
27
|
/** Persistent JSON-RPC controller for one live Codex thread. */
|
|
27
28
|
export class CodexAdapter {
|
|
28
29
|
opts;
|
|
@@ -40,9 +41,21 @@ export class CodexAdapter {
|
|
|
40
41
|
activeText = [];
|
|
41
42
|
usage;
|
|
42
43
|
usageRevision = 0;
|
|
44
|
+
contextSequence = 0;
|
|
43
45
|
providerSessionUsage;
|
|
44
46
|
lastTurnTokens;
|
|
45
47
|
stderr = "";
|
|
48
|
+
contextPolicy;
|
|
49
|
+
nativeTokenLimit;
|
|
50
|
+
spawnedNativeTokenLimit;
|
|
51
|
+
contextPrepared = false;
|
|
52
|
+
setupTurnActive = false;
|
|
53
|
+
setupToolAttempted = false;
|
|
54
|
+
activeTurnId;
|
|
55
|
+
boundaryInterrupt;
|
|
56
|
+
compactionSequence = 0;
|
|
57
|
+
activeCompaction;
|
|
58
|
+
manualCompactionPending = false;
|
|
46
59
|
constructor(opts) {
|
|
47
60
|
this.opts = opts;
|
|
48
61
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
@@ -52,7 +65,13 @@ export class CodexAdapter {
|
|
|
52
65
|
return [this.opts.systemPromptAppend, this.buildSkillsAppendix(), instruction]
|
|
53
66
|
.filter((part) => Boolean(part)).join("\n\n");
|
|
54
67
|
}
|
|
55
|
-
buildAppServerArgs() {
|
|
68
|
+
buildAppServerArgs() {
|
|
69
|
+
const args = ["app-server", "--listen", "stdio://"];
|
|
70
|
+
if (this.nativeTokenLimit !== undefined) {
|
|
71
|
+
args.push("-c", `model_auto_compact_token_limit=${this.nativeTokenLimit}`, "-c", 'model_auto_compact_token_limit_scope="total"');
|
|
72
|
+
}
|
|
73
|
+
return args;
|
|
74
|
+
}
|
|
56
75
|
/** Deprecated fixture helper. Runtime execution uses only app-server. */
|
|
57
76
|
buildArgs(instruction) {
|
|
58
77
|
const args = ["exec", "--json", "--skip-git-repo-check", "--sandbox", this.opts.sandboxMode ?? "workspace-write", "-C", this.opts.cwd];
|
|
@@ -68,9 +87,11 @@ export class CodexAdapter {
|
|
|
68
87
|
return args;
|
|
69
88
|
}
|
|
70
89
|
async sendTurn(instruction) {
|
|
90
|
+
if (this.opts.contextManagementPolicy && !this.contextPrepared)
|
|
91
|
+
await this.prepareContextManagement(this.opts.contextManagementPolicy);
|
|
71
92
|
return withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction));
|
|
72
93
|
}
|
|
73
|
-
async sendTurnInternal(instruction) {
|
|
94
|
+
async sendTurnInternal(instruction, setup = false) {
|
|
74
95
|
if (this.closed)
|
|
75
96
|
throw new Error("builder is closed");
|
|
76
97
|
this.eventQueue.push({ kind: "activity", state: "starting Codex turn", provider: "codex", model: this.opts.model });
|
|
@@ -79,29 +100,49 @@ export class CodexAdapter {
|
|
|
79
100
|
await this.ensureThread();
|
|
80
101
|
this.activeText = [];
|
|
81
102
|
this.lastTurnTokens = undefined;
|
|
103
|
+
this.setupTurnActive = setup;
|
|
104
|
+
this.setupToolAttempted = false;
|
|
82
105
|
const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId);
|
|
83
106
|
turnStartDispatched = true;
|
|
84
|
-
await this.request("turn/start", {
|
|
107
|
+
const started = await this.request("turn/start", {
|
|
85
108
|
threadId: this._sessionId,
|
|
86
|
-
input: [{ type: "text", text: this.buildInstruction(instruction), text_elements: [] }],
|
|
109
|
+
input: [{ type: "text", text: setup ? instruction : this.buildInstruction(instruction), text_elements: [] }],
|
|
87
110
|
cwd: this.opts.cwd,
|
|
88
111
|
model: this.opts.model ?? null,
|
|
89
112
|
effort: this.opts.effort ?? (this.opts.fast ? "low" : null),
|
|
90
113
|
});
|
|
114
|
+
const startedTurn = started.turn;
|
|
115
|
+
this.activeTurnId = typeof startedTurn?.id === "string" ? startedTurn.id : undefined;
|
|
91
116
|
const params = await completion;
|
|
92
117
|
const turn = params.turn;
|
|
93
118
|
const failed = turn?.status === "failed";
|
|
119
|
+
const interrupted = turn?.status === "interrupted" || Boolean(this.boundaryInterrupt);
|
|
94
120
|
const error = turn?.error;
|
|
95
121
|
const text = this.activeText.join("\n");
|
|
96
122
|
const result = {
|
|
97
123
|
text: failed ? normalizeRuntimeErrorText("codex", String(error?.message ?? text), null, "app-server turn") : text,
|
|
98
124
|
isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
|
|
99
125
|
...(this.lastTurnTokens ?? {}),
|
|
126
|
+
...(interrupted ? { interrupted: { reason: "compaction-boundary", ...(this.boundaryInterrupt?.providerEventId ? { providerEventId: this.boundaryInterrupt.providerEventId } : {}) } } : {}),
|
|
100
127
|
};
|
|
128
|
+
if (setup && this.setupToolAttempted)
|
|
129
|
+
throw new Error("Codex context setup attempted to invoke a tool; native context capability was not established");
|
|
101
130
|
this.eventQueue.push({ kind: "turn-complete", result });
|
|
102
131
|
return result;
|
|
103
132
|
}
|
|
104
133
|
catch (error) {
|
|
134
|
+
if (this.boundaryInterrupt) {
|
|
135
|
+
const result = {
|
|
136
|
+
text: this.activeText.join("\n"), isError: false, numTurns: 1, costUsd: 0,
|
|
137
|
+
costAuthoritative: false, ...(this.lastTurnTokens ?? {}),
|
|
138
|
+
interrupted: {
|
|
139
|
+
reason: "compaction-boundary",
|
|
140
|
+
...(this.boundaryInterrupt.providerEventId ? { providerEventId: this.boundaryInterrupt.providerEventId } : {}),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
this.eventQueue.push({ kind: "turn-complete", result });
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
105
146
|
if (error instanceof SessionUnavailableError) {
|
|
106
147
|
const result = sessionUnavailableResult(error);
|
|
107
148
|
this.eventQueue.push({ kind: "error", message: result.text });
|
|
@@ -123,10 +164,87 @@ export class CodexAdapter {
|
|
|
123
164
|
this.eventQueue.push({ kind: "turn-complete", result });
|
|
124
165
|
return result;
|
|
125
166
|
}
|
|
167
|
+
finally {
|
|
168
|
+
this.activeTurnId = undefined;
|
|
169
|
+
this.setupTurnActive = false;
|
|
170
|
+
this.boundaryInterrupt = undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async prepareContextManagement(policy) {
|
|
174
|
+
validateContextPolicy(policy);
|
|
175
|
+
this.contextPolicy = { ...policy };
|
|
176
|
+
this.compactionSequence = Math.max(this.compactionSequence, policy.providerSequenceStart ?? 0);
|
|
177
|
+
this.contextSequence = Math.max(this.contextSequence, policy.providerSequenceStart ?? 0);
|
|
178
|
+
if (policy.knownModelContextWindow && policy.knownModelContextWindow > 0) {
|
|
179
|
+
this.nativeTokenLimit = tokenCeiling(policy.knownModelContextWindow, policy.configuredThresholdPercent);
|
|
180
|
+
}
|
|
181
|
+
await this.ensureThread();
|
|
182
|
+
if (!this.usage) {
|
|
183
|
+
const setup = await this.sendTurnInternal("Rafi context setup only. Do not call tools or change files. Reply with exactly CONTEXT_READY.", true);
|
|
184
|
+
if (setup.isError || setup.text.trim() !== "CONTEXT_READY") {
|
|
185
|
+
throw new Error(`Codex context setup did not complete safely: ${setup.text.slice(0, 240)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const maximum = this.usage?.maximum ?? policy.knownModelContextWindow;
|
|
189
|
+
if (!maximum || !Number.isFinite(maximum) || maximum <= 0) {
|
|
190
|
+
throw new Error("Codex did not expose modelContextWindow during context setup");
|
|
191
|
+
}
|
|
192
|
+
const configuredTokenLimit = tokenCeiling(maximum, policy.configuredThresholdPercent);
|
|
193
|
+
this.nativeTokenLimit = configuredTokenLimit;
|
|
194
|
+
if (this.spawnedNativeTokenLimit !== configuredTokenLimit) {
|
|
195
|
+
await this.restartTransport();
|
|
196
|
+
await this.ensureThread();
|
|
197
|
+
}
|
|
198
|
+
this.contextPrepared = true;
|
|
199
|
+
return {
|
|
200
|
+
modelContextWindow: maximum,
|
|
201
|
+
configuredTokenLimit,
|
|
202
|
+
installedNativeTokenLimit: configuredTokenLimit,
|
|
203
|
+
installedNativePercent: configuredTokenLimit / maximum * 100,
|
|
204
|
+
...(this.usage ? { sample: { ...this.usage } } : {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async updateContextManagement(policy) {
|
|
208
|
+
validateContextPolicy(policy);
|
|
209
|
+
if (this.activeTurnId)
|
|
210
|
+
throw new Error("Codex native context control can be reconfigured only after the active turn settles");
|
|
211
|
+
const maximum = this.usage?.maximum ?? policy.knownModelContextWindow;
|
|
212
|
+
if (!maximum || maximum <= 0)
|
|
213
|
+
return this.prepareContextManagement(policy);
|
|
214
|
+
this.contextPolicy = { ...policy };
|
|
215
|
+
this.compactionSequence = Math.max(this.compactionSequence, policy.providerSequenceStart ?? 0);
|
|
216
|
+
this.contextSequence = Math.max(this.contextSequence, policy.providerSequenceStart ?? 0);
|
|
217
|
+
const configuredTokenLimit = tokenCeiling(maximum, policy.configuredThresholdPercent);
|
|
218
|
+
this.nativeTokenLimit = configuredTokenLimit;
|
|
219
|
+
if (this.spawnedNativeTokenLimit !== configuredTokenLimit) {
|
|
220
|
+
await this.restartTransport();
|
|
221
|
+
await this.ensureThread();
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
modelContextWindow: maximum,
|
|
225
|
+
configuredTokenLimit,
|
|
226
|
+
installedNativeTokenLimit: configuredTokenLimit,
|
|
227
|
+
installedNativePercent: configuredTokenLimit / maximum * 100,
|
|
228
|
+
...(this.usage ? { sample: { ...this.usage } } : {}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async interruptTurnAtCompactionBoundary(providerEventId) {
|
|
232
|
+
if (!this.activeTurnId || !this._sessionId)
|
|
233
|
+
return { ok: false, error: "no active Codex turn exists at the compaction boundary", providerEventId };
|
|
234
|
+
try {
|
|
235
|
+
this.boundaryInterrupt = { providerEventId };
|
|
236
|
+
await this.request("turn/interrupt", { threadId: this._sessionId, turnId: this.activeTurnId });
|
|
237
|
+
return { ok: true, providerEventId };
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
this.boundaryInterrupt = undefined;
|
|
241
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error), providerEventId };
|
|
242
|
+
}
|
|
126
243
|
}
|
|
127
244
|
async compact() {
|
|
128
245
|
try {
|
|
129
246
|
await this.ensureThread();
|
|
247
|
+
this.manualCompactionPending = true;
|
|
130
248
|
this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
|
|
131
249
|
const usageRevision = this.usageRevision;
|
|
132
250
|
// A pre-compaction observation must never be mistaken for proof that the
|
|
@@ -137,12 +255,12 @@ export class CodexAdapter {
|
|
|
137
255
|
const done = this.waitFor("item/completed", (params) => {
|
|
138
256
|
const item = params.item;
|
|
139
257
|
return params.threadId === this._sessionId && item?.type === "contextCompaction";
|
|
140
|
-
},
|
|
258
|
+
}, CODEX_COMPACTION_TIMEOUT_MS);
|
|
141
259
|
const postCompactUsage = this.waitFor("thread/tokenUsage/updated", (params) => {
|
|
142
260
|
return (params.threadId === undefined || params.threadId === this._sessionId)
|
|
143
261
|
&& this.usageRevision > usageRevision
|
|
144
262
|
&& this.usage !== undefined;
|
|
145
|
-
},
|
|
263
|
+
}, CODEX_COMPACTION_TIMEOUT_MS);
|
|
146
264
|
await Promise.all([
|
|
147
265
|
this.request("thread/compact/start", { threadId: this._sessionId }),
|
|
148
266
|
done,
|
|
@@ -156,6 +274,9 @@ export class CodexAdapter {
|
|
|
156
274
|
return { ok: false, error: error.message, failure: error.failure };
|
|
157
275
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
158
276
|
}
|
|
277
|
+
finally {
|
|
278
|
+
this.manualCompactionPending = false;
|
|
279
|
+
}
|
|
159
280
|
}
|
|
160
281
|
async contextUsage() { return this.usage; }
|
|
161
282
|
async sessionUsage() { return this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined; }
|
|
@@ -292,6 +413,7 @@ export class CodexAdapter {
|
|
|
292
413
|
this.stderr = "";
|
|
293
414
|
const executable = this.opts.runtimeExecutable ?? "codex";
|
|
294
415
|
const child = spawn(executable, this.buildAppServerArgs(), { cwd: this.opts.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
416
|
+
this.spawnedNativeTokenLimit = this.nativeTokenLimit;
|
|
295
417
|
this.process = child;
|
|
296
418
|
// A short-lived server may close stdin before its stderr/exit event. The
|
|
297
419
|
// close handler owns rejection so diagnostics include the complete stderr.
|
|
@@ -303,8 +425,10 @@ export class CodexAdapter {
|
|
|
303
425
|
}
|
|
304
426
|
catch { /* ignore non-protocol stdout */ } });
|
|
305
427
|
child.stderr.on("data", (chunk) => { this.stderr = `${this.stderr}${chunk.toString()}`.slice(-8192); });
|
|
306
|
-
child.on("error", (error) => this.
|
|
307
|
-
|
|
428
|
+
child.on("error", (error) => { if (this.process === child)
|
|
429
|
+
this.disconnect(error); });
|
|
430
|
+
child.on("close", (code) => { if (this.process === child)
|
|
431
|
+
this.disconnect(new Error(`Codex app-server exited with code ${code ?? "unknown"}`)); });
|
|
308
432
|
await this.request("initialize", { clientInfo: { name: "rafi", title: "Rafi", version: "1" }, capabilities: { experimentalApi: true, requestAttestation: false } });
|
|
309
433
|
this.write({ method: "initialized", params: {} });
|
|
310
434
|
this.initialized = true;
|
|
@@ -339,8 +463,29 @@ export class CodexAdapter {
|
|
|
339
463
|
if (!message.method)
|
|
340
464
|
return;
|
|
341
465
|
const params = message.params ?? {};
|
|
342
|
-
if (message.method === "
|
|
466
|
+
if (message.method === "turn/started") {
|
|
467
|
+
const turn = params.turn;
|
|
468
|
+
if (typeof turn?.id === "string")
|
|
469
|
+
this.activeTurnId = turn.id;
|
|
470
|
+
}
|
|
471
|
+
else if (message.method === "item/started") {
|
|
343
472
|
const item = params.item;
|
|
473
|
+
if (this.setupTurnActive && item && isToolItem(item.type)) {
|
|
474
|
+
this.setupToolAttempted = true;
|
|
475
|
+
if (this.activeTurnId && this._sessionId)
|
|
476
|
+
void this.request("turn/interrupt", { threadId: this._sessionId, turnId: this.activeTurnId }).catch(() => { });
|
|
477
|
+
}
|
|
478
|
+
if (item?.type === "contextCompaction") {
|
|
479
|
+
const providerSequence = ++this.compactionSequence;
|
|
480
|
+
const providerEventId = String(item.id ?? `codex-compaction-${providerSequence}`);
|
|
481
|
+
const origin = this.manualCompactionPending ? "rafi-manual" : "provider-auto";
|
|
482
|
+
this.activeCompaction = { providerEventId, providerSequence, origin, usageRevision: this.usageRevision };
|
|
483
|
+
this.eventQueue.push({
|
|
484
|
+
kind: "context-compaction", phase: "started", origin, providerEventId, providerSequence,
|
|
485
|
+
provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef,
|
|
486
|
+
observedAt: new Date().toISOString(),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
344
489
|
const activity = codexItemActivity(item);
|
|
345
490
|
if (activity)
|
|
346
491
|
this.eventQueue.push({ kind: "activity", provider: "codex", ...activity });
|
|
@@ -354,6 +499,21 @@ export class CodexAdapter {
|
|
|
354
499
|
else if (item?.type === "commandExecution")
|
|
355
500
|
this.eventQueue.push({ kind: "tool", name: "command_execution", input: { command: item.command } });
|
|
356
501
|
else {
|
|
502
|
+
if (item?.type === "contextCompaction") {
|
|
503
|
+
const active = this.activeCompaction;
|
|
504
|
+
const providerSequence = active?.providerSequence ?? ++this.compactionSequence;
|
|
505
|
+
const providerEventId = String(item.id ?? active?.providerEventId ?? `codex-compaction-${providerSequence}`);
|
|
506
|
+
const origin = active?.origin ?? (this.manualCompactionPending ? "rafi-manual" : "provider-auto");
|
|
507
|
+
const failed = item.status === "failed" || item.status === "error" || item.error !== undefined;
|
|
508
|
+
this.eventQueue.push({
|
|
509
|
+
kind: "context-compaction", phase: failed ? "failed" : "succeeded", origin, providerEventId, providerSequence,
|
|
510
|
+
provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef,
|
|
511
|
+
observedAt: new Date().toISOString(),
|
|
512
|
+
...(failed ? { reason: String(item.error?.message ?? item.error ?? "Codex context compaction failed") } : {}),
|
|
513
|
+
...(!failed && this.usage && (!active || this.usageRevision > active.usageRevision) ? { postCompactSample: { ...this.usage, source: "post-compact" } } : {}),
|
|
514
|
+
});
|
|
515
|
+
this.activeCompaction = undefined;
|
|
516
|
+
}
|
|
357
517
|
const activity = codexItemActivity(item, true);
|
|
358
518
|
if (activity)
|
|
359
519
|
this.eventQueue.push({ kind: "activity", provider: "codex", ...activity });
|
|
@@ -371,7 +531,8 @@ export class CodexAdapter {
|
|
|
371
531
|
const tokenUsage = params.tokenUsage;
|
|
372
532
|
const total = tokenUsage?.total;
|
|
373
533
|
const last = tokenUsage?.last;
|
|
374
|
-
|
|
534
|
+
// `last` is active context occupancy. `total` is lifetime session usage.
|
|
535
|
+
const used = Number(last?.totalTokens);
|
|
375
536
|
const maximum = Number(tokenUsage?.modelContextWindow);
|
|
376
537
|
const observedAt = new Date().toISOString();
|
|
377
538
|
const totalInput = optionalNonNegative(total?.inputTokens);
|
|
@@ -385,7 +546,11 @@ export class CodexAdapter {
|
|
|
385
546
|
outputTokens: optionalNonNegative(last?.outputTokens),
|
|
386
547
|
};
|
|
387
548
|
if (Number.isFinite(used) && used >= 0 && Number.isFinite(maximum) && maximum > 0) {
|
|
388
|
-
|
|
549
|
+
const sequence = ++this.contextSequence;
|
|
550
|
+
this.usage = {
|
|
551
|
+
used, maximum, percentage: used / maximum * 100, observedAt, source: "provider-event", sequence,
|
|
552
|
+
...(this._sessionId ? { sessionId: this._sessionId } : {}), ...(this.opts.model ? { model: this.opts.model } : {}),
|
|
553
|
+
};
|
|
389
554
|
this.usageRevision += 1;
|
|
390
555
|
this.eventQueue.push({ kind: "context-usage", ...this.usage });
|
|
391
556
|
}
|
|
@@ -441,6 +606,24 @@ export class CodexAdapter {
|
|
|
441
606
|
waiter.reject(error);
|
|
442
607
|
this.notificationWaiters.clear();
|
|
443
608
|
}
|
|
609
|
+
/** Restart only app-server; the scoped thread remains the continuity boundary. */
|
|
610
|
+
async restartTransport() {
|
|
611
|
+
if (this.activeTurnId)
|
|
612
|
+
throw new Error("cannot restart Codex app-server while a turn is active");
|
|
613
|
+
const child = this.process;
|
|
614
|
+
this.process = undefined;
|
|
615
|
+
this.initialized = false;
|
|
616
|
+
this.threadAttached = false;
|
|
617
|
+
if (child && child.exitCode === null)
|
|
618
|
+
child.kill("SIGTERM");
|
|
619
|
+
for (const pending of this.pending.values())
|
|
620
|
+
pending.reject(new Error("Codex app-server restarted for context policy"));
|
|
621
|
+
this.pending.clear();
|
|
622
|
+
for (const waiters of this.notificationWaiters.values())
|
|
623
|
+
for (const waiter of waiters)
|
|
624
|
+
waiter.reject(new Error("Codex app-server restarted for context policy"));
|
|
625
|
+
this.notificationWaiters.clear();
|
|
626
|
+
}
|
|
444
627
|
buildSkillsAppendix() {
|
|
445
628
|
if (!this.opts.skills?.length)
|
|
446
629
|
return undefined;
|
|
@@ -452,6 +635,19 @@ function optionalNonNegative(value) {
|
|
|
452
635
|
const number = Number(value);
|
|
453
636
|
return Number.isFinite(number) && number >= 0 ? number : undefined;
|
|
454
637
|
}
|
|
638
|
+
function tokenCeiling(maximum, percentage) {
|
|
639
|
+
return Math.max(1, Math.floor(maximum * percentage / 100));
|
|
640
|
+
}
|
|
641
|
+
function validateContextPolicy(policy) {
|
|
642
|
+
if (!Number.isInteger(policy.configuredThresholdPercent) || policy.configuredThresholdPercent < 1 || policy.configuredThresholdPercent > 99) {
|
|
643
|
+
throw new Error("configured context threshold must be an integer from 1 to 99");
|
|
644
|
+
}
|
|
645
|
+
if (!Number.isSafeInteger(policy.compactMaximum) || policy.compactMaximum < 1)
|
|
646
|
+
throw new Error("compact maximum must be a positive safe integer");
|
|
647
|
+
}
|
|
648
|
+
function isToolItem(type) {
|
|
649
|
+
return type === "commandExecution" || type === "fileChange" || type === "mcpToolCall" || type === "dynamicToolCall" || type === "webSearch";
|
|
650
|
+
}
|
|
455
651
|
function activityPhase(phase) {
|
|
456
652
|
if (phase === "planning")
|
|
457
653
|
return "planning";
|
|
@@ -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, ContextManagementPolicy, ContextUsage, InterruptResult, ManagedTurnDispatcher, PreparedContextManagement, 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 {
|
|
@@ -21,6 +21,7 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
21
21
|
private readonly eventQueue;
|
|
22
22
|
private eventPump;
|
|
23
23
|
private closed;
|
|
24
|
+
private turnDispatcher?;
|
|
24
25
|
constructor(opts: RecoveringAdapterOptions);
|
|
25
26
|
get agent(): "claude" | "codex";
|
|
26
27
|
sendTurn(text: string): Promise<TurnResult>;
|
|
@@ -29,6 +30,10 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
29
30
|
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
30
31
|
validateSession(): Promise<SessionAvailabilityV1>;
|
|
31
32
|
compact(): Promise<CompactResult>;
|
|
33
|
+
prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
34
|
+
updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
35
|
+
interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
|
|
36
|
+
installManagedTurnDispatcher(dispatcher: ManagedTurnDispatcher): void;
|
|
32
37
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
33
38
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
34
39
|
events(): AsyncIterable<BuilderEvent>;
|
|
@@ -9,6 +9,7 @@ export class RecoveringAdapter {
|
|
|
9
9
|
eventQueue = new AsyncQueue();
|
|
10
10
|
eventPump;
|
|
11
11
|
closed = false;
|
|
12
|
+
turnDispatcher;
|
|
12
13
|
constructor(opts) {
|
|
13
14
|
this.opts = opts;
|
|
14
15
|
this.adapter = opts.initial;
|
|
@@ -20,7 +21,8 @@ export class RecoveringAdapter {
|
|
|
20
21
|
}
|
|
21
22
|
async sendTurn(text) {
|
|
22
23
|
while (true) {
|
|
23
|
-
const
|
|
24
|
+
const invoke = () => this.adapter.sendTurn(text);
|
|
25
|
+
const result = this.turnDispatcher ? await this.turnDispatcher(text, invoke) : await invoke();
|
|
24
26
|
const sessionRef = this.adapter.sessionRef?.();
|
|
25
27
|
if (sessionRef && this.opts.onSessionRef) {
|
|
26
28
|
try {
|
|
@@ -67,6 +69,12 @@ export class RecoveringAdapter {
|
|
|
67
69
|
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
70
|
}
|
|
69
71
|
compact() { return this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" }); }
|
|
72
|
+
prepareContextManagement(policy) { if (!this.adapter.prepareContextManagement)
|
|
73
|
+
return Promise.reject(new Error("native context management unavailable")); return this.adapter.prepareContextManagement(policy); }
|
|
74
|
+
updateContextManagement(policy) { if (!this.adapter.updateContextManagement)
|
|
75
|
+
return Promise.reject(new Error("native context reconfiguration unavailable")); return this.adapter.updateContextManagement(policy); }
|
|
76
|
+
interruptTurnAtCompactionBoundary(providerEventId) { return this.adapter.interruptTurnAtCompactionBoundary?.(providerEventId) ?? Promise.resolve({ ok: false, error: "compaction-boundary interruption unavailable", providerEventId }); }
|
|
77
|
+
installManagedTurnDispatcher(dispatcher) { this.turnDispatcher = dispatcher; }
|
|
70
78
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
71
79
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
72
80
|
events() {
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -59,7 +59,55 @@ export interface TurnResult {
|
|
|
59
59
|
inputTokens?: number;
|
|
60
60
|
outputTokens?: number;
|
|
61
61
|
failure?: RuntimeFailure;
|
|
62
|
+
/** A provider turn stopped at a context-compaction boundary, not a failure. */
|
|
63
|
+
interrupted?: {
|
|
64
|
+
reason: "compaction-boundary";
|
|
65
|
+
providerEventId?: string;
|
|
66
|
+
};
|
|
62
67
|
}
|
|
68
|
+
export type ContextCompactionOrigin = "provider-auto" | "rafi-manual" | "recovery" | "boundary";
|
|
69
|
+
export type ContextCompactionPhase = "started" | "succeeded" | "failed";
|
|
70
|
+
/** Native context policy installed before a Builder/QA role may do real work. */
|
|
71
|
+
export interface ContextManagementPolicy {
|
|
72
|
+
role: "builder" | "qa";
|
|
73
|
+
configuredThresholdPercent: number;
|
|
74
|
+
compactMaximum: number;
|
|
75
|
+
settingsRevision: number;
|
|
76
|
+
model: string;
|
|
77
|
+
/** Continue a provider-monotonic sequence after process recovery. */
|
|
78
|
+
providerSequenceStart?: number;
|
|
79
|
+
/** A persisted, model-matched maximum may avoid a discovery turn. */
|
|
80
|
+
knownModelContextWindow?: number;
|
|
81
|
+
/** Native compaction stays enabled until the host vetoes a boundary. */
|
|
82
|
+
nativeCompactionEnabled?: boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface PreparedContextManagement {
|
|
85
|
+
modelContextWindow: number;
|
|
86
|
+
configuredTokenLimit: number;
|
|
87
|
+
installedNativeTokenLimit: number;
|
|
88
|
+
installedNativePercent: number;
|
|
89
|
+
sample?: ContextUsage;
|
|
90
|
+
}
|
|
91
|
+
export interface ContextCompactionEvent {
|
|
92
|
+
kind: "context-compaction";
|
|
93
|
+
phase: ContextCompactionPhase;
|
|
94
|
+
origin: ContextCompactionOrigin;
|
|
95
|
+
providerEventId: string;
|
|
96
|
+
providerSequence: number;
|
|
97
|
+
provider: "claude" | "codex";
|
|
98
|
+
sessionId?: string;
|
|
99
|
+
sessionRef?: ProviderSessionRefV1;
|
|
100
|
+
observedAt: string;
|
|
101
|
+
postCompactSample?: ContextUsage;
|
|
102
|
+
reason?: string;
|
|
103
|
+
}
|
|
104
|
+
export interface InterruptResult {
|
|
105
|
+
ok: boolean;
|
|
106
|
+
error?: string;
|
|
107
|
+
providerEventId?: string;
|
|
108
|
+
}
|
|
109
|
+
/** Host-owned gate used by wrappers that need additional provider turns. */
|
|
110
|
+
export type ManagedTurnDispatcher = (text: string, invoke: () => Promise<TurnResult>) => Promise<TurnResult>;
|
|
63
111
|
/** Observability events emitted while a builder works. */
|
|
64
112
|
export type BuilderEvent = {
|
|
65
113
|
kind: "text";
|
|
@@ -97,7 +145,10 @@ export type BuilderEvent = {
|
|
|
97
145
|
percentage?: number;
|
|
98
146
|
observedAt?: string;
|
|
99
147
|
source?: "provider-event" | "provider-query" | "post-compact";
|
|
100
|
-
|
|
148
|
+
sequence?: number;
|
|
149
|
+
sessionId?: string;
|
|
150
|
+
model?: string;
|
|
151
|
+
} | ContextCompactionEvent | {
|
|
101
152
|
kind: "error";
|
|
102
153
|
message: string;
|
|
103
154
|
};
|
|
@@ -107,6 +158,10 @@ export interface ContextUsage {
|
|
|
107
158
|
percentage?: number;
|
|
108
159
|
observedAt?: string;
|
|
109
160
|
source?: "provider-event" | "provider-query" | "post-compact";
|
|
161
|
+
/** Monotonic within one scoped provider session. */
|
|
162
|
+
sequence?: number;
|
|
163
|
+
sessionId?: string;
|
|
164
|
+
model?: string;
|
|
110
165
|
}
|
|
111
166
|
export interface ProviderSessionUsage {
|
|
112
167
|
inputTokens?: number;
|
|
@@ -163,6 +218,8 @@ export interface BuilderAdapterOptions {
|
|
|
163
218
|
systemPromptAppend?: string;
|
|
164
219
|
/** Skill names to preload for this session (Claude: lazy-loaded; Codex: flattened). */
|
|
165
220
|
skills?: string[];
|
|
221
|
+
/** Builder/QA native policy used even for setup and handoff-acceptance turns. */
|
|
222
|
+
contextManagementPolicy?: ContextManagementPolicy;
|
|
166
223
|
}
|
|
167
224
|
export interface BuilderAdapter {
|
|
168
225
|
readonly agent: "claude" | "codex";
|
|
@@ -178,6 +235,14 @@ export interface BuilderAdapter {
|
|
|
178
235
|
validateSession?(): Promise<SessionAvailabilityV1>;
|
|
179
236
|
/** Provider-native compaction on the exact live conversation. */
|
|
180
237
|
compact?(): Promise<CompactResult>;
|
|
238
|
+
/** Initialize the session and install native in-turn context controls. */
|
|
239
|
+
prepareContextManagement?(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
240
|
+
/** Safely reconfigure native in-turn controls on the same session. */
|
|
241
|
+
updateContextManagement?(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
242
|
+
/** Stop only at a provider-observed compaction boundary. */
|
|
243
|
+
interruptTurnAtCompactionBoundary?(providerEventId?: string): Promise<InterruptResult>;
|
|
244
|
+
/** Route wrapper-owned repair/retry/follow-up turns through the role gate. */
|
|
245
|
+
installManagedTurnDispatcher?(dispatcher: ManagedTurnDispatcher): void;
|
|
181
246
|
/** Truthful provider context occupancy, when exposed by the provider. */
|
|
182
247
|
contextUsage?(): Promise<ContextUsage | undefined>;
|
|
183
248
|
/** 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, ContextManagementPolicy, ContextUsage, InterruptResult, ManagedTurnDispatcher, PreparedContextManagement, 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
|
+
prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
30
|
+
updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
31
|
+
interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
|
|
32
|
+
installManagedTurnDispatcher(dispatcher: ManagedTurnDispatcher): void;
|
|
29
33
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
30
34
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
31
35
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
@@ -48,6 +48,12 @@ 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
|
+
async prepareContextManagement(policy) { assertCurrentWorkflowIdentity(this.cwd, this.expected); if (!this.adapter.prepareContextManagement)
|
|
52
|
+
throw new Error("native context management unavailable"); const result = await this.adapter.prepareContextManagement(policy); assertCurrentWorkflowIdentity(this.cwd, this.expected); return result; }
|
|
53
|
+
async updateContextManagement(policy) { assertCurrentWorkflowIdentity(this.cwd, this.expected); if (!this.adapter.updateContextManagement)
|
|
54
|
+
throw new Error("native context reconfiguration unavailable"); const result = await this.adapter.updateContextManagement(policy); assertCurrentWorkflowIdentity(this.cwd, this.expected); return result; }
|
|
55
|
+
interruptTurnAtCompactionBoundary(providerEventId) { return this.adapter.interruptTurnAtCompactionBoundary?.(providerEventId) ?? Promise.resolve({ ok: false, error: "compaction-boundary interruption unavailable", providerEventId }); }
|
|
56
|
+
installManagedTurnDispatcher(dispatcher) { this.adapter.installManagedTurnDispatcher?.(dispatcher); }
|
|
51
57
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
52
58
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
53
59
|
switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
|