ai-foreman 1.7.6 → 1.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +31 -6
- package/dist/adapters/claude.d.ts +61 -6
- package/dist/adapters/claude.js +544 -36
- package/dist/adapters/codex.d.ts +28 -1
- package/dist/adapters/codex.js +398 -27
- package/dist/adapters/recovering.d.ts +13 -2
- package/dist/adapters/recovering.js +32 -4
- package/dist/adapters/sessionFailure.d.ts +18 -0
- package/dist/adapters/sessionFailure.js +51 -0
- package/dist/adapters/types.d.ts +117 -9
- package/dist/agentRun.d.ts +4 -1
- package/dist/agentRun.js +96 -9
- package/dist/branch/currentGuard.d.ts +38 -0
- package/dist/branch/currentGuard.js +71 -0
- package/dist/branch/planner.d.ts +1 -1
- package/dist/branch/planner.js +2 -7
- package/dist/branch/prefix.d.ts +8 -0
- package/dist/branch/prefix.js +26 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +8 -0
- package/dist/branch/runner.d.ts +14 -2
- package/dist/branch/runner.js +77 -17
- package/dist/buildRuns.d.ts +13 -4
- package/dist/buildRuns.js +85 -19
- package/dist/cli/handoffs.d.ts +2 -0
- package/dist/cli/handoffs.js +74 -0
- package/dist/cli/start.js +1079 -43
- package/dist/cli/tickets.js +199 -3
- package/dist/continuity.d.ts +92 -0
- package/dist/continuity.js +415 -0
- package/dist/foreman.d.ts +5 -1
- package/dist/foreman.js +41 -6
- package/dist/handoffs.d.ts +69 -0
- package/dist/handoffs.js +546 -0
- package/dist/log.d.ts +1 -1
- package/dist/permissions/policy.d.ts +5 -1
- package/dist/permissions/policy.js +48 -1
- package/dist/qaReview.d.ts +6 -6
- package/dist/qaReview.js +24 -14
- package/dist/sessionAvailability.d.ts +15 -0
- package/dist/sessionAvailability.js +57 -0
- package/dist/sessionIdentity.d.ts +33 -0
- package/dist/sessionIdentity.js +132 -0
- package/dist/sessionLifecycle.d.ts +198 -5
- package/dist/sessionLifecycle.js +1373 -20
- package/dist/statusReporter.d.ts +84 -8
- package/dist/statusReporter.js +270 -14
- package/dist/ticketPlanning.js +8 -0
- package/dist/ticketPopulation.js +88 -12
- package/dist/tickets/commands.js +20 -12
- package/dist/tickets/groupReset.d.ts +25 -0
- package/dist/tickets/groupReset.js +279 -0
- package/dist/tickets/groups.d.ts +36 -0
- package/dist/tickets/groups.js +68 -0
- package/dist/tickets/importer.js +27 -3
- package/dist/tickets/recommendations.js +31 -7
- package/dist/tickets/reset.d.ts +1 -0
- package/dist/tickets/reset.js +1 -0
- package/dist/tickets/setupConfig.d.ts +1 -0
- package/dist/tickets/setupConfig.js +10 -2
- package/dist/tickets/stateDb.d.ts +34 -0
- package/dist/tickets/stateDb.js +207 -0
- package/dist/tickets/ticketLoader.js +5 -2
- package/dist/tickets/validate.js +4 -0
- package/dist/workflowDb.d.ts +145 -3
- package/dist/workflowDb.js +571 -10
- package/package.json +32 -4
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
|
|
1
|
+
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextManagementPolicy, ContextUsage, InterruptResult, PreparedContextManagement, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
|
|
2
|
+
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
2
3
|
export interface CodexLineResult {
|
|
3
4
|
events: BuilderEvent[];
|
|
4
5
|
sessionId?: string;
|
|
@@ -16,11 +17,28 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
16
17
|
private pending;
|
|
17
18
|
private notificationWaiters;
|
|
18
19
|
private _sessionId?;
|
|
20
|
+
private _sessionRef?;
|
|
21
|
+
private sessionAvailability?;
|
|
19
22
|
private initialized;
|
|
20
23
|
private closed;
|
|
21
24
|
private activeText;
|
|
22
25
|
private usage?;
|
|
26
|
+
private usageRevision;
|
|
27
|
+
private contextSequence;
|
|
28
|
+
private providerSessionUsage?;
|
|
29
|
+
private lastTurnTokens?;
|
|
23
30
|
private stderr;
|
|
31
|
+
private contextPolicy?;
|
|
32
|
+
private nativeTokenLimit?;
|
|
33
|
+
private spawnedNativeTokenLimit?;
|
|
34
|
+
private contextPrepared;
|
|
35
|
+
private setupTurnActive;
|
|
36
|
+
private setupToolAttempted;
|
|
37
|
+
private activeTurnId?;
|
|
38
|
+
private boundaryInterrupt?;
|
|
39
|
+
private compactionSequence;
|
|
40
|
+
private activeCompaction?;
|
|
41
|
+
private manualCompactionPending;
|
|
24
42
|
constructor(opts: BuilderAdapterOptions);
|
|
25
43
|
buildInstruction(instruction: string): string;
|
|
26
44
|
buildAppServerArgs(): string[];
|
|
@@ -28,10 +46,17 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
28
46
|
buildArgs(instruction: string): string[];
|
|
29
47
|
sendTurn(instruction: string): Promise<TurnResult>;
|
|
30
48
|
private sendTurnInternal;
|
|
49
|
+
prepareContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
50
|
+
updateContextManagement(policy: ContextManagementPolicy): Promise<PreparedContextManagement>;
|
|
51
|
+
interruptTurnAtCompactionBoundary(providerEventId?: string): Promise<InterruptResult>;
|
|
31
52
|
compact(): Promise<CompactResult>;
|
|
32
53
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
54
|
+
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
33
55
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
34
56
|
sessionId(): string | undefined;
|
|
57
|
+
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
58
|
+
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
59
|
+
validateSession(): Promise<SessionAvailabilityV1>;
|
|
35
60
|
events(): AsyncIterable<BuilderEvent>;
|
|
36
61
|
close(): Promise<void>;
|
|
37
62
|
private ensureThread;
|
|
@@ -42,5 +67,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
42
67
|
private handle;
|
|
43
68
|
private waitFor;
|
|
44
69
|
private disconnect;
|
|
70
|
+
/** Restart only app-server; the scoped thread remains the continuity boundary. */
|
|
71
|
+
private restartTransport;
|
|
45
72
|
private buildSkillsAppendix;
|
|
46
73
|
}
|
package/dist/adapters/codex.js
CHANGED
|
@@ -5,6 +5,8 @@ import { createInterface } from "node:readline";
|
|
|
5
5
|
import { loadSkill } from "special-agents";
|
|
6
6
|
import { BuilderEventQueue, withActivityPhase } from "../activity.js";
|
|
7
7
|
import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
|
|
8
|
+
import { canonicalSessionPath, createProviderSessionRef, validateProviderSessionScope } from "../sessionIdentity.js";
|
|
9
|
+
import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
|
|
8
10
|
/** Compatibility parser for recorded pre-app-server JSONL fixtures. */
|
|
9
11
|
export function parseCodexLine(raw) {
|
|
10
12
|
const type = raw.type;
|
|
@@ -21,6 +23,7 @@ export function parseCodexLine(raw) {
|
|
|
21
23
|
return { events: [{ kind: "error", message: String(raw.error?.message ?? JSON.stringify(raw)) }] };
|
|
22
24
|
return { events: [] };
|
|
23
25
|
}
|
|
26
|
+
const CODEX_COMPACTION_TIMEOUT_MS = 120_000;
|
|
24
27
|
/** Persistent JSON-RPC controller for one live Codex thread. */
|
|
25
28
|
export class CodexAdapter {
|
|
26
29
|
opts;
|
|
@@ -31,20 +34,44 @@ export class CodexAdapter {
|
|
|
31
34
|
pending = new Map();
|
|
32
35
|
notificationWaiters = new Map();
|
|
33
36
|
_sessionId;
|
|
37
|
+
_sessionRef;
|
|
38
|
+
sessionAvailability;
|
|
34
39
|
initialized = false;
|
|
35
40
|
closed = false;
|
|
36
41
|
activeText = [];
|
|
37
42
|
usage;
|
|
43
|
+
usageRevision = 0;
|
|
44
|
+
contextSequence = 0;
|
|
45
|
+
providerSessionUsage;
|
|
46
|
+
lastTurnTokens;
|
|
38
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;
|
|
39
59
|
constructor(opts) {
|
|
40
60
|
this.opts = opts;
|
|
41
|
-
this._sessionId = opts.resumeSessionId;
|
|
61
|
+
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
62
|
+
this._sessionRef = opts.resumeSessionRef;
|
|
42
63
|
}
|
|
43
64
|
buildInstruction(instruction) {
|
|
44
65
|
return [this.opts.systemPromptAppend, this.buildSkillsAppendix(), instruction]
|
|
45
66
|
.filter((part) => Boolean(part)).join("\n\n");
|
|
46
67
|
}
|
|
47
|
-
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
|
+
}
|
|
48
75
|
/** Deprecated fixture helper. Runtime execution uses only app-server. */
|
|
49
76
|
buildArgs(instruction) {
|
|
50
77
|
const args = ["exec", "--json", "--skip-git-repo-check", "--sandbox", this.opts.sandboxMode ?? "workspace-write", "-C", this.opts.cwd];
|
|
@@ -60,61 +87,199 @@ export class CodexAdapter {
|
|
|
60
87
|
return args;
|
|
61
88
|
}
|
|
62
89
|
async sendTurn(instruction) {
|
|
90
|
+
if (this.opts.contextManagementPolicy && !this.contextPrepared)
|
|
91
|
+
await this.prepareContextManagement(this.opts.contextManagementPolicy);
|
|
63
92
|
return withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction));
|
|
64
93
|
}
|
|
65
|
-
async sendTurnInternal(instruction) {
|
|
94
|
+
async sendTurnInternal(instruction, setup = false) {
|
|
66
95
|
if (this.closed)
|
|
67
96
|
throw new Error("builder is closed");
|
|
68
97
|
this.eventQueue.push({ kind: "activity", state: "starting Codex turn", provider: "codex", model: this.opts.model });
|
|
98
|
+
let turnStartDispatched = false;
|
|
69
99
|
try {
|
|
70
100
|
await this.ensureThread();
|
|
71
101
|
this.activeText = [];
|
|
102
|
+
this.lastTurnTokens = undefined;
|
|
103
|
+
this.setupTurnActive = setup;
|
|
104
|
+
this.setupToolAttempted = false;
|
|
72
105
|
const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId);
|
|
73
|
-
|
|
106
|
+
turnStartDispatched = true;
|
|
107
|
+
const started = await this.request("turn/start", {
|
|
74
108
|
threadId: this._sessionId,
|
|
75
|
-
input: [{ type: "text", text: this.buildInstruction(instruction), text_elements: [] }],
|
|
109
|
+
input: [{ type: "text", text: setup ? instruction : this.buildInstruction(instruction), text_elements: [] }],
|
|
76
110
|
cwd: this.opts.cwd,
|
|
77
111
|
model: this.opts.model ?? null,
|
|
78
112
|
effort: this.opts.effort ?? (this.opts.fast ? "low" : null),
|
|
79
113
|
});
|
|
114
|
+
const startedTurn = started.turn;
|
|
115
|
+
this.activeTurnId = typeof startedTurn?.id === "string" ? startedTurn.id : undefined;
|
|
80
116
|
const params = await completion;
|
|
81
117
|
const turn = params.turn;
|
|
82
118
|
const failed = turn?.status === "failed";
|
|
119
|
+
const interrupted = turn?.status === "interrupted" || Boolean(this.boundaryInterrupt);
|
|
83
120
|
const error = turn?.error;
|
|
84
121
|
const text = this.activeText.join("\n");
|
|
85
|
-
const result = {
|
|
122
|
+
const result = {
|
|
123
|
+
text: failed ? normalizeRuntimeErrorText("codex", String(error?.message ?? text), null, "app-server turn") : text,
|
|
124
|
+
isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
|
|
125
|
+
...(this.lastTurnTokens ?? {}),
|
|
126
|
+
...(interrupted ? { interrupted: { reason: "compaction-boundary", ...(this.boundaryInterrupt?.providerEventId ? { providerEventId: this.boundaryInterrupt.providerEventId } : {}) } } : {}),
|
|
127
|
+
};
|
|
128
|
+
if (setup && this.setupToolAttempted)
|
|
129
|
+
throw new Error("Codex context setup attempted to invoke a tool; native context capability was not established");
|
|
86
130
|
this.eventQueue.push({ kind: "turn-complete", result });
|
|
87
131
|
return result;
|
|
88
132
|
}
|
|
89
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
|
+
}
|
|
146
|
+
if (error instanceof SessionUnavailableError) {
|
|
147
|
+
const result = sessionUnavailableResult(error);
|
|
148
|
+
this.eventQueue.push({ kind: "error", message: result.text });
|
|
149
|
+
this.eventQueue.push({ kind: "turn-complete", result });
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
90
152
|
const detail = [error instanceof Error ? error.message : String(error), this.stderr].filter(Boolean).join("\n");
|
|
91
153
|
const text = normalizeRuntimeErrorText("codex", detail, this.process?.exitCode ?? null, "builder turn");
|
|
92
154
|
this.disconnect(new Error(text));
|
|
93
|
-
const
|
|
155
|
+
const resumed = Boolean(this.opts.resumeSessionRef);
|
|
156
|
+
const result = resumed
|
|
157
|
+
? sessionUnavailableResult(new SessionUnavailableError({
|
|
158
|
+
runtime: "codex", phase: turnStartDispatched ? "turn" : "attach", dispatchState: turnStartDispatched ? "unknown" : "not-sent",
|
|
159
|
+
executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: text,
|
|
160
|
+
availability: { version: 1, status: turnStartDispatched ? "unknown" : "unavailable", checkedAt: new Date().toISOString(), reason: turnStartDispatched ? "probe-failed" : "attach-failed", detail: text, sessionRef: this.opts.resumeSessionRef },
|
|
161
|
+
}))
|
|
162
|
+
: { text, isError: true, numTurns: 1, costUsd: 0, costAuthoritative: false };
|
|
94
163
|
this.eventQueue.push({ kind: "error", message: text });
|
|
95
164
|
this.eventQueue.push({ kind: "turn-complete", result });
|
|
96
165
|
return result;
|
|
97
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
|
+
}
|
|
98
243
|
}
|
|
99
244
|
async compact() {
|
|
100
245
|
try {
|
|
101
246
|
await this.ensureThread();
|
|
247
|
+
this.manualCompactionPending = true;
|
|
102
248
|
this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
|
|
249
|
+
const usageRevision = this.usageRevision;
|
|
250
|
+
// A pre-compaction observation must never be mistaken for proof that the
|
|
251
|
+
// provider reduced the live context. The app server emits a fresh token
|
|
252
|
+
// usage notification for the compacted thread; require it alongside the
|
|
253
|
+
// explicit contextCompaction completion item.
|
|
254
|
+
this.usage = undefined;
|
|
103
255
|
const done = this.waitFor("item/completed", (params) => {
|
|
104
256
|
const item = params.item;
|
|
105
257
|
return params.threadId === this._sessionId && item?.type === "contextCompaction";
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
258
|
+
}, CODEX_COMPACTION_TIMEOUT_MS);
|
|
259
|
+
const postCompactUsage = this.waitFor("thread/tokenUsage/updated", (params) => {
|
|
260
|
+
return (params.threadId === undefined || params.threadId === this._sessionId)
|
|
261
|
+
&& this.usageRevision > usageRevision
|
|
262
|
+
&& this.usage !== undefined;
|
|
263
|
+
}, CODEX_COMPACTION_TIMEOUT_MS);
|
|
264
|
+
await Promise.all([
|
|
265
|
+
this.request("thread/compact/start", { threadId: this._sessionId }),
|
|
266
|
+
done,
|
|
267
|
+
postCompactUsage,
|
|
268
|
+
]);
|
|
110
269
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
|
|
111
270
|
return { ok: true };
|
|
112
271
|
}
|
|
113
272
|
catch (error) {
|
|
273
|
+
if (error instanceof SessionUnavailableError)
|
|
274
|
+
return { ok: false, error: error.message, failure: error.failure };
|
|
114
275
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
115
276
|
}
|
|
277
|
+
finally {
|
|
278
|
+
this.manualCompactionPending = false;
|
|
279
|
+
}
|
|
116
280
|
}
|
|
117
281
|
async contextUsage() { return this.usage; }
|
|
282
|
+
async sessionUsage() { return this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined; }
|
|
118
283
|
async switchSettings(settings) {
|
|
119
284
|
this.opts.model = settings.model;
|
|
120
285
|
this.opts.effort = settings.effort;
|
|
@@ -122,6 +287,26 @@ export class CodexAdapter {
|
|
|
122
287
|
return { ok: true };
|
|
123
288
|
}
|
|
124
289
|
sessionId() { return this._sessionId; }
|
|
290
|
+
sessionRef() { return this._sessionRef; }
|
|
291
|
+
adoptSessionRef(ref) {
|
|
292
|
+
if (ref.provider !== "codex" || ref.sessionId !== this._sessionId)
|
|
293
|
+
throw new Error("cannot adopt a session reference for a different Codex thread");
|
|
294
|
+
this._sessionRef = ref;
|
|
295
|
+
}
|
|
296
|
+
async validateSession() {
|
|
297
|
+
const checkedAt = new Date().toISOString();
|
|
298
|
+
if (!this._sessionRef)
|
|
299
|
+
return { version: 1, status: "unknown", checkedAt, reason: "legacy-unscoped", detail: "Codex raw thread IDs cannot be declared exact without a stored location scope" };
|
|
300
|
+
try {
|
|
301
|
+
await this.ensureThread();
|
|
302
|
+
return this.sessionAvailability ?? { version: 1, status: "available", checkedAt, observedCwd: canonicalSessionPath(this.opts.cwd), sessionRef: this._sessionRef };
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (error instanceof SessionUnavailableError && error.failure.availability)
|
|
306
|
+
return error.failure.availability;
|
|
307
|
+
return { version: 1, status: "unknown", checkedAt, reason: "probe-failed", detail: error instanceof Error ? error.message : String(error), sessionRef: this._sessionRef };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
125
310
|
events() { return this.eventQueue; }
|
|
126
311
|
async close() {
|
|
127
312
|
if (this.closed)
|
|
@@ -131,20 +316,93 @@ export class CodexAdapter {
|
|
|
131
316
|
this.eventQueue.close();
|
|
132
317
|
}
|
|
133
318
|
async ensureThread() {
|
|
319
|
+
if (this._sessionRef && !this.threadAttached) {
|
|
320
|
+
if (this._sessionRef.source === "legacy-inferred") {
|
|
321
|
+
const availability = { version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped", detail: "legacy Codex thread IDs cannot be proven exact without an observed scoped binding", sessionRef: this._sessionRef };
|
|
322
|
+
this.sessionAvailability = availability;
|
|
323
|
+
throw new SessionUnavailableError({ runtime: "codex", phase: "preflight", dispatchState: "not-sent", executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: availability.detail, availability });
|
|
324
|
+
}
|
|
325
|
+
const scoped = validateProviderSessionScope(this._sessionRef, {
|
|
326
|
+
provider: "codex", cwd: this.opts.cwd, configRoot: this.opts.configRoot ?? this.opts.cwd,
|
|
327
|
+
role: this.opts.sessionRole ?? this._sessionRef.role, stream: this.opts.sessionStream ?? this._sessionRef.stream,
|
|
328
|
+
workspaceIdentity: this.opts.workspaceIdentity, ticketId: this.opts.ticketId, deliveryUnitId: this.opts.deliveryUnitId,
|
|
329
|
+
});
|
|
330
|
+
if (scoped.status !== "available" || !scoped.sessionRef) {
|
|
331
|
+
this.sessionAvailability = scoped;
|
|
332
|
+
throw new SessionUnavailableError({
|
|
333
|
+
runtime: "codex", phase: "preflight", dispatchState: "not-sent", executable: this.opts.runtimeExecutable ?? "codex",
|
|
334
|
+
cwd: this.opts.cwd, diagnostics: scoped.detail ?? `Codex session ${this._sessionRef.sessionId} is ${scoped.status}`, availability: scoped,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
this._sessionRef = scoped.sessionRef;
|
|
338
|
+
}
|
|
134
339
|
await this.ensureConnection();
|
|
135
340
|
if (this._sessionId && this.initialized === true && this.threadAttached)
|
|
136
341
|
return;
|
|
137
342
|
const method = this._sessionId ? "thread/resume" : "thread/start";
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
343
|
+
let result;
|
|
344
|
+
try {
|
|
345
|
+
result = await this.request(method, {
|
|
346
|
+
...(this._sessionId ? { threadId: this._sessionId } : {}), cwd: this.opts.cwd,
|
|
347
|
+
model: this.opts.model ?? null, approvalPolicy: "never",
|
|
348
|
+
sandbox: this.opts.sandboxMode === "read-only" ? "read-only" : "workspace-write",
|
|
349
|
+
developerInstructions: this.opts.systemPromptAppend ?? null,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
if (this._sessionRef) {
|
|
354
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
355
|
+
const availability = {
|
|
356
|
+
version: 1, status: "unavailable", checkedAt: new Date().toISOString(),
|
|
357
|
+
reason: /not found|unknown thread|no thread/i.test(detail) ? "not-found" : "attach-failed", detail, sessionRef: this._sessionRef,
|
|
358
|
+
};
|
|
359
|
+
this.sessionAvailability = availability;
|
|
360
|
+
throw new SessionUnavailableError({ runtime: "codex", phase: "attach", dispatchState: "not-sent", executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: detail, availability, cause: error });
|
|
361
|
+
}
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
144
364
|
const thread = result.thread;
|
|
145
|
-
|
|
365
|
+
const returnedSessionId = String(thread?.id ?? this._sessionId ?? "") || undefined;
|
|
366
|
+
if (this._sessionRef && returnedSessionId !== this._sessionRef.sessionId) {
|
|
367
|
+
const availability = {
|
|
368
|
+
version: 1,
|
|
369
|
+
status: "unavailable",
|
|
370
|
+
checkedAt: new Date().toISOString(),
|
|
371
|
+
reason: "attach-failed",
|
|
372
|
+
detail: `Codex resumed thread ${returnedSessionId ?? "without an ID"} instead of requested thread ${this._sessionRef.sessionId}`,
|
|
373
|
+
sessionRef: this._sessionRef,
|
|
374
|
+
};
|
|
375
|
+
this.sessionAvailability = availability;
|
|
376
|
+
throw new SessionUnavailableError({
|
|
377
|
+
runtime: "codex",
|
|
378
|
+
phase: "attach",
|
|
379
|
+
dispatchState: "not-sent",
|
|
380
|
+
executable: this.opts.runtimeExecutable ?? "codex",
|
|
381
|
+
cwd: this.opts.cwd,
|
|
382
|
+
diagnostics: availability.detail,
|
|
383
|
+
availability,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
this._sessionId = returnedSessionId;
|
|
146
387
|
if (!this._sessionId)
|
|
147
388
|
throw new Error(`${method} did not return a thread ID`);
|
|
389
|
+
const providerCwd = typeof thread?.cwd === "string" ? canonicalSessionPath(thread.cwd) : undefined;
|
|
390
|
+
if (this._sessionRef && providerCwd && providerCwd !== canonicalSessionPath(this._sessionRef.cwd)) {
|
|
391
|
+
const availability = { version: 1, status: "unavailable", checkedAt: new Date().toISOString(), reason: "cwd-mismatch", observedCwd: providerCwd, detail: `Codex thread cwd ${providerCwd} does not match ${this._sessionRef.cwd}`, sessionRef: this._sessionRef };
|
|
392
|
+
this.sessionAvailability = availability;
|
|
393
|
+
throw new SessionUnavailableError({ runtime: "codex", phase: "attach", dispatchState: "not-sent", executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: availability.detail, availability });
|
|
394
|
+
}
|
|
395
|
+
if (!this._sessionRef) {
|
|
396
|
+
this._sessionRef = createProviderSessionRef({
|
|
397
|
+
provider: "codex", sessionId: this._sessionId, cwd: providerCwd ?? this.opts.cwd, configRoot: this.opts.configRoot ?? this.opts.cwd,
|
|
398
|
+
role: this.opts.sessionRole, stream: this.opts.sessionStream, generation: this.opts.sessionGeneration,
|
|
399
|
+
workspaceIdentity: this.opts.workspaceIdentity, ticketId: this.opts.ticketId, deliveryUnitId: this.opts.deliveryUnitId,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
this._sessionRef = { ...this._sessionRef, validatedAt: new Date().toISOString() };
|
|
404
|
+
}
|
|
405
|
+
this.sessionAvailability = { version: 1, status: "available", checkedAt: new Date().toISOString(), observedCwd: providerCwd ?? canonicalSessionPath(this.opts.cwd), sessionRef: this._sessionRef };
|
|
148
406
|
this.threadAttached = true;
|
|
149
407
|
this.eventQueue.push({ kind: "session-transition", transition: method === "thread/resume" ? "resumed" : "started" });
|
|
150
408
|
}
|
|
@@ -155,6 +413,7 @@ export class CodexAdapter {
|
|
|
155
413
|
this.stderr = "";
|
|
156
414
|
const executable = this.opts.runtimeExecutable ?? "codex";
|
|
157
415
|
const child = spawn(executable, this.buildAppServerArgs(), { cwd: this.opts.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
416
|
+
this.spawnedNativeTokenLimit = this.nativeTokenLimit;
|
|
158
417
|
this.process = child;
|
|
159
418
|
// A short-lived server may close stdin before its stderr/exit event. The
|
|
160
419
|
// close handler owns rejection so diagnostics include the complete stderr.
|
|
@@ -166,8 +425,10 @@ export class CodexAdapter {
|
|
|
166
425
|
}
|
|
167
426
|
catch { /* ignore non-protocol stdout */ } });
|
|
168
427
|
child.stderr.on("data", (chunk) => { this.stderr = `${this.stderr}${chunk.toString()}`.slice(-8192); });
|
|
169
|
-
child.on("error", (error) => this.
|
|
170
|
-
|
|
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"}`)); });
|
|
171
432
|
await this.request("initialize", { clientInfo: { name: "rafi", title: "Rafi", version: "1" }, capabilities: { experimentalApi: true, requestAttestation: false } });
|
|
172
433
|
this.write({ method: "initialized", params: {} });
|
|
173
434
|
this.initialized = true;
|
|
@@ -202,8 +463,29 @@ export class CodexAdapter {
|
|
|
202
463
|
if (!message.method)
|
|
203
464
|
return;
|
|
204
465
|
const params = message.params ?? {};
|
|
205
|
-
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") {
|
|
206
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
|
+
}
|
|
207
489
|
const activity = codexItemActivity(item);
|
|
208
490
|
if (activity)
|
|
209
491
|
this.eventQueue.push({ kind: "activity", provider: "codex", ...activity });
|
|
@@ -217,6 +499,21 @@ export class CodexAdapter {
|
|
|
217
499
|
else if (item?.type === "commandExecution")
|
|
218
500
|
this.eventQueue.push({ kind: "tool", name: "command_execution", input: { command: item.command } });
|
|
219
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
|
+
}
|
|
220
517
|
const activity = codexItemActivity(item, true);
|
|
221
518
|
if (activity)
|
|
222
519
|
this.eventQueue.push({ kind: "activity", provider: "codex", ...activity });
|
|
@@ -233,10 +530,30 @@ export class CodexAdapter {
|
|
|
233
530
|
else if (message.method === "thread/tokenUsage/updated") {
|
|
234
531
|
const tokenUsage = params.tokenUsage;
|
|
235
532
|
const total = tokenUsage?.total;
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
533
|
+
const last = tokenUsage?.last;
|
|
534
|
+
// `last` is active context occupancy. `total` is lifetime session usage.
|
|
535
|
+
const used = Number(last?.totalTokens);
|
|
536
|
+
const maximum = Number(tokenUsage?.modelContextWindow);
|
|
537
|
+
const observedAt = new Date().toISOString();
|
|
538
|
+
const totalInput = optionalNonNegative(total?.inputTokens);
|
|
539
|
+
const totalOutput = optionalNonNegative(total?.outputTokens);
|
|
540
|
+
const sessionTotal = optionalNonNegative(total?.totalTokens);
|
|
541
|
+
if (totalInput !== undefined || totalOutput !== undefined || sessionTotal !== undefined) {
|
|
542
|
+
this.providerSessionUsage = { inputTokens: totalInput, outputTokens: totalOutput, totalTokens: sessionTotal, observedAt, source: "provider" };
|
|
543
|
+
}
|
|
544
|
+
this.lastTurnTokens = {
|
|
545
|
+
inputTokens: optionalNonNegative(last?.inputTokens),
|
|
546
|
+
outputTokens: optionalNonNegative(last?.outputTokens),
|
|
547
|
+
};
|
|
548
|
+
if (Number.isFinite(used) && used >= 0 && Number.isFinite(maximum) && maximum > 0) {
|
|
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
|
+
};
|
|
554
|
+
this.usageRevision += 1;
|
|
555
|
+
this.eventQueue.push({ kind: "context-usage", ...this.usage });
|
|
556
|
+
}
|
|
240
557
|
}
|
|
241
558
|
else if (message.method === "error") {
|
|
242
559
|
const error = params.error;
|
|
@@ -252,8 +569,27 @@ export class CodexAdapter {
|
|
|
252
569
|
waiter.predicate(params) ? waiter.resolve(params) : remaining.push(waiter);
|
|
253
570
|
this.notificationWaiters.set(message.method, remaining);
|
|
254
571
|
}
|
|
255
|
-
waitFor(method, predicate) {
|
|
256
|
-
return new Promise((resolve, reject) => {
|
|
572
|
+
waitFor(method, predicate, timeoutMs) {
|
|
573
|
+
return new Promise((resolve, reject) => {
|
|
574
|
+
let timeout;
|
|
575
|
+
const waiter = {
|
|
576
|
+
predicate,
|
|
577
|
+
resolve: (params) => { if (timeout)
|
|
578
|
+
clearTimeout(timeout); resolve(params); },
|
|
579
|
+
reject: (error) => { if (timeout)
|
|
580
|
+
clearTimeout(timeout); reject(error); },
|
|
581
|
+
};
|
|
582
|
+
const waiters = this.notificationWaiters.get(method) ?? [];
|
|
583
|
+
waiters.push(waiter);
|
|
584
|
+
this.notificationWaiters.set(method, waiters);
|
|
585
|
+
if (timeoutMs !== undefined) {
|
|
586
|
+
timeout = setTimeout(() => {
|
|
587
|
+
const active = this.notificationWaiters.get(method) ?? [];
|
|
588
|
+
this.notificationWaiters.set(method, active.filter((candidate) => candidate !== waiter));
|
|
589
|
+
reject(new Error(`Timed out waiting for Codex app-server notification ${method}`));
|
|
590
|
+
}, timeoutMs);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
257
593
|
}
|
|
258
594
|
disconnect(error) {
|
|
259
595
|
const child = this.process;
|
|
@@ -270,6 +606,24 @@ export class CodexAdapter {
|
|
|
270
606
|
waiter.reject(error);
|
|
271
607
|
this.notificationWaiters.clear();
|
|
272
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
|
+
}
|
|
273
627
|
buildSkillsAppendix() {
|
|
274
628
|
if (!this.opts.skills?.length)
|
|
275
629
|
return undefined;
|
|
@@ -277,6 +631,23 @@ export class CodexAdapter {
|
|
|
277
631
|
return blocks.length ? ["# Preloaded Skills", "Use the following skills for this run.", ...blocks].join("\n\n") : undefined;
|
|
278
632
|
}
|
|
279
633
|
}
|
|
634
|
+
function optionalNonNegative(value) {
|
|
635
|
+
const number = Number(value);
|
|
636
|
+
return Number.isFinite(number) && number >= 0 ? number : undefined;
|
|
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
|
+
}
|
|
280
651
|
function activityPhase(phase) {
|
|
281
652
|
if (phase === "planning")
|
|
282
653
|
return "planning";
|