ai-foreman 1.7.18 → 1.7.19
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 +18 -7
- package/dist/activity.d.ts +11 -1
- package/dist/activity.js +70 -15
- package/dist/adapters/claude.d.ts +11 -1
- package/dist/adapters/claude.js +122 -18
- package/dist/adapters/codex.d.ts +13 -1
- package/dist/adapters/codex.js +148 -31
- package/dist/adapters/recovering.d.ts +3 -1
- package/dist/adapters/recovering.js +12 -4
- package/dist/adapters/types.d.ts +27 -1
- package/dist/agentRun.js +2 -0
- package/dist/branch/currentGuard.d.ts +3 -1
- package/dist/branch/currentGuard.js +11 -2
- package/dist/branch/resume.js +1 -1
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +90 -11
- package/dist/buildApproval.d.ts +9 -0
- package/dist/buildApproval.js +126 -0
- package/dist/buildRuns.js +70 -32
- package/dist/cli/recovery.js +9 -13
- package/dist/cli/runtimeAuthPrompt.d.ts +7 -0
- package/dist/cli/runtimeAuthPrompt.js +40 -5
- package/dist/cli/start.d.ts +16 -1
- package/dist/cli/start.js +159 -79
- package/dist/cli/tickets.d.ts +11 -0
- package/dist/cli/tickets.js +382 -145
- package/dist/continuity.d.ts +7 -1
- package/dist/continuity.js +86 -7
- package/dist/diagnostics.js +35 -1
- package/dist/foreman.d.ts +8 -1
- package/dist/foreman.js +144 -29
- package/dist/handoffs.js +121 -79
- package/dist/humanDecision.d.ts +18 -0
- package/dist/humanDecision.js +43 -0
- package/dist/index.js +2 -1
- package/dist/log.d.ts +1 -1
- package/dist/managerPacket.js +1 -0
- package/dist/observability.js +2 -1
- package/dist/providerQuestions.d.ts +5 -0
- package/dist/providerQuestions.js +24 -3
- package/dist/qaDeliveryJournal.d.ts +70 -0
- package/dist/qaDeliveryJournal.js +1 -0
- package/dist/qaFailureDelivery.d.ts +26 -10
- package/dist/qaFailureDelivery.js +399 -412
- package/dist/qaHandbackHistory.d.ts +4 -0
- package/dist/qaHandbackHistory.js +30 -0
- package/dist/qaHandbackMigration.d.ts +9 -0
- package/dist/qaHandbackMigration.js +147 -0
- package/dist/qaPrerequisites.d.ts +26 -0
- package/dist/qaPrerequisites.js +61 -0
- package/dist/qaProtocolV2.d.ts +8 -0
- package/dist/qaProtocolV2.js +3 -3
- package/dist/qaReview.d.ts +8 -0
- package/dist/qaReview.js +54 -10
- package/dist/questionTrace.d.ts +31 -0
- package/dist/questionTrace.js +85 -0
- package/dist/readOnlyProposal.d.ts +8 -0
- package/dist/readOnlyProposal.js +41 -0
- package/dist/recoveryPolicy.js +14 -3
- package/dist/runtimeAuth.d.ts +2 -1
- package/dist/runtimeAuth.js +2 -2
- package/dist/runtimeReadiness.d.ts +13 -1
- package/dist/runtimeReadiness.js +69 -11
- package/dist/sessionLifecycle.d.ts +1 -0
- package/dist/sessionLifecycle.js +53 -3
- package/dist/sources/sourceRegistry.d.ts +7 -0
- package/dist/sources/sourceRegistry.js +14 -0
- package/dist/stateTransfer.js +2 -0
- package/dist/statusReporter.d.ts +3 -1
- package/dist/statusReporter.js +8 -2
- package/dist/supervisedStart.d.ts +12 -0
- package/dist/supervisedStart.js +276 -0
- package/dist/supervisor.js +44 -22
- package/dist/ticketPopulation.js +1 -0
- package/dist/tickets/setupConfig.d.ts +8 -0
- package/dist/tickets/setupConfig.js +38 -4
- package/dist/util/deadline.d.ts +13 -0
- package/dist/util/deadline.js +60 -0
- package/dist/workflowDb.d.ts +55 -11
- package/dist/workflowDb.js +288 -63
- package/dist/workflowReader.d.ts +10 -2
- package/dist/workflowReader.js +35 -1
- package/package.json +3 -3
package/dist/adapters/codex.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OperationDeadline } from "../util/deadline.js";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -44,6 +45,7 @@ export class CodexAdapter {
|
|
|
44
45
|
usage;
|
|
45
46
|
usageRevision = 0;
|
|
46
47
|
providerSessionUsage;
|
|
48
|
+
turnUsageBaseline;
|
|
47
49
|
lastTurnTokens;
|
|
48
50
|
stderr = "";
|
|
49
51
|
nativeAutoCompactTokenLimit;
|
|
@@ -53,6 +55,9 @@ export class CodexAdapter {
|
|
|
53
55
|
nativeCompactions = [];
|
|
54
56
|
nativeCompactionSequence = 0;
|
|
55
57
|
manualCompactionInFlight = false;
|
|
58
|
+
compactionPromise;
|
|
59
|
+
compactionActive = false;
|
|
60
|
+
compactionUncertain = false;
|
|
56
61
|
observedToolCalls = 0;
|
|
57
62
|
activeProviderTurnId;
|
|
58
63
|
activeProviderTurnSpanId;
|
|
@@ -87,13 +92,13 @@ export class CodexAdapter {
|
|
|
87
92
|
args.push(instruction);
|
|
88
93
|
return args;
|
|
89
94
|
}
|
|
90
|
-
async sendTurn(instruction) {
|
|
95
|
+
async sendTurn(instruction, policy) {
|
|
91
96
|
const turnId = randomUUID();
|
|
92
97
|
this.activeProviderTurnId = turnId;
|
|
93
98
|
const observer = this.opts.observer;
|
|
94
99
|
if (!observer) {
|
|
95
100
|
try {
|
|
96
|
-
return await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction));
|
|
101
|
+
return await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction, policy));
|
|
97
102
|
}
|
|
98
103
|
finally {
|
|
99
104
|
this.activeProviderTurnId = undefined;
|
|
@@ -104,7 +109,7 @@ export class CodexAdapter {
|
|
|
104
109
|
this.activeProviderTurnSpanId = spanId;
|
|
105
110
|
observer.store.updateCurrentState({ runId: observer.runId, role: context.role ?? "host", stream: context.stream ?? "codex", executionId: observer.executionId, ticketId: context.ticketId, deliveryUnitId: context.deliveryUnitId, providerSessionId: context.providerSessionId, phase: "provider turn", activeSpanId: spanId, activeSpanKind: "provider_turn", lastSemanticProgressAt: new Date().toISOString() });
|
|
106
111
|
try {
|
|
107
|
-
const result = await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction));
|
|
112
|
+
const result = await withActivityPhase(`Codex ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(instruction, policy));
|
|
108
113
|
observer.store.finishSpan(spanId, { outcome: result.isError ? "failed" : "completed", attributes: { usage: result.usage } });
|
|
109
114
|
return result;
|
|
110
115
|
}
|
|
@@ -121,7 +126,7 @@ export class CodexAdapter {
|
|
|
121
126
|
this.activeProviderTurnSpanId = undefined;
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
|
-
async sendTurnInternal(instruction) {
|
|
129
|
+
async sendTurnInternal(instruction, policy) {
|
|
125
130
|
if (this.closed)
|
|
126
131
|
throw new Error("builder is closed");
|
|
127
132
|
this.eventQueue.push({ kind: "activity", state: "starting Codex turn", provider: "codex", model: this.opts.model });
|
|
@@ -129,8 +134,10 @@ export class CodexAdapter {
|
|
|
129
134
|
const providerInstruction = this.buildInstruction(instruction);
|
|
130
135
|
try {
|
|
131
136
|
await this.ensureThread();
|
|
137
|
+
const toolsBefore = this.observedToolCalls;
|
|
132
138
|
this.activeText = [];
|
|
133
139
|
this.lastTurnTokens = undefined;
|
|
140
|
+
this.turnUsageBaseline = this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined;
|
|
134
141
|
const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId, this.providerIdleTimeoutMs(), true);
|
|
135
142
|
turnStartDispatched = true;
|
|
136
143
|
await this.request("turn/start", {
|
|
@@ -140,16 +147,17 @@ export class CodexAdapter {
|
|
|
140
147
|
model: this.opts.model ?? null,
|
|
141
148
|
effort: this.opts.effort ?? (this.opts.fast ? "low" : null),
|
|
142
149
|
});
|
|
143
|
-
const params = await completion;
|
|
150
|
+
const params = await new OperationDeadline("Codex active turn", this.opts.turnDeadlineMs ?? 3_600_000).run(() => completion);
|
|
144
151
|
const turn = params.turn;
|
|
145
|
-
const
|
|
152
|
+
const responseOnlyViolation = Boolean(policy?.responseOnly && this.observedToolCalls !== toolsBefore);
|
|
153
|
+
const failed = turn?.status === "failed" || responseOnlyViolation;
|
|
146
154
|
const error = turn?.error;
|
|
147
155
|
const text = this.activeText.join("\n");
|
|
148
156
|
const result = {
|
|
149
|
-
text: failed ? normalizeRuntimeErrorText("codex", String(error?.message ?? text), null, "app-server turn") : text,
|
|
157
|
+
text: failed ? normalizeRuntimeErrorText("codex", String(responseOnlyViolation ? "Response-only correction used tools; its result cannot authorize completion" : error?.message ?? text), null, "app-server turn") : text,
|
|
150
158
|
isError: failed, numTurns: 1, costUsd: 0, costAuthoritative: false,
|
|
151
159
|
turnId: this.activeProviderTurnId, hostInstruction: instruction, providerInstruction,
|
|
152
|
-
rawResponse: text, cleanedResponse: failed ? normalizeRuntimeErrorText("codex", String(error?.message ?? text), null, "app-server turn") : text,
|
|
160
|
+
rawResponse: text, cleanedResponse: failed ? normalizeRuntimeErrorText("codex", String(responseOnlyViolation ? "Response-only correction used tools; its result cannot authorize completion" : error?.message ?? text), null, "app-server turn") : text,
|
|
153
161
|
providerMetadata: { provider: "codex", sessionId: this._sessionId, sessionRef: this._sessionRef },
|
|
154
162
|
...(this.lastTurnTokens ?? {}),
|
|
155
163
|
};
|
|
@@ -181,7 +189,7 @@ export class CodexAdapter {
|
|
|
181
189
|
executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: text,
|
|
182
190
|
availability: { version: 1, status: turnStartDispatched ? "unknown" : "unavailable", checkedAt: new Date().toISOString(), reason: turnStartDispatched ? "probe-failed" : "attach-failed", detail: text, sessionRef: this.opts.resumeSessionRef },
|
|
183
191
|
}))
|
|
184
|
-
: { text, isError: true, numTurns: 1, costUsd: 0, costAuthoritative: false };
|
|
192
|
+
: { text, isError: true, numTurns: 1, costUsd: 0, costAuthoritative: false, failure: { runtime: "codex", phase: turnStartDispatched ? "turn" : "attach", category: "agent-stream", executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: text, dispatchState: turnStartDispatched ? "unknown" : "not-sent" } };
|
|
185
193
|
this.eventQueue.push({ kind: "error", message: text });
|
|
186
194
|
result.turnId = this.activeProviderTurnId;
|
|
187
195
|
result.hostInstruction = instruction;
|
|
@@ -194,13 +202,36 @@ export class CodexAdapter {
|
|
|
194
202
|
}
|
|
195
203
|
}
|
|
196
204
|
async compact() {
|
|
197
|
-
|
|
205
|
+
if (this.compactionPromise)
|
|
206
|
+
return this.compactionPromise;
|
|
207
|
+
const operation = this.opts.observer
|
|
198
208
|
? this.opts.observer.span("compaction", "Codex context compaction", () => this.compactInternal())
|
|
199
209
|
: this.compactInternal();
|
|
210
|
+
this.compactionPromise = operation;
|
|
211
|
+
try {
|
|
212
|
+
return await operation;
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
if (this.compactionPromise === operation)
|
|
216
|
+
this.compactionPromise = undefined;
|
|
217
|
+
}
|
|
200
218
|
}
|
|
201
219
|
async compactInternal() {
|
|
220
|
+
if (this.compactionUncertain)
|
|
221
|
+
return { ok: false, error: "Prior compaction outcome is uncertain; validate a fresh successor before further work" };
|
|
222
|
+
if (this.compactionActive)
|
|
223
|
+
return { ok: false, error: "Compaction is already in progress for this session" };
|
|
224
|
+
if (this.manualCompactionInFlight)
|
|
225
|
+
return { ok: false, error: "Codex compaction already in flight" };
|
|
226
|
+
this.compactionActive = true;
|
|
227
|
+
const controller = new AbortController();
|
|
228
|
+
const began = performance.now();
|
|
229
|
+
const timeoutMs = Math.min(180_000, Math.max(1, this.opts.compactionTimeoutMs ?? 120_000));
|
|
230
|
+
const hardTimeoutMs = Math.min(180_000, timeoutMs * 1.5);
|
|
231
|
+
this.traceLifecycle("compaction-started", { timeoutMs });
|
|
232
|
+
const deadline = new OperationDeadline("Codex compaction", timeoutMs, hardTimeoutMs);
|
|
202
233
|
try {
|
|
203
|
-
await this.ensureThread();
|
|
234
|
+
await deadline.run(() => this.ensureThread());
|
|
204
235
|
this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
|
|
205
236
|
const usageRevision = this.usageRevision;
|
|
206
237
|
// A pre-compaction observation must never be mistaken for proof that the
|
|
@@ -208,36 +239,71 @@ export class CodexAdapter {
|
|
|
208
239
|
// usage notification for the compacted thread; require it alongside the
|
|
209
240
|
// explicit contextCompaction completion item.
|
|
210
241
|
this.usage = undefined;
|
|
242
|
+
let compactionItemId;
|
|
243
|
+
const progress = this.waitFor("item/started", params => {
|
|
244
|
+
const item = params.item;
|
|
245
|
+
if (params.threadId !== this._sessionId || item?.type !== "contextCompaction" || typeof item.id !== "string")
|
|
246
|
+
return false;
|
|
247
|
+
compactionItemId = item.id;
|
|
248
|
+
deadline.extendForCorrelatedProgress();
|
|
249
|
+
this.traceLifecycle("compaction-deadline-extended", { timeoutMs: hardTimeoutMs });
|
|
250
|
+
return true;
|
|
251
|
+
}, hardTimeoutMs, false, controller.signal);
|
|
252
|
+
void progress.catch(() => { });
|
|
211
253
|
const done = this.waitFor("item/completed", (params) => {
|
|
212
254
|
const item = params.item;
|
|
213
|
-
return params.threadId === this._sessionId && item?.type === "contextCompaction";
|
|
214
|
-
},
|
|
255
|
+
return params.threadId === this._sessionId && item?.type === "contextCompaction" && (!compactionItemId || item.id === compactionItemId);
|
|
256
|
+
}, hardTimeoutMs, false, controller.signal);
|
|
215
257
|
const postCompactUsage = this.waitFor("thread/tokenUsage/updated", (params) => {
|
|
216
|
-
return
|
|
258
|
+
return params.threadId === this._sessionId
|
|
217
259
|
&& this.usageRevision > usageRevision
|
|
218
260
|
&& this.usage !== undefined;
|
|
219
|
-
},
|
|
261
|
+
}, hardTimeoutMs, false, controller.signal);
|
|
220
262
|
this.manualCompactionInFlight = true;
|
|
221
263
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
264
|
+
const acknowledgementFailure = this.request("thread/compact/start", { threadId: this._sessionId }).then(() => { this.traceLifecycle("compaction-acknowledged"); return new Promise(() => { }); }, error => {
|
|
265
|
+
// A missing acknowledgement is not failure evidence. Explicit
|
|
266
|
+
// completion plus fresh occupancy is sufficient to reconcile it.
|
|
267
|
+
if (/deadline|timed out/i.test(String(error)))
|
|
268
|
+
return new Promise(() => { });
|
|
269
|
+
throw error;
|
|
270
|
+
});
|
|
271
|
+
await deadline.run(() => Promise.race([Promise.all([done, postCompactUsage]), acknowledgementFailure]));
|
|
227
272
|
}
|
|
228
273
|
finally {
|
|
229
274
|
this.manualCompactionInFlight = false;
|
|
230
275
|
}
|
|
231
276
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
|
|
277
|
+
this.traceLifecycle("compaction-completed", { elapsedMs: performance.now() - began });
|
|
232
278
|
return { ok: true };
|
|
233
279
|
}
|
|
234
280
|
catch (error) {
|
|
235
281
|
if (error instanceof SessionUnavailableError)
|
|
236
282
|
return { ok: false, error: error.message, failure: error.failure };
|
|
237
|
-
|
|
283
|
+
this.compactionUncertain = true;
|
|
284
|
+
this.usage = undefined;
|
|
285
|
+
this.traceLifecycle("compaction-failed", { elapsedMs: performance.now() - began });
|
|
286
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
287
|
+
if (/deadline|timed out/i.test(detail)) {
|
|
288
|
+
// Waiting expired, not the provider operation. Quarantine this adapter;
|
|
289
|
+
// the host must reconcile it instead of automatically starting a writer.
|
|
290
|
+
const failure = new SessionUnavailableError({ runtime: "codex", phase: "turn", dispatchState: "unknown", executable: this.opts.runtimeExecutable ?? "codex", cwd: this.opts.cwd, diagnostics: detail });
|
|
291
|
+
this.closed = true;
|
|
292
|
+
this.disconnect(failure);
|
|
293
|
+
this.eventQueue.close();
|
|
294
|
+
return { ok: false, error: detail, failure: failure.failure };
|
|
295
|
+
}
|
|
296
|
+
return { ok: false, error: detail };
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
controller.abort();
|
|
300
|
+
this.compactionActive = false;
|
|
238
301
|
}
|
|
239
302
|
}
|
|
240
303
|
async prepareAutoCompaction(thresholdPercent = this.opts.autoCompactThresholdPercent) {
|
|
304
|
+
return new OperationDeadline("Codex auto-compaction preparation", this.opts.preparationTimeoutMs ?? 120_000).run(() => this.prepareAutoCompactionInternal(thresholdPercent), () => this.disconnect(new Error("Codex preparation expired")));
|
|
305
|
+
}
|
|
306
|
+
async prepareAutoCompactionInternal(thresholdPercent = this.opts.autoCompactThresholdPercent) {
|
|
241
307
|
if (thresholdPercent === undefined)
|
|
242
308
|
return;
|
|
243
309
|
const threshold = validThreshold(thresholdPercent);
|
|
@@ -334,15 +400,35 @@ export class CodexAdapter {
|
|
|
334
400
|
return { version: 1, status: "unknown", checkedAt, reason: "probe-failed", detail: error instanceof Error ? error.message : String(error), sessionRef: this._sessionRef };
|
|
335
401
|
}
|
|
336
402
|
}
|
|
403
|
+
observeEvents(listener) { return this.eventQueue.observe(listener); }
|
|
337
404
|
events() { return this.eventQueue; }
|
|
338
405
|
async close() {
|
|
339
406
|
if (this.closed)
|
|
340
407
|
return;
|
|
341
408
|
this.closed = true;
|
|
409
|
+
const child = this.process;
|
|
410
|
+
let onExit;
|
|
411
|
+
const exited = child && child.exitCode === null ? new Promise(resolve => { onExit = resolve; child.once("exit", resolve); }) : Promise.resolve();
|
|
342
412
|
this.disconnect(new Error("app-server closed"));
|
|
343
413
|
this.eventQueue.close();
|
|
414
|
+
try {
|
|
415
|
+
try {
|
|
416
|
+
await new OperationDeadline("Codex shutdown", this.opts.shutdownTimeoutMs ?? 10_000).run(() => exited);
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
child?.kill("SIGKILL");
|
|
420
|
+
await new OperationDeadline("Codex forced shutdown", 5_000).run(() => exited);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
if (onExit)
|
|
425
|
+
child?.off("exit", onExit);
|
|
426
|
+
}
|
|
344
427
|
}
|
|
345
428
|
async ensureThread() {
|
|
429
|
+
return new OperationDeadline("Codex preparation", this.opts.preparationTimeoutMs ?? 120_000).run(() => this.ensureThreadInternal(), () => this.disconnect(new Error("Codex preparation deadline exceeded")));
|
|
430
|
+
}
|
|
431
|
+
async ensureThreadInternal() {
|
|
346
432
|
if (this._sessionRef && !this.threadAttached) {
|
|
347
433
|
if (this._sessionRef.source === "legacy-inferred") {
|
|
348
434
|
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 };
|
|
@@ -463,7 +549,7 @@ export class CodexAdapter {
|
|
|
463
549
|
}
|
|
464
550
|
request(method, params) {
|
|
465
551
|
const id = this.nextId++;
|
|
466
|
-
return new Promise((resolve, reject) => {
|
|
552
|
+
return new OperationDeadline(`Codex RPC ${method}`, this.opts.rpcTimeoutMs ?? 60_000).run(() => new Promise((resolve, reject) => {
|
|
467
553
|
this.pending.set(id, { resolve, reject });
|
|
468
554
|
try {
|
|
469
555
|
this.write({ id, method, params });
|
|
@@ -472,7 +558,7 @@ export class CodexAdapter {
|
|
|
472
558
|
this.pending.delete(id);
|
|
473
559
|
reject(error);
|
|
474
560
|
}
|
|
475
|
-
});
|
|
561
|
+
}), () => { this.pending.delete(id); });
|
|
476
562
|
}
|
|
477
563
|
write(message) {
|
|
478
564
|
if (!this.process?.stdin.writable)
|
|
@@ -566,7 +652,9 @@ export class CodexAdapter {
|
|
|
566
652
|
const tokenUsage = params.tokenUsage;
|
|
567
653
|
const total = tokenUsage?.total;
|
|
568
654
|
const last = tokenUsage?.last;
|
|
569
|
-
|
|
655
|
+
if (this._sessionId && params.threadId !== this._sessionId)
|
|
656
|
+
return;
|
|
657
|
+
const used = optionalNonNegative(last?.totalTokens);
|
|
570
658
|
const maximum = Number(tokenUsage?.modelContextWindow);
|
|
571
659
|
const observedAt = new Date().toISOString();
|
|
572
660
|
const totalInput = optionalNonNegative(total?.inputTokens);
|
|
@@ -575,11 +663,14 @@ export class CodexAdapter {
|
|
|
575
663
|
if (totalInput !== undefined || totalOutput !== undefined || sessionTotal !== undefined) {
|
|
576
664
|
this.providerSessionUsage = { inputTokens: totalInput, outputTokens: totalOutput, totalTokens: sessionTotal, observedAt, source: "provider" };
|
|
577
665
|
}
|
|
666
|
+
if ((totalInput !== undefined && this.turnUsageBaseline?.inputTokens !== undefined && totalInput < this.turnUsageBaseline.inputTokens) || (totalOutput !== undefined && this.turnUsageBaseline?.outputTokens !== undefined && totalOutput < this.turnUsageBaseline.outputTokens))
|
|
667
|
+
this.turnUsageBaseline = undefined;
|
|
578
668
|
this.lastTurnTokens = {
|
|
579
|
-
inputTokens:
|
|
580
|
-
outputTokens:
|
|
669
|
+
inputTokens: cumulativeDelta(this.turnUsageBaseline?.inputTokens, totalInput),
|
|
670
|
+
outputTokens: cumulativeDelta(this.turnUsageBaseline?.outputTokens, totalOutput),
|
|
581
671
|
};
|
|
582
|
-
|
|
672
|
+
this.usage = undefined;
|
|
673
|
+
if (used !== undefined && Number.isFinite(maximum) && maximum > 0) {
|
|
583
674
|
this.usage = { used, maximum, percentage: used / maximum * 100, observedAt, source: "provider-event" };
|
|
584
675
|
this.usageRevision += 1;
|
|
585
676
|
this.eventQueue.push({ kind: "context-usage", ...this.usage });
|
|
@@ -617,16 +708,29 @@ export class CodexAdapter {
|
|
|
617
708
|
ticketId: this.opts.ticketId ?? inherited.ticketId, deliveryUnitId: this.opts.deliveryUnitId ?? inherited.deliveryUnitId,
|
|
618
709
|
parentSpanId: this.activeProviderTurnSpanId ?? inherited.parentSpanId, providerTurnId: this.activeProviderTurnId ?? inherited.providerTurnId };
|
|
619
710
|
}
|
|
620
|
-
waitFor(method, predicate, timeoutMs, resetOnProviderActivity = false) {
|
|
711
|
+
waitFor(method, predicate, timeoutMs, resetOnProviderActivity = false, signal) {
|
|
621
712
|
return new Promise((resolve, reject) => {
|
|
622
713
|
let timeout;
|
|
714
|
+
const began = performance.now();
|
|
623
715
|
const clear = () => { if (timeout)
|
|
624
|
-
clearTimeout(timeout); timeout = undefined; };
|
|
716
|
+
clearTimeout(timeout); timeout = undefined; signal?.removeEventListener("abort", abort); };
|
|
625
717
|
const waiter = {
|
|
626
718
|
predicate,
|
|
627
|
-
resolve: (params) => { clear(); resolve(params); },
|
|
719
|
+
resolve: (params) => { clear(); this.traceLifecycle("waiter-completed", { method, elapsedMs: performance.now() - began }); resolve(params); },
|
|
628
720
|
reject: (error) => { clear(); reject(error); },
|
|
629
721
|
};
|
|
722
|
+
const abort = () => {
|
|
723
|
+
this.notificationWaiters.set(method, (this.notificationWaiters.get(method) ?? []).filter(candidate => candidate !== waiter));
|
|
724
|
+
clear();
|
|
725
|
+
this.traceLifecycle("waiter-cancelled", { method });
|
|
726
|
+
reject(new Error(`Codex waiter cancelled: ${method}`));
|
|
727
|
+
};
|
|
728
|
+
if (signal?.aborted) {
|
|
729
|
+
abort();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
733
|
+
this.traceLifecycle("waiter-registered", { method, timeoutMs });
|
|
630
734
|
const waiters = this.notificationWaiters.get(method) ?? [];
|
|
631
735
|
waiters.push(waiter);
|
|
632
736
|
this.notificationWaiters.set(method, waiters);
|
|
@@ -635,7 +739,9 @@ export class CodexAdapter {
|
|
|
635
739
|
timeout = setTimeout(() => {
|
|
636
740
|
const active = this.notificationWaiters.get(method) ?? [];
|
|
637
741
|
this.notificationWaiters.set(method, active.filter((candidate) => candidate !== waiter));
|
|
638
|
-
|
|
742
|
+
clear();
|
|
743
|
+
this.traceLifecycle("waiter-timeout", { method, timeoutMs, elapsedMs: performance.now() - began });
|
|
744
|
+
reject(new Error(`Codex provider wait timed out after ${timeoutMs} ms while waiting for ${method}; the turn may have been dispatched and will not be retried automatically`));
|
|
639
745
|
}, timeoutMs);
|
|
640
746
|
};
|
|
641
747
|
if (resetOnProviderActivity)
|
|
@@ -644,6 +750,12 @@ export class CodexAdapter {
|
|
|
644
750
|
}
|
|
645
751
|
});
|
|
646
752
|
}
|
|
753
|
+
traceLifecycle(phase, data = {}) {
|
|
754
|
+
try {
|
|
755
|
+
this.opts.onLifecycleTrace?.({ phase, at: new Date().toISOString(), sessionId: this._sessionId, ...data });
|
|
756
|
+
}
|
|
757
|
+
catch { /* diagnostics cannot change execution */ }
|
|
758
|
+
}
|
|
647
759
|
touchWaiters() {
|
|
648
760
|
for (const waiters of this.notificationWaiters.values())
|
|
649
761
|
for (const waiter of waiters)
|
|
@@ -689,6 +801,8 @@ export class CodexAdapter {
|
|
|
689
801
|
}
|
|
690
802
|
}
|
|
691
803
|
function optionalNonNegative(value) {
|
|
804
|
+
if (value === null || value === undefined || value === "")
|
|
805
|
+
return undefined;
|
|
692
806
|
const number = Number(value);
|
|
693
807
|
return Number.isFinite(number) && number >= 0 ? number : undefined;
|
|
694
808
|
}
|
|
@@ -759,3 +873,6 @@ function loadSkillMarkdown(cwd, skill) {
|
|
|
759
873
|
return undefined;
|
|
760
874
|
}
|
|
761
875
|
}
|
|
876
|
+
function cumulativeDelta(before, after) {
|
|
877
|
+
return before !== undefined && after !== undefined && after >= before ? after - before : undefined;
|
|
878
|
+
}
|
|
@@ -25,7 +25,7 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
25
25
|
private closed;
|
|
26
26
|
constructor(opts: RecoveringAdapterOptions);
|
|
27
27
|
get agent(): "claude" | "codex";
|
|
28
|
-
sendTurn(text: string): Promise<TurnResult>;
|
|
28
|
+
sendTurn(text: string, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
|
|
29
29
|
sessionId(): string | undefined;
|
|
30
30
|
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
31
31
|
prepareSession(): Promise<ProviderSessionRefV1>;
|
|
@@ -41,6 +41,8 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
41
41
|
contextUsage(): Promise<ContextUsage | undefined>;
|
|
42
42
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
43
43
|
events(): AsyncIterable<BuilderEvent>;
|
|
44
|
+
observeEvents(listener: (event: BuilderEvent) => void): () => void;
|
|
45
|
+
acceptHandbackTurn(turn: TurnResult): void;
|
|
44
46
|
close(): Promise<void>;
|
|
45
47
|
private replaceAdapter;
|
|
46
48
|
private forwardEvents;
|
|
@@ -19,9 +19,9 @@ export class RecoveringAdapter {
|
|
|
19
19
|
get agent() {
|
|
20
20
|
return this.adapter.agent;
|
|
21
21
|
}
|
|
22
|
-
async sendTurn(text) {
|
|
22
|
+
async sendTurn(text, policy) {
|
|
23
23
|
while (true) {
|
|
24
|
-
const result = await this.adapter.sendTurn(text);
|
|
24
|
+
const result = await this.adapter.sendTurn(text, policy);
|
|
25
25
|
const sessionRef = this.adapter.sessionRef?.();
|
|
26
26
|
if (sessionRef && this.opts.onSessionRef) {
|
|
27
27
|
try {
|
|
@@ -31,11 +31,11 @@ export class RecoveringAdapter {
|
|
|
31
31
|
this.eventQueue.push({ kind: "error", message: `failed to persist scoped provider session binding: ${error instanceof Error ? error.message : String(error)}` });
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
if (!this.opts.enabled || !result.isError || !result.failure)
|
|
34
|
+
if (policy?.handback || !this.opts.enabled || !result.isError || !result.failure)
|
|
35
35
|
return result;
|
|
36
36
|
// The host cannot know whether a missing exact session received this
|
|
37
37
|
// instruction. Never replay it or silently switch providers.
|
|
38
|
-
if (result.failure.category === "session-unavailable")
|
|
38
|
+
if (result.failure.category === "session-unavailable" || result.failure.dispatchState === "unknown")
|
|
39
39
|
return result;
|
|
40
40
|
const otherRuntime = this.runtime === "claude" ? "codex" : "claude";
|
|
41
41
|
const choice = this.opts.choose
|
|
@@ -93,6 +93,14 @@ export class RecoveringAdapter {
|
|
|
93
93
|
events() {
|
|
94
94
|
return this.eventQueue;
|
|
95
95
|
}
|
|
96
|
+
observeEvents(listener) {
|
|
97
|
+
if (!this.adapter.observeEvents)
|
|
98
|
+
throw new Error("wrapped provider does not expose turn observation");
|
|
99
|
+
// Observe synchronously at the provider, before sendTurn resolves. The async
|
|
100
|
+
// display pump may still have buffered events when the journal unsubscribes.
|
|
101
|
+
return this.adapter.observeEvents(listener);
|
|
102
|
+
}
|
|
103
|
+
acceptHandbackTurn(turn) { this.adapter.acceptHandbackTurn?.(turn); }
|
|
96
104
|
async close() {
|
|
97
105
|
if (this.closed)
|
|
98
106
|
return;
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -70,6 +70,8 @@ export interface TurnResult {
|
|
|
70
70
|
failure?: RuntimeFailure;
|
|
71
71
|
/** Stable host/provider correlation ID for this turn. */
|
|
72
72
|
turnId?: string;
|
|
73
|
+
/** Deferred handback continuity validation; no hidden provider repair. */
|
|
74
|
+
continuityErrors?: string[];
|
|
73
75
|
/** Exact instruction requested by the immediate host caller. */
|
|
74
76
|
hostInstruction?: string;
|
|
75
77
|
/** Exact instruction dispatched to the provider after all wrappers. */
|
|
@@ -246,6 +248,8 @@ export interface BuilderAdapterOptions {
|
|
|
246
248
|
allowAutoCompactionSetupTurn?: boolean;
|
|
247
249
|
/** Raw-adapter observability; wrapper adapters must not persist re-emitted events. */
|
|
248
250
|
observer?: RunObserver;
|
|
251
|
+
/** Redacted AskUserQuestion diagnostics, independent of build observability. */
|
|
252
|
+
onQuestionTrace?: import("../questionTrace.js").QuestionTraceSink;
|
|
249
253
|
/**
|
|
250
254
|
* Maximum time a dispatched provider turn may remain completely silent.
|
|
251
255
|
* This is an idle timeout, not a total turn limit: every provider message
|
|
@@ -253,11 +257,33 @@ export interface BuilderAdapterOptions {
|
|
|
253
257
|
* state, so callers must never replay the instruction automatically.
|
|
254
258
|
*/
|
|
255
259
|
providerIdleTimeoutMs?: number;
|
|
260
|
+
compactionTimeoutMs?: number;
|
|
261
|
+
preparationTimeoutMs?: number;
|
|
262
|
+
rpcTimeoutMs?: number;
|
|
263
|
+
turnDeadlineMs?: number;
|
|
264
|
+
shutdownTimeoutMs?: number;
|
|
265
|
+
/** Metadata-only lifecycle trace; no prompts, responses, credentials, or tool inputs. */
|
|
266
|
+
onLifecycleTrace?: (event: {
|
|
267
|
+
phase: string;
|
|
268
|
+
at: string;
|
|
269
|
+
elapsedMs?: number;
|
|
270
|
+
sessionId?: string;
|
|
271
|
+
method?: string;
|
|
272
|
+
timeoutMs?: number;
|
|
273
|
+
}) => void;
|
|
256
274
|
}
|
|
257
275
|
export interface BuilderAdapter {
|
|
258
276
|
readonly agent: "claude" | "codex";
|
|
259
277
|
/** Send one instruction; resolves when that turn completes. */
|
|
260
|
-
sendTurn(text: string
|
|
278
|
+
sendTurn(text: string, policy?: {
|
|
279
|
+
handback?: boolean;
|
|
280
|
+
responseOnly?: boolean;
|
|
281
|
+
logicalActionId?: string;
|
|
282
|
+
}): Promise<TurnResult>;
|
|
283
|
+
/** Synchronous observation through the event owner. Terminal precedes sendTurn resolution. */
|
|
284
|
+
observeEvents?(listener: (event: BuilderEvent) => void): () => void;
|
|
285
|
+
/** Commit a deferred handback checkpoint only after the host validates the turn. */
|
|
286
|
+
acceptHandbackTurn?(turn: TurnResult): void;
|
|
261
287
|
/** Current session id, once known — used for resume. */
|
|
262
288
|
sessionId(): string | undefined;
|
|
263
289
|
/** Current location-scoped session reference, once known. */
|
package/dist/agentRun.js
CHANGED
|
@@ -178,6 +178,8 @@ export async function createRoleBuilder(opts) {
|
|
|
178
178
|
configRoot: opts.projectDir,
|
|
179
179
|
runtimeExecutable,
|
|
180
180
|
runtimePhase: phaseForRole(opts.role),
|
|
181
|
+
onQuestionTrace: (trace) => log.write("question-round-trip", { ...trace }),
|
|
182
|
+
onLifecycleTrace: (trace) => log.write("provider-lifecycle", { ...trace, role: opts.role }),
|
|
181
183
|
model,
|
|
182
184
|
...(scopedRef ? { resumeSessionRef: scopedRef } : {}),
|
|
183
185
|
sessionRole: configurableRole(opts.role),
|
|
@@ -20,7 +20,7 @@ export declare class CurrentWorkflowGuardAdapter implements BuilderAdapter {
|
|
|
20
20
|
private readonly expected;
|
|
21
21
|
readonly agent: BuilderAdapter["agent"];
|
|
22
22
|
constructor(adapter: BuilderAdapter, cwd: string, expected?: CurrentWorkflowIdentity);
|
|
23
|
-
sendTurn(text: string): Promise<TurnResult>;
|
|
23
|
+
sendTurn(text: string, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
|
|
24
24
|
sessionId(): string | undefined;
|
|
25
25
|
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
26
26
|
prepareSession(): Promise<ProviderSessionRefV1>;
|
|
@@ -37,5 +37,7 @@ export declare class CurrentWorkflowGuardAdapter implements BuilderAdapter {
|
|
|
37
37
|
sessionUsage(): Promise<ProviderSessionUsage | undefined>;
|
|
38
38
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
39
39
|
events(): AsyncIterable<BuilderEvent>;
|
|
40
|
+
observeEvents(listener: (event: BuilderEvent) => void): () => void;
|
|
41
|
+
acceptHandbackTurn(turn: TurnResult): void;
|
|
40
42
|
close(): Promise<void>;
|
|
41
43
|
}
|
|
@@ -37,9 +37,9 @@ export class CurrentWorkflowGuardAdapter {
|
|
|
37
37
|
this.expected = expected;
|
|
38
38
|
this.agent = adapter.agent;
|
|
39
39
|
}
|
|
40
|
-
async sendTurn(text) {
|
|
40
|
+
async sendTurn(text, policy) {
|
|
41
41
|
assertCurrentWorkflowIdentity(this.cwd, this.expected);
|
|
42
|
-
const result = await this.adapter.sendTurn(text);
|
|
42
|
+
const result = await this.adapter.sendTurn(text, policy);
|
|
43
43
|
assertCurrentWorkflowIdentity(this.cwd, this.expected);
|
|
44
44
|
return result;
|
|
45
45
|
}
|
|
@@ -66,6 +66,15 @@ export class CurrentWorkflowGuardAdapter {
|
|
|
66
66
|
sessionUsage() { return this.adapter.sessionUsage?.() ?? Promise.resolve(undefined); }
|
|
67
67
|
switchSettings(settings) { return this.adapter.switchSettings?.(settings) ?? Promise.resolve({ ok: false, error: "settings switch unavailable" }); }
|
|
68
68
|
events() { return this.adapter.events(); }
|
|
69
|
+
observeEvents(listener) {
|
|
70
|
+
if (!this.adapter.observeEvents)
|
|
71
|
+
throw new Error("wrapped provider does not expose turn observation");
|
|
72
|
+
return this.adapter.observeEvents(listener);
|
|
73
|
+
}
|
|
74
|
+
acceptHandbackTurn(turn) {
|
|
75
|
+
assertCurrentWorkflowIdentity(this.cwd, this.expected);
|
|
76
|
+
this.adapter.acceptHandbackTurn?.(turn);
|
|
77
|
+
}
|
|
69
78
|
close() { return this.adapter.close(); }
|
|
70
79
|
}
|
|
71
80
|
function currentRef(cwd) {
|
package/dist/branch/resume.js
CHANGED
|
@@ -7,7 +7,7 @@ export function findResumableBranchSessions(foremanDir) {
|
|
|
7
7
|
const reader = new WorkflowReader(dirname(foremanDir));
|
|
8
8
|
try {
|
|
9
9
|
const structured = reader.branchResumeSessions().filter(session => existsSync(session.worktreePath));
|
|
10
|
-
if (structured.length)
|
|
10
|
+
if (structured.length || reader.branchResumeSessions(false).length || reader.buildRuns().length)
|
|
11
11
|
return structured.sort((a, b) => a.ticket.localeCompare(b.ticket, undefined, { numeric: true }));
|
|
12
12
|
}
|
|
13
13
|
finally {
|
package/dist/branch/runner.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { SessionUnavailableError } from "../adapters/sessionFailure.js";
|
|
|
8
8
|
import { SessionUnavailableContinuityError } from "../continuity.js";
|
|
9
9
|
import type { QaRecoveryPacket } from "../qaRecovery.js";
|
|
10
10
|
export interface DeliveryUnitSession {
|
|
11
|
+
runId?: string;
|
|
11
12
|
unitId: string;
|
|
12
13
|
branch: string;
|
|
13
14
|
worktreePath: string;
|
|
@@ -18,6 +19,7 @@ export interface DeliveryUnitSession {
|
|
|
18
19
|
export type BaseWorktreePolicy = "enforce" | "warn" | "skip";
|
|
19
20
|
export declare function readDeliveryUnitSession(projectDir: string, unitId: string): DeliveryUnitSession | undefined;
|
|
20
21
|
export interface BranchRunnerOptions {
|
|
22
|
+
continueIndependentTickets?: boolean;
|
|
21
23
|
projectDir: string;
|
|
22
24
|
runId: string;
|
|
23
25
|
plan: BranchPlan;
|