ai-foreman 1.7.12 → 1.7.14
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 +47 -2
- package/dist/activity.d.ts +5 -1
- package/dist/activity.js +1 -1
- package/dist/adapters/claude.d.ts +12 -0
- package/dist/adapters/claude.js +255 -23
- package/dist/adapters/codex.d.ts +7 -0
- package/dist/adapters/codex.js +149 -17
- package/dist/adapters/recovering.d.ts +4 -0
- package/dist/adapters/recovering.js +17 -1
- package/dist/adapters/types.d.ts +69 -1
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +28 -4
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +9 -0
- package/dist/branch/finalization.d.ts +22 -0
- package/dist/branch/finalization.js +120 -0
- package/dist/branch/git.js +7 -2
- package/dist/branch/resume.js +11 -1
- package/dist/branch/runner.d.ts +12 -3
- package/dist/branch/runner.js +381 -125
- package/dist/buildRuns.d.ts +7 -2
- package/dist/buildRuns.js +109 -35
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +84 -1
- package/dist/cli/manager.d.ts +17 -0
- package/dist/cli/manager.js +135 -0
- package/dist/cli/recovery.d.ts +4 -0
- package/dist/cli/recovery.js +80 -0
- package/dist/cli/start.js +543 -79
- package/dist/cli/state.d.ts +4 -0
- package/dist/cli/state.js +69 -0
- package/dist/cli/status.js +13 -2
- package/dist/cli/tickets.js +26 -8
- package/dist/config.d.ts +2 -0
- package/dist/config.js +23 -0
- package/dist/continuity.d.ts +4 -0
- package/dist/continuity.js +38 -10
- package/dist/diagnostics.d.ts +25 -0
- package/dist/diagnostics.js +428 -0
- package/dist/foreman.d.ts +34 -6
- package/dist/foreman.js +164 -90
- package/dist/handoffs.d.ts +10 -3
- package/dist/handoffs.js +94 -10
- package/dist/index.js +8 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.js +4 -1
- package/dist/managerPacket.d.ts +25 -0
- package/dist/managerPacket.js +83 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +4 -1
- package/dist/observability.d.ts +248 -0
- package/dist/observability.js +1018 -0
- package/dist/planChanges.d.ts +4 -0
- package/dist/planChanges.js +16 -0
- package/dist/projectDiagnostics.d.ts +27 -0
- package/dist/projectDiagnostics.js +407 -0
- package/dist/qaFailureDelivery.d.ts +100 -0
- package/dist/qaFailureDelivery.js +570 -0
- package/dist/qaProtocolV2.d.ts +233 -0
- package/dist/qaProtocolV2.js +114 -0
- package/dist/qaRecovery.d.ts +152 -0
- package/dist/qaRecovery.js +989 -0
- package/dist/qaReview.d.ts +153 -14
- package/dist/qaReview.js +1747 -71
- package/dist/qaRuntime.d.ts +14 -0
- package/dist/qaRuntime.js +44 -0
- package/dist/qaSnapshot.d.ts +65 -5
- package/dist/qaSnapshot.js +473 -130
- package/dist/recoveryPolicy.d.ts +37 -0
- package/dist/recoveryPolicy.js +251 -0
- package/dist/roles.js +1 -0
- package/dist/stateTransfer.d.ts +57 -0
- package/dist/stateTransfer.js +644 -0
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +6 -0
- package/dist/supervisor.d.ts +40 -0
- package/dist/supervisor.js +102 -0
- package/dist/ticketHistoryReader.d.ts +19 -0
- package/dist/ticketHistoryReader.js +58 -0
- package/dist/tickets/details.d.ts +8 -0
- package/dist/tickets/details.js +59 -11
- package/dist/tickets/output.d.ts +5 -0
- package/dist/tickets/output.js +73 -0
- package/dist/tickets/stateDb.d.ts +1 -0
- package/dist/tickets/stateDb.js +5 -0
- package/dist/workflowDb.d.ts +275 -3
- package/dist/workflowDb.js +839 -29
- package/dist/workflowReader.d.ts +42 -0
- package/dist/workflowReader.js +171 -0
- package/foreman.yaml +9 -0
- package/package.json +63 -3
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ ai-foreman doctor ./my-project
|
|
|
70
70
|
ai-foreman tickets init --project ./my-project --app-name "My App"
|
|
71
71
|
ai-foreman tickets populate --project ./my-project --agent codex --model gpt-5.5 --effort xhigh
|
|
72
72
|
ai-foreman start ./my-project --agent codex --model gpt-5.5 --effort xhigh --steps 5
|
|
73
|
+
ai-foreman state export ./my-project --output rafi-state.rafi.gz
|
|
73
74
|
```
|
|
74
75
|
|
|
75
76
|
What each part does:
|
|
@@ -89,6 +90,20 @@ What each part does:
|
|
|
89
90
|
- ticket files
|
|
90
91
|
- folders or globs containing planning notes
|
|
91
92
|
|
|
93
|
+
## Move Local State Between Machines
|
|
94
|
+
|
|
95
|
+
Use `ai-foreman state export` for a sequential handoff from one checkout to another:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
ai-foreman state export ./my-project --output rafi-state.rafi.gz
|
|
99
|
+
ai-foreman state inspect rafi-state.rafi.gz
|
|
100
|
+
ai-foreman state import ./matching-checkout rafi-state.rafi.gz
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The bundle includes the local tracker database, delivery state, workflow/recovery databases, compiled role bundles, source snapshots, interview recovery records, and diagnostic run/recovery files. It does not include source code, provider sessions, or live worktrees. Export/import refuse dirty Git checkouts, active workflow leases, incompatible branch/HEAD state, and divergent transfer lineage. Import asks before replacing existing local state unless `--yes` is supplied, keeps a backup under `.rafi/state-transfer-backups/`, and rolls back failed imports.
|
|
104
|
+
|
|
105
|
+
Provider sessions are historical after import. Resume with `rafi build:resume <project> --fresh-with-handoff` or the equivalent guided recovery path when exact session recovery is unavailable.
|
|
106
|
+
|
|
92
107
|
## Primary Options
|
|
93
108
|
|
|
94
109
|
### `ai-foreman start`
|
|
@@ -148,6 +163,23 @@ With the saved `current` strategy, Foreman works in the active branch while the
|
|
|
148
163
|
|
|
149
164
|
The current terminal status includes the role/provider, activity, truthful context state, successful compaction count, and handoff generation. At the Builder threshold, Foreman settles the in-flight action, verifies native compaction plus a fresh provider usage sample, and resumes the frozen action. The configured maximum defaults to ten successful compactions per location-scoped provider session; the next crossing uses a validated fresh handoff instead of exceeding it. Disposable QA snapshots never reuse a prior QA conversation in a new `/tmp/rafi-qa-*` worktree; they transfer cumulative state through an accepted handoff.
|
|
150
165
|
|
|
166
|
+
Failed QA reviews use the versioned `RAFI_QA_FAILURE_REPORT_START` / `RAFI_QA_FAILURE_REPORT_END` JSON contract. Foreman validates the report before Builder remediation, namespaces finding IDs by run, ticket, and review, and durably indexes the exact report, remediation response, and bounded fix summary by digest. A malformed failing report receives two correction-only turns in its original session, then a validated fresh successor performs one complete source-bound review followed by up to two correction-only turns. Any later operator-requested fresh successor performs one complete source-bound review followed by five usable correction-only turns.
|
|
167
|
+
|
|
168
|
+
Owner-only recovery packets under `.foreman/qa-report-recovery/` bind the review to a full frozen source digest: commit, porcelain status, staged and unstaged bytes, and untracked content. Every fresh attempt captures and compares that complete state. Drift appends the new state and forces a complete review; correction-only reconstruction is never applied to changed source. Packet revisions and the authoritative WorkflowDb recovery head advance together. A successor must have a location-scoped identity, accept a handoff containing the complete resource inventory (purpose, byte count, digest, and recovery requirement), and acknowledge the copied packet before reporting. Paused recovery resumes with `rafi build:resume <project> --run <run-id> --ticket <ticket-id> --fresh-with-handoff`.
|
|
169
|
+
|
|
170
|
+
### `ai-foreman manager`
|
|
171
|
+
|
|
172
|
+
Manager is a project-wide, read-only diagnostic conversation over every retained build run. It starts with a verified live run when one exists, otherwise the most recently updated run; `--run <id>` changes only that initial focus. Each question refreshes the catalog, active state, deterministic aggregates, and relevant run details.
|
|
173
|
+
|
|
174
|
+
Permanent sanitized run summaries keep timing, counts, provider/model configuration, usage coverage, failure category, and terminal Git facts useful after detailed spans expire. Detailed spans retain the configured 30-day policy. Missing historical facts are reported as partial rather than reconstructed or treated as zero.
|
|
175
|
+
|
|
176
|
+
When the initial bounded packet omits evidence needed for a question, Manager can request catalog rows, aggregates, comparisons, or details through a host-validated read-only lookup. Each question is limited to two rounds, six operations per round, five detailed runs per operation, and fifty catalog rows per list operation. Questions, answers, and lookup envelopes are not stored as project evidence.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
ai-foreman manager ./my-project
|
|
180
|
+
ai-foreman manager ./my-project --run <run-id> --ask "Compare this run with the last five successful runs"
|
|
181
|
+
```
|
|
182
|
+
|
|
151
183
|
### GitHub PR Failure Recovery
|
|
152
184
|
|
|
153
185
|
When `--create-pr` cannot prepare GitHub, push a branch, or create a PR, Foreman records a structured failure and blocks the affected ticket cleanly. Blocked worktrees are retained automatically so you can repair the environment and retry without losing the ticket session.
|
|
@@ -482,6 +514,11 @@ ai-foreman start ./my-project --steps 5 --no-qa
|
|
|
482
514
|
# Resume the latest session / a specific session
|
|
483
515
|
ai-foreman start ./my-project --steps 5 --continue
|
|
484
516
|
ai-foreman start ./my-project --steps 5 --resume <session-id>
|
|
517
|
+
|
|
518
|
+
# Export, inspect, and import local Rafi state
|
|
519
|
+
ai-foreman state export ./my-project --output rafi-state.rafi.gz
|
|
520
|
+
ai-foreman state inspect rafi-state.rafi.gz
|
|
521
|
+
ai-foreman state import ./matching-checkout rafi-state.rafi.gz
|
|
485
522
|
```
|
|
486
523
|
|
|
487
524
|
## Ticket Lifecycle Commands
|
|
@@ -543,16 +580,24 @@ STEP_STATUS: needs_input | question="Which storage backend?" choices="SQLite|Pos
|
|
|
543
580
|
|
|
544
581
|
Provider-native `AskUserQuestion` prompts preserve their existing input/output behavior. Hosts may also observe a question only after the user successfully supplies a non-empty answer; cancelled, denied, malformed, and empty prompts do not emit answered-question telemetry. Rafi uses this boundary to recognize machine-shaped exhaustive-planning exchanges without treating generic questions as grill-me coverage.
|
|
545
582
|
|
|
546
|
-
When QA is enabled, Foreman
|
|
583
|
+
When QA is enabled, Foreman creates an independent read-only review snapshot. A pass ends with:
|
|
547
584
|
|
|
548
585
|
```txt
|
|
549
586
|
STEP_STATUS: qa_pass | summary="tests pass and acceptance criteria are met"
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Failure uses a structured report immediately before the status marker (the short `issues` field is only a human synopsis and fallback input):
|
|
590
|
+
|
|
591
|
+
```txt
|
|
592
|
+
RAFI_QA_FAILURE_REPORT_START
|
|
593
|
+
{"version":1,"summary":"missing empty-config coverage","checks_run":[{"check":"tests","command":"pnpm test","outcome":"failed","evidence":"empty-config case is absent"}],"findings":[{"id":"F1","requirement":"empty config is handled","locations":["src/config.ts","test/config.test.ts"],"problem":"no regression case exists","evidence":"test search found no empty-config case","expected":"the suite covers empty config","fix_direction":"add the missing case and repair behavior if needed","verification":["pnpm test"]}],"observations":[]}
|
|
594
|
+
RAFI_QA_FAILURE_REPORT_END
|
|
550
595
|
STEP_STATUS: qa_fail | issues="missing test for empty config"
|
|
551
596
|
```
|
|
552
597
|
|
|
553
598
|
If QA fails:
|
|
554
599
|
|
|
555
|
-
- Foreman sends
|
|
600
|
+
- Foreman sends the Builder the complete validated report, ticket requirements, current source digest, and prior report/remediation history.
|
|
556
601
|
- Foreman reruns QA.
|
|
557
602
|
- QA turns do not count against `--steps`.
|
|
558
603
|
|
package/dist/activity.d.ts
CHANGED
|
@@ -63,7 +63,11 @@ export declare class ActivityReporter {
|
|
|
63
63
|
export declare function currentActivity(): ActivityReporter | undefined;
|
|
64
64
|
export declare function withActivityContext<T>(command: string, operation: () => Promise<T>, options?: ActivityReporterOptions): Promise<T>;
|
|
65
65
|
export declare function withActivityPhase<T>(label: string, operation: () => Promise<T>): Promise<T>;
|
|
66
|
-
export
|
|
66
|
+
export interface ActivityInputPauseMetadata {
|
|
67
|
+
label?: string;
|
|
68
|
+
context?: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
export declare function pauseActivityForInput<T>(operation: () => Promise<T>, _metadata?: ActivityInputPauseMetadata): Promise<T>;
|
|
67
71
|
export declare function reportBuilderEvent(event: BuilderEvent): void;
|
|
68
72
|
/** Async event queue that also feeds the active CLI reporter without consuming the stream. */
|
|
69
73
|
export declare class BuilderEventQueue extends AsyncQueue<BuilderEvent> {
|
package/dist/activity.js
CHANGED
|
@@ -225,7 +225,7 @@ export async function withActivityPhase(label, operation) {
|
|
|
225
225
|
end?.();
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
-
export async function pauseActivityForInput(operation) {
|
|
228
|
+
export async function pauseActivityForInput(operation, _metadata = {}) {
|
|
229
229
|
const resume = currentActivity()?.pause();
|
|
230
230
|
try {
|
|
231
231
|
return await operation();
|
|
@@ -50,6 +50,9 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
50
50
|
private readonly pumpDone;
|
|
51
51
|
private _sessionId?;
|
|
52
52
|
private _sessionRef?;
|
|
53
|
+
private readonly sessionIdentityReady;
|
|
54
|
+
private resolveSessionIdentity;
|
|
55
|
+
private rejectSessionIdentity;
|
|
53
56
|
private readonly stderrChunks;
|
|
54
57
|
private turnSignals;
|
|
55
58
|
private structuredError?;
|
|
@@ -62,6 +65,10 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
62
65
|
private nativeCompactions;
|
|
63
66
|
private nativeCompactionSequence;
|
|
64
67
|
private cumulativeUsage;
|
|
68
|
+
private priorTurnUsage;
|
|
69
|
+
private activeProviderTurnId?;
|
|
70
|
+
private activeProviderTurnSpanId?;
|
|
71
|
+
private readonly toolSpans;
|
|
65
72
|
private pending?;
|
|
66
73
|
private terminalResult?;
|
|
67
74
|
private streamEnded;
|
|
@@ -74,11 +81,15 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
74
81
|
sendTurn(text: string): Promise<TurnResult>;
|
|
75
82
|
private sendTurnInternal;
|
|
76
83
|
private captureStderr;
|
|
84
|
+
private observationContext;
|
|
85
|
+
private observeToolResults;
|
|
77
86
|
sessionId(): string | undefined;
|
|
78
87
|
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
88
|
+
prepareSession(timeoutMs?: number): Promise<ProviderSessionRefV1>;
|
|
79
89
|
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
80
90
|
validateSession(): Promise<SessionAvailabilityV1>;
|
|
81
91
|
compact(): Promise<CompactResult>;
|
|
92
|
+
private compactInternal;
|
|
82
93
|
prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<NativeAutoCompactionPolicy | void>;
|
|
83
94
|
autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
|
|
84
95
|
drainNativeCompactions(): NativeCompaction[];
|
|
@@ -88,6 +99,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
88
99
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
89
100
|
events(): AsyncIterable<BuilderEvent>;
|
|
90
101
|
close(): Promise<void>;
|
|
102
|
+
private failSessionIdentity;
|
|
91
103
|
private observeSession;
|
|
92
104
|
private settlePending;
|
|
93
105
|
private streamFailureResult;
|
package/dist/adapters/claude.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
1
3
|
/** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
|
|
2
4
|
export async function requireClaudeSDK() {
|
|
3
5
|
try {
|
|
@@ -21,25 +23,42 @@ import { SessionUnavailableError, sessionUnavailableResult } from "./sessionFail
|
|
|
21
23
|
* `canUseTool` is omitted here — it's a closure that the constructor adds.
|
|
22
24
|
*/
|
|
23
25
|
export function buildClaudeQueryOptions(opts) {
|
|
26
|
+
const qaReadOnly = opts.sessionRole === "qa" || opts.sandboxMode === "read-only";
|
|
24
27
|
const base = {
|
|
25
28
|
cwd: opts.cwd,
|
|
26
29
|
pathToClaudeCodeExecutable: opts.runtimeExecutable,
|
|
27
|
-
env: { ...process.env },
|
|
30
|
+
env: qaReadOnly ? qaEnvironment(process.env, opts.cwd) : { ...process.env },
|
|
28
31
|
model: opts.model,
|
|
29
32
|
resume: opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId,
|
|
30
|
-
permissionMode: "acceptEdits",
|
|
33
|
+
permissionMode: qaReadOnly ? "default" : "acceptEdits",
|
|
31
34
|
effort: opts.effort,
|
|
32
35
|
extraArgs: opts.fast ? { fast: null } : undefined,
|
|
33
|
-
settingSources: ["user", "project", "local"],
|
|
36
|
+
settingSources: qaReadOnly ? [] : ["user", "project", "local"],
|
|
37
|
+
...(qaReadOnly ? { disallowedTools: ["Write", "Edit", "NotebookEdit"] } : {}),
|
|
38
|
+
...(qaReadOnly ? { sandbox: {
|
|
39
|
+
enabled: true,
|
|
40
|
+
failIfUnavailable: true,
|
|
41
|
+
autoAllowBashIfSandboxed: true,
|
|
42
|
+
allowUnsandboxedCommands: false,
|
|
43
|
+
filesystem: { allowWrite: [join(dirname(opts.cwd), "scratch")], denyWrite: [opts.cwd, ...(opts.configRoot ? [opts.configRoot] : [])] },
|
|
44
|
+
} } : {}),
|
|
34
45
|
};
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
const exactSkills = opts.preloadedSkillContent?.length
|
|
47
|
+
? ["# Preloaded Skills", "Use the following skills for this run.", ...opts.preloadedSkillContent.map((skill) => skill.content)].join("\n\n")
|
|
48
|
+
: undefined;
|
|
49
|
+
const systemAppend = [opts.systemPromptAppend, exactSkills].filter((part) => Boolean(part)).join("\n\n");
|
|
50
|
+
if (systemAppend) {
|
|
51
|
+
base.systemPrompt = { type: "preset", preset: "claude_code", append: systemAppend };
|
|
37
52
|
}
|
|
38
53
|
if (opts.skills !== undefined) {
|
|
39
54
|
base.skills = opts.skills;
|
|
40
55
|
}
|
|
41
56
|
return base;
|
|
42
57
|
}
|
|
58
|
+
function qaEnvironment(source, cwd) {
|
|
59
|
+
const allowed = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "LANG", "LC_ALL", "TERM", "NO_COLOR", "FORCE_COLOR", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"];
|
|
60
|
+
return { ...Object.fromEntries(allowed.flatMap((key) => source[key] === undefined ? [] : [[key, source[key]]])), TMPDIR: join(dirname(cwd), "scratch") };
|
|
61
|
+
}
|
|
43
62
|
export async function probeClaudeSession(ref, input) {
|
|
44
63
|
const now = input.now ?? new Date();
|
|
45
64
|
const local = validateProviderSessionScope(ref, {
|
|
@@ -147,6 +166,9 @@ export class ClaudeAdapter {
|
|
|
147
166
|
pumpDone;
|
|
148
167
|
_sessionId;
|
|
149
168
|
_sessionRef;
|
|
169
|
+
sessionIdentityReady;
|
|
170
|
+
resolveSessionIdentity;
|
|
171
|
+
rejectSessionIdentity;
|
|
150
172
|
stderrChunks = [];
|
|
151
173
|
turnSignals = [];
|
|
152
174
|
structuredError;
|
|
@@ -159,6 +181,10 @@ export class ClaudeAdapter {
|
|
|
159
181
|
nativeCompactions = [];
|
|
160
182
|
nativeCompactionSequence = 0;
|
|
161
183
|
cumulativeUsage = { observedAt: new Date(0).toISOString(), source: "provider" };
|
|
184
|
+
priorTurnUsage = {};
|
|
185
|
+
activeProviderTurnId;
|
|
186
|
+
activeProviderTurnSpanId;
|
|
187
|
+
toolSpans = new Map();
|
|
162
188
|
pending;
|
|
163
189
|
terminalResult;
|
|
164
190
|
streamEnded = false;
|
|
@@ -205,11 +231,28 @@ export class ClaudeAdapter {
|
|
|
205
231
|
this.opts = opts;
|
|
206
232
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
207
233
|
this._sessionRef = opts.resumeSessionRef;
|
|
234
|
+
this.sessionIdentityReady = new Promise((resolve, reject) => {
|
|
235
|
+
this.resolveSessionIdentity = resolve;
|
|
236
|
+
this.rejectSessionIdentity = reject;
|
|
237
|
+
});
|
|
238
|
+
void this.sessionIdentityReady.catch(() => { });
|
|
208
239
|
this.query = query({
|
|
209
240
|
prompt: this.inbox,
|
|
210
241
|
options: {
|
|
211
242
|
...buildClaudeQueryOptions(opts),
|
|
212
243
|
abortController: this.abort,
|
|
244
|
+
hooks: { SessionStart: [{ hooks: [async (input) => {
|
|
245
|
+
if (input.hook_event_name !== "SessionStart" || input.agent_id)
|
|
246
|
+
return {};
|
|
247
|
+
try {
|
|
248
|
+
this.observeSession(input.session_id, input.cwd);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
this.failSessionIdentity(error instanceof Error ? error : new Error(String(error)));
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
return {};
|
|
255
|
+
}] }] },
|
|
213
256
|
stderr: (data) => this.captureStderr(data),
|
|
214
257
|
canUseTool: async (toolName, input, requestOptions = {}) => {
|
|
215
258
|
const decision = await opts.permission({
|
|
@@ -246,6 +289,7 @@ export class ClaudeAdapter {
|
|
|
246
289
|
const message = normalizeRuntimeErrorText("claude", rawMessage, null, "builder stream");
|
|
247
290
|
this.eventQueue.push({ kind: "error", message });
|
|
248
291
|
const result = this.streamFailureResult(message, isMissingClaudeSession(rawMessage));
|
|
292
|
+
this.rejectSessionIdentity(new Error(message));
|
|
249
293
|
this.terminalResult = result;
|
|
250
294
|
this.settlePending(result);
|
|
251
295
|
}
|
|
@@ -260,13 +304,14 @@ export class ClaudeAdapter {
|
|
|
260
304
|
}
|
|
261
305
|
if (!this.closed && !this.terminalResult)
|
|
262
306
|
this.terminalResult = this.streamFailureResult("Claude stream ended without a result", Boolean(this.opts.resumeSessionRef ?? this.opts.resumeSessionId));
|
|
307
|
+
if (!this._sessionRef)
|
|
308
|
+
this.rejectSessionIdentity(new Error("Claude stream ended before exposing a scoped session identity"));
|
|
263
309
|
this.eventQueue.close();
|
|
264
310
|
}
|
|
265
311
|
}
|
|
266
312
|
handle(msg) {
|
|
267
313
|
if ("session_id" in msg && typeof msg.session_id === "string") {
|
|
268
|
-
this.
|
|
269
|
-
this.observeSession(msg.session_id);
|
|
314
|
+
this.observeSession(msg.session_id, "cwd" in msg && typeof msg.cwd === "string" ? msg.cwd : undefined);
|
|
270
315
|
}
|
|
271
316
|
if (msg.type === "assistant") {
|
|
272
317
|
if (msg.error) {
|
|
@@ -274,15 +319,30 @@ export class ClaudeAdapter {
|
|
|
274
319
|
this.turnSignals.push(`assistant error: ${msg.error}`);
|
|
275
320
|
}
|
|
276
321
|
for (const block of msg.message.content) {
|
|
322
|
+
this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: String(block.type ?? "unknown"), payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
277
323
|
if (block.type === "text" && block.text) {
|
|
278
|
-
this.eventQueue.push({ kind: "text", text: block.text });
|
|
324
|
+
this.eventQueue.push({ kind: "text", text: block.text, byteCount: Buffer.byteLength(block.text), digest: createHash("sha256").update(block.text).digest("hex"), eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
325
|
+
this.opts.observer?.signal(true);
|
|
279
326
|
}
|
|
280
327
|
else if (block.type === "tool_use") {
|
|
328
|
+
const callId = block.id;
|
|
281
329
|
this.eventQueue.push({
|
|
282
330
|
kind: "tool",
|
|
283
331
|
name: block.name,
|
|
284
332
|
input: block.input,
|
|
333
|
+
inputCompleteness: "complete",
|
|
334
|
+
lifecycle: "started",
|
|
335
|
+
callId,
|
|
336
|
+
completionKnown: false,
|
|
337
|
+
providerTurnId: this.activeProviderTurnId,
|
|
338
|
+
eventId: randomUUID(),
|
|
339
|
+
observedAt: new Date().toISOString(),
|
|
285
340
|
});
|
|
341
|
+
if (this.opts.observer) {
|
|
342
|
+
const spanId = this.opts.observer.store.startSpan(this.observationContext(), { kind: "tool", name: block.name, providerTurnId: this.activeProviderTurnId, attributes: { callId, input: block.input } });
|
|
343
|
+
this.toolSpans.set(callId, spanId);
|
|
344
|
+
this.opts.observer.signal(true);
|
|
345
|
+
}
|
|
286
346
|
}
|
|
287
347
|
}
|
|
288
348
|
}
|
|
@@ -309,10 +369,24 @@ export class ClaudeAdapter {
|
|
|
309
369
|
this.structuredError = msg.error;
|
|
310
370
|
this.apiErrorStatus = msg.error_status;
|
|
311
371
|
this.turnSignals.push(`API retry ${msg.attempt}/${msg.max_retries}: ${msg.error}${msg.error_status === null ? "" : ` (HTTP ${msg.error_status})`}`);
|
|
312
|
-
this.
|
|
372
|
+
const retryEvent = { ...claudeApiRetryEvent(msg), retryId: randomUUID(), providerTurnId: this.activeProviderTurnId, observedAt: new Date().toISOString() };
|
|
373
|
+
this.eventQueue.push(retryEvent);
|
|
374
|
+
if (this.opts.observer) {
|
|
375
|
+
const attributes = { provider: "claude", attempt: msg.attempt, maximum: msg.max_retries, reportedDelayMs: msg.retry_delay_ms, reasonDigest: createHash("sha256").update(msg.error).digest("hex") };
|
|
376
|
+
const retrySpanId = this.opts.observer.store.startSpan(this.observationContext(), { spanId: retryEvent.retryId, kind: "retry", name: "Claude provider retry", providerTurnId: this.activeProviderTurnId, attributes });
|
|
377
|
+
this.opts.observer.store.recordEvent(this.observationContext(), "retry", { eventId: retryEvent.retryId, spanId: retrySpanId, severity: "warning", attributes });
|
|
378
|
+
this.opts.observer.store.finishSpan(retrySpanId, { outcome: "reported" });
|
|
379
|
+
}
|
|
313
380
|
}
|
|
314
381
|
else if (msg.type === "tool_progress") {
|
|
315
382
|
this.eventQueue.push({ kind: "activity", provider: "claude", state: "running tool", detail: `${msg.tool_name} (${Math.floor(msg.elapsed_time_seconds)}s)`, transient: true });
|
|
383
|
+
const progress = msg;
|
|
384
|
+
const callId = typeof progress.tool_use_id === "string" ? progress.tool_use_id : typeof progress.toolUseID === "string" ? progress.toolUseID : undefined;
|
|
385
|
+
this.eventQueue.push({ kind: "tool", name: msg.tool_name, input: {}, lifecycle: "progress", callId, durationMs: Math.max(0, msg.elapsed_time_seconds * 1000), completionKnown: false, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
386
|
+
this.opts.observer?.signal(false);
|
|
387
|
+
}
|
|
388
|
+
else if (msg.type === "user") {
|
|
389
|
+
this.observeToolResults(msg);
|
|
316
390
|
}
|
|
317
391
|
else if (msg.type === "system") {
|
|
318
392
|
const system = msg;
|
|
@@ -364,13 +438,20 @@ export class ClaudeAdapter {
|
|
|
364
438
|
costUsd: msg.total_cost_usd,
|
|
365
439
|
costAuthoritative: Number.isFinite(msg.total_cost_usd),
|
|
366
440
|
failure,
|
|
441
|
+
turnId: this.pending?.turnId ?? this.activeProviderTurnId,
|
|
442
|
+
hostInstruction: this.pending?.instruction,
|
|
443
|
+
providerInstruction: this.pending?.instruction,
|
|
444
|
+
rawResponse: text,
|
|
445
|
+
cleanedResponse: failure ? formatClaudeFailure(failure) : text,
|
|
446
|
+
providerMetadata: { provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef },
|
|
367
447
|
};
|
|
368
448
|
const rawResult = msg;
|
|
369
449
|
const mergedUsage = mergeClaudeProviderSessionUsage(this.cumulativeUsage, rawResult);
|
|
370
450
|
result.inputTokens = mergedUsage.inputTokens;
|
|
371
451
|
result.outputTokens = mergedUsage.outputTokens;
|
|
452
|
+
result.usage = { scope: "session-cumulative", inputTokens: mergedUsage.sample.inputTokens, outputTokens: mergedUsage.sample.outputTokens, totalTokens: mergedUsage.sample.totalTokens, costUsd: mergedUsage.sample.authoritativeCostUsd };
|
|
372
453
|
this.cumulativeUsage = mergedUsage.sample;
|
|
373
|
-
this.eventQueue.push({ kind: "turn-complete", result });
|
|
454
|
+
this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
|
|
374
455
|
this.settlePending(result, false);
|
|
375
456
|
this.turnSignals = [];
|
|
376
457
|
this.structuredError = undefined;
|
|
@@ -378,8 +459,43 @@ export class ClaudeAdapter {
|
|
|
378
459
|
this.stderrChunks.length = 0;
|
|
379
460
|
}
|
|
380
461
|
}
|
|
381
|
-
sendTurn(text) {
|
|
382
|
-
|
|
462
|
+
async sendTurn(text) {
|
|
463
|
+
const turnId = randomUUID();
|
|
464
|
+
this.activeProviderTurnId = turnId;
|
|
465
|
+
const observer = this.opts.observer;
|
|
466
|
+
if (!observer) {
|
|
467
|
+
try {
|
|
468
|
+
return await withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(text));
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
this.activeProviderTurnId = undefined;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const context = this.observationContext();
|
|
475
|
+
const spanId = observer.store.startSpan(context, { spanId: turnId, kind: "provider_turn", name: `Claude ${activityPhase(this.opts.runtimePhase)}`, providerTurnId: turnId, attributes: { provider: "claude" } });
|
|
476
|
+
this.activeProviderTurnSpanId = spanId;
|
|
477
|
+
observer.store.updateCurrentState({ runId: observer.runId, role: context.role ?? "host", stream: context.stream ?? "claude", executionId: observer.executionId, ticketId: context.ticketId, deliveryUnitId: context.deliveryUnitId, providerSessionId: context.providerSessionId, phase: "provider turn", activeSpanId: spanId, activeSpanKind: "provider_turn", lastSemanticProgressAt: new Date().toISOString() });
|
|
478
|
+
try {
|
|
479
|
+
const result = await withActivityPhase(`Claude ${activityPhase(this.opts.runtimePhase)}`, () => this.sendTurnInternal(text));
|
|
480
|
+
const deltaInput = nonNegativeDelta(result.usage?.inputTokens, this.priorTurnUsage.inputTokens);
|
|
481
|
+
const deltaOutput = nonNegativeDelta(result.usage?.outputTokens, this.priorTurnUsage.outputTokens);
|
|
482
|
+
const deltaCost = nonNegativeDelta(result.usage?.costUsd, this.priorTurnUsage.costUsd);
|
|
483
|
+
this.priorTurnUsage = { inputTokens: result.usage?.inputTokens, outputTokens: result.usage?.outputTokens, costUsd: result.usage?.costUsd };
|
|
484
|
+
observer.store.finishSpan(spanId, { outcome: result.isError ? "failed" : "completed", attributes: { cumulativeUsage: result.usage, turnDelta: { inputTokens: deltaInput, outputTokens: deltaOutput, costUsd: deltaCost } } });
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
observer.store.finishSpan(spanId, { outcome: "failed", attributes: { error: String(error).slice(0, 500) } });
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
for (const [callId, toolSpanId] of this.toolSpans) {
|
|
493
|
+
observer.store.finishSpan(toolSpanId, { outcome: "unknown", completionKnown: false, attributes: { callId } });
|
|
494
|
+
this.toolSpans.delete(callId);
|
|
495
|
+
}
|
|
496
|
+
this.activeProviderTurnId = undefined;
|
|
497
|
+
this.activeProviderTurnSpanId = undefined;
|
|
498
|
+
}
|
|
383
499
|
}
|
|
384
500
|
sendTurnInternal(text) {
|
|
385
501
|
if (this.closed)
|
|
@@ -394,7 +510,7 @@ export class ClaudeAdapter {
|
|
|
394
510
|
this.turnSignals = [];
|
|
395
511
|
this.structuredError = undefined;
|
|
396
512
|
this.apiErrorStatus = undefined;
|
|
397
|
-
this.pending = { resolve, reject };
|
|
513
|
+
this.pending = { resolve, reject, instruction: text, turnId: this.activeProviderTurnId ?? randomUUID() };
|
|
398
514
|
this.inbox.push({
|
|
399
515
|
type: "user",
|
|
400
516
|
message: { role: "user", content: text },
|
|
@@ -407,10 +523,73 @@ export class ClaudeAdapter {
|
|
|
407
523
|
while (this.stderrChunks.join("").length > 8 * 1024)
|
|
408
524
|
this.stderrChunks.shift();
|
|
409
525
|
}
|
|
526
|
+
observationContext() {
|
|
527
|
+
const observer = this.opts.observer;
|
|
528
|
+
const inherited = observer.context();
|
|
529
|
+
return { ...inherited, runId: observer.runId, executionId: observer.executionId, role: this.opts.sessionRole ?? inherited.role ?? "builder",
|
|
530
|
+
stream: this.opts.sessionStream ?? inherited.stream ?? this.opts.sessionRole ?? "builder", providerSessionId: this._sessionId,
|
|
531
|
+
ticketId: this.opts.ticketId ?? inherited.ticketId, deliveryUnitId: this.opts.deliveryUnitId ?? inherited.deliveryUnitId,
|
|
532
|
+
parentSpanId: this.activeProviderTurnSpanId ?? inherited.parentSpanId, providerTurnId: this.activeProviderTurnId ?? inherited.providerTurnId };
|
|
533
|
+
}
|
|
534
|
+
observeToolResults(message) {
|
|
535
|
+
const body = message.message;
|
|
536
|
+
if (!Array.isArray(body?.content))
|
|
537
|
+
return;
|
|
538
|
+
for (const block of body.content) {
|
|
539
|
+
if (block.type !== "tool_result")
|
|
540
|
+
continue;
|
|
541
|
+
this.eventQueue.push({ kind: "provider-item", provider: "claude", lifecycle: "completed", itemType: "tool_result", payload: structuredClone(block), payloadCompleteness: "complete", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
542
|
+
const callId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
|
|
543
|
+
if (!callId)
|
|
544
|
+
continue;
|
|
545
|
+
const spanId = this.toolSpans.get(callId);
|
|
546
|
+
const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
|
|
547
|
+
const sanitized = text.replace(/\b(?:token|password|secret|api[_-]?key)\s*[:=]\s*\S+/gi, "$1=[REDACTED]");
|
|
548
|
+
const failed = block.is_error === true;
|
|
549
|
+
const outputDigest = createHash("sha256").update(text).digest("hex");
|
|
550
|
+
this.eventQueue.push({ kind: "tool", name: "tool_result", input: {}, inputCompleteness: "unavailable", lifecycle: "completed", callId, status: failed ? "failed" : "completed", outputSummary: sanitized.slice(0, 1000), outputDigest, outputCompleteness: block.is_truncated === true ? "truncated" : "complete", rawOutputBytes: Buffer.byteLength(text), completionKnown: true, providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
551
|
+
if (spanId && this.opts.observer) {
|
|
552
|
+
this.opts.observer.store.finishSpan(spanId, { outcome: failed ? "failed" : "completed", completionKnown: true, attributes: { callId, outputSummary: sanitized.slice(0, 1000), outputDigest } });
|
|
553
|
+
this.toolSpans.delete(callId);
|
|
554
|
+
}
|
|
555
|
+
this.opts.observer?.signal(true);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
410
558
|
sessionId() {
|
|
411
559
|
return this._sessionId;
|
|
412
560
|
}
|
|
413
561
|
sessionRef() { return this._sessionRef; }
|
|
562
|
+
async prepareSession(timeoutMs = 30_000) {
|
|
563
|
+
if (this.closed || this.streamEnded || this.terminalResult)
|
|
564
|
+
throw new Error(this.terminalResult?.text ?? "Claude session is closed");
|
|
565
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
566
|
+
throw new Error("Claude session initialization timeout must be positive");
|
|
567
|
+
let timer;
|
|
568
|
+
let aborted;
|
|
569
|
+
try {
|
|
570
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
571
|
+
aborted = () => reject(new Error(this.terminalResult?.text ?? "Claude session closed during initialization"));
|
|
572
|
+
this.abort.signal.addEventListener("abort", aborted, { once: true });
|
|
573
|
+
if (this.abort.signal.aborted)
|
|
574
|
+
aborted();
|
|
575
|
+
timer = setTimeout(() => reject(new Error(`Claude did not expose a scoped session identity within ${timeoutMs}ms`)), timeoutMs);
|
|
576
|
+
});
|
|
577
|
+
const [, ref] = await Promise.race([Promise.all([this.query.initializationResult(), this.sessionIdentityReady]), interrupted]);
|
|
578
|
+
if (!ref.validatedAt)
|
|
579
|
+
throw new Error("Claude session identity was not provider-validated during initialization");
|
|
580
|
+
return ref;
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
this.failSessionIdentity(error instanceof Error ? error : new Error(String(error)));
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
if (timer)
|
|
588
|
+
clearTimeout(timer);
|
|
589
|
+
if (aborted)
|
|
590
|
+
this.abort.signal.removeEventListener("abort", aborted);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
414
593
|
adoptSessionRef(ref) {
|
|
415
594
|
if (ref.provider !== "claude" || ref.sessionId !== this._sessionId)
|
|
416
595
|
throw new Error("cannot adopt a session reference for a different Claude conversation");
|
|
@@ -430,6 +609,11 @@ export class ClaudeAdapter {
|
|
|
430
609
|
});
|
|
431
610
|
}
|
|
432
611
|
async compact() {
|
|
612
|
+
return this.opts.observer
|
|
613
|
+
? this.opts.observer.span("compaction", "Claude context compaction", () => this.compactInternal())
|
|
614
|
+
: this.compactInternal();
|
|
615
|
+
}
|
|
616
|
+
async compactInternal() {
|
|
433
617
|
this.compactResult = undefined;
|
|
434
618
|
this.manualCompactionInFlight = true;
|
|
435
619
|
let result;
|
|
@@ -528,6 +712,8 @@ export class ClaudeAdapter {
|
|
|
528
712
|
if (this.closed)
|
|
529
713
|
return;
|
|
530
714
|
this.closed = true;
|
|
715
|
+
this.rejectSessionIdentity(new Error("Claude session closed during initialization"));
|
|
716
|
+
this.abort.abort();
|
|
531
717
|
this.inbox.close();
|
|
532
718
|
try {
|
|
533
719
|
await this.query.interrupt();
|
|
@@ -535,26 +721,65 @@ export class ClaudeAdapter {
|
|
|
535
721
|
catch {
|
|
536
722
|
// interrupt is best-effort — ignore if no turn is active
|
|
537
723
|
}
|
|
538
|
-
this.abort.abort();
|
|
539
724
|
await this.pumpDone.catch(() => { });
|
|
540
725
|
}
|
|
541
|
-
|
|
542
|
-
|
|
726
|
+
failSessionIdentity(error) {
|
|
727
|
+
this.rejectSessionIdentity(error);
|
|
728
|
+
const result = sessionUnavailableResult(new SessionUnavailableError({
|
|
729
|
+
runtime: "claude", phase: this.pending ? "turn" : "preflight", dispatchState: this.pending ? "unknown" : "not-sent",
|
|
730
|
+
executable: this.opts.runtimeExecutable ?? "claude", cwd: this.opts.cwd, diagnostics: error.message,
|
|
731
|
+
availability: { version: 1, status: "unavailable", checkedAt: new Date().toISOString(), reason: "attach-failed", detail: error.message, ...(this._sessionRef ? { sessionRef: this._sessionRef } : {}) },
|
|
732
|
+
}));
|
|
733
|
+
this.terminalResult = result;
|
|
734
|
+
this.settlePending(result);
|
|
735
|
+
this.inbox.close();
|
|
736
|
+
this.abort.abort();
|
|
737
|
+
}
|
|
738
|
+
observeSession(sessionId, observedCwd) {
|
|
739
|
+
const expectedId = this._sessionRef?.sessionId ?? this._sessionId;
|
|
740
|
+
if (!sessionId.trim() || (expectedId && expectedId !== sessionId)) {
|
|
741
|
+
const error = new Error(`Claude initialized session ${sessionId || "without an ID"} instead of requested session ${expectedId ?? "a nonempty ID"}`);
|
|
742
|
+
this.failSessionIdentity(error);
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
if (observedCwd !== undefined && canonicalSessionPath(observedCwd) !== canonicalSessionPath(this.opts.cwd)) {
|
|
746
|
+
const error = new Error(`Claude session cwd ${observedCwd} does not match ${this.opts.cwd}`);
|
|
747
|
+
this.failSessionIdentity(error);
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
this._sessionId = sessionId;
|
|
751
|
+
if (this._sessionRef?.validatedAt) {
|
|
752
|
+
this.resolveSessionIdentity(this._sessionRef);
|
|
543
753
|
return;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
source: "observed",
|
|
549
|
-
|
|
754
|
+
}
|
|
755
|
+
const now = new Date().toISOString();
|
|
756
|
+
const validatedAt = observedCwd !== undefined || this.opts.sessionRole !== "qa" ? now : undefined;
|
|
757
|
+
this._sessionRef = this._sessionRef
|
|
758
|
+
? { ...this._sessionRef, source: "observed", ...(validatedAt ? { validatedAt } : {}) }
|
|
759
|
+
: createProviderSessionRef({
|
|
760
|
+
provider: "claude", sessionId, cwd: this.opts.cwd, configRoot: this.opts.configRoot ?? this.opts.cwd,
|
|
761
|
+
role: this.opts.sessionRole, stream: this.opts.sessionStream, generation: this.opts.sessionGeneration,
|
|
762
|
+
workspaceIdentity: this.opts.workspaceIdentity, ticketId: this.opts.ticketId, deliveryUnitId: this.opts.deliveryUnitId,
|
|
763
|
+
source: "observed", validatedAt,
|
|
764
|
+
});
|
|
765
|
+
if (this._sessionRef.validatedAt)
|
|
766
|
+
this.resolveSessionIdentity(this._sessionRef);
|
|
550
767
|
}
|
|
551
768
|
settlePending(result, emit = true) {
|
|
552
769
|
const pending = this.pending;
|
|
553
770
|
if (!pending)
|
|
554
771
|
return;
|
|
555
772
|
this.pending = undefined;
|
|
773
|
+
if (!result.turnId) {
|
|
774
|
+
result.turnId = pending.turnId;
|
|
775
|
+
result.hostInstruction = pending.instruction;
|
|
776
|
+
result.providerInstruction = pending.instruction;
|
|
777
|
+
result.rawResponse = result.text;
|
|
778
|
+
result.cleanedResponse = result.text;
|
|
779
|
+
result.providerMetadata = { provider: "claude", sessionId: this._sessionId, sessionRef: this._sessionRef };
|
|
780
|
+
}
|
|
556
781
|
if (emit)
|
|
557
|
-
this.eventQueue.push({ kind: "turn-complete", result });
|
|
782
|
+
this.eventQueue.push({ kind: "turn-complete", result, turnId: result.turnId });
|
|
558
783
|
pending.resolve(result);
|
|
559
784
|
}
|
|
560
785
|
streamFailureResult(message, sessionUnavailable) {
|
|
@@ -575,6 +800,11 @@ export class ClaudeAdapter {
|
|
|
575
800
|
}
|
|
576
801
|
}
|
|
577
802
|
function finiteNumber(value) { const parsed = Number(value); return value !== null && value !== undefined && value !== "" && Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; }
|
|
803
|
+
function nonNegativeDelta(current, prior) {
|
|
804
|
+
if (current === undefined)
|
|
805
|
+
return undefined;
|
|
806
|
+
return prior === undefined ? current : Math.max(0, current - prior);
|
|
807
|
+
}
|
|
578
808
|
function validThreshold(value) {
|
|
579
809
|
if (!Number.isInteger(value) || value < 1 || value > 99)
|
|
580
810
|
throw new Error("automatic compaction threshold must be an integer from 1 to 99");
|
|
@@ -595,6 +825,8 @@ function activityPhase(phase) {
|
|
|
595
825
|
return "reviewing with QA";
|
|
596
826
|
if (phase === "uninstaller")
|
|
597
827
|
return "planning uninstall";
|
|
828
|
+
if (phase === "manager")
|
|
829
|
+
return "analyzing diagnostics";
|
|
598
830
|
return "building";
|
|
599
831
|
}
|
|
600
832
|
function formatClaudeFailure(failure) {
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -35,6 +35,9 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
35
35
|
private nativeCompactionSequence;
|
|
36
36
|
private manualCompactionInFlight;
|
|
37
37
|
private observedToolCalls;
|
|
38
|
+
private activeProviderTurnId?;
|
|
39
|
+
private activeProviderTurnSpanId?;
|
|
40
|
+
private readonly toolSpans;
|
|
38
41
|
constructor(opts: BuilderAdapterOptions);
|
|
39
42
|
buildInstruction(instruction: string): string;
|
|
40
43
|
buildAppServerArgs(): string[];
|
|
@@ -43,7 +46,9 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
43
46
|
sendTurn(instruction: string): Promise<TurnResult>;
|
|
44
47
|
private sendTurnInternal;
|
|
45
48
|
compact(): Promise<CompactResult>;
|
|
49
|
+
private compactInternal;
|
|
46
50
|
prepareAutoCompaction(thresholdPercent?: number | undefined): Promise<NativeAutoCompactionPolicy | void>;
|
|
51
|
+
requiresAutoCompactionSetupTurn(): boolean;
|
|
47
52
|
autoCompactionPolicy(): NativeAutoCompactionPolicy | undefined;
|
|
48
53
|
drainNativeCompactions(): NativeCompaction[];
|
|
49
54
|
restoreNativeCompactions(compactions: NativeCompaction[]): void;
|
|
@@ -53,6 +58,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
53
58
|
switchSettings(settings: ProviderSettingSwitch): Promise<CompactResult>;
|
|
54
59
|
sessionId(): string | undefined;
|
|
55
60
|
sessionRef(): ProviderSessionRefV1 | undefined;
|
|
61
|
+
prepareSession(): Promise<ProviderSessionRefV1>;
|
|
56
62
|
adoptSessionRef(ref: ProviderSessionRefV1): void;
|
|
57
63
|
validateSession(): Promise<SessionAvailabilityV1>;
|
|
58
64
|
events(): AsyncIterable<BuilderEvent>;
|
|
@@ -63,6 +69,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
63
69
|
private request;
|
|
64
70
|
private write;
|
|
65
71
|
private handle;
|
|
72
|
+
private observationContext;
|
|
66
73
|
private waitFor;
|
|
67
74
|
private disconnect;
|
|
68
75
|
private restartForAutoCompaction;
|