ai-foreman 1.7.5 → 1.7.7
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 +17 -2
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +29 -4
- package/dist/adapters/claude.d.ts +33 -2
- package/dist/adapters/claude.js +208 -17
- package/dist/adapters/codex.d.ts +11 -1
- package/dist/adapters/codex.js +195 -20
- package/dist/adapters/recovering.d.ts +8 -2
- package/dist/adapters/recovering.js +23 -3
- package/dist/adapters/sessionFailure.d.ts +18 -0
- package/dist/adapters/sessionFailure.js +51 -0
- package/dist/adapters/types.d.ts +52 -9
- package/dist/agentRun.d.ts +4 -1
- package/dist/agentRun.js +96 -9
- package/dist/branch/currentGuard.d.ts +34 -0
- package/dist/branch/currentGuard.js +65 -0
- package/dist/branch/github.js +3 -2
- package/dist/branch/gitlab.js +3 -2
- package/dist/branch/planner.d.ts +5 -0
- package/dist/branch/planner.js +37 -3
- 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 +51 -18
- package/dist/buildRuns.js +318 -31
- package/dist/cli/handoffs.d.ts +2 -0
- package/dist/cli/handoffs.js +74 -0
- package/dist/cli/start.js +1040 -62
- package/dist/cli/tickets.d.ts +10 -1
- package/dist/cli/tickets.js +615 -80
- package/dist/continuity.d.ts +86 -0
- package/dist/continuity.js +365 -0
- package/dist/foreman.d.ts +9 -5
- package/dist/foreman.js +76 -20
- package/dist/handoffs.d.ts +69 -0
- package/dist/handoffs.js +326 -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 +7 -6
- package/dist/qaReview.js +31 -20
- 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 +102 -3
- package/dist/sessionLifecycle.js +578 -20
- package/dist/statusReporter.d.ts +58 -8
- package/dist/statusReporter.js +210 -14
- package/dist/ticketPlanning.js +8 -0
- package/dist/ticketPopulation.js +88 -12
- package/dist/tickets/commands.js +20 -12
- package/dist/tickets/details.d.ts +17 -0
- package/dist/tickets/details.js +94 -0
- package/dist/tickets/eligibility.d.ts +28 -0
- package/dist/tickets/eligibility.js +71 -0
- 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 +28 -0
- package/dist/tickets/reset.js +214 -0
- package/dist/tickets/reviewStandards.d.ts +7 -0
- package/dist/tickets/reviewStandards.js +53 -0
- package/dist/tickets/setupConfig.d.ts +23 -0
- package/dist/tickets/setupConfig.js +115 -3
- package/dist/tickets/stateDb.d.ts +42 -1
- package/dist/tickets/stateDb.js +261 -23
- package/dist/tickets/ticketLoader.js +5 -2
- package/dist/tickets/validate.js +4 -0
- package/dist/workflowDb.d.ts +102 -2
- package/dist/workflowDb.js +460 -8
- package/package.json +35 -3
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;
|
|
@@ -31,14 +33,20 @@ export class CodexAdapter {
|
|
|
31
33
|
pending = new Map();
|
|
32
34
|
notificationWaiters = new Map();
|
|
33
35
|
_sessionId;
|
|
36
|
+
_sessionRef;
|
|
37
|
+
sessionAvailability;
|
|
34
38
|
initialized = false;
|
|
35
39
|
closed = false;
|
|
36
40
|
activeText = [];
|
|
37
41
|
usage;
|
|
42
|
+
usageRevision = 0;
|
|
43
|
+
providerSessionUsage;
|
|
44
|
+
lastTurnTokens;
|
|
38
45
|
stderr = "";
|
|
39
46
|
constructor(opts) {
|
|
40
47
|
this.opts = opts;
|
|
41
|
-
this._sessionId = opts.resumeSessionId;
|
|
48
|
+
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
49
|
+
this._sessionRef = opts.resumeSessionRef;
|
|
42
50
|
}
|
|
43
51
|
buildInstruction(instruction) {
|
|
44
52
|
return [this.opts.systemPromptAppend, this.buildSkillsAppendix(), instruction]
|
|
@@ -66,10 +74,13 @@ export class CodexAdapter {
|
|
|
66
74
|
if (this.closed)
|
|
67
75
|
throw new Error("builder is closed");
|
|
68
76
|
this.eventQueue.push({ kind: "activity", state: "starting Codex turn", provider: "codex", model: this.opts.model });
|
|
77
|
+
let turnStartDispatched = false;
|
|
69
78
|
try {
|
|
70
79
|
await this.ensureThread();
|
|
71
80
|
this.activeText = [];
|
|
81
|
+
this.lastTurnTokens = undefined;
|
|
72
82
|
const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId);
|
|
83
|
+
turnStartDispatched = true;
|
|
73
84
|
await this.request("turn/start", {
|
|
74
85
|
threadId: this._sessionId,
|
|
75
86
|
input: [{ type: "text", text: this.buildInstruction(instruction), text_elements: [] }],
|
|
@@ -82,15 +93,32 @@ export class CodexAdapter {
|
|
|
82
93
|
const failed = turn?.status === "failed";
|
|
83
94
|
const error = turn?.error;
|
|
84
95
|
const text = this.activeText.join("\n");
|
|
85
|
-
const result = {
|
|
96
|
+
const result = {
|
|
97
|
+
text: failed ? normalizeRuntimeErrorText("codex", String(error?.message ?? text), null, "app-server turn") : text,
|
|
98
|
+
isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
|
|
99
|
+
...(this.lastTurnTokens ?? {}),
|
|
100
|
+
};
|
|
86
101
|
this.eventQueue.push({ kind: "turn-complete", result });
|
|
87
102
|
return result;
|
|
88
103
|
}
|
|
89
104
|
catch (error) {
|
|
105
|
+
if (error instanceof SessionUnavailableError) {
|
|
106
|
+
const result = sessionUnavailableResult(error);
|
|
107
|
+
this.eventQueue.push({ kind: "error", message: result.text });
|
|
108
|
+
this.eventQueue.push({ kind: "turn-complete", result });
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
90
111
|
const detail = [error instanceof Error ? error.message : String(error), this.stderr].filter(Boolean).join("\n");
|
|
91
112
|
const text = normalizeRuntimeErrorText("codex", detail, this.process?.exitCode ?? null, "builder turn");
|
|
92
113
|
this.disconnect(new Error(text));
|
|
93
|
-
const
|
|
114
|
+
const resumed = Boolean(this.opts.resumeSessionRef);
|
|
115
|
+
const result = resumed
|
|
116
|
+
? sessionUnavailableResult(new SessionUnavailableError({
|
|
117
|
+
runtime: "codex", phase: turnStartDispatched ? "turn" : "attach", dispatchState: turnStartDispatched ? "unknown" : "not-sent",
|
|
118
|
+
executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: text,
|
|
119
|
+
availability: { version: 1, status: turnStartDispatched ? "unknown" : "unavailable", checkedAt: new Date().toISOString(), reason: turnStartDispatched ? "probe-failed" : "attach-failed", detail: text, sessionRef: this.opts.resumeSessionRef },
|
|
120
|
+
}))
|
|
121
|
+
: { text, isError: true, numTurns: 1, costUsd: 0, costAuthoritative: false };
|
|
94
122
|
this.eventQueue.push({ kind: "error", message: text });
|
|
95
123
|
this.eventQueue.push({ kind: "turn-complete", result });
|
|
96
124
|
return result;
|
|
@@ -100,21 +128,37 @@ export class CodexAdapter {
|
|
|
100
128
|
try {
|
|
101
129
|
await this.ensureThread();
|
|
102
130
|
this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
|
|
131
|
+
const usageRevision = this.usageRevision;
|
|
132
|
+
// A pre-compaction observation must never be mistaken for proof that the
|
|
133
|
+
// provider reduced the live context. The app server emits a fresh token
|
|
134
|
+
// usage notification for the compacted thread; require it alongside the
|
|
135
|
+
// explicit contextCompaction completion item.
|
|
136
|
+
this.usage = undefined;
|
|
103
137
|
const done = this.waitFor("item/completed", (params) => {
|
|
104
138
|
const item = params.item;
|
|
105
139
|
return params.threadId === this._sessionId && item?.type === "contextCompaction";
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
}, 30_000);
|
|
141
|
+
const postCompactUsage = this.waitFor("thread/tokenUsage/updated", (params) => {
|
|
142
|
+
return (params.threadId === undefined || params.threadId === this._sessionId)
|
|
143
|
+
&& this.usageRevision > usageRevision
|
|
144
|
+
&& this.usage !== undefined;
|
|
145
|
+
}, 30_000);
|
|
146
|
+
await Promise.all([
|
|
147
|
+
this.request("thread/compact/start", { threadId: this._sessionId }),
|
|
148
|
+
done,
|
|
149
|
+
postCompactUsage,
|
|
150
|
+
]);
|
|
110
151
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
|
|
111
152
|
return { ok: true };
|
|
112
153
|
}
|
|
113
154
|
catch (error) {
|
|
155
|
+
if (error instanceof SessionUnavailableError)
|
|
156
|
+
return { ok: false, error: error.message, failure: error.failure };
|
|
114
157
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
115
158
|
}
|
|
116
159
|
}
|
|
117
160
|
async contextUsage() { return this.usage; }
|
|
161
|
+
async sessionUsage() { return this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined; }
|
|
118
162
|
async switchSettings(settings) {
|
|
119
163
|
this.opts.model = settings.model;
|
|
120
164
|
this.opts.effort = settings.effort;
|
|
@@ -122,6 +166,26 @@ export class CodexAdapter {
|
|
|
122
166
|
return { ok: true };
|
|
123
167
|
}
|
|
124
168
|
sessionId() { return this._sessionId; }
|
|
169
|
+
sessionRef() { return this._sessionRef; }
|
|
170
|
+
adoptSessionRef(ref) {
|
|
171
|
+
if (ref.provider !== "codex" || ref.sessionId !== this._sessionId)
|
|
172
|
+
throw new Error("cannot adopt a session reference for a different Codex thread");
|
|
173
|
+
this._sessionRef = ref;
|
|
174
|
+
}
|
|
175
|
+
async validateSession() {
|
|
176
|
+
const checkedAt = new Date().toISOString();
|
|
177
|
+
if (!this._sessionRef)
|
|
178
|
+
return { version: 1, status: "unknown", checkedAt, reason: "legacy-unscoped", detail: "Codex raw thread IDs cannot be declared exact without a stored location scope" };
|
|
179
|
+
try {
|
|
180
|
+
await this.ensureThread();
|
|
181
|
+
return this.sessionAvailability ?? { version: 1, status: "available", checkedAt, observedCwd: canonicalSessionPath(this.opts.cwd), sessionRef: this._sessionRef };
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error instanceof SessionUnavailableError && error.failure.availability)
|
|
185
|
+
return error.failure.availability;
|
|
186
|
+
return { version: 1, status: "unknown", checkedAt, reason: "probe-failed", detail: error instanceof Error ? error.message : String(error), sessionRef: this._sessionRef };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
125
189
|
events() { return this.eventQueue; }
|
|
126
190
|
async close() {
|
|
127
191
|
if (this.closed)
|
|
@@ -131,20 +195,93 @@ export class CodexAdapter {
|
|
|
131
195
|
this.eventQueue.close();
|
|
132
196
|
}
|
|
133
197
|
async ensureThread() {
|
|
198
|
+
if (this._sessionRef && !this.threadAttached) {
|
|
199
|
+
if (this._sessionRef.source === "legacy-inferred") {
|
|
200
|
+
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 };
|
|
201
|
+
this.sessionAvailability = availability;
|
|
202
|
+
throw new SessionUnavailableError({ runtime: "codex", phase: "preflight", dispatchState: "not-sent", executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: availability.detail, availability });
|
|
203
|
+
}
|
|
204
|
+
const scoped = validateProviderSessionScope(this._sessionRef, {
|
|
205
|
+
provider: "codex", cwd: this.opts.cwd, configRoot: this.opts.configRoot ?? this.opts.cwd,
|
|
206
|
+
role: this.opts.sessionRole ?? this._sessionRef.role, stream: this.opts.sessionStream ?? this._sessionRef.stream,
|
|
207
|
+
workspaceIdentity: this.opts.workspaceIdentity, ticketId: this.opts.ticketId, deliveryUnitId: this.opts.deliveryUnitId,
|
|
208
|
+
});
|
|
209
|
+
if (scoped.status !== "available" || !scoped.sessionRef) {
|
|
210
|
+
this.sessionAvailability = scoped;
|
|
211
|
+
throw new SessionUnavailableError({
|
|
212
|
+
runtime: "codex", phase: "preflight", dispatchState: "not-sent", executable: this.opts.runtimeExecutable ?? "codex",
|
|
213
|
+
cwd: this.opts.cwd, diagnostics: scoped.detail ?? `Codex session ${this._sessionRef.sessionId} is ${scoped.status}`, availability: scoped,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
this._sessionRef = scoped.sessionRef;
|
|
217
|
+
}
|
|
134
218
|
await this.ensureConnection();
|
|
135
219
|
if (this._sessionId && this.initialized === true && this.threadAttached)
|
|
136
220
|
return;
|
|
137
221
|
const method = this._sessionId ? "thread/resume" : "thread/start";
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
222
|
+
let result;
|
|
223
|
+
try {
|
|
224
|
+
result = await this.request(method, {
|
|
225
|
+
...(this._sessionId ? { threadId: this._sessionId } : {}), cwd: this.opts.cwd,
|
|
226
|
+
model: this.opts.model ?? null, approvalPolicy: "never",
|
|
227
|
+
sandbox: this.opts.sandboxMode === "read-only" ? "read-only" : "workspace-write",
|
|
228
|
+
developerInstructions: this.opts.systemPromptAppend ?? null,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (this._sessionRef) {
|
|
233
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
234
|
+
const availability = {
|
|
235
|
+
version: 1, status: "unavailable", checkedAt: new Date().toISOString(),
|
|
236
|
+
reason: /not found|unknown thread|no thread/i.test(detail) ? "not-found" : "attach-failed", detail, sessionRef: this._sessionRef,
|
|
237
|
+
};
|
|
238
|
+
this.sessionAvailability = availability;
|
|
239
|
+
throw new SessionUnavailableError({ runtime: "codex", phase: "attach", dispatchState: "not-sent", executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: detail, availability, cause: error });
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
144
243
|
const thread = result.thread;
|
|
145
|
-
|
|
244
|
+
const returnedSessionId = String(thread?.id ?? this._sessionId ?? "") || undefined;
|
|
245
|
+
if (this._sessionRef && returnedSessionId !== this._sessionRef.sessionId) {
|
|
246
|
+
const availability = {
|
|
247
|
+
version: 1,
|
|
248
|
+
status: "unavailable",
|
|
249
|
+
checkedAt: new Date().toISOString(),
|
|
250
|
+
reason: "attach-failed",
|
|
251
|
+
detail: `Codex resumed thread ${returnedSessionId ?? "without an ID"} instead of requested thread ${this._sessionRef.sessionId}`,
|
|
252
|
+
sessionRef: this._sessionRef,
|
|
253
|
+
};
|
|
254
|
+
this.sessionAvailability = availability;
|
|
255
|
+
throw new SessionUnavailableError({
|
|
256
|
+
runtime: "codex",
|
|
257
|
+
phase: "attach",
|
|
258
|
+
dispatchState: "not-sent",
|
|
259
|
+
executable: this.opts.runtimeExecutable ?? "codex",
|
|
260
|
+
cwd: this.opts.cwd,
|
|
261
|
+
diagnostics: availability.detail,
|
|
262
|
+
availability,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
this._sessionId = returnedSessionId;
|
|
146
266
|
if (!this._sessionId)
|
|
147
267
|
throw new Error(`${method} did not return a thread ID`);
|
|
268
|
+
const providerCwd = typeof thread?.cwd === "string" ? canonicalSessionPath(thread.cwd) : undefined;
|
|
269
|
+
if (this._sessionRef && providerCwd && providerCwd !== canonicalSessionPath(this._sessionRef.cwd)) {
|
|
270
|
+
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 };
|
|
271
|
+
this.sessionAvailability = availability;
|
|
272
|
+
throw new SessionUnavailableError({ runtime: "codex", phase: "attach", dispatchState: "not-sent", executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: availability.detail, availability });
|
|
273
|
+
}
|
|
274
|
+
if (!this._sessionRef) {
|
|
275
|
+
this._sessionRef = createProviderSessionRef({
|
|
276
|
+
provider: "codex", sessionId: this._sessionId, cwd: providerCwd ?? this.opts.cwd, configRoot: this.opts.configRoot ?? this.opts.cwd,
|
|
277
|
+
role: this.opts.sessionRole, stream: this.opts.sessionStream, generation: this.opts.sessionGeneration,
|
|
278
|
+
workspaceIdentity: this.opts.workspaceIdentity, ticketId: this.opts.ticketId, deliveryUnitId: this.opts.deliveryUnitId,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
this._sessionRef = { ...this._sessionRef, validatedAt: new Date().toISOString() };
|
|
283
|
+
}
|
|
284
|
+
this.sessionAvailability = { version: 1, status: "available", checkedAt: new Date().toISOString(), observedCwd: providerCwd ?? canonicalSessionPath(this.opts.cwd), sessionRef: this._sessionRef };
|
|
148
285
|
this.threadAttached = true;
|
|
149
286
|
this.eventQueue.push({ kind: "session-transition", transition: method === "thread/resume" ? "resumed" : "started" });
|
|
150
287
|
}
|
|
@@ -233,10 +370,25 @@ export class CodexAdapter {
|
|
|
233
370
|
else if (message.method === "thread/tokenUsage/updated") {
|
|
234
371
|
const tokenUsage = params.tokenUsage;
|
|
235
372
|
const total = tokenUsage?.total;
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
373
|
+
const last = tokenUsage?.last;
|
|
374
|
+
const used = Number(total?.totalTokens);
|
|
375
|
+
const maximum = Number(tokenUsage?.modelContextWindow);
|
|
376
|
+
const observedAt = new Date().toISOString();
|
|
377
|
+
const totalInput = optionalNonNegative(total?.inputTokens);
|
|
378
|
+
const totalOutput = optionalNonNegative(total?.outputTokens);
|
|
379
|
+
const sessionTotal = optionalNonNegative(total?.totalTokens);
|
|
380
|
+
if (totalInput !== undefined || totalOutput !== undefined || sessionTotal !== undefined) {
|
|
381
|
+
this.providerSessionUsage = { inputTokens: totalInput, outputTokens: totalOutput, totalTokens: sessionTotal, observedAt, source: "provider" };
|
|
382
|
+
}
|
|
383
|
+
this.lastTurnTokens = {
|
|
384
|
+
inputTokens: optionalNonNegative(last?.inputTokens),
|
|
385
|
+
outputTokens: optionalNonNegative(last?.outputTokens),
|
|
386
|
+
};
|
|
387
|
+
if (Number.isFinite(used) && used >= 0 && Number.isFinite(maximum) && maximum > 0) {
|
|
388
|
+
this.usage = { used, maximum, percentage: used / maximum * 100, observedAt, source: "provider-event" };
|
|
389
|
+
this.usageRevision += 1;
|
|
390
|
+
this.eventQueue.push({ kind: "context-usage", ...this.usage });
|
|
391
|
+
}
|
|
240
392
|
}
|
|
241
393
|
else if (message.method === "error") {
|
|
242
394
|
const error = params.error;
|
|
@@ -252,8 +404,27 @@ export class CodexAdapter {
|
|
|
252
404
|
waiter.predicate(params) ? waiter.resolve(params) : remaining.push(waiter);
|
|
253
405
|
this.notificationWaiters.set(message.method, remaining);
|
|
254
406
|
}
|
|
255
|
-
waitFor(method, predicate) {
|
|
256
|
-
return new Promise((resolve, reject) => {
|
|
407
|
+
waitFor(method, predicate, timeoutMs) {
|
|
408
|
+
return new Promise((resolve, reject) => {
|
|
409
|
+
let timeout;
|
|
410
|
+
const waiter = {
|
|
411
|
+
predicate,
|
|
412
|
+
resolve: (params) => { if (timeout)
|
|
413
|
+
clearTimeout(timeout); resolve(params); },
|
|
414
|
+
reject: (error) => { if (timeout)
|
|
415
|
+
clearTimeout(timeout); reject(error); },
|
|
416
|
+
};
|
|
417
|
+
const waiters = this.notificationWaiters.get(method) ?? [];
|
|
418
|
+
waiters.push(waiter);
|
|
419
|
+
this.notificationWaiters.set(method, waiters);
|
|
420
|
+
if (timeoutMs !== undefined) {
|
|
421
|
+
timeout = setTimeout(() => {
|
|
422
|
+
const active = this.notificationWaiters.get(method) ?? [];
|
|
423
|
+
this.notificationWaiters.set(method, active.filter((candidate) => candidate !== waiter));
|
|
424
|
+
reject(new Error(`Timed out waiting for Codex app-server notification ${method}`));
|
|
425
|
+
}, timeoutMs);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
257
428
|
}
|
|
258
429
|
disconnect(error) {
|
|
259
430
|
const child = this.process;
|
|
@@ -277,6 +448,10 @@ export class CodexAdapter {
|
|
|
277
448
|
return blocks.length ? ["# Preloaded Skills", "Use the following skills for this run.", ...blocks].join("\n\n") : undefined;
|
|
278
449
|
}
|
|
279
450
|
}
|
|
451
|
+
function optionalNonNegative(value) {
|
|
452
|
+
const number = Number(value);
|
|
453
|
+
return Number.isFinite(number) && number >= 0 ? number : undefined;
|
|
454
|
+
}
|
|
280
455
|
function activityPhase(phase) {
|
|
281
456
|
if (phase === "planning")
|
|
282
457
|
return "planning";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentRuntime } from "../runtimeAuth.js";
|
|
2
|
-
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, TurnResult } from "./types.js";
|
|
2
|
+
import type { BuilderAdapter, BuilderEvent, CompactResult, ContextUsage, ProviderSessionUsage, TurnResult } from "./types.js";
|
|
3
|
+
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
3
4
|
export type TurnRecoveryChoice = "retry" | "switch" | "cancel";
|
|
4
5
|
export interface RecoveringAdapterOptions {
|
|
5
6
|
initial: BuilderAdapter;
|
|
@@ -7,9 +8,10 @@ export interface RecoveringAdapterOptions {
|
|
|
7
8
|
enabled: boolean;
|
|
8
9
|
allowSwitch: boolean;
|
|
9
10
|
label: string;
|
|
10
|
-
recreate: (runtime: AgentRuntime, resumeSessionId?: string) => Promise<BuilderAdapter>;
|
|
11
|
+
recreate: (runtime: AgentRuntime, resumeSessionId?: string, resumeSessionRef?: ProviderSessionRefV1) => Promise<BuilderAdapter>;
|
|
11
12
|
choose?: (result: TurnResult, runtime: AgentRuntime, otherRuntime: AgentRuntime) => Promise<TurnRecoveryChoice>;
|
|
12
13
|
onRuntimeChange?: (runtime: AgentRuntime) => void;
|
|
14
|
+
onSessionRef?: (ref: ProviderSessionRefV1) => void;
|
|
13
15
|
}
|
|
14
16
|
/** Adds interactive recovery around structured runtime turn failures. */
|
|
15
17
|
export declare class RecoveringAdapter implements BuilderAdapter {
|
|
@@ -23,8 +25,12 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
23
25
|
get agent(): "claude" | "codex";
|
|
24
26
|
sendTurn(text: string): Promise<TurnResult>;
|
|
25
27
|
sessionId(): string | undefined;
|
|
28
|
+
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
29
|
+
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
30
|
+
validateSession(): Promise<SessionAvailabilityV1>;
|
|
26
31
|
compact(): Promise<CompactResult>;
|
|
27
32
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
33
|
+
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
28
34
|
events(): AsyncIterable<BuilderEvent>;
|
|
29
35
|
close(): Promise<void>;
|
|
30
36
|
private replaceAdapter;
|
|
@@ -21,8 +21,21 @@ export class RecoveringAdapter {
|
|
|
21
21
|
async sendTurn(text) {
|
|
22
22
|
while (true) {
|
|
23
23
|
const result = await this.adapter.sendTurn(text);
|
|
24
|
+
const sessionRef = this.adapter.sessionRef?.();
|
|
25
|
+
if (sessionRef && this.opts.onSessionRef) {
|
|
26
|
+
try {
|
|
27
|
+
this.opts.onSessionRef(sessionRef);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
this.eventQueue.push({ kind: "error", message: `failed to persist scoped provider session binding: ${error instanceof Error ? error.message : String(error)}` });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
24
33
|
if (!this.opts.enabled || !result.isError || !result.failure)
|
|
25
34
|
return result;
|
|
35
|
+
// The host cannot know whether a missing exact session received this
|
|
36
|
+
// instruction. Never replay it or silently switch providers.
|
|
37
|
+
if (result.failure.category === "session-unavailable")
|
|
38
|
+
return result;
|
|
26
39
|
const otherRuntime = this.runtime === "claude" ? "codex" : "claude";
|
|
27
40
|
const choice = this.opts.choose
|
|
28
41
|
? await this.opts.choose(result, this.runtime, otherRuntime)
|
|
@@ -38,7 +51,8 @@ export class RecoveringAdapter {
|
|
|
38
51
|
reportBuilderEvent(recoveryEvent);
|
|
39
52
|
const nextRuntime = choice === "switch" ? otherRuntime : this.runtime;
|
|
40
53
|
const resumeSessionId = choice === "retry" ? this.adapter.sessionId() : undefined;
|
|
41
|
-
|
|
54
|
+
const resumeSessionRef = choice === "retry" ? this.adapter.sessionRef?.() : undefined;
|
|
55
|
+
await this.replaceAdapter(nextRuntime, resumeSessionId, resumeSessionRef);
|
|
42
56
|
if (choice === "switch") {
|
|
43
57
|
log.info(`Using ${nextRuntime} for the rest of this run. This is a fresh provider session; conversational continuity was not transferred.`);
|
|
44
58
|
}
|
|
@@ -47,8 +61,14 @@ export class RecoveringAdapter {
|
|
|
47
61
|
sessionId() {
|
|
48
62
|
return this.adapter.sessionId();
|
|
49
63
|
}
|
|
64
|
+
sessionRef() { return this.adapter.sessionRef?.(); }
|
|
65
|
+
adoptSessionRef(ref) { this.adapter.adoptSessionRef?.(ref); }
|
|
66
|
+
validateSession() {
|
|
67
|
+
return this.adapter.validateSession?.() ?? Promise.resolve({ version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped", detail: "wrapped adapter does not expose scoped session validation" });
|
|
68
|
+
}
|
|
50
69
|
compact() { return this.adapter.compact?.() ?? Promise.resolve({ ok: false, error: "native compaction unavailable" }); }
|
|
51
70
|
contextUsage() { return this.adapter.contextUsage?.() ?? Promise.resolve(undefined); }
|
|
71
|
+
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
52
72
|
events() {
|
|
53
73
|
return this.eventQueue;
|
|
54
74
|
}
|
|
@@ -60,10 +80,10 @@ export class RecoveringAdapter {
|
|
|
60
80
|
await this.eventPump.catch(() => { });
|
|
61
81
|
this.eventQueue.close();
|
|
62
82
|
}
|
|
63
|
-
async replaceAdapter(runtime, resumeSessionId) {
|
|
83
|
+
async replaceAdapter(runtime, resumeSessionId, resumeSessionRef) {
|
|
64
84
|
await this.adapter.close().catch(() => { });
|
|
65
85
|
await this.eventPump.catch(() => { });
|
|
66
|
-
this.adapter = await this.opts.recreate(runtime, resumeSessionId);
|
|
86
|
+
this.adapter = await this.opts.recreate(runtime, resumeSessionId, resumeSessionRef);
|
|
67
87
|
this.runtime = runtime;
|
|
68
88
|
this.opts.onRuntimeChange?.(runtime);
|
|
69
89
|
this.eventPump = this.forwardEvents(this.adapter);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SessionAvailabilityV1 } from "rafi-spec";
|
|
2
|
+
import type { RuntimeFailure, SessionFailurePhase, TurnDispatchState, TurnResult } from "./types.js";
|
|
3
|
+
export declare class SessionUnavailableError extends Error {
|
|
4
|
+
readonly failure: RuntimeFailure;
|
|
5
|
+
constructor(input: {
|
|
6
|
+
runtime: "claude" | "codex";
|
|
7
|
+
phase: SessionFailurePhase;
|
|
8
|
+
dispatchState: TurnDispatchState;
|
|
9
|
+
executable: string;
|
|
10
|
+
cwd: string;
|
|
11
|
+
diagnostics: string;
|
|
12
|
+
availability?: SessionAvailabilityV1;
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export declare function sessionUnavailableResult(error: SessionUnavailableError): TurnResult;
|
|
17
|
+
export declare function isSessionUnavailableFailure(value: unknown): boolean;
|
|
18
|
+
export declare function sessionUnavailableErrorFromFailure(failure: RuntimeFailure): SessionUnavailableError;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class SessionUnavailableError extends Error {
|
|
2
|
+
failure;
|
|
3
|
+
constructor(input) {
|
|
4
|
+
super(input.diagnostics, input.cause === undefined ? undefined : { cause: input.cause });
|
|
5
|
+
this.name = "SessionUnavailableError";
|
|
6
|
+
this.failure = {
|
|
7
|
+
runtime: input.runtime,
|
|
8
|
+
phase: input.phase,
|
|
9
|
+
category: "session-unavailable",
|
|
10
|
+
executable: input.executable,
|
|
11
|
+
cwd: input.cwd,
|
|
12
|
+
diagnostics: input.diagnostics,
|
|
13
|
+
dispatchState: input.dispatchState,
|
|
14
|
+
...(input.availability ? { availability: input.availability } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function sessionUnavailableResult(error) {
|
|
19
|
+
return {
|
|
20
|
+
text: error.message,
|
|
21
|
+
isError: true,
|
|
22
|
+
numTurns: 0,
|
|
23
|
+
costUsd: 0,
|
|
24
|
+
costAuthoritative: false,
|
|
25
|
+
failure: error.failure,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function isSessionUnavailableFailure(value) {
|
|
29
|
+
if (value instanceof SessionUnavailableError)
|
|
30
|
+
return true;
|
|
31
|
+
if (!value || typeof value !== "object")
|
|
32
|
+
return false;
|
|
33
|
+
const failure = "failure" in value ? value.failure : value;
|
|
34
|
+
return failure?.category === "session-unavailable";
|
|
35
|
+
}
|
|
36
|
+
export function sessionUnavailableErrorFromFailure(failure) {
|
|
37
|
+
if (failure.category !== "session-unavailable")
|
|
38
|
+
throw new Error("runtime failure is not a session-unavailable failure");
|
|
39
|
+
const phase = failure.phase === "preflight" || failure.phase === "attach" || failure.phase === "turn"
|
|
40
|
+
? failure.phase
|
|
41
|
+
: "turn";
|
|
42
|
+
return new SessionUnavailableError({
|
|
43
|
+
runtime: failure.runtime,
|
|
44
|
+
phase,
|
|
45
|
+
dispatchState: failure.dispatchState ?? "unknown",
|
|
46
|
+
executable: failure.executable,
|
|
47
|
+
cwd: failure.cwd,
|
|
48
|
+
diagnostics: failure.diagnostics,
|
|
49
|
+
availability: failure.availability,
|
|
50
|
+
});
|
|
51
|
+
}
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent-agnostic interface over a coding agent ("builder").
|
|
3
3
|
*/
|
|
4
|
-
import type { RuntimeProbeCategory, RuntimeProbePhase } from "rafi-spec";
|
|
4
|
+
import type { ConfigurableAgentRole, ProviderSessionRefV1, RuntimeProbeCategory, RuntimeProbePhase, SessionAvailabilityV1 } from "rafi-spec";
|
|
5
5
|
/** A permission request surfaced by a builder before it runs a tool. */
|
|
6
6
|
export interface PermissionRequest {
|
|
7
7
|
toolName: string;
|
|
@@ -36,20 +36,29 @@ export type PermissionDecision = {
|
|
|
36
36
|
/** Decides each permission request. Supplied by the foreman, called by the adapter. */
|
|
37
37
|
export type PermissionHandler = (req: PermissionRequest) => Promise<PermissionDecision>;
|
|
38
38
|
/** Result of a single completed turn. */
|
|
39
|
+
export type SessionFailurePhase = "preflight" | "attach" | "turn";
|
|
40
|
+
export type TurnDispatchState = "not-sent" | "unknown";
|
|
41
|
+
export interface RuntimeFailure {
|
|
42
|
+
runtime: "claude" | "codex";
|
|
43
|
+
phase: RuntimeProbePhase | SessionFailurePhase;
|
|
44
|
+
category: RuntimeProbeCategory;
|
|
45
|
+
executable: string;
|
|
46
|
+
cwd: string;
|
|
47
|
+
diagnostics: string;
|
|
48
|
+
dispatchState?: TurnDispatchState;
|
|
49
|
+
availability?: SessionAvailabilityV1;
|
|
50
|
+
}
|
|
39
51
|
export interface TurnResult {
|
|
40
52
|
/** Final assistant message text — where the STEP_STATUS marker lives. */
|
|
41
53
|
text: string;
|
|
42
54
|
isError: boolean;
|
|
43
55
|
numTurns: number;
|
|
44
56
|
costUsd: number;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
cwd: string;
|
|
51
|
-
diagnostics: string;
|
|
52
|
-
};
|
|
57
|
+
/** True only when the provider supplied this value; never inferred by Rafi. */
|
|
58
|
+
costAuthoritative?: boolean;
|
|
59
|
+
inputTokens?: number;
|
|
60
|
+
outputTokens?: number;
|
|
61
|
+
failure?: RuntimeFailure;
|
|
53
62
|
}
|
|
54
63
|
/** Observability events emitted while a builder works. */
|
|
55
64
|
export type BuilderEvent = {
|
|
@@ -86,6 +95,8 @@ export type BuilderEvent = {
|
|
|
86
95
|
used: number;
|
|
87
96
|
maximum?: number;
|
|
88
97
|
percentage?: number;
|
|
98
|
+
observedAt?: string;
|
|
99
|
+
source?: "provider-event" | "provider-query" | "post-compact";
|
|
89
100
|
} | {
|
|
90
101
|
kind: "error";
|
|
91
102
|
message: string;
|
|
@@ -94,10 +105,22 @@ export interface ContextUsage {
|
|
|
94
105
|
used: number;
|
|
95
106
|
maximum?: number;
|
|
96
107
|
percentage?: number;
|
|
108
|
+
observedAt?: string;
|
|
109
|
+
source?: "provider-event" | "provider-query" | "post-compact";
|
|
110
|
+
}
|
|
111
|
+
export interface ProviderSessionUsage {
|
|
112
|
+
inputTokens?: number;
|
|
113
|
+
outputTokens?: number;
|
|
114
|
+
totalTokens?: number;
|
|
115
|
+
authoritativeCostUsd?: number;
|
|
116
|
+
observedAt: string;
|
|
117
|
+
source: "provider" | "turn-aggregate";
|
|
97
118
|
}
|
|
98
119
|
export interface CompactResult {
|
|
99
120
|
ok: boolean;
|
|
100
121
|
error?: string;
|
|
122
|
+
/** Preserve exact-session loss so lifecycle policy never treats it as an ordinary retryable compaction failure. */
|
|
123
|
+
failure?: RuntimeFailure;
|
|
101
124
|
}
|
|
102
125
|
export interface ProviderSettingSwitch {
|
|
103
126
|
model?: string;
|
|
@@ -115,7 +138,19 @@ export interface BuilderAdapterOptions {
|
|
|
115
138
|
/** Permission decision callback. */
|
|
116
139
|
permission: PermissionHandler;
|
|
117
140
|
/** Resume a prior session instead of starting fresh. */
|
|
141
|
+
/** @deprecated Foreman-controlled recovery must use resumeSessionRef. */
|
|
118
142
|
resumeSessionId?: string;
|
|
143
|
+
/** Location-scoped provider conversation to validate before exact resume. */
|
|
144
|
+
resumeSessionRef?: ProviderSessionRefV1;
|
|
145
|
+
/** Canonical Rafi configuration/recovery root. Defaults to cwd for compatibility callers. */
|
|
146
|
+
configRoot?: string;
|
|
147
|
+
/** Metadata used when the provider first reveals a fresh session identity. */
|
|
148
|
+
sessionRole?: ConfigurableAgentRole;
|
|
149
|
+
sessionStream?: string;
|
|
150
|
+
sessionGeneration?: number;
|
|
151
|
+
workspaceIdentity?: string;
|
|
152
|
+
ticketId?: string;
|
|
153
|
+
deliveryUnitId?: string;
|
|
119
154
|
/** Override the model; omit for the agent's default. */
|
|
120
155
|
model?: string;
|
|
121
156
|
/** Reasoning effort level. Claude also accepts "max"; Codex supports up to "xhigh". */
|
|
@@ -135,10 +170,18 @@ export interface BuilderAdapter {
|
|
|
135
170
|
sendTurn(text: string): Promise<TurnResult>;
|
|
136
171
|
/** Current session id, once known — used for resume. */
|
|
137
172
|
sessionId(): string | undefined;
|
|
173
|
+
/** Current location-scoped session reference, once known. */
|
|
174
|
+
sessionRef?(): ProviderSessionRefV1 | undefined;
|
|
175
|
+
/** Host-only metadata promotion after a validated handoff is accepted. */
|
|
176
|
+
adoptSessionRef?(ref: ProviderSessionRefV1): void;
|
|
177
|
+
/** Attach/probe a requested exact session without starting a provider turn. */
|
|
178
|
+
validateSession?(): Promise<SessionAvailabilityV1>;
|
|
138
179
|
/** Provider-native compaction on the exact live conversation. */
|
|
139
180
|
compact?(): Promise<CompactResult>;
|
|
140
181
|
/** Truthful provider context occupancy, when exposed by the provider. */
|
|
141
182
|
contextUsage?(): Promise<ContextUsage | undefined>;
|
|
183
|
+
/** Provider/session cumulative totals; distinct from live context occupancy. */
|
|
184
|
+
sessionUsage?(): Promise<ProviderSessionUsage | undefined>;
|
|
142
185
|
/** Attempt a provider-supported in-conversation model/reasoning transition. */
|
|
143
186
|
switchSettings?(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
144
187
|
/** Stream of observability events. Iterate to drive a live view. */
|
package/dist/agentRun.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { BuilderAdapter, BuilderAdapterOptions, EffortLevel, TurnResult } f
|
|
|
5
5
|
import type { AnsweredProviderQuestion } from "./providerQuestions.js";
|
|
6
6
|
import { type RoleBundle } from "./roles.js";
|
|
7
7
|
import type { AgentRuntime } from "./runtimeAuth.js";
|
|
8
|
-
import type { AgentRoleDefaultsV1 } from "rafi-spec";
|
|
8
|
+
import type { AgentRoleDefaultsV1, ProviderSessionRefV1 } from "rafi-spec";
|
|
9
9
|
export type { EffortLevel } from "./adapters/types.js";
|
|
10
10
|
export declare const VALID_EFFORT: readonly EffortLevel[];
|
|
11
11
|
export interface RoleBuilderOptions {
|
|
@@ -23,6 +23,8 @@ export interface RoleBuilderOptions {
|
|
|
23
23
|
extraSkills?: string[];
|
|
24
24
|
sandboxMode?: BuilderAdapterOptions["sandboxMode"];
|
|
25
25
|
resumeSessionId?: string;
|
|
26
|
+
/** Location-scoped exact session. Raw IDs are resolved through durable project bindings or treated as legacy candidates. */
|
|
27
|
+
resumeSessionRef?: ProviderSessionRefV1;
|
|
26
28
|
/** Observe successfully answered provider-native questions. */
|
|
27
29
|
onAnsweredQuestion?: (event: AnsweredProviderQuestion) => void;
|
|
28
30
|
/** Observe any provider-native question attempt, including denied prompts. */
|
|
@@ -51,6 +53,7 @@ export interface RoleInstructionRunResult {
|
|
|
51
53
|
model?: string;
|
|
52
54
|
effort?: EffortLevel;
|
|
53
55
|
sessionId?: string;
|
|
56
|
+
sessionRef?: ProviderSessionRefV1;
|
|
54
57
|
logPath: string;
|
|
55
58
|
roleBundle: RoleBundle;
|
|
56
59
|
skills: string[];
|