@tangle-network/sdk-provider-cli-base 0.13.17 → 0.13.18

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.
@@ -0,0 +1,4586 @@
1
+ import { DEFAULT_INTERACTION_DECISION_TIMEOUT_MS as PERMISSION_DECISION_TIMEOUT_MS, InteractionBroker, InteractionBroker as InteractionBroker$1, InteractionHttpBridge, brokerInteractionTools, brokerInteractionTools as brokerInteractionTools$1 } from "@tangle-network/agent-core/interactions";
2
+ import { CODEX_DISABLED_FEATURE_ARGS, HARNESS_LAUNCH_MATERIALIZE_OPTIONS, UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, applyWorkspacePlanWithSecretProvider, bindLaunchToolBinDirs, bindSecretSessionView, codexDeveloperInstructionArgs, enforceWorkspacePlanSupport, ensurePublicConcurrentProfileRoots, joinSystemPromptAdditions, keepsHarnessSystemPrompt, materializeProfile, modelIdsMatch, parseSandboxToolBinDirs, preparePublicConcurrentProfileRevision, prepareSecretConcurrentProfileRevision, resolveAgentProfileResources, secretSessionViewWorkspace } from "@tangle-network/agent-profile-materialize";
3
+ import { BridgeClosedError, InvalidAttachmentError, JsonRpcError, RpcRequestTimeoutError, execCli, launchCliProcess, resolveRawCapturePath, spawnCliProcess, spawnRpcBridge } from "@tangle-network/sdk-cli-runner";
4
+ import { createHash, createHmac, randomUUID } from "node:crypto";
5
+ import { AgentExecutionError, AgentExecutionOutcomeSchema, AgentRuntimeAttachmentsSchema, InteractionExecutionBindingSchema, InteractionKind, REASONING_EFFORTS, agentProfileSchema, nativeReasoningControl, normalizeInputParts } from "@tangle-network/agent-interface";
6
+ import { HarnessSessionContractError, assertArgvHonoursSessionContract, assertFlagsHonourSessionContract, assertStoreLocationHonoursSessionContract, compileSessionStoreGlob } from "@tangle-network/cli-agent-registry/session-contract";
7
+ import { MAX_SECRET_PROFILE_REPLAY_CHARS, RESTORED_SESSION_CONTEXT_PREAMBLE, RESTORED_SESSION_CONTEXT_TAG, buildCompleteForkContextBlock, buildForkContextBlock, createMessageStore, createSessionStore, forkSessionWithMessages, readProviderMessageIds } from "@tangle-network/sdk-session-persistence";
8
+ import { mkdir, open, readdir, writeFile } from "node:fs/promises";
9
+ import path, { basename, dirname, isAbsolute, join, normalize, resolve } from "node:path";
10
+ import * as fs$1 from "node:fs";
11
+ import fs, { constants, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
12
+ import { execFile } from "node:child_process";
13
+ import { isDeepStrictEqual, promisify } from "node:util";
14
+ import { messageStorageDirOf } from "@tangle-network/cli-agent-registry";
15
+ import { NATIVE_SESSION_CONTRACTS, NATIVE_SESSION_CONTRACTS as NATIVE_SESSION_CONTRACTS$1, configDirHomeEnv } from "@tangle-network/cli-agent-registry/native-session-contracts";
16
+ import * as W3C from "@tangle-network/agent-trace-contract";
17
+ import { z } from "zod";
18
+ import { fileURLToPath, pathToFileURL } from "node:url";
19
+ import { DatabaseSync } from "node:sqlite";
20
+ import { StringDecoder } from "node:string_decoder";
21
+ import { DEFAULT_INTERACTION_MCP_SERVER_NAME, InteractionMcpServer, interactionMcpToolName } from "@tangle-network/agent-core/interactions/mcp";
22
+ import { isRecord, isRecord as isPlainRecord } from "@tangle-network/agent-core";
23
+ //#region src/acp-process-launch.ts
24
+ /** Spawn an ACP agent over NDJSON stdio from `env` alone, never the sidecar's own environment. The adapter owns abort, so no signal. */
25
+ function spawnAcpBridge(request) {
26
+ return spawnRpcBridge({
27
+ ...request,
28
+ inheritEnv: false
29
+ });
30
+ }
31
+ /**
32
+ * A JSON-RPC error with the cause the agent put in `data`: hermes sends
33
+ * `data.details`, codex-acp `data.message`.
34
+ */
35
+ function jsonRpcErrorText(err) {
36
+ const data = err.data && typeof err.data === "object" ? err.data : void 0;
37
+ const cause = [data?.details, data?.message].find((value) => typeof value === "string" && value.length > 0);
38
+ return cause && cause !== err.message ? `${err.message}: ${cause}` : err.message;
39
+ }
40
+ /**
41
+ * An ACP process env: the spec's keys with the agent environment over them,
42
+ * unset keys dropped. The agent environment is the whole inherited part.
43
+ */
44
+ function withAgentEnvironment(env, agent) {
45
+ const merged = {};
46
+ for (const [key, value] of Object.entries(env ?? {})) if (value !== void 0) merged[key] = value;
47
+ for (const [key, value] of Object.entries(agent)) if (value !== void 0) merged[key] = value;
48
+ return merged;
49
+ }
50
+ //#endregion
51
+ //#region src/acp-protocol.ts
52
+ /** The ACP protocol version this client speaks, and the oldest it accepts. */
53
+ const ACP_PROTOCOL_VERSION = 1;
54
+ const ACP_MIN_PROTOCOL_VERSION = 1;
55
+ /** Text carried by ACP content blocks, including tool-call content. */
56
+ function acpContentText(content) {
57
+ if (!content) return "";
58
+ const blocks = Array.isArray(content) ? content : [content];
59
+ const parts = [];
60
+ for (const block of blocks) {
61
+ if (!block || typeof block !== "object") continue;
62
+ if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
63
+ else if (block.type === "content" && block.content?.type === "text" && typeof block.content.text === "string") parts.push(block.content.text);
64
+ }
65
+ return parts.join("");
66
+ }
67
+ //#endregion
68
+ //#region src/reasoning-parts.ts
69
+ /**
70
+ * Assigns a stable part id and an append-only delta to a reasoning block
71
+ * emission, absorbing provider replay/growth patterns:
72
+ *
73
+ * - text exactly matches an already-tracked block → `null` (duplicate
74
+ * delivery, e.g. a final assistant message replaying streamed content —
75
+ * emit nothing). Byte-identical distinct blocks therefore dedupe to one
76
+ * part; safer than duplicate-part spam.
77
+ * - text extends the most recent block → same part id, delta = the suffix
78
+ * (cumulative re-emission of a growing block).
79
+ * - otherwise → a new block: fresh part id, delta = the full text, so
80
+ * consumers can be pure appenders (prev text for a fresh id is empty).
81
+ */
82
+ function trackReasoningBlock(state, blockText) {
83
+ if (blockText === "") return null;
84
+ state.reasoningParts ??= [];
85
+ const parts = state.reasoningParts;
86
+ if (parts.some((p) => p.text === blockText)) return null;
87
+ const last = parts[parts.length - 1];
88
+ if (last && last.text !== "" && blockText.startsWith(last.text)) {
89
+ const delta = blockText.slice(last.text.length);
90
+ last.text = blockText;
91
+ return {
92
+ partId: last.id,
93
+ text: blockText,
94
+ delta,
95
+ isNewBlock: false
96
+ };
97
+ }
98
+ const id = randomUUID();
99
+ parts.push({
100
+ id,
101
+ text: blockText
102
+ });
103
+ return {
104
+ partId: id,
105
+ text: blockText,
106
+ delta: blockText,
107
+ isNewBlock: true
108
+ };
109
+ }
110
+ //#endregion
111
+ //#region src/stream-parts.ts
112
+ /**
113
+ * The text, reasoning and tool part emissions every stream-json harness
114
+ * shares. A provider's event mapper decodes its own wire shape, then calls
115
+ * these so part ids, buffers, tool timing and processing phases stay one
116
+ * implementation across claude-code, codex, kimi, amp and factory-droids.
117
+ */
118
+ /**
119
+ * Records the CLI's native session id on the turn and announces it: the
120
+ * generic `session.updated` plus the provider-attributed observation the
121
+ * turn observer keys evidence on.
122
+ */
123
+ function sessionObservedEvents(state, provider, sessionId) {
124
+ state.sessionId = sessionId;
125
+ return [{
126
+ type: "session.updated",
127
+ sessionId
128
+ }, {
129
+ type: "native.session.observed",
130
+ provider,
131
+ nativeSessionId: sessionId
132
+ }];
133
+ }
134
+ /** Correlation fields every emitted part carries. */
135
+ function streamPartBase(state, partId) {
136
+ return {
137
+ id: partId ?? randomUUID(),
138
+ sessionID: state.sessionId ?? "",
139
+ messageID: state.messageId ?? ""
140
+ };
141
+ }
142
+ function textPartEvent(state, delta) {
143
+ state.textPartId ??= randomUUID();
144
+ return {
145
+ type: "message.part.updated",
146
+ part: {
147
+ ...streamPartBase(state, state.textPartId),
148
+ type: "text",
149
+ text: state.textBuffer
150
+ },
151
+ delta
152
+ };
153
+ }
154
+ /**
155
+ * Appends a text delta to the turn's single text part. The first text of a
156
+ * turn also signals the `generating` phase.
157
+ */
158
+ function textDeltaEvents(state, delta) {
159
+ const first = state.textBuffer.length === 0;
160
+ state.textBuffer += delta;
161
+ const update = textPartEvent(state, delta);
162
+ return first ? [{
163
+ type: "model-processing",
164
+ phase: "generating"
165
+ }, update] : [update];
166
+ }
167
+ /** The whole reply as one text part, for harnesses that do not stream. */
168
+ function finalTextEvent(state, text) {
169
+ state.textBuffer = text;
170
+ return textPartEvent(state, text);
171
+ }
172
+ /**
173
+ * Applies a cumulative text frame (the whole assistant text so far). Only the
174
+ * suffix past the buffer is emitted; a frame that does not extend the buffer
175
+ * replaces it and is emitted whole.
176
+ */
177
+ function textSnapshotEvents(state, text) {
178
+ const delta = text.startsWith(state.textBuffer) ? text.slice(state.textBuffer.length) : text;
179
+ if (!delta) return [];
180
+ state.textBuffer = text;
181
+ return [textPartEvent(state, delta)];
182
+ }
183
+ /**
184
+ * Records a thinking block under a stable part id (see trackReasoningBlock).
185
+ * With `signalPhase`, the turn's first block also signals `thinking`.
186
+ */
187
+ function reasoningEvents(state, thinking, options) {
188
+ const tracked = trackReasoningBlock(state, thinking);
189
+ if (!tracked) return [];
190
+ if (tracked.isNewBlock) state.reasoningBuffer.push(tracked.text);
191
+ else state.reasoningBuffer[state.reasoningBuffer.length - 1] = tracked.text;
192
+ const update = {
193
+ type: "message.part.updated",
194
+ part: {
195
+ ...streamPartBase(state, tracked.partId),
196
+ type: "reasoning",
197
+ text: tracked.text
198
+ },
199
+ delta: tracked.delta
200
+ };
201
+ return options.signalPhase && tracked.isNewBlock && state.reasoningBuffer.length === 1 ? [{
202
+ type: "model-processing",
203
+ phase: "thinking"
204
+ }, update] : [update];
205
+ }
206
+ /** Registers a running tool call and emits its running part. */
207
+ function toolStartEvents(state, call) {
208
+ const now = Date.now();
209
+ state.toolCalls.set(call.id, {
210
+ id: call.id,
211
+ name: call.name,
212
+ input: call.input,
213
+ startTime: now,
214
+ status: "running"
215
+ });
216
+ const toolState = {
217
+ status: "running",
218
+ input: call.input ?? {},
219
+ time: { start: now }
220
+ };
221
+ return [{
222
+ type: "message.part.updated",
223
+ part: {
224
+ ...streamPartBase(state, call.id),
225
+ type: "tool",
226
+ callID: call.id,
227
+ tool: call.name,
228
+ state: toolState
229
+ }
230
+ }];
231
+ }
232
+ /**
233
+ * Completes a tracked tool call. A successful result emits `afterSuccess`
234
+ * (file parts the provider detected) and then the `tool-result` phase. A
235
+ * result for an untracked call is a warning.
236
+ */
237
+ function toolResultEvents(state, result) {
238
+ const toolCall = state.toolCalls.get(result.id);
239
+ if (!toolCall) return [{
240
+ type: "warning",
241
+ code: "UNKNOWN_TOOL_RESULT",
242
+ message: `Received result for unknown tool call: ${result.id}`
243
+ }];
244
+ toolCall.status = result.isError ? "failed" : "completed";
245
+ toolCall.output = result.content;
246
+ const now = Date.now();
247
+ const input = toolCall.input ?? {};
248
+ const time = {
249
+ start: toolCall.startTime,
250
+ end: now
251
+ };
252
+ const metadata = result.metadata ? { metadata: result.metadata } : {};
253
+ const toolState = result.isError ? {
254
+ status: "failed",
255
+ input,
256
+ error: typeof result.content === "string" ? result.content : void 0,
257
+ output: result.content,
258
+ ...metadata,
259
+ time
260
+ } : {
261
+ status: "completed",
262
+ input,
263
+ output: result.content,
264
+ ...metadata,
265
+ time
266
+ };
267
+ const events = [{
268
+ type: "message.part.updated",
269
+ part: {
270
+ ...streamPartBase(state, result.id),
271
+ type: "tool",
272
+ callID: result.id,
273
+ tool: toolCall.name,
274
+ state: toolState
275
+ }
276
+ }];
277
+ if (!result.isError) events.push(...result.afterSuccess?.(toolCall) ?? [], {
278
+ type: "model-processing",
279
+ phase: "tool-result",
280
+ toolName: toolCall.name,
281
+ elapsedMs: now - toolCall.startTime
282
+ });
283
+ return events;
284
+ }
285
+ //#endregion
286
+ //#region src/acp-session-updates.ts
287
+ /** An agent message or thought chunk: text extends the message part, thought joins reasoning. */
288
+ function acpMessageChunkEvents(kind, update, state) {
289
+ const text = acpContentText(update.content);
290
+ if (!text) return [];
291
+ if (kind === "agent_message_chunk") return textDeltaEvents(state, text);
292
+ state.reasoningBuffer.push(text);
293
+ return [];
294
+ }
295
+ /** A new tool call: recorded as running and reported as its tool part. */
296
+ function acpToolCallEvents(update, state) {
297
+ const call = update;
298
+ const name = call.title ?? call.kind ?? "tool";
299
+ return toolStartEvents(state, {
300
+ id: call.toolCallId,
301
+ name,
302
+ input: call.rawInput
303
+ });
304
+ }
305
+ /**
306
+ * Fold one tool_call_update into the call's record. Tool output arrives as
307
+ * content blocks; rawOutput is the fallback. Returns the update's text output.
308
+ */
309
+ function applyAcpToolCallUpdate(existing, mapped, change) {
310
+ const outputText = acpContentText(change.content);
311
+ if (mapped === "running" && outputText) existing.output = typeof existing.output === "string" ? existing.output + outputText : outputText;
312
+ existing.status = mapped;
313
+ if (mapped === "completed" || mapped === "failed") {
314
+ if (outputText) existing.output = typeof existing.output === "string" && existing.output.length > 0 ? existing.output + outputText : outputText;
315
+ else if (change.rawOutput !== void 0) existing.output = change.rawOutput;
316
+ }
317
+ return outputText;
318
+ }
319
+ /** The events a call's current record reports: its settled result, or its open part. */
320
+ function acpToolCallUpdateEvents(existing, state) {
321
+ if (existing.status === "completed" || existing.status === "failed") return toolResultEvents(state, {
322
+ id: existing.id,
323
+ content: existing.output,
324
+ isError: existing.status === "failed"
325
+ });
326
+ const input = existing.input ?? {};
327
+ return [{
328
+ type: "message.part.updated",
329
+ part: {
330
+ ...streamPartBase(state, existing.id),
331
+ type: "tool",
332
+ tool: existing.name,
333
+ callID: existing.id,
334
+ state: existing.status === "running" ? {
335
+ status: "running",
336
+ input,
337
+ time: { start: existing.startTime }
338
+ } : {
339
+ status: "pending",
340
+ input
341
+ }
342
+ }
343
+ }];
344
+ }
345
+ /**
346
+ * Plan entries are the agent's step list. Fold their text into the
347
+ * reasoning record, bounded per notification and per turn so a
348
+ * misbehaving agent cannot grow turn memory or storage without limit.
349
+ */
350
+ function acpPlanEvents(update, state, provider) {
351
+ const MAX_ENTRIES_PER_UPDATE = 256;
352
+ const MAX_REASONING_ENTRIES_PER_TURN = 1024;
353
+ const MAX_ENTRY_LENGTH = 8 * 1024;
354
+ const raw = update.entries;
355
+ const entries = (Array.isArray(raw) ? raw : []).slice(0, MAX_ENTRIES_PER_UPDATE);
356
+ for (const entry of entries) {
357
+ if (state.reasoningBuffer.length >= MAX_REASONING_ENTRIES_PER_TURN) break;
358
+ if (typeof entry.content === "string" && entry.content.length > 0) state.reasoningBuffer.push(entry.content.slice(0, MAX_ENTRY_LENGTH));
359
+ }
360
+ return [{
361
+ type: "raw",
362
+ backend: provider,
363
+ event: {
364
+ sessionUpdate: "plan",
365
+ entries
366
+ }
367
+ }];
368
+ }
369
+ //#endregion
370
+ //#region src/provider-config.ts
371
+ /**
372
+ * Render a value as a digest that does not change with key order. The type
373
+ * parameter keeps the caller's own type at the call site, so the digest of a
374
+ * configuration cannot silently accept a value of an unrelated shape.
375
+ */
376
+ function stableSerialize(value) {
377
+ if (value === null) return "null";
378
+ if (value === void 0) return "undefined";
379
+ if (typeof value === "string") return JSON.stringify(value);
380
+ if (typeof value === "number") {
381
+ if (Number.isNaN(value)) return "number:NaN";
382
+ if (Object.is(value, -0)) return "number:-0";
383
+ return `number:${value}`;
384
+ }
385
+ if (typeof value === "boolean") return value ? "true" : "false";
386
+ if (typeof value === "bigint") return `bigint:${value.toString()}`;
387
+ if (Array.isArray(value)) return `[${value.map((entry) => stableSerialize(entry)).join(",")}]`;
388
+ if (typeof value === "object") return `{${Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${stableSerialize(entry)}`).join(",")}}`;
389
+ return `${typeof value}:${String(value)}`;
390
+ }
391
+ function cloneConfig(config) {
392
+ return structuredClone(config);
393
+ }
394
+ function isInteractionObject(value) {
395
+ return typeof value === "object" && value !== null && !Array.isArray(value);
396
+ }
397
+ function mergeCliProviderConfig(current, next) {
398
+ const merged = {
399
+ ...current,
400
+ ...next
401
+ };
402
+ const currentInteractions = current.interactions;
403
+ const nextInteractions = next.interactions;
404
+ if (isInteractionObject(currentInteractions) && isInteractionObject(nextInteractions)) merged.interactions = {
405
+ ...currentInteractions,
406
+ ...nextInteractions
407
+ };
408
+ return merged;
409
+ }
410
+ //#endregion
411
+ //#region src/adapter-initialization.ts
412
+ /**
413
+ * The in-flight and settled initialization of one adapter, and the pending
414
+ * shutdown: which `initialize()` call is current, which configuration it
415
+ * carries, and the generation that a `shutdown()` bumps to cancel it.
416
+ */
417
+ var InitializationTracker = class {
418
+ generation = 0;
419
+ promise = null;
420
+ configKey = null;
421
+ cancellation = null;
422
+ initializedConfigKey = null;
423
+ shutdownPromise;
424
+ providerTails = /* @__PURE__ */ new Set();
425
+ constructor(provider) {
426
+ this.provider = provider;
427
+ }
428
+ assertCurrent(generation, cancellation) {
429
+ if (generation !== this.generation || cancellation.cancelled) throw new Error(`${this.provider()} provider initialization cancelled by shutdown`);
430
+ }
431
+ /** Forget `promise` once it settles, unless a newer one replaced it. */
432
+ clear(promise) {
433
+ if (this.promise !== promise) return;
434
+ this.promise = null;
435
+ this.configKey = null;
436
+ this.cancellation = null;
437
+ }
438
+ /** Cancel the in-flight initialization and invalidate its generation. */
439
+ cancel() {
440
+ this.generation += 1;
441
+ if (this.cancellation) this.cancellation.cancelled = true;
442
+ this.promise = null;
443
+ this.configKey = null;
444
+ this.cancellation = null;
445
+ this.initializedConfigKey = null;
446
+ }
447
+ };
448
+ /**
449
+ * The adapter configuration an `initialize()` call produces: the initial
450
+ * configuration overlaid with the host's runtime values. Throws when a
451
+ * trusted-path or identity value is malformed.
452
+ */
453
+ function overlayProviderConfig(provider, initialConfig, config) {
454
+ const nextConfig = cloneConfig(initialConfig);
455
+ const runtimeAttachments = config.runtimeAttachments;
456
+ if (runtimeAttachments !== void 0) nextConfig.runtimeAttachments = structuredClone(runtimeAttachments);
457
+ const runtimeSecrets = config.runtimeSecrets;
458
+ if (runtimeSecrets !== void 0) nextConfig.runtimeSecrets = { ...runtimeSecrets };
459
+ const runtimeHome = config.runtimeHome;
460
+ if (runtimeHome !== void 0) {
461
+ if (typeof runtimeHome !== "string" || !path.isAbsolute(runtimeHome)) throw new Error(`${provider} runtimeHome must be an absolute filesystem path`);
462
+ nextConfig.runtimeHome = runtimeHome;
463
+ }
464
+ const agentHome = config.agentHome;
465
+ if (agentHome !== void 0) {
466
+ if (typeof agentHome !== "string" || !path.isAbsolute(agentHome)) throw new Error(`${provider} agentHome must be an absolute filesystem path`);
467
+ nextConfig.agentHome = agentHome;
468
+ }
469
+ const subprocessUser = config.subprocessUser;
470
+ if (subprocessUser !== void 0) {
471
+ if (process.getuid?.() !== 0 || !Number.isSafeInteger(subprocessUser.uid) || subprocessUser.uid <= 0 || !Number.isSafeInteger(subprocessUser.gid) || subprocessUser.gid <= 0) throw new Error(`${provider}: invalid trusted subprocess identity`);
472
+ nextConfig.subprocessUser = { ...subprocessUser };
473
+ if (config.stableIsolatedIdentity === true) nextConfig.stableIsolatedIdentity = true;
474
+ }
475
+ const publicProfileRevisionRoot = config.publicProfileRevisionRoot;
476
+ if (publicProfileRevisionRoot !== void 0) {
477
+ if (typeof publicProfileRevisionRoot !== "string" || !path.isAbsolute(publicProfileRevisionRoot)) throw new Error(`${provider} publicProfileRevisionRoot must be an absolute filesystem path`);
478
+ nextConfig.publicProfileRevisionRoot = publicProfileRevisionRoot;
479
+ }
480
+ const privateRevision = config;
481
+ if (privateRevision.secretProfileRevisionRoot !== void 0 || privateRevision.secretProfileNamingKeyPath !== void 0) {
482
+ if (!subprocessUser || publicProfileRevisionRoot !== void 0 || !privateRevision.secretProfileRevisionRoot || !path.isAbsolute(privateRevision.secretProfileRevisionRoot) || !privateRevision.secretProfileNamingKeyPath || !path.isAbsolute(privateRevision.secretProfileNamingKeyPath)) throw new Error(`${provider}: invalid secret profile revision root`);
483
+ nextConfig.secretProfileRevisionRoot = privateRevision.secretProfileRevisionRoot;
484
+ nextConfig.secretProfileNamingKeyPath = privateRevision.secretProfileNamingKeyPath;
485
+ }
486
+ const sandboxedRuntime = config.sandboxedRuntime;
487
+ if (sandboxedRuntime !== void 0) nextConfig.sandboxedRuntime = sandboxedRuntime === true;
488
+ const nativeAuthEnv = config.nativeAuthEnv;
489
+ if (nativeAuthEnv !== void 0) {
490
+ nextConfig.nativeAuthEnv = { ...nativeAuthEnv };
491
+ nextConfig.nativeAuthSecretEnv = config.nativeAuthSecretEnv;
492
+ }
493
+ if (config.model?.model !== void 0) nextConfig.model = config.model.model;
494
+ if (config.model?.provider !== void 0) nextConfig.provider = config.model.provider;
495
+ if (config.model?.apiKey !== void 0) nextConfig.apiKey = config.model.apiKey;
496
+ if (config.model?.baseUrl !== void 0) nextConfig.baseUrl = config.model.baseUrl;
497
+ if (config.model?.authMode !== void 0) nextConfig.authMode = config.model.authMode;
498
+ if (config.model?.authFiles !== void 0) nextConfig.authFiles = config.model.authFiles.map((file) => ({ ...file }));
499
+ return nextConfig;
500
+ }
501
+ //#endregion
502
+ //#region src/execution-lifecycle.ts
503
+ const lifecycles = /* @__PURE__ */ new WeakMap();
504
+ function executionLifecycle(owner) {
505
+ let lifecycle = lifecycles.get(owner);
506
+ if (!lifecycle) {
507
+ lifecycle = {
508
+ inFlightExecutions: /* @__PURE__ */ new Set(),
509
+ shuttingDown: false
510
+ };
511
+ lifecycles.set(owner, lifecycle);
512
+ }
513
+ return lifecycle;
514
+ }
515
+ function beginExecution(owner, callerAbortSignal) {
516
+ const lifecycle = executionLifecycle(owner);
517
+ if (lifecycle.shuttingDown) throw new Error(`${owner.provider} provider is shutting down`);
518
+ let resolveSettled;
519
+ const execution = {
520
+ controller: new AbortController(),
521
+ settled: new Promise((resolve) => {
522
+ resolveSettled = resolve;
523
+ }),
524
+ resolveSettled: () => resolveSettled()
525
+ };
526
+ lifecycle.inFlightExecutions.add(execution);
527
+ if (callerAbortSignal) {
528
+ const abortFromCaller = () => execution.controller.abort();
529
+ execution.callerAbortSignal = callerAbortSignal;
530
+ execution.callerAbortListener = abortFromCaller;
531
+ if (callerAbortSignal.aborted) execution.controller.abort();
532
+ else callerAbortSignal.addEventListener("abort", abortFromCaller, { once: true });
533
+ }
534
+ return execution;
535
+ }
536
+ function finishExecution(owner, execution) {
537
+ executionLifecycle(owner).inFlightExecutions.delete(execution);
538
+ if (execution.callerAbortSignal && execution.callerAbortListener) execution.callerAbortSignal.removeEventListener("abort", execution.callerAbortListener);
539
+ execution.resolveSettled();
540
+ }
541
+ //#endregion
542
+ //#region src/writable-home.ts
543
+ /**
544
+ * The nearest ancestor of `target` that exists, or null when even the root of
545
+ * the path is absent. A directory that does not exist yet is usable only if
546
+ * something above it can be written to, so the writability question always
547
+ * resolves against an ancestor that is really there.
548
+ */
549
+ function nearestExistingAncestor(target) {
550
+ let current = path.resolve(target);
551
+ for (;;) {
552
+ if (fs.existsSync(current)) return current;
553
+ const parent = path.dirname(current);
554
+ if (parent === current) return null;
555
+ current = parent;
556
+ }
557
+ }
558
+ function isWritableTarget(target) {
559
+ const ancestor = nearestExistingAncestor(target);
560
+ if (!ancestor) return false;
561
+ try {
562
+ fs.accessSync(ancestor, fs.constants.W_OK);
563
+ return true;
564
+ } catch {
565
+ return false;
566
+ }
567
+ }
568
+ /** `/root` belongs to the calling process, never to the agent subprocess. */
569
+ const READ_ONLY_ROOT_HOME = "/root";
570
+ function isRootHomePath(value) {
571
+ return value === READ_ONLY_ROOT_HOME || value.startsWith(`${READ_ONLY_ROOT_HOME}/`);
572
+ }
573
+ /**
574
+ * Whether an inherited path must not reach the CLI, given whether it can be
575
+ * written. Two independent reasons, kept apart from the filesystem probe so
576
+ * each is decidable on its own:
577
+ *
578
+ * - It sits under `/root`. The agent subprocess runs as its own user, so
579
+ * that path is wrong even while the calling process can write there.
580
+ * - Nothing can create it. A read-only mount refuses `mkdir` for every user,
581
+ * including root.
582
+ */
583
+ function shouldReplacePath(value, writable) {
584
+ return isRootHomePath(value) || !writable;
585
+ }
586
+ function mustReplace(value) {
587
+ return shouldReplacePath(value, isWritableTarget(value));
588
+ }
589
+ /**
590
+ * Point `HOME` and the XDG directories at somewhere a CLI can actually write.
591
+ *
592
+ * An inherited value is kept whenever it is usable, so an operator can still
593
+ * aim a cache at a large volume. An unusable value is replaced by the
594
+ * equivalent path under the writable home, because keeping it only moves the
595
+ * failure into the CLI, where it surfaces as a run that emits nothing.
596
+ */
597
+ function applyWritableHomeDefaults(env, workspaceRoot) {
598
+ const next = { ...env };
599
+ const writableHome = next.SIDECAR_AGENT_HOME?.trim() || next.AGENT_WORKSPACE_ROOT?.trim() || workspaceRoot;
600
+ const currentHome = next.HOME?.trim();
601
+ const home = currentHome && !mustReplace(currentHome) ? currentHome : writableHome;
602
+ for (const [key, fallback] of Object.entries(homeXdgEnv(home))) {
603
+ const current = next[key]?.trim();
604
+ if (key === "HOME" || !current || mustReplace(current)) next[key] = fallback;
605
+ }
606
+ return next;
607
+ }
608
+ /** HOME and the XDG base directories a CLI resolves under it. */
609
+ function homeXdgEnv(home) {
610
+ return {
611
+ HOME: home,
612
+ XDG_CONFIG_HOME: path.join(home, ".config"),
613
+ XDG_CACHE_HOME: path.join(home, ".cache"),
614
+ XDG_DATA_HOME: path.join(home, ".local", "share"),
615
+ XDG_STATE_HOME: path.join(home, ".local", "state")
616
+ };
617
+ }
618
+ //#endregion
619
+ //#region src/adapter-lifecycle.ts
620
+ const DEFAULT_CONTEXT_WINDOW = 2e5;
621
+ const execFileAsync = promisify(execFile);
622
+ /** Console-based logger fallback. */
623
+ const consoleLogger = {
624
+ debug: (msg, meta) => console.debug(`[cli-provider] ${msg}`, meta ?? ""),
625
+ info: (msg, meta) => console.info(`[cli-provider] ${msg}`, meta ?? ""),
626
+ warn: (msg, meta) => console.warn(`[cli-provider] ${msg}`, meta ?? ""),
627
+ error: (msg, meta) => console.error(`[cli-provider] ${msg}`, meta ?? "")
628
+ };
629
+ /** Owns configuration, initialization, cancellation, and subprocess identity. */
630
+ var CliAdapterLifecycle = class {
631
+ /** The message log directory when the caller names none. */
632
+ defaultStorageDir;
633
+ inheritProcessEnv;
634
+ config;
635
+ initialConfig;
636
+ logger;
637
+ sessionStore = null;
638
+ messageStore = null;
639
+ storagePath;
640
+ _storageDir;
641
+ agentEnvironment;
642
+ /** The consumer's executor for turn processes; a local spawn when absent. */
643
+ launcher;
644
+ /** Whether a turn's leftover process-group members are stopped at exit. */
645
+ ownProcessGroup;
646
+ initialized = false;
647
+ activeProcesses = /* @__PURE__ */ new Set();
648
+ initialization = new InitializationTracker(() => this.provider);
649
+ constructor(options = {}) {
650
+ executionLifecycle(this);
651
+ this.initialConfig = cloneConfig(options.config ?? {});
652
+ this.config = cloneConfig(this.initialConfig);
653
+ this.logger = options.logger ?? consoleLogger;
654
+ this.storagePath = options.storagePath ?? process.cwd();
655
+ this._storageDir = options.storageDir;
656
+ this.agentEnvironment = options.agentEnvironment;
657
+ this.inheritProcessEnv = options.agentEnvironment === void 0;
658
+ this.launcher = options.launcher;
659
+ this.ownProcessGroup = options.ownProcessGroup ?? false;
660
+ }
661
+ get storageDir() {
662
+ return this._storageDir ?? this.defaultStorageDir ?? messageStorageDirOf(this.provider);
663
+ }
664
+ /** Bind a live interaction to the exact provider execution that originated it. */
665
+ interactionExecutionBinding(input, sessionId) {
666
+ if (!input.interactionBinding) throw new Error(`${this.provider} interactions require AgentExecutionInput.interactionBinding`);
667
+ const binding = InteractionExecutionBindingSchema.parse(input.interactionBinding);
668
+ if (binding.provider !== this.provider) throw new Error(`Interaction binding provider "${binding.provider}" does not match adapter "${this.provider}"`);
669
+ if (binding.sessionId !== sessionId) throw new Error(`Interaction binding session "${binding.sessionId}" does not match adapter session "${sessionId}"`);
670
+ return binding;
671
+ }
672
+ /** The uid/gid every CLI process this adapter spawns runs as, or undefined when the host process is already unprivileged. */
673
+ subprocessIdentity() {
674
+ const assigned = this.config.subprocessUser;
675
+ if (assigned) {
676
+ if (process.getuid?.() !== 0 || !Number.isSafeInteger(assigned.uid) || assigned.uid <= 0 || !Number.isSafeInteger(assigned.gid) || assigned.gid <= 0) throw new Error(`${this.provider}: invalid trusted subprocess identity`);
677
+ return assigned;
678
+ }
679
+ if (process.getuid?.() !== 0) return void 0;
680
+ const rawUid = process.env.AGENT_SUBPROCESS_UID ?? "1000";
681
+ const rawGid = process.env.AGENT_SUBPROCESS_GID ?? "1000";
682
+ const uid = Number(rawUid);
683
+ const gid = Number(rawGid);
684
+ if (!Number.isInteger(uid) || uid < 0 || !Number.isInteger(gid) || gid < 0) throw new Error(`${this.provider}: AGENT_SUBPROCESS_UID/AGENT_SUBPROCESS_GID must be non-negative integers (got ${rawUid}/${rawGid})`);
685
+ return {
686
+ uid,
687
+ gid
688
+ };
689
+ }
690
+ /** `subprocessIdentity` shaped for `spawn`'s options bag. */
691
+ subprocessSpawnOptions() {
692
+ return this.subprocessIdentity() ?? {};
693
+ }
694
+ /** Run a one-shot CLI command under this adapter's subprocess identity. */
695
+ runCli(config) {
696
+ return execCli({
697
+ ...config,
698
+ inheritEnv: this.inheritProcessEnv && config.inheritEnv !== false,
699
+ rawCapturePath: void 0,
700
+ ...this.config.subprocessUser ? { isolatedIdentity: this.config.subprocessUser } : {},
701
+ spawnOptions: {
702
+ ...this.subprocessSpawnOptions(),
703
+ ...config.spawnOptions
704
+ }
705
+ });
706
+ }
707
+ /** Run a builtin-only Node script under this adapter's subprocess identity and return its stdout. */
708
+ async runNodeAsSubprocess(script, args, input = "") {
709
+ try {
710
+ const pending = execFileAsync(process.execPath, [
711
+ "-e",
712
+ script,
713
+ "--",
714
+ ...args
715
+ ], {
716
+ ...this.subprocessSpawnOptions(),
717
+ cwd: "/",
718
+ env: {},
719
+ timeout: 3e4,
720
+ maxBuffer: 64 * 1024 * 1024
721
+ });
722
+ pending.child.stdin?.on("error", () => {});
723
+ pending.child.stdin?.end(input);
724
+ return (await pending).stdout;
725
+ } catch (error) {
726
+ const stderr = String(error.stderr ?? "").trim();
727
+ throw new Error((stderr || (error instanceof Error ? error.message : String(error))).slice(-500));
728
+ }
729
+ }
730
+ getCapabilities() {
731
+ return {
732
+ streaming: true,
733
+ toolUse: true,
734
+ reasoning: true,
735
+ multimodal: false,
736
+ contextWindow: DEFAULT_CONTEXT_WINDOW
737
+ };
738
+ }
739
+ initialize(config) {
740
+ const attachments = config.runtimeAttachments === void 0 ? void 0 : AgentRuntimeAttachmentsSchema.safeParse(config.runtimeAttachments);
741
+ if (attachments && !attachments.success) return Promise.reject(/* @__PURE__ */ new Error("Invalid runtime MCP attachments"));
742
+ const capturedConfig = attachments?.success ? {
743
+ ...config,
744
+ runtimeAttachments: attachments.data
745
+ } : config;
746
+ const configKey = stableSerialize(capturedConfig);
747
+ if (this.initialization.promise) {
748
+ if (configKey === this.initialization.configKey) return this.initialization.promise;
749
+ return Promise.reject(/* @__PURE__ */ new Error(`${this.provider} provider cannot start a conflicting initialize() while initialization is in flight; concurrent calls must use the same configuration`));
750
+ }
751
+ if (this.initialized) {
752
+ if (configKey === this.initialization.initializedConfigKey) return Promise.resolve();
753
+ return Promise.reject(/* @__PURE__ */ new Error(`${this.provider} provider is already initialized with a different configuration; call shutdown() before reinitializing`));
754
+ }
755
+ const pendingShutdown = this.initialization.shutdownPromise;
756
+ const generation = this.initialization.generation;
757
+ const cancellation = { cancelled: false };
758
+ const initialization = Promise.resolve().then(async () => {
759
+ if (pendingShutdown) await pendingShutdown;
760
+ this.initialization.assertCurrent(generation, cancellation);
761
+ executionLifecycle(this).shuttingDown = false;
762
+ await this.initializeAtGeneration(capturedConfig, configKey, generation, cancellation);
763
+ });
764
+ this.initialization.promise = initialization;
765
+ this.initialization.configKey = configKey;
766
+ this.initialization.cancellation = cancellation;
767
+ initialization.then(() => this.initialization.clear(initialization), () => this.initialization.clear(initialization));
768
+ return initialization;
769
+ }
770
+ async initializeAtGeneration(config, configKey, generation, cancellation) {
771
+ this.initialization.assertCurrent(generation, cancellation);
772
+ const nextStoragePath = config.workspace?.rootPath || this.storagePath;
773
+ const nextConfig = overlayProviderConfig(this.provider, this.initialConfig, config);
774
+ const configuredProfile = config.profile ?? nextConfig.profile;
775
+ if (configuredProfile !== void 0) {
776
+ nextConfig.profile = await resolveAgentProfileResources(agentProfileSchema.parse(configuredProfile));
777
+ this.initialization.assertCurrent(generation, cancellation);
778
+ }
779
+ const contract = this.sessionContract();
780
+ if (contract) assertFlagsHonourSessionContract(this.provider, contract, nextConfig.extraFlags ?? [], "extraFlags");
781
+ const persistenceConfig = {
782
+ storagePath: nextStoragePath,
783
+ storageDir: this.storageDir
784
+ };
785
+ const nextSessionStore = createSessionStore(persistenceConfig);
786
+ const nextMessageStore = createMessageStore(persistenceConfig);
787
+ this.initialization.assertCurrent(generation, cancellation);
788
+ this.storagePath = nextStoragePath;
789
+ this.config = nextConfig;
790
+ this.sessionStore = nextSessionStore;
791
+ this.messageStore = nextMessageStore;
792
+ const providerTail = Promise.resolve().then(() => this.afterInitialize(config));
793
+ this.initialization.providerTails.add(providerTail);
794
+ try {
795
+ await providerTail;
796
+ this.initialization.assertCurrent(generation, cancellation);
797
+ this.initialized = true;
798
+ this.initialization.initializedConfigKey = configKey;
799
+ } catch (error) {
800
+ if (generation === this.initialization.generation) {
801
+ this.config = cloneConfig(this.initialConfig);
802
+ this.sessionStore = null;
803
+ this.messageStore = null;
804
+ this.initialized = false;
805
+ this.initialization.initializedConfigKey = null;
806
+ }
807
+ throw error;
808
+ } finally {
809
+ this.initialization.providerTails.delete(providerTail);
810
+ }
811
+ this.logger.info(`${this.provider} provider initialized`, {
812
+ storagePath: this.storagePath,
813
+ storageDir: this.storageDir,
814
+ model: this.config.model
815
+ });
816
+ }
817
+ /** Auth/config files the adapter needs on disk that do NOT arrive through `config.model.authFiles` — e.g. */
818
+ getManagedAuthFiles() {
819
+ return [];
820
+ }
821
+ /** Provider-specific initialization runs inside the tracked lifecycle. */
822
+ async afterInitialize(_config) {}
823
+ /** The harness session contract this adapter honours (cli-agent-registry NATIVE_SESSION_CONTRACTS), or null for a provider that declares none. */
824
+ sessionContract() {
825
+ return NATIVE_SESSION_CONTRACTS[this.provider] ?? null;
826
+ }
827
+ async withExecution(signal, run) {
828
+ const execution = beginExecution(this, signal);
829
+ try {
830
+ return await run(execution.controller.signal);
831
+ } catch (error) {
832
+ execution.controller.abort();
833
+ throw error;
834
+ } finally {
835
+ finishExecution(this, execution);
836
+ }
837
+ }
838
+ async shutdown() {
839
+ this.initialization.cancel();
840
+ this.initialized = false;
841
+ executionLifecycle(this).shuttingDown = true;
842
+ this.config = cloneConfig(this.initialConfig);
843
+ if (this.initialization.shutdownPromise) return this.initialization.shutdownPromise;
844
+ const executions = [...executionLifecycle(this).inFlightExecutions];
845
+ const processes = [...this.activeProcesses];
846
+ const providerInitializationTails = [...this.initialization.providerTails];
847
+ for (const execution of executions) execution.controller.abort();
848
+ const trackedShutdown = (async () => {
849
+ await Promise.allSettled([
850
+ ...providerInitializationTails,
851
+ ...processes.map((process) => terminateTurnProcess(process, this.activeProcesses)),
852
+ ...executions.map((execution) => execution.settled)
853
+ ]);
854
+ this.sessionStore = null;
855
+ this.messageStore = null;
856
+ executionLifecycle(this).shuttingDown = false;
857
+ this.logger.info(`${this.provider} provider shut down`);
858
+ })().finally(() => {
859
+ if (this.initialization.shutdownPromise === trackedShutdown) this.initialization.shutdownPromise = void 0;
860
+ });
861
+ this.initialization.shutdownPromise = trackedShutdown;
862
+ return trackedShutdown;
863
+ }
864
+ async healthCheck() {
865
+ try {
866
+ return (await this.runAuxiliaryCli(["--version"], 5e3)).exit.success;
867
+ } catch {
868
+ return false;
869
+ }
870
+ }
871
+ /** Run this CLI's own one-shot command (version, MCP) in the workspace. */
872
+ runAuxiliaryCli(args, timeoutMs) {
873
+ return this.runCli({
874
+ command: this.getCliPath(),
875
+ args,
876
+ cwd: this.storagePath,
877
+ env: this.applyWritableHomeDefaults(this.buildNativeExecutionEnvironment(this.config), this.storagePath),
878
+ inheritEnv: this.inheritProcessEnv,
879
+ timeoutMs
880
+ });
881
+ }
882
+ async updateConfig(config) {
883
+ const resolvedConfig = { ...config };
884
+ if (config.profile !== void 0) resolvedConfig.profile = await resolveAgentProfileResources(agentProfileSchema.parse(config.profile));
885
+ this.config = mergeCliProviderConfig(this.config, resolvedConfig);
886
+ return this.config;
887
+ }
888
+ /** Apply private native credentials to every CLI subprocess through one path. */
889
+ buildNativeExecutionEnvironment(config) {
890
+ const env = this.buildExecEnv(config);
891
+ if (config.nativeAuthSecretEnv) env[config.nativeAuthSecretEnv] = void 0;
892
+ return {
893
+ ...env,
894
+ ...config.nativeAuthEnv
895
+ };
896
+ }
897
+ getCliPath() {
898
+ return this.config.cliPath ?? this.defaultCliPath;
899
+ }
900
+ ensureInitialized() {
901
+ if (executionLifecycle(this).shuttingDown) throw new Error(`${this.provider} provider is shutting down`);
902
+ if (!this.initialized) throw new Error(`${this.provider} provider not initialized. Call initialize() first.`);
903
+ }
904
+ getSessionStore() {
905
+ if (!this.sessionStore) throw new Error("Session store not initialized");
906
+ return this.sessionStore;
907
+ }
908
+ getMessageStore() {
909
+ if (!this.messageStore) throw new Error("Message store not initialized");
910
+ return this.messageStore;
911
+ }
912
+ /** The agent environment the host supplied (CliProviderOptions.agentEnvironment). */
913
+ agentEnvironmentValues() {
914
+ const secretProfile = !!this.config.secretProfileRevisionRoot;
915
+ return this.agentEnvironment?.({ secretProfile }) ?? (secretProfile ? {} : { ...process.env });
916
+ }
917
+ applyWritableHomeDefaults(adapterEnv, workspaceRoot) {
918
+ const env = {
919
+ ...this.agentEnvironmentValues(),
920
+ ...adapterEnv
921
+ };
922
+ const runtimeHome = this.config.runtimeHome?.trim();
923
+ if (!runtimeHome) return applyWritableHomeDefaults(env, workspaceRoot);
924
+ const agentHome = this.config.agentHome?.trim();
925
+ const configDir = agentHome ? configDirHomeEnv(this.provider, runtimeHome, agentHome) : void 0;
926
+ if (configDir) {
927
+ const adapterStores = Object.keys(configDir).flatMap((name) => {
928
+ if (name === "HOME" || name.startsWith("XDG_")) return [];
929
+ const value = adapterEnv[name]?.trim();
930
+ return value && value !== process.env[name] ? [[name, value]] : [];
931
+ });
932
+ return applyWritableHomeDefaults({
933
+ ...env,
934
+ ...configDir,
935
+ ...Object.fromEntries(adapterStores)
936
+ }, workspaceRoot);
937
+ }
938
+ return applyWritableHomeDefaults({
939
+ ...env,
940
+ ...homeXdgEnv(runtimeHome)
941
+ }, workspaceRoot);
942
+ }
943
+ };
944
+ async function terminateTurnProcess(process, activeProcesses) {
945
+ process.kill();
946
+ const escalation = setTimeout(() => process.kill("SIGKILL"), 1e3);
947
+ escalation.unref();
948
+ try {
949
+ await process.exitPromise;
950
+ } finally {
951
+ clearTimeout(escalation);
952
+ activeProcesses.delete(process);
953
+ }
954
+ }
955
+ //#endregion
956
+ //#region src/execution-request-headers.ts
957
+ const headerValue = z.string().regex(/^[\x21-\x2b\x2d-\x7e]{1,256}$/);
958
+ const EXECUTION_ID_HEADER = "X-Tangle-Execution-ID";
959
+ const CORRELATION_FIELDS = {
960
+ "X-Tangle-Environment-ID": "environmentId",
961
+ "X-Tangle-Session-ID": "sessionId",
962
+ [EXECUTION_ID_HEADER]: "executionId"
963
+ };
964
+ /** Router column that joins a model request to the turn trace that caused it. */
965
+ const EXECUTION_TRACE_HEADER = "X-Tangle-Trace-ID";
966
+ const MANAGED_QUOTE_VERSION_HEADER = "X-Tangle-Managed-Quote-Version";
967
+ /** Every header a turn's correlation may set, lowercased. */
968
+ const EXECUTION_CORRELATION_HEADERS = [
969
+ ...Object.keys(CORRELATION_FIELDS),
970
+ EXECUTION_TRACE_HEADER,
971
+ MANAGED_QUOTE_VERSION_HEADER,
972
+ "traceparent"
973
+ ].map((name) => name.toLowerCase());
974
+ const managedQuoteVersion = z.string().regex(/^mmq_[a-f0-9]{40}$/);
975
+ /**
976
+ * Capture diagnostic identity without forwarding caller-supplied transport headers.
977
+ * `traceId` is the turn's trace id; a value no header can carry is omitted,
978
+ * because a trace is diagnostic and must never refuse the run.
979
+ */
980
+ function executionCorrelationHeaders(binding, traceId, quoteVersion) {
981
+ const quote = managedQuoteVersion.optional().parse(quoteVersion);
982
+ if (quote !== void 0 && binding === void 0) throw new Error("Managed quote requires an exact execution binding");
983
+ const trace = headerValue.safeParse(traceId);
984
+ const headers = {};
985
+ if (trace.success) {
986
+ headers[EXECUTION_TRACE_HEADER] = trace.data;
987
+ const span = W3C.toW3CSpanId(`${trace.data}:sidecar`);
988
+ headers.traceparent = `00-${W3C.toW3CTraceId(trace.data)}-${span}-01`;
989
+ }
990
+ if (binding === void 0) return trace.success ? Object.freeze(headers) : void 0;
991
+ if (quote !== void 0) headers[MANAGED_QUOTE_VERSION_HEADER] = quote;
992
+ for (const [name, field] of Object.entries(CORRELATION_FIELDS)) {
993
+ const value = headerValue.safeParse(binding[field]);
994
+ if (!value.success) throw new Error(`Execution correlation ${field} must contain 1–256 printable ASCII characters without spaces or commas`);
995
+ headers[name] = value.data;
996
+ }
997
+ return Object.freeze(headers);
998
+ }
999
+ //#endregion
1000
+ //#region src/profile-config.ts
1001
+ /**
1002
+ * Per-turn credential delivery wins over host-env resolution; the host box
1003
+ * environment stays the fallback for references the turn did not deliver.
1004
+ * With no turn secrets the host provider is returned unchanged, so the
1005
+ * overlay adds no behavior to executions that never asked for it.
1006
+ */
1007
+ function runtimeSecretsOverlay(runtimeSecrets, fallback) {
1008
+ if (!runtimeSecrets || Object.keys(runtimeSecrets).length === 0) return fallback;
1009
+ return { get: async (key) => runtimeSecrets[key] ?? await fallback.get(key) };
1010
+ }
1011
+ function hasPortableProfileControls(profile) {
1012
+ return profile.prompt !== void 0 || profile.model !== void 0 || profile.harness !== void 0 || profile.permissions !== void 0 || profile.tools !== void 0 || profile.mcp !== void 0 || profile.connections !== void 0 || profile.subagents !== void 0 || profile.resources !== void 0 || profile.hooks !== void 0 || profile.modes !== void 0 || profile.confidential !== void 0 || profile.extensions !== void 0;
1013
+ }
1014
+ function systemPromptConflict(request, profile) {
1015
+ return /* @__PURE__ */ new Error(`execution system prompt (${request.length} chars) conflicts with the materialized AgentProfile prompt.systemPrompt (${profile.length} chars); send no systemPrompt, or move the addition to appendSystemPrompt`);
1016
+ }
1017
+ /** A blank profile prompt declares none, so the request prompt stands. */
1018
+ function resolveSystemPrompt(requestPrompt, profilePrompt) {
1019
+ if (!profilePrompt?.trim()) return requestPrompt;
1020
+ if (requestPrompt && requestPrompt !== profilePrompt) throw systemPromptConflict(requestPrompt, profilePrompt);
1021
+ return profilePrompt;
1022
+ }
1023
+ function profileForExecution(profile, requestPrompt) {
1024
+ const profilePrompt = profile?.prompt?.systemPrompt;
1025
+ if (!profile || !profilePrompt?.trim() || !requestPrompt || requestPrompt === profilePrompt) return profile;
1026
+ if (!requestPrompt.startsWith(`${profilePrompt}\n\n`)) throw systemPromptConflict(requestPrompt, profilePrompt);
1027
+ return {
1028
+ ...profile,
1029
+ prompt: {
1030
+ ...profile.prompt,
1031
+ systemPrompt: requestPrompt
1032
+ }
1033
+ };
1034
+ }
1035
+ /**
1036
+ * Refuse to dispatch an execution against a model the profile did not
1037
+ * materialize — a caller must not silently get a different model than the one
1038
+ * the workspace was built for.
1039
+ *
1040
+ * The request and the profile carry the same model at different qualification
1041
+ * levels: the request is the id the caller sent, the profile default is that id
1042
+ * qualified with its provider. Both go through the one shared canonicalization
1043
+ * so only a genuinely different model is a conflict.
1044
+ */
1045
+ function resolveModel(requestModel, profileModel, configuredModel, profileProvider) {
1046
+ if (profileModel !== void 0 && requestModel !== void 0 && !modelIdsMatch(requestModel, profileModel, profileProvider)) throw new Error("execution model conflicts with the materialized AgentProfile");
1047
+ return profileModel ?? requestModel ?? configuredModel;
1048
+ }
1049
+ //#endregion
1050
+ //#region src/profile-revision-receipts.ts
1051
+ /** Receipts belong to their adapter; resolved secrets never enter them. */
1052
+ var ProfileRevisionReceipts = class {
1053
+ public = /* @__PURE__ */ new WeakMap();
1054
+ secret = /* @__PURE__ */ new WeakMap();
1055
+ };
1056
+ const receiptsByAdapter = /* @__PURE__ */ new WeakMap();
1057
+ function profileRevisionReceipts(owner) {
1058
+ let receipts = receiptsByAdapter.get(owner);
1059
+ if (!receipts) {
1060
+ receipts = new ProfileRevisionReceipts();
1061
+ receiptsByAdapter.set(owner, receipts);
1062
+ }
1063
+ return receipts;
1064
+ }
1065
+ function assertExecutionProfileRevision(owner, getConfig, input, profilePlan) {
1066
+ const config = getConfig();
1067
+ const receipts = profileRevisionReceipts(owner);
1068
+ const actualPublic = profilePlan ? receipts.public.get(profilePlan) : void 0;
1069
+ const actualSecret = profilePlan ? receipts.secret.get(profilePlan) : void 0;
1070
+ for (const [root, receipt, kind] of [[
1071
+ config.publicProfileRevisionRoot,
1072
+ actualPublic,
1073
+ "public"
1074
+ ], [
1075
+ config.secretProfileRevisionRoot,
1076
+ actualSecret,
1077
+ "secret"
1078
+ ]]) if (root && profilePlan && !receipt) throw new Error(`Provider added ${kind} profile launch controls after immutable revision preparation`);
1079
+ const { expectedSecretProfileReceipt, expectedPublicProfileRevision } = input;
1080
+ if (config.secretProfileRevisionRoot) {
1081
+ if (!expectedSecretProfileReceipt || !actualSecret || actualSecret.executorReceipt !== expectedSecretProfileReceipt) throw new Error("Secret native profile receipt mismatch before native spawn");
1082
+ } else if (expectedSecretProfileReceipt !== void 0) throw new Error("Secret native profile execution is unavailable");
1083
+ assertPublicReceipt(actualPublic, expectedPublicProfileRevision);
1084
+ }
1085
+ function assertPublicReceipt(actual, expected) {
1086
+ if (expected !== void 0 && (!actual || [
1087
+ "workspacePlanDigest",
1088
+ "materializedDigest",
1089
+ "launchDigest"
1090
+ ].some((key) => actual[key] !== expected[key]))) throw new Error("Public native profile revision mismatch before native spawn");
1091
+ }
1092
+ //#endregion
1093
+ //#region src/errors.ts
1094
+ /**
1095
+ * A session's persisted transcript exists but could not be read for replay
1096
+ * into the fresh provider session this turn starts. The turn fails instead
1097
+ * of serving an empty conversation under the same session id. The sidecar's
1098
+ * client-visible whitelist matches on `code`; the wire contract is the
1099
+ * duck-typed field, not class identity.
1100
+ */
1101
+ var SessionRehydrationError = class extends Error {
1102
+ code = "session_rehydration_failed";
1103
+ constructor(message, cause) {
1104
+ super(message, { cause });
1105
+ this.name = "SessionRehydrationError";
1106
+ }
1107
+ };
1108
+ const NOT_LAUNCHED = Symbol.for("@tangle-network/sdk-provider-cli-base.not-launched");
1109
+ /**
1110
+ * Marks a turn failure the adapter raised before it planned the launch:
1111
+ * session preparation or profile materialization refused, so no native
1112
+ * process received this turn and it left no native transcript to copy.
1113
+ * Only the adapter marks, on the error object it caught; error text never
1114
+ * does, so a provider message that reads the same is not marked.
1115
+ */
1116
+ function rejectNotLaunched(error) {
1117
+ if (error instanceof Error && !Object.isFrozen(error)) Object.defineProperty(error, NOT_LAUNCHED, { value: true });
1118
+ throw error;
1119
+ }
1120
+ /** Whether this error is one the adapter raised before launching anything. */
1121
+ function wasNeverLaunched(error) {
1122
+ return Object.getOwnPropertyDescriptor(error, NOT_LAUNCHED)?.value === true;
1123
+ }
1124
+ //#endregion
1125
+ //#region src/file-parts.ts
1126
+ /** Extension → MIME type, keyed by lowercased extension without the dot. */
1127
+ const EXTENSION_TO_MEDIA_TYPE = {
1128
+ png: "image/png",
1129
+ jpg: "image/jpeg",
1130
+ jpeg: "image/jpeg",
1131
+ gif: "image/gif",
1132
+ webp: "image/webp",
1133
+ svg: "image/svg+xml",
1134
+ pdf: "application/pdf",
1135
+ mp3: "audio/mpeg",
1136
+ wav: "audio/wav",
1137
+ mp4: "video/mp4",
1138
+ webm: "video/webm",
1139
+ txt: "text/plain",
1140
+ md: "text/markdown",
1141
+ csv: "text/csv",
1142
+ json: "application/json",
1143
+ html: "text/html",
1144
+ markdown: "text/markdown",
1145
+ htm: "text/html",
1146
+ xml: "application/xml",
1147
+ yaml: "application/yaml",
1148
+ yml: "application/yaml",
1149
+ js: "text/javascript",
1150
+ ts: "application/typescript",
1151
+ py: "text/x-python"
1152
+ };
1153
+ /**
1154
+ * Inverse of the table above, for synthesizing a filename from a media type.
1155
+ * Built once from the first extension seen per media type (e.g. image/jpeg
1156
+ * → "jpg", not "jpeg") so the mapping is stable across calls.
1157
+ */
1158
+ const MEDIA_TYPE_TO_EXTENSION = Object.entries(EXTENSION_TO_MEDIA_TYPE).reduce((acc, [ext, mediaType]) => {
1159
+ if (!(mediaType in acc)) acc[mediaType] = ext;
1160
+ return acc;
1161
+ }, {});
1162
+ function sha1Hex(value) {
1163
+ return createHash("sha1").update(value).digest("hex");
1164
+ }
1165
+ /** Infers a MIME type from a filename's extension. Unknown/no extension → undefined. */
1166
+ function inferMediaType(filename) {
1167
+ const dot = filename.lastIndexOf(".");
1168
+ if (dot < 0 || dot === filename.length - 1) return void 0;
1169
+ return EXTENSION_TO_MEDIA_TYPE[filename.slice(dot + 1).toLowerCase()];
1170
+ }
1171
+ /**
1172
+ * Builds the canonical URL for a file part. A data URI passes through
1173
+ * verbatim; a path is returned unchanged if it already carries a recognized
1174
+ * scheme, else converted to a percent-encoded `file://` URL.
1175
+ *
1176
+ * Callers must pass an already-resolved absolute path — `trackFilePart`
1177
+ * resolves relative paths (or drops the observation when it can't) before
1178
+ * ever reaching here, so a relative path arriving at this layer means a
1179
+ * caller bypassed that resolution. We throw rather than mint a malformed
1180
+ * `file://` URL (e.g. `file://./chart.png`, whose authority is `.`).
1181
+ */
1182
+ function buildFileUrl(input) {
1183
+ if (input.dataUri !== void 0) return input.dataUri;
1184
+ if (input.path === void 0) throw new Error("buildFileUrl requires either `path` or `dataUri` to build a file part URL");
1185
+ if (input.path.startsWith("file://") || input.path.startsWith("http://") || input.path.startsWith("https://") || input.path.startsWith("data:")) return input.path;
1186
+ if (!isAbsolute(input.path)) throw new Error(`buildFileUrl requires an absolute path to build a file:// URL, got "${input.path}" — resolve it against a workspace root before calling buildFileUrl`);
1187
+ return pathToFileURL(input.path).href;
1188
+ }
1189
+ /** Deterministic, stable part id for a file announced within one message. */
1190
+ function deterministicFilePartId(messageId, key) {
1191
+ return `file-${sha1Hex(`${messageId}|${key}`).slice(0, 16)}`;
1192
+ }
1193
+ function deriveFilePart(input) {
1194
+ const normalized = normalize(input.path);
1195
+ let resolvedPath;
1196
+ if (isAbsolute(normalized)) resolvedPath = normalized;
1197
+ else if (input.workspaceRoot !== void 0) resolvedPath = resolve(input.workspaceRoot, normalized);
1198
+ else return null;
1199
+ const filename = basename(resolvedPath);
1200
+ return {
1201
+ id: deterministicFilePartId(input.messageId, resolvedPath),
1202
+ key: resolvedPath,
1203
+ filename,
1204
+ mediaType: inferMediaType(filename),
1205
+ url: buildFileUrl({ path: resolvedPath })
1206
+ };
1207
+ }
1208
+ function trackFilePart(state, args) {
1209
+ if (args.changeKind === "delete") return null;
1210
+ if (args.path === void 0 && args.dataUri === void 0) return null;
1211
+ if (state.messageId === void 0) throw new Error("trackFilePart requires state.messageId (set by createExecutionState) to derive a stable part id");
1212
+ let derived = null;
1213
+ if (args.path !== void 0) {
1214
+ derived = deriveFilePart({
1215
+ path: args.path,
1216
+ messageId: state.messageId,
1217
+ workspaceRoot: state.workspaceRoot
1218
+ });
1219
+ if (derived === null) return null;
1220
+ }
1221
+ state.fileParts ??= [];
1222
+ const parts = state.fileParts;
1223
+ const key = derived !== null ? derived.key : `data:${sha1Hex(args.dataUri)}`;
1224
+ const url = derived !== null ? derived.url : buildFileUrl({ dataUri: args.dataUri });
1225
+ const existing = parts.find((p) => p.key === key);
1226
+ let filename = args.filename ?? derived?.filename ?? existing?.filename;
1227
+ const mediaType = args.mediaType ?? (filename !== void 0 ? inferMediaType(filename) : void 0) ?? existing?.mediaType;
1228
+ if (filename === void 0 && args.path === void 0) {
1229
+ const priorDataUriEntries = parts.filter((p) => p.key.startsWith("data:")).length;
1230
+ const ext = mediaType ? MEDIA_TYPE_TO_EXTENSION[mediaType] ?? "bin" : "bin";
1231
+ filename = `attachment-${priorDataUriEntries + 1}.${ext}`;
1232
+ }
1233
+ if (existing) {
1234
+ existing.filename = filename;
1235
+ existing.mediaType = mediaType;
1236
+ existing.url = url;
1237
+ return {
1238
+ id: existing.id,
1239
+ filename: existing.filename,
1240
+ mediaType: existing.mediaType,
1241
+ url: existing.url
1242
+ };
1243
+ }
1244
+ const id = derived !== null ? derived.id : deterministicFilePartId(state.messageId, key);
1245
+ parts.push({
1246
+ id,
1247
+ key,
1248
+ filename,
1249
+ mediaType,
1250
+ url
1251
+ });
1252
+ return {
1253
+ id,
1254
+ filename,
1255
+ mediaType,
1256
+ url
1257
+ };
1258
+ }
1259
+ //#endregion
1260
+ //#region src/prompt-input.ts
1261
+ /** Raw-byte ceiling for inlining one image as base64 (~1.33x expansion in
1262
+ * memory and on the wire); larger images keep only their `path` and the
1263
+ * adapter falls back to path-based delivery. */
1264
+ const MAX_INLINE_IMAGE_BYTES = 10 * 1024 * 1024;
1265
+ /** Aggregate raw-byte budget for inlined images across one request's parts —
1266
+ * the request schema admits up to 64 parts, so a per-image cap alone would
1267
+ * still let one request hold hundreds of MB of base64 until execution. */
1268
+ const MAX_TOTAL_INLINE_IMAGE_BYTES = 32 * 1024 * 1024;
1269
+ async function readImageBase64(filePath, wroteFile, budgetBytes) {
1270
+ const limit = Math.min(MAX_INLINE_IMAGE_BYTES, budgetBytes);
1271
+ if (limit <= 0) return void 0;
1272
+ let handle;
1273
+ try {
1274
+ handle = await open(filePath, constants.O_RDONLY | constants.O_NONBLOCK);
1275
+ const stats = await handle.stat();
1276
+ if (!stats.isFile() || stats.size > limit) return void 0;
1277
+ const buffer = Buffer.alloc(stats.size + 1);
1278
+ let offset = 0;
1279
+ while (offset < buffer.length) {
1280
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
1281
+ if (bytesRead === 0) break;
1282
+ offset += bytesRead;
1283
+ }
1284
+ if (offset > stats.size) return void 0;
1285
+ return {
1286
+ base64: buffer.subarray(0, offset).toString("base64"),
1287
+ rawBytes: offset
1288
+ };
1289
+ } catch (error) {
1290
+ if (wroteFile) throw error;
1291
+ return;
1292
+ } finally {
1293
+ await handle?.close();
1294
+ }
1295
+ }
1296
+ function inferExtension(mediaType) {
1297
+ if (!mediaType) return "bin";
1298
+ if (mediaType === "application/pdf") return "pdf";
1299
+ if (mediaType.startsWith("image/")) return mediaType.split("/")[1] || "img";
1300
+ if (mediaType.startsWith("audio/")) return mediaType.split("/")[1] || "audio";
1301
+ if (mediaType.startsWith("video/")) return mediaType.split("/")[1] || "video";
1302
+ return mediaType.includes("/") ? mediaType.split("/")[1] || "bin" : "bin";
1303
+ }
1304
+ function sanitizeFilename(filename) {
1305
+ return filename.replace(/[^a-zA-Z0-9._-]/g, "-") || `attachment-${randomUUID()}`;
1306
+ }
1307
+ const TEXT_MEDIA_TYPES = new Set([
1308
+ "application/json",
1309
+ "application/xml",
1310
+ "application/yaml",
1311
+ "application/x-yaml",
1312
+ "application/javascript",
1313
+ "application/csv"
1314
+ ]);
1315
+ function isTextMediaType(mediaType) {
1316
+ if (!mediaType) return true;
1317
+ const essence = (mediaType.split(";")[0] ?? "").trim().toLowerCase();
1318
+ if (essence.startsWith("text/")) return true;
1319
+ if (TEXT_MEDIA_TYPES.has(essence)) return true;
1320
+ return essence.endsWith("+json") || essence.endsWith("+xml");
1321
+ }
1322
+ async function materializeAttachmentPart(part, workspaceRoot, sessionId, index) {
1323
+ const attachmentDir = path.join(workspaceRoot, ".agent-inputs", sessionId);
1324
+ await mkdir(attachmentDir, { recursive: true });
1325
+ let dataUriMatch;
1326
+ if (part.url?.startsWith("data:")) {
1327
+ const match = part.url.match(/^data:([^;,]+)?;base64,(.*)$/s);
1328
+ if (!match) throw new InvalidAttachmentError(`Invalid data URI for ${part.type} attachment`);
1329
+ dataUriMatch = match;
1330
+ }
1331
+ const mediaType = part.mediaType || dataUriMatch?.[1] || void 0;
1332
+ const filename = sanitizeFilename(part.filename || (part.path ? path.basename(part.path) : void 0) || `${part.type}-${index + 1}.${inferExtension(mediaType)}`);
1333
+ const targetPath = path.join(attachmentDir, filename);
1334
+ let referencePath = part.path ? path.resolve(workspaceRoot, part.path) : targetPath;
1335
+ let wroteFile = false;
1336
+ const content = "content" in part ? part.content : void 0;
1337
+ const payload = dataUriMatch ? Buffer.from(dataUriMatch[2], "base64") : typeof content === "string" ? content : void 0;
1338
+ if (payload !== void 0) {
1339
+ if (!dataUriMatch && !isTextMediaType(part.mediaType)) throw new InvalidAttachmentError(`Cannot write binary attachment "${filename}" (mediaType: ${part.mediaType}) via "content" — deliver it as a base64 data: URL (data:${part.mediaType};base64,...) or via "path"/"file://" url instead.`);
1340
+ const partDirectory = path.join(attachmentDir, `part-${randomUUID()}`);
1341
+ await mkdir(partDirectory);
1342
+ referencePath = path.join(partDirectory, filename);
1343
+ await writeFile(referencePath, payload, { flag: "wx" });
1344
+ wroteFile = true;
1345
+ } else if (part.url?.startsWith("file://")) try {
1346
+ referencePath = fileURLToPath(part.url);
1347
+ } catch (err) {
1348
+ throw new InvalidAttachmentError(`Invalid file:// URL for ${part.type} attachment: ${JSON.stringify(part.url)} (${err instanceof Error ? err.message : String(err)})`);
1349
+ }
1350
+ else if (part.url && /^https?:\/\//i.test(part.url)) referencePath = part.url;
1351
+ const isHttpReference = referencePath.startsWith("http://") || referencePath.startsWith("https://");
1352
+ return {
1353
+ reference: referencePath,
1354
+ filename,
1355
+ ...mediaType ? { mediaType } : {},
1356
+ wroteFile,
1357
+ persistedPart: {
1358
+ type: "file",
1359
+ filename,
1360
+ mediaType,
1361
+ url: isHttpReference ? referencePath : buildFileUrl({ path: referencePath })
1362
+ }
1363
+ };
1364
+ }
1365
+ async function preparePromptInputParts(parts, workspaceRoot, sessionId, options) {
1366
+ const nativeKinds = new Set(options?.nativeAttachmentKinds ?? []);
1367
+ const promptSections = [];
1368
+ const attachmentRefs = [];
1369
+ const persistedParts = [];
1370
+ const attachments = [];
1371
+ const inlineImages = nativeKinds.has("image");
1372
+ let inlineBudget = options?.inlineImageBudgetBytes ?? 33554432;
1373
+ for (const [index, part] of parts.entries()) {
1374
+ if (part.type === "text") {
1375
+ if (!part.text) continue;
1376
+ promptSections.push(part.text);
1377
+ persistedParts.push({
1378
+ type: "text",
1379
+ text: part.text
1380
+ });
1381
+ continue;
1382
+ }
1383
+ const materialized = await materializeAttachmentPart(part, workspaceRoot, sessionId, index);
1384
+ const marker = `[${part.type === "image" ? "Image" : "File"}: ${materialized.reference}]`;
1385
+ if (!nativeKinds.has(part.type)) attachmentRefs.push(marker);
1386
+ persistedParts.push(materialized.persistedPart);
1387
+ const isRemote = /^https?:\/\//i.test(materialized.reference);
1388
+ const attachment = {
1389
+ kind: part.type,
1390
+ filename: materialized.filename,
1391
+ marker,
1392
+ ...materialized.mediaType ? { mediaType: materialized.mediaType } : {},
1393
+ ...isRemote ? { url: materialized.reference } : { path: materialized.reference }
1394
+ };
1395
+ if (part.type === "image" && attachment.path && inlineImages) {
1396
+ const inlined = await readImageBase64(attachment.path, materialized.wroteFile, inlineBudget);
1397
+ if (inlined) {
1398
+ attachment.base64 = inlined.base64;
1399
+ inlineBudget -= inlined.rawBytes;
1400
+ }
1401
+ }
1402
+ attachments.push(attachment);
1403
+ }
1404
+ if (attachmentRefs.length > 0) promptSections.push(`Attached files:\n${attachmentRefs.join("\n")}`);
1405
+ return {
1406
+ promptText: promptSections.join("\n\n").trim(),
1407
+ persistedParts,
1408
+ attachments
1409
+ };
1410
+ }
1411
+ /**
1412
+ * Partition a turn's image attachments into the ones the adapter will
1413
+ * forward natively and the "stranded" rest, folding each stranded image's
1414
+ * `[Image: ...]` marker back into the message text. Once an adapter's
1415
+ * nativeAttachmentKinds() claims "image", the base prompt renderer omits
1416
+ * every image marker — so any image the mechanism can't carry (no base64
1417
+ * for a stdin block, no local path for an argv flag, beyond a single-path
1418
+ * flag's capacity) MUST re-enter the prompt here or the model never sees
1419
+ * a reference to it. Uses the same "Attached files:" block format the
1420
+ * renderer itself emits for non-native kinds.
1421
+ */
1422
+ function partitionImageAttachments(options) {
1423
+ const { message, attachments, qualifies, maxNative } = options;
1424
+ const native = [];
1425
+ const strandedMarkers = [];
1426
+ for (const attachment of attachments ?? []) {
1427
+ if (attachment.kind !== "image") continue;
1428
+ if ((maxNative === void 0 || native.length < maxNative) && qualifies(attachment)) native.push(attachment);
1429
+ else strandedMarkers.push(attachment.marker);
1430
+ }
1431
+ return {
1432
+ native,
1433
+ message: strandedMarkers.length > 0 ? `${message}\n\nAttached files:\n${strandedMarkers.join("\n")}` : message
1434
+ };
1435
+ }
1436
+ /** Separates the sections a harness receives as one user message. */
1437
+ const PROMPT_SECTION_SEPARATOR = "\n\n---\n\n";
1438
+ /**
1439
+ * Puts `section` (a system prompt or replayed context) ahead of the message
1440
+ * for harnesses that take no separate channel for it.
1441
+ */
1442
+ function prependPromptSection(section, body) {
1443
+ return section ? `${section}${PROMPT_SECTION_SEPARATOR}${body}` : body;
1444
+ }
1445
+ //#endregion
1446
+ //#region src/types.ts
1447
+ function interactionToggleEnabled(config, kind) {
1448
+ return config === true || typeof config === "object" && config !== null && config[kind] === true;
1449
+ }
1450
+ function enabledInteractionKinds(config, supported) {
1451
+ if (config === true) return [...supported];
1452
+ if (typeof config !== "object" || config === null) return [];
1453
+ return supported.filter((kind) => config[kind] === true);
1454
+ }
1455
+ function readEphemeralContext(input) {
1456
+ const context = input.ephemeralContext;
1457
+ if (context === void 0 || context === null) return void 0;
1458
+ if (typeof context !== "string") throw new TypeError(`ephemeralContext must be a string, received ${typeof context}`);
1459
+ return context || void 0;
1460
+ }
1461
+ /** Compose the model-facing prompt for a turn: the caller's own text, then this turn's ephemeral context. */
1462
+ function withEphemeralContext(promptText, input) {
1463
+ const context = readEphemeralContext(input);
1464
+ if (!context) return promptText;
1465
+ return promptText ? `${promptText}\n\n${context}` : context;
1466
+ }
1467
+ function turnInteractionEnabled(input, latched, kind) {
1468
+ const perTurn = input.interactions;
1469
+ return interactionToggleEnabled(perTurn !== void 0 ? perTurn : latched, kind);
1470
+ }
1471
+ /** Overlay this turn's interactions onto a latched config, returning a NEW object (no shared mutation — safe when. */
1472
+ function withTurnInteractions(config, input) {
1473
+ if (input.interactions === void 0) return config;
1474
+ return {
1475
+ ...config,
1476
+ interactions: input.interactions
1477
+ };
1478
+ }
1479
+ /**
1480
+ * The one precedence every engine names a failed turn's end with (#16404): an
1481
+ * end the abort, the timeout or the harness named is theirs; only an
1482
+ * unexplained death is the link's, the memory limit's or the harness's.
1483
+ */
1484
+ function turnEndReason(end) {
1485
+ if (end.aborted) return "abort";
1486
+ if (end.timedOut) return "timeout";
1487
+ if (end.harnessFailed) return "failed";
1488
+ if (end.linkLost) return "gateway_disconnect";
1489
+ if (end.memoryKilled) return "memory_limit";
1490
+ return end.signal ? "harness_exit" : "failed";
1491
+ }
1492
+ //#endregion
1493
+ //#region src/adapter-profile.ts
1494
+ /** Revisions that already passed the native health check in this process, by the verifier's scope and the revision's materialized digest. */
1495
+ const verifiedSecretProfileRevisions = /* @__PURE__ */ new Set();
1496
+ const VERIFIED_SECRET_PROFILE_REVISIONS_MAX = 1024;
1497
+ function rememberVerifiedSecretProfileRevision(key) {
1498
+ verifiedSecretProfileRevisions.delete(key);
1499
+ verifiedSecretProfileRevisions.add(key);
1500
+ if (verifiedSecretProfileRevisions.size > VERIFIED_SECRET_PROFILE_REVISIONS_MAX) {
1501
+ const oldest = verifiedSecretProfileRevisions.values().next().value;
1502
+ if (oldest !== void 0) verifiedSecretProfileRevisions.delete(oldest);
1503
+ }
1504
+ }
1505
+ /** The executor's naming key, refused unless root owns it alone. */
1506
+ function readSecretProfileNamingKey(keyPath) {
1507
+ if (!keyPath) throw new Error("Secret profile naming key is unavailable");
1508
+ const keyStat = lstatSync(keyPath);
1509
+ if (!keyStat.isFile() || keyStat.isSymbolicLink() || keyStat.uid !== 0 || (keyStat.mode & 511) !== 384 || keyStat.size !== 32) throw new Error("Secret profile naming key is not root-owned");
1510
+ return readFileSync(keyPath);
1511
+ }
1512
+ /** Owns native profile revisions, launch controls, and prompt composition. */
1513
+ var CliAdapterProfile = class extends CliAdapterLifecycle {
1514
+ /** Native profile lowering explicitly owned by this adapter, never inferred from a path. */
1515
+ profileHarness = void 0;
1516
+ /** Trusted implementation capabilities; never read from caller-owned configuration. */
1517
+ supportsSecretProfileRevisions = false;
1518
+ profileSecretProvider;
1519
+ secretProfileRuntimeKeyProvider;
1520
+ /** Private native preparation state; never part of a returned receipt. */
1521
+ secretProfileRevisions = /* @__PURE__ */ new WeakMap();
1522
+ constructor(options = {}) {
1523
+ super(options);
1524
+ this.profileSecretProvider = options.profileSecretProvider ?? UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER;
1525
+ this.secretProfileRuntimeKeyProvider = options.secretProfileRuntimeKeyProvider ?? UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER;
1526
+ }
1527
+ /** The exact child environment a turn uses; shared with prewarm paths. */
1528
+ buildTurnEnvironment(executionConfig, profilePlan, workspaceRoot, correlationHeaders) {
1529
+ const merged = this.mergeProfilePlanEnvironment(this.buildNativeExecutionEnvironment(executionConfig), profilePlan, executionConfig.nativeAuthEnv !== void 0 || (executionConfig.authFiles?.length ?? 0) > 0);
1530
+ return this.bindExecutionCorrelation(this.applyWritableHomeDefaults(this.bindProfileToolBinDirs(merged, profilePlan), workspaceRoot), correlationHeaders);
1531
+ }
1532
+ /** A profile's declared executable tools, sealed in its revision, stand in for the app's own tool dirs on this launch's PATH tail. */
1533
+ bindProfileToolBinDirs(env, profilePlan) {
1534
+ const toolBinDirs = profilePlan?.spawn.toolBinDirs;
1535
+ if (!toolBinDirs?.length) return env;
1536
+ return bindLaunchToolBinDirs("PATH" in env || !this.inheritProcessEnv || process.env.PATH === void 0 ? env : {
1537
+ ...env,
1538
+ PATH: process.env.PATH
1539
+ }, toolBinDirs, [...parseSandboxToolBinDirs(env.SANDBOX_TOOL_BIN_DIRS), ...parseSandboxToolBinDirs(process.env.SANDBOX_TOOL_BIN_DIRS)]);
1540
+ }
1541
+ bindExecutionCorrelation(environment, correlationHeaders) {
1542
+ if (correlationHeaders?.["X-Tangle-Execution-ID"] !== void 0) this.logger.warn("Native model request correlation is unsupported", { provider: this.provider });
1543
+ return environment;
1544
+ }
1545
+ /** Launcher controls this adapter OWNS on the spawn it starts; the pre-box verdict reads the same table. */
1546
+ profileMaterializationBinds() {
1547
+ const harness = this.profileHarness;
1548
+ return (harness && HARNESS_LAUNCH_MATERIALIZE_OPTIONS[harness]?.binds) ?? [];
1549
+ }
1550
+ /** Optional native instruction root. */
1551
+ profileAgentDirectory(_workspaceRoot) {}
1552
+ /** Provider transport bound into the private immutable revision and receipt. */
1553
+ secretProfileProviderConfig() {}
1554
+ /** The values behind a secret revision's `{env:NAME}` config references, for an executor that serves those references itself. */
1555
+ secretProfileReferences(application) {
1556
+ return application ? this.secretProfileRevisions.get(application)?.references : void 0;
1557
+ }
1558
+ /** The one sealed session view repointed only while its native turn owns the queue. */
1559
+ secretProfileSessionView(sessionId) {
1560
+ const root = this.config.secretProfileRevisionRoot;
1561
+ const subprocessUid = this.config.subprocessUser?.uid;
1562
+ if (!root || subprocessUid === void 0) throw new Error("Secret profile session view is unavailable");
1563
+ const launchRoot = join(root, "launch");
1564
+ return {
1565
+ launchRoot,
1566
+ view: secretSessionViewWorkspace({
1567
+ launchRoot,
1568
+ sharedWorkspaceDir: this.storagePath,
1569
+ sessionId,
1570
+ subprocessUid
1571
+ }),
1572
+ subprocessUid
1573
+ };
1574
+ }
1575
+ /** Bind the private revision into the session's stable native cwd. */
1576
+ bindSecretProfileSessionView(revisionWorkspaceDir, sessionId) {
1577
+ const { launchRoot, view, subprocessUid } = this.secretProfileSessionView(sessionId);
1578
+ bindSecretSessionView({
1579
+ launchRoot,
1580
+ viewWorkspaceDir: view,
1581
+ revisionWorkspaceDir,
1582
+ subprocessUid
1583
+ });
1584
+ return view;
1585
+ }
1586
+ async materializeProfileResources(inputProfile, workspaceRoot, ownerId, sessionId, scope = "turn") {
1587
+ if (!inputProfile && !this.config.runtimeAttachments) return void 0;
1588
+ const started = performance.now();
1589
+ try {
1590
+ return await this.applyProfileResources(inputProfile, workspaceRoot, ownerId, sessionId);
1591
+ } finally {
1592
+ this.logger.observePhase?.("profile_materialize", performance.now() - started, scope);
1593
+ }
1594
+ }
1595
+ /** This spawn's plan: a strict refusal throws, a best-effort one is logged. */
1596
+ planProfile(profile, harness, agentInstructions = false) {
1597
+ const plan = materializeProfile(profile, harness, {
1598
+ runtimeAttachments: this.config.runtimeAttachments,
1599
+ binds: this.profileMaterializationBinds(),
1600
+ ...agentInstructions ? { instructionsRoot: "agent" } : {}
1601
+ });
1602
+ const unsupported = enforceWorkspacePlanSupport(plan, profile.resources?.failOnError);
1603
+ if (unsupported.length > 0) this.logger.warn("profile contains unsupported best-effort resources", {
1604
+ harness,
1605
+ unsupported
1606
+ });
1607
+ return plan;
1608
+ }
1609
+ async applyProfileResources(inputProfile, workspaceRoot, ownerId, sessionId) {
1610
+ const profile = inputProfile ?? {};
1611
+ const harness = this.profileHarness;
1612
+ if (!harness) {
1613
+ if (hasPortableProfileControls(profile) || this.config.runtimeAttachments !== void 0) throw new Error(`${this.provider} does not support portable AgentProfile controls`);
1614
+ return;
1615
+ }
1616
+ const agentDir = this.profileAgentDirectory(workspaceRoot);
1617
+ const plan = this.planProfile(profile, harness, agentDir !== void 0);
1618
+ if (this.config.secretProfileRevisionRoot) return await this.applySecretProfileRevision(plan, this.config.secretProfileRevisionRoot, workspaceRoot, ownerId, sessionId);
1619
+ if (this.config.publicProfileRevisionRoot) {
1620
+ if (!ownerId) throw new Error("Concurrent public profile launch requires a session scope; idle prewarm is unsupported");
1621
+ const revision = await preparePublicConcurrentProfileRevision({
1622
+ plan,
1623
+ ...ensurePublicConcurrentProfileRoots(this.config.publicProfileRevisionRoot),
1624
+ sharedWorkspaceDir: workspaceRoot,
1625
+ ownerId,
1626
+ secretProvider: runtimeSecretsOverlay(this.config.runtimeSecrets, this.profileSecretProvider)
1627
+ });
1628
+ this.logger.debug("materialized public concurrent profile revision", {
1629
+ harness,
1630
+ directory: revision.directory,
1631
+ digest: revision.manifest.materializedDigest
1632
+ });
1633
+ const launchDigest = revision.manifest.launchDigest;
1634
+ if (!launchDigest) throw new Error("Concurrent profile launch digest was not recorded");
1635
+ profileRevisionReceipts(this).public.set(revision.application, {
1636
+ workspacePlanDigest: revision.manifest.workspacePlanDigest,
1637
+ materializedDigest: revision.manifest.materializedDigest,
1638
+ launchDigest,
1639
+ conflicts: revision.manifest.conflicts
1640
+ });
1641
+ return revision.application;
1642
+ }
1643
+ const application = await applyWorkspacePlanWithSecretProvider(plan, workspaceRoot, runtimeSecretsOverlay(this.config.runtimeSecrets, this.profileSecretProvider), {
1644
+ ownership: this.subprocessIdentity(),
1645
+ ...agentDir === void 0 ? {} : { agentDir }
1646
+ });
1647
+ this.logger.debug("materialized profile resources", {
1648
+ harness,
1649
+ files: plan.files.length,
1650
+ flags: plan.flags.length,
1651
+ environment: Object.keys(plan.env).length
1652
+ });
1653
+ return application;
1654
+ }
1655
+ /** Publish the profile as an executor-sealed revision for this session's subprocess identity, and record the receipt that binds it to the turn. */
1656
+ async applySecretProfileRevision(plan, secretProfileRevisionRoot, workspaceRoot, ownerId, sessionId) {
1657
+ if (!this.supportsSecretProfileRevisions || !ownerId || !sessionId || !this.config.subprocessUser) throw new Error("Secret concurrent profile needs an opted-in adapter, a session, and per-turn runtime secrets only");
1658
+ const namingKey = readSecretProfileNamingKey(this.config.secretProfileNamingKeyPath);
1659
+ const roots = ensurePublicConcurrentProfileRoots(secretProfileRevisionRoot);
1660
+ const revision = await prepareSecretConcurrentProfileRevision({
1661
+ plan,
1662
+ providerConfig: this.secretProfileProviderConfig(),
1663
+ ...roots,
1664
+ sharedWorkspaceDir: workspaceRoot,
1665
+ ownerId,
1666
+ subprocessUid: this.config.subprocessUser.uid,
1667
+ namingKey,
1668
+ secretProvider: runtimeSecretsOverlay(this.config.runtimeSecrets, this.secretProfileRuntimeKeyProvider)
1669
+ });
1670
+ const launchDigest = revision.manifest.launchDigest;
1671
+ if (!launchDigest) throw new Error("Secret concurrent profile launch digest is absent");
1672
+ const executorReceipt = `hmac-sha256:${createHmac("sha256", namingKey).update(ownerId).update("\0").update(sessionId).update("\0").update(String(this.config.subprocessUser.uid)).update("\0").update(revision.manifest.workspacePlanDigest).update("\0").update(revision.manifest.materializedDigest).update("\0").update(launchDigest).digest("hex")}`;
1673
+ this.secretProfileRevisions.set(revision.application, {
1674
+ materializedDigest: revision.manifest.materializedDigest,
1675
+ references: revision.secretReferences
1676
+ });
1677
+ profileRevisionReceipts(this).secret.set(revision.application, {
1678
+ workspacePlanDigest: revision.manifest.workspacePlanDigest,
1679
+ executorReceipt,
1680
+ conflicts: revision.manifest.conflicts.map((conflict) => ({ ...conflict }))
1681
+ });
1682
+ return revision.application;
1683
+ }
1684
+ /** Prepare the same provider-composed native plan that execute() will check. */
1685
+ async preparePublicProfileRevision(input) {
1686
+ if (!this.config.publicProfileRevisionRoot) throw new Error("Concurrent public profile materialization is disabled");
1687
+ const sessionScope = input.userId ?? input.sessionId;
1688
+ if (!sessionScope) throw new Error("Concurrent public profile preparation needs a session scope");
1689
+ const application = await this.materializeProfileResources(this.resolveExecutionProfileForInput(input), input.workspaceRoot ?? this.storagePath, sessionScope, input.sessionId);
1690
+ const receipt = application ? profileRevisionReceipts(this).public.get(application) : void 0;
1691
+ if (!receipt) throw new Error("Concurrent public profile preparation produced no native plan");
1692
+ return receipt;
1693
+ }
1694
+ /** Prepare the private native plan and return only an opaque executor receipt. */
1695
+ async prepareSecretProfileRevision(input) {
1696
+ if (!this.config.secretProfileRevisionRoot) throw new Error("Secret concurrent profile materialization is disabled");
1697
+ const sessionScope = input.userId ?? input.sessionId;
1698
+ if (!sessionScope) throw new Error("Secret profile preparation needs a session scope");
1699
+ const startedAt = performance.now();
1700
+ const application = await this.materializeProfileResources(this.resolveExecutionProfileForInput(input), input.workspaceRoot ?? this.storagePath, sessionScope, input.sessionId);
1701
+ const materializedAt = performance.now();
1702
+ const receipt = application ? profileRevisionReceipts(this).secret.get(application) : void 0;
1703
+ if (!application || !receipt) throw new Error("Secret profile preparation produced no native plan");
1704
+ const materializedDigest = this.secretProfileRevisions.get(application)?.materializedDigest;
1705
+ if (!materializedDigest) throw new Error("Secret profile preparation produced no revision digest");
1706
+ const verification = `${this.secretProfileVerificationScope()}\0${materializedDigest}`;
1707
+ const verifyCached = verifiedSecretProfileRevisions.has(verification);
1708
+ if (!verifyCached) await this.verifyPreparedSecretProfileRevision(input, application);
1709
+ rememberVerifiedSecretProfileRevision(verification);
1710
+ this.logger.info("prepared secret profile revision", {
1711
+ materializeMs: Math.round(materializedAt - startedAt),
1712
+ verifyMs: Math.round(performance.now() - materializedAt),
1713
+ verifyCached
1714
+ });
1715
+ return receipt;
1716
+ }
1717
+ /** What the native health check's verdict depends on besides a revision's bytes: the executable that loads them. */
1718
+ secretProfileVerificationScope() {
1719
+ return this.provider;
1720
+ }
1721
+ /** A secret receipt is issued only after the provider can load this revision. */
1722
+ async verifyPreparedSecretProfileRevision(_input, _application) {
1723
+ throw new Error(`${this.provider} has no native secret profile health check`);
1724
+ }
1725
+ /** Resolve the canonical profile used for this execution. */
1726
+ resolveExecutionProfile(requestPrompt) {
1727
+ return profileForExecution(this.config.profile, requestPrompt);
1728
+ }
1729
+ /** Select the profile materialized for this exact turn. */
1730
+ resolveExecutionProfileForInput(input) {
1731
+ return this.resolveExecutionProfile(input.systemPrompt);
1732
+ }
1733
+ /** Compose the provider's user-message channel for this exact turn. */
1734
+ composePromptText(promptText, input) {
1735
+ return withEphemeralContext(promptText, input);
1736
+ }
1737
+ withProfilePlan(config, profilePlan) {
1738
+ if (!profilePlan || profilePlan.spawn.flags.length === 0) return config;
1739
+ const contract = this.sessionContract();
1740
+ if (contract) assertFlagsHonourSessionContract(this.provider, contract, profilePlan.spawn.flags, "profile flags");
1741
+ return {
1742
+ ...config,
1743
+ extraFlags: [...config.extraFlags ?? [], ...profilePlan.spawn.flags]
1744
+ };
1745
+ }
1746
+ /** A profile's addition precedes host context while preserving its harness prompt. */
1747
+ resolveProfilePrompts(input, plan, profile = this.resolveExecutionProfileForInput(input)) {
1748
+ const addition = plan?.spawn.appendSystemPrompt;
1749
+ const keep = keepsHarnessSystemPrompt(profile, this.profileHarness);
1750
+ return {
1751
+ systemPrompt: keep ? void 0 : this.resolveProfileSystemPrompt(input.systemPrompt, plan),
1752
+ appendSystemPrompt: keep ? joinSystemPromptAdditions(addition, input.systemPrompt, input.appendSystemPrompt) : input.appendSystemPrompt ? joinSystemPromptAdditions(addition, input.appendSystemPrompt) : addition
1753
+ };
1754
+ }
1755
+ resolveProfileSystemPrompt(requestPrompt, profilePlan) {
1756
+ return resolveSystemPrompt(requestPrompt, profilePlan?.spawn.systemPrompt);
1757
+ }
1758
+ resolveProfileModel(requestModel) {
1759
+ return resolveModel(requestModel, this.config.profile?.model?.default, this.config.model, this.config.profile?.model?.provider);
1760
+ }
1761
+ mergeProfilePlanEnvironment(environment, profilePlan, preserveClearedVariables = false) {
1762
+ if (!profilePlan || Object.keys(profilePlan.spawn.env).length === 0) return environment;
1763
+ const merged = { ...environment };
1764
+ for (const [name, value] of Object.entries(profilePlan.spawn.env)) {
1765
+ const existing = merged[name];
1766
+ if (existing !== void 0 && existing !== value || preserveClearedVariables && existing === void 0 && Object.hasOwn(environment, name) && !this.profilePlanMayFillClearedEnvironmentVariable(name, value)) throw new Error(`profile environment conflicts with provider environment: ${name}`);
1767
+ merged[name] = value;
1768
+ }
1769
+ return merged;
1770
+ }
1771
+ /** Provider-owned allowlist for a generated revision control after env scrub. */
1772
+ profilePlanMayFillClearedEnvironmentVariable(_name, _value) {
1773
+ return false;
1774
+ }
1775
+ async preparePromptInput(parts, workspaceRoot, sessionId, options) {
1776
+ return preparePromptInputParts(parts, workspaceRoot, sessionId, {
1777
+ ...options,
1778
+ nativeAttachmentKinds: options?.nativeAttachmentKinds ?? this.nativeAttachmentKinds()
1779
+ });
1780
+ }
1781
+ /** Attachment kinds this adapter forwards natively via buildExecArgs (argv flag / stdin content block) instead of the `[Image: ...]` / `[File: ...]` text marker. */
1782
+ nativeAttachmentKinds() {
1783
+ return [];
1784
+ }
1785
+ };
1786
+ //#endregion
1787
+ //#region src/native-session-enforcement.ts
1788
+ /** Every shipped harness is enforced. A missing receipt is a failed proof, not a skip. */
1789
+ function sessionContractFullyEnforced(_provider) {
1790
+ return true;
1791
+ }
1792
+ //#endregion
1793
+ //#region src/adapter-sessions.ts
1794
+ /** Providers without a repair tool share one empty set. */
1795
+ const NO_REPAIR_TOOL_NAMES$1 = /* @__PURE__ */ new Set();
1796
+ /** Owns durable transcripts, provider-native session bindings and the turn hooks providers override. */
1797
+ var CliAdapterSessions = class extends CliAdapterProfile {
1798
+ async createSession(options) {
1799
+ this.ensureInitialized();
1800
+ const session = await this.getSessionStore().create({ workspaceRoot: options?.workspaceRoot ?? this.storagePath });
1801
+ this.logger.debug("Created session", { sessionId: session.id });
1802
+ return session.id;
1803
+ }
1804
+ async deleteSession(sessionId) {
1805
+ this.ensureInitialized();
1806
+ await this.getMessageStore().clear(sessionId);
1807
+ const deleted = await this.getSessionStore().delete(sessionId);
1808
+ this.logger.debug("Deleted session", {
1809
+ sessionId,
1810
+ deleted
1811
+ });
1812
+ return deleted;
1813
+ }
1814
+ /**
1815
+ * Choose the native ID before its argv check, durable binding, and
1816
+ * reservation. `input` and `timeoutMs` are the turn's, for a harness that
1817
+ * decides here whether a process started earlier can serve it.
1818
+ */
1819
+ mintNativeSessionId(_context) {
1820
+ return randomUUID();
1821
+ }
1822
+ /**
1823
+ * Make a pre-minted native session durable before the CLI starts, for a
1824
+ * harness that would otherwise write nothing until its first model reply
1825
+ * (Pi). Called after the argv check and before spawn.
1826
+ */
1827
+ reserveNativeSession(_nativeSessionId, _context) {}
1828
+ /**
1829
+ * Verify that the provider can resume the native session binding. By
1830
+ * default a session in a per-session store is resumable only when one of
1831
+ * its declared files exists under the runtime HOME; a pre-minted id whose
1832
+ * first turn died before the CLI wrote anything is not.
1833
+ */
1834
+ providerSessionIsResumable(runtimeHome, providerSessionId) {
1835
+ const contract = this.sessionContract();
1836
+ if (!contract) return true;
1837
+ if (!sessionContractFullyEnforced(this.provider) && contract.identity.kind !== "pre-minted") return true;
1838
+ return declaredSessionExists(contract, runtimeHome, providerSessionId);
1839
+ }
1840
+ /**
1841
+ * Resolve the provider-native session id the next turn can resume, or
1842
+ * undefined when no usable binding exists. Provider session data lives
1843
+ * under the runtime home that minted it. A different home or missing
1844
+ * provider storage cannot resume that session. A record without a home
1845
+ * stamp predates the field, so treat it as same-home.
1846
+ */
1847
+ async resumableProviderSessionId(session) {
1848
+ if (!session.providerSessionId) return void 0;
1849
+ const currentHome = this.config.runtimeHome?.trim() || void 0;
1850
+ const mintedHome = session.providerSessionHome?.trim() || void 0;
1851
+ if (currentHome !== void 0 && mintedHome !== void 0 && mintedHome !== currentHome) return;
1852
+ if (currentHome !== void 0) {
1853
+ if (!await this.providerSessionIsResumable(currentHome, session.providerSessionId)) return;
1854
+ }
1855
+ return session.providerSessionId;
1856
+ }
1857
+ /** Bind a checkpoint-proven native session to this provider identity before execution. */
1858
+ async seedNativeSessionForResume(input, nativeSessionId) {
1859
+ if (!input.sessionId) throw new Error("Native resume requires a named session");
1860
+ const runtimeHome = this.config.runtimeHome?.trim();
1861
+ if (!runtimeHome || !await this.providerSessionIsResumable(runtimeHome, nativeSessionId)) throw new Error("Native resume session is absent from this provider home");
1862
+ const store = this.getSessionStore();
1863
+ const existing = await store.get(input.sessionId);
1864
+ if (!existing) await store.create({
1865
+ id: input.sessionId,
1866
+ workspaceRoot: input.workspaceRoot ?? this.storagePath,
1867
+ providerSessionId: nativeSessionId,
1868
+ providerSessionHome: runtimeHome
1869
+ });
1870
+ else if (existing.providerSessionId !== nativeSessionId || existing.providerSessionHome !== runtimeHome) await store.update(input.sessionId, {
1871
+ providerSessionId: nativeSessionId,
1872
+ providerSessionHome: runtimeHome
1873
+ });
1874
+ }
1875
+ /**
1876
+ * Forks the persisted session and copies its messages up to `messageId`.
1877
+ * The forked record carries no provider session id, so its first turn
1878
+ * starts a fresh CLI session and receives the copied transcript as
1879
+ * execution-only context. Callers that address turns by their own session
1880
+ * id (the sidecar) pass that id as `targetSessionId` so later turns find
1881
+ * the forked record.
1882
+ */
1883
+ async forkSession(options) {
1884
+ this.ensureInitialized();
1885
+ const result = await forkSessionWithMessages(this.getSessionStore(), this.getMessageStore(), options.sessionId, {
1886
+ id: options.targetSessionId,
1887
+ messageId: options.messageId,
1888
+ workspaceRoot: options.workspaceRoot
1889
+ });
1890
+ this.logger.debug("Forked session", {
1891
+ parentId: options.sessionId,
1892
+ newId: result.session.id,
1893
+ messagesCopied: result.messagesCopied
1894
+ });
1895
+ return result.session.id;
1896
+ }
1897
+ async getMessages(options) {
1898
+ this.ensureInitialized();
1899
+ return (await this.getMessageStore().list(options.sessionId, {
1900
+ limit: options.limit,
1901
+ offset: options.offset
1902
+ })).map((msg) => this.toBackendMessage(msg));
1903
+ }
1904
+ toBackendMessage(msg) {
1905
+ return {
1906
+ id: msg.id,
1907
+ role: msg.role,
1908
+ parts: msg.parts,
1909
+ timestamp: msg.timestamp.toISOString(),
1910
+ metadata: msg.metadata
1911
+ };
1912
+ }
1913
+ buildAssistantParts(state) {
1914
+ const parts = [];
1915
+ for (const reasoning of state.reasoningBuffer) parts.push({
1916
+ type: "reasoning",
1917
+ text: reasoning
1918
+ });
1919
+ if (state.textBuffer) parts.push({
1920
+ type: "text",
1921
+ text: state.textBuffer
1922
+ });
1923
+ for (const toolCall of state.toolCalls.values()) {
1924
+ const inputRecord = toolCall.input ?? {};
1925
+ let toolState;
1926
+ if (toolCall.status === "completed") toolState = {
1927
+ status: "completed",
1928
+ input: inputRecord,
1929
+ output: toolCall.output
1930
+ };
1931
+ else if (toolCall.status === "failed") toolState = {
1932
+ status: "failed",
1933
+ input: inputRecord,
1934
+ error: toolCall.output ? String(toolCall.output) : void 0
1935
+ };
1936
+ else if (toolCall.status === "running") toolState = {
1937
+ status: "running",
1938
+ input: inputRecord
1939
+ };
1940
+ else toolState = {
1941
+ status: "pending",
1942
+ input: inputRecord
1943
+ };
1944
+ parts.push({
1945
+ type: "tool",
1946
+ tool: toolCall.name,
1947
+ callID: toolCall.id,
1948
+ state: toolState
1949
+ });
1950
+ }
1951
+ for (const file of state.fileParts ?? []) parts.push({
1952
+ type: "file",
1953
+ id: file.id,
1954
+ filename: file.filename,
1955
+ mediaType: file.mediaType,
1956
+ url: file.url
1957
+ });
1958
+ return parts;
1959
+ }
1960
+ extractToolInvocations(state) {
1961
+ const invocations = [];
1962
+ for (const toolCall of state.toolCalls.values()) invocations.push({
1963
+ toolName: toolCall.name,
1964
+ input: toolCall.input,
1965
+ result: toolCall.output,
1966
+ ...toolCall.status === "failed" ? { isError: true } : {}
1967
+ });
1968
+ return invocations;
1969
+ }
1970
+ /**
1971
+ * Names of tools this provider substitutes for a model tool call it could
1972
+ * not parse into any real tool.
1973
+ *
1974
+ * Default: none. A provider that has one must say so, because these calls
1975
+ * do not present as failures. OpenCode's `invalid` reports the parse error
1976
+ * as its output and completes, so the repeated-failure guard reads a run of
1977
+ * them as progress and clears its count on every one.
1978
+ */
1979
+ toolCallRepairToolNames() {
1980
+ return NO_REPAIR_TOOL_NAMES$1;
1981
+ }
1982
+ /**
1983
+ * Decide whether a non-zero exit should be treated as success.
1984
+ *
1985
+ * Default: no — any non-zero exit is a failure.
1986
+ *
1987
+ * Providers override this when their CLI has a known "exits non-zero
1988
+ * on a successful run" quirk (e.g. kimi writes its resume trailer to
1989
+ * stderr and exits non-zero even when the task completed cleanly).
1990
+ *
1991
+ * The decision is made AFTER the stream has drained and the exit
1992
+ * code is known, so `state` reflects the full set of events the
1993
+ * parser emitted (including any terminal `result` / `done` event).
1994
+ * A safe heuristic is: "non-zero exit + stderr matches known trailer
1995
+ * pattern + at least one assistant part was recorded".
1996
+ */
1997
+ isBenignNonZeroExit(_params) {
1998
+ return false;
1999
+ }
2000
+ /** Observe settled process output before success/failure classification. */
2001
+ async afterCliProcessExit(_params) {}
2002
+ executionOutcome(_state, _input) {
2003
+ return { type: "completed" };
2004
+ }
2005
+ /**
2006
+ * Format a message for stdin input.
2007
+ */
2008
+ formatStdinMessage(message) {
2009
+ return message;
2010
+ }
2011
+ };
2012
+ /**
2013
+ * Whether any file below `root` (relative path) matches one of `matchers`.
2014
+ * Bounded: native stores nest a few levels and hold one file per session.
2015
+ */
2016
+ async function anyFileMatches(root, matchers) {
2017
+ const pending = [""];
2018
+ let visited = 0;
2019
+ while (pending.length > 0 && visited < 2e4) {
2020
+ const relative = pending.pop();
2021
+ if (relative === void 0) break;
2022
+ let entries;
2023
+ try {
2024
+ entries = await readdir(path.join(root, relative), { withFileTypes: true });
2025
+ } catch {
2026
+ continue;
2027
+ }
2028
+ for (const entry of entries) {
2029
+ visited++;
2030
+ const child = relative ? `${relative}/${entry.name}` : entry.name;
2031
+ if (entry.isDirectory()) pending.push(child);
2032
+ else if (entry.isFile() && matchers.some((matcher) => matcher.test(child))) return true;
2033
+ }
2034
+ }
2035
+ return false;
2036
+ }
2037
+ /** Whether a per-session store holds a declared file for this id. */
2038
+ async function declaredSessionExists(contract, runtimeHome, nativeSessionId) {
2039
+ for (const store of contract.stores) {
2040
+ if (store.shared) return true;
2041
+ const matchers = store.files.map((glob) => compileSessionStoreGlob(glob, nativeSessionId));
2042
+ if (await anyFileMatches(path.join(runtimeHome, store.root), matchers)) return true;
2043
+ }
2044
+ return false;
2045
+ }
2046
+ //#endregion
2047
+ //#region src/cli-exit-diagnostics.ts
2048
+ /** Characters of stderr a failure message carries, taken from the tail. */
2049
+ const FAILURE_STDERR_TAIL_LIMIT = 600;
2050
+ /**
2051
+ * Attach the process-level diagnostics this run already collected to a failure
2052
+ * message.
2053
+ *
2054
+ * A provider that reports its OWN terminal failure — an empty event stream, an
2055
+ * unfinished model step — explains what it saw but not how the process ended:
2056
+ * a CLI that exited 0 having emitted nothing is misconfigured, one killed on the
2057
+ * timeout is stuck, one that exited 9 with stderr crashed. Each part is skipped
2058
+ * when the message already states it, so a message derived FROM stderr or the
2059
+ * exit code is never doubled.
2060
+ */
2061
+ function withCliExitDiagnostics(message, exit, stderr) {
2062
+ const parts = [];
2063
+ if (exit.stdinError && !message.includes(exit.stdinError)) parts.push(`stdin failed: ${exit.stdinError}`);
2064
+ const timeout = exit.timedOut ? exit.timeoutDiagnostic ?? "timed out" : "";
2065
+ if (timeout && !message.toLowerCase().includes(timeout.toLowerCase())) parts.push(timeout);
2066
+ if (exit.code !== null) {
2067
+ if (!message.includes(`code ${exit.code}`)) parts.push(`exit code ${exit.code}`);
2068
+ } else if (exit.signal) parts.push(`killed by ${exit.signal}`);
2069
+ const trimmedStderr = stderr.trim();
2070
+ if (trimmedStderr && !message.includes(trimmedStderr)) {
2071
+ const tail = trimmedStderr.slice(-FAILURE_STDERR_TAIL_LIMIT);
2072
+ parts.push(`stderr: ${tail === trimmedStderr ? "" : "…"}${tail}`);
2073
+ }
2074
+ return parts.length === 0 ? message : `${message} (${parts.join("; ")})`;
2075
+ }
2076
+ //#endregion
2077
+ //#region src/mcp-commands.ts
2078
+ /** Execute one prepared MCP registration command. Serialization belongs to the adapter. */
2079
+ async function addCliMcp(options) {
2080
+ const { name, config, run, logger } = options;
2081
+ logger.debug("Adding MCP server", {
2082
+ name,
2083
+ type: config.type
2084
+ });
2085
+ const result = await run();
2086
+ if (!result.exit.success) throw new Error(`Failed to add MCP server "${name}": ${result.stderr || `exit code ${result.exit.code}`}`);
2087
+ logger.info("Added MCP server", { name });
2088
+ }
2089
+ /** List native MCP servers with the provider's parser and command budget. */
2090
+ async function listCliMcps(options) {
2091
+ const { provider, args, run, logger, parse } = options;
2092
+ if (!args) {
2093
+ logger.warn(`getMcpStatus: ${provider} CLI does not support listing MCP servers`);
2094
+ return { servers: {} };
2095
+ }
2096
+ const result = await run(args);
2097
+ if (!result.exit.success) throw new Error(withCliExitDiagnostics(`${provider} failed to list MCP servers`, result.exit, result.stderr));
2098
+ return parse(result.stdout ?? "");
2099
+ }
2100
+ const MCP_STATUSES = [
2101
+ "connected",
2102
+ "disconnected",
2103
+ "error",
2104
+ "unknown"
2105
+ ];
2106
+ const MCP_TYPES = [
2107
+ "local",
2108
+ "remote",
2109
+ "stdio",
2110
+ "http"
2111
+ ];
2112
+ function pick(known, value, fallback) {
2113
+ return known.includes(value) ? value : fallback;
2114
+ }
2115
+ /**
2116
+ * Read an `mcp list` report: a JSON array of `{name, status, type}`, a JSON
2117
+ * object keyed by name, or a whitespace table whose first column is the name
2118
+ * and whose next columns may hold the type and the status.
2119
+ */
2120
+ function parseMcpListReport(output) {
2121
+ const servers = {};
2122
+ const add = (name, status, type) => {
2123
+ servers[name] = {
2124
+ name,
2125
+ status: pick(MCP_STATUSES, status, "unknown"),
2126
+ type: pick(MCP_TYPES, type, "stdio")
2127
+ };
2128
+ };
2129
+ let data;
2130
+ try {
2131
+ data = JSON.parse(output);
2132
+ } catch {
2133
+ for (const line of output.trim().split("\n")) {
2134
+ if (!line.trim() || line.includes("NAME") || line.startsWith("-")) continue;
2135
+ const [name = "", type, status] = line.trim().split(/\s{2,}/);
2136
+ add(name, status, type);
2137
+ }
2138
+ return { servers };
2139
+ }
2140
+ if (Array.isArray(data)) for (const server of data) add(server.name, server.status ?? "connected", server.type);
2141
+ else if (typeof data === "object" && data !== null) for (const [name, info] of Object.entries(data)) add(name, info.status, void 0);
2142
+ return { servers };
2143
+ }
2144
+ /** A local MCP server's argv: the `command` array, or `command` then `args`. */
2145
+ function localMcpArgv(config) {
2146
+ const local = config;
2147
+ return Array.isArray(local.command) ? local.command : [local.command, ...local.args ?? []];
2148
+ }
2149
+ /**
2150
+ * A local MCP server's environment, from `environment` (agent-backend-types)
2151
+ * or `env` (agent-interface): reading one drops the other family's variables.
2152
+ */
2153
+ function localMcpEnv(config) {
2154
+ return config.environment ?? config.env;
2155
+ }
2156
+ const isLocalMcpConfig = (c) => c.type === "local" || c.type === "stdio";
2157
+ /** Build MCP add command arguments. Override for CLI-specific syntax. */
2158
+ function defaultMcpAddArgs(name, config) {
2159
+ if (isLocalMcpConfig(config)) {
2160
+ const env = Object.entries(localMcpEnv(config) ?? {});
2161
+ return [
2162
+ "mcp",
2163
+ "add",
2164
+ name,
2165
+ "--",
2166
+ ...localMcpArgv(config),
2167
+ ...env.flatMap(([key, value]) => ["--env", `${key}=${value}`])
2168
+ ];
2169
+ }
2170
+ const headers = Object.entries(config.headers ?? {});
2171
+ return [
2172
+ "mcp",
2173
+ "add",
2174
+ name,
2175
+ config.url,
2176
+ ...headers.flatMap(([key, value]) => ["--header", `${key}: ${value}`])
2177
+ ];
2178
+ }
2179
+ //#endregion
2180
+ //#region src/adapter-mcp.ts
2181
+ /** Owns serialized MCP mutations and provider-native MCP status commands. */
2182
+ var CliAdapterMcp = class extends CliAdapterSessions {
2183
+ /** Budget for one `mcp list` run, CLI startup and every server's handshake. */
2184
+ mcpStatusTimeoutMs = 1e4;
2185
+ mcpMutationChain = Promise.resolve();
2186
+ async addMcp(name, config) {
2187
+ const run = this.mcpMutationChain.then(() => this.addMcpSerialized(name, config));
2188
+ this.mcpMutationChain = run.catch(() => void 0);
2189
+ return run;
2190
+ }
2191
+ async addMcpSerialized(name, config) {
2192
+ this.ensureInitialized();
2193
+ const args = this.buildMcpAddArgs(name, config);
2194
+ return addCliMcp({
2195
+ name,
2196
+ config,
2197
+ logger: this.logger,
2198
+ run: () => this.runAuxiliaryCli(args, 3e4)
2199
+ });
2200
+ }
2201
+ async getMcpStatus() {
2202
+ this.ensureInitialized();
2203
+ return listCliMcps({
2204
+ provider: this.provider,
2205
+ args: this.buildMcpListArgs(),
2206
+ logger: this.logger,
2207
+ run: (args) => this.runAuxiliaryCli(args, this.mcpStatusTimeoutMs),
2208
+ parse: (output) => this.parseMcpListOutput(output)
2209
+ });
2210
+ }
2211
+ async ensureMcps(required) {
2212
+ const currentStatus = await this.getMcpStatus();
2213
+ const existingServers = new Set(Object.keys(currentStatus.servers));
2214
+ for (const [name, config] of Object.entries(required)) {
2215
+ if (existingServers.has(name)) {
2216
+ this.logger.debug("MCP server already exists, skipping", { name });
2217
+ continue;
2218
+ }
2219
+ await this.addMcp(name, config);
2220
+ }
2221
+ }
2222
+ /** Build MCP add command arguments. Override for CLI-specific syntax. */
2223
+ buildMcpAddArgs(name, config) {
2224
+ return defaultMcpAddArgs(name, config);
2225
+ }
2226
+ /**
2227
+ * Build MCP list command arguments. Return null if not supported.
2228
+ */
2229
+ buildMcpListArgs() {
2230
+ return null;
2231
+ }
2232
+ /** Parse MCP list output. Override for CLI-specific output format. */
2233
+ parseMcpListOutput(output) {
2234
+ return parseMcpListReport(output);
2235
+ }
2236
+ };
2237
+ //#endregion
2238
+ //#region src/execution-state.ts
2239
+ /** A file of the sandbox's cgroup (#7571); undefined off Linux or outside one. */
2240
+ function readCgroupFile(name) {
2241
+ try {
2242
+ return readFileSync(`/sys/fs/cgroup/${name}`, "utf8").trim();
2243
+ } catch {
2244
+ return;
2245
+ }
2246
+ }
2247
+ /** The sandbox's OOM-kill count: a running total for the whole box. */
2248
+ function oomKills() {
2249
+ const events = readCgroupFile("memory.events") ?? "";
2250
+ return Number(/^oom_kill (\d+)$/m.exec(events)?.[1] ?? 0);
2251
+ }
2252
+ /** Creates a new execution state. */
2253
+ function createExecutionState(sessionId, messageId, workspaceRoot) {
2254
+ return {
2255
+ sessionId,
2256
+ messageId: messageId ?? randomUUID(),
2257
+ workspaceRoot,
2258
+ toolCalls: /* @__PURE__ */ new Map(),
2259
+ textBuffer: "",
2260
+ reasoningBuffer: [],
2261
+ oomKillsAtStart: oomKills()
2262
+ };
2263
+ }
2264
+ //#endregion
2265
+ //#region src/session-preparation.ts
2266
+ async function prepareExecutionSession(context, input) {
2267
+ let sessionId = input.sessionId;
2268
+ let providerSessionId;
2269
+ let transcriptReplay;
2270
+ if (sessionId) {
2271
+ const session = await context.getSessionStore().get(sessionId);
2272
+ if (session) {
2273
+ providerSessionId = await context.resumableProviderSessionId(session);
2274
+ if (session.parentId !== void 0 && session.providerSessionId === void 0) transcriptReplay = "fork";
2275
+ else if (context.config.secretProfileRevisionRoot && session.providerSessionId === void 0 && (await context.getMessageStore().list(sessionId)).length > 0) transcriptReplay = "restored";
2276
+ else if (session.providerSessionId && providerSessionId === void 0) {
2277
+ context.logger.info("Provider session data is not resumable; replaying durable history into a fresh provider session", {
2278
+ sessionId,
2279
+ providerSessionId: session.providerSessionId
2280
+ });
2281
+ transcriptReplay = "restored";
2282
+ }
2283
+ await context.getSessionStore().update(sessionId, {});
2284
+ } else {
2285
+ if (context.config.secretProfileRevisionRoot) try {
2286
+ transcriptReplay = (await context.getMessageStore().list(sessionId, {
2287
+ requireComplete: true,
2288
+ allowAbsent: true
2289
+ })).length > 0 ? "restored" : void 0;
2290
+ } catch (error) {
2291
+ throw new SessionRehydrationError(`Session ${sessionId} has incomplete persisted history`, error);
2292
+ }
2293
+ else transcriptReplay = "restored";
2294
+ await context.getSessionStore().create({
2295
+ id: sessionId,
2296
+ workspaceRoot: input.workspaceRoot ?? context.storagePath
2297
+ });
2298
+ }
2299
+ } else sessionId = await context.createSession({ workspaceRoot: input.workspaceRoot ?? context.storagePath });
2300
+ return {
2301
+ sessionId,
2302
+ providerSessionId,
2303
+ transcriptReplay
2304
+ };
2305
+ }
2306
+ //#endregion
2307
+ //#region src/turn-execution.ts
2308
+ /**
2309
+ * Turn idempotency: if the caller supplied a turnId and a previously-
2310
+ * completed turn with that id exists, return its cached result without
2311
+ * re-issuing the upstream LLM call. Both `findCompletedTurn` and
2312
+ * `setTurnId` are optional on the recorder interface so providers using
2313
+ * a no-op recorder (sdk-backend) still typecheck.
2314
+ */
2315
+ async function findCachedTurn(recorder, input, logger, sessionId) {
2316
+ if (!(input.turnId && recorder.findCompletedTurn)) return void 0;
2317
+ const cached = await recorder.findCompletedTurn(input.turnId);
2318
+ if (!cached) return void 0;
2319
+ logger.info("Returning cached turn result (idempotent retry)", {
2320
+ turnId: input.turnId,
2321
+ sessionId
2322
+ });
2323
+ return {
2324
+ ...cached,
2325
+ outcome: AgentExecutionOutcomeSchema.parse(cached.outcome)
2326
+ };
2327
+ }
2328
+ /**
2329
+ * The persisted transcript, read before this turn's user message is appended
2330
+ * so the block never contains the current message. Execution-only: it reaches
2331
+ * the model prompt and stays out of both stores.
2332
+ */
2333
+ async function buildTranscriptContext(params) {
2334
+ const { provider, sessionId, transcriptReplay, secretProfileRevisionRoot } = params;
2335
+ if (!transcriptReplay) return void 0;
2336
+ try {
2337
+ return (secretProfileRevisionRoot ? buildCompleteForkContextBlock : buildForkContextBlock)(await params.getMessageStore().list(sessionId, { requireComplete: Boolean(secretProfileRevisionRoot) }), transcriptReplay === "restored" ? {
2338
+ preamble: RESTORED_SESSION_CONTEXT_PREAMBLE,
2339
+ tag: RESTORED_SESSION_CONTEXT_TAG,
2340
+ ...secretProfileRevisionRoot ? { maxChars: MAX_SECRET_PROFILE_REPLAY_CHARS } : {}
2341
+ } : secretProfileRevisionRoot ? { maxChars: MAX_SECRET_PROFILE_REPLAY_CHARS } : void 0);
2342
+ } catch (error) {
2343
+ throw new SessionRehydrationError(`Session ${sessionId} has persisted history that could not be read for replay into a fresh ${provider} session: ${error instanceof Error ? error.message : String(error)}`, error);
2344
+ }
2345
+ }
2346
+ /**
2347
+ * A harness that can only report its id in an event must do so promptly; a
2348
+ * turn whose native session cannot be named is not traceable. Returns the
2349
+ * deadline timer, or undefined when the contract sets none.
2350
+ */
2351
+ function armNativeSessionDeadline(params) {
2352
+ const { provider, sessionId, sessionContract, state, observer, logger } = params;
2353
+ const identity = sessionContract?.identity;
2354
+ if (identity?.kind !== "first-event" || !sessionContractFullyEnforced(provider)) return;
2355
+ const deadline = setTimeout(() => {
2356
+ if (observer.nativeSessionObserved || state.terminalStatus !== void 0) return;
2357
+ const detail = new HarnessSessionContractError("native_session_id_unobserved", `${provider} reported no native session id within ${identity.deadlineMs} ms`).message;
2358
+ state.terminalStatus = "failed";
2359
+ state.lastDiagnostic = detail;
2360
+ logger.error("Native session id unobserved; stopping turn", { sessionId });
2361
+ params.kill();
2362
+ }, identity.deadlineMs);
2363
+ deadline.unref();
2364
+ return deadline;
2365
+ }
2366
+ function idIn(glob, rel) {
2367
+ const g = glob.split("/");
2368
+ const r = rel.split("/");
2369
+ let gi = 0;
2370
+ let ri = 0;
2371
+ let id = null;
2372
+ while (gi < g.length && ri < r.length) {
2373
+ const part = g[gi];
2374
+ if (part === "**") {
2375
+ const rest = g.slice(gi + 1).join("/");
2376
+ if (!rest) return id;
2377
+ for (let k = ri; k < r.length; k++) {
2378
+ const found = idIn(rest, r.slice(k).join("/"));
2379
+ if (found) return found;
2380
+ }
2381
+ return null;
2382
+ }
2383
+ if (part?.includes("{id}")) {
2384
+ const [pre, post] = part.split("{id}");
2385
+ const name = r[ri] ?? "";
2386
+ if (!name.startsWith(pre ?? "") || !name.endsWith(post ?? "")) return null;
2387
+ id = name.slice((pre ?? "").length, name.length - (post ?? "").length);
2388
+ } else if (part !== "*" && part !== r[ri]) return null;
2389
+ gi += 1;
2390
+ ri += 1;
2391
+ }
2392
+ return gi === g.length && ri === r.length ? id : null;
2393
+ }
2394
+ function filesUnder(dir, prefix, out) {
2395
+ let entries;
2396
+ try {
2397
+ entries = readdirSync(dir, { withFileTypes: true });
2398
+ } catch {
2399
+ return;
2400
+ }
2401
+ for (const entry of entries) {
2402
+ const name = String(entry.name);
2403
+ const rel = prefix ? `${prefix}/${name}` : name;
2404
+ if (entry.isDirectory()) filesUnder(join(dir, name), rel, out);
2405
+ else out.push(rel);
2406
+ }
2407
+ }
2408
+ function idsNow(home, stores) {
2409
+ const ids = /* @__PURE__ */ new Set();
2410
+ for (const store of stores) {
2411
+ const root = join(home, store.root);
2412
+ if (store.shared && store.write === "sqlite-wal") {
2413
+ try {
2414
+ const db = new DatabaseSync(join(root, store.files[0] ?? "state.db"), { readOnly: true });
2415
+ try {
2416
+ let rows;
2417
+ try {
2418
+ rows = db.prepare("SELECT id FROM sessions").all();
2419
+ } catch {
2420
+ rows = db.prepare("SELECT session_id AS id FROM session_windows").all();
2421
+ }
2422
+ for (const row of rows) if (row.id) ids.add(row.id);
2423
+ } finally {
2424
+ db.close();
2425
+ }
2426
+ } catch {}
2427
+ continue;
2428
+ }
2429
+ const found = [];
2430
+ filesUnder(root, "", found);
2431
+ for (const rel of found) for (const glob of store.files) {
2432
+ const id = idIn(glob, rel);
2433
+ if (id) ids.add(id);
2434
+ }
2435
+ }
2436
+ return ids;
2437
+ }
2438
+ function watchNewSessionIds(params) {
2439
+ const home = params.runtimeHome?.trim().replace(/\/+$/u, "");
2440
+ const contract = params.sessionContract;
2441
+ if (!home || contract?.identity.kind !== "first-event") return () => {};
2442
+ const seen = idsNow(home, contract.stores);
2443
+ const timer = setInterval(() => {
2444
+ if (params.observer.nativeSessionObserved) return;
2445
+ for (const id of idsNow(home, contract.stores)) {
2446
+ if (seen.has(id)) continue;
2447
+ seen.add(id);
2448
+ params.observer.onEvent({
2449
+ type: "native.session.observed",
2450
+ provider: params.provider,
2451
+ nativeSessionId: id
2452
+ });
2453
+ }
2454
+ }, 200);
2455
+ timer.unref();
2456
+ return () => clearInterval(timer);
2457
+ }
2458
+ function collectStderr(cliProcess) {
2459
+ let text = "";
2460
+ const stderrDecoder = new StringDecoder("utf8");
2461
+ cliProcess.stderr?.on("data", (chunk) => {
2462
+ text += stderrDecoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2463
+ });
2464
+ cliProcess.stderr?.on("end", () => {
2465
+ text += stderrDecoder.end();
2466
+ });
2467
+ return { get text() {
2468
+ return text;
2469
+ } };
2470
+ }
2471
+ /** A turn's stderr or ACP error when its link to its harness's server dropped (#8819). */
2472
+ const HARNESS_LINK_LOST = /\b(?:ECONNRESET|ECONNREFUSED|EPIPE|UND_ERR_SOCKET)\b|ended before (?:it|the turn) completed|Gateway disconnected/u;
2473
+ /**
2474
+ * Names a SIGKILL from the memory limit (#7571): the CLI runs in the sandbox's
2475
+ * cgroup, whose `memory.events` counts OOM kills under the `memory.max` limit.
2476
+ * The count is the box's running total, so only a kill since the turn started
2477
+ * is this turn's.
2478
+ */
2479
+ function memoryLimitKill(killsAtStart = 0) {
2480
+ const kills = oomKills() - killsAtStart;
2481
+ if (kills < 1) return void 0;
2482
+ const max = readCgroupFile("memory.max") ?? "max";
2483
+ return `The sandbox's ${max === "max" ? "host" : `${Number(max) / 2 ** 20} MiB`} memory limit OOM-killed ${kills} process(es) during this turn: give the sandbox more memory or make the tool use less`;
2484
+ }
2485
+ /**
2486
+ * A failed CLI turn's end and error (#16404): a guard's end is its own, the
2487
+ * rest is `turnEndReason`'s; a session death and a memory-limit kill carry
2488
+ * their wire code.
2489
+ */
2490
+ function endFailedTurn(params) {
2491
+ const { provider, state, exitResult, stderr, startTime, endTime } = params;
2492
+ const oom = exitResult.signal === "SIGKILL" && memoryLimitKill(state.oomKillsAtStart);
2493
+ state.endReason ??= turnEndReason({
2494
+ aborted: params.aborted,
2495
+ timedOut: exitResult.timedOut,
2496
+ harnessFailed: Boolean(state.terminalStatus),
2497
+ linkLost: HARNESS_LINK_LOST.test(stderr),
2498
+ memoryKilled: Boolean(oom),
2499
+ signal: exitResult.signal
2500
+ });
2501
+ if (oom && state.endReason === "memory_limit") state.failure ??= {
2502
+ code: "MEMORY_LIMIT_KILLED",
2503
+ message: oom
2504
+ };
2505
+ if (["gateway_disconnect", "harness_exit"].includes(state.endReason)) state.failure ??= {
2506
+ code: "SESSION_DIED",
2507
+ message: `Harness session died (${state.endReason}): start a new session, or send the next turn to this session to resume it`
2508
+ };
2509
+ const nix = params.nixBinPath ? ` (NIX_BIN_PATH=${params.nixBinPath})` : " (NIX_BIN_PATH unset — is the Nix profile mounted?)";
2510
+ const errorMessage = exitResult.spawnError ? `Failed to spawn "${params.command}": ${exitResult.spawnError}${exitResult.spawnError.includes("ENOENT") ? ` — binary not found in PATH${nix}` : ""}` : withCliExitDiagnostics(state.failure?.message ?? state.lastDiagnostic ?? (exitResult.timedOut ? exitResult.timeoutDiagnostic ?? "Execution timed out" : void 0) ?? (stderr.trim() || (state.terminalStatus === "failed" ? `${provider} reported a failed terminal status` : `Process exited with code ${exitResult.code}`)), exitResult, stderr);
2511
+ const failure = new AgentExecutionError(`${provider} execution failed: ${errorMessage}`, {
2512
+ ...state.tokenUsage ? { tokenUsage: state.tokenUsage } : {},
2513
+ timing: {
2514
+ startedAt: startTime,
2515
+ completedAt: endTime,
2516
+ durationMs: endTime - startTime
2517
+ }
2518
+ });
2519
+ Object.assign(failure, {
2520
+ reason: state.endReason,
2521
+ ...state.failure?.code ? { code: state.failure.code } : {},
2522
+ ...state.failure?.statusCode === void 0 ? {} : {
2523
+ statusCode: state.failure.statusCode,
2524
+ status: state.failure.statusCode
2525
+ },
2526
+ ...state.failure?.resetAt ? { resetAt: state.failure.resetAt } : {}
2527
+ });
2528
+ return {
2529
+ errorMessage,
2530
+ failure
2531
+ };
2532
+ }
2533
+ /**
2534
+ * Best-effort: stamp the assistant message interrupted so reattach can
2535
+ * surface "this turn died" without inferring from absence. Swallow any
2536
+ * metadata-write error — the upstream failure is the one the caller
2537
+ * needs to see. The coalesced part updates drain first so no stale
2538
+ * snapshot lands after the terminal stamp.
2539
+ */
2540
+ async function recordTurnInterruption(params) {
2541
+ const { recorder, observer, logger, errorMessage, exitResult } = params;
2542
+ await observer.flushPartUpdates();
2543
+ if (recorder.markTurnInterrupted) try {
2544
+ await recorder.markTurnInterrupted({
2545
+ reason: errorMessage,
2546
+ exitCode: exitResult.code,
2547
+ timedOut: exitResult.timedOut,
2548
+ failureReceipt: params.failure.receipt
2549
+ });
2550
+ } catch (markError) {
2551
+ logger.warn("markTurnInterrupted failed (suppressed to preserve original error)", { error: markError instanceof Error ? markError.message : String(markError) });
2552
+ }
2553
+ }
2554
+ /** The result of a turn that completed. */
2555
+ function buildExecutionResult(params) {
2556
+ const { state, publicProfileRevision, startTime, endTime } = params;
2557
+ return {
2558
+ outcome: params.outcome,
2559
+ text: state.textBuffer,
2560
+ toolInvocations: params.toolInvocations,
2561
+ reasoning: state.reasoningBuffer.length > 0 ? state.reasoningBuffer : void 0,
2562
+ sessionId: params.sessionId,
2563
+ ...publicProfileRevision ? { metadata: { publicProfileRevision } } : {},
2564
+ tokenUsage: state.tokenUsage ? {
2565
+ inputTokens: state.tokenUsage.inputTokens ?? 0,
2566
+ outputTokens: state.tokenUsage.outputTokens ?? 0,
2567
+ totalTokens: state.tokenUsage.totalTokens,
2568
+ reasoningTokens: state.tokenUsage.reasoningTokens,
2569
+ cacheReadInputTokens: state.tokenUsage.cacheReadInputTokens,
2570
+ cacheCreationInputTokens: state.tokenUsage.cacheCreationInputTokens,
2571
+ cost: state.tokenUsage.cost
2572
+ } : void 0,
2573
+ timing: {
2574
+ startedAt: startTime,
2575
+ completedAt: endTime,
2576
+ durationMs: endTime - startTime
2577
+ }
2578
+ };
2579
+ }
2580
+ function failOnProviderSessionPersistence(observer, state) {
2581
+ if (!observer.providerSessionPersistenceError) return;
2582
+ const message = observer.providerSessionPersistenceError instanceof Error ? observer.providerSessionPersistenceError.message : String(observer.providerSessionPersistenceError);
2583
+ state.terminalStatus = "failed";
2584
+ state.lastDiagnostic = `Provider session persistence failed: ${message}`;
2585
+ }
2586
+ function logAcceptedCliExit(logger, exitResult, stderr) {
2587
+ if (!exitResult.success) logger.info("CLI process exited non-zero but was treated as success by provider", {
2588
+ exitCode: exitResult.code,
2589
+ stderr: stderr.slice(0, 500)
2590
+ });
2591
+ if (stderr.trim()) logger.debug("CLI process stderr (success)", { stderr: stderr.slice(0, 1e3) });
2592
+ }
2593
+ //#endregion
2594
+ //#region src/part-update-queue.ts
2595
+ const MAX_PENDING_PART_UPDATES = 256;
2596
+ /**
2597
+ * Per-chunk persistence backpressure. Accumulated-text providers re-emit
2598
+ * the whole part on every token, and each update joins the recorder's
2599
+ * serialized write chain — if disk latency exceeds the token rate, the
2600
+ * chain grows for the turn's lifetime. Coalescing keeps only the newest
2601
+ * pending snapshot per partId (a stale one is a strict prefix of it), so
2602
+ * the queue stays O(distinct parts); a single drain loop preserves write
2603
+ * order. The terminal paths flush the drain before their final writes so
2604
+ * no stale snapshot can land after the consolidated parts.
2605
+ */
2606
+ var PartUpdateQueue = class {
2607
+ pending = /* @__PURE__ */ new Map();
2608
+ drain = null;
2609
+ constructor(recorder, logger) {
2610
+ this.recorder = recorder;
2611
+ this.logger = logger;
2612
+ }
2613
+ schedule(part, delta) {
2614
+ const partId = part.id;
2615
+ if (!partId) {
2616
+ this.recorder.recordAssistantPartUpdate?.(part, delta)?.catch((cause) => {
2617
+ this.logger.warn("Per-chunk persistence failed (continuing stream)", { error: cause instanceof Error ? cause.message : String(cause) });
2618
+ });
2619
+ return;
2620
+ }
2621
+ this.pending.set(partId, {
2622
+ part,
2623
+ delta
2624
+ });
2625
+ if (this.pending.size > MAX_PENDING_PART_UPDATES) {
2626
+ const oldestPending = this.pending.keys().next();
2627
+ if (!oldestPending.done) this.pending.delete(oldestPending.value);
2628
+ this.logger.warn("Part-update persist backlog overflow, dropped oldest pending snapshot", { pendingLimit: MAX_PENDING_PART_UPDATES });
2629
+ }
2630
+ if (!this.drain) this.drain = this.drainPending().finally(() => {
2631
+ this.drain = null;
2632
+ });
2633
+ }
2634
+ async flush() {
2635
+ while (this.drain) await this.drain;
2636
+ }
2637
+ async drainPending() {
2638
+ for (;;) {
2639
+ const oldest = this.pending.keys().next();
2640
+ if (oldest.done) break;
2641
+ const update = this.pending.get(oldest.value);
2642
+ this.pending.delete(oldest.value);
2643
+ if (!update) continue;
2644
+ try {
2645
+ await this.recorder.recordAssistantPartUpdate?.(update.part, update.delta);
2646
+ } catch (err) {
2647
+ this.logger.warn("Per-chunk persistence failed (continuing stream)", { error: err instanceof Error ? err.message : String(err) });
2648
+ }
2649
+ }
2650
+ }
2651
+ };
2652
+ //#endregion
2653
+ //#region src/repeated-tool-failure.ts
2654
+ const NO_REPAIR_TOOL_NAMES = /* @__PURE__ */ new Set();
2655
+ /** A part in one of these states has not resolved, so it decides nothing. */
2656
+ function isUnresolved(state) {
2657
+ return state.status === "pending" || state.status === "running";
2658
+ }
2659
+ function readString(input, key) {
2660
+ const value = input[key];
2661
+ return typeof value === "string" && value.trim() !== "" ? value : void 0;
2662
+ }
2663
+ /** The tool's own error text, for the states that carry one. */
2664
+ function readStateError(state) {
2665
+ if (state.status !== "error" && state.status !== "failed") return void 0;
2666
+ return state.error?.trim() ? state.error : void 0;
2667
+ }
2668
+ /**
2669
+ * Describe a resolved tool part as a failure, or return `undefined` when it
2670
+ * produced something.
2671
+ *
2672
+ * A repair tool reports the call it stood in for through its input, so the
2673
+ * trip names `write` rather than the substitute. That is the tool the reader
2674
+ * has to go and look at.
2675
+ */
2676
+ function describeFailure(part, repairToolNames) {
2677
+ const { state } = part;
2678
+ if (isUnresolved(state)) return void 0;
2679
+ if (repairToolNames.has(part.tool)) return {
2680
+ tool: readString(state.input, "tool") ?? part.tool,
2681
+ detail: readString(state.input, "error") ?? readStateError(state) ?? `${part.tool} reported a tool call it could not parse`
2682
+ };
2683
+ if (state.status !== "error" && state.status !== "failed") return void 0;
2684
+ return {
2685
+ tool: part.tool,
2686
+ detail: readStateError(state) ?? `${part.tool} failed without reporting a reason`
2687
+ };
2688
+ }
2689
+ function createRepeatedToolFailureGuard(options) {
2690
+ const { limit } = options;
2691
+ const repairToolNames = options.repairToolNames ?? NO_REPAIR_TOOL_NAMES;
2692
+ let signature;
2693
+ let previousInput;
2694
+ let count = 0;
2695
+ let lastScoredPartId;
2696
+ let tripped = false;
2697
+ return {
2698
+ /**
2699
+ * Feed one tool part. Returns the trip on the call that reaches the limit,
2700
+ * and `undefined` every other time. A tripped guard stays tripped and
2701
+ * reports once, so a caller can kill the turn without racing the events
2702
+ * still in flight behind it.
2703
+ */
2704
+ observe(part) {
2705
+ if (tripped) return void 0;
2706
+ if (isUnresolved(part.state)) return void 0;
2707
+ if (part.id === lastScoredPartId) return void 0;
2708
+ lastScoredPartId = part.id;
2709
+ const failure = describeFailure(part, repairToolNames);
2710
+ if (!failure) {
2711
+ signature = void 0;
2712
+ previousInput = void 0;
2713
+ count = 0;
2714
+ return;
2715
+ }
2716
+ const next = JSON.stringify([failure.tool, failure.detail]);
2717
+ count = next === signature && isDeepStrictEqual(part.state.input, previousInput) ? count + 1 : 1;
2718
+ previousInput = structuredClone(part.state.input);
2719
+ signature = next;
2720
+ if (count < limit) return void 0;
2721
+ tripped = true;
2722
+ return {
2723
+ ...failure,
2724
+ count
2725
+ };
2726
+ } };
2727
+ }
2728
+ //#endregion
2729
+ //#region src/turn-liveness.ts
2730
+ const STREAM_LIVENESS_QUIET_MS = 5e3;
2731
+ const STREAM_LIVENESS_CHECK_MS = 1e3;
2732
+ /**
2733
+ * How long the adapter vouches for an unbroken silence.
2734
+ *
2735
+ * A ping says the CLI process runs. It cannot say the model makes progress.
2736
+ * An unbounded ping stream would hide a CLI that starts and then hangs.
2737
+ *
2738
+ * No model in use here emits nothing for ten minutes. Past that the ticker
2739
+ * stops so the consumer can observe the silence. A later provider event
2740
+ * restarts the window.
2741
+ */
2742
+ const STREAM_LIVENESS_MAX_QUIET_MS = 6e5;
2743
+ /**
2744
+ * The clocks and running-tool bookkeeping behind the stream liveness ping.
2745
+ *
2746
+ * `lastProviderEventAt` moves only on a real provider event, and it bounds
2747
+ * how long one silence can run. `lastLivenessAt` also moves on each ping, and
2748
+ * it paces the pings.
2749
+ */
2750
+ var TurnLiveness = class {
2751
+ lastProviderEventAt = Date.now();
2752
+ lastLivenessAt = this.lastProviderEventAt;
2753
+ runningToolParts = /* @__PURE__ */ new Map();
2754
+ constructor(state, logger, onEvent) {
2755
+ this.state = state;
2756
+ this.logger = logger;
2757
+ this.onEvent = onEvent;
2758
+ }
2759
+ /** A real provider event arrived: reset both clocks. */
2760
+ noteProviderEvent() {
2761
+ this.lastProviderEventAt = Date.now();
2762
+ this.lastLivenessAt = this.lastProviderEventAt;
2763
+ }
2764
+ /** Track a tool part by its reported state; call after `noteProviderEvent`. */
2765
+ noteToolPart(id, tool, status) {
2766
+ if (status === "running") {
2767
+ if (!this.runningToolParts.has(id)) this.runningToolParts.set(id, {
2768
+ toolName: tool,
2769
+ startedAt: this.lastProviderEventAt
2770
+ });
2771
+ } else this.runningToolParts.delete(id);
2772
+ }
2773
+ start(executionSignal, startTime) {
2774
+ const livenessTicker = setInterval(() => {
2775
+ const now = Date.now();
2776
+ if (executionSignal.aborted || this.state.terminalStatus !== void 0 || now - this.lastLivenessAt < STREAM_LIVENESS_QUIET_MS || now - this.lastProviderEventAt > STREAM_LIVENESS_MAX_QUIET_MS) return;
2777
+ this.lastLivenessAt = now;
2778
+ const oldestRunningTool = this.runningToolParts.entries().next();
2779
+ const ping = oldestRunningTool.done ? {
2780
+ type: "model-processing",
2781
+ phase: "thinking",
2782
+ elapsedMs: now - startTime
2783
+ } : {
2784
+ type: "tool-heartbeat",
2785
+ toolName: oldestRunningTool.value[1].toolName,
2786
+ partId: oldestRunningTool.value[0],
2787
+ elapsedMs: now - oldestRunningTool.value[1].startedAt
2788
+ };
2789
+ try {
2790
+ this.onEvent?.(ping);
2791
+ } catch (error) {
2792
+ this.logger.warn("Stream liveness ping failed (continuing turn)", { error: error instanceof Error ? error.message : String(error) });
2793
+ }
2794
+ }, STREAM_LIVENESS_CHECK_MS);
2795
+ livenessTicker.unref();
2796
+ return livenessTicker;
2797
+ }
2798
+ };
2799
+ //#endregion
2800
+ //#region src/turn-observer.ts
2801
+ /**
2802
+ * How many times a tool call may be rejected identically in a row before the
2803
+ * adapter kills the turn.
2804
+ *
2805
+ * Three separates a model that recovers from one that cannot. The first
2806
+ * attempt is the mistake. The second is the model reading the error. A third
2807
+ * identical attempt says the error does not reach it. Against a staging run
2808
+ * that made 76 attempts, this cuts the turn on the third.
2809
+ */
2810
+ const REPEATED_TOOL_FAILURE_LIMIT = 3;
2811
+ /**
2812
+ * Observes one turn's provider stream: keeps the execution state current,
2813
+ * persists parts and the provider session id as they arrive, stops a turn
2814
+ * that repeats a rejected tool call, and pings while the stream is quiet.
2815
+ */
2816
+ var TurnObserver = class {
2817
+ providerSessionPersistence = Promise.resolve();
2818
+ providerSessionPersistenceError;
2819
+ nativeSessionObserved = false;
2820
+ partUpdates;
2821
+ liveness;
2822
+ repeatedToolFailureGuard;
2823
+ killForRepeatedToolFailure;
2824
+ constructor(options) {
2825
+ this.options = options;
2826
+ const { state, logger, recorder, repairToolNames, onEvent } = options;
2827
+ this.partUpdates = new PartUpdateQueue(recorder, logger);
2828
+ this.liveness = new TurnLiveness(state, logger, onEvent);
2829
+ this.repeatedToolFailureGuard = createRepeatedToolFailureGuard({
2830
+ limit: REPEATED_TOOL_FAILURE_LIMIT,
2831
+ repairToolNames
2832
+ });
2833
+ }
2834
+ flushPartUpdates() {
2835
+ return this.partUpdates.flush();
2836
+ }
2837
+ attachProcess(process) {
2838
+ this.killForRepeatedToolFailure = () => process.kill();
2839
+ }
2840
+ startLiveness(executionSignal, startTime) {
2841
+ return this.liveness.start(executionSignal, startTime);
2842
+ }
2843
+ onEvent = (event) => {
2844
+ const { provider, state, recorder } = this.options;
2845
+ this.liveness.noteProviderEvent();
2846
+ if (event.type === "message.part.updated" && event.part.type === "tool") this.observeToolPart(event.part);
2847
+ const providerSessionId = event.type === "native.session.observed" && event.provider === provider ? event.nativeSessionId : event.type === "session.updated" ? event.sessionId : void 0;
2848
+ if (event.type === "native.session.observed" && event.provider === provider) this.nativeSessionObserved = true;
2849
+ if (providerSessionId) {
2850
+ state.sessionId = providerSessionId;
2851
+ this.persistProviderSession(providerSessionId);
2852
+ }
2853
+ if (event.type === "warning") {
2854
+ state.lastDiagnostic = event.message;
2855
+ if (event.code === "EXECUTION_INTERRUPTED") state.failure ??= {
2856
+ code: event.code,
2857
+ message: event.message
2858
+ };
2859
+ }
2860
+ if (event.type === "status" && (event.status === "completed" || event.status === "failed")) {
2861
+ state.terminalStatus = event.status;
2862
+ if (event.status === "failed" && event.detail) state.lastDiagnostic = event.detail;
2863
+ }
2864
+ if (event.type === "message.part.updated" && recorder.recordAssistantPartUpdate) this.partUpdates.schedule(event.part, event.delta);
2865
+ this.options.onEvent?.(event);
2866
+ };
2867
+ observeToolPart(part) {
2868
+ const { provider, state, logger, sessionId } = this.options;
2869
+ const { id, tool, state: toolState } = part;
2870
+ this.liveness.noteToolPart(id, tool, toolState.status);
2871
+ const repeated = this.repeatedToolFailureGuard.observe({
2872
+ id,
2873
+ tool,
2874
+ state: toolState
2875
+ });
2876
+ if (repeated) {
2877
+ const detail = `${provider} sent the same ${repeated.tool} call ${repeated.count} times and every one was rejected. The turn stopped before it ran the model again: ${repeated.detail}`;
2878
+ state.endReason = "repeated_tool_failure";
2879
+ state.terminalStatus = "failed";
2880
+ state.lastDiagnostic = detail;
2881
+ logger.error("Repeated tool-call rejection stopped the turn", {
2882
+ tool: repeated.tool,
2883
+ attempts: repeated.count,
2884
+ reason: repeated.detail,
2885
+ sessionId
2886
+ });
2887
+ this.killForRepeatedToolFailure?.();
2888
+ }
2889
+ }
2890
+ persistProviderSession(providerSessionId) {
2891
+ const { getRuntimeHome, getSessionStore, sessionId } = this.options;
2892
+ this.providerSessionPersistence = this.providerSessionPersistence.then(async () => {
2893
+ const runtimeHome = getRuntimeHome();
2894
+ if (!await getSessionStore().update(sessionId, {
2895
+ providerSessionId,
2896
+ ...runtimeHome?.trim() ? { providerSessionHome: runtimeHome.trim() } : {}
2897
+ })) throw new Error(`Session ${sessionId} disappeared before its provider session could be persisted`);
2898
+ }).catch((error) => {
2899
+ this.providerSessionPersistenceError ??= error;
2900
+ });
2901
+ }
2902
+ };
2903
+ //#endregion
2904
+ //#region src/turn-process.ts
2905
+ /** How one turn's argv runs: cwd, environment, identity or launcher, stdio. */
2906
+ function cliSpawnConfig(params) {
2907
+ const { execArgs, launcher, subprocessUser } = params;
2908
+ const stdio = !execArgs.useStdin ? { stdio: [
2909
+ "ignore",
2910
+ "pipe",
2911
+ "pipe"
2912
+ ] } : {};
2913
+ return {
2914
+ command: execArgs.command,
2915
+ args: execArgs.args,
2916
+ cwd: execArgs.cwd ?? params.workspaceRoot,
2917
+ env: params.env,
2918
+ inheritEnv: params.inheritEnv,
2919
+ ...params.ownProcessGroup ? { ownProcessGroup: true } : {},
2920
+ ...launcher ? {
2921
+ launcher,
2922
+ spawnOptions: stdio
2923
+ } : {
2924
+ ...subprocessUser ? { isolatedIdentity: subprocessUser } : {},
2925
+ spawnOptions: {
2926
+ ...params.subprocessSpawnOptions,
2927
+ ...stdio
2928
+ }
2929
+ }
2930
+ };
2931
+ }
2932
+ /** A started CLI as the turn drives it, timing its spawn and first output. */
2933
+ function cliTurnProcess(child, logger, spawnStarted) {
2934
+ if (logger.observePhase) {
2935
+ child.process.once("spawn", () => {
2936
+ logger.observePhase?.("cli_spawn", performance.now() - spawnStarted);
2937
+ });
2938
+ child.process.stdout?.once("data", () => {
2939
+ logger.observePhase?.("cli_boot", performance.now() - spawnStarted);
2940
+ });
2941
+ }
2942
+ return {
2943
+ pid: child.pid,
2944
+ stderr: child.process.stderr ?? void 0,
2945
+ write: child.write,
2946
+ closeInput: child.closeInput,
2947
+ kill: child.kill,
2948
+ exitPromise: child.exitPromise,
2949
+ beginRawCapture: child.beginRawCapture
2950
+ };
2951
+ }
2952
+ //#endregion
2953
+ //#region src/base-adapter.ts
2954
+ /** Base adapter for CLI-based AI agent providers. */
2955
+ var CliProviderAdapterBase = class extends CliAdapterMcp {
2956
+ supportsManagedQuote = false;
2957
+ async execute(input, services, onEvent) {
2958
+ this.ensureInitialized();
2959
+ const quoteVersion = input.managedQuoteVersion;
2960
+ if (quoteVersion !== void 0 && !this.supportsManagedQuote) throw new Error(`${this.provider} does not support the managed-quote native model transport`);
2961
+ const correlationHeaders = executionCorrelationHeaders(input.interactionBinding, input.traceId, quoteVersion);
2962
+ return this.withExecution(input.abortSignal, (signal) => this.executeInternal(input, services, onEvent, signal, correlationHeaders));
2963
+ }
2964
+ async executeInternal(input, services, onEvent, executionSignal, correlationHeaders) {
2965
+ const startTime = Date.now();
2966
+ const recorder = services.recorder;
2967
+ const opened = await this.openTurnSession(input, recorder, onEvent);
2968
+ if (opened.cached) return opened.cached;
2969
+ const { sessionId, providerSessionId, providerMessageIds } = opened;
2970
+ const { executionProfile, profilePlan } = await this.materializeTurnProfile(input, sessionId).catch(rejectNotLaunched);
2971
+ const { preparedInput, promptText } = await this.recordTurnPrompt(input, opened, recorder).catch(rejectNotLaunched);
2972
+ onEvent?.({
2973
+ type: "status",
2974
+ status: "started"
2975
+ });
2976
+ const launch = await this.planLaunch({
2977
+ input,
2978
+ sessionId,
2979
+ providerSessionId,
2980
+ executionSignal,
2981
+ correlationHeaders,
2982
+ executionProfile,
2983
+ profilePlan,
2984
+ attachments: preparedInput.attachments,
2985
+ promptText
2986
+ });
2987
+ const { workspaceRoot } = launch;
2988
+ const state = createExecutionState(sessionId, providerMessageIds?.assistantMessageId, workspaceRoot);
2989
+ const observer = new TurnObserver({
2990
+ provider: this.provider,
2991
+ state,
2992
+ sessionId,
2993
+ getRuntimeHome: () => this.config.runtimeHome,
2994
+ recorder,
2995
+ getSessionStore: () => this.getSessionStore(),
2996
+ logger: this.logger,
2997
+ repairToolNames: this.toolCallRepairToolNames(),
2998
+ onEvent
2999
+ });
3000
+ await this.bindNativeSession({
3001
+ launch,
3002
+ providerSessionId,
3003
+ observer
3004
+ });
3005
+ const { exitResult, stderr } = await this.runCliTurn({
3006
+ input,
3007
+ sessionId,
3008
+ providerSessionId,
3009
+ executionSignal,
3010
+ launch,
3011
+ promptText,
3012
+ state,
3013
+ observer,
3014
+ startTime
3015
+ });
3016
+ await observer.providerSessionPersistence;
3017
+ failOnProviderSessionPersistence(observer, state);
3018
+ const endTime = Date.now();
3019
+ const benignNonZeroExit = !exitResult.success && !exitResult.timedOut && exitResult.stdinError === void 0 && this.isBenignNonZeroExit({
3020
+ exitCode: exitResult.code,
3021
+ stderr,
3022
+ state
3023
+ });
3024
+ if (!exitResult.success && !benignNonZeroExit || state.terminalStatus === "failed" || state.endReason) return this.failCliTurn({
3025
+ aborted: executionSignal.aborted,
3026
+ onEvent,
3027
+ recorder,
3028
+ observer,
3029
+ state,
3030
+ exitResult,
3031
+ stderr,
3032
+ launch,
3033
+ startTime,
3034
+ endTime
3035
+ });
3036
+ logAcceptedCliExit(this.logger, exitResult, stderr);
3037
+ return this.completeTurn({
3038
+ input,
3039
+ sessionId,
3040
+ state,
3041
+ observer,
3042
+ recorder,
3043
+ providerMessageIds,
3044
+ profilePlan,
3045
+ startTime,
3046
+ endTime
3047
+ });
3048
+ }
3049
+ async completeTurn(params) {
3050
+ const { input, sessionId, state, recorder, profilePlan } = params;
3051
+ if (state.sessionId && sessionId && state.sessionId !== sessionId) await this.getSessionStore().update(sessionId, {
3052
+ providerSessionId: state.sessionId,
3053
+ ...this.config.runtimeHome?.trim() ? { providerSessionHome: this.config.runtimeHome.trim() } : {}
3054
+ });
3055
+ const assistantParts = this.buildAssistantParts(state);
3056
+ await params.observer?.flushPartUpdates();
3057
+ await recorder.appendAssistantParts(assistantParts);
3058
+ await this.getMessageStore().append({
3059
+ sessionId,
3060
+ ...params.providerMessageIds ? { id: params.providerMessageIds.assistantMessageId } : {},
3061
+ role: "assistant",
3062
+ parts: assistantParts,
3063
+ tokenUsage: state.tokenUsage ? {
3064
+ inputTokens: state.tokenUsage.inputTokens,
3065
+ outputTokens: state.tokenUsage.outputTokens
3066
+ } : void 0
3067
+ });
3068
+ const publicProfileRevision = profilePlan ? profileRevisionReceipts(this).public.get(profilePlan) : void 0;
3069
+ const result = buildExecutionResult({
3070
+ outcome: await this.executionOutcome(state, input),
3071
+ state,
3072
+ toolInvocations: this.extractToolInvocations(state),
3073
+ sessionId,
3074
+ publicProfileRevision,
3075
+ startTime: params.startTime,
3076
+ endTime: params.endTime
3077
+ });
3078
+ if (recorder.markTurnCompleted) await recorder.markTurnCompleted({ result });
3079
+ return result;
3080
+ }
3081
+ /** Report and record a failed turn, then throw its error. */
3082
+ async failCliTurn(params) {
3083
+ const { onEvent, recorder, observer, state, exitResult, stderr } = params;
3084
+ const { launch, startTime, endTime } = params;
3085
+ const { errorMessage, failure } = endFailedTurn({
3086
+ provider: this.provider,
3087
+ state,
3088
+ exitResult,
3089
+ stderr,
3090
+ aborted: params.aborted,
3091
+ command: launch.execArgs.command,
3092
+ nixBinPath: launch.env.NIX_BIN_PATH,
3093
+ startTime,
3094
+ endTime
3095
+ });
3096
+ this.logger.error("CLI process failed", {
3097
+ exitCode: exitResult.code,
3098
+ timedOut: exitResult.timedOut,
3099
+ timeoutReason: exitResult.timeoutReason,
3100
+ timeoutDiagnostic: exitResult.timeoutDiagnostic,
3101
+ spawnError: exitResult.spawnError,
3102
+ stdinError: exitResult.stdinError,
3103
+ stderr: stderr.slice(0, 2e3),
3104
+ diagnostic: state.lastDiagnostic,
3105
+ durationMs: endTime - startTime,
3106
+ command: launch.execArgs.command
3107
+ });
3108
+ if (exitResult.timedOut) onEvent?.({
3109
+ type: "warning",
3110
+ code: "TIMEOUT",
3111
+ message: exitResult.timeoutDiagnostic ?? "Execution timed out"
3112
+ });
3113
+ onEvent?.({
3114
+ type: "status",
3115
+ status: "failed",
3116
+ detail: errorMessage
3117
+ });
3118
+ await recordTurnInterruption({
3119
+ recorder,
3120
+ observer,
3121
+ logger: this.logger,
3122
+ errorMessage,
3123
+ exitResult,
3124
+ failure
3125
+ });
3126
+ throw failure;
3127
+ }
3128
+ async openTurnSession(input, recorder, onEvent) {
3129
+ const sessionPrepareStarted = performance.now();
3130
+ const adapter = this;
3131
+ const prepared = await prepareExecutionSession({
3132
+ get config() {
3133
+ return adapter.config;
3134
+ },
3135
+ get logger() {
3136
+ return adapter.logger;
3137
+ },
3138
+ get storagePath() {
3139
+ return adapter.storagePath;
3140
+ },
3141
+ get getSessionStore() {
3142
+ return adapter.getSessionStore.bind(adapter);
3143
+ },
3144
+ get getMessageStore() {
3145
+ return adapter.getMessageStore.bind(adapter);
3146
+ },
3147
+ get resumableProviderSessionId() {
3148
+ return adapter.resumableProviderSessionId.bind(adapter);
3149
+ },
3150
+ get createSession() {
3151
+ return adapter.createSession.bind(adapter);
3152
+ }
3153
+ }, input).catch(rejectNotLaunched);
3154
+ this.logger.observePhase?.("session_prepare", performance.now() - sessionPrepareStarted);
3155
+ this.logger.observePhase?.(prepared.transcriptReplay ? "history_replay" : prepared.providerSessionId ? "native_resume" : "native_session_new", 0);
3156
+ const providerMessageIds = readProviderMessageIds(input);
3157
+ const cached = await findCachedTurn(recorder, input, this.logger, prepared.sessionId);
3158
+ if (cached) {
3159
+ onEvent?.({
3160
+ type: "status",
3161
+ status: "completed"
3162
+ });
3163
+ return {
3164
+ ...prepared,
3165
+ providerMessageIds,
3166
+ cached
3167
+ };
3168
+ }
3169
+ if (input.turnId) recorder.setTurnId?.(input.turnId);
3170
+ return {
3171
+ ...prepared,
3172
+ providerMessageIds,
3173
+ cached: void 0
3174
+ };
3175
+ }
3176
+ async materializeTurnProfile(input, sessionId) {
3177
+ const executionProfile = this.resolveExecutionProfileForInput(input);
3178
+ const profilePlan = await this.materializeProfileResources(executionProfile, input.workspaceRoot ?? this.storagePath, input.userId ?? sessionId, sessionId);
3179
+ assertExecutionProfileRevision(this, () => this.config, input, profilePlan);
3180
+ return {
3181
+ executionProfile,
3182
+ profilePlan
3183
+ };
3184
+ }
3185
+ async recordTurnPrompt(input, session, recorder, attachmentOptions) {
3186
+ const { sessionId, providerMessageIds } = session;
3187
+ const preparedInput = await this.preparePromptInput(normalizeInputParts(input), input.workspaceRoot ?? this.storagePath, sessionId, attachmentOptions);
3188
+ const transcriptContext = await buildTranscriptContext({
3189
+ provider: this.provider,
3190
+ sessionId,
3191
+ transcriptReplay: session.transcriptReplay,
3192
+ secretProfileRevisionRoot: this.config.secretProfileRevisionRoot,
3193
+ getMessageStore: () => this.getMessageStore()
3194
+ });
3195
+ await recorder.recordUserMessage(preparedInput.persistedParts);
3196
+ await this.getMessageStore().append({
3197
+ sessionId,
3198
+ ...providerMessageIds ? { id: providerMessageIds.userMessageId } : {},
3199
+ role: "user",
3200
+ parts: preparedInput.persistedParts
3201
+ });
3202
+ return {
3203
+ preparedInput,
3204
+ promptText: this.composePromptText(prependPromptSection(transcriptContext, preparedInput.promptText), input)
3205
+ };
3206
+ }
3207
+ /** Resolve this turn's config, environment and argv, and check them against the session contract. */
3208
+ async planLaunch(params) {
3209
+ const { input, sessionId, providerSessionId, executionSignal, correlationHeaders, executionProfile, profilePlan, attachments, promptText } = params;
3210
+ const workspaceRoot = input.workspaceRoot ?? this.storagePath;
3211
+ const turnConfig = withTurnInteractions(this.config, input);
3212
+ const executionConfig = this.withProfilePlan(turnConfig, profilePlan);
3213
+ const sessionContract = this.sessionContract();
3214
+ const executionModel = this.resolveProfileModel(input.model?.model);
3215
+ const env = this.buildTurnEnvironment(executionConfig, profilePlan, workspaceRoot, correlationHeaders);
3216
+ const buildExecArgs = (mintedSessionId) => this.buildExecArgs({
3217
+ message: promptText,
3218
+ config: executionConfig,
3219
+ correlationHeaders,
3220
+ model: executionModel,
3221
+ sessionId: providerSessionId,
3222
+ ...mintedSessionId ? { mintedSessionId } : {},
3223
+ workspaceRoot,
3224
+ ...this.resolveProfilePrompts(input, profilePlan, executionProfile),
3225
+ attachments,
3226
+ profilePlan
3227
+ });
3228
+ const timeoutMs = input.timeoutMs;
3229
+ const mintedSessionId = sessionContract?.identity.kind === "pre-minted" && !providerSessionId ? params.reserveHeldProcess === false ? randomUUID() : await this.mintNativeSessionId({
3230
+ sessionId,
3231
+ workspaceRoot,
3232
+ config: executionConfig,
3233
+ model: executionModel,
3234
+ env,
3235
+ signal: executionSignal,
3236
+ buildExecArgs,
3237
+ input,
3238
+ ...timeoutMs === void 0 ? {} : { timeoutMs }
3239
+ }) : void 0;
3240
+ const execArgs = buildExecArgs(mintedSessionId);
3241
+ if (sessionContract) {
3242
+ assertArgvHonoursSessionContract(this.provider, sessionContract, execArgs.args, { ...mintedSessionId ? { mintedId: mintedSessionId } : {} });
3243
+ const runtimeHome = this.config.runtimeHome?.trim();
3244
+ if (runtimeHome && sessionContractFullyEnforced(this.provider)) assertStoreLocationHonoursSessionContract(this.provider, sessionContract, {
3245
+ runtimeHome,
3246
+ cwd: workspaceRoot,
3247
+ env,
3248
+ argv: execArgs.args
3249
+ });
3250
+ }
3251
+ this.logger.debug("CLI execution", {
3252
+ command: execArgs.command,
3253
+ cwd: workspaceRoot,
3254
+ hasSessionId: !!providerSessionId
3255
+ });
3256
+ return {
3257
+ workspaceRoot,
3258
+ executionConfig,
3259
+ executionModel,
3260
+ env,
3261
+ sessionContract,
3262
+ timeoutMs,
3263
+ mintedSessionId,
3264
+ execArgs
3265
+ };
3266
+ }
3267
+ /** Reserve and announce the native session id a pre-minted harness already has. */
3268
+ async bindNativeSession(params) {
3269
+ const { launch, providerSessionId, observer } = params;
3270
+ const { workspaceRoot, sessionContract, mintedSessionId } = launch;
3271
+ const nativeBindStarted = performance.now();
3272
+ if (mintedSessionId) await this.reserveNativeSession(mintedSessionId, { workspaceRoot });
3273
+ const knownNativeSessionId = sessionContract?.identity.kind === "pre-minted" ? mintedSessionId ?? providerSessionId : void 0;
3274
+ if (knownNativeSessionId) {
3275
+ observer.onEvent({
3276
+ type: "native.session.observed",
3277
+ provider: this.provider,
3278
+ nativeSessionId: knownNativeSessionId
3279
+ });
3280
+ await observer.providerSessionPersistence;
3281
+ if (observer.providerSessionPersistenceError) throw observer.providerSessionPersistenceError;
3282
+ this.logger.observePhase?.("native_session_bind", performance.now() - nativeBindStarted);
3283
+ }
3284
+ }
3285
+ async runCliTurn(params) {
3286
+ const { input, sessionId, providerSessionId, executionSignal } = params;
3287
+ const { launch, promptText, state, observer, startTime } = params;
3288
+ const { execArgs, workspaceRoot, env } = launch;
3289
+ const parserConfig = this.createEventParser(state, observer.onEvent);
3290
+ const spawnedProcess = this.spawnExecutionProcess({
3291
+ input,
3292
+ sessionId,
3293
+ providerSessionId,
3294
+ config: launch.executionConfig,
3295
+ execArgs,
3296
+ workspaceRoot,
3297
+ env,
3298
+ signal: executionSignal,
3299
+ timeoutMs: launch.timeoutMs,
3300
+ parser: parserConfig
3301
+ });
3302
+ const cliProcess = spawnedProcess instanceof Promise ? await spawnedProcess : spawnedProcess;
3303
+ this.activeProcesses.add(cliProcess);
3304
+ observer.attachProcess(cliProcess);
3305
+ const stopSessionWatch = watchNewSessionIds({
3306
+ provider: this.provider,
3307
+ runtimeHome: this.config.runtimeHome,
3308
+ sessionContract: launch.sessionContract,
3309
+ observer
3310
+ });
3311
+ if (providerSessionId && launch.sessionContract?.identity.kind === "first-event") observer.onEvent({
3312
+ type: "native.session.observed",
3313
+ provider: this.provider,
3314
+ nativeSessionId: providerSessionId
3315
+ });
3316
+ const identityDeadline = armNativeSessionDeadline({
3317
+ provider: this.provider,
3318
+ sessionId,
3319
+ sessionContract: launch.sessionContract,
3320
+ state,
3321
+ observer,
3322
+ logger: this.logger,
3323
+ kill: () => cliProcess.kill()
3324
+ });
3325
+ const stderr = collectStderr(cliProcess);
3326
+ const livenessTicker = observer.startLiveness(executionSignal, startTime);
3327
+ let exitResult;
3328
+ try {
3329
+ if (execArgs.useStdin) {
3330
+ cliProcess.write(this.formatStdinMessage(execArgs.stdin || promptText));
3331
+ cliProcess.closeInput();
3332
+ }
3333
+ exitResult = await cliProcess.exitPromise;
3334
+ } catch (error) {
3335
+ cliProcess.kill();
3336
+ await cliProcess.exitPromise;
3337
+ throw error;
3338
+ } finally {
3339
+ stopSessionWatch();
3340
+ clearInterval(livenessTicker);
3341
+ if (identityDeadline) clearTimeout(identityDeadline);
3342
+ this.activeProcesses.delete(cliProcess);
3343
+ }
3344
+ await this.afterCliProcessExit({
3345
+ state,
3346
+ stderr: stderr.text,
3347
+ exit: exitResult,
3348
+ workspaceRoot,
3349
+ env,
3350
+ onEvent: observer.onEvent
3351
+ });
3352
+ return {
3353
+ exitResult,
3354
+ stderr: stderr.text
3355
+ };
3356
+ }
3357
+ /** How one turn's argv runs: cwd, environment, identity or launcher, stdio. */
3358
+ turnSpawnConfig(execArgs, workspaceRoot, env) {
3359
+ return cliSpawnConfig({
3360
+ execArgs,
3361
+ workspaceRoot,
3362
+ env,
3363
+ inheritEnv: this.inheritProcessEnv,
3364
+ launcher: this.launcher,
3365
+ ownProcessGroup: this.ownProcessGroup,
3366
+ subprocessUser: this.config.subprocessUser,
3367
+ subprocessSpawnOptions: this.subprocessSpawnOptions()
3368
+ });
3369
+ }
3370
+ /** The launch one turn would make, without starting it: the spawn config (argv, cwd, environment, identity or launcher). */
3371
+ async prepareLaunch(input, options = {}) {
3372
+ this.ensureInitialized();
3373
+ const sessionId = input.sessionId ?? randomUUID();
3374
+ const { executionProfile, profilePlan } = await this.materializeTurnProfile(input, sessionId);
3375
+ const workspaceRoot = input.workspaceRoot ?? this.storagePath;
3376
+ const preparedInput = await this.preparePromptInput(normalizeInputParts(input), workspaceRoot, sessionId);
3377
+ const promptText = this.composePromptText(preparedInput.promptText, input);
3378
+ const launch = await this.planLaunch({
3379
+ input,
3380
+ sessionId,
3381
+ providerSessionId: options.providerSessionId,
3382
+ executionSignal: input.abortSignal ?? new AbortController().signal,
3383
+ correlationHeaders: executionCorrelationHeaders(input.interactionBinding, input.traceId),
3384
+ executionProfile,
3385
+ profilePlan,
3386
+ attachments: preparedInput.attachments,
3387
+ promptText,
3388
+ reserveHeldProcess: false
3389
+ });
3390
+ const { execArgs } = launch;
3391
+ return {
3392
+ spawn: this.turnSpawnConfig(execArgs, launch.workspaceRoot, launch.env),
3393
+ ...execArgs.useStdin ? { stdin: this.formatStdinMessage(execArgs.stdin || promptText) } : {},
3394
+ ...launch.mintedSessionId ? { nativeSessionId: launch.mintedSessionId } : {},
3395
+ ...profilePlan ? { workspacePlan: profilePlan.receipt } : {}
3396
+ };
3397
+ }
3398
+ spawnExecutionProcess(request) {
3399
+ const { execArgs, workspaceRoot, env, signal, timeoutMs, parser } = request;
3400
+ const spawnStarted = performance.now();
3401
+ const config = {
3402
+ ...this.turnSpawnConfig(execArgs, workspaceRoot, env),
3403
+ signal,
3404
+ ...globalThis.process.getuid?.() === 0 && this.config.runtimeHome?.trim() ? {
3405
+ rawCapturePath: resolveRawCapturePath(this.config.runtimeHome.trim()),
3406
+ ...request.deferRawCapture ? { rawCaptureDeferred: true } : {}
3407
+ } : {},
3408
+ ...timeoutMs === void 0 ? {} : { timeoutMs }
3409
+ };
3410
+ const turnProcess = (process) => cliTurnProcess(process, this.logger, spawnStarted);
3411
+ return this.launcher ? launchCliProcess(config, parser).then(turnProcess) : turnProcess(spawnCliProcess(config, parser));
3412
+ }
3413
+ };
3414
+ //#endregion
3415
+ //#region src/metering.ts
3416
+ const UsageRecordSchema = z.record(z.string(), z.unknown());
3417
+ const UsageNumberSchema = z.union([z.number(), z.string().trim().min(1)]).transform(Number).pipe(z.number().finite().nonnegative());
3418
+ const TOKEN_USAGE_INPUT_KEYS = Object.freeze([
3419
+ "inputTokens",
3420
+ "input",
3421
+ "input_tokens",
3422
+ "promptTokens",
3423
+ "prompt_tokens"
3424
+ ]);
3425
+ const TOKEN_USAGE_OUTPUT_KEYS = Object.freeze([
3426
+ "outputTokens",
3427
+ "output",
3428
+ "output_tokens",
3429
+ "completionTokens",
3430
+ "completion_tokens"
3431
+ ]);
3432
+ const TOKEN_USAGE_TOTAL_KEYS = Object.freeze([
3433
+ "totalTokens",
3434
+ "total_tokens",
3435
+ "total"
3436
+ ]);
3437
+ const TOKEN_USAGE_REASONING_KEYS = Object.freeze([
3438
+ "reasoningTokens",
3439
+ "reasoning_tokens",
3440
+ "reasoning",
3441
+ "reasoning_output_tokens"
3442
+ ]);
3443
+ const TOKEN_USAGE_COST_KEYS = Object.freeze([
3444
+ "totalCostUsd",
3445
+ "costUsd",
3446
+ "total_cost_usd",
3447
+ "cost_usd",
3448
+ "cost"
3449
+ ]);
3450
+ const CACHE_READ_INPUT_KEYS = Object.freeze([
3451
+ "cacheReadInputTokens",
3452
+ "cache_read_input_tokens",
3453
+ "cacheRead",
3454
+ "cache_read",
3455
+ "cached_input_tokens"
3456
+ ]);
3457
+ const CACHE_CREATION_INPUT_KEYS = Object.freeze([
3458
+ "cacheCreationInputTokens",
3459
+ "cache_creation_input_tokens",
3460
+ "cacheWrite",
3461
+ "cache_write"
3462
+ ]);
3463
+ const NESTED_COST_KEYS = Object.freeze([
3464
+ "total",
3465
+ "totalUsd",
3466
+ "total_usd",
3467
+ "usd"
3468
+ ]);
3469
+ function asRecord(raw) {
3470
+ const parsed = UsageRecordSchema.safeParse(raw);
3471
+ return parsed.success ? parsed.data : void 0;
3472
+ }
3473
+ function tokenCount(raw) {
3474
+ const parsed = UsageNumberSchema.safeParse(raw);
3475
+ return parsed.success ? Math.trunc(parsed.data) : void 0;
3476
+ }
3477
+ function firstTokenCount(source, keys) {
3478
+ for (const key of keys) {
3479
+ const count = tokenCount(source[key]);
3480
+ if (count !== void 0) return count;
3481
+ }
3482
+ }
3483
+ function firstUsageCostUsd(source, keys = TOKEN_USAGE_COST_KEYS) {
3484
+ for (const key of keys) {
3485
+ const parsed = UsageNumberSchema.safeParse(source[key]);
3486
+ if (parsed.success) return parsed.data;
3487
+ }
3488
+ }
3489
+ function usageRecord(source) {
3490
+ return asRecord(source.tokenUsage) ?? asRecord(source.usage) ?? source;
3491
+ }
3492
+ function readTokenUsage(source) {
3493
+ const usageSource = usageRecord(source);
3494
+ const inputTokens = firstTokenCount(usageSource, TOKEN_USAGE_INPUT_KEYS);
3495
+ const outputTokens = firstTokenCount(usageSource, TOKEN_USAGE_OUTPUT_KEYS);
3496
+ return inputTokens !== void 0 || outputTokens !== void 0 ? {
3497
+ inputTokens,
3498
+ outputTokens
3499
+ } : void 0;
3500
+ }
3501
+ function readTokenCostUsd(source) {
3502
+ const usageSource = usageRecord(source);
3503
+ return firstUsageCostUsd(source) ?? firstUsageCostUsd(usageSource) ?? readNestedCostUsd(source) ?? readNestedCostUsd(usageSource);
3504
+ }
3505
+ function readNestedCostUsd(source) {
3506
+ const cost = asRecord(source.cost);
3507
+ return cost ? firstUsageCostUsd(cost, NESTED_COST_KEYS) : void 0;
3508
+ }
3509
+ function readCliTokenUsage(raw) {
3510
+ const parsed = UsageRecordSchema.safeParse(raw);
3511
+ if (!parsed.success) return void 0;
3512
+ const source = parsed.data;
3513
+ const usageSource = usageRecord(source);
3514
+ const usage = readTokenUsage(source) ?? readTokenUsage(usageSource);
3515
+ const cost = readTokenCostUsd(source);
3516
+ const totalTokens = firstTokenCount(usageSource, TOKEN_USAGE_TOTAL_KEYS);
3517
+ const reasoningTokens = firstTokenCount(usageSource, TOKEN_USAGE_REASONING_KEYS);
3518
+ const cacheReadInputTokens = firstTokenCount(usageSource, CACHE_READ_INPUT_KEYS);
3519
+ const cacheCreationInputTokens = firstTokenCount(usageSource, CACHE_CREATION_INPUT_KEYS);
3520
+ if (!usage && totalTokens === void 0 && reasoningTokens === void 0 && cost === void 0 && cacheReadInputTokens === void 0 && cacheCreationInputTokens === void 0) return;
3521
+ return {
3522
+ inputTokens: usage?.inputTokens,
3523
+ outputTokens: usage?.outputTokens,
3524
+ totalTokens,
3525
+ reasoningTokens,
3526
+ cacheReadInputTokens,
3527
+ cacheCreationInputTokens,
3528
+ cost
3529
+ };
3530
+ }
3531
+ function recordCliTokenUsage(state, ...sources) {
3532
+ for (const source of sources) {
3533
+ const tokenUsage = readCliTokenUsage(source);
3534
+ if (tokenUsage) {
3535
+ state.tokenUsage = tokenUsage;
3536
+ return true;
3537
+ }
3538
+ }
3539
+ return false;
3540
+ }
3541
+ function addUsage(left, right) {
3542
+ if (left === void 0) return right;
3543
+ if (right === void 0) return left;
3544
+ const sum = left + right;
3545
+ return Number.isFinite(sum) ? sum : Math.max(left, right);
3546
+ }
3547
+ /**
3548
+ * Adds one usage report to the running total instead of replacing it.
3549
+ *
3550
+ * For a CLI that reports usage PER TURN (pi) rather than once per run,
3551
+ * `recordCliTokenUsage` would leave the execution billed as its final turn
3552
+ * alone. Use this wherever a single execution can produce more than one usage
3553
+ * report; use `recordCliTokenUsage` when the CLI emits one cumulative total.
3554
+ *
3555
+ * Returns true when a usage report was found in `sources` and folded in.
3556
+ */
3557
+ function accumulateCliTokenUsage(state, ...sources) {
3558
+ for (const source of sources) {
3559
+ const tokenUsage = readCliTokenUsage(source);
3560
+ if (!tokenUsage) continue;
3561
+ const total = state.tokenUsage;
3562
+ state.tokenUsage = total ? {
3563
+ inputTokens: addUsage(total.inputTokens, tokenUsage.inputTokens),
3564
+ outputTokens: addUsage(total.outputTokens, tokenUsage.outputTokens),
3565
+ totalTokens: addUsage(total.totalTokens, tokenUsage.totalTokens),
3566
+ reasoningTokens: addUsage(total.reasoningTokens, tokenUsage.reasoningTokens),
3567
+ cacheReadInputTokens: addUsage(total.cacheReadInputTokens, tokenUsage.cacheReadInputTokens),
3568
+ cacheCreationInputTokens: addUsage(total.cacheCreationInputTokens, tokenUsage.cacheCreationInputTokens),
3569
+ cost: addUsage(total.cost, tokenUsage.cost)
3570
+ } : tokenUsage;
3571
+ return true;
3572
+ }
3573
+ return false;
3574
+ }
3575
+ function parseCliUsageLine(line) {
3576
+ if (!line.startsWith("usage:")) return void 0;
3577
+ const raw = line.slice(6).trim();
3578
+ if (!raw) return void 0;
3579
+ if (raw.startsWith("{")) return asRecord(JSON.parse(raw));
3580
+ const fields = {};
3581
+ for (const part of raw.split(/[,\s]+/)) {
3582
+ if (!part) continue;
3583
+ const separator = part.includes("=") ? "=" : part.includes(":") ? ":" : "";
3584
+ if (!separator) continue;
3585
+ const [key, value] = part.split(separator, 2);
3586
+ if (!key || value === void 0) continue;
3587
+ const numeric = Number(value);
3588
+ fields[key] = Number.isFinite(numeric) ? numeric : value;
3589
+ }
3590
+ return Object.keys(fields).length > 0 ? fields : void 0;
3591
+ }
3592
+ //#endregion
3593
+ //#region src/acp-session-adapter.ts
3594
+ const DEFAULT_PROMPT_TIMEOUT_MS = 6e5;
3595
+ const DEFAULT_BOOTSTRAP_TIMEOUT_MS = 12e4;
3596
+ const DEFAULT_BRIDGE_IDLE_MS = 300 * 1e3;
3597
+ const ABORT_GRACE_MS = 250;
3598
+ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3599
+ acpSessions = /* @__PURE__ */ new Map();
3600
+ spawnBridge(request) {
3601
+ return spawnAcpBridge(request);
3602
+ }
3603
+ checkAgentProtocolVersion(_advertised, _turn) {}
3604
+ resumeSessionId() {}
3605
+ async onSessionOpened(_bridge, _acpSessionId, _turn, _options) {}
3606
+ onBridgeSpawned(_bridge, _sessionId, _turn) {}
3607
+ beginTurn(_sessionId, _input, _onEvent) {
3608
+ return () => {};
3609
+ }
3610
+ onToolFailed(_sessionId, _failureText) {
3611
+ return [];
3612
+ }
3613
+ takeTurnFailure(_sessionId) {}
3614
+ onSessionEvicted(_sessionId) {}
3615
+ turnLogContext(_turn) {
3616
+ return {};
3617
+ }
3618
+ async execute(input, services, onEvent) {
3619
+ this.ensureInitialized();
3620
+ const turn = this.resolveTurn(input);
3621
+ const { config } = turn;
3622
+ const { recorder } = services;
3623
+ const startTime = Date.now();
3624
+ const workspaceRoot = input.workspaceRoot ?? this.storagePath;
3625
+ const turnInput = input.sessionId ? input : {
3626
+ ...input,
3627
+ sessionId: input.interactionBinding?.sessionId
3628
+ };
3629
+ const opened = await this.openTurnSession(turnInput, recorder, onEvent);
3630
+ if (opened.cached) return opened.cached;
3631
+ const { sessionId, providerSessionId, providerMessageIds } = opened;
3632
+ const endTurn = this.beginTurn(sessionId, input, onEvent);
3633
+ try {
3634
+ const { preparedInput, promptText } = await this.recordTurnPrompt(turnInput, opened, recorder);
3635
+ onEvent?.({
3636
+ type: "status",
3637
+ status: "started"
3638
+ });
3639
+ const state = createExecutionState(sessionId, providerMessageIds?.assistantMessageId);
3640
+ let entry;
3641
+ try {
3642
+ entry = await this.acquireAcpSession({
3643
+ sessionId,
3644
+ ...this.loadsNativeSessions && providerSessionId ? { providerSessionId } : {},
3645
+ turn,
3646
+ workspaceRoot,
3647
+ onEvent
3648
+ });
3649
+ } catch (err) {
3650
+ onEvent?.({
3651
+ type: "status",
3652
+ status: "failed",
3653
+ detail: err instanceof Error ? err.message : String(err)
3654
+ });
3655
+ throw err instanceof Error ? err : new Error(String(err));
3656
+ }
3657
+ const { bridge, acpSessionId } = entry;
3658
+ onEvent?.({
3659
+ type: "session.updated",
3660
+ sessionId
3661
+ });
3662
+ onEvent?.({
3663
+ type: "native.session.observed",
3664
+ provider: this.provider,
3665
+ nativeSessionId: acpSessionId
3666
+ });
3667
+ const timeoutMs = config.promptTimeoutMs ?? DEFAULT_PROMPT_TIMEOUT_MS;
3668
+ const prompt = this.promptBlocks(input, {
3669
+ ...preparedInput,
3670
+ promptText
3671
+ }, turn);
3672
+ let promptResult;
3673
+ try {
3674
+ for (let attempt = 0; attempt < 3; attempt++) try {
3675
+ promptResult = await this.promptAcpSession(entry, {
3676
+ prompt,
3677
+ timeoutMs: Math.min(timeoutMs, 12e4),
3678
+ abortSignal: input.abortSignal,
3679
+ onUpdate: (params) => {
3680
+ for (const event of this.mapAcpUpdate(params, state, sessionId)) onEvent?.(event);
3681
+ }
3682
+ });
3683
+ break;
3684
+ } catch (err) {
3685
+ const detail = this.formatBridgeError(err);
3686
+ if (detail.includes("timed out")) this.interruptStuckTurn();
3687
+ if (attempt === 0 && detail.includes("timed out")) continue;
3688
+ if (attempt < 2 && await this.acknowledgeLostTurn(detail)) continue;
3689
+ throw err;
3690
+ }
3691
+ const recorded = this.takeTurnFailure(sessionId);
3692
+ if (recorded) throw recorded;
3693
+ recordCliTokenUsage(state, promptResult);
3694
+ const stop = promptResult?.stopReason;
3695
+ if (stop !== "end_turn" && stop !== "max_tokens") throw new Error(`${this.provider} stopped the turn unfinished (stop reason ${stop ?? "missing"}): send the next turn to continue it`);
3696
+ onEvent?.({
3697
+ type: "status",
3698
+ status: "completed"
3699
+ });
3700
+ await this.getSessionStore().update(sessionId, { providerSessionId: acpSessionId });
3701
+ return await this.completeTurn({
3702
+ input: turnInput,
3703
+ sessionId,
3704
+ state,
3705
+ recorder,
3706
+ providerMessageIds,
3707
+ startTime,
3708
+ endTime: Date.now()
3709
+ });
3710
+ } catch (err) {
3711
+ const finalError = this.takeTurnFailure(sessionId) ?? err;
3712
+ const detail = this.formatBridgeError(finalError);
3713
+ this.logger.error(`${this.provider} execute failed`, {
3714
+ ...this.turnLogContext(turn),
3715
+ error: detail,
3716
+ stderrTail: bridge.getStderrTail().slice(-500)
3717
+ });
3718
+ onEvent?.({
3719
+ type: "status",
3720
+ status: "failed",
3721
+ detail
3722
+ });
3723
+ this.evictAcpSession(sessionId, "execute-error");
3724
+ const error = finalError instanceof JsonRpcError && detail !== finalError.message ? new Error(detail, { cause: finalError }) : finalError instanceof Error ? finalError : new Error(detail);
3725
+ throw Object.assign(error, { reason: error.reason ?? turnEndReason({
3726
+ aborted: input.abortSignal?.aborted,
3727
+ timedOut: finalError instanceof RpcRequestTimeoutError,
3728
+ linkLost: finalError instanceof BridgeClosedError || HARNESS_LINK_LOST.test(detail)
3729
+ }) });
3730
+ } finally {
3731
+ this.scheduleIdleReap(sessionId, config.bridgeIdleMs);
3732
+ }
3733
+ } finally {
3734
+ endTurn();
3735
+ }
3736
+ }
3737
+ interruptStuckTurn() {}
3738
+ async acknowledgeLostTurn(_detail) {
3739
+ return false;
3740
+ }
3741
+ async acquireAcpSession(opts) {
3742
+ const { sessionId, turn, workspaceRoot, onEvent } = opts;
3743
+ const existing = this.acpSessions.get(sessionId);
3744
+ if (existing && !existing.bridge.closed) {
3745
+ if (existing.processKey === turn.processKey) {
3746
+ this.cancelIdleReap(existing);
3747
+ return existing;
3748
+ }
3749
+ this.logger.warn(`${this.provider}: cached session was started for another process configuration — respawning`, { sessionId });
3750
+ this.evictAcpSession(sessionId, "process-change");
3751
+ }
3752
+ const storedSessionId = opts.providerSessionId ? this.resumeSessionId() : void 0;
3753
+ const spec = await this.processSpec(turn, {
3754
+ sessionId,
3755
+ workspaceRoot
3756
+ });
3757
+ onEvent?.({
3758
+ type: "status",
3759
+ status: "processing",
3760
+ detail: spec.statusDetail
3761
+ });
3762
+ let bridge;
3763
+ const spawnStarted = performance.now();
3764
+ try {
3765
+ const runtimeHome = this.config.runtimeHome?.trim();
3766
+ bridge = this.spawnBridge({
3767
+ command: spec.command,
3768
+ args: spec.args,
3769
+ cwd: workspaceRoot,
3770
+ env: withAgentEnvironment(spec.env, this.agentEnvironmentValues()),
3771
+ ...this.subprocessSpawnOptions(),
3772
+ ...globalThis.process.getuid?.() === 0 && runtimeHome ? { rawCapturePath: resolveRawCapturePath(runtimeHome) } : {}
3773
+ });
3774
+ } catch (err) {
3775
+ const message = err instanceof Error ? err.message : String(err);
3776
+ throw new Error(this.spawnFailureMessage({
3777
+ message,
3778
+ missingBinary: message.includes("ENOENT") || message.includes("not found")
3779
+ }, turn));
3780
+ }
3781
+ bridge.process?.once("spawn", () => {
3782
+ this.logger.observePhase?.("cli_spawn", performance.now() - spawnStarted);
3783
+ });
3784
+ this.onBridgeSpawned(bridge, sessionId, turn);
3785
+ const timeoutMs = turn.config.bootstrapTimeoutMs ?? DEFAULT_BOOTSTRAP_TIMEOUT_MS;
3786
+ let acpSessionId;
3787
+ const initializeStarted = performance.now();
3788
+ try {
3789
+ const requested = turn.config.protocolVersion ?? 1;
3790
+ const init = await bridge.request("initialize", {
3791
+ protocolVersion: requested,
3792
+ clientCapabilities: {}
3793
+ }, { timeoutMs });
3794
+ this.checkProtocolVersion(init, requested, turn);
3795
+ acpSessionId = await this.openNativeSession(bridge, {
3796
+ providerSessionId: opts.providerSessionId,
3797
+ storedSessionId,
3798
+ cwd: workspaceRoot,
3799
+ timeoutMs
3800
+ });
3801
+ await this.getSessionStore().update(sessionId, { providerSessionId: acpSessionId });
3802
+ await this.onSessionOpened(bridge, acpSessionId, turn, { timeoutMs });
3803
+ this.logger.observePhase?.("cli_initialize", performance.now() - initializeStarted);
3804
+ } catch (err) {
3805
+ try {
3806
+ await bridge.close();
3807
+ } catch {}
3808
+ throw this.sessionSetupError(err, bridge);
3809
+ }
3810
+ const unsubscribeClose = bridge.onClose((reason) => {
3811
+ this.logger.warn(`${this.provider}: agent process closed unexpectedly — evicting`, {
3812
+ sessionId,
3813
+ reason: reason.reason,
3814
+ code: reason.code,
3815
+ signal: reason.signal
3816
+ });
3817
+ this.evictAcpSession(sessionId, "process-exit");
3818
+ });
3819
+ const entry = {
3820
+ bridge,
3821
+ acpSessionId,
3822
+ processKey: turn.processKey,
3823
+ inflight: Promise.resolve(),
3824
+ unsubscribeClose
3825
+ };
3826
+ this.acpSessions.set(sessionId, entry);
3827
+ return entry;
3828
+ }
3829
+ checkProtocolVersion(init, requested, turn) {
3830
+ const advertised = init.protocolVersion;
3831
+ if (typeof advertised !== "number") throw new Error(`${this.provider}: initialize response missing protocolVersion (got ${JSON.stringify(init)})`);
3832
+ if (advertised > requested) throw new Error(`${this.provider}: agent advertises protocolVersion ${advertised} but client speaks at most ${requested}. Upgrade the provider package or pin an older agent binary.`);
3833
+ if (advertised < 1) throw new Error(`${this.provider}: agent advertises protocolVersion ${advertised} but client requires at least 1. The backing agent binary is too old; upgrade it.`);
3834
+ this.checkAgentProtocolVersion(advertised, turn);
3835
+ }
3836
+ async openNativeSession(bridge, opts) {
3837
+ const load = async (sessionId) => {
3838
+ try {
3839
+ const loaded = await bridge.request("session/load", {
3840
+ sessionId,
3841
+ cwd: opts.cwd,
3842
+ mcpServers: []
3843
+ }, { timeoutMs: opts.timeoutMs });
3844
+ if (!loaded || typeof loaded !== "object") return void 0;
3845
+ return loaded.sessionId ?? sessionId;
3846
+ } catch (err) {
3847
+ this.logger.debug("session/load failed", { error: err instanceof Error ? err.message : String(err) });
3848
+ return;
3849
+ }
3850
+ };
3851
+ const stored = opts.storedSessionId;
3852
+ if (stored) {
3853
+ const loaded = await load(stored);
3854
+ if (loaded) return loaded;
3855
+ }
3856
+ if (opts.providerSessionId && opts.providerSessionId !== stored) {
3857
+ const loaded = await load(opts.providerSessionId);
3858
+ if (loaded) return loaded;
3859
+ }
3860
+ return (await bridge.request("session/new", {
3861
+ cwd: opts.cwd,
3862
+ mcpServers: []
3863
+ }, { timeoutMs: opts.timeoutMs })).sessionId;
3864
+ }
3865
+ async promptAcpSession(entry, turn) {
3866
+ const { bridge, acpSessionId } = entry;
3867
+ const unsubscribe = bridge.onNotification("session/update", (params) => {
3868
+ if (!params || params.sessionId !== acpSessionId) return;
3869
+ turn.onUpdate(params);
3870
+ });
3871
+ const onAbort = () => {
3872
+ try {
3873
+ bridge.notify("session/cancel", { sessionId: acpSessionId });
3874
+ } catch (err) {
3875
+ this.logger.warn(`${this.provider}: session/cancel notify failed on abort`, { error: err instanceof Error ? err.message : String(err) });
3876
+ }
3877
+ setTimeout(() => {
3878
+ bridge.close().catch(() => {});
3879
+ }, ABORT_GRACE_MS).unref();
3880
+ };
3881
+ const { abortSignal } = turn;
3882
+ if (abortSignal) if (abortSignal.aborted) onAbort();
3883
+ else abortSignal.addEventListener("abort", onAbort, { once: true });
3884
+ const previous = entry.inflight;
3885
+ let releaseLock;
3886
+ const lock = new Promise((resolve) => {
3887
+ releaseLock = resolve;
3888
+ });
3889
+ entry.inflight = previous.then(() => lock).catch(() => lock);
3890
+ const readyWaitStarted = performance.now();
3891
+ await previous.catch(() => {});
3892
+ this.logger.observePhase?.("cli_ready_wait", performance.now() - readyWaitStarted);
3893
+ try {
3894
+ return await bridge.request("session/prompt", {
3895
+ sessionId: acpSessionId,
3896
+ prompt: turn.prompt
3897
+ }, { timeoutMs: turn.timeoutMs });
3898
+ } finally {
3899
+ unsubscribe();
3900
+ abortSignal?.removeEventListener("abort", onAbort);
3901
+ releaseLock();
3902
+ }
3903
+ }
3904
+ scheduleIdleReap(sessionId, bridgeIdleMs) {
3905
+ const entry = this.acpSessions.get(sessionId);
3906
+ if (!entry) return;
3907
+ const idleMs = bridgeIdleMs ?? DEFAULT_BRIDGE_IDLE_MS;
3908
+ if (idleMs <= 0) {
3909
+ this.evictAcpSession(sessionId, "idle-disabled");
3910
+ return;
3911
+ }
3912
+ if (entry.idleTimer) clearTimeout(entry.idleTimer);
3913
+ entry.idleTimer = setTimeout(() => {
3914
+ this.evictAcpSession(sessionId, "idle-reap");
3915
+ }, idleMs);
3916
+ entry.idleTimer.unref();
3917
+ }
3918
+ cancelIdleReap(entry) {
3919
+ if (entry.idleTimer) {
3920
+ clearTimeout(entry.idleTimer);
3921
+ entry.idleTimer = void 0;
3922
+ }
3923
+ }
3924
+ evictAcpSession(sessionId, reason) {
3925
+ const entry = this.acpSessions.get(sessionId);
3926
+ if (!entry) return;
3927
+ this.acpSessions.delete(sessionId);
3928
+ this.onSessionEvicted(sessionId);
3929
+ this.cancelIdleReap(entry);
3930
+ try {
3931
+ entry.unsubscribeClose();
3932
+ } catch {}
3933
+ if (!entry.bridge.closed) entry.bridge.close().catch((err) => {
3934
+ this.logger.warn(`${this.provider}: bridge.close threw during evict`, {
3935
+ sessionId,
3936
+ reason,
3937
+ error: err instanceof Error ? err.message : String(err)
3938
+ });
3939
+ });
3940
+ }
3941
+ evictAcpSessionsWhere(matches, reason) {
3942
+ for (const [sessionId, entry] of [...this.acpSessions]) if (matches(entry.processKey)) this.evictAcpSession(sessionId, reason);
3943
+ }
3944
+ mapAcpUpdate(params, state, sessionId) {
3945
+ const update = params.update;
3946
+ if (!update || typeof update !== "object") return [];
3947
+ const kind = update.sessionUpdate;
3948
+ if (kind === "agent_message_chunk" || kind === "agent_thought_chunk") return acpMessageChunkEvents(kind, update, state);
3949
+ if (kind === "tool_call") return acpToolCallEvents(update, state);
3950
+ if (kind === "tool_call_update") {
3951
+ const change = update;
3952
+ const existing = state.toolCalls.get(change.toolCallId);
3953
+ if (!existing) return [];
3954
+ const mapped = this.mapToolStatus(change.status);
3955
+ const outputText = applyAcpToolCallUpdate(existing, mapped, change);
3956
+ return [...mapped === "failed" ? this.onToolFailed(sessionId, typeof existing.output === "string" ? existing.output : outputText || void 0) : [], ...acpToolCallUpdateEvents(existing, state)];
3957
+ }
3958
+ if (kind === "plan") return acpPlanEvents(update, state, this.provider);
3959
+ if (kind === "available_commands_update") return [{
3960
+ type: "raw",
3961
+ backend: this.provider,
3962
+ event: {
3963
+ sessionUpdate: "available_commands_update",
3964
+ availableCommands: update.availableCommands ?? []
3965
+ }
3966
+ }];
3967
+ return [{
3968
+ type: "raw",
3969
+ backend: this.provider,
3970
+ event: update
3971
+ }];
3972
+ }
3973
+ mapToolStatus(status) {
3974
+ switch (status) {
3975
+ case "completed": return "completed";
3976
+ case "failed": return "failed";
3977
+ case "in_progress": return "running";
3978
+ case "pending":
3979
+ case void 0: return "pending";
3980
+ default:
3981
+ this.logger.warn(`${this.provider}: unknown tool_call_update status`, {
3982
+ status,
3983
+ mappedTo: "running"
3984
+ });
3985
+ return "running";
3986
+ }
3987
+ }
3988
+ formatBridgeError(raw) {
3989
+ if (raw instanceof BridgeClosedError) return `${this.provider} bridge closed unexpectedly: ${raw.message}`;
3990
+ if (raw instanceof RpcRequestTimeoutError) return `${this.provider} request "${raw.method}" timed out after ${raw.timeoutMs}ms`;
3991
+ if (raw instanceof JsonRpcError) return jsonRpcErrorText(raw);
3992
+ if (raw instanceof Error) return raw.message;
3993
+ return String(raw);
3994
+ }
3995
+ sessionSetupError(err, bridge) {
3996
+ if (err instanceof BridgeClosedError) {
3997
+ const stderr = bridge.getStderrTail().trim().slice(-500);
3998
+ return new Error(`${this.provider}: agent exited before its session opened (${err.message})${stderr ? `; stderr: ${stderr}` : ""}`, { cause: err });
3999
+ }
4000
+ if (err instanceof JsonRpcError) return new Error(`${this.provider}: ${jsonRpcErrorText(err)}`, { cause: err });
4001
+ return err instanceof Error ? err : new Error(String(err));
4002
+ }
4003
+ async healthCheck() {
4004
+ return true;
4005
+ }
4006
+ async shutdown() {
4007
+ this.evictAcpSessionsWhere(() => true, "adapter-shutdown");
4008
+ await super.shutdown();
4009
+ }
4010
+ async addMcp(_name, _config) {
4011
+ throw new Error(`${this.provider}: addMcp is not supported — an ACP agent takes MCP servers through session/new`);
4012
+ }
4013
+ async getMcpStatus() {
4014
+ return { servers: {} };
4015
+ }
4016
+ buildExecArgs() {
4017
+ throw new Error(`${this.provider}: buildExecArgs is not used — ACP turns run over the RPC bridge`);
4018
+ }
4019
+ buildExecEnv(_config) {
4020
+ return {};
4021
+ }
4022
+ createEventParser() {
4023
+ throw new Error(`${this.provider}: createEventParser is not used — session/update notifications arrive over the RPC bridge`);
4024
+ }
4025
+ buildMcpAddArgs(_name, _config) {
4026
+ throw new Error(`${this.provider}: buildMcpAddArgs is not used`);
4027
+ }
4028
+ };
4029
+ //#endregion
4030
+ //#region src/event-parsers.ts
4031
+ /**
4032
+ * Parser for harnesses that print one JSON event per line (claude-code,
4033
+ * codex, amp, factory-droids). Values that fail `accept` are logged and
4034
+ * dropped; unparseable lines become a `PARSE_ERROR` warning. The first event
4035
+ * `isReady` accepts reports the CLI's own start time as `cli_initialize`,
4036
+ * measured from parser creation, which immediately precedes the spawn.
4037
+ */
4038
+ function jsonEventParser(logger, onEvent, spec) {
4039
+ const startedAt = performance.now();
4040
+ let readyReported = false;
4041
+ return {
4042
+ transform: (raw) => {
4043
+ if (spec.accept(raw)) return raw;
4044
+ logger.warn(`Received non-${spec.label} event`, { raw });
4045
+ return null;
4046
+ },
4047
+ onEvent: (event) => {
4048
+ if (!readyReported && spec.isReady?.(event)) {
4049
+ readyReported = true;
4050
+ logger.observePhase?.("cli_initialize", performance.now() - startedAt);
4051
+ }
4052
+ for (const stream of spec.map(event)) onEvent?.(stream);
4053
+ },
4054
+ onError: (error, line) => {
4055
+ logger.warn("Failed to parse event", {
4056
+ error: error.message,
4057
+ line
4058
+ });
4059
+ onEvent?.({
4060
+ type: "warning",
4061
+ code: "PARSE_ERROR",
4062
+ message: `Failed to parse event: ${error.message}`
4063
+ });
4064
+ }
4065
+ };
4066
+ }
4067
+ /**
4068
+ * Parser for harnesses that print their reply as plain stdout lines (hermes,
4069
+ * nanoclaw). Lines classify into provider events; the reply is
4070
+ * emitted as one text part when stdout closes. A turn that printed errors
4071
+ * but no reply ends `failed` with the last error instead of an empty
4072
+ * `completed`.
4073
+ */
4074
+ function plaintextResponseParser(state, onEvent, spec) {
4075
+ const lines = [];
4076
+ let errorSeen = false;
4077
+ let lastError;
4078
+ const response = {
4079
+ text: (line) => {
4080
+ lines.push(line);
4081
+ },
4082
+ error: (message) => {
4083
+ errorSeen = true;
4084
+ lastError = message;
4085
+ }
4086
+ };
4087
+ return {
4088
+ parseAs: "lines",
4089
+ transform: (raw) => spec.classify(String(raw)),
4090
+ onEvent: (event) => {
4091
+ for (const stream of spec.map(event, response)) onEvent?.(stream);
4092
+ },
4093
+ onComplete: () => {
4094
+ const text = lines.join("\n").trimEnd();
4095
+ if (text.length > 0) {
4096
+ const reply = finalTextEvent(state, text);
4097
+ onEvent?.(reply);
4098
+ onEvent?.({
4099
+ type: "status",
4100
+ status: "completed"
4101
+ });
4102
+ } else if (errorSeen) onEvent?.({
4103
+ type: "status",
4104
+ status: "failed",
4105
+ detail: lastError ?? spec.emptyFailureDetail
4106
+ });
4107
+ }
4108
+ };
4109
+ }
4110
+ //#endregion
4111
+ //#region src/interaction-mcp-server.ts
4112
+ var InteractionMcpServer$1 = class extends InteractionMcpServer {
4113
+ constructor(config) {
4114
+ super({
4115
+ ...config,
4116
+ onStop: config.onStop ?? (() => void 0)
4117
+ });
4118
+ }
4119
+ };
4120
+ //#endregion
4121
+ //#region src/terminal-plan-support.ts
4122
+ /** One-shot durable plan submission for a single provider turn. */
4123
+ var TerminalPlanSupport = class {
4124
+ committed;
4125
+ submittedBody;
4126
+ inFlight;
4127
+ confirmInFlight;
4128
+ confirmed = false;
4129
+ constructor(options) {
4130
+ this.options = options;
4131
+ }
4132
+ get plan() {
4133
+ return this.committed;
4134
+ }
4135
+ /** Wait for a submission already accepted by this turn before failure cleanup. */
4136
+ async settleSubmission() {
4137
+ await this.inFlight?.catch(() => void 0);
4138
+ }
4139
+ async submit(body, sourceToolCallId) {
4140
+ const normalized = body.trim();
4141
+ if (!normalized) throw new Error("Plan body must not be empty");
4142
+ if (this.submittedBody && this.submittedBody !== normalized) throw new Error(this.committed ? "This turn already submitted a different plan" : "This turn is already submitting a different plan");
4143
+ if (this.committed) return this.committed;
4144
+ if (this.inFlight) return this.inFlight;
4145
+ this.submittedBody = normalized;
4146
+ this.inFlight = this.options.planHost.submit({
4147
+ body: normalized,
4148
+ sourceToolCallId,
4149
+ providerState: this.options.providerState()
4150
+ }).then(async (plan) => {
4151
+ this.committed = plan;
4152
+ await this.options.onCommitted?.(plan);
4153
+ return plan;
4154
+ }).catch((error) => {
4155
+ this.inFlight = void 0;
4156
+ if (!this.committed) this.submittedBody = void 0;
4157
+ throw error;
4158
+ });
4159
+ return this.inFlight;
4160
+ }
4161
+ async confirm() {
4162
+ if (!this.committed) throw new Error("No durable plan has been committed by this turn");
4163
+ if (this.confirmed) return this.committed;
4164
+ if (this.confirmInFlight) return this.confirmInFlight;
4165
+ const committed = this.committed;
4166
+ this.confirmInFlight = this.options.planHost.confirm(committed.id).then(() => {
4167
+ this.confirmed = true;
4168
+ try {
4169
+ this.options.emit?.({
4170
+ type: "plan.submitted",
4171
+ plan: committed
4172
+ });
4173
+ } catch {}
4174
+ return committed;
4175
+ }).catch((error) => {
4176
+ this.confirmInFlight = void 0;
4177
+ throw error;
4178
+ });
4179
+ return this.confirmInFlight;
4180
+ }
4181
+ async withdraw(reason) {
4182
+ if (!this.committed) return;
4183
+ await this.options.planHost.withdraw(this.committed.id, reason);
4184
+ }
4185
+ };
4186
+ //#endregion
4187
+ //#region src/interaction-support.ts
4188
+ var McpInteractionSupport = class {
4189
+ /** The shared emit → block → respond → fail-closed round-trip. */
4190
+ broker = new InteractionBroker();
4191
+ activeServer;
4192
+ terminalPlan;
4193
+ constructor(serverName, kinds) {
4194
+ this.serverName = serverName;
4195
+ this.kinds = kinds;
4196
+ }
4197
+ /** The interaction kinds to advertise from `getCapabilities().interactions`. */
4198
+ capabilityKinds() {
4199
+ return [...this.kinds];
4200
+ }
4201
+ /** The active per-execution server (its `url`/`token`), once started. */
4202
+ get server() {
4203
+ return this.activeServer;
4204
+ }
4205
+ /**
4206
+ * Start a per-execution server bound to 127.0.0.1 with an ephemeral bearer
4207
+ * token, registering only the enabled tools. `emit` is the live event sink;
4208
+ * broker-backed tools block on an answer, while plans emit only after their
4209
+ * terminal confirmation. Returns the server for CLI injection.
4210
+ */
4211
+ async start(sessionId, binding, emit, plan) {
4212
+ const tools = brokerInteractionTools(this.broker, {
4213
+ sessionId,
4214
+ binding,
4215
+ emit
4216
+ });
4217
+ const has = (kind) => this.kinds.includes(kind);
4218
+ if (has(InteractionKind.Plan) && !plan) throw new Error("Plan interaction requires a durable plan host and provider resume state");
4219
+ this.terminalPlan = plan ? new TerminalPlanSupport({
4220
+ ...plan,
4221
+ emit
4222
+ }) : void 0;
4223
+ const server = new InteractionMcpServer({
4224
+ serverName: this.serverName,
4225
+ onStop: tools.onStop,
4226
+ ...has(InteractionKind.Permission) ? { requestPermission: tools.requestPermission } : {},
4227
+ ...has(InteractionKind.Question) ? { askUser: tools.askUser } : {},
4228
+ ...has(InteractionKind.Plan) && this.terminalPlan ? { requestPlan: ({ plan: body, sourceToolCallId }) => this.terminalPlan.submit(body, sourceToolCallId) } : {}
4229
+ });
4230
+ await server.start();
4231
+ this.activeServer = server;
4232
+ return server;
4233
+ }
4234
+ async confirmSubmittedPlan() {
4235
+ if (!this.terminalPlan) throw new Error("No terminal plan support is active for this turn");
4236
+ return this.terminalPlan.confirm();
4237
+ }
4238
+ async withdrawSubmittedPlan(reason) {
4239
+ await this.terminalPlan?.withdraw(reason);
4240
+ }
4241
+ /** Close the request boundary before inspecting or withdrawing its plan. */
4242
+ async drainSubmittedPlan(sessionId) {
4243
+ this.broker.failSession(sessionId);
4244
+ let stopError;
4245
+ try {
4246
+ await this.activeServer?.stop();
4247
+ this.activeServer = void 0;
4248
+ } catch (error) {
4249
+ stopError = error;
4250
+ }
4251
+ await this.terminalPlan?.settleSubmission();
4252
+ if (stopError) throw stopError;
4253
+ }
4254
+ /**
4255
+ * Fail-closed every pending interaction for the session, then stop the server.
4256
+ * `failSession` runs FIRST so any request still blocked in the server resolves
4257
+ * (deny) before close, and the whole thing swallows errors — it runs in adapter
4258
+ * `finally` blocks and must never mask the turn's real error.
4259
+ */
4260
+ async stop(sessionId) {
4261
+ try {
4262
+ this.broker.failSession(sessionId);
4263
+ const server = this.activeServer;
4264
+ this.activeServer = void 0;
4265
+ await server?.stop();
4266
+ } catch {}
4267
+ }
4268
+ /**
4269
+ * Route an inbound live `/interactions` response for permission or question.
4270
+ * Plans are durable records and never enter this response channel.
4271
+ */
4272
+ respond(response) {
4273
+ if (this.broker.respond(response)) return true;
4274
+ return this.broker.respondQuestion(response);
4275
+ }
4276
+ };
4277
+ //#endregion
4278
+ //#region src/secret-native-proof.ts
4279
+ const proofKey = Symbol.for("tangle.secret-native-binary-proofs");
4280
+ const runtime$1 = globalThis;
4281
+ const proven = runtime$1[proofKey] ?? /* @__PURE__ */ new Set();
4282
+ runtime$1[proofKey] = proven;
4283
+ /** Prove an immutable executable, under the launch's own identity and environment. */
4284
+ async function provenStoreBinary(expected, config) {
4285
+ if (!config.isolatedIdentity || config.inheritEnv !== false) refuseSecretLaunch("Secret native binary proof requires an isolated identity and environment", { path: config.command });
4286
+ const command = resolveExecutableRealpath(config.command, config.env?.PATH);
4287
+ if (!command || !isTrustedStoreBinary(command)) refuseSecretLaunch("Secret native launch requires a root-owned Nix store binary", { path: command ?? config.command });
4288
+ const key = `${command}\0${expected}`;
4289
+ if (proven.has(key)) return command;
4290
+ const probe = await execCli({
4291
+ command,
4292
+ cwd: config.cwd,
4293
+ env: config.env,
4294
+ inheritEnv: config.inheritEnv,
4295
+ signal: config.signal,
4296
+ isolatedIdentity: config.isolatedIdentity,
4297
+ args: ["--version"],
4298
+ spawnOptions: config.isolatedIdentity,
4299
+ timeoutMs: 15e3
4300
+ });
4301
+ const version = probe.stdout?.trim();
4302
+ if (!probe.exit.success || version !== expected) refuseSecretLaunch(`Secret native launch expected ${expected} at ${command}, received no matching version`, { exitCode: probe.exit.code });
4303
+ proven.add(key);
4304
+ return command;
4305
+ }
4306
+ /** A reported version alone cannot establish an immutable executable. */
4307
+ function isTrustedStoreBinary(realpath) {
4308
+ if (!realpath.startsWith("/nix/store/")) return false;
4309
+ try {
4310
+ for (const path of [realpath, dirname(realpath)]) {
4311
+ const stat = fs$1.lstatSync(path);
4312
+ if (stat.uid !== 0 || (stat.mode & 18) !== 0) return false;
4313
+ }
4314
+ return true;
4315
+ } catch {
4316
+ return false;
4317
+ }
4318
+ }
4319
+ /** The real file a launch of `command` executes under `pathEnv`. */
4320
+ function resolveExecutableRealpath(command, pathEnv) {
4321
+ if (command.includes("/") && !isAbsolute(command)) return void 0;
4322
+ const candidates = command.includes("/") ? [command] : (pathEnv ?? "").split(":").filter(isAbsolute).map((dir) => join(dir, command));
4323
+ for (const candidate of candidates) try {
4324
+ fs$1.accessSync(candidate, fs$1.constants.X_OK);
4325
+ if (fs$1.statSync(candidate).isFile()) return fs$1.realpathSync(candidate);
4326
+ } catch {}
4327
+ }
4328
+ const SEALED_NATIVE_HOME_LAYOUT = {
4329
+ codex: [
4330
+ [
4331
+ ".",
4332
+ 1016,
4333
+ true
4334
+ ],
4335
+ [
4336
+ ".codex",
4337
+ 1016,
4338
+ true
4339
+ ],
4340
+ [
4341
+ ".agents",
4342
+ 360,
4343
+ true
4344
+ ],
4345
+ [
4346
+ ".codex/skills",
4347
+ 360,
4348
+ true
4349
+ ],
4350
+ [
4351
+ ".codex/rules",
4352
+ 360,
4353
+ true
4354
+ ],
4355
+ [
4356
+ ".codex/config.toml",
4357
+ 288,
4358
+ false
4359
+ ],
4360
+ [
4361
+ ".codex/AGENTS.md",
4362
+ 288,
4363
+ false
4364
+ ],
4365
+ [
4366
+ ".codex/AGENTS.override.md",
4367
+ 288,
4368
+ false
4369
+ ],
4370
+ [
4371
+ ".codex/hooks.json",
4372
+ 288,
4373
+ false
4374
+ ]
4375
+ ],
4376
+ opencode: [[
4377
+ ".",
4378
+ 456,
4379
+ true
4380
+ ], [
4381
+ ".opencode",
4382
+ 360,
4383
+ true
4384
+ ]]
4385
+ };
4386
+ function assertSealedNativeHome(home, gid, layout) {
4387
+ for (const [relative, mode, directory] of layout) {
4388
+ const path = join(home, relative);
4389
+ const stat = fs$1.lstatSync(path);
4390
+ if (fs$1.realpathSync(path) !== path || stat.uid !== 0 || stat.gid !== gid || (stat.mode & 4095) !== mode || stat.isDirectory() !== directory) refuseSecretLaunch(`Secret native HOME authority is not sealed at ${path}: expected uid0 gid${gid} mode${mode.toString(8)} directory=${directory}; actual uid${stat.uid} gid${stat.gid} mode${(stat.mode & 4095).toString(8)} directory=${stat.isDirectory()}`);
4391
+ }
4392
+ }
4393
+ /** Only selected structural facts enter a refusal; payloads and causes never do. */
4394
+ function refuseSecretLaunch(reason, { path, expected, actual, exitCode } = {}) {
4395
+ const detail = JSON.stringify({
4396
+ path,
4397
+ expected,
4398
+ actual
4399
+ }).slice(0, 768);
4400
+ throw new Error(detail === "{}" ? reason : `${reason} ${detail}`, { cause: typeof exitCode === "number" || exitCode === null ? { exitCode } : void 0 });
4401
+ }
4402
+ //#endregion
4403
+ //#region src/reasoning-effort.ts
4404
+ /** How each CLI takes agent-interface's native control token on its argv. */
4405
+ const EFFORT_FLAG = {
4406
+ "claude-code": (value) => ["--effort", value],
4407
+ codex: (value) => ["-c", `model_reasoning_effort=${value}`],
4408
+ openclaw: (value) => ["--thinking", value],
4409
+ pi: (value) => ["--thinking", value]
4410
+ };
4411
+ /** The argv carrying `nativeReasoningControl` for a profile effort, or none. */
4412
+ function nativeEffortArgs(harness, effort) {
4413
+ const value = nativeReasoningControl(harness, effort ?? null);
4414
+ const flag = EFFORT_FLAG[harness];
4415
+ return value === null || flag === void 0 ? [] : flag(value);
4416
+ }
4417
+ /**
4418
+ * Kimi stays local until #14839: the sandbox ships kimi-cli 1.49.0 as
4419
+ * `kimi-code` (binary `--thinking`/`--no-thinking`) while other paths run
4420
+ * kimi-code 2.x. A model whose name has "thinking" or "reason" always thinks
4421
+ * (kimi-cli `derive_model_capabilities`), so `off` throws there.
4422
+ */
4423
+ function kimiThinkingArgs(level, modelId) {
4424
+ if (level === void 0) return [];
4425
+ const off = level === "none" || level === "minimal";
4426
+ if (!REASONING_EFFORTS.includes(level) || off && /thinking|reason/i.test(modelId ?? "")) throw new Error(`kimi-code cannot honor reasoning effort "${level}"`);
4427
+ return off ? ["--no-thinking"] : ["--thinking"];
4428
+ }
4429
+ //#endregion
4430
+ //#region src/secret-profile-mcp.ts
4431
+ /** Executor-held MCP credentials shared by native private-profile adapters. */
4432
+ const SECRET_REFERENCE_PREFIX = "TANGLE_PROFILE_SECRET_";
4433
+ const SECRET_REFERENCE = /\{env:(TANGLE_PROFILE_SECRET_[0-9A-F]{24})\}/g;
4434
+ function requireRecord(raw, message) {
4435
+ return isRecord(raw) ? raw : refuseSecretLaunch(message);
4436
+ }
4437
+ /** A config value with its secret references resolved, or undefined. */
4438
+ function resolveReferences(value, references) {
4439
+ let unresolved = false;
4440
+ const resolved = value.replace(SECRET_REFERENCE, (_match, reference) => {
4441
+ const secret = references[reference];
4442
+ if (secret === void 0) unresolved = true;
4443
+ return secret ?? "";
4444
+ });
4445
+ return unresolved || /\{(?:env|file):/.test(resolved) ? void 0 : resolved;
4446
+ }
4447
+ /** A stdio server the executor can run: its command and its environment with every secret reference resolved. */
4448
+ function servableStdioServer(name, entry, references) {
4449
+ const command = entry.command;
4450
+ if (!Array.isArray(command) || command.length === 0 || command.some((part) => typeof part !== "string" || /\{(?:env|file):/.test(part))) return "its command is not a fixed argument list";
4451
+ if (entry.cwd !== void 0) return "it names a working directory";
4452
+ const declared = entry.environment === void 0 ? {} : entry.environment;
4453
+ if (!isRecord(declared)) return "its environment is not a map";
4454
+ const environment = {};
4455
+ const secrets = /* @__PURE__ */ new Set();
4456
+ for (const [variable, value] of Object.entries(declared)) {
4457
+ if (typeof value !== "string") return `its ${variable} is not text`;
4458
+ const resolved = resolveReferences(value, references);
4459
+ if (resolved === void 0) return `its ${variable} names a value the executor cannot resolve`;
4460
+ environment[variable] = resolved;
4461
+ for (const [, reference] of value.matchAll(SECRET_REFERENCE)) {
4462
+ const secret = references[reference];
4463
+ if (secret) secrets.add(secret);
4464
+ }
4465
+ }
4466
+ return {
4467
+ name,
4468
+ command,
4469
+ environment,
4470
+ secrets: [...secrets]
4471
+ };
4472
+ }
4473
+ /** A remote server the executor can serve: its fixed upstream URL and every header with its secret references resolved. */
4474
+ function servableMcpServer(name, entry, references) {
4475
+ const url = entry.url;
4476
+ if (typeof url !== "string" || /\{(?:env|file):/.test(url)) return "its URL is not a fixed address";
4477
+ try {
4478
+ const parsed = new URL(url);
4479
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return "its URL is not http(s)";
4480
+ } catch {
4481
+ return "its URL is not a fixed address";
4482
+ }
4483
+ const headers = {};
4484
+ const declared = entry.headers === void 0 ? {} : entry.headers;
4485
+ if (!isRecord(declared)) return "its headers are not a map";
4486
+ for (const [header, value] of Object.entries(declared)) {
4487
+ if (typeof value !== "string") return `its ${header} header is not text`;
4488
+ const resolved = resolveReferences(value, references);
4489
+ if (resolved === void 0) return `its ${header} header names a value the executor cannot resolve`;
4490
+ headers[header] = resolved;
4491
+ }
4492
+ return {
4493
+ name,
4494
+ url,
4495
+ headers
4496
+ };
4497
+ }
4498
+ /** Join the queue at `key`; `previous` settles when earlier holders release. */
4499
+ function joinQueue(queues, key) {
4500
+ const previous = queues.get(key) ?? Promise.resolve();
4501
+ let release;
4502
+ const finished = new Promise((resolve) => {
4503
+ release = resolve;
4504
+ });
4505
+ const tail = previous.then(() => finished);
4506
+ queues.set(key, tail);
4507
+ tail.then(() => {
4508
+ if (queues.get(key) === tail) queues.delete(key);
4509
+ });
4510
+ return {
4511
+ previous,
4512
+ release
4513
+ };
4514
+ }
4515
+ function untilSettledOrAborted(previous, signal) {
4516
+ const aborted = () => signal.reason ?? /* @__PURE__ */ new Error("Secret native turn aborted before its session view was free");
4517
+ if (signal.aborted) return Promise.reject(aborted());
4518
+ return new Promise((resolve, reject) => {
4519
+ const onAbort = () => reject(aborted());
4520
+ signal.addEventListener("abort", onAbort, { once: true });
4521
+ previous.then(() => {
4522
+ signal.removeEventListener("abort", onAbort);
4523
+ resolve();
4524
+ });
4525
+ });
4526
+ }
4527
+ /** Every native discovery ancestor is sealed and free of ambient configuration. */
4528
+ function assertSealedNativeSearchPath(cwd, configNames) {
4529
+ for (let child = cwd, parent = dirname(cwd);; child = parent, parent = dirname(parent)) {
4530
+ const stat = lstatSync(child);
4531
+ if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(child) !== child || stat.uid !== 0 || (stat.mode & 18) !== 0 || child !== cwd && configNames.some((name) => lstatSync(join(child, name), { throwIfNoEntry: false }))) throw new Error("Secret native launch has mutable or ambient discovery authority");
4532
+ if (parent === child) break;
4533
+ }
4534
+ }
4535
+ /** One session view changes only after its previous physical native process exits. */
4536
+ const turnsKey = Symbol.for("tangle.secret-native-session-turns");
4537
+ const turnsRuntime = globalThis;
4538
+ const sessionTurns = turnsRuntime[turnsKey] ?? /* @__PURE__ */ new Map();
4539
+ turnsRuntime[turnsKey] = sessionTurns;
4540
+ function secretSessionTurnQueue(view) {
4541
+ return joinQueue(sessionTurns, view);
4542
+ }
4543
+ /** An HTTP URL on loopback with an explicit port and no URL credentials. */
4544
+ function isLoopbackEndpoint(value) {
4545
+ if (typeof value !== "string") return false;
4546
+ try {
4547
+ const url = new URL(value);
4548
+ return url.protocol === "http:" && [
4549
+ "127.0.0.1",
4550
+ "[::1]",
4551
+ "localhost"
4552
+ ].includes(url.hostname) && url.port !== "" && !url.username && !url.password;
4553
+ } catch {
4554
+ return false;
4555
+ }
4556
+ }
4557
+ //#endregion
4558
+ //#region src/secret-profile-scope.ts
4559
+ const key = Symbol.for("tangle.secret-profile-mcp.scopes");
4560
+ const runtime = globalThis;
4561
+ const openScopes = runtime[key] ??= /* @__PURE__ */ new Map();
4562
+ function secretMcpScope(sessionId, uid, owner) {
4563
+ if (sessionId.includes("\0")) throw new Error("A secret MCP session id has no NUL");
4564
+ const scope = `${sessionId}\0${uid ?? ""}`;
4565
+ if (owner?.proxy) {
4566
+ const owners = openScopes.get(scope) ?? /* @__PURE__ */ new Set();
4567
+ owners.add(owner);
4568
+ openScopes.set(scope, owners);
4569
+ }
4570
+ return scope;
4571
+ }
4572
+ async function forgetSessionScopes(target) {
4573
+ for (const [scope, owners] of openScopes) {
4574
+ if ("sessionId" in target && !scope.startsWith(`${target.sessionId}\0`)) continue;
4575
+ for (const owner of [...owners]) {
4576
+ if ("owner" in target && owner !== target.owner) continue;
4577
+ if (![...owners].some((other) => other !== owner && other.proxy === owner.proxy)) await owner.proxy.forget(scope);
4578
+ owners.delete(owner);
4579
+ }
4580
+ if (owners.size === 0) openScopes.delete(scope);
4581
+ }
4582
+ }
4583
+ //#endregion
4584
+ export { withEphemeralContext as $, TerminalPlanSupport as A, toolStartEvents as At, readCliTokenUsage as B, SEALED_NATIVE_HOME_LAYOUT as C, finalTextEvent as Ct, refuseSecretLaunch as D, textDeltaEvents as Dt, provenStoreBinary as E, streamPartBase as Et, jsonEventParser as F, spawnAcpBridge as Ft, isLocalMcpConfig as G, CliProviderAdapterBase as H, plaintextResponseParser as I, enabledInteractionKinds as J, localMcpArgv as K, AcpProviderAdapterBase as L, InteractionMcpServer$1 as M, ACP_MIN_PROTOCOL_VERSION as Mt, brokerInteractionTools$1 as N, ACP_PROTOCOL_VERSION as Nt, resolveExecutableRealpath as O, textSnapshotEvents as Ot, interactionMcpToolName as P, acpContentText as Pt, turnInteractionEnabled as Q, accumulateCliTokenUsage as R, NATIVE_SESSION_CONTRACTS$1 as S, homeXdgEnv as St, isTrustedStoreBinary as T, sessionObservedEvents as Tt, createRepeatedToolFailureGuard as U, recordCliTokenUsage as V, createExecutionState as W, readEphemeralContext as X, interactionToggleEnabled as Y, turnEndReason as Z, servableMcpServer as _, EXECUTION_CORRELATION_HEADERS as _t, codexDeveloperInstructionArgs as a, preparePromptInputParts as at, kimiThinkingArgs as b, MANAGED_QUOTE_VERSION_HEADER as bt, SECRET_REFERENCE as c, deriveFilePart as ct, isLoopbackEndpoint as d, trackFilePart as dt, withTurnInteractions as et, isPlainRecord as f, InvalidAttachmentError as ft, secretSessionTurnQueue as g, runtimeSecretsOverlay as gt, resolveReferences as h, wasNeverLaunched as ht, PERMISSION_DECISION_TIMEOUT_MS as i, partitionImageAttachments as it, DEFAULT_INTERACTION_MCP_SERVER_NAME as j, trackReasoningBlock as jt, McpInteractionSupport as k, toolResultEvents as kt, SECRET_REFERENCE_PREFIX as l, deterministicFilePartId as lt, requireRecord as m, rejectNotLaunched as mt, InteractionBroker$1 as n, MAX_TOTAL_INLINE_IMAGE_BYTES as nt, forgetSessionScopes as o, prependPromptSection as ot, joinQueue as p, SessionRehydrationError as pt, localMcpEnv as q, InteractionHttpBridge as r, PROMPT_SECTION_SEPARATOR as rt, secretMcpScope as s, buildFileUrl as st, CODEX_DISABLED_FEATURE_ARGS as t, MAX_INLINE_IMAGE_BYTES as tt, assertSealedNativeSearchPath as u, inferMediaType as ut, servableStdioServer as v, EXECUTION_ID_HEADER as vt, assertSealedNativeHome as w, reasoningEvents as wt, nativeEffortArgs as x, executionCorrelationHeaders as xt, untilSettledOrAborted as y, EXECUTION_TRACE_HEADER as yt, parseCliUsageLine as z };
4585
+
4586
+ //# sourceMappingURL=src-Fn8Pu1xq.mjs.map