ai-foreman 1.7.6 → 1.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -1
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +31 -6
- package/dist/adapters/claude.d.ts +61 -6
- package/dist/adapters/claude.js +544 -36
- package/dist/adapters/codex.d.ts +28 -1
- package/dist/adapters/codex.js +398 -27
- package/dist/adapters/recovering.d.ts +13 -2
- package/dist/adapters/recovering.js +32 -4
- package/dist/adapters/sessionFailure.d.ts +18 -0
- package/dist/adapters/sessionFailure.js +51 -0
- package/dist/adapters/types.d.ts +117 -9
- package/dist/agentRun.d.ts +4 -1
- package/dist/agentRun.js +96 -9
- package/dist/branch/currentGuard.d.ts +38 -0
- package/dist/branch/currentGuard.js +71 -0
- package/dist/branch/planner.d.ts +1 -1
- package/dist/branch/planner.js +2 -7
- package/dist/branch/prefix.d.ts +8 -0
- package/dist/branch/prefix.js +26 -0
- package/dist/branch/resume.d.ts +2 -0
- package/dist/branch/resume.js +8 -0
- package/dist/branch/runner.d.ts +14 -2
- package/dist/branch/runner.js +77 -17
- package/dist/buildRuns.d.ts +13 -4
- package/dist/buildRuns.js +85 -19
- package/dist/cli/handoffs.d.ts +2 -0
- package/dist/cli/handoffs.js +74 -0
- package/dist/cli/start.js +1079 -43
- package/dist/cli/tickets.js +199 -3
- package/dist/continuity.d.ts +92 -0
- package/dist/continuity.js +415 -0
- package/dist/foreman.d.ts +5 -1
- package/dist/foreman.js +41 -6
- package/dist/handoffs.d.ts +69 -0
- package/dist/handoffs.js +546 -0
- package/dist/log.d.ts +1 -1
- package/dist/permissions/policy.d.ts +5 -1
- package/dist/permissions/policy.js +48 -1
- package/dist/qaReview.d.ts +6 -6
- package/dist/qaReview.js +24 -14
- package/dist/sessionAvailability.d.ts +15 -0
- package/dist/sessionAvailability.js +57 -0
- package/dist/sessionIdentity.d.ts +33 -0
- package/dist/sessionIdentity.js +132 -0
- package/dist/sessionLifecycle.d.ts +198 -5
- package/dist/sessionLifecycle.js +1373 -20
- package/dist/statusReporter.d.ts +84 -8
- package/dist/statusReporter.js +270 -14
- package/dist/ticketPlanning.js +8 -0
- package/dist/ticketPopulation.js +88 -12
- package/dist/tickets/commands.js +20 -12
- package/dist/tickets/groupReset.d.ts +25 -0
- package/dist/tickets/groupReset.js +279 -0
- package/dist/tickets/groups.d.ts +36 -0
- package/dist/tickets/groups.js +68 -0
- package/dist/tickets/importer.js +27 -3
- package/dist/tickets/recommendations.js +31 -7
- package/dist/tickets/reset.d.ts +1 -0
- package/dist/tickets/reset.js +1 -0
- package/dist/tickets/setupConfig.d.ts +1 -0
- package/dist/tickets/setupConfig.js +10 -2
- package/dist/tickets/stateDb.d.ts +34 -0
- package/dist/tickets/stateDb.js +207 -0
- package/dist/tickets/ticketLoader.js +5 -2
- package/dist/tickets/validate.js +4 -0
- package/dist/workflowDb.d.ts +145 -3
- package/dist/workflowDb.js +571 -10
- package/package.json +32 -4
package/dist/adapters/claude.js
CHANGED
|
@@ -13,6 +13,8 @@ import { AsyncQueue } from "../util/asyncQueue.js";
|
|
|
13
13
|
import { BuilderEventQueue, withActivityPhase } from "../activity.js";
|
|
14
14
|
import { normalizeRuntimeErrorText } from "../runtimeAuth.js";
|
|
15
15
|
import { classifyClaudeSdkFailure, resolveExecutablePath, sanitizeDiagnostics, } from "../runtimeReadiness.js";
|
|
16
|
+
import { createProviderSessionRef, validateProviderSessionScope, canonicalSessionPath } from "../sessionIdentity.js";
|
|
17
|
+
import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFailure.js";
|
|
16
18
|
/**
|
|
17
19
|
* Pure function: build the `options` object passed to `query()`.
|
|
18
20
|
* Extracted so tests can assert on the shape without making a live SDK call.
|
|
@@ -24,7 +26,7 @@ export function buildClaudeQueryOptions(opts) {
|
|
|
24
26
|
pathToClaudeCodeExecutable: opts.runtimeExecutable,
|
|
25
27
|
env: { ...process.env },
|
|
26
28
|
model: opts.model,
|
|
27
|
-
resume: opts.resumeSessionId,
|
|
29
|
+
resume: opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId,
|
|
28
30
|
permissionMode: "acceptEdits",
|
|
29
31
|
effort: opts.effort,
|
|
30
32
|
extraArgs: opts.fast ? { fast: null } : undefined,
|
|
@@ -38,6 +40,38 @@ export function buildClaudeQueryOptions(opts) {
|
|
|
38
40
|
}
|
|
39
41
|
return base;
|
|
40
42
|
}
|
|
43
|
+
export async function probeClaudeSession(ref, input) {
|
|
44
|
+
const now = input.now ?? new Date();
|
|
45
|
+
const local = validateProviderSessionScope(ref, {
|
|
46
|
+
provider: "claude",
|
|
47
|
+
cwd: input.cwd,
|
|
48
|
+
configRoot: input.configRoot ?? input.cwd,
|
|
49
|
+
role: input.role ?? ref.role,
|
|
50
|
+
stream: input.stream ?? ref.stream,
|
|
51
|
+
workspaceIdentity: input.workspaceIdentity,
|
|
52
|
+
ticketId: input.ticketId ?? ref.ticketId,
|
|
53
|
+
deliveryUnitId: input.deliveryUnitId ?? ref.deliveryUnitId,
|
|
54
|
+
}, now);
|
|
55
|
+
if (local.status !== "available")
|
|
56
|
+
return local;
|
|
57
|
+
try {
|
|
58
|
+
const getSessionInfo = input.getSessionInfo ?? (await requireClaudeSDK()).getSessionInfo;
|
|
59
|
+
const info = await getSessionInfo(ref.sessionId, { dir: ref.cwd });
|
|
60
|
+
if (!info)
|
|
61
|
+
return { version: 1, status: "unavailable", checkedAt: now.toISOString(), reason: "not-found", detail: `Claude has no conversation ${ref.sessionId} in ${ref.cwd}`, sessionRef: ref };
|
|
62
|
+
if (info.sessionId !== ref.sessionId)
|
|
63
|
+
return { version: 1, status: "unavailable", checkedAt: now.toISOString(), reason: "not-found", detail: "Claude returned metadata for a different session", sessionRef: ref };
|
|
64
|
+
if (!info.cwd)
|
|
65
|
+
return { version: 1, status: "unknown", checkedAt: now.toISOString(), reason: "probe-failed", detail: "Claude session metadata did not include cwd", sessionRef: ref };
|
|
66
|
+
const observedCwd = canonicalSessionPath(info.cwd);
|
|
67
|
+
if (observedCwd !== canonicalSessionPath(ref.cwd))
|
|
68
|
+
return { version: 1, status: "unavailable", checkedAt: now.toISOString(), reason: "cwd-mismatch", detail: `Claude metadata cwd ${observedCwd} does not match ${ref.cwd}`, observedCwd, sessionRef: ref };
|
|
69
|
+
return { ...local, checkedAt: now.toISOString(), observedCwd, sessionRef: { ...local.sessionRef, validatedAt: now.toISOString() } };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return { version: 1, status: "unknown", checkedAt: now.toISOString(), reason: "probe-failed", detail: sanitizeDiagnostics(error instanceof Error ? error.message : String(error)), sessionRef: ref };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
41
75
|
export function permissionDecisionToClaudeResult(decision, toolUseID) {
|
|
42
76
|
if (decision.behavior === "allow") {
|
|
43
77
|
return {
|
|
@@ -54,6 +88,39 @@ export function permissionDecisionToClaudeResult(decision, toolUseID) {
|
|
|
54
88
|
toolUseID,
|
|
55
89
|
};
|
|
56
90
|
}
|
|
91
|
+
/** Project one cumulative SDK result without inventing or double-counting counters. */
|
|
92
|
+
export function mergeClaudeProviderSessionUsage(prior, rawResult, observedAt = new Date().toISOString()) {
|
|
93
|
+
const rawUsage = rawResult.usage && typeof rawResult.usage === "object" ? rawResult.usage : {};
|
|
94
|
+
const directInputTokens = finiteNumber(rawUsage.input_tokens);
|
|
95
|
+
const cacheCreationInputTokens = finiteNumber(rawUsage.cache_creation_input_tokens);
|
|
96
|
+
const cacheReadInputTokens = finiteNumber(rawUsage.cache_read_input_tokens);
|
|
97
|
+
const inputTokens = [directInputTokens, cacheCreationInputTokens, cacheReadInputTokens].some((value) => value !== undefined)
|
|
98
|
+
? (directInputTokens ?? 0) + (cacheCreationInputTokens ?? 0) + (cacheReadInputTokens ?? 0)
|
|
99
|
+
: undefined;
|
|
100
|
+
const outputTokens = finiteNumber(rawUsage.output_tokens);
|
|
101
|
+
const authoritativeCostUsd = finiteNumber(rawResult.total_cost_usd);
|
|
102
|
+
// SDK result usage and total_cost_usd are cumulative for this query
|
|
103
|
+
// conversation. Preserve the latest authoritative absolute counters;
|
|
104
|
+
// summing successive result messages would double-count prior turns.
|
|
105
|
+
const cumulativeInput = inputTokens ?? prior.inputTokens;
|
|
106
|
+
const cumulativeOutput = outputTokens ?? prior.outputTokens;
|
|
107
|
+
const cumulativeTotal = inputTokens === undefined && outputTokens === undefined
|
|
108
|
+
? prior.totalTokens
|
|
109
|
+
: (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
110
|
+
const cumulativeCost = authoritativeCostUsd ?? prior.authoritativeCostUsd;
|
|
111
|
+
return {
|
|
112
|
+
sample: {
|
|
113
|
+
...(cumulativeInput !== undefined ? { inputTokens: cumulativeInput } : {}),
|
|
114
|
+
...(cumulativeOutput !== undefined ? { outputTokens: cumulativeOutput } : {}),
|
|
115
|
+
...(cumulativeTotal !== undefined ? { totalTokens: cumulativeTotal } : {}),
|
|
116
|
+
...(cumulativeCost !== undefined ? { authoritativeCostUsd: cumulativeCost } : {}),
|
|
117
|
+
observedAt,
|
|
118
|
+
source: "provider",
|
|
119
|
+
},
|
|
120
|
+
inputTokens,
|
|
121
|
+
outputTokens,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
57
124
|
export function claudeApiRetryEvent(message) {
|
|
58
125
|
return {
|
|
59
126
|
kind: "retry",
|
|
@@ -78,13 +145,34 @@ export class ClaudeAdapter {
|
|
|
78
145
|
query;
|
|
79
146
|
abort = new AbortController();
|
|
80
147
|
pumpDone;
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
queryFactory;
|
|
150
|
+
transportGeneration = 0;
|
|
151
|
+
transportObservedSessionId;
|
|
81
152
|
_sessionId;
|
|
153
|
+
_sessionRef;
|
|
82
154
|
stderrChunks = [];
|
|
83
155
|
turnSignals = [];
|
|
84
156
|
structuredError;
|
|
85
157
|
apiErrorStatus;
|
|
86
158
|
compactResult;
|
|
159
|
+
contextPolicy;
|
|
160
|
+
contextSequence = 0;
|
|
161
|
+
compactionSequence = 0;
|
|
162
|
+
activeCompaction;
|
|
163
|
+
manualCompactionPending = false;
|
|
164
|
+
boundaryInterrupt;
|
|
165
|
+
compactionTerminalObserved = false;
|
|
166
|
+
lastContextUsage;
|
|
167
|
+
/** Full provider context capacity, before Rafi narrows Claude's auto-compact window. */
|
|
168
|
+
modelContextWindow;
|
|
169
|
+
/** SDK-owned space between autoCompactWindow and the actual used-token trigger. */
|
|
170
|
+
nativeAutoCompactReserve;
|
|
171
|
+
contextPrepared = false;
|
|
172
|
+
cumulativeUsage = { observedAt: new Date(0).toISOString(), source: "provider" };
|
|
87
173
|
pending;
|
|
174
|
+
terminalResult;
|
|
175
|
+
streamEnded = false;
|
|
88
176
|
closed = false;
|
|
89
177
|
static async create(opts) {
|
|
90
178
|
try {
|
|
@@ -92,25 +180,62 @@ export class ClaudeAdapter {
|
|
|
92
180
|
if (!runtimeExecutable) {
|
|
93
181
|
throw new Error("Claude Code executable not found on PATH. Install your organization-approved Claude Code CLI, then retry.");
|
|
94
182
|
}
|
|
95
|
-
const
|
|
96
|
-
|
|
183
|
+
const sdk = await requireClaudeSDK();
|
|
184
|
+
let validatedOpts = { ...opts, runtimeExecutable };
|
|
185
|
+
if (opts.resumeSessionRef) {
|
|
186
|
+
const availability = await probeClaudeSession(opts.resumeSessionRef, {
|
|
187
|
+
cwd: opts.cwd,
|
|
188
|
+
configRoot: opts.configRoot,
|
|
189
|
+
workspaceIdentity: opts.workspaceIdentity,
|
|
190
|
+
role: opts.sessionRole,
|
|
191
|
+
stream: opts.sessionStream,
|
|
192
|
+
ticketId: opts.ticketId,
|
|
193
|
+
deliveryUnitId: opts.deliveryUnitId,
|
|
194
|
+
getSessionInfo: sdk.getSessionInfo,
|
|
195
|
+
});
|
|
196
|
+
if (availability.status !== "available" || !availability.sessionRef) {
|
|
197
|
+
throw new SessionUnavailableError({
|
|
198
|
+
runtime: "claude", phase: "preflight", dispatchState: "not-sent", executable: runtimeExecutable,
|
|
199
|
+
cwd: opts.cwd, diagnostics: availability.detail ?? `Claude session ${opts.resumeSessionRef.sessionId} is ${availability.status}`,
|
|
200
|
+
availability,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
validatedOpts = { ...validatedOpts, resumeSessionId: availability.sessionRef.sessionId, resumeSessionRef: availability.sessionRef };
|
|
204
|
+
}
|
|
205
|
+
return new ClaudeAdapter(validatedOpts, sdk.query);
|
|
97
206
|
}
|
|
98
207
|
catch (err) {
|
|
208
|
+
if (err instanceof SessionUnavailableError)
|
|
209
|
+
throw err;
|
|
99
210
|
const message = err instanceof Error ? err.message : String(err);
|
|
100
211
|
throw new Error(normalizeRuntimeErrorText("claude", message, null, "adapter startup"), { cause: err });
|
|
101
212
|
}
|
|
102
213
|
}
|
|
103
214
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
-
constructor(opts,
|
|
215
|
+
constructor(opts, queryFactory) {
|
|
105
216
|
this.opts = opts;
|
|
106
|
-
this.
|
|
217
|
+
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
218
|
+
this._sessionRef = opts.resumeSessionRef;
|
|
219
|
+
this.queryFactory = queryFactory;
|
|
220
|
+
this.query = this.openQuery();
|
|
221
|
+
const generation = ++this.transportGeneration;
|
|
222
|
+
this.pumpDone = this.pump(this.query, generation);
|
|
223
|
+
}
|
|
224
|
+
openQuery(nativeAutoCompactWindow) {
|
|
225
|
+
const scopedOpts = this._sessionRef
|
|
226
|
+
? { ...this.opts, resumeSessionId: this._sessionRef.sessionId, resumeSessionRef: this._sessionRef }
|
|
227
|
+
: this.opts;
|
|
228
|
+
return this.queryFactory({
|
|
107
229
|
prompt: this.inbox,
|
|
108
230
|
options: {
|
|
109
|
-
...buildClaudeQueryOptions(
|
|
231
|
+
...buildClaudeQueryOptions(scopedOpts),
|
|
232
|
+
...(nativeAutoCompactWindow === undefined ? {} : {
|
|
233
|
+
settings: { autoCompactEnabled: true, autoCompactWindow: nativeAutoCompactWindow },
|
|
234
|
+
}),
|
|
110
235
|
abortController: this.abort,
|
|
111
236
|
stderr: (data) => this.captureStderr(data),
|
|
112
237
|
canUseTool: async (toolName, input, requestOptions = {}) => {
|
|
113
|
-
const decision = await opts.permission({
|
|
238
|
+
const decision = await this.opts.permission({
|
|
114
239
|
toolName,
|
|
115
240
|
input,
|
|
116
241
|
signal: requestOptions.signal,
|
|
@@ -125,16 +250,22 @@ export class ClaudeAdapter {
|
|
|
125
250
|
},
|
|
126
251
|
},
|
|
127
252
|
});
|
|
128
|
-
this.pumpDone = this.pump();
|
|
129
253
|
}
|
|
130
254
|
/** Background loop: consume the SDK message stream until it ends. */
|
|
131
|
-
async pump() {
|
|
255
|
+
async pump(query, generation) {
|
|
132
256
|
try {
|
|
133
|
-
for await (const msg of
|
|
134
|
-
this.
|
|
257
|
+
for await (const msg of query) {
|
|
258
|
+
if (generation !== this.transportGeneration)
|
|
259
|
+
return;
|
|
260
|
+
// Keep provider messages ordered through post-compaction measurement. In
|
|
261
|
+
// particular, a result message must not overtake the fresh context sample
|
|
262
|
+
// produced for the preceding compact_result/compact_boundary message.
|
|
263
|
+
await this.handle(msg);
|
|
135
264
|
}
|
|
136
265
|
}
|
|
137
266
|
catch (err) {
|
|
267
|
+
if (generation !== this.transportGeneration)
|
|
268
|
+
return;
|
|
138
269
|
// Suppress the AbortError that fires when close() aborts the stream.
|
|
139
270
|
const isShutdownAbort = this.closed &&
|
|
140
271
|
(err instanceof Error &&
|
|
@@ -143,17 +274,31 @@ export class ClaudeAdapter {
|
|
|
143
274
|
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
144
275
|
const message = normalizeRuntimeErrorText("claude", rawMessage, null, "builder stream");
|
|
145
276
|
this.eventQueue.push({ kind: "error", message });
|
|
146
|
-
this.
|
|
147
|
-
this.
|
|
277
|
+
const result = this.streamFailureResult(message, isMissingClaudeSession(rawMessage));
|
|
278
|
+
this.terminalResult = result;
|
|
279
|
+
this.settlePending(result);
|
|
148
280
|
}
|
|
149
281
|
}
|
|
150
282
|
finally {
|
|
283
|
+
if (generation !== this.transportGeneration)
|
|
284
|
+
return;
|
|
285
|
+
this.streamEnded = true;
|
|
286
|
+
if (!this.closed && this.pending) {
|
|
287
|
+
const result = this.streamFailureResult("Claude stream ended without a result", Boolean(this.opts.resumeSessionRef ?? this.opts.resumeSessionId));
|
|
288
|
+
this.terminalResult = result;
|
|
289
|
+
this.eventQueue.push({ kind: "error", message: result.text });
|
|
290
|
+
this.settlePending(result);
|
|
291
|
+
}
|
|
292
|
+
if (!this.closed && !this.terminalResult)
|
|
293
|
+
this.terminalResult = this.streamFailureResult("Claude stream ended without a result", Boolean(this.opts.resumeSessionRef ?? this.opts.resumeSessionId));
|
|
151
294
|
this.eventQueue.close();
|
|
152
295
|
}
|
|
153
296
|
}
|
|
154
|
-
handle(msg) {
|
|
297
|
+
async handle(msg) {
|
|
155
298
|
if ("session_id" in msg && typeof msg.session_id === "string") {
|
|
299
|
+
this.transportObservedSessionId = msg.session_id;
|
|
156
300
|
this._sessionId = msg.session_id;
|
|
301
|
+
this.observeSession(msg.session_id);
|
|
157
302
|
}
|
|
158
303
|
if (msg.type === "assistant") {
|
|
159
304
|
if (msg.error) {
|
|
@@ -180,14 +325,43 @@ export class ClaudeAdapter {
|
|
|
180
325
|
this.turnSignals.push(...msg.output.map((line) => `auth: ${line}`));
|
|
181
326
|
}
|
|
182
327
|
else if (msg.type === "system" && msg.subtype === "status") {
|
|
183
|
-
if (msg.status === "compacting")
|
|
328
|
+
if (msg.status === "compacting") {
|
|
329
|
+
this.compactionTerminalObserved = false;
|
|
184
330
|
this.eventQueue.push({ kind: "session-transition", transition: "compacting" });
|
|
331
|
+
const providerSequence = ++this.compactionSequence;
|
|
332
|
+
const origin = this.manualCompactionPending ? "rafi-manual" : "provider-auto";
|
|
333
|
+
this.activeCompaction = { providerEventId: msg.uuid, providerSequence, origin };
|
|
334
|
+
this.eventQueue.push({
|
|
335
|
+
kind: "context-compaction", phase: "started", origin, providerEventId: msg.uuid,
|
|
336
|
+
providerSequence, provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef,
|
|
337
|
+
observedAt: new Date().toISOString(),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
185
340
|
if (msg.compact_result === "success") {
|
|
186
341
|
this.compactResult = { ok: true };
|
|
187
342
|
this.eventQueue.push({ kind: "session-transition", transition: "compacted" });
|
|
343
|
+
if (!this.compactionTerminalObserved)
|
|
344
|
+
await this.completeCompaction(true);
|
|
188
345
|
}
|
|
189
346
|
else if (msg.compact_result === "failed") {
|
|
190
347
|
this.compactResult = { ok: false, error: sanitizeDiagnostics(msg.compact_error ?? "Claude native compaction failed") };
|
|
348
|
+
if (!this.compactionTerminalObserved)
|
|
349
|
+
await this.completeCompaction(false, this.compactResult.error);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (msg.type === "system" && msg.subtype === "compact_boundary") {
|
|
353
|
+
if (!this.compactionTerminalObserved) {
|
|
354
|
+
// The boundary is itself a successful native compact signal. Query the
|
|
355
|
+
// SDK immediately and use its post_tokens only as a fallback if that
|
|
356
|
+
// authoritative query is unavailable.
|
|
357
|
+
if (!this.activeCompaction) {
|
|
358
|
+
this.activeCompaction = {
|
|
359
|
+
providerEventId: msg.uuid,
|
|
360
|
+
providerSequence: ++this.compactionSequence,
|
|
361
|
+
origin: msg.compact_metadata.trigger === "manual" ? "rafi-manual" : "provider-auto",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
await this.completeCompaction(true, undefined, msg.compact_metadata.post_tokens);
|
|
191
365
|
}
|
|
192
366
|
}
|
|
193
367
|
else if (msg.type === "system" && msg.subtype === "api_retry") {
|
|
@@ -221,24 +395,44 @@ export class ClaudeAdapter {
|
|
|
221
395
|
...this.turnSignals,
|
|
222
396
|
...this.stderrChunks,
|
|
223
397
|
].filter(Boolean).join("\n"));
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
398
|
+
const missingResumedSession = msg.is_error
|
|
399
|
+
&& Boolean(this.opts.resumeSessionRef ?? this.opts.resumeSessionId)
|
|
400
|
+
&& isMissingClaudeSession(rawDiagnostics);
|
|
401
|
+
const failure = msg.is_error ? missingResumedSession
|
|
402
|
+
? new SessionUnavailableError({
|
|
403
|
+
runtime: "claude", phase: "turn", dispatchState: "unknown",
|
|
404
|
+
executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd,
|
|
405
|
+
diagnostics: rawDiagnostics,
|
|
406
|
+
availability: {
|
|
407
|
+
version: 1, status: "unavailable", checkedAt: new Date().toISOString(), reason: "not-found",
|
|
408
|
+
detail: rawDiagnostics, ...(this._sessionRef ? { sessionRef: this._sessionRef } : {}),
|
|
409
|
+
},
|
|
410
|
+
}).failure
|
|
411
|
+
: {
|
|
412
|
+
runtime: "claude",
|
|
413
|
+
phase: this.opts.runtimePhase ?? "builder",
|
|
414
|
+
category: classifyClaudeSdkFailure(this.structuredError, this.apiErrorStatus, rawDiagnostics),
|
|
415
|
+
executable: this.opts.runtimeExecutable ?? "claude",
|
|
416
|
+
cwd: this.opts.cwd,
|
|
417
|
+
diagnostics: rawDiagnostics,
|
|
418
|
+
} : undefined;
|
|
232
419
|
const result = {
|
|
233
420
|
text: failure ? formatClaudeFailure(failure) : text,
|
|
234
421
|
isError: msg.is_error,
|
|
235
422
|
numTurns: msg.num_turns,
|
|
236
423
|
costUsd: msg.total_cost_usd,
|
|
424
|
+
costAuthoritative: Number.isFinite(msg.total_cost_usd),
|
|
237
425
|
failure,
|
|
426
|
+
...(this.boundaryInterrupt ? { interrupted: { reason: "compaction-boundary", ...(this.boundaryInterrupt.providerEventId ? { providerEventId: this.boundaryInterrupt.providerEventId } : {}) } } : {}),
|
|
238
427
|
};
|
|
428
|
+
this.boundaryInterrupt = undefined;
|
|
429
|
+
const rawResult = msg;
|
|
430
|
+
const mergedUsage = mergeClaudeProviderSessionUsage(this.cumulativeUsage, rawResult);
|
|
431
|
+
result.inputTokens = mergedUsage.inputTokens;
|
|
432
|
+
result.outputTokens = mergedUsage.outputTokens;
|
|
433
|
+
this.cumulativeUsage = mergedUsage.sample;
|
|
239
434
|
this.eventQueue.push({ kind: "turn-complete", result });
|
|
240
|
-
this.
|
|
241
|
-
this.pending = undefined;
|
|
435
|
+
this.settlePending(result, false);
|
|
242
436
|
this.turnSignals = [];
|
|
243
437
|
this.structuredError = undefined;
|
|
244
438
|
this.apiErrorStatus = undefined;
|
|
@@ -246,11 +440,17 @@ export class ClaudeAdapter {
|
|
|
246
440
|
}
|
|
247
441
|
}
|
|
248
442
|
sendTurn(text) {
|
|
249
|
-
return withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`, () =>
|
|
443
|
+
return withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`, async () => {
|
|
444
|
+
if (this.opts.contextManagementPolicy && !this.contextPrepared)
|
|
445
|
+
await this.prepareContextManagement(this.opts.contextManagementPolicy);
|
|
446
|
+
return this.sendTurnInternal(text);
|
|
447
|
+
});
|
|
250
448
|
}
|
|
251
449
|
sendTurnInternal(text) {
|
|
252
450
|
if (this.closed)
|
|
253
451
|
return Promise.reject(new Error("builder is closed"));
|
|
452
|
+
if (this.terminalResult || this.streamEnded)
|
|
453
|
+
return Promise.resolve(this.terminalResult ?? this.streamFailureResult("Claude stream is no longer available", Boolean(this.opts.resumeSessionRef ?? this.opts.resumeSessionId)));
|
|
254
454
|
if (this.pending) {
|
|
255
455
|
return Promise.reject(new Error("a turn is already in progress"));
|
|
256
456
|
}
|
|
@@ -275,17 +475,46 @@ export class ClaudeAdapter {
|
|
|
275
475
|
sessionId() {
|
|
276
476
|
return this._sessionId;
|
|
277
477
|
}
|
|
478
|
+
sessionRef() { return this._sessionRef; }
|
|
479
|
+
adoptSessionRef(ref) {
|
|
480
|
+
if (ref.provider !== "claude" || ref.sessionId !== this._sessionId)
|
|
481
|
+
throw new Error("cannot adopt a session reference for a different Claude conversation");
|
|
482
|
+
this._sessionRef = ref;
|
|
483
|
+
}
|
|
484
|
+
async validateSession() {
|
|
485
|
+
if (!this._sessionRef)
|
|
486
|
+
return { version: 1, status: "unknown", checkedAt: new Date().toISOString(), reason: "legacy-unscoped", detail: "Claude adapter was constructed from an unscoped raw session ID" };
|
|
487
|
+
return probeClaudeSession(this._sessionRef, {
|
|
488
|
+
cwd: this.opts.cwd,
|
|
489
|
+
configRoot: this.opts.configRoot,
|
|
490
|
+
workspaceIdentity: this.opts.workspaceIdentity,
|
|
491
|
+
role: this.opts.sessionRole,
|
|
492
|
+
stream: this.opts.sessionStream,
|
|
493
|
+
ticketId: this.opts.ticketId,
|
|
494
|
+
deliveryUnitId: this.opts.deliveryUnitId,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
278
497
|
async compact() {
|
|
279
498
|
this.compactResult = undefined;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
499
|
+
this.manualCompactionPending = true;
|
|
500
|
+
try {
|
|
501
|
+
const result = await this.sendTurn("/compact");
|
|
502
|
+
if (result.failure?.category === "session-unavailable") {
|
|
503
|
+
return { ok: false, error: result.text || result.failure.diagnostics, failure: result.failure };
|
|
504
|
+
}
|
|
505
|
+
if (this.compactResult)
|
|
506
|
+
return this.compactResult;
|
|
507
|
+
return { ok: false, error: sanitizeDiagnostics(result.text || "Claude did not emit an explicit compact status") };
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
this.manualCompactionPending = false;
|
|
511
|
+
}
|
|
284
512
|
}
|
|
285
513
|
async contextUsage() {
|
|
286
514
|
try {
|
|
287
515
|
const usage = await this.query.getContextUsage();
|
|
288
|
-
const result =
|
|
516
|
+
const result = this.makeContextUsage(usage);
|
|
517
|
+
this.lastContextUsage = result;
|
|
289
518
|
this.eventQueue.push({ kind: "context-usage", ...result });
|
|
290
519
|
return result;
|
|
291
520
|
}
|
|
@@ -293,16 +522,48 @@ export class ClaudeAdapter {
|
|
|
293
522
|
return undefined;
|
|
294
523
|
}
|
|
295
524
|
}
|
|
525
|
+
async prepareContextManagement(policy) {
|
|
526
|
+
validateContextPolicy(policy);
|
|
527
|
+
if (this.contextPrepared)
|
|
528
|
+
return this.configureContextManagement(policy, false);
|
|
529
|
+
await this.query.initializationResult();
|
|
530
|
+
const prepared = await this.configureContextManagement(policy, true);
|
|
531
|
+
this.contextPrepared = true;
|
|
532
|
+
return prepared;
|
|
533
|
+
}
|
|
534
|
+
async updateContextManagement(policy) {
|
|
535
|
+
validateContextPolicy(policy);
|
|
536
|
+
return this.configureContextManagement(policy, false);
|
|
537
|
+
}
|
|
538
|
+
async interruptTurnAtCompactionBoundary(providerEventId) {
|
|
539
|
+
if (!this.pending)
|
|
540
|
+
return { ok: false, error: "no active Claude turn exists at the compaction boundary", providerEventId };
|
|
541
|
+
try {
|
|
542
|
+
this.boundaryInterrupt = { providerEventId };
|
|
543
|
+
await this.query.interrupt();
|
|
544
|
+
return { ok: true, providerEventId };
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
this.boundaryInterrupt = undefined;
|
|
548
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error), providerEventId };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async sessionUsage() {
|
|
552
|
+
return this.cumulativeUsage.observedAt === new Date(0).toISOString() ? undefined : { ...this.cumulativeUsage };
|
|
553
|
+
}
|
|
296
554
|
async switchSettings(settings) {
|
|
297
555
|
if (settings.effort !== this.opts.effort || settings.fast !== this.opts.fast)
|
|
298
556
|
return { ok: false, error: "Claude SDK cannot change reasoning/fast controls on an existing transport" };
|
|
299
557
|
if (settings.model === this.opts.model)
|
|
300
558
|
return { ok: true };
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
559
|
+
try {
|
|
560
|
+
await this.query.setModel(settings.model);
|
|
561
|
+
this.opts.model = settings.model;
|
|
562
|
+
return { ok: true };
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
566
|
+
}
|
|
306
567
|
}
|
|
307
568
|
events() {
|
|
308
569
|
return this.eventQueue;
|
|
@@ -321,6 +582,253 @@ export class ClaudeAdapter {
|
|
|
321
582
|
this.abort.abort();
|
|
322
583
|
await this.pumpDone.catch(() => { });
|
|
323
584
|
}
|
|
585
|
+
observeSession(sessionId) {
|
|
586
|
+
if (this._sessionRef?.sessionId === sessionId)
|
|
587
|
+
return;
|
|
588
|
+
this._sessionRef = createProviderSessionRef({
|
|
589
|
+
provider: "claude", sessionId, cwd: this.opts.cwd, configRoot: this.opts.configRoot ?? this.opts.cwd,
|
|
590
|
+
role: this.opts.sessionRole, stream: this.opts.sessionStream, generation: this.opts.sessionGeneration,
|
|
591
|
+
workspaceIdentity: this.opts.workspaceIdentity, ticketId: this.opts.ticketId, deliveryUnitId: this.opts.deliveryUnitId,
|
|
592
|
+
source: "observed",
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
makeContextUsage(usage) {
|
|
596
|
+
const maximum = this.modelContextWindow
|
|
597
|
+
?? (Number.isFinite(usage.maxTokens) && usage.maxTokens > 0 ? usage.maxTokens : this.lastContextUsage?.maximum);
|
|
598
|
+
// Once Rafi installs a smaller autoCompactWindow, the SDK's percentage and
|
|
599
|
+
// maxTokens describe that artificial window. Enforcement and display stay
|
|
600
|
+
// relative to the full model capacity discovered before reconfiguration.
|
|
601
|
+
const percentage = maximum ? usage.totalTokens / maximum * 100
|
|
602
|
+
: Number.isFinite(usage.percentage) ? usage.percentage : undefined;
|
|
603
|
+
return {
|
|
604
|
+
used: usage.totalTokens,
|
|
605
|
+
...(maximum ? { maximum } : {}),
|
|
606
|
+
...(percentage !== undefined ? { percentage } : {}),
|
|
607
|
+
observedAt: new Date().toISOString(),
|
|
608
|
+
source: "provider-query",
|
|
609
|
+
sequence: ++this.contextSequence,
|
|
610
|
+
...(this._sessionId ? { sessionId: this._sessionId } : {}),
|
|
611
|
+
...(this.opts.model ?? usage.model ? { model: this.opts.model ?? usage.model } : {}),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
async configureContextManagement(policy, initial) {
|
|
615
|
+
const modelChanged = this.contextPolicy !== undefined && this.contextPolicy.model !== policy.model;
|
|
616
|
+
if (modelChanged) {
|
|
617
|
+
this.modelContextWindow = undefined;
|
|
618
|
+
this.nativeAutoCompactReserve = undefined;
|
|
619
|
+
await this.restartTransport();
|
|
620
|
+
}
|
|
621
|
+
this.contextPolicy = { ...policy };
|
|
622
|
+
this.compactionSequence = Math.max(this.compactionSequence, policy.providerSequenceStart ?? 0);
|
|
623
|
+
this.contextSequence = Math.max(this.contextSequence, policy.providerSequenceStart ?? 0);
|
|
624
|
+
if (initial || modelChanged || !this.modelContextWindow || !this.nativeAutoCompactReserve) {
|
|
625
|
+
// Enable first so getContextUsage exposes the provider's real trigger.
|
|
626
|
+
await this.query.applyFlagSettings({ autoCompactEnabled: true });
|
|
627
|
+
const baseline = await this.query.getContextUsage();
|
|
628
|
+
const baselineWindow = positiveNumber(baseline.maxTokens);
|
|
629
|
+
const baselineTrigger = positiveNumber(baseline.autoCompactThreshold);
|
|
630
|
+
if (!baselineWindow || !baselineTrigger || baselineTrigger >= baselineWindow) {
|
|
631
|
+
throw new Error("Claude did not expose a usable maxTokens/autoCompactThreshold pair after enabling native auto-compaction");
|
|
632
|
+
}
|
|
633
|
+
this.modelContextWindow = policy.knownModelContextWindow ?? baselineWindow;
|
|
634
|
+
this.nativeAutoCompactReserve = baselineWindow - baselineTrigger;
|
|
635
|
+
}
|
|
636
|
+
const maximum = this.modelContextWindow;
|
|
637
|
+
const reserve = this.nativeAutoCompactReserve;
|
|
638
|
+
const configuredTokenLimit = tokenCeiling(maximum, policy.configuredThresholdPercent);
|
|
639
|
+
// Claude's setting is a total window, not the used-token trigger. Preserve
|
|
640
|
+
// the SDK-reported output/compaction reserve so its resulting trigger lands
|
|
641
|
+
// on (or earlier than) Rafi's configured absolute ceiling.
|
|
642
|
+
const requestedNativeWindow = configuredTokenLimit + reserve;
|
|
643
|
+
await this.query.applyFlagSettings({ autoCompactEnabled: true, autoCompactWindow: requestedNativeWindow });
|
|
644
|
+
let verified = await this.query.getContextUsage();
|
|
645
|
+
// The SDK applies flag-layer changes through an internal settings watcher;
|
|
646
|
+
// give that watcher a short bounded interval before declaring the native
|
|
647
|
+
// control unavailable.
|
|
648
|
+
for (let attempt = 0; attempt < 4
|
|
649
|
+
&& positiveNumber(verified.autoCompactThreshold) !== undefined
|
|
650
|
+
&& positiveNumber(verified.autoCompactThreshold) > configuredTokenLimit; attempt += 1) {
|
|
651
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
652
|
+
verified = await this.query.getContextUsage();
|
|
653
|
+
}
|
|
654
|
+
if (initial && positiveNumber(verified.autoCompactThreshold) > configuredTokenLimit) {
|
|
655
|
+
// Some SDK builds commit flag-layer changes to the active query state at
|
|
656
|
+
// the next streaming-input boundary. Use the same approved, tool-free
|
|
657
|
+
// setup exchange that the handoff monitor already recognizes, then
|
|
658
|
+
// verify again before any real role work is dispatched.
|
|
659
|
+
const setup = await this.sendTurnInternal("Rafi context setup only. Do not call tools or change files. Reply with exactly CONTEXT_READY.");
|
|
660
|
+
if (setup.isError || setup.text.trim() !== "CONTEXT_READY") {
|
|
661
|
+
throw new Error(`Claude context setup did not complete safely: ${setup.text.slice(0, 240)}`);
|
|
662
|
+
}
|
|
663
|
+
verified = await this.query.getContextUsage();
|
|
664
|
+
}
|
|
665
|
+
if (positiveNumber(verified.autoCompactThreshold) > configuredTokenLimit) {
|
|
666
|
+
// Current SDK releases accept applyFlagSettings but some noninteractive
|
|
667
|
+
// transports do not project autoCompactWindow into their already-created
|
|
668
|
+
// query state. Restart only that transport with equivalent inline flag
|
|
669
|
+
// settings and resume the exact scoped conversation.
|
|
670
|
+
await this.restartTransport(requestedNativeWindow);
|
|
671
|
+
verified = await this.query.getContextUsage();
|
|
672
|
+
}
|
|
673
|
+
if (!verified.isAutoCompactEnabled)
|
|
674
|
+
throw new Error("Claude disabled native auto-compaction during context configuration");
|
|
675
|
+
const installed = positiveNumber(verified.autoCompactThreshold);
|
|
676
|
+
if (!installed)
|
|
677
|
+
throw new Error("Claude did not report the installed native auto-compaction trigger");
|
|
678
|
+
if (installed > configuredTokenLimit) {
|
|
679
|
+
throw new Error(`Claude installed native compaction at ${installed} tokens, later than the configured ${configuredTokenLimit}-token ceiling `
|
|
680
|
+
+ `(requested SDK window ${requestedNativeWindow}; this ceiling may be below the provider's supported minimum)`);
|
|
681
|
+
}
|
|
682
|
+
const sample = this.makeContextUsage(verified);
|
|
683
|
+
this.lastContextUsage = sample;
|
|
684
|
+
this.eventQueue.push({ kind: "context-usage", ...sample });
|
|
685
|
+
return {
|
|
686
|
+
modelContextWindow: maximum,
|
|
687
|
+
configuredTokenLimit,
|
|
688
|
+
installedNativeTokenLimit: installed,
|
|
689
|
+
installedNativePercent: installed / maximum * 100,
|
|
690
|
+
sample,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
async restartTransport(nativeAutoCompactWindow) {
|
|
694
|
+
if (this.pending)
|
|
695
|
+
throw new Error("Claude context transport cannot restart while a provider turn is active");
|
|
696
|
+
const expectedSessionId = this._sessionId;
|
|
697
|
+
const expectedSessionRef = this._sessionRef;
|
|
698
|
+
if (!expectedSessionId || !expectedSessionRef) {
|
|
699
|
+
throw new Error("Claude context transport restart requires an established location-scoped session");
|
|
700
|
+
}
|
|
701
|
+
const priorQuery = this.query;
|
|
702
|
+
const priorInbox = this.inbox;
|
|
703
|
+
const priorPump = this.pumpDone;
|
|
704
|
+
const generation = ++this.transportGeneration;
|
|
705
|
+
priorInbox.close();
|
|
706
|
+
priorQuery.close();
|
|
707
|
+
await priorPump.catch(() => { });
|
|
708
|
+
this.inbox = new AsyncQueue();
|
|
709
|
+
this.abort = new AbortController();
|
|
710
|
+
this.streamEnded = false;
|
|
711
|
+
this.terminalResult = undefined;
|
|
712
|
+
this.transportObservedSessionId = undefined;
|
|
713
|
+
this.query = this.openQuery(nativeAutoCompactWindow);
|
|
714
|
+
this.pumpDone = this.pump(this.query, generation);
|
|
715
|
+
await this.query.initializationResult();
|
|
716
|
+
for (let attempt = 0; attempt < 10 && !this.transportObservedSessionId; attempt += 1) {
|
|
717
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
718
|
+
}
|
|
719
|
+
if (this.transportObservedSessionId && this.transportObservedSessionId !== expectedSessionId) {
|
|
720
|
+
this.query.close();
|
|
721
|
+
throw new Error(`Claude context transport did not resume the exact scoped session ${expectedSessionId} `
|
|
722
|
+
+ `(observed ${this.transportObservedSessionId})`);
|
|
723
|
+
}
|
|
724
|
+
if (!this.transportObservedSessionId) {
|
|
725
|
+
// Claude does not always repeat the session-start frame until the next
|
|
726
|
+
// user message. Validate the exact persisted, location-scoped target;
|
|
727
|
+
// the immediately following context query then proves this transport is
|
|
728
|
+
// usable before any real role action is dispatched.
|
|
729
|
+
const availability = await probeClaudeSession(expectedSessionRef, {
|
|
730
|
+
cwd: this.opts.cwd,
|
|
731
|
+
configRoot: this.opts.configRoot,
|
|
732
|
+
workspaceIdentity: this.opts.workspaceIdentity,
|
|
733
|
+
role: this.opts.sessionRole,
|
|
734
|
+
stream: this.opts.sessionStream,
|
|
735
|
+
ticketId: this.opts.ticketId,
|
|
736
|
+
deliveryUnitId: this.opts.deliveryUnitId,
|
|
737
|
+
});
|
|
738
|
+
if (availability.status !== "available") {
|
|
739
|
+
this.query.close();
|
|
740
|
+
throw new Error(`Claude context transport could not validate scoped session ${expectedSessionId}: `
|
|
741
|
+
+ `${availability.reason ?? availability.status} (${availability.detail ?? "no provider detail"})`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async completeCompaction(ok, reason, fallbackPostTokens) {
|
|
746
|
+
this.compactionTerminalObserved = true;
|
|
747
|
+
const active = this.activeCompaction;
|
|
748
|
+
const providerSequence = active?.providerSequence ?? ++this.compactionSequence;
|
|
749
|
+
const providerEventId = active?.providerEventId ?? `claude:${this._sessionId ?? "pending"}:${providerSequence}`;
|
|
750
|
+
const origin = active?.origin ?? (this.manualCompactionPending ? "rafi-manual" : "provider-auto");
|
|
751
|
+
this.activeCompaction = undefined;
|
|
752
|
+
if (!ok) {
|
|
753
|
+
this.eventQueue.push({
|
|
754
|
+
kind: "context-compaction", phase: "failed", origin, providerEventId, providerSequence,
|
|
755
|
+
provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef,
|
|
756
|
+
observedAt: new Date().toISOString(), reason,
|
|
757
|
+
});
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
let postCompactSample;
|
|
761
|
+
try {
|
|
762
|
+
const usage = await this.query.getContextUsage();
|
|
763
|
+
postCompactSample = { ...this.makeContextUsage(usage), source: "post-compact" };
|
|
764
|
+
this.lastContextUsage = postCompactSample;
|
|
765
|
+
this.eventQueue.push({ kind: "context-usage", ...postCompactSample });
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
if (fallbackPostTokens !== undefined && this.lastContextUsage?.maximum) {
|
|
769
|
+
postCompactSample = {
|
|
770
|
+
...this.makeContextUsage({
|
|
771
|
+
totalTokens: fallbackPostTokens,
|
|
772
|
+
maxTokens: this.lastContextUsage.maximum,
|
|
773
|
+
model: this.opts.model,
|
|
774
|
+
}),
|
|
775
|
+
source: "post-compact",
|
|
776
|
+
};
|
|
777
|
+
this.lastContextUsage = postCompactSample;
|
|
778
|
+
this.eventQueue.push({ kind: "context-usage", ...postCompactSample });
|
|
779
|
+
}
|
|
780
|
+
// Otherwise the shared coordinator records this success as unverified
|
|
781
|
+
// and hands off after its own bounded query attempts.
|
|
782
|
+
}
|
|
783
|
+
this.eventQueue.push({
|
|
784
|
+
kind: "context-compaction", phase: "succeeded", origin, providerEventId, providerSequence,
|
|
785
|
+
provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef,
|
|
786
|
+
observedAt: new Date().toISOString(), ...(postCompactSample ? { postCompactSample } : {}),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
settlePending(result, emit = true) {
|
|
790
|
+
const pending = this.pending;
|
|
791
|
+
if (!pending)
|
|
792
|
+
return;
|
|
793
|
+
this.pending = undefined;
|
|
794
|
+
if (emit)
|
|
795
|
+
this.eventQueue.push({ kind: "turn-complete", result });
|
|
796
|
+
pending.resolve(result);
|
|
797
|
+
}
|
|
798
|
+
streamFailureResult(message, sessionUnavailable) {
|
|
799
|
+
if (sessionUnavailable) {
|
|
800
|
+
const availability = {
|
|
801
|
+
version: 1, status: "unavailable", checkedAt: new Date().toISOString(), reason: "not-found",
|
|
802
|
+
detail: message, ...(this._sessionRef ? { sessionRef: this._sessionRef } : {}),
|
|
803
|
+
};
|
|
804
|
+
return sessionUnavailableResult(new SessionUnavailableError({
|
|
805
|
+
runtime: "claude", phase: "turn", dispatchState: "unknown", executable: this.opts.runtimeExecutable ?? "claude",
|
|
806
|
+
cwd: this.opts.cwd, diagnostics: message, availability,
|
|
807
|
+
}));
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
text: message, isError: true, numTurns: 0, costUsd: 0, costAuthoritative: false,
|
|
811
|
+
failure: { runtime: "claude", phase: this.opts.runtimePhase ?? "builder", category: "agent-stream", executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd, diagnostics: message, dispatchState: "unknown" },
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function finiteNumber(value) { const parsed = Number(value); return value !== null && value !== undefined && value !== "" && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; }
|
|
816
|
+
function positiveNumber(value) {
|
|
817
|
+
const parsed = Number(value);
|
|
818
|
+
return value !== null && value !== undefined && value !== "" && Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
819
|
+
}
|
|
820
|
+
function tokenCeiling(maximum, percentage) {
|
|
821
|
+
return Math.max(1, Math.floor(maximum * percentage / 100));
|
|
822
|
+
}
|
|
823
|
+
function validateContextPolicy(policy) {
|
|
824
|
+
if (!Number.isInteger(policy.configuredThresholdPercent) || policy.configuredThresholdPercent < 1 || policy.configuredThresholdPercent > 99) {
|
|
825
|
+
throw new Error("configured context threshold must be an integer from 1 to 99");
|
|
826
|
+
}
|
|
827
|
+
if (!Number.isSafeInteger(policy.compactMaximum) || policy.compactMaximum < 1)
|
|
828
|
+
throw new Error("compact maximum must be a positive safe integer");
|
|
829
|
+
}
|
|
830
|
+
function isMissingClaudeSession(message) {
|
|
831
|
+
return /no conversation found with session id|session .* not found|conversation .* not found/i.test(message);
|
|
324
832
|
}
|
|
325
833
|
function activityPhase(phase) {
|
|
326
834
|
if (phase === "planning")
|