ai-foreman 1.7.17 → 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 +84 -16
- package/dist/adapters/claude.d.ts +16 -1
- package/dist/adapters/claude.js +196 -29
- package/dist/adapters/codex.d.ts +15 -1
- package/dist/adapters/codex.js +172 -38
- package/dist/adapters/recovering.d.ts +3 -1
- package/dist/adapters/recovering.js +12 -4
- package/dist/adapters/types.d.ts +34 -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 +74 -55
- package/dist/cli/events.js +21 -1
- 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 +161 -81
- 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 +37 -8
- 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 +3 -7
- package/dist/processIdentity.d.ts +8 -0
- package/dist/processIdentity.js +40 -0
- package/dist/projectDiagnostics.js +3 -16
- package/dist/providerQuestions.d.ts +5 -0
- package/dist/providerQuestions.js +27 -2
- 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 +284 -70
- package/dist/workflowReader.d.ts +10 -2
- package/dist/workflowReader.js +35 -1
- package/package.json +3 -3
package/dist/adapters/claude.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OperationDeadline } from "../util/deadline.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
/** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
|
|
@@ -11,12 +12,14 @@ export async function requireClaudeSDK() {
|
|
|
11
12
|
"Or use Codex for this run with --agent codex.");
|
|
12
13
|
}
|
|
13
14
|
}
|
|
15
|
+
import { QuestionRoundTripTrace } from "../questionTrace.js";
|
|
14
16
|
import { AsyncQueue } from "../util/asyncQueue.js";
|
|
15
|
-
import { BuilderEventQueue, withActivityPhase } from "../activity.js";
|
|
17
|
+
import { BuilderEventQueue, currentActivity, withActivityPhase } from "../activity.js";
|
|
16
18
|
import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
|
|
17
19
|
import { classifyClaudeSdkFailure, resolveExecutablePath, sanitizeDiagnostics, } from "../runtimeReadiness.js";
|
|
18
20
|
import { createProviderSessionRef, validateProviderSessionScope, canonicalSessionPath } from "../sessionIdentity.js";
|
|
19
21
|
import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
|
|
22
|
+
const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
20
23
|
/**
|
|
21
24
|
* Pure function: build the `options` object passed to `query()`.
|
|
22
25
|
* Extracted so tests can assert on the shape without making a live SDK call.
|
|
@@ -177,6 +180,8 @@ export class ClaudeAdapter {
|
|
|
177
180
|
structuredError;
|
|
178
181
|
apiErrorStatus;
|
|
179
182
|
compactResult;
|
|
183
|
+
manualCompactionDeadline;
|
|
184
|
+
compactionPromise;
|
|
180
185
|
autoCompactionPrepared = false;
|
|
181
186
|
preparedAutoCompactThreshold;
|
|
182
187
|
preparedAutoCompactionPolicy;
|
|
@@ -189,6 +194,8 @@ export class ClaudeAdapter {
|
|
|
189
194
|
activeProviderTurnSpanId;
|
|
190
195
|
toolSpans = new Map();
|
|
191
196
|
pending;
|
|
197
|
+
questionTrace;
|
|
198
|
+
responseOnlyTurn = false;
|
|
192
199
|
terminalResult;
|
|
193
200
|
streamEnded = false;
|
|
194
201
|
closed = false;
|
|
@@ -239,6 +246,10 @@ export class ClaudeAdapter {
|
|
|
239
246
|
this.rejectSessionIdentity = reject;
|
|
240
247
|
});
|
|
241
248
|
void this.sessionIdentityReady.catch(() => { });
|
|
249
|
+
this.questionTrace = new QuestionRoundTripTrace(opts.runtimePhase ?? "builder", opts.onQuestionTrace, opts.observer ? {
|
|
250
|
+
start: (attemptId) => opts.observer.store.startSpan(this.observationContext(), { kind: "provider_wait", name: "Waiting for Claude after question answer", attributes: { attemptId } }),
|
|
251
|
+
finish: (spanId, outcome) => opts.observer.store.finishSpan(spanId, { outcome }),
|
|
252
|
+
} : undefined);
|
|
242
253
|
this.query = query({
|
|
243
254
|
prompt: this.inbox,
|
|
244
255
|
options: {
|
|
@@ -258,18 +269,38 @@ export class ClaudeAdapter {
|
|
|
258
269
|
}] }] },
|
|
259
270
|
stderr: (data) => this.captureStderr(data),
|
|
260
271
|
canUseTool: async (toolName, input, requestOptions = {}) => {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
272
|
+
if (this.responseOnlyTurn)
|
|
273
|
+
return { behavior: "deny", message: "QA response correction forbids all tools, including read-only tools", interrupt: true };
|
|
274
|
+
const isProviderQuestion = toolName === "AskUserQuestion";
|
|
275
|
+
const questionAttempt = isProviderQuestion ? this.questionTrace?.begin() : undefined;
|
|
276
|
+
if (isProviderQuestion)
|
|
277
|
+
this.beginProviderQuestionWait();
|
|
278
|
+
try {
|
|
279
|
+
const decision = await opts.permission({
|
|
280
|
+
toolName,
|
|
281
|
+
input,
|
|
282
|
+
signal: requestOptions.signal,
|
|
283
|
+
title: requestOptions.title,
|
|
284
|
+
displayName: requestOptions.displayName,
|
|
285
|
+
description: requestOptions.description,
|
|
286
|
+
decisionReason: requestOptions.decisionReason,
|
|
287
|
+
blockedPath: requestOptions.blockedPath,
|
|
288
|
+
toolUseID: requestOptions.toolUseID,
|
|
289
|
+
});
|
|
290
|
+
const result = permissionDecisionToClaudeResult(decision, requestOptions.toolUseID);
|
|
291
|
+
if (questionAttempt)
|
|
292
|
+
this.questionTrace?.returned(questionAttempt, decision.behavior === "allow" ? "allowed" : "denied");
|
|
293
|
+
if (isProviderQuestion)
|
|
294
|
+
this.endProviderQuestionWait(decision.behavior === "allow");
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (questionAttempt)
|
|
299
|
+
this.questionTrace?.returned(questionAttempt, "callback-error");
|
|
300
|
+
if (isProviderQuestion)
|
|
301
|
+
this.endProviderQuestionWait(false);
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
273
304
|
},
|
|
274
305
|
},
|
|
275
306
|
});
|
|
@@ -283,6 +314,7 @@ export class ClaudeAdapter {
|
|
|
283
314
|
}
|
|
284
315
|
}
|
|
285
316
|
catch (err) {
|
|
317
|
+
this.questionTrace?.finish(this.closed ? "closed" : "stream-error");
|
|
286
318
|
// Suppress the AbortError that fires when close() aborts the stream.
|
|
287
319
|
const isShutdownAbort = this.closed &&
|
|
288
320
|
(err instanceof Error &&
|
|
@@ -298,6 +330,7 @@ export class ClaudeAdapter {
|
|
|
298
330
|
}
|
|
299
331
|
}
|
|
300
332
|
finally {
|
|
333
|
+
this.questionTrace?.finish(this.closed ? "closed" : "stream-ended");
|
|
301
334
|
this.streamEnded = true;
|
|
302
335
|
if (!this.closed && this.pending) {
|
|
303
336
|
const result = this.streamFailureResult("Claude stream ended without a result", Boolean(this.opts.resumeSessionRef ?? this.opts.resumeSessionId));
|
|
@@ -313,6 +346,16 @@ export class ClaudeAdapter {
|
|
|
313
346
|
}
|
|
314
347
|
}
|
|
315
348
|
handle(msg) {
|
|
349
|
+
const questionStreamOutcome = msg.type === "system" && msg.subtype === "api_retry" ? "provider-retry"
|
|
350
|
+
: msg.type === "auth_status" && msg.error ? "provider-auth-error"
|
|
351
|
+
: msg.type === "assistant" && (msg.error === "authentication_failed" || msg.error === "oauth_org_not_allowed") ? "provider-auth-error"
|
|
352
|
+
: msg.type === "assistant" && msg.error ? "provider-error" : "stream-message";
|
|
353
|
+
this.questionTrace?.message(questionStreamOutcome);
|
|
354
|
+
if (this.pending?.providerQuestionAnswered && !this.pending.providerQuestionWaits) {
|
|
355
|
+
this.pending.providerQuestionAnswered = false;
|
|
356
|
+
currentActivity()?.update("Claude stream resumed");
|
|
357
|
+
}
|
|
358
|
+
this.touchPendingTurn();
|
|
316
359
|
if ("session_id" in msg && typeof msg.session_id === "string") {
|
|
317
360
|
this.observeSession(msg.session_id, "cwd" in msg && typeof msg.cwd === "string" ? msg.cwd : undefined);
|
|
318
361
|
}
|
|
@@ -356,8 +399,11 @@ export class ClaudeAdapter {
|
|
|
356
399
|
this.turnSignals.push(...msg.output.map((line) => `auth: ${line}`));
|
|
357
400
|
}
|
|
358
401
|
else if (msg.type === "system" && msg.subtype === "status") {
|
|
359
|
-
if (msg.status === "compacting")
|
|
402
|
+
if (msg.status === "compacting") {
|
|
403
|
+
if (this.manualCompactionInFlight && msg.session_id === this._sessionId)
|
|
404
|
+
this.manualCompactionDeadline?.extendForCorrelatedProgress();
|
|
360
405
|
this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
|
|
406
|
+
}
|
|
361
407
|
if (msg.compact_result === "success") {
|
|
362
408
|
this.compactResult = { ok: true };
|
|
363
409
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
|
|
@@ -462,7 +508,10 @@ export class ClaudeAdapter {
|
|
|
462
508
|
this.stderrChunks.length = 0;
|
|
463
509
|
}
|
|
464
510
|
}
|
|
465
|
-
async sendTurn(text) {
|
|
511
|
+
async sendTurn(text, policy) {
|
|
512
|
+
if (this.pending)
|
|
513
|
+
throw new Error("a turn is already in progress");
|
|
514
|
+
this.responseOnlyTurn = Boolean(policy?.responseOnly);
|
|
466
515
|
const turnId = randomUUID();
|
|
467
516
|
this.activeProviderTurnId = turnId;
|
|
468
517
|
const observer = this.opts.observer;
|
|
@@ -472,6 +521,7 @@ export class ClaudeAdapter {
|
|
|
472
521
|
}
|
|
473
522
|
finally {
|
|
474
523
|
this.activeProviderTurnId = undefined;
|
|
524
|
+
this.responseOnlyTurn = false;
|
|
475
525
|
}
|
|
476
526
|
}
|
|
477
527
|
const context = this.observationContext();
|
|
@@ -498,6 +548,7 @@ export class ClaudeAdapter {
|
|
|
498
548
|
}
|
|
499
549
|
this.activeProviderTurnId = undefined;
|
|
500
550
|
this.activeProviderTurnSpanId = undefined;
|
|
551
|
+
this.responseOnlyTurn = false;
|
|
501
552
|
}
|
|
502
553
|
}
|
|
503
554
|
sendTurnInternal(text) {
|
|
@@ -513,7 +564,8 @@ export class ClaudeAdapter {
|
|
|
513
564
|
this.turnSignals = [];
|
|
514
565
|
this.structuredError = undefined;
|
|
515
566
|
this.apiErrorStatus = undefined;
|
|
516
|
-
this.pending = { resolve, reject, instruction: text, turnId: this.activeProviderTurnId ?? randomUUID() };
|
|
567
|
+
const pending = this.pending = { resolve, reject, instruction: text, turnId: this.activeProviderTurnId ?? randomUUID() };
|
|
568
|
+
this.armPendingTurn(pending);
|
|
517
569
|
this.inbox.push({
|
|
518
570
|
type: "user",
|
|
519
571
|
message: { role: "user", content: text },
|
|
@@ -562,7 +614,7 @@ export class ClaudeAdapter {
|
|
|
562
614
|
return this._sessionId;
|
|
563
615
|
}
|
|
564
616
|
sessionRef() { return this._sessionRef; }
|
|
565
|
-
async prepareSession(timeoutMs =
|
|
617
|
+
async prepareSession(timeoutMs = this.opts.preparationTimeoutMs ?? 120_000) {
|
|
566
618
|
if (this.closed || this.streamEnded || this.terminalResult)
|
|
567
619
|
throw new Error(this.terminalResult?.text ?? "Claude session is closed");
|
|
568
620
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
@@ -577,7 +629,24 @@ export class ClaudeAdapter {
|
|
|
577
629
|
aborted();
|
|
578
630
|
timer = setTimeout(() => reject(new Error(`Claude did not expose a scoped session identity within ${timeoutMs}ms`)), timeoutMs);
|
|
579
631
|
});
|
|
580
|
-
const
|
|
632
|
+
const prepare = async () => {
|
|
633
|
+
await this.query.initializationResult();
|
|
634
|
+
if (!this._sessionRef?.validatedAt && typeof this.query.supportedCommands === "function") {
|
|
635
|
+
const commands = await this.query.supportedCommands();
|
|
636
|
+
// Some SDK/CLI pairs expose init identity only after their first input.
|
|
637
|
+
// Require an advertised built-in local command; never substitute a
|
|
638
|
+
// model-generated warm-up for QA's pre-dispatch identity validation.
|
|
639
|
+
if (commands.some(command => command.name === "context" && "builtin" in command && command.builtin === true)) {
|
|
640
|
+
this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-start", at: new Date().toISOString() });
|
|
641
|
+
const result = await this.sendTurn("/context", { responseOnly: true });
|
|
642
|
+
this.opts.onLifecycleTrace?.({ phase: "local-context-initialization-complete", at: new Date().toISOString() });
|
|
643
|
+
if (result.isError || result.failure || result.costUsd > 0)
|
|
644
|
+
throw new Error("Claude local context initialization did not complete without model work");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return this.sessionIdentityReady;
|
|
648
|
+
};
|
|
649
|
+
const ref = await Promise.race([prepare(), interrupted]);
|
|
581
650
|
if (!ref.validatedAt)
|
|
582
651
|
throw new Error("Claude session identity was not provider-validated during initialization");
|
|
583
652
|
return ref;
|
|
@@ -612,19 +681,38 @@ export class ClaudeAdapter {
|
|
|
612
681
|
});
|
|
613
682
|
}
|
|
614
683
|
async compact() {
|
|
615
|
-
|
|
684
|
+
if (this.compactionPromise)
|
|
685
|
+
return this.compactionPromise;
|
|
686
|
+
const operation = this.opts.observer
|
|
616
687
|
? this.opts.observer.span("compaction", "Claude context compaction", () => this.compactInternal())
|
|
617
688
|
: this.compactInternal();
|
|
689
|
+
this.compactionPromise = operation;
|
|
690
|
+
try {
|
|
691
|
+
return await operation;
|
|
692
|
+
}
|
|
693
|
+
finally {
|
|
694
|
+
if (this.compactionPromise === operation)
|
|
695
|
+
this.compactionPromise = undefined;
|
|
696
|
+
}
|
|
618
697
|
}
|
|
619
698
|
async compactInternal() {
|
|
620
699
|
this.compactResult = undefined;
|
|
621
700
|
this.manualCompactionInFlight = true;
|
|
701
|
+
const normalMs = Math.min(180_000, Math.max(1, this.opts.compactionTimeoutMs ?? 120_000));
|
|
702
|
+
const deadline = new OperationDeadline("Claude compaction", normalMs, Math.min(180_000, normalMs * 1.5));
|
|
703
|
+
this.manualCompactionDeadline = deadline;
|
|
622
704
|
let result;
|
|
623
705
|
try {
|
|
624
|
-
result = await this.sendTurn("/compact");
|
|
706
|
+
result = await deadline.run(() => this.sendTurn("/compact"), () => this.abort.abort());
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
const failure = new SessionUnavailableError({ runtime: "claude", phase: "turn", dispatchState: "unknown", executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd, diagnostics: String(error) });
|
|
710
|
+
await this.close();
|
|
711
|
+
return { ok: false, error: failure.message, failure: failure.failure };
|
|
625
712
|
}
|
|
626
713
|
finally {
|
|
627
714
|
this.manualCompactionInFlight = false;
|
|
715
|
+
this.manualCompactionDeadline = undefined;
|
|
628
716
|
}
|
|
629
717
|
if (result.failure?.category === "session-unavailable") {
|
|
630
718
|
return { ok: false, error: result.text || result.failure.diagnostics, failure: result.failure };
|
|
@@ -634,15 +722,23 @@ export class ClaudeAdapter {
|
|
|
634
722
|
return { ok: false, error: sanitizeDiagnostics(result.text || "Claude did not emit an explicit compact status") };
|
|
635
723
|
}
|
|
636
724
|
async prepareAutoCompaction(thresholdPercent = this.opts.autoCompactThresholdPercent) {
|
|
725
|
+
return new OperationDeadline("Claude auto-compaction preparation", this.opts.preparationTimeoutMs ?? 120_000).run(() => this.prepareAutoCompactionInternal(thresholdPercent), () => this.abort.abort());
|
|
726
|
+
}
|
|
727
|
+
async prepareAutoCompactionInternal(thresholdPercent = this.opts.autoCompactThresholdPercent) {
|
|
637
728
|
if (thresholdPercent === undefined)
|
|
638
729
|
return;
|
|
639
730
|
const threshold = validThreshold(thresholdPercent);
|
|
640
731
|
if (this.autoCompactionPrepared && this.preparedAutoCompactThreshold === threshold)
|
|
641
732
|
return this.preparedAutoCompactionPolicy;
|
|
642
733
|
this.opts.autoCompactThresholdPercent = threshold;
|
|
643
|
-
await this.query.initializationResult();
|
|
644
|
-
|
|
645
|
-
|
|
734
|
+
await new OperationDeadline("Claude initialization", this.opts.preparationTimeoutMs ?? 120_000).run(() => this.query.initializationResult());
|
|
735
|
+
// The SDK can acknowledge initialization while its CLI is still waiting
|
|
736
|
+
// for first input. Sending getContextUsage then deadlocks the control
|
|
737
|
+
// queue, including a later local command. Establish identity first.
|
|
738
|
+
if (!this._sessionRef?.validatedAt && typeof this.query.supportedCommands === "function")
|
|
739
|
+
await this.prepareSession();
|
|
740
|
+
await new OperationDeadline("Claude settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ autoCompactEnabled: true }));
|
|
741
|
+
const baseline = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
|
|
646
742
|
if (!Number.isFinite(baseline.maxTokens) || baseline.maxTokens <= 0
|
|
647
743
|
|| !Number.isFinite(baseline.autoCompactThreshold) || baseline.autoCompactThreshold === undefined
|
|
648
744
|
|| !baseline.isAutoCompactEnabled) {
|
|
@@ -655,8 +751,8 @@ export class ClaudeAdapter {
|
|
|
655
751
|
if (reserve < 0)
|
|
656
752
|
throw new Error("Claude reported an invalid automatic-compaction reserve");
|
|
657
753
|
const requestedWindow = tokenLimit(baseline.maxTokens, threshold) + reserve;
|
|
658
|
-
await this.query.applyFlagSettings({ autoCompactEnabled: true, autoCompactWindow: requestedWindow });
|
|
659
|
-
const installed = await this.query.getContextUsage();
|
|
754
|
+
await new OperationDeadline("Claude settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.applyFlagSettings({ autoCompactEnabled: true, autoCompactWindow: requestedWindow }));
|
|
755
|
+
const installed = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
|
|
660
756
|
if (!installed.isAutoCompactEnabled || !Number.isFinite(installed.maxTokens) || installed.maxTokens <= 0
|
|
661
757
|
|| !Number.isFinite(installed.autoCompactThreshold) || installed.autoCompactThreshold === undefined) {
|
|
662
758
|
throw new Error("Claude did not expose an enabled native automatic-compaction threshold after configuration");
|
|
@@ -685,7 +781,7 @@ export class ClaudeAdapter {
|
|
|
685
781
|
restoreNativeCompactions(compactions) { this.nativeCompactions.unshift(...compactions); }
|
|
686
782
|
async contextUsage() {
|
|
687
783
|
try {
|
|
688
|
-
const usage = await this.query.getContextUsage();
|
|
784
|
+
const usage = await new OperationDeadline("Claude context usage", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.query.getContextUsage());
|
|
689
785
|
const result = { used: usage.totalTokens, maximum: usage.maxTokens, percentage: usage.percentage, observedAt: new Date().toISOString(), source: "provider-query" };
|
|
690
786
|
this.eventQueue.push({ kind: "context-usage", ...result });
|
|
691
787
|
return result;
|
|
@@ -702,12 +798,13 @@ export class ClaudeAdapter {
|
|
|
702
798
|
return { ok: false, error: "Claude SDK cannot change reasoning/fast controls on an existing transport" };
|
|
703
799
|
if (settings.model === this.opts.model)
|
|
704
800
|
return { ok: true };
|
|
705
|
-
const result = await this.sendTurn(`/model ${settings.model ?? "default"}`);
|
|
801
|
+
const result = await new OperationDeadline("Claude model settings", this.opts.rpcTimeoutMs ?? 60_000).run(() => this.sendTurn(`/model ${settings.model ?? "default"}`), () => this.abort.abort());
|
|
706
802
|
if (result.isError)
|
|
707
803
|
return { ok: false, error: result.text, ...(result.failure ? { failure: result.failure } : {}) };
|
|
708
804
|
this.opts.model = settings.model;
|
|
709
805
|
return { ok: true };
|
|
710
806
|
}
|
|
807
|
+
observeEvents(listener) { return this.eventQueue.observe(listener); }
|
|
711
808
|
events() {
|
|
712
809
|
return this.eventQueue;
|
|
713
810
|
}
|
|
@@ -715,16 +812,18 @@ export class ClaudeAdapter {
|
|
|
715
812
|
if (this.closed)
|
|
716
813
|
return;
|
|
717
814
|
this.closed = true;
|
|
815
|
+
this.questionTrace?.finish("closed");
|
|
718
816
|
this.rejectSessionIdentity(new Error("Claude session closed during initialization"));
|
|
719
817
|
this.abort.abort();
|
|
720
818
|
this.inbox.close();
|
|
721
819
|
try {
|
|
722
|
-
await this.query.interrupt();
|
|
820
|
+
await new OperationDeadline("Claude interrupt", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.query.interrupt());
|
|
723
821
|
}
|
|
724
822
|
catch {
|
|
725
823
|
// interrupt is best-effort — ignore if no turn is active
|
|
726
824
|
}
|
|
727
|
-
|
|
825
|
+
this.query.close?.();
|
|
826
|
+
await new OperationDeadline("Claude shutdown", this.opts.shutdownTimeoutMs ?? 10_000).run(() => this.pumpDone).catch(() => { });
|
|
728
827
|
}
|
|
729
828
|
failSessionIdentity(error) {
|
|
730
829
|
this.rejectSessionIdentity(error);
|
|
@@ -769,10 +868,15 @@ export class ClaudeAdapter {
|
|
|
769
868
|
this.resolveSessionIdentity(this._sessionRef);
|
|
770
869
|
}
|
|
771
870
|
settlePending(result, emit = true) {
|
|
871
|
+
this.questionTrace?.finish(result.isError ? "result-error" : "result");
|
|
772
872
|
const pending = this.pending;
|
|
773
873
|
if (!pending)
|
|
774
874
|
return;
|
|
775
875
|
this.pending = undefined;
|
|
876
|
+
if (pending.idleTimer)
|
|
877
|
+
clearTimeout(pending.idleTimer);
|
|
878
|
+
if (pending.hardTimer)
|
|
879
|
+
clearTimeout(pending.hardTimer);
|
|
776
880
|
if (!result.turnId) {
|
|
777
881
|
result.turnId = pending.turnId;
|
|
778
882
|
result.hostInstruction = pending.instruction;
|
|
@@ -785,6 +889,69 @@ export class ClaudeAdapter {
|
|
|
785
889
|
this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
|
|
786
890
|
pending.resolve(result);
|
|
787
891
|
}
|
|
892
|
+
touchPendingTurn() {
|
|
893
|
+
if (this.pending)
|
|
894
|
+
this.armPendingTurn(this.pending);
|
|
895
|
+
}
|
|
896
|
+
beginProviderQuestionWait() {
|
|
897
|
+
const pending = this.pending;
|
|
898
|
+
if (!pending)
|
|
899
|
+
return;
|
|
900
|
+
pending.providerQuestionWaits = (pending.providerQuestionWaits ?? 0) + 1;
|
|
901
|
+
if (pending.idleTimer)
|
|
902
|
+
clearTimeout(pending.idleTimer);
|
|
903
|
+
pending.idleTimer = undefined;
|
|
904
|
+
if (pending.hardTimer) {
|
|
905
|
+
clearTimeout(pending.hardTimer);
|
|
906
|
+
pending.hardTimer = undefined;
|
|
907
|
+
pending.hardRemainingMs = Math.max(0, (pending.hardRemainingMs ?? this.opts.turnDeadlineMs ?? 3_600_000) - (performance.now() - (pending.hardArmedAt ?? performance.now())));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
endProviderQuestionWait(answered) {
|
|
911
|
+
const pending = this.pending;
|
|
912
|
+
if (!pending)
|
|
913
|
+
return;
|
|
914
|
+
pending.providerQuestionWaits = Math.max(0, (pending.providerQuestionWaits ?? 1) - 1);
|
|
915
|
+
pending.providerQuestionAnswered ||= answered;
|
|
916
|
+
if (pending.providerQuestionWaits === 0)
|
|
917
|
+
this.armPendingTurn(pending);
|
|
918
|
+
}
|
|
919
|
+
armPendingTurn(pending) {
|
|
920
|
+
if (pending.idleTimer)
|
|
921
|
+
clearTimeout(pending.idleTimer);
|
|
922
|
+
if ((pending.providerQuestionWaits ?? 0) > 0)
|
|
923
|
+
return;
|
|
924
|
+
if (!pending.hardTimer) {
|
|
925
|
+
pending.hardRemainingMs ??= this.opts.turnDeadlineMs ?? 3_600_000;
|
|
926
|
+
pending.hardArmedAt = performance.now();
|
|
927
|
+
pending.hardTimer = setTimeout(() => {
|
|
928
|
+
if (this.pending !== pending)
|
|
929
|
+
return;
|
|
930
|
+
this.settlePending(this.streamFailureResult("Claude active turn deadline exceeded; dispatched work requires reconciliation", false));
|
|
931
|
+
void this.close();
|
|
932
|
+
}, pending.hardRemainingMs);
|
|
933
|
+
pending.hardTimer.unref();
|
|
934
|
+
}
|
|
935
|
+
const timeoutMs = this.providerIdleTimeoutMs();
|
|
936
|
+
pending.idleTimer = setTimeout(() => {
|
|
937
|
+
if (this.pending !== pending || this.closed)
|
|
938
|
+
return;
|
|
939
|
+
this.questionTrace?.finish("idle-timeout");
|
|
940
|
+
const context = pending.providerQuestionAnswered ? " after your answer was sent; Claude has not resumed its stream yet" : "";
|
|
941
|
+
const message = `Claude provider was silent for ${Math.round(timeoutMs / 60_000)} minutes${context}; the turn may have been dispatched and will not be retried automatically`;
|
|
942
|
+
const result = this.streamFailureResult(message, false);
|
|
943
|
+
this.eventQueue.push({ kind: "error", message });
|
|
944
|
+
this.settlePending(result);
|
|
945
|
+
// Do not leave a hidden stream running after the host has declared its
|
|
946
|
+
// result uncertain. close() is intentionally not awaited here.
|
|
947
|
+
void this.close();
|
|
948
|
+
}, timeoutMs);
|
|
949
|
+
pending.idleTimer.unref();
|
|
950
|
+
}
|
|
951
|
+
providerIdleTimeoutMs() {
|
|
952
|
+
const configured = this.opts.providerIdleTimeoutMs;
|
|
953
|
+
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PROVIDER_IDLE_TIMEOUT_MS;
|
|
954
|
+
}
|
|
788
955
|
streamFailureResult(message, sessionUnavailable) {
|
|
789
956
|
if (sessionUnavailable) {
|
|
790
957
|
const availability = {
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
25
25
|
private usage?;
|
|
26
26
|
private usageRevision;
|
|
27
27
|
private providerSessionUsage?;
|
|
28
|
+
private turnUsageBaseline?;
|
|
28
29
|
private lastTurnTokens?;
|
|
29
30
|
private stderr;
|
|
30
31
|
private nativeAutoCompactTokenLimit?;
|
|
@@ -34,6 +35,9 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
34
35
|
private nativeCompactions;
|
|
35
36
|
private nativeCompactionSequence;
|
|
36
37
|
private manualCompactionInFlight;
|
|
38
|
+
private compactionPromise?;
|
|
39
|
+
private compactionActive;
|
|
40
|
+
private compactionUncertain;
|
|
37
41
|
private observedToolCalls;
|
|
38
42
|
private activeProviderTurnId?;
|
|
39
43
|
private activeProviderTurnSpanId?;
|
|
@@ -43,11 +47,16 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
43
47
|
buildAppServerArgs(): string[];
|
|
44
48
|
/** Deprecated fixture helper. Runtime execution uses only app-server. */
|
|
45
49
|
buildArgs(instruction: string): string[];
|
|
46
|
-
sendTurn(instruction: string
|
|
50
|
+
sendTurn(instruction: string, policy?: {
|
|
51
|
+
handback?: boolean;
|
|
52
|
+
responseOnly?: boolean;
|
|
53
|
+
logicalActionId?: string;
|
|
54
|
+
}): Promise<TurnResult>;
|
|
47
55
|
private sendTurnInternal;
|
|
48
56
|
compact(): Promise<CompactResult>;
|
|
49
57
|
private compactInternal;
|
|
50
58
|
prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<NativeAutoCompactionPolicy | void>;
|
|
59
|
+
private prepareAutoCompactionInternal;
|
|
51
60
|
requiresAutoCompactionSetupTurn(): boolean;
|
|
52
61
|
autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
|
|
53
62
|
drainNativeCompactions(): NativeCompaction[];
|
|
@@ -61,9 +70,11 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
61
70
|
prepareSession(): Promise<ProviderSessionRefV1>;
|
|
62
71
|
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
63
72
|
validateSession(): Promise<SessionAvailabilityV1>;
|
|
73
|
+
observeEvents(listener: (event: BuilderEvent) => void): () => void;
|
|
64
74
|
events(): AsyncIterable<BuilderEvent>;
|
|
65
75
|
close(): Promise<void>;
|
|
66
76
|
private ensureThread;
|
|
77
|
+
private ensureThreadInternal;
|
|
67
78
|
private threadAttached;
|
|
68
79
|
private ensureConnection;
|
|
69
80
|
private request;
|
|
@@ -71,6 +82,9 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
71
82
|
private handle;
|
|
72
83
|
private observationContext;
|
|
73
84
|
private waitFor;
|
|
85
|
+
private traceLifecycle;
|
|
86
|
+
private touchWaiters;
|
|
87
|
+
private providerIdleTimeoutMs;
|
|
74
88
|
private disconnect;
|
|
75
89
|
private restartForAutoCompaction;
|
|
76
90
|
private buildSkillsAppendix;
|