@tangle-network/sdk-provider-cli-base 0.12.0 → 0.13.1

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.
Files changed (3) hide show
  1. package/dist/index.d.mts +187 -555
  2. package/dist/index.mjs +952 -785
  3. package/package.json +8 -8
package/dist/index.mjs CHANGED
@@ -1,27 +1,30 @@
1
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, 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";
2
3
  import { BridgeClosedError, InvalidAttachmentError, JsonRpcError, RpcRequestTimeoutError, execCli, launchCliProcess, resolveRawCapturePath, spawnCliProcess, spawnRpcBridge } from "@tangle-network/sdk-cli-runner";
3
4
  import { createHash, createHmac, randomUUID } from "node:crypto";
4
5
  import { AgentExecutionError, AgentExecutionOutcomeSchema, AgentRuntimeAttachmentsSchema, InteractionExecutionBindingSchema, InteractionKind, REASONING_EFFORTS, agentProfileSchema, nativeReasoningControl, normalizeInputParts } from "@tangle-network/agent-interface";
5
- import { UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER, applyWorkspacePlanWithSecretProvider, bindLaunchToolBinDirs, enforceWorkspacePlanSupport, ensurePublicConcurrentProfileRoots, joinSystemPromptAdditions, keepsHarnessSystemPrompt, materializeProfile, modelIdsMatch, parseSandboxToolBinDirs, preparePublicConcurrentProfileRevision, prepareSecretConcurrentProfileRevision, resolveAgentProfileResources } from "@tangle-network/agent-profile-materialize";
6
6
  import { HarnessSessionContractError, assertArgvHonoursSessionContract, assertFlagsHonourSessionContract, assertStoreLocationHonoursSessionContract, compileSessionStoreGlob } from "@tangle-network/cli-agent-registry/session-contract";
7
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
8
  import { mkdir, open, readdir, writeFile } from "node:fs/promises";
9
- import path, { basename, isAbsolute, normalize, resolve } from "node:path";
10
- import fs, { constants, lstatSync, readFileSync } from "node:fs";
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";
11
12
  import { execFile } from "node:child_process";
12
13
  import { isDeepStrictEqual, promisify } from "node:util";
13
14
  import { messageStorageDirOf } from "@tangle-network/cli-agent-registry";
14
- import { HARNESS_CONFORMANCE_PENDING, NATIVE_SESSION_CONTRACTS, configDirHomeEnv } from "@tangle-network/cli-agent-registry/native-session-contracts";
15
+ import { NATIVE_SESSION_CONTRACTS, NATIVE_SESSION_CONTRACTS as NATIVE_SESSION_CONTRACTS$1, configDirHomeEnv } from "@tangle-network/cli-agent-registry/native-session-contracts";
15
16
  import { z } from "zod";
16
17
  import { fileURLToPath, pathToFileURL } from "node:url";
18
+ import { DatabaseSync } from "node:sqlite";
17
19
  import { StringDecoder } from "node:string_decoder";
18
20
  import { DEFAULT_INTERACTION_MCP_SERVER_NAME, InteractionMcpServer as InteractionMcpServer$1, interactionMcpToolName } from "@tangle-network/agent-core/interactions/mcp";
21
+ import { isRecord, isRecord as isPlainRecord } from "@tangle-network/agent-core";
19
22
  //#region src/acp-process-launch.ts
20
- /** Spawn an ACP agent over NDJSON stdio. The adapter owns abort, so no signal. */
23
+ /** Spawn an ACP agent over NDJSON stdio from `env` alone, never the sidecar's own environment. The adapter owns abort, so no signal. */
21
24
  function spawnAcpBridge(request) {
22
25
  return spawnRpcBridge({
23
26
  ...request,
24
- maxStderrBytes: 65536
27
+ inheritEnv: false
25
28
  });
26
29
  }
27
30
  /**
@@ -34,13 +37,12 @@ function jsonRpcErrorText(err) {
34
37
  return cause && cause !== err.message ? `${err.message}: ${cause}` : err.message;
35
38
  }
36
39
  /**
37
- * Overlay the agent environment on an ACP process env, dropping unset keys. An
38
- * absent env means the inherited one, which the overlay then starts from.
40
+ * An ACP process env: the spec's keys with the agent environment over them,
41
+ * unset keys dropped. The agent environment is the whole inherited part.
39
42
  */
40
43
  function withAgentEnvironment(env, agent) {
41
- if (Object.keys(agent).length === 0) return env;
42
44
  const merged = {};
43
- for (const [key, value] of Object.entries(env ?? process.env)) if (value !== void 0) merged[key] = value;
45
+ for (const [key, value] of Object.entries(env ?? {})) if (value !== void 0) merged[key] = value;
44
46
  for (const [key, value] of Object.entries(agent)) if (value !== void 0) merged[key] = value;
45
47
  return merged;
46
48
  }
@@ -61,32 +63,6 @@ function acpContentText(content) {
61
63
  }
62
64
  return parts.join("");
63
65
  }
64
- /** Map an ACP stop reason onto a turn status. */
65
- function mapAcpStopReason(reason) {
66
- switch (reason) {
67
- case "end_turn": return { status: "completed" };
68
- case "max_tokens": return {
69
- status: "completed",
70
- detail: "Stopped at max tokens (output may be truncated)"
71
- };
72
- case "max_turn_requests": return {
73
- status: "failed",
74
- detail: "Hit max turn-requests limit"
75
- };
76
- case "refusal": return {
77
- status: "failed",
78
- detail: "Agent refused the request"
79
- };
80
- case "cancelled": return {
81
- status: "failed",
82
- detail: "Cancelled"
83
- };
84
- default: return {
85
- status: "failed",
86
- detail: `Unknown stop reason: ${reason ?? "<missing>"}`
87
- };
88
- }
89
- }
90
66
  //#endregion
91
67
  //#region src/reasoning-parts.ts
92
68
  /**
@@ -578,23 +554,6 @@ function nearestExistingAncestor(target) {
578
554
  current = parent;
579
555
  }
580
556
  }
581
- /**
582
- * True when a CLI can create and write `target`.
583
- *
584
- * A read-only mount is the case this exists for: the shared tool-cache root is
585
- * mounted read-only with only named subdirectories bound read-write, so a path
586
- * under it that has no bind mount refuses `mkdir` with `EROFS`. A CLI that
587
- * creates its cache directory at startup then dies before it runs, and a
588
- * caller waiting on that run observes silence, not an error.
589
- *
590
- * The check reads permissions and never creates anything. Creating the
591
- * directory here would run as the sidecar's own user and leave a directory the
592
- * agent user cannot write, which trades one unusable path for another.
593
- *
594
- * A directory the calling process may write but the agent subprocess may not
595
- * still reads as usable. Ownership is the spawn layer's concern; this answers
596
- * the mount-level question that silently kills a CLI.
597
- */
598
557
  function isWritableTarget(target) {
599
558
  const ancestor = nearestExistingAncestor(target);
600
559
  if (!ancestor) return false;
@@ -659,9 +618,7 @@ function homeXdgEnv(home) {
659
618
  //#region src/adapter-lifecycle.ts
660
619
  const DEFAULT_CONTEXT_WINDOW = 2e5;
661
620
  const execFileAsync = promisify(execFile);
662
- /**
663
- * Console-based logger fallback.
664
- */
621
+ /** Console-based logger fallback. */
665
622
  const consoleLogger = {
666
623
  debug: (msg, meta) => console.debug(`[cli-provider] ${msg}`, meta ?? ""),
667
624
  info: (msg, meta) => console.info(`[cli-provider] ${msg}`, meta ?? ""),
@@ -670,13 +627,9 @@ const consoleLogger = {
670
627
  };
671
628
  /** Owns configuration, initialization, cancellation, and subprocess identity. */
672
629
  var CliAdapterLifecycle = class {
673
- /**
674
- * The message log directory when the caller names none. Unset, it is the
675
- * registry spec's `messageStorageDir` for `provider`, the one source; only
676
- * an adapter for a provider the registry does not know sets it.
677
- */
630
+ /** The message log directory when the caller names none. */
678
631
  defaultStorageDir;
679
- inheritProcessEnv = true;
632
+ inheritProcessEnv;
680
633
  config;
681
634
  initialConfig;
682
635
  logger;
@@ -700,12 +653,10 @@ var CliAdapterLifecycle = class {
700
653
  this.storagePath = options.storagePath ?? process.cwd();
701
654
  this._storageDir = options.storageDir;
702
655
  this.agentEnvironment = options.agentEnvironment;
656
+ this.inheritProcessEnv = options.agentEnvironment === void 0;
703
657
  this.launcher = options.launcher;
704
658
  this.ownProcessGroup = options.ownProcessGroup ?? false;
705
659
  }
706
- /**
707
- * Get the storage directory. Uses provided storageDir or falls back to defaultStorageDir.
708
- */
709
660
  get storageDir() {
710
661
  return this._storageDir ?? this.defaultStorageDir ?? messageStorageDirOf(this.provider);
711
662
  }
@@ -717,17 +668,7 @@ var CliAdapterLifecycle = class {
717
668
  if (binding.sessionId !== sessionId) throw new Error(`Interaction binding session "${binding.sessionId}" does not match adapter session "${sessionId}"`);
718
669
  return binding;
719
670
  }
720
- /**
721
- * The uid/gid every CLI process this adapter spawns runs as, or undefined
722
- * when the host process is already unprivileged.
723
- *
724
- * This is an adapter-wide invariant, not a per-callsite option, because
725
- * these CLIs seed their own state on first use — a sqlite database, a log
726
- * directory, a downloaded-binary cache under the harness HOME. An auxiliary
727
- * invocation left at root (`mcp add`, `mcp list`, `--version`) creates that
728
- * tree root-owned, and the streamed run, which does drop privileges, then
729
- * cannot open its own log and exits before emitting a single event.
730
- */
671
+ /** The uid/gid every CLI process this adapter spawns runs as, or undefined when the host process is already unprivileged. */
731
672
  subprocessIdentity() {
732
673
  const assigned = this.config.subprocessUser;
733
674
  if (assigned) {
@@ -749,16 +690,11 @@ var CliAdapterLifecycle = class {
749
690
  subprocessSpawnOptions() {
750
691
  return this.subprocessIdentity() ?? {};
751
692
  }
752
- /**
753
- * Run a one-shot CLI command under this adapter's subprocess identity.
754
- *
755
- * Every auxiliary invocation an adapter makes goes through here so the
756
- * identity cannot be forgotten at a call site. Auxiliary output stays out
757
- * of the streamed turn's raw evidence.
758
- */
693
+ /** Run a one-shot CLI command under this adapter's subprocess identity. */
759
694
  runCli(config) {
760
695
  return execCli({
761
696
  ...config,
697
+ inheritEnv: this.inheritProcessEnv && config.inheritEnv !== false,
762
698
  rawCapturePath: void 0,
763
699
  ...this.config.subprocessUser ? { isolatedIdentity: this.config.subprocessUser } : {},
764
700
  spawnOptions: {
@@ -767,14 +703,7 @@ var CliAdapterLifecycle = class {
767
703
  }
768
704
  });
769
705
  }
770
- /**
771
- * Run a builtin-only Node script under this adapter's subprocess identity
772
- * and return its stdout. File work inside a directory the CLI user owns goes
773
- * through here, so a root sidecar never creates, reads or writes there with
774
- * its own rights: a link the agent plants reaches only what the agent could
775
- * already reach. The script reads `args` from argv after `--` and `input`
776
- * from stdin. A failure carries the script's stderr, not the script text.
777
- */
706
+ /** Run a builtin-only Node script under this adapter's subprocess identity and return its stdout. */
778
707
  async runNodeAsSubprocess(script, args, input = "") {
779
708
  try {
780
709
  const pending = execFileAsync(process.execPath, [
@@ -884,26 +813,26 @@ var CliAdapterLifecycle = class {
884
813
  model: this.config.model
885
814
  });
886
815
  }
887
- /**
888
- * Auth/config files the adapter needs on disk that do NOT arrive through
889
- * `config.model.authFiles` — e.g. a synthesized `~/.openclaw/openclaw.json`
890
- * pointing at the managed router. The sidecar materializes these through the
891
- * hardened `materializeCliAuthFiles` path AFTER `initialize()`, so an adapter
892
- * can derive them from the resolved config/env. Default: none.
893
- */
816
+ /** Auth/config files the adapter needs on disk that do NOT arrive through `config.model.authFiles` — e.g. */
894
817
  getManagedAuthFiles() {
895
818
  return [];
896
819
  }
897
820
  /** Provider-specific initialization runs inside the tracked lifecycle. */
898
821
  async afterInitialize(_config) {}
899
- /**
900
- * The harness session contract this adapter honours (cli-agent-registry
901
- * NATIVE_SESSION_CONTRACTS), or null for a provider that declares none.
902
- * Registration refuses a harness spec without one; ACP overrides this per
903
- * sub-agent.
904
- */
822
+ /** The harness session contract this adapter honours (cli-agent-registry NATIVE_SESSION_CONTRACTS), or null for a provider that declares none. */
905
823
  sessionContract() {
906
- return NATIVE_SESSION_CONTRACTS[this.provider] ?? null;
824
+ return NATIVE_SESSION_CONTRACTS$1[this.provider] ?? null;
825
+ }
826
+ async withExecution(signal, run) {
827
+ const execution = beginExecution(this, signal);
828
+ try {
829
+ return await run(execution.controller.signal);
830
+ } catch (error) {
831
+ execution.controller.abort();
832
+ throw error;
833
+ } finally {
834
+ finishExecution(this, execution);
835
+ }
907
836
  }
908
837
  async shutdown() {
909
838
  this.initialization.cancel();
@@ -981,7 +910,8 @@ var CliAdapterLifecycle = class {
981
910
  }
982
911
  /** The agent environment the host supplied (CliProviderOptions.agentEnvironment). */
983
912
  agentEnvironmentValues() {
984
- return this.agentEnvironment?.() ?? {};
913
+ const secretProfile = !!this.config.secretProfileRevisionRoot;
914
+ return this.agentEnvironment?.({ secretProfile }) ?? (secretProfile ? {} : { ...process.env });
985
915
  }
986
916
  applyWritableHomeDefaults(adapterEnv, workspaceRoot) {
987
917
  const env = {
@@ -1068,15 +998,19 @@ function runtimeSecretsOverlay(runtimeSecrets, fallback) {
1068
998
  function hasPortableProfileControls(profile) {
1069
999
  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;
1070
1000
  }
1001
+ function systemPromptConflict(request, profile) {
1002
+ 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`);
1003
+ }
1004
+ /** A blank profile prompt declares none, so the request prompt stands. */
1071
1005
  function resolveSystemPrompt(requestPrompt, profilePrompt) {
1072
- if (profilePrompt === void 0) return requestPrompt;
1073
- if (requestPrompt !== void 0 && requestPrompt.length > 0 && requestPrompt !== profilePrompt) throw new Error("execution system prompt conflicts with the materialized AgentProfile");
1006
+ if (!profilePrompt?.trim()) return requestPrompt;
1007
+ if (requestPrompt && requestPrompt !== profilePrompt) throw systemPromptConflict(requestPrompt, profilePrompt);
1074
1008
  return profilePrompt;
1075
1009
  }
1076
1010
  function profileForExecution(profile, requestPrompt) {
1077
1011
  const profilePrompt = profile?.prompt?.systemPrompt;
1078
- if (!profile || profilePrompt === void 0 || requestPrompt === void 0 || requestPrompt.length === 0 || requestPrompt === profilePrompt) return profile;
1079
- if (!requestPrompt.startsWith(`${profilePrompt}\n\n`)) throw new Error("execution system prompt conflicts with the materialized AgentProfile");
1012
+ if (!profile || !profilePrompt?.trim() || !requestPrompt || requestPrompt === profilePrompt) return profile;
1013
+ if (!requestPrompt.startsWith(`${profilePrompt}\n\n`)) throw systemPromptConflict(requestPrompt, profilePrompt);
1080
1014
  return {
1081
1015
  ...profile,
1082
1016
  prompt: {
@@ -1101,10 +1035,10 @@ function resolveModel(requestModel, profileModel, configuredModel, profileProvid
1101
1035
  }
1102
1036
  //#endregion
1103
1037
  //#region src/profile-revision-receipts.ts
1104
- /** Receipts belong to their adapter; they never become subclass controls. */
1038
+ /** Receipts belong to their adapter; resolved secrets never enter them. */
1105
1039
  var ProfileRevisionReceipts = class {
1106
- publicProfileRevisionReceipts = /* @__PURE__ */ new WeakMap();
1107
- secretProfileRevisionReceipts = /* @__PURE__ */ new WeakMap();
1040
+ public = /* @__PURE__ */ new WeakMap();
1041
+ secret = /* @__PURE__ */ new WeakMap();
1108
1042
  };
1109
1043
  const receiptsByAdapter = /* @__PURE__ */ new WeakMap();
1110
1044
  function profileRevisionReceipts(owner) {
@@ -1116,18 +1050,31 @@ function profileRevisionReceipts(owner) {
1116
1050
  return receipts;
1117
1051
  }
1118
1052
  function assertExecutionProfileRevision(owner, getConfig, input, profilePlan) {
1119
- if (getConfig().publicProfileRevisionRoot && profilePlan && !profileRevisionReceipts(owner).publicProfileRevisionReceipts.has(profilePlan)) throw new Error("Provider added public profile launch controls after immutable revision preparation");
1120
- if (getConfig().secretProfileRevisionRoot && profilePlan && !profileRevisionReceipts(owner).secretProfileRevisionReceipts.has(profilePlan)) throw new Error("Provider added secret profile launch controls after immutable revision preparation");
1121
- const expectedSecretReceipt = input.expectedSecretProfileReceipt;
1122
- if (getConfig().secretProfileRevisionRoot) {
1123
- const actual = profilePlan ? profileRevisionReceipts(owner).secretProfileRevisionReceipts.get(profilePlan) : void 0;
1124
- if (!expectedSecretReceipt || !actual || actual.executorReceipt !== expectedSecretReceipt) throw new Error("Secret native profile receipt mismatch before native spawn");
1125
- } else if (expectedSecretReceipt !== void 0) throw new Error("Secret native profile execution is unavailable");
1126
- const expectedRevision = input.expectedPublicProfileRevision;
1127
- if (expectedRevision !== void 0) {
1128
- const actualRevision = profilePlan ? profileRevisionReceipts(owner).publicProfileRevisionReceipts.get(profilePlan) : void 0;
1129
- if (!actualRevision || actualRevision.workspacePlanDigest !== expectedRevision.workspacePlanDigest || actualRevision.materializedDigest !== expectedRevision.materializedDigest || actualRevision.launchDigest !== expectedRevision.launchDigest) throw new Error("Public native profile revision mismatch before native spawn");
1130
- }
1053
+ const config = getConfig();
1054
+ const receipts = profileRevisionReceipts(owner);
1055
+ const actualPublic = profilePlan ? receipts.public.get(profilePlan) : void 0;
1056
+ const actualSecret = profilePlan ? receipts.secret.get(profilePlan) : void 0;
1057
+ for (const [root, receipt, kind] of [[
1058
+ config.publicProfileRevisionRoot,
1059
+ actualPublic,
1060
+ "public"
1061
+ ], [
1062
+ config.secretProfileRevisionRoot,
1063
+ actualSecret,
1064
+ "secret"
1065
+ ]]) if (root && profilePlan && !receipt) throw new Error(`Provider added ${kind} profile launch controls after immutable revision preparation`);
1066
+ const { expectedSecretProfileReceipt, expectedPublicProfileRevision } = input;
1067
+ if (config.secretProfileRevisionRoot) {
1068
+ if (!expectedSecretProfileReceipt || !actualSecret || actualSecret.executorReceipt !== expectedSecretProfileReceipt) throw new Error("Secret native profile receipt mismatch before native spawn");
1069
+ } else if (expectedSecretProfileReceipt !== void 0) throw new Error("Secret native profile execution is unavailable");
1070
+ assertPublicReceipt(actualPublic, expectedPublicProfileRevision);
1071
+ }
1072
+ function assertPublicReceipt(actual, expected) {
1073
+ if (expected !== void 0 && (!actual || [
1074
+ "workspacePlanDigest",
1075
+ "materializedDigest",
1076
+ "launchDigest"
1077
+ ].some((key) => actual[key] !== expected[key]))) throw new Error("Public native profile revision mismatch before native spawn");
1131
1078
  }
1132
1079
  //#endregion
1133
1080
  //#region src/errors.ts
@@ -1230,23 +1177,6 @@ function buildFileUrl(input) {
1230
1177
  function deterministicFilePartId(messageId, key) {
1231
1178
  return `file-${sha1Hex(`${messageId}|${key}`).slice(0, 16)}`;
1232
1179
  }
1233
- /**
1234
- * Stateless derivation of the canonical file-part fields for one observed
1235
- * path: resolve-once (normalize, then anchor a relative path to
1236
- * `workspaceRoot`), and from that single resolved path derive the dedupe
1237
- * `key`, the deterministic `id`, the `filename`, the inferred `mediaType`,
1238
- * and the percent-encoded `file://` `url` — so every consumer that surfaces
1239
- * the same file agrees on all five.
1240
- *
1241
- * Returns `null` for a relative path with no `workspaceRoot` to anchor it:
1242
- * detection is opportunistic and a fabricated location is worse than a
1243
- * missing part (see `trackFilePart` for the full rationale).
1244
- *
1245
- * This is the ONE derivation chain. `trackFilePart` (exec path, with
1246
- * ExecutionState memoization) and stateless mappers that own no
1247
- * ExecutionState (codex's app-server item mapper) both call it — never
1248
- * re-implement the chain, or the two paths drift.
1249
- */
1250
1180
  function deriveFilePart(input) {
1251
1181
  const normalized = normalize(input.path);
1252
1182
  let resolvedPath;
@@ -1262,33 +1192,6 @@ function deriveFilePart(input) {
1262
1192
  url: buildFileUrl({ path: resolvedPath })
1263
1193
  };
1264
1194
  }
1265
- /**
1266
- * Tracks a file an adapter observed being created/modified (e.g. from a
1267
- * write-tool call) and returns the canonical `FilePart` fields to stream,
1268
- * deduping by resolved-absolute-path or data-URI content so repeated writes
1269
- * to the same file update one part instead of appending duplicates.
1270
- *
1271
- * The path is normalized and resolved to an absolute form ONCE, and that same
1272
- * resolved value drives both the dedupe key and the `file://` url — so two
1273
- * observations of the same file that differ only in spelling (`./chart.png`
1274
- * vs `/workspace/chart.png`, or `/workspace/sub/../chart.png` vs
1275
- * `/workspace/chart.png`) unify on one part with one stable url, satisfying
1276
- * the stream invariant that a given part id keeps a stable url.
1277
- *
1278
- * A relative path can only be resolved against `state.workspaceRoot` (set by
1279
- * the adapter from the CLI's cwd). Detection here is opportunistic — a
1280
- * tool-call observation, not a guarantee of an absolute path — so when no
1281
- * workspaceRoot is available to anchor a relative path, we drop the
1282
- * observation (`null`) rather than fabricate an anchor (e.g. resolving
1283
- * against "/"): a wrong url is worse than a missing part, and a caller that
1284
- * genuinely lost the file would get a wrong, permanent url once it's on the
1285
- * stream.
1286
- *
1287
- * Detection is opportunistic for other reasons too: a delete, or an
1288
- * observation carrying neither a path nor a data URI, also yields no part
1289
- * (`null`) rather than an error — callers are not required to have a file
1290
- * signal on every tool result.
1291
- */
1292
1195
  function trackFilePart(state, args) {
1293
1196
  if (args.changeKind === "delete") return null;
1294
1197
  if (args.path === void 0 && args.dataUri === void 0) return null;
@@ -1536,51 +1439,23 @@ function enabledInteractionKinds(config, supported) {
1536
1439
  if (typeof config !== "object" || config === null) return [];
1537
1440
  return supported.filter((kind) => config[kind] === true);
1538
1441
  }
1539
- /**
1540
- * Read a turn's ephemeral context, rejecting a wrong-typed value loudly.
1541
- *
1542
- * The published `AgentExecutionInput` does not declare the field, so nothing
1543
- * type-checks it at the process boundary. Left unchecked, a caller passing a
1544
- * non-string gets it stringified into the model prompt (`[object Object]`) —
1545
- * a corrupted turn with no error anywhere. `undefined`/`null` mean "no context
1546
- * this turn"; an empty string is likewise nothing to append.
1547
- */
1548
1442
  function readEphemeralContext(input) {
1549
1443
  const context = input.ephemeralContext;
1550
1444
  if (context === void 0 || context === null) return void 0;
1551
1445
  if (typeof context !== "string") throw new TypeError(`ephemeralContext must be a string, received ${typeof context}`);
1552
1446
  return context || void 0;
1553
1447
  }
1554
- /**
1555
- * Compose the model-facing prompt for a turn: the caller's own text, then this
1556
- * turn's ephemeral context. Call it at the point the prompt is handed to the
1557
- * provider — NEVER on the parts handed to `recordUserMessage` or the message
1558
- * store.
1559
- */
1448
+ /** Compose the model-facing prompt for a turn: the caller's own text, then this turn's ephemeral context. */
1560
1449
  function withEphemeralContext(promptText, input) {
1561
1450
  const context = readEphemeralContext(input);
1562
1451
  if (!context) return promptText;
1563
1452
  return promptText ? `${promptText}\n\n${context}` : context;
1564
1453
  }
1565
- /**
1566
- * Resolve whether an interaction kind is active for THIS turn. When the turn
1567
- * carries `interactions` it is authoritative — a complete set that can also
1568
- * turn a kind OFF mid-session; otherwise the adapter falls back to its latched
1569
- * config (the initialize-time wiring, or a direct non-sidecar caller's option).
1570
- */
1571
1454
  function turnInteractionEnabled(input, latched, kind) {
1572
1455
  const perTurn = input.interactions;
1573
1456
  return interactionToggleEnabled(perTurn !== void 0 ? perTurn : latched, kind);
1574
1457
  }
1575
- /**
1576
- * Overlay this turn's interactions onto a latched config, returning a NEW
1577
- * object (no shared mutation — safe when one adapter instance serves concurrent
1578
- * turns from different sessions). When the turn omits `interactions` the config
1579
- * is returned unchanged, preserving behavior for direct non-sidecar callers.
1580
- * Adapters read plan/permission/question posture off the returned config so a
1581
- * mid-session toggle takes effect and can turn a kind off (which the latched
1582
- * config, only ever merged by updateConfig, cannot).
1583
- */
1458
+ /** Overlay this turn's interactions onto a latched config, returning a NEW object (no shared mutation — safe when. */
1584
1459
  function withTurnInteractions(config, input) {
1585
1460
  if (input.interactions === void 0) return config;
1586
1461
  return {
@@ -1588,29 +1463,30 @@ function withTurnInteractions(config, input) {
1588
1463
  interactions: input.interactions
1589
1464
  };
1590
1465
  }
1591
- //#endregion
1592
- //#region src/adapter-profile.ts
1593
1466
  /**
1594
- * Revisions that already passed the native health check in this process, by
1595
- * the verifier's scope and the revision's materialized digest. The check asks
1596
- * whether the pinned native CLI loads this revision's bytes: a function of
1597
- * those bytes and that CLI, not of the session or subprocess UID that will
1598
- * read them (the ACL grant that publishes a revision to a UID throws when it
1599
- * fails). A session's identity changes whenever its profile variant or turn
1600
- * credential does, so keying by the session's receipt checked an unchanged
1601
- * profile again every turn. Secret values are excluded from the digest.
1602
- * Agents are rebuilt per request, so this outlives any adapter.
1603
- * Insertion-ordered and bounded: the oldest entry is forgotten first.
1467
+ * The one precedence every engine names a failed turn's end with (#16404): an
1468
+ * end the abort, the timeout or the harness named is theirs; only an
1469
+ * unexplained death is the link's, the memory limit's or the harness's.
1604
1470
  */
1471
+ function turnEndReason(end) {
1472
+ if (end.aborted) return "abort";
1473
+ if (end.timedOut) return "timeout";
1474
+ if (end.harnessFailed) return "failed";
1475
+ if (end.linkLost) return "gateway_disconnect";
1476
+ if (end.memoryKilled) return "memory_limit";
1477
+ return end.signal ? "harness_exit" : "failed";
1478
+ }
1479
+ //#endregion
1480
+ //#region src/adapter-profile.ts
1481
+ /** Revisions that already passed the native health check in this process, by the verifier's scope and the revision's materialized digest. */
1605
1482
  const verifiedSecretProfileRevisions = /* @__PURE__ */ new Set();
1606
1483
  const VERIFIED_SECRET_PROFILE_REVISIONS_MAX = 1024;
1607
1484
  function rememberVerifiedSecretProfileRevision(key) {
1608
1485
  verifiedSecretProfileRevisions.delete(key);
1609
1486
  verifiedSecretProfileRevisions.add(key);
1610
- while (verifiedSecretProfileRevisions.size > VERIFIED_SECRET_PROFILE_REVISIONS_MAX) {
1487
+ if (verifiedSecretProfileRevisions.size > VERIFIED_SECRET_PROFILE_REVISIONS_MAX) {
1611
1488
  const oldest = verifiedSecretProfileRevisions.values().next().value;
1612
- if (oldest === void 0) break;
1613
- verifiedSecretProfileRevisions.delete(oldest);
1489
+ if (oldest !== void 0) verifiedSecretProfileRevisions.delete(oldest);
1614
1490
  }
1615
1491
  }
1616
1492
  /** The executor's naming key, refused unless root owns it alone. */
@@ -1628,35 +1504,19 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
1628
1504
  supportsSecretProfileRevisions = false;
1629
1505
  profileSecretProvider;
1630
1506
  secretProfileRuntimeKeyProvider;
1631
- /**
1632
- * The materialized digest behind each secret revision. It stays here: the
1633
- * executor receipt is all a caller sees of a revision's resolved contents.
1634
- */
1635
- secretProfileMaterializedDigests = /* @__PURE__ */ new WeakMap();
1636
- /**
1637
- * The value behind each `{env:NAME}` reference in a secret revision's
1638
- * config, by that revision's application. Executor memory only: no launch
1639
- * environment, argv or file ever carries them.
1640
- */
1641
- secretProfileReferenceValues = /* @__PURE__ */ new WeakMap();
1507
+ /** Private native preparation state; never part of a returned receipt. */
1508
+ secretProfileRevisions = /* @__PURE__ */ new WeakMap();
1642
1509
  constructor(options = {}) {
1643
1510
  super(options);
1644
- profileRevisionReceipts(this);
1645
1511
  this.profileSecretProvider = options.profileSecretProvider ?? UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER;
1646
1512
  this.secretProfileRuntimeKeyProvider = options.secretProfileRuntimeKeyProvider ?? UNAVAILABLE_AGENT_PROFILE_SECRET_PROVIDER;
1647
1513
  }
1648
- /** Apply captured diagnostic identity to this spawn, after profile composition. */
1649
1514
  /** The exact child environment a turn uses; shared with prewarm paths. */
1650
1515
  buildTurnEnvironment(executionConfig, profilePlan, workspaceRoot, correlationHeaders) {
1651
1516
  const merged = this.mergeProfilePlanEnvironment(this.buildNativeExecutionEnvironment(executionConfig), profilePlan, executionConfig.nativeAuthEnv !== void 0 || (executionConfig.authFiles?.length ?? 0) > 0);
1652
1517
  return this.bindExecutionCorrelation(this.applyWritableHomeDefaults(this.bindProfileToolBinDirs(merged, profilePlan), workspaceRoot), correlationHeaders);
1653
1518
  }
1654
- /**
1655
- * A profile's declared executable tools, sealed in its revision, stand in
1656
- * for the app's own tool dirs on this launch's PATH tail. A secret profile's
1657
- * env no longer names the app's dirs (app env is scrubbed), so the sidecar's
1658
- * own SANDBOX_TOOL_BIN_DIRS says which inherited PATH entries they are.
1659
- */
1519
+ /** 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. */
1660
1520
  bindProfileToolBinDirs(env, profilePlan) {
1661
1521
  const toolBinDirs = profilePlan?.spawn.toolBinDirs;
1662
1522
  if (!toolBinDirs?.length) return env;
@@ -1669,35 +1529,45 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
1669
1529
  if (correlationHeaders !== void 0 && Object.keys(correlationHeaders).some((name) => name !== "X-Tangle-Trace-ID")) this.logger.warn("Native model request correlation is unsupported", { provider: this.provider });
1670
1530
  return environment;
1671
1531
  }
1672
- /**
1673
- * Provision the inline profile's files and native execution controls with the
1674
- * shared materializer used by cli-bridge. Unsupported portable controls fail
1675
- * before spawn unless the profile is explicitly best-effort
1676
- * (resources.failOnError: false), which materializes the supported subset
1677
- * and warns about the rest.
1678
- */
1679
- /**
1680
- * Launcher controls this adapter OWNS on the spawn it starts — see
1681
- * `BINDABLE_LAUNCH_CONTROLS` in the materializer. Empty by default: a plan
1682
- * whose intent needs a launcher binding then fails closed instead of being
1683
- * silently dropped. An adapter that really applies the plan's launch flags
1684
- * to its own spawn (sdk-provider-opencode selecting the plan's primary
1685
- * agent via `--agent`) overrides this with the controls it honors.
1686
- */
1532
+ /** Launcher controls this adapter OWNS on the spawn it starts — see `BINDABLE_LAUNCH_CONTROLS` in the materializer. */
1687
1533
  profileMaterializationBinds() {
1688
1534
  return [];
1689
1535
  }
1690
- /** Optional native instruction root. The adapter owns how its runtime locates this directory. */
1536
+ /** Optional native instruction root. */
1691
1537
  profileAgentDirectory(_workspaceRoot) {}
1692
1538
  /** Provider transport bound into the private immutable revision and receipt. */
1693
1539
  secretProfileProviderConfig() {}
1694
- /**
1695
- * The values behind a secret revision's `{env:NAME}` config references, for
1696
- * an executor that serves those references itself. Never pass them to the
1697
- * native process.
1698
- */
1540
+ /** The values behind a secret revision's `{env:NAME}` config references, for an executor that serves those references itself. */
1699
1541
  secretProfileReferences(application) {
1700
- return application ? this.secretProfileReferenceValues.get(application) : void 0;
1542
+ return application ? this.secretProfileRevisions.get(application)?.references : void 0;
1543
+ }
1544
+ /** The one sealed session view repointed only while its native turn owns the queue. */
1545
+ secretProfileSessionView(sessionId) {
1546
+ const root = this.config.secretProfileRevisionRoot;
1547
+ const subprocessUid = this.config.subprocessUser?.uid;
1548
+ if (!root || subprocessUid === void 0) throw new Error("Secret profile session view is unavailable");
1549
+ const launchRoot = join(root, "launch");
1550
+ return {
1551
+ launchRoot,
1552
+ view: secretSessionViewWorkspace({
1553
+ launchRoot,
1554
+ sharedWorkspaceDir: this.storagePath,
1555
+ sessionId,
1556
+ subprocessUid
1557
+ }),
1558
+ subprocessUid
1559
+ };
1560
+ }
1561
+ /** Bind the private revision into the session's stable native cwd. */
1562
+ bindSecretProfileSessionView(revisionWorkspaceDir, sessionId) {
1563
+ const { launchRoot, view, subprocessUid } = this.secretProfileSessionView(sessionId);
1564
+ bindSecretSessionView({
1565
+ launchRoot,
1566
+ viewWorkspaceDir: view,
1567
+ revisionWorkspaceDir,
1568
+ subprocessUid
1569
+ });
1570
+ return view;
1701
1571
  }
1702
1572
  async materializeProfileResources(inputProfile, workspaceRoot, ownerId, sessionId, scope = "turn") {
1703
1573
  if (!inputProfile && !this.config.runtimeAttachments) return void 0;
@@ -1743,7 +1613,7 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
1743
1613
  });
1744
1614
  const launchDigest = revision.manifest.launchDigest;
1745
1615
  if (!launchDigest) throw new Error("Concurrent profile launch digest was not recorded");
1746
- profileRevisionReceipts(this).publicProfileRevisionReceipts.set(revision.application, {
1616
+ profileRevisionReceipts(this).public.set(revision.application, {
1747
1617
  workspacePlanDigest: revision.manifest.workspacePlanDigest,
1748
1618
  materializedDigest: revision.manifest.materializedDigest,
1749
1619
  launchDigest,
@@ -1763,10 +1633,7 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
1763
1633
  });
1764
1634
  return application;
1765
1635
  }
1766
- /**
1767
- * Publish the profile as an executor-sealed revision for this session's
1768
- * subprocess identity, and record the receipt that binds it to the turn.
1769
- */
1636
+ /** Publish the profile as an executor-sealed revision for this session's subprocess identity, and record the receipt that binds it to the turn. */
1770
1637
  async applySecretProfileRevision(plan, secretProfileRevisionRoot, workspaceRoot, ownerId, sessionId) {
1771
1638
  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");
1772
1639
  const namingKey = readSecretProfileNamingKey(this.config.secretProfileNamingKeyPath);
@@ -1784,25 +1651,24 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
1784
1651
  const launchDigest = revision.manifest.launchDigest;
1785
1652
  if (!launchDigest) throw new Error("Secret concurrent profile launch digest is absent");
1786
1653
  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")}`;
1787
- this.secretProfileReferenceValues.set(revision.application, revision.secretReferences);
1788
- this.secretProfileMaterializedDigests.set(revision.application, revision.manifest.materializedDigest);
1789
- profileRevisionReceipts(this).secretProfileRevisionReceipts.set(revision.application, {
1654
+ this.secretProfileRevisions.set(revision.application, {
1655
+ materializedDigest: revision.manifest.materializedDigest,
1656
+ references: revision.secretReferences
1657
+ });
1658
+ profileRevisionReceipts(this).secret.set(revision.application, {
1790
1659
  workspacePlanDigest: revision.manifest.workspacePlanDigest,
1791
1660
  executorReceipt,
1792
1661
  conflicts: revision.manifest.conflicts.map((conflict) => ({ ...conflict }))
1793
1662
  });
1794
1663
  return revision.application;
1795
1664
  }
1796
- /**
1797
- * Prepare the same provider-composed native plan that execute() will check.
1798
- * This may publish an immutable public revision, but never starts a model turn.
1799
- */
1665
+ /** Prepare the same provider-composed native plan that execute() will check. */
1800
1666
  async preparePublicProfileRevision(input) {
1801
1667
  if (!this.config.publicProfileRevisionRoot) throw new Error("Concurrent public profile materialization is disabled");
1802
1668
  const sessionScope = input.userId ?? input.sessionId;
1803
1669
  if (!sessionScope) throw new Error("Concurrent public profile preparation needs a session scope");
1804
1670
  const application = await this.materializeProfileResources(this.resolveExecutionProfileForInput(input), input.workspaceRoot ?? this.storagePath, sessionScope, input.sessionId);
1805
- const receipt = application ? profileRevisionReceipts(this).publicProfileRevisionReceipts.get(application) : void 0;
1671
+ const receipt = application ? profileRevisionReceipts(this).public.get(application) : void 0;
1806
1672
  if (!receipt) throw new Error("Concurrent public profile preparation produced no native plan");
1807
1673
  return receipt;
1808
1674
  }
@@ -1814,9 +1680,9 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
1814
1680
  const startedAt = performance.now();
1815
1681
  const application = await this.materializeProfileResources(this.resolveExecutionProfileForInput(input), input.workspaceRoot ?? this.storagePath, sessionScope, input.sessionId);
1816
1682
  const materializedAt = performance.now();
1817
- const receipt = application ? profileRevisionReceipts(this).secretProfileRevisionReceipts.get(application) : void 0;
1683
+ const receipt = application ? profileRevisionReceipts(this).secret.get(application) : void 0;
1818
1684
  if (!application || !receipt) throw new Error("Secret profile preparation produced no native plan");
1819
- const materializedDigest = this.secretProfileMaterializedDigests.get(application);
1685
+ const materializedDigest = this.secretProfileRevisions.get(application)?.materializedDigest;
1820
1686
  if (!materializedDigest) throw new Error("Secret profile preparation produced no revision digest");
1821
1687
  const verification = `${this.secretProfileVerificationScope()}\0${materializedDigest}`;
1822
1688
  const verifyCached = verifiedSecretProfileRevisions.has(verification);
@@ -1829,11 +1695,7 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
1829
1695
  });
1830
1696
  return receipt;
1831
1697
  }
1832
- /**
1833
- * What the native health check's verdict depends on besides a revision's
1834
- * bytes: the executable that loads them. A provider whose check runs
1835
- * through a configurable binary names it here.
1836
- */
1698
+ /** What the native health check's verdict depends on besides a revision's bytes: the executable that loads them. */
1837
1699
  secretProfileVerificationScope() {
1838
1700
  return this.provider;
1839
1701
  }
@@ -1862,18 +1724,18 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
1862
1724
  extraFlags: [...config.extraFlags ?? [], ...profilePlan.spawn.flags]
1863
1725
  };
1864
1726
  }
1727
+ /** A profile's addition precedes host context while preserving its harness prompt. */
1728
+ resolveProfilePrompts(input, plan, profile = this.resolveExecutionProfileForInput(input)) {
1729
+ const addition = plan?.spawn.appendSystemPrompt;
1730
+ const keep = keepsHarnessSystemPrompt(profile, this.profileHarness);
1731
+ return {
1732
+ systemPrompt: keep ? void 0 : this.resolveProfileSystemPrompt(input.systemPrompt, plan),
1733
+ appendSystemPrompt: keep ? joinSystemPromptAdditions(addition, input.systemPrompt, input.appendSystemPrompt) : input.appendSystemPrompt ? joinSystemPromptAdditions(addition, input.appendSystemPrompt) : addition
1734
+ };
1735
+ }
1865
1736
  resolveProfileSystemPrompt(requestPrompt, profilePlan) {
1866
1737
  return resolveSystemPrompt(requestPrompt, profilePlan?.spawn.systemPrompt);
1867
1738
  }
1868
- /**
1869
- * The plan's additive prompt intent for this spawn. No request-level
1870
- * counterpart exists (`AgentExecutionInput` carries only the replacement),
1871
- * so unlike {@link resolveProfileSystemPrompt} there is nothing to
1872
- * conflict-check against.
1873
- */
1874
- resolveProfileAppendSystemPrompt(profilePlan) {
1875
- return profilePlan?.spawn.appendSystemPrompt;
1876
- }
1877
1739
  resolveProfileModel(requestModel) {
1878
1740
  return resolveModel(requestModel, this.config.profile?.model?.default, this.config.model, this.config.profile?.model?.provider);
1879
1741
  }
@@ -1893,26 +1755,20 @@ var CliAdapterProfile = class extends CliAdapterLifecycle {
1893
1755
  }
1894
1756
  async preparePromptInput(parts, workspaceRoot, sessionId, options) {
1895
1757
  return preparePromptInputParts(parts, workspaceRoot, sessionId, {
1896
- nativeAttachmentKinds: options?.nativeAttachmentKinds ?? this.nativeAttachmentKinds(),
1897
- ...options?.inlineImageBudgetBytes !== void 0 ? { inlineImageBudgetBytes: options.inlineImageBudgetBytes } : {}
1758
+ ...options,
1759
+ nativeAttachmentKinds: options?.nativeAttachmentKinds ?? this.nativeAttachmentKinds()
1898
1760
  });
1899
1761
  }
1900
- /**
1901
- * Attachment kinds this adapter forwards natively via buildExecArgs (argv
1902
- * flag / stdin content block) instead of the `[Image: ...]` / `[File: ...]`
1903
- * text marker. The base adapter omits those kinds' markers from the prompt
1904
- * text — an adapter that opts a kind in MUST deliver every attachment of
1905
- * that kind itself, or the model never sees it.
1906
- */
1762
+ /** Attachment kinds this adapter forwards natively via buildExecArgs (argv flag / stdin content block) instead of the `[Image: ...]` / `[File: ...]` text marker. */
1907
1763
  nativeAttachmentKinds() {
1908
1764
  return [];
1909
1765
  }
1910
1766
  };
1911
1767
  //#endregion
1912
1768
  //#region src/native-session-enforcement.ts
1913
- /** Store location and identity deadlines apply once a receipt proves the contract. */
1914
- function sessionContractFullyEnforced(provider) {
1915
- return !Object.hasOwn(HARNESS_CONFORMANCE_PENDING, provider);
1769
+ /** Every shipped harness is enforced. A missing receipt is a failed proof, not a skip. */
1770
+ function sessionContractFullyEnforced(_provider) {
1771
+ return true;
1916
1772
  }
1917
1773
  //#endregion
1918
1774
  //#region src/adapter-sessions.ts
@@ -2177,30 +2033,25 @@ const FAILURE_STDERR_TAIL_LIMIT = 600;
2177
2033
  * message.
2178
2034
  *
2179
2035
  * A provider that reports its OWN terminal failure — an empty event stream, an
2180
- * unfinished model step — explains what it saw but not how the process ended,
2181
- * and those are different defects: a CLI that exited 0 having emitted nothing
2182
- * is misconfigured, one killed on the timeout is stuck, one that exited 9 with
2183
- * stderr crashed. The exit status and the CLI's stderr are in hand at the
2184
- * point the error is built; without this they reached only a log line and the
2185
- * caller got a bare sentence to debug from.
2186
- *
2187
- * Each part is skipped when the message already states it, so a message
2188
- * derived FROM stderr or from the exit code is never doubled.
2036
+ * unfinished model step — explains what it saw but not how the process ended:
2037
+ * a CLI that exited 0 having emitted nothing is misconfigured, one killed on the
2038
+ * timeout is stuck, one that exited 9 with stderr crashed. Each part is skipped
2039
+ * when the message already states it, so a message derived FROM stderr or the
2040
+ * exit code is never doubled.
2189
2041
  */
2190
2042
  function withCliExitDiagnostics(message, exit, stderr) {
2191
2043
  const parts = [];
2192
2044
  if (exit.stdinError && !message.includes(exit.stdinError)) parts.push(`stdin failed: ${exit.stdinError}`);
2193
- if (exit.timedOut) {
2194
- const timeoutDetail = exit.timeoutDiagnostic;
2195
- if (timeoutDetail) {
2196
- if (!message.includes(timeoutDetail)) parts.push(timeoutDetail);
2197
- } else if (!/timed out/i.test(message)) parts.push("timed out");
2198
- }
2045
+ const timeout = exit.timedOut ? exit.timeoutDiagnostic ?? "timed out" : "";
2046
+ if (timeout && !message.toLowerCase().includes(timeout.toLowerCase())) parts.push(timeout);
2199
2047
  if (exit.code !== null) {
2200
2048
  if (!message.includes(`code ${exit.code}`)) parts.push(`exit code ${exit.code}`);
2201
2049
  } else if (exit.signal) parts.push(`killed by ${exit.signal}`);
2202
2050
  const trimmedStderr = stderr.trim();
2203
- if (trimmedStderr && !message.includes(trimmedStderr)) parts.push(`stderr: ${trimmedStderr.length > FAILURE_STDERR_TAIL_LIMIT ? `…${trimmedStderr.slice(-FAILURE_STDERR_TAIL_LIMIT)}` : trimmedStderr}`);
2051
+ if (trimmedStderr && !message.includes(trimmedStderr)) {
2052
+ const tail = trimmedStderr.slice(-FAILURE_STDERR_TAIL_LIMIT);
2053
+ parts.push(`stderr: ${tail === trimmedStderr ? "" : "…"}${tail}`);
2054
+ }
2204
2055
  return parts.length === 0 ? message : `${message} (${parts.join("; ")})`;
2205
2056
  }
2206
2057
  //#endregion
@@ -2366,9 +2217,20 @@ var CliAdapterMcp = class extends CliAdapterSessions {
2366
2217
  };
2367
2218
  //#endregion
2368
2219
  //#region src/execution-state.ts
2369
- /**
2370
- * Creates a new execution state.
2371
- */
2220
+ /** A file of the sandbox's cgroup (#7571); undefined off Linux or outside one. */
2221
+ function readCgroupFile(name) {
2222
+ try {
2223
+ return readFileSync(`/sys/fs/cgroup/${name}`, "utf8").trim();
2224
+ } catch {
2225
+ return;
2226
+ }
2227
+ }
2228
+ /** The sandbox's OOM-kill count: a running total for the whole box. */
2229
+ function oomKills() {
2230
+ const events = readCgroupFile("memory.events") ?? "";
2231
+ return Number(/^oom_kill (\d+)$/m.exec(events)?.[1] ?? 0);
2232
+ }
2233
+ /** Creates a new execution state. */
2372
2234
  function createExecutionState(sessionId, messageId, workspaceRoot) {
2373
2235
  return {
2374
2236
  sessionId,
@@ -2376,7 +2238,8 @@ function createExecutionState(sessionId, messageId, workspaceRoot) {
2376
2238
  workspaceRoot,
2377
2239
  toolCalls: /* @__PURE__ */ new Map(),
2378
2240
  textBuffer: "",
2379
- reasoningBuffer: []
2241
+ reasoningBuffer: [],
2242
+ oomKillsAtStart: oomKills()
2380
2243
  };
2381
2244
  }
2382
2245
  //#endregion
@@ -2438,10 +2301,9 @@ async function findCachedTurn(recorder, input, logger, sessionId) {
2438
2301
  turnId: input.turnId,
2439
2302
  sessionId
2440
2303
  });
2441
- const outcome = cached.outcome === void 0 ? { type: "completed" } : AgentExecutionOutcomeSchema.parse(cached.outcome);
2442
2304
  return {
2443
2305
  ...cached,
2444
- outcome
2306
+ outcome: AgentExecutionOutcomeSchema.parse(cached.outcome)
2445
2307
  };
2446
2308
  }
2447
2309
  /**
@@ -2482,7 +2344,98 @@ function armNativeSessionDeadline(params) {
2482
2344
  deadline.unref();
2483
2345
  return deadline;
2484
2346
  }
2485
- /** Accumulate a turn process's stderr as text. */
2347
+ function idIn(glob, rel) {
2348
+ const g = glob.split("/");
2349
+ const r = rel.split("/");
2350
+ let gi = 0;
2351
+ let ri = 0;
2352
+ let id = null;
2353
+ while (gi < g.length && ri < r.length) {
2354
+ const part = g[gi];
2355
+ if (part === "**") {
2356
+ const rest = g.slice(gi + 1).join("/");
2357
+ if (!rest) return id;
2358
+ for (let k = ri; k < r.length; k++) {
2359
+ const found = idIn(rest, r.slice(k).join("/"));
2360
+ if (found) return found;
2361
+ }
2362
+ return null;
2363
+ }
2364
+ if (part?.includes("{id}")) {
2365
+ const [pre, post] = part.split("{id}");
2366
+ const name = r[ri] ?? "";
2367
+ if (!name.startsWith(pre ?? "") || !name.endsWith(post ?? "")) return null;
2368
+ id = name.slice((pre ?? "").length, name.length - (post ?? "").length);
2369
+ } else if (part !== "*" && part !== r[ri]) return null;
2370
+ gi += 1;
2371
+ ri += 1;
2372
+ }
2373
+ return gi === g.length && ri === r.length ? id : null;
2374
+ }
2375
+ function filesUnder(dir, prefix, out) {
2376
+ let entries;
2377
+ try {
2378
+ entries = readdirSync(dir, { withFileTypes: true });
2379
+ } catch {
2380
+ return;
2381
+ }
2382
+ for (const entry of entries) {
2383
+ const name = String(entry.name);
2384
+ const rel = prefix ? `${prefix}/${name}` : name;
2385
+ if (entry.isDirectory()) filesUnder(join(dir, name), rel, out);
2386
+ else out.push(rel);
2387
+ }
2388
+ }
2389
+ function idsNow(home, stores) {
2390
+ const ids = /* @__PURE__ */ new Set();
2391
+ for (const store of stores) {
2392
+ const root = join(home, store.root);
2393
+ if (store.shared && store.write === "sqlite-wal") {
2394
+ try {
2395
+ const db = new DatabaseSync(join(root, store.files[0] ?? "state.db"), { readOnly: true });
2396
+ try {
2397
+ let rows;
2398
+ try {
2399
+ rows = db.prepare("SELECT id FROM sessions").all();
2400
+ } catch {
2401
+ rows = db.prepare("SELECT session_id AS id FROM session_windows").all();
2402
+ }
2403
+ for (const row of rows) if (row.id) ids.add(row.id);
2404
+ } finally {
2405
+ db.close();
2406
+ }
2407
+ } catch {}
2408
+ continue;
2409
+ }
2410
+ const found = [];
2411
+ filesUnder(root, "", found);
2412
+ for (const rel of found) for (const glob of store.files) {
2413
+ const id = idIn(glob, rel);
2414
+ if (id) ids.add(id);
2415
+ }
2416
+ }
2417
+ return ids;
2418
+ }
2419
+ function watchNewSessionIds(params) {
2420
+ const home = params.runtimeHome?.trim().replace(/\/+$/u, "");
2421
+ const contract = params.sessionContract;
2422
+ if (!home || contract?.identity.kind !== "first-event") return () => {};
2423
+ const seen = idsNow(home, contract.stores);
2424
+ const timer = setInterval(() => {
2425
+ if (params.observer.nativeSessionObserved) return;
2426
+ for (const id of idsNow(home, contract.stores)) {
2427
+ if (seen.has(id)) continue;
2428
+ seen.add(id);
2429
+ params.observer.onEvent({
2430
+ type: "native.session.observed",
2431
+ provider: params.provider,
2432
+ nativeSessionId: id
2433
+ });
2434
+ }
2435
+ }, 200);
2436
+ timer.unref();
2437
+ return () => clearInterval(timer);
2438
+ }
2486
2439
  function collectStderr(cliProcess) {
2487
2440
  let text = "";
2488
2441
  const stderrDecoder = new StringDecoder("utf8");
@@ -2496,16 +2449,46 @@ function collectStderr(cliProcess) {
2496
2449
  return text;
2497
2450
  } };
2498
2451
  }
2499
- /** The message a failed turn reports, from the strongest available cause. */
2500
- function describeCliFailure(params) {
2501
- const { provider, state, exitResult, stderr, command, nixBinPath } = params;
2502
- const spawnFailure = exitResult.spawnError ? `Failed to spawn "${command}": ${exitResult.spawnError}` + (exitResult.spawnError.includes("ENOENT") ? ` — binary not found in PATH${nixBinPath ? ` (NIX_BIN_PATH=${nixBinPath})` : " (NIX_BIN_PATH unset — is the Nix profile mounted?)"}` : "") : null;
2503
- const failureCause = 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}`));
2504
- return spawnFailure ?? withCliExitDiagnostics(failureCause, exitResult, stderr);
2452
+ /** A turn's stderr or ACP error when its link to its harness's server dropped (#8819). */
2453
+ const HARNESS_LINK_LOST = /\b(?:ECONNRESET|ECONNREFUSED|EPIPE|UND_ERR_SOCKET)\b|ended before (?:it|the turn) completed|Gateway disconnected/u;
2454
+ /**
2455
+ * Names a SIGKILL from the memory limit (#7571): the CLI runs in the sandbox's
2456
+ * cgroup, whose `memory.events` counts OOM kills under the `memory.max` limit.
2457
+ * The count is the box's running total, so only a kill since the turn started
2458
+ * is this turn's.
2459
+ */
2460
+ function memoryLimitKill(killsAtStart = 0) {
2461
+ const kills = oomKills() - killsAtStart;
2462
+ if (kills < 1) return void 0;
2463
+ const max = readCgroupFile("memory.max") ?? "max";
2464
+ 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`;
2505
2465
  }
2506
- /** The error a failed turn throws, carrying the stream's failure details. */
2507
- function buildTurnFailure(params) {
2508
- const { provider, errorMessage, state, startTime, endTime } = params;
2466
+ /**
2467
+ * A failed CLI turn's end and error (#16404): a guard's end is its own, the
2468
+ * rest is `turnEndReason`'s; a session death and a memory-limit kill carry
2469
+ * their wire code.
2470
+ */
2471
+ function endFailedTurn(params) {
2472
+ const { provider, state, exitResult, stderr, startTime, endTime } = params;
2473
+ const oom = exitResult.signal === "SIGKILL" && memoryLimitKill(state.oomKillsAtStart);
2474
+ state.endReason ??= turnEndReason({
2475
+ aborted: params.aborted,
2476
+ timedOut: exitResult.timedOut,
2477
+ harnessFailed: Boolean(state.terminalStatus),
2478
+ linkLost: HARNESS_LINK_LOST.test(stderr),
2479
+ memoryKilled: Boolean(oom),
2480
+ signal: exitResult.signal
2481
+ });
2482
+ if (oom && state.endReason === "memory_limit") state.failure ??= {
2483
+ code: "MEMORY_LIMIT_KILLED",
2484
+ message: oom
2485
+ };
2486
+ if (["gateway_disconnect", "harness_exit"].includes(state.endReason)) state.failure ??= {
2487
+ code: "SESSION_DIED",
2488
+ message: `Harness session died (${state.endReason}): start a new session, or send the next turn to this session to resume it`
2489
+ };
2490
+ const nix = params.nixBinPath ? ` (NIX_BIN_PATH=${params.nixBinPath})` : " (NIX_BIN_PATH unset — is the Nix profile mounted?)";
2491
+ 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);
2509
2492
  const failure = new AgentExecutionError(`${provider} execution failed: ${errorMessage}`, {
2510
2493
  ...state.tokenUsage ? { tokenUsage: state.tokenUsage } : {},
2511
2494
  timing: {
@@ -2514,14 +2497,19 @@ function buildTurnFailure(params) {
2514
2497
  durationMs: endTime - startTime
2515
2498
  }
2516
2499
  });
2517
- if (state.failure) Object.assign(failure, {
2518
- ...state.failure.code ? { code: state.failure.code } : {},
2519
- ...state.failure.statusCode === void 0 ? {} : {
2500
+ Object.assign(failure, {
2501
+ reason: state.endReason,
2502
+ ...state.failure?.code ? { code: state.failure.code } : {},
2503
+ ...state.failure?.statusCode === void 0 ? {} : {
2520
2504
  statusCode: state.failure.statusCode,
2521
2505
  status: state.failure.statusCode
2522
- }
2506
+ },
2507
+ ...state.failure?.resetAt ? { resetAt: state.failure.resetAt } : {}
2523
2508
  });
2524
- return failure;
2509
+ return {
2510
+ errorMessage,
2511
+ failure
2512
+ };
2525
2513
  }
2526
2514
  /**
2527
2515
  * Best-effort: stamp the assistant message interrupted so reattach can
@@ -2570,14 +2558,12 @@ function buildExecutionResult(params) {
2570
2558
  }
2571
2559
  };
2572
2560
  }
2573
- /** A turn whose provider session record failed to persist ends failed. */
2574
2561
  function failOnProviderSessionPersistence(observer, state) {
2575
2562
  if (!observer.providerSessionPersistenceError) return;
2576
2563
  const message = observer.providerSessionPersistenceError instanceof Error ? observer.providerSessionPersistenceError.message : String(observer.providerSessionPersistenceError);
2577
2564
  state.terminalStatus = "failed";
2578
2565
  state.lastDiagnostic = `Provider session persistence failed: ${message}`;
2579
2566
  }
2580
- /** Log a non-zero exit the provider accepted, and the stderr a successful turn left. */
2581
2567
  function logAcceptedCliExit(logger, exitResult, stderr) {
2582
2568
  if (!exitResult.success) logger.info("CLI process exited non-zero but was treated as success by provider", {
2583
2569
  exitCode: exitResult.code,
@@ -2683,14 +2669,20 @@ function describeFailure(part, repairToolNames) {
2683
2669
  }
2684
2670
  function createRepeatedToolFailureGuard(options) {
2685
2671
  const { limit } = options;
2686
- if (!Number.isInteger(limit) || limit < 1) throw new Error(`Repeated tool-failure limit must be a positive whole number (got ${limit})`);
2687
2672
  const repairToolNames = options.repairToolNames ?? NO_REPAIR_TOOL_NAMES;
2688
2673
  let signature;
2689
2674
  let previousInput;
2690
2675
  let count = 0;
2691
2676
  let lastScoredPartId;
2692
2677
  let tripped = false;
2693
- return { observe(part) {
2678
+ return {
2679
+ /**
2680
+ * Feed one tool part. Returns the trip on the call that reaches the limit,
2681
+ * and `undefined` every other time. A tripped guard stays tripped and
2682
+ * reports once, so a caller can kill the turn without racing the events
2683
+ * still in flight behind it.
2684
+ */
2685
+ observe(part) {
2694
2686
  if (tripped) return void 0;
2695
2687
  if (isUnresolved(part.state)) return void 0;
2696
2688
  if (part.id === lastScoredPartId) return void 0;
@@ -2716,22 +2708,6 @@ function createRepeatedToolFailureGuard(options) {
2716
2708
  }
2717
2709
  //#endregion
2718
2710
  //#region src/turn-liveness.ts
2719
- /**
2720
- * How long the provider stream stays quiet before the adapter emits a liveness
2721
- * event, and how often it re-checks that quiet window.
2722
- *
2723
- * A model that reasons for a minute and a run that died look the same on the
2724
- * wire: an open stream with nothing on it. A consumer that must show progress
2725
- * resolves the ambiguity with an inactivity timeout. The Blueprint Agent
2726
- * frontend allows 20 s (blueprint-agent
2727
- * apps/web/src/ui-provider/devcontainer-events/hooks/useStreamWatchdog.ts).
2728
- * It then aborts a turn that the CLI is still running.
2729
- *
2730
- * A ping on quiet feeds that timeout without inventing content. The check
2731
- * interval bounds the longest silence at quiet plus check, so these values put
2732
- * the worst case at 6 s. A provider that emits more often than the quiet
2733
- * window never trips the ticker, so nothing double-emits.
2734
- */
2735
2711
  const STREAM_LIVENESS_QUIET_MS = 5e3;
2736
2712
  const STREAM_LIVENESS_CHECK_MS = 1e3;
2737
2713
  /**
@@ -2821,7 +2797,6 @@ const REPEATED_TOOL_FAILURE_LIMIT = 3;
2821
2797
  var TurnObserver = class {
2822
2798
  providerSessionPersistence = Promise.resolve();
2823
2799
  providerSessionPersistenceError;
2824
- repeatedToolFailureTripped = false;
2825
2800
  nativeSessionObserved = false;
2826
2801
  partUpdates;
2827
2802
  liveness;
@@ -2875,7 +2850,7 @@ var TurnObserver = class {
2875
2850
  });
2876
2851
  if (repeated) {
2877
2852
  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
- this.repeatedToolFailureTripped = true;
2853
+ state.endReason = "repeated_tool_failure";
2879
2854
  state.terminalStatus = "failed";
2880
2855
  state.lastDiagnostic = detail;
2881
2856
  logger.error("Repeated tool-call rejection stopped the turn", {
@@ -2951,26 +2926,7 @@ function cliTurnProcess(child, logger, spawnStarted) {
2951
2926
  }
2952
2927
  //#endregion
2953
2928
  //#region src/base-adapter.ts
2954
- /**
2955
- * Base adapter for CLI-based AI agent providers.
2956
- *
2957
- * Subclasses must implement:
2958
- * - provider: string identifier
2959
- * - defaultCliPath: default CLI executable name
2960
- * - buildExecArgs(): build CLI execution arguments
2961
- * - buildExecEnv(): build environment variables
2962
- * - createEventParser(): create JSONL event parser
2963
- * - mapEventToStreamEvents(): map parsed events to StreamEvents
2964
- *
2965
- * The message log directory is the registry spec's `messageStorageDir` for
2966
- * `provider` (cli-agent-registry `messageStorageDirOf`); only an adapter for
2967
- * a provider the registry does not know sets `defaultStorageDir`.
2968
- *
2969
- * Optional overrides:
2970
- * - buildMcpAddArgs(): MCP add command arguments
2971
- * - buildMcpListArgs(): MCP list command arguments (if supported)
2972
- * - parseMcpListOutput(): parse MCP list output
2973
- */
2929
+ /** Base adapter for CLI-based AI agent providers. */
2974
2930
  var CliProviderAdapterBase = class extends CliAdapterMcp {
2975
2931
  supportsManagedQuote = false;
2976
2932
  async execute(input, services, onEvent) {
@@ -2978,13 +2934,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
2978
2934
  const quoteVersion = input.managedQuoteVersion;
2979
2935
  if (quoteVersion !== void 0 && !this.supportsManagedQuote) throw new Error(`${this.provider} does not support the managed-quote native model transport`);
2980
2936
  const correlationHeaders = executionCorrelationHeaders(input.interactionBinding, input.traceId, quoteVersion);
2981
- const execution = beginExecution(this, input.abortSignal);
2982
- const result = this.executeInternal(input, services, onEvent, execution.controller.signal, correlationHeaders);
2983
- result.then(() => finishExecution(this, execution), () => {
2984
- execution.controller.abort();
2985
- finishExecution(this, execution);
2986
- });
2987
- return result;
2937
+ return this.withExecution(input.abortSignal, (signal) => this.executeInternal(input, services, onEvent, signal, correlationHeaders));
2988
2938
  }
2989
2939
  async executeInternal(input, services, onEvent, executionSignal, correlationHeaders) {
2990
2940
  const startTime = Date.now();
@@ -3046,7 +2996,8 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3046
2996
  stderr,
3047
2997
  state
3048
2998
  });
3049
- if (!exitResult.success && !benignNonZeroExit || state.terminalStatus === "failed" || observer.repeatedToolFailureTripped) return this.failCliTurn({
2999
+ if (!exitResult.success && !benignNonZeroExit || state.terminalStatus === "failed" || state.endReason) return this.failCliTurn({
3000
+ aborted: executionSignal.aborted,
3050
3001
  onEvent,
3051
3002
  recorder,
3052
3003
  observer,
@@ -3070,10 +3021,6 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3070
3021
  endTime
3071
3022
  });
3072
3023
  }
3073
- /**
3074
- * Record a successful turn: the provider session id, the assistant message,
3075
- * and the result, cached for a same-turnId retry.
3076
- */
3077
3024
  async completeTurn(params) {
3078
3025
  const { input, sessionId, state, recorder, profilePlan } = params;
3079
3026
  if (state.sessionId && sessionId && state.sessionId !== sessionId) await this.getSessionStore().update(sessionId, {
@@ -3093,7 +3040,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3093
3040
  outputTokens: state.tokenUsage.outputTokens
3094
3041
  } : void 0
3095
3042
  });
3096
- const publicProfileRevision = profilePlan ? profileRevisionReceipts(this).publicProfileRevisionReceipts.get(profilePlan) : void 0;
3043
+ const publicProfileRevision = profilePlan ? profileRevisionReceipts(this).public.get(profilePlan) : void 0;
3097
3044
  const result = buildExecutionResult({
3098
3045
  outcome: await this.executionOutcome(state, input),
3099
3046
  state,
@@ -3110,14 +3057,16 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3110
3057
  async failCliTurn(params) {
3111
3058
  const { onEvent, recorder, observer, state, exitResult, stderr } = params;
3112
3059
  const { launch, startTime, endTime } = params;
3113
- const { env, execArgs } = launch;
3114
- const errorMessage = describeCliFailure({
3060
+ const { errorMessage, failure } = endFailedTurn({
3115
3061
  provider: this.provider,
3116
3062
  state,
3117
3063
  exitResult,
3118
3064
  stderr,
3119
- command: execArgs.command,
3120
- nixBinPath: env.NIX_BIN_PATH
3065
+ aborted: params.aborted,
3066
+ command: launch.execArgs.command,
3067
+ nixBinPath: launch.env.NIX_BIN_PATH,
3068
+ startTime,
3069
+ endTime
3121
3070
  });
3122
3071
  this.logger.error("CLI process failed", {
3123
3072
  exitCode: exitResult.code,
@@ -3129,7 +3078,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3129
3078
  stderr: stderr.slice(0, 2e3),
3130
3079
  diagnostic: state.lastDiagnostic,
3131
3080
  durationMs: endTime - startTime,
3132
- command: execArgs.command
3081
+ command: launch.execArgs.command
3133
3082
  });
3134
3083
  if (exitResult.timedOut) onEvent?.({
3135
3084
  type: "warning",
@@ -3141,13 +3090,6 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3141
3090
  status: "failed",
3142
3091
  detail: errorMessage
3143
3092
  });
3144
- const failure = buildTurnFailure({
3145
- provider: this.provider,
3146
- errorMessage,
3147
- state,
3148
- startTime,
3149
- endTime
3150
- });
3151
3093
  await recordTurnInterruption({
3152
3094
  recorder,
3153
3095
  observer,
@@ -3158,11 +3100,6 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3158
3100
  });
3159
3101
  throw failure;
3160
3102
  }
3161
- /**
3162
- * Open the turn's session (sidecar/provider persistence only; native CLI
3163
- * startup is measured separately). A same-turnId retry of a completed turn
3164
- * gets that turn's cached result instead of running again.
3165
- */
3166
3103
  async openTurnSession(input, recorder, onEvent) {
3167
3104
  const sessionPrepareStarted = performance.now();
3168
3105
  const adapter = this;
@@ -3204,14 +3141,13 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3204
3141
  cached
3205
3142
  };
3206
3143
  }
3207
- if (input.turnId && recorder.setTurnId) recorder.setTurnId(input.turnId);
3144
+ if (input.turnId) recorder.setTurnId?.(input.turnId);
3208
3145
  return {
3209
3146
  ...prepared,
3210
3147
  providerMessageIds,
3211
3148
  cached: void 0
3212
3149
  };
3213
3150
  }
3214
- /** Provision the profile's cwd-native dimensions for this turn. */
3215
3151
  async materializeTurnProfile(input, sessionId) {
3216
3152
  const executionProfile = this.resolveExecutionProfileForInput(input);
3217
3153
  const profilePlan = await this.materializeProfileResources(executionProfile, input.workspaceRoot ?? this.storagePath, input.userId ?? sessionId, sessionId);
@@ -3221,14 +3157,9 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3221
3157
  profilePlan
3222
3158
  };
3223
3159
  }
3224
- /**
3225
- * Prepare the prompt and record the caller's own parts. `promptText`
3226
- * additionally carries the replayed transcript and this turn's ephemeral
3227
- * context, which are execution-only and stay out of both stores.
3228
- */
3229
- async recordTurnPrompt(input, session, recorder) {
3160
+ async recordTurnPrompt(input, session, recorder, attachmentOptions) {
3230
3161
  const { sessionId, providerMessageIds } = session;
3231
- const preparedInput = await this.preparePromptInput(normalizeInputParts(input), input.workspaceRoot ?? this.storagePath, sessionId);
3162
+ const preparedInput = await this.preparePromptInput(normalizeInputParts(input), input.workspaceRoot ?? this.storagePath, sessionId, attachmentOptions);
3232
3163
  const transcriptContext = await buildTranscriptContext({
3233
3164
  provider: this.provider,
3234
3165
  sessionId,
@@ -3265,13 +3196,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3265
3196
  sessionId: providerSessionId,
3266
3197
  ...mintedSessionId ? { mintedSessionId } : {},
3267
3198
  workspaceRoot,
3268
- ...keepsHarnessSystemPrompt(executionProfile, this.profileHarness) ? {
3269
- systemPrompt: void 0,
3270
- appendSystemPrompt: joinSystemPromptAdditions(this.resolveProfileAppendSystemPrompt(profilePlan), input.systemPrompt, input.appendSystemPrompt)
3271
- } : {
3272
- systemPrompt: this.resolveProfileSystemPrompt(input.systemPrompt, profilePlan),
3273
- appendSystemPrompt: input.appendSystemPrompt ? joinSystemPromptAdditions(this.resolveProfileAppendSystemPrompt(profilePlan), input.appendSystemPrompt) : this.resolveProfileAppendSystemPrompt(profilePlan)
3274
- },
3199
+ ...this.resolveProfilePrompts(input, profilePlan, executionProfile),
3275
3200
  attachments,
3276
3201
  profilePlan
3277
3202
  });
@@ -3329,10 +3254,9 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3329
3254
  });
3330
3255
  await observer.providerSessionPersistence;
3331
3256
  if (observer.providerSessionPersistenceError) throw observer.providerSessionPersistenceError;
3257
+ this.logger.observePhase?.("native_session_bind", performance.now() - nativeBindStarted);
3332
3258
  }
3333
- if (knownNativeSessionId) this.logger.observePhase?.("native_session_bind", performance.now() - nativeBindStarted);
3334
3259
  }
3335
- /** Spawn the turn's process, feed its input, and wait for it to exit. */
3336
3260
  async runCliTurn(params) {
3337
3261
  const { input, sessionId, providerSessionId, executionSignal } = params;
3338
3262
  const { launch, promptText, state, observer, startTime } = params;
@@ -3353,6 +3277,17 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3353
3277
  const cliProcess = spawnedProcess instanceof Promise ? await spawnedProcess : spawnedProcess;
3354
3278
  this.activeProcesses.add(cliProcess);
3355
3279
  observer.attachProcess(cliProcess);
3280
+ const stopSessionWatch = watchNewSessionIds({
3281
+ provider: this.provider,
3282
+ runtimeHome: this.config.runtimeHome,
3283
+ sessionContract: launch.sessionContract,
3284
+ observer
3285
+ });
3286
+ if (providerSessionId && launch.sessionContract?.identity.kind === "first-event") observer.onEvent({
3287
+ type: "native.session.observed",
3288
+ provider: this.provider,
3289
+ nativeSessionId: providerSessionId
3290
+ });
3356
3291
  const identityDeadline = armNativeSessionDeadline({
3357
3292
  provider: this.provider,
3358
3293
  sessionId,
@@ -3367,8 +3302,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3367
3302
  let exitResult;
3368
3303
  try {
3369
3304
  if (execArgs.useStdin) {
3370
- const stdinMessage = execArgs.stdin && execArgs.stdin.length > 0 ? execArgs.stdin : promptText;
3371
- cliProcess.write(this.formatStdinMessage(stdinMessage));
3305
+ cliProcess.write(this.formatStdinMessage(execArgs.stdin || promptText));
3372
3306
  cliProcess.closeInput();
3373
3307
  }
3374
3308
  exitResult = await cliProcess.exitPromise;
@@ -3377,6 +3311,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3377
3311
  await cliProcess.exitPromise;
3378
3312
  throw error;
3379
3313
  } finally {
3314
+ stopSessionWatch();
3380
3315
  clearInterval(livenessTicker);
3381
3316
  if (identityDeadline) clearTimeout(identityDeadline);
3382
3317
  this.activeProcesses.delete(cliProcess);
@@ -3407,13 +3342,7 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3407
3342
  subprocessSpawnOptions: this.subprocessSpawnOptions()
3408
3343
  });
3409
3344
  }
3410
- /**
3411
- * The launch one turn would make, without starting it: the spawn config
3412
- * (argv, cwd, environment, identity or launcher), the stdin payload, and
3413
- * the profile files written into the workspace. Session and message
3414
- * stores are untouched and no held process is reserved, so a caller that
3415
- * runs the process itself (`execCli`) owns its output and lifecycle.
3416
- */
3345
+ /** The launch one turn would make, without starting it: the spawn config (argv, cwd, environment, identity or launcher). */
3417
3346
  async prepareLaunch(input, options = {}) {
3418
3347
  this.ensureInitialized();
3419
3348
  const sessionId = input.sessionId ?? randomUUID();
@@ -3436,12 +3365,11 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3436
3365
  const { execArgs } = launch;
3437
3366
  return {
3438
3367
  spawn: this.turnSpawnConfig(execArgs, launch.workspaceRoot, launch.env),
3439
- ...execArgs.useStdin ? { stdin: this.formatStdinMessage(execArgs.stdin && execArgs.stdin.length > 0 ? execArgs.stdin : promptText) } : {},
3368
+ ...execArgs.useStdin ? { stdin: this.formatStdinMessage(execArgs.stdin || promptText) } : {},
3440
3369
  ...launch.mintedSessionId ? { nativeSessionId: launch.mintedSessionId } : {},
3441
3370
  ...profilePlan ? { workspacePlan: profilePlan.receipt } : {}
3442
3371
  };
3443
3372
  }
3444
- /** Override only when the provider has a turn transport besides a CLI child. */
3445
3373
  spawnExecutionProcess(request) {
3446
3374
  const { execArgs, workspaceRoot, env, signal, timeoutMs, parser } = request;
3447
3375
  const spawnStarted = performance.now();
@@ -3459,103 +3387,245 @@ var CliProviderAdapterBase = class extends CliAdapterMcp {
3459
3387
  }
3460
3388
  };
3461
3389
  //#endregion
3462
- //#region src/acp-session-adapter.ts
3463
- const DEFAULT_PROMPT_TIMEOUT_MS = 6e5;
3464
- const DEFAULT_BOOTSTRAP_TIMEOUT_MS = 12e4;
3465
- const DEFAULT_BRIDGE_IDLE_MS = 300 * 1e3;
3466
- /**
3467
- * Grace between `session/cancel` and closing the bridge: long enough for the
3468
- * agent to answer the in-flight `session/prompt` with `stopReason:
3469
- * "cancelled"`, short enough that abort stays prompt.
3470
- */
3471
- const ABORT_GRACE_MS = 250;
3472
- /**
3473
- * Shared lifecycle of a provider that drives an Agent Client Protocol agent
3474
- * over a JSON-RPC stdio bridge: one cached process and native ACP session per
3475
- * Tangle session, `initialize` then `session/new` (or `session/load` for a
3476
- * provider that resumes), serialized `session/prompt` turns, the cancel
3477
- * sequence (`session/cancel`, grace, close), `session/update` translation,
3478
- * idle reaping and eviction on process exit or turn failure.
3479
- *
3480
- * A provider supplies only its translation: how a turn resolves its config,
3481
- * how the process starts, the prompt blocks, and its own failure signals.
3482
- */
3483
- var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3484
- acpSessions = /* @__PURE__ */ new Map();
3485
- /** Spawn the bridge. Tests override it to swap the child process. */
3486
- spawnBridge(request) {
3487
- return spawnAcpBridge(request);
3488
- }
3489
- /** Refuse an agent version beyond the provider's own floor. */
3490
- checkAgentProtocolVersion(_advertised, _turn) {}
3491
- /**
3492
- * Configure a native session the process just opened or loaded, before its
3493
- * first prompt (e.g. `session/set_model`). A failure fails the acquire.
3494
- */
3495
- async onSessionOpened(_bridge, _acpSessionId, _turn, _options) {}
3496
- /** Install reverse-call handlers on a new bridge, before `initialize`. */
3497
- onBridgeSpawned(_bridge, _sessionId, _turn) {}
3498
- /** Start per-turn state; the returned function ends it on every exit. */
3499
- beginTurn(_sessionId, _input, _onEvent) {
3500
- return () => {};
3501
- }
3502
- /** Inspect a failed tool's output; returned events precede its part update. */
3503
- onToolFailed(_sessionId, _failureText) {
3504
- return [];
3505
- }
3506
- /**
3507
- * A failure the provider recorded for this session during the turn,
3508
- * cleared on read. It outranks the error the turn surfaced as.
3509
- */
3510
- takeTurnFailure(_sessionId) {}
3511
- /** Release provider state tied to a session's process. */
3512
- onSessionEvicted(_sessionId) {}
3513
- /** Extra fields for the turn-failure log line. */
3514
- turnLogContext(_turn) {
3515
- return {};
3516
- }
3517
- /**
3518
- * One turn through the base turn pipeline: session open with transcript
3519
- * replay and cached-turn replay, then input and result recording around the
3520
- * bridge's `session/prompt`. The agent owns its profile, so the base's
3521
- * profile materialization does not run.
3522
- */
3523
- async execute(input, services, onEvent) {
3524
- this.ensureInitialized();
3525
- const turn = this.resolveTurn(input);
3526
- const { config } = turn;
3527
- const { recorder } = services;
3528
- const startTime = Date.now();
3529
- const workspaceRoot = input.workspaceRoot ?? this.storagePath;
3530
- const turnInput = input.sessionId ? input : {
3531
- ...input,
3532
- sessionId: input.interactionBinding?.sessionId
3533
- };
3534
- const opened = await this.openTurnSession(turnInput, recorder, onEvent);
3535
- if (opened.cached) return opened.cached;
3536
- const { sessionId, providerSessionId, providerMessageIds } = opened;
3537
- const endTurn = this.beginTurn(sessionId, input, onEvent);
3538
- try {
3539
- const { preparedInput, promptText } = await this.recordTurnPrompt(turnInput, opened, recorder);
3540
- onEvent?.({
3541
- type: "status",
3542
- status: "started"
3543
- });
3544
- const state = createExecutionState(sessionId, providerMessageIds?.assistantMessageId);
3545
- let entry;
3546
- try {
3547
- entry = await this.acquireAcpSession({
3548
- sessionId,
3549
- ...this.loadsNativeSessions && providerSessionId ? { providerSessionId } : {},
3550
- turn,
3551
- workspaceRoot,
3552
- onEvent
3553
- });
3554
- } catch (err) {
3555
- onEvent?.({
3556
- type: "status",
3557
- status: "failed",
3558
- detail: err instanceof Error ? err.message : String(err)
3390
+ //#region src/metering.ts
3391
+ const UsageRecordSchema = z.record(z.string(), z.unknown());
3392
+ const UsageNumberSchema = z.union([z.number(), z.string().trim().min(1)]).transform(Number).pipe(z.number().finite().nonnegative());
3393
+ const TOKEN_USAGE_INPUT_KEYS = Object.freeze([
3394
+ "inputTokens",
3395
+ "input",
3396
+ "input_tokens",
3397
+ "promptTokens",
3398
+ "prompt_tokens"
3399
+ ]);
3400
+ const TOKEN_USAGE_OUTPUT_KEYS = Object.freeze([
3401
+ "outputTokens",
3402
+ "output",
3403
+ "output_tokens",
3404
+ "completionTokens",
3405
+ "completion_tokens"
3406
+ ]);
3407
+ const TOKEN_USAGE_TOTAL_KEYS = Object.freeze([
3408
+ "totalTokens",
3409
+ "total_tokens",
3410
+ "total"
3411
+ ]);
3412
+ const TOKEN_USAGE_REASONING_KEYS = Object.freeze([
3413
+ "reasoningTokens",
3414
+ "reasoning_tokens",
3415
+ "reasoning",
3416
+ "reasoning_output_tokens"
3417
+ ]);
3418
+ const TOKEN_USAGE_COST_KEYS = Object.freeze([
3419
+ "totalCostUsd",
3420
+ "costUsd",
3421
+ "total_cost_usd",
3422
+ "cost_usd",
3423
+ "cost"
3424
+ ]);
3425
+ const CACHE_READ_INPUT_KEYS = Object.freeze([
3426
+ "cacheReadInputTokens",
3427
+ "cache_read_input_tokens",
3428
+ "cacheRead",
3429
+ "cache_read",
3430
+ "cached_input_tokens"
3431
+ ]);
3432
+ const CACHE_CREATION_INPUT_KEYS = Object.freeze([
3433
+ "cacheCreationInputTokens",
3434
+ "cache_creation_input_tokens",
3435
+ "cacheWrite",
3436
+ "cache_write"
3437
+ ]);
3438
+ const NESTED_COST_KEYS = Object.freeze([
3439
+ "total",
3440
+ "totalUsd",
3441
+ "total_usd",
3442
+ "usd"
3443
+ ]);
3444
+ function asRecord(raw) {
3445
+ const parsed = UsageRecordSchema.safeParse(raw);
3446
+ return parsed.success ? parsed.data : void 0;
3447
+ }
3448
+ function tokenCount(raw) {
3449
+ const parsed = UsageNumberSchema.safeParse(raw);
3450
+ return parsed.success ? Math.trunc(parsed.data) : void 0;
3451
+ }
3452
+ function firstTokenCount(source, keys) {
3453
+ for (const key of keys) {
3454
+ const count = tokenCount(source[key]);
3455
+ if (count !== void 0) return count;
3456
+ }
3457
+ }
3458
+ function firstUsageCostUsd(source, keys = TOKEN_USAGE_COST_KEYS) {
3459
+ for (const key of keys) {
3460
+ const parsed = UsageNumberSchema.safeParse(source[key]);
3461
+ if (parsed.success) return parsed.data;
3462
+ }
3463
+ }
3464
+ function usageRecord(source) {
3465
+ return asRecord(source.tokenUsage) ?? asRecord(source.usage) ?? source;
3466
+ }
3467
+ function readTokenUsage(source) {
3468
+ const usageSource = usageRecord(source);
3469
+ const inputTokens = firstTokenCount(usageSource, TOKEN_USAGE_INPUT_KEYS);
3470
+ const outputTokens = firstTokenCount(usageSource, TOKEN_USAGE_OUTPUT_KEYS);
3471
+ return inputTokens !== void 0 || outputTokens !== void 0 ? {
3472
+ inputTokens,
3473
+ outputTokens
3474
+ } : void 0;
3475
+ }
3476
+ function readTokenCostUsd(source) {
3477
+ const usageSource = usageRecord(source);
3478
+ return firstUsageCostUsd(source) ?? firstUsageCostUsd(usageSource) ?? readNestedCostUsd(source) ?? readNestedCostUsd(usageSource);
3479
+ }
3480
+ function readNestedCostUsd(source) {
3481
+ const cost = asRecord(source.cost);
3482
+ return cost ? firstUsageCostUsd(cost, NESTED_COST_KEYS) : void 0;
3483
+ }
3484
+ function readCliTokenUsage(raw) {
3485
+ const parsed = UsageRecordSchema.safeParse(raw);
3486
+ if (!parsed.success) return void 0;
3487
+ const source = parsed.data;
3488
+ const usageSource = usageRecord(source);
3489
+ const usage = readTokenUsage(source) ?? readTokenUsage(usageSource);
3490
+ const cost = readTokenCostUsd(source);
3491
+ const totalTokens = firstTokenCount(usageSource, TOKEN_USAGE_TOTAL_KEYS);
3492
+ const reasoningTokens = firstTokenCount(usageSource, TOKEN_USAGE_REASONING_KEYS);
3493
+ const cacheReadInputTokens = firstTokenCount(usageSource, CACHE_READ_INPUT_KEYS);
3494
+ const cacheCreationInputTokens = firstTokenCount(usageSource, CACHE_CREATION_INPUT_KEYS);
3495
+ if (!usage && totalTokens === void 0 && reasoningTokens === void 0 && cost === void 0 && cacheReadInputTokens === void 0 && cacheCreationInputTokens === void 0) return;
3496
+ return {
3497
+ inputTokens: usage?.inputTokens,
3498
+ outputTokens: usage?.outputTokens,
3499
+ totalTokens,
3500
+ reasoningTokens,
3501
+ cacheReadInputTokens,
3502
+ cacheCreationInputTokens,
3503
+ cost
3504
+ };
3505
+ }
3506
+ function recordCliTokenUsage(state, ...sources) {
3507
+ for (const source of sources) {
3508
+ const tokenUsage = readCliTokenUsage(source);
3509
+ if (tokenUsage) {
3510
+ state.tokenUsage = tokenUsage;
3511
+ return true;
3512
+ }
3513
+ }
3514
+ return false;
3515
+ }
3516
+ function addUsage(left, right) {
3517
+ if (left === void 0) return right;
3518
+ if (right === void 0) return left;
3519
+ const sum = left + right;
3520
+ return Number.isFinite(sum) ? sum : Math.max(left, right);
3521
+ }
3522
+ /**
3523
+ * Adds one usage report to the running total instead of replacing it.
3524
+ *
3525
+ * For a CLI that reports usage PER TURN (pi) rather than once per run,
3526
+ * `recordCliTokenUsage` would leave the execution billed as its final turn
3527
+ * alone. Use this wherever a single execution can produce more than one usage
3528
+ * report; use `recordCliTokenUsage` when the CLI emits one cumulative total.
3529
+ *
3530
+ * Returns true when a usage report was found in `sources` and folded in.
3531
+ */
3532
+ function accumulateCliTokenUsage(state, ...sources) {
3533
+ for (const source of sources) {
3534
+ const tokenUsage = readCliTokenUsage(source);
3535
+ if (!tokenUsage) continue;
3536
+ const total = state.tokenUsage;
3537
+ state.tokenUsage = total ? {
3538
+ inputTokens: addUsage(total.inputTokens, tokenUsage.inputTokens),
3539
+ outputTokens: addUsage(total.outputTokens, tokenUsage.outputTokens),
3540
+ totalTokens: addUsage(total.totalTokens, tokenUsage.totalTokens),
3541
+ reasoningTokens: addUsage(total.reasoningTokens, tokenUsage.reasoningTokens),
3542
+ cacheReadInputTokens: addUsage(total.cacheReadInputTokens, tokenUsage.cacheReadInputTokens),
3543
+ cacheCreationInputTokens: addUsage(total.cacheCreationInputTokens, tokenUsage.cacheCreationInputTokens),
3544
+ cost: addUsage(total.cost, tokenUsage.cost)
3545
+ } : tokenUsage;
3546
+ return true;
3547
+ }
3548
+ return false;
3549
+ }
3550
+ function parseCliUsageLine(line) {
3551
+ if (!line.startsWith("usage:")) return void 0;
3552
+ const raw = line.slice(6).trim();
3553
+ if (!raw) return void 0;
3554
+ if (raw.startsWith("{")) return asRecord(JSON.parse(raw));
3555
+ const fields = {};
3556
+ for (const part of raw.split(/[,\s]+/)) {
3557
+ if (!part) continue;
3558
+ const separator = part.includes("=") ? "=" : part.includes(":") ? ":" : "";
3559
+ if (!separator) continue;
3560
+ const [key, value] = part.split(separator, 2);
3561
+ if (!key || value === void 0) continue;
3562
+ const numeric = Number(value);
3563
+ fields[key] = Number.isFinite(numeric) ? numeric : value;
3564
+ }
3565
+ return Object.keys(fields).length > 0 ? fields : void 0;
3566
+ }
3567
+ //#endregion
3568
+ //#region src/acp-session-adapter.ts
3569
+ const DEFAULT_PROMPT_TIMEOUT_MS = 6e5;
3570
+ const DEFAULT_BOOTSTRAP_TIMEOUT_MS = 12e4;
3571
+ const DEFAULT_BRIDGE_IDLE_MS = 300 * 1e3;
3572
+ const ABORT_GRACE_MS = 250;
3573
+ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3574
+ acpSessions = /* @__PURE__ */ new Map();
3575
+ spawnBridge(request) {
3576
+ return spawnAcpBridge(request);
3577
+ }
3578
+ checkAgentProtocolVersion(_advertised, _turn) {}
3579
+ resumeSessionId() {}
3580
+ async onSessionOpened(_bridge, _acpSessionId, _turn, _options) {}
3581
+ onBridgeSpawned(_bridge, _sessionId, _turn) {}
3582
+ beginTurn(_sessionId, _input, _onEvent) {
3583
+ return () => {};
3584
+ }
3585
+ onToolFailed(_sessionId, _failureText) {
3586
+ return [];
3587
+ }
3588
+ takeTurnFailure(_sessionId) {}
3589
+ onSessionEvicted(_sessionId) {}
3590
+ turnLogContext(_turn) {
3591
+ return {};
3592
+ }
3593
+ async execute(input, services, onEvent) {
3594
+ this.ensureInitialized();
3595
+ const turn = this.resolveTurn(input);
3596
+ const { config } = turn;
3597
+ const { recorder } = services;
3598
+ const startTime = Date.now();
3599
+ const workspaceRoot = input.workspaceRoot ?? this.storagePath;
3600
+ const turnInput = input.sessionId ? input : {
3601
+ ...input,
3602
+ sessionId: input.interactionBinding?.sessionId
3603
+ };
3604
+ const opened = await this.openTurnSession(turnInput, recorder, onEvent);
3605
+ if (opened.cached) return opened.cached;
3606
+ const { sessionId, providerSessionId, providerMessageIds } = opened;
3607
+ const endTurn = this.beginTurn(sessionId, input, onEvent);
3608
+ try {
3609
+ const { preparedInput, promptText } = await this.recordTurnPrompt(turnInput, opened, recorder);
3610
+ onEvent?.({
3611
+ type: "status",
3612
+ status: "started"
3613
+ });
3614
+ const state = createExecutionState(sessionId, providerMessageIds?.assistantMessageId);
3615
+ let entry;
3616
+ try {
3617
+ entry = await this.acquireAcpSession({
3618
+ sessionId,
3619
+ ...this.loadsNativeSessions && providerSessionId ? { providerSessionId } : {},
3620
+ turn,
3621
+ workspaceRoot,
3622
+ onEvent
3623
+ });
3624
+ } catch (err) {
3625
+ onEvent?.({
3626
+ type: "status",
3627
+ status: "failed",
3628
+ detail: err instanceof Error ? err.message : String(err)
3559
3629
  });
3560
3630
  throw err instanceof Error ? err : new Error(String(err));
3561
3631
  }
@@ -3569,28 +3639,38 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3569
3639
  provider: this.provider,
3570
3640
  nativeSessionId: acpSessionId
3571
3641
  });
3642
+ const timeoutMs = config.promptTimeoutMs ?? DEFAULT_PROMPT_TIMEOUT_MS;
3643
+ const prompt = this.promptBlocks(input, {
3644
+ ...preparedInput,
3645
+ promptText
3646
+ }, turn);
3647
+ let promptResult;
3572
3648
  try {
3573
- const promptResult = await this.promptAcpSession(entry, {
3574
- prompt: this.promptBlocks(input, {
3575
- ...preparedInput,
3576
- promptText
3577
- }, turn),
3578
- timeoutMs: config.promptTimeoutMs ?? DEFAULT_PROMPT_TIMEOUT_MS,
3579
- abortSignal: input.abortSignal,
3580
- onUpdate: (params) => {
3581
- for (const event of this.mapAcpUpdate(params, state, sessionId)) onEvent?.(event);
3582
- }
3583
- });
3649
+ for (let attempt = 0; attempt < 3; attempt++) try {
3650
+ promptResult = await this.promptAcpSession(entry, {
3651
+ prompt,
3652
+ timeoutMs: Math.min(timeoutMs, 12e4),
3653
+ abortSignal: input.abortSignal,
3654
+ onUpdate: (params) => {
3655
+ for (const event of this.mapAcpUpdate(params, state, sessionId)) onEvent?.(event);
3656
+ }
3657
+ });
3658
+ break;
3659
+ } catch (err) {
3660
+ const detail = this.formatBridgeError(err);
3661
+ if (detail.includes("timed out")) this.interruptStuckTurn();
3662
+ if (attempt === 0 && detail.includes("timed out")) continue;
3663
+ if (attempt < 2 && await this.acknowledgeLostTurn(detail)) continue;
3664
+ throw err;
3665
+ }
3584
3666
  const recorded = this.takeTurnFailure(sessionId);
3585
3667
  if (recorded) throw recorded;
3586
- const finalStatus = mapAcpStopReason(promptResult.stopReason);
3587
- onEvent?.(finalStatus.detail ? {
3588
- type: "status",
3589
- status: finalStatus.status,
3590
- detail: finalStatus.detail
3591
- } : {
3668
+ recordCliTokenUsage(state, promptResult);
3669
+ const stop = promptResult?.stopReason;
3670
+ 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`);
3671
+ onEvent?.({
3592
3672
  type: "status",
3593
- status: finalStatus.status
3673
+ status: "completed"
3594
3674
  });
3595
3675
  await this.getSessionStore().update(sessionId, { providerSessionId: acpSessionId });
3596
3676
  return await this.completeTurn({
@@ -3603,7 +3683,25 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3603
3683
  endTime: Date.now()
3604
3684
  });
3605
3685
  } catch (err) {
3606
- return this.failAcpTurn(err, sessionId, turn, bridge, onEvent);
3686
+ const finalError = this.takeTurnFailure(sessionId) ?? err;
3687
+ const detail = this.formatBridgeError(finalError);
3688
+ this.logger.error(`${this.provider} execute failed`, {
3689
+ ...this.turnLogContext(turn),
3690
+ error: detail,
3691
+ stderrTail: bridge.getStderrTail().slice(-500)
3692
+ });
3693
+ onEvent?.({
3694
+ type: "status",
3695
+ status: "failed",
3696
+ detail
3697
+ });
3698
+ this.evictAcpSession(sessionId, "execute-error");
3699
+ const error = finalError instanceof JsonRpcError && detail !== finalError.message ? new Error(detail, { cause: finalError }) : finalError instanceof Error ? finalError : new Error(detail);
3700
+ throw Object.assign(error, { reason: error.reason ?? turnEndReason({
3701
+ aborted: input.abortSignal?.aborted,
3702
+ timedOut: finalError instanceof RpcRequestTimeoutError,
3703
+ linkLost: finalError instanceof BridgeClosedError || HARNESS_LINK_LOST.test(detail)
3704
+ }) });
3607
3705
  } finally {
3608
3706
  this.scheduleIdleReap(sessionId, config.bridgeIdleMs);
3609
3707
  }
@@ -3611,23 +3709,9 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3611
3709
  endTurn();
3612
3710
  }
3613
3711
  }
3614
- /** Report a failed turn, retire its process, and throw its error. */
3615
- failAcpTurn(err, sessionId, turn, bridge, onEvent) {
3616
- const finalError = this.takeTurnFailure(sessionId) ?? err;
3617
- const detail = this.formatBridgeError(finalError);
3618
- this.logger.error(`${this.provider} execute failed`, {
3619
- ...this.turnLogContext(turn),
3620
- error: detail,
3621
- stderrTail: bridge.getStderrTail().slice(-500)
3622
- });
3623
- onEvent?.({
3624
- type: "status",
3625
- status: "failed",
3626
- detail
3627
- });
3628
- this.evictAcpSession(sessionId, "execute-error");
3629
- if (finalError instanceof JsonRpcError && detail !== finalError.message) throw new Error(detail, { cause: finalError });
3630
- throw finalError instanceof Error ? finalError : new Error(detail);
3712
+ interruptStuckTurn() {}
3713
+ async acknowledgeLostTurn(_detail) {
3714
+ return false;
3631
3715
  }
3632
3716
  async acquireAcpSession(opts) {
3633
3717
  const { sessionId, turn, workspaceRoot, onEvent } = opts;
@@ -3640,6 +3724,7 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3640
3724
  this.logger.warn(`${this.provider}: cached session was started for another process configuration — respawning`, { sessionId });
3641
3725
  this.evictAcpSession(sessionId, "process-change");
3642
3726
  }
3727
+ const storedSessionId = opts.providerSessionId ? this.resumeSessionId() : void 0;
3643
3728
  const spec = await this.processSpec(turn, {
3644
3729
  sessionId,
3645
3730
  workspaceRoot
@@ -3684,9 +3769,11 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3684
3769
  this.checkProtocolVersion(init, requested, turn);
3685
3770
  acpSessionId = await this.openNativeSession(bridge, {
3686
3771
  providerSessionId: opts.providerSessionId,
3772
+ storedSessionId,
3687
3773
  cwd: workspaceRoot,
3688
3774
  timeoutMs
3689
3775
  });
3776
+ await this.getSessionStore().update(sessionId, { providerSessionId: acpSessionId });
3690
3777
  await this.onSessionOpened(bridge, acpSessionId, turn, { timeoutMs });
3691
3778
  this.logger.observePhase?.("cli_initialize", performance.now() - initializeStarted);
3692
3779
  } catch (err) {
@@ -3721,31 +3808,35 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3721
3808
  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.`);
3722
3809
  this.checkAgentProtocolVersion(advertised, turn);
3723
3810
  }
3724
- /**
3725
- * Open the native ACP session. A recorded session is loaded when the agent
3726
- * can; an agent that refuses the load starts a new native session, and the
3727
- * Tangle transcript stays the record of the earlier turns.
3728
- */
3729
3811
  async openNativeSession(bridge, opts) {
3730
- const params = {
3731
- cwd: opts.cwd,
3732
- mcpServers: []
3812
+ const load = async (sessionId) => {
3813
+ try {
3814
+ const loaded = await bridge.request("session/load", {
3815
+ sessionId,
3816
+ cwd: opts.cwd,
3817
+ mcpServers: []
3818
+ }, { timeoutMs: opts.timeoutMs });
3819
+ if (!loaded || typeof loaded !== "object") return void 0;
3820
+ return loaded.sessionId ?? sessionId;
3821
+ } catch (err) {
3822
+ this.logger.debug("session/load failed", { error: err instanceof Error ? err.message : String(err) });
3823
+ return;
3824
+ }
3733
3825
  };
3734
- if (opts.providerSessionId) try {
3735
- return (await bridge.request("session/load", {
3736
- sessionId: opts.providerSessionId,
3737
- ...params
3738
- }, { timeoutMs: opts.timeoutMs }))?.sessionId ?? opts.providerSessionId;
3739
- } catch (err) {
3740
- this.logger.debug("session/load failed, creating new", { error: err instanceof Error ? err.message : String(err) });
3826
+ const stored = opts.storedSessionId;
3827
+ if (stored) {
3828
+ const loaded = await load(stored);
3829
+ if (loaded) return loaded;
3741
3830
  }
3742
- return (await bridge.request("session/new", params, { timeoutMs: opts.timeoutMs })).sessionId;
3831
+ if (opts.providerSessionId && opts.providerSessionId !== stored) {
3832
+ const loaded = await load(opts.providerSessionId);
3833
+ if (loaded) return loaded;
3834
+ }
3835
+ return (await bridge.request("session/new", {
3836
+ cwd: opts.cwd,
3837
+ mcpServers: []
3838
+ }, { timeoutMs: opts.timeoutMs })).sessionId;
3743
3839
  }
3744
- /**
3745
- * One `session/prompt` turn. Abort sends `session/cancel` before closing so
3746
- * the agent can settle the turn; the bridge's own signal handling would
3747
- * close first and lose the notify.
3748
- */
3749
3840
  async promptAcpSession(entry, turn) {
3750
3841
  const { bridge, acpSessionId } = entry;
3751
3842
  const unsubscribe = bridge.onNotification("session/update", (params) => {
@@ -3805,7 +3896,6 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3805
3896
  entry.idleTimer = void 0;
3806
3897
  }
3807
3898
  }
3808
- /** Close a session's agent process and drop it. Idempotent. */
3809
3899
  evictAcpSession(sessionId, reason) {
3810
3900
  const entry = this.acpSessions.get(sessionId);
3811
3901
  if (!entry) return;
@@ -3823,11 +3913,9 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3823
3913
  });
3824
3914
  });
3825
3915
  }
3826
- /** Evict every cached session whose process key matches. */
3827
3916
  evictAcpSessionsWhere(matches, reason) {
3828
3917
  for (const [sessionId, entry] of [...this.acpSessions]) if (matches(entry.processKey)) this.evictAcpSession(sessionId, reason);
3829
3918
  }
3830
- /** Translate one `session/update` notification into stream events. */
3831
3919
  mapAcpUpdate(params, state, sessionId) {
3832
3920
  const update = params.update;
3833
3921
  if (!update || typeof update !== "object") return [];
@@ -3879,12 +3967,6 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3879
3967
  if (raw instanceof Error) return raw.message;
3880
3968
  return String(raw);
3881
3969
  }
3882
- /**
3883
- * The error a failed process start surfaces. An agent that exits before its
3884
- * session opens reports why only on stderr, and an agent's JSON-RPC error
3885
- * carries its cause in `data`; a bare "process-exit" or "Internal error"
3886
- * hides both from the caller.
3887
- */
3888
3970
  sessionSetupError(err, bridge) {
3889
3971
  if (err instanceof BridgeClosedError) {
3890
3972
  const stderr = bridge.getStderrTail().trim().slice(-500);
@@ -3910,7 +3992,7 @@ var AcpProviderAdapterBase = class extends CliProviderAdapterBase {
3910
3992
  throw new Error(`${this.provider}: buildExecArgs is not used — ACP turns run over the RPC bridge`);
3911
3993
  }
3912
3994
  buildExecEnv(_config) {
3913
- return { ...process.env };
3995
+ return {};
3914
3996
  }
3915
3997
  createEventParser() {
3916
3998
  throw new Error(`${this.provider}: createEventParser is not used — session/update notifications arrive over the RPC bridge`);
@@ -3959,7 +4041,7 @@ function jsonEventParser(logger, onEvent, spec) {
3959
4041
  }
3960
4042
  /**
3961
4043
  * Parser for harnesses that print their reply as plain stdout lines (hermes,
3962
- * nanoclaw, forge). Lines classify into provider events; the reply is
4044
+ * nanoclaw). Lines classify into provider events; the reply is
3963
4045
  * emitted as one text part when stdout closes. A turn that printed errors
3964
4046
  * but no reply ends `failed` with the last error instead of an empty
3965
4047
  * `completed`.
@@ -4078,22 +4160,6 @@ var TerminalPlanSupport = class {
4078
4160
  };
4079
4161
  //#endregion
4080
4162
  //#region src/interaction-support.ts
4081
- /**
4082
- * Per-execution lifecycle wrapper for an MCP-capable harness. Permission and
4083
- * question tools use the live {@link InteractionBroker}; plan tools commit
4084
- * through {@link TerminalPlanSupport} and end the provider turn.
4085
- *
4086
- * Mirrors the claude-code permission-server pattern, but for the GENERIC
4087
- * `request_permission` / `ask_user` / `request_plan` tools the agent calls
4088
- * itself — there is no harness-specific reply contract, so any MCP-capable CLI
4089
- * (factory-droids, …) composes this with ~10 lines: declare the capability
4090
- * kinds, start/stop around the spawn, inject the server via the CLI's MCP
4091
- * mechanism, and forward `respondToInteraction` here.
4092
- *
4093
- * Unlike claude-code's `--permission-prompt-tool`, these generic tools fire
4094
- * only when the model chooses to call them. Permission is therefore advisory,
4095
- * while plan-capable adapters separately prove the terminal turn boundary.
4096
- */
4097
4163
  var McpInteractionSupport = class {
4098
4164
  /** The shared emit → block → respond → fail-closed round-trip. */
4099
4165
  broker = new InteractionBroker$1();
@@ -4184,182 +4250,129 @@ var McpInteractionSupport = class {
4184
4250
  }
4185
4251
  };
4186
4252
  //#endregion
4187
- //#region src/metering.ts
4188
- const UsageRecordSchema = z.record(z.string(), z.unknown());
4189
- const UsageNumberSchema = z.union([z.number(), z.string().trim().min(1)]).transform(Number).pipe(z.number().finite().nonnegative());
4190
- const TOKEN_USAGE_INPUT_KEYS = Object.freeze([
4191
- "inputTokens",
4192
- "input",
4193
- "input_tokens",
4194
- "promptTokens",
4195
- "prompt_tokens"
4196
- ]);
4197
- const TOKEN_USAGE_OUTPUT_KEYS = Object.freeze([
4198
- "outputTokens",
4199
- "output",
4200
- "output_tokens",
4201
- "completionTokens",
4202
- "completion_tokens"
4203
- ]);
4204
- const TOKEN_USAGE_TOTAL_KEYS = Object.freeze([
4205
- "totalTokens",
4206
- "total_tokens",
4207
- "total"
4208
- ]);
4209
- const TOKEN_USAGE_REASONING_KEYS = Object.freeze([
4210
- "reasoningTokens",
4211
- "reasoning_tokens",
4212
- "reasoning",
4213
- "reasoning_output_tokens"
4214
- ]);
4215
- const TOKEN_USAGE_COST_KEYS = Object.freeze([
4216
- "totalCostUsd",
4217
- "costUsd",
4218
- "total_cost_usd",
4219
- "cost_usd",
4220
- "cost"
4221
- ]);
4222
- const CACHE_READ_INPUT_KEYS = Object.freeze([
4223
- "cacheReadInputTokens",
4224
- "cache_read_input_tokens",
4225
- "cacheRead",
4226
- "cache_read",
4227
- "cached_input_tokens"
4228
- ]);
4229
- const CACHE_CREATION_INPUT_KEYS = Object.freeze([
4230
- "cacheCreationInputTokens",
4231
- "cache_creation_input_tokens",
4232
- "cacheWrite",
4233
- "cache_write"
4234
- ]);
4235
- const NESTED_COST_KEYS = Object.freeze([
4236
- "total",
4237
- "totalUsd",
4238
- "total_usd",
4239
- "usd"
4240
- ]);
4241
- function asRecord(raw) {
4242
- const parsed = UsageRecordSchema.safeParse(raw);
4243
- return parsed.success ? parsed.data : void 0;
4244
- }
4245
- function tokenCount(raw) {
4246
- const parsed = UsageNumberSchema.safeParse(raw);
4247
- return parsed.success ? Math.trunc(parsed.data) : void 0;
4248
- }
4249
- function firstTokenCount(source, keys) {
4250
- for (const key of keys) {
4251
- const count = tokenCount(source[key]);
4252
- if (count !== void 0) return count;
4253
- }
4254
- }
4255
- function firstUsageCostUsd(source, keys = TOKEN_USAGE_COST_KEYS) {
4256
- for (const key of keys) {
4257
- const parsed = UsageNumberSchema.safeParse(source[key]);
4258
- if (parsed.success) return parsed.data;
4259
- }
4260
- }
4261
- function usageRecord(source) {
4262
- return asRecord(source.tokenUsage) ?? asRecord(source.usage) ?? source;
4263
- }
4264
- function readTokenUsage(source) {
4265
- const usageSource = usageRecord(source);
4266
- const inputTokens = firstTokenCount(usageSource, TOKEN_USAGE_INPUT_KEYS);
4267
- const outputTokens = firstTokenCount(usageSource, TOKEN_USAGE_OUTPUT_KEYS);
4268
- return inputTokens !== void 0 || outputTokens !== void 0 ? {
4269
- inputTokens,
4270
- outputTokens
4271
- } : void 0;
4272
- }
4273
- function readTokenCostUsd(source) {
4274
- const usageSource = usageRecord(source);
4275
- return firstUsageCostUsd(source) ?? firstUsageCostUsd(usageSource) ?? readNestedCostUsd(source) ?? readNestedCostUsd(usageSource);
4276
- }
4277
- function readNestedCostUsd(source) {
4278
- const cost = asRecord(source.cost);
4279
- return cost ? firstUsageCostUsd(cost, NESTED_COST_KEYS) : void 0;
4280
- }
4281
- function readCliTokenUsage(raw) {
4282
- const parsed = UsageRecordSchema.safeParse(raw);
4283
- if (!parsed.success) return void 0;
4284
- const source = parsed.data;
4285
- const usageSource = usageRecord(source);
4286
- const usage = readTokenUsage(source) ?? readTokenUsage(usageSource);
4287
- const cost = readTokenCostUsd(source);
4288
- const totalTokens = firstTokenCount(usageSource, TOKEN_USAGE_TOTAL_KEYS);
4289
- const reasoningTokens = firstTokenCount(usageSource, TOKEN_USAGE_REASONING_KEYS);
4290
- const cacheReadInputTokens = firstTokenCount(usageSource, CACHE_READ_INPUT_KEYS);
4291
- const cacheCreationInputTokens = firstTokenCount(usageSource, CACHE_CREATION_INPUT_KEYS);
4292
- if (!usage && totalTokens === void 0 && reasoningTokens === void 0 && cost === void 0 && cacheReadInputTokens === void 0 && cacheCreationInputTokens === void 0) return;
4293
- return {
4294
- inputTokens: usage?.inputTokens,
4295
- outputTokens: usage?.outputTokens,
4296
- totalTokens,
4297
- reasoningTokens,
4298
- cacheReadInputTokens,
4299
- cacheCreationInputTokens,
4300
- cost
4301
- };
4253
+ //#region src/secret-native-proof.ts
4254
+ const proofKey = Symbol.for("tangle.secret-native-binary-proofs");
4255
+ const runtime$1 = globalThis;
4256
+ const proven = runtime$1[proofKey] ?? /* @__PURE__ */ new Set();
4257
+ runtime$1[proofKey] = proven;
4258
+ /** Prove an immutable executable, under the launch's own identity and environment. */
4259
+ async function provenStoreBinary(expected, config) {
4260
+ if (!config.isolatedIdentity || config.inheritEnv !== false) refuseSecretLaunch("Secret native binary proof requires an isolated identity and environment", { path: config.command });
4261
+ const command = resolveExecutableRealpath(config.command, config.env?.PATH);
4262
+ if (!command || !isTrustedStoreBinary(command)) refuseSecretLaunch("Secret native launch requires a root-owned Nix store binary", { path: command ?? config.command });
4263
+ const key = `${command}\0${expected}`;
4264
+ if (proven.has(key)) return command;
4265
+ const probe = await execCli({
4266
+ command,
4267
+ cwd: config.cwd,
4268
+ env: config.env,
4269
+ inheritEnv: config.inheritEnv,
4270
+ signal: config.signal,
4271
+ isolatedIdentity: config.isolatedIdentity,
4272
+ args: ["--version"],
4273
+ spawnOptions: config.isolatedIdentity,
4274
+ timeoutMs: 15e3
4275
+ });
4276
+ const version = probe.stdout?.trim();
4277
+ if (!probe.exit.success || version !== expected) refuseSecretLaunch(`Secret native launch expected ${expected} at ${command}, received no matching version`, { exitCode: probe.exit.code });
4278
+ proven.add(key);
4279
+ return command;
4302
4280
  }
4303
- function recordCliTokenUsage(state, ...sources) {
4304
- for (const source of sources) {
4305
- const tokenUsage = readCliTokenUsage(source);
4306
- if (tokenUsage) {
4307
- state.tokenUsage = tokenUsage;
4308
- return true;
4281
+ /** A reported version alone cannot establish an immutable executable. */
4282
+ function isTrustedStoreBinary(realpath) {
4283
+ if (!realpath.startsWith("/nix/store/")) return false;
4284
+ try {
4285
+ for (const path of [realpath, dirname(realpath)]) {
4286
+ const stat = fs$1.lstatSync(path);
4287
+ if (stat.uid !== 0 || (stat.mode & 18) !== 0) return false;
4309
4288
  }
4289
+ return true;
4290
+ } catch {
4291
+ return false;
4310
4292
  }
4311
- return false;
4312
4293
  }
4313
- function addUsage(left, right) {
4314
- if (left === void 0) return right;
4315
- if (right === void 0) return left;
4316
- const sum = left + right;
4317
- return Number.isFinite(sum) ? sum : Math.max(left, right);
4294
+ /** The real file a launch of `command` executes under `pathEnv`. */
4295
+ function resolveExecutableRealpath(command, pathEnv) {
4296
+ if (command.includes("/") && !isAbsolute(command)) return void 0;
4297
+ const candidates = command.includes("/") ? [command] : (pathEnv ?? "").split(":").filter(isAbsolute).map((dir) => join(dir, command));
4298
+ for (const candidate of candidates) try {
4299
+ fs$1.accessSync(candidate, fs$1.constants.X_OK);
4300
+ if (fs$1.statSync(candidate).isFile()) return fs$1.realpathSync(candidate);
4301
+ } catch {}
4318
4302
  }
4319
- /**
4320
- * Adds one usage report to the running total instead of replacing it.
4321
- *
4322
- * For a CLI that reports usage PER TURN (pi) rather than once per run,
4323
- * `recordCliTokenUsage` would leave the execution billed as its final turn
4324
- * alone. Use this wherever a single execution can produce more than one usage
4325
- * report; use `recordCliTokenUsage` when the CLI emits one cumulative total.
4326
- *
4327
- * Returns true when a usage report was found in `sources` and folded in.
4328
- */
4329
- function accumulateCliTokenUsage(state, ...sources) {
4330
- for (const source of sources) {
4331
- const tokenUsage = readCliTokenUsage(source);
4332
- if (!tokenUsage) continue;
4333
- const total = state.tokenUsage;
4334
- state.tokenUsage = total ? {
4335
- inputTokens: addUsage(total.inputTokens, tokenUsage.inputTokens),
4336
- outputTokens: addUsage(total.outputTokens, tokenUsage.outputTokens),
4337
- totalTokens: addUsage(total.totalTokens, tokenUsage.totalTokens),
4338
- reasoningTokens: addUsage(total.reasoningTokens, tokenUsage.reasoningTokens),
4339
- cacheReadInputTokens: addUsage(total.cacheReadInputTokens, tokenUsage.cacheReadInputTokens),
4340
- cacheCreationInputTokens: addUsage(total.cacheCreationInputTokens, tokenUsage.cacheCreationInputTokens),
4341
- cost: addUsage(total.cost, tokenUsage.cost)
4342
- } : tokenUsage;
4343
- return true;
4303
+ const SEALED_NATIVE_HOME_LAYOUT = {
4304
+ codex: [
4305
+ [
4306
+ ".",
4307
+ 1016,
4308
+ true
4309
+ ],
4310
+ [
4311
+ ".codex",
4312
+ 1016,
4313
+ true
4314
+ ],
4315
+ [
4316
+ ".agents",
4317
+ 360,
4318
+ true
4319
+ ],
4320
+ [
4321
+ ".codex/skills",
4322
+ 360,
4323
+ true
4324
+ ],
4325
+ [
4326
+ ".codex/rules",
4327
+ 360,
4328
+ true
4329
+ ],
4330
+ [
4331
+ ".codex/config.toml",
4332
+ 288,
4333
+ false
4334
+ ],
4335
+ [
4336
+ ".codex/AGENTS.md",
4337
+ 288,
4338
+ false
4339
+ ],
4340
+ [
4341
+ ".codex/AGENTS.override.md",
4342
+ 288,
4343
+ false
4344
+ ],
4345
+ [
4346
+ ".codex/hooks.json",
4347
+ 288,
4348
+ false
4349
+ ]
4350
+ ],
4351
+ opencode: [[
4352
+ ".",
4353
+ 456,
4354
+ true
4355
+ ], [
4356
+ ".opencode",
4357
+ 360,
4358
+ true
4359
+ ]]
4360
+ };
4361
+ function assertSealedNativeHome(home, gid, layout) {
4362
+ for (const [relative, mode, directory] of layout) {
4363
+ const path = join(home, relative);
4364
+ const stat = fs$1.lstatSync(path);
4365
+ 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()}`);
4344
4366
  }
4345
- return false;
4346
4367
  }
4347
- function parseCliUsageLine(line) {
4348
- if (!line.startsWith("usage:")) return void 0;
4349
- const raw = line.slice(6).trim();
4350
- if (!raw) return void 0;
4351
- if (raw.startsWith("{")) return asRecord(JSON.parse(raw));
4352
- const fields = {};
4353
- for (const part of raw.split(/[,\s]+/)) {
4354
- if (!part) continue;
4355
- const separator = part.includes("=") ? "=" : part.includes(":") ? ":" : "";
4356
- if (!separator) continue;
4357
- const [key, value] = part.split(separator, 2);
4358
- if (!key || value === void 0) continue;
4359
- const numeric = Number(value);
4360
- fields[key] = Number.isFinite(numeric) ? numeric : value;
4361
- }
4362
- return Object.keys(fields).length > 0 ? fields : void 0;
4368
+ /** Only selected structural facts enter a refusal; payloads and causes never do. */
4369
+ function refuseSecretLaunch(reason, { path, expected, actual, exitCode } = {}) {
4370
+ const detail = JSON.stringify({
4371
+ path,
4372
+ expected,
4373
+ actual
4374
+ }).slice(0, 768);
4375
+ throw new Error(detail === "{}" ? reason : `${reason} ${detail}`, { cause: typeof exitCode === "number" || exitCode === null ? { exitCode } : void 0 });
4363
4376
  }
4364
4377
  //#endregion
4365
4378
  //#region src/reasoning-effort.ts
@@ -4389,6 +4402,160 @@ function kimiThinkingArgs(level, modelId) {
4389
4402
  return off ? ["--no-thinking"] : ["--thinking"];
4390
4403
  }
4391
4404
  //#endregion
4392
- export { ACP_MIN_PROTOCOL_VERSION, ACP_PROTOCOL_VERSION, AcpProviderAdapterBase, CliProviderAdapterBase, DEFAULT_INTERACTION_MCP_SERVER_NAME, EXECUTION_TRACE_HEADER, InteractionBroker, InteractionHttpBridge, InteractionMcpServer, InvalidAttachmentError, MANAGED_QUOTE_VERSION_HEADER, MAX_INLINE_IMAGE_BYTES, MAX_TOTAL_INLINE_IMAGE_BYTES, McpInteractionSupport, PERMISSION_DECISION_TIMEOUT_MS, PROMPT_SECTION_SEPARATOR, SessionRehydrationError, TerminalPlanSupport, accumulateCliTokenUsage, acpContentText, brokerInteractionTools, buildFileUrl, createExecutionState, createRepeatedToolFailureGuard, deriveFilePart, deterministicFilePartId, enabledInteractionKinds, executionCorrelationHeaders, finalTextEvent, homeXdgEnv, inferMediaType, interactionMcpToolName, interactionToggleEnabled, isLocalMcpConfig, jsonEventParser, kimiThinkingArgs, localMcpArgv, localMcpEnv, mapAcpStopReason, nativeEffortArgs, parseCliUsageLine, partitionImageAttachments, plaintextResponseParser, preparePromptInputParts, prependPromptSection, readCliTokenUsage, readEphemeralContext, reasoningEvents, recordCliTokenUsage, rejectNotLaunched, runtimeSecretsOverlay, sessionObservedEvents, spawnAcpBridge, streamPartBase, textDeltaEvents, textSnapshotEvents, toolResultEvents, toolStartEvents, trackFilePart, trackReasoningBlock, turnInteractionEnabled, wasNeverLaunched, withEphemeralContext, withTurnInteractions };
4405
+ //#region src/secret-profile-mcp.ts
4406
+ /** Executor-held MCP credentials shared by native private-profile adapters. */
4407
+ const SECRET_REFERENCE_PREFIX = "TANGLE_PROFILE_SECRET_";
4408
+ const SECRET_REFERENCE = /\{env:(TANGLE_PROFILE_SECRET_[0-9A-F]{24})\}/g;
4409
+ function requireRecord(raw, message) {
4410
+ return isRecord(raw) ? raw : refuseSecretLaunch(message);
4411
+ }
4412
+ /** A config value with its secret references resolved, or undefined. */
4413
+ function resolveReferences(value, references) {
4414
+ let unresolved = false;
4415
+ const resolved = value.replace(SECRET_REFERENCE, (_match, reference) => {
4416
+ const secret = references[reference];
4417
+ if (secret === void 0) unresolved = true;
4418
+ return secret ?? "";
4419
+ });
4420
+ return unresolved || /\{(?:env|file):/.test(resolved) ? void 0 : resolved;
4421
+ }
4422
+ /** A stdio server the executor can run: its command and its environment with every secret reference resolved. */
4423
+ function servableStdioServer(name, entry, references) {
4424
+ const command = entry.command;
4425
+ 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";
4426
+ if (entry.cwd !== void 0) return "it names a working directory";
4427
+ const declared = entry.environment === void 0 ? {} : entry.environment;
4428
+ if (!isRecord(declared)) return "its environment is not a map";
4429
+ const environment = {};
4430
+ const secrets = /* @__PURE__ */ new Set();
4431
+ for (const [variable, value] of Object.entries(declared)) {
4432
+ if (typeof value !== "string") return `its ${variable} is not text`;
4433
+ const resolved = resolveReferences(value, references);
4434
+ if (resolved === void 0) return `its ${variable} names a value the executor cannot resolve`;
4435
+ environment[variable] = resolved;
4436
+ for (const [, reference] of value.matchAll(SECRET_REFERENCE)) {
4437
+ const secret = references[reference];
4438
+ if (secret) secrets.add(secret);
4439
+ }
4440
+ }
4441
+ return {
4442
+ name,
4443
+ command,
4444
+ environment,
4445
+ secrets: [...secrets]
4446
+ };
4447
+ }
4448
+ /** A remote server the executor can serve: its fixed upstream URL and every header with its secret references resolved. */
4449
+ function servableMcpServer(name, entry, references) {
4450
+ const url = entry.url;
4451
+ if (typeof url !== "string" || /\{(?:env|file):/.test(url)) return "its URL is not a fixed address";
4452
+ try {
4453
+ const parsed = new URL(url);
4454
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return "its URL is not http(s)";
4455
+ } catch {
4456
+ return "its URL is not a fixed address";
4457
+ }
4458
+ const headers = {};
4459
+ const declared = entry.headers === void 0 ? {} : entry.headers;
4460
+ if (!isRecord(declared)) return "its headers are not a map";
4461
+ for (const [header, value] of Object.entries(declared)) {
4462
+ if (typeof value !== "string") return `its ${header} header is not text`;
4463
+ const resolved = resolveReferences(value, references);
4464
+ if (resolved === void 0) return `its ${header} header names a value the executor cannot resolve`;
4465
+ headers[header] = resolved;
4466
+ }
4467
+ return {
4468
+ name,
4469
+ url,
4470
+ headers
4471
+ };
4472
+ }
4473
+ /** Join the queue at `key`; `previous` settles when earlier holders release. */
4474
+ function joinQueue(queues, key) {
4475
+ const previous = queues.get(key) ?? Promise.resolve();
4476
+ let release;
4477
+ const finished = new Promise((resolve) => {
4478
+ release = resolve;
4479
+ });
4480
+ const tail = previous.then(() => finished);
4481
+ queues.set(key, tail);
4482
+ tail.then(() => {
4483
+ if (queues.get(key) === tail) queues.delete(key);
4484
+ });
4485
+ return {
4486
+ previous,
4487
+ release
4488
+ };
4489
+ }
4490
+ function untilSettledOrAborted(previous, signal) {
4491
+ const aborted = () => signal.reason ?? /* @__PURE__ */ new Error("Secret native turn aborted before its session view was free");
4492
+ if (signal.aborted) return Promise.reject(aborted());
4493
+ return new Promise((resolve, reject) => {
4494
+ const onAbort = () => reject(aborted());
4495
+ signal.addEventListener("abort", onAbort, { once: true });
4496
+ previous.then(() => {
4497
+ signal.removeEventListener("abort", onAbort);
4498
+ resolve();
4499
+ });
4500
+ });
4501
+ }
4502
+ /** Every native discovery ancestor is sealed and free of ambient configuration. */
4503
+ function assertSealedNativeSearchPath(cwd, configNames) {
4504
+ for (let child = cwd, parent = dirname(cwd);; child = parent, parent = dirname(parent)) {
4505
+ const stat = lstatSync(child);
4506
+ 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");
4507
+ if (parent === child) break;
4508
+ }
4509
+ }
4510
+ /** One session view changes only after its previous physical native process exits. */
4511
+ const turnsKey = Symbol.for("tangle.secret-native-session-turns");
4512
+ const turnsRuntime = globalThis;
4513
+ const sessionTurns = turnsRuntime[turnsKey] ?? /* @__PURE__ */ new Map();
4514
+ turnsRuntime[turnsKey] = sessionTurns;
4515
+ function secretSessionTurnQueue(view) {
4516
+ return joinQueue(sessionTurns, view);
4517
+ }
4518
+ /** An HTTP URL on loopback with an explicit port and no URL credentials. */
4519
+ function isLoopbackEndpoint(value) {
4520
+ if (typeof value !== "string") return false;
4521
+ try {
4522
+ const url = new URL(value);
4523
+ return url.protocol === "http:" && [
4524
+ "127.0.0.1",
4525
+ "[::1]",
4526
+ "localhost"
4527
+ ].includes(url.hostname) && url.port !== "" && !url.username && !url.password;
4528
+ } catch {
4529
+ return false;
4530
+ }
4531
+ }
4532
+ //#endregion
4533
+ //#region src/secret-profile-scope.ts
4534
+ const key = Symbol.for("tangle.secret-profile-mcp.scopes");
4535
+ const runtime = globalThis;
4536
+ const openScopes = runtime[key] ??= /* @__PURE__ */ new Map();
4537
+ function secretMcpScope(sessionId, uid, owner) {
4538
+ if (sessionId.includes("\0")) throw new Error("A secret MCP session id has no NUL");
4539
+ const scope = `${sessionId}\0${uid ?? ""}`;
4540
+ if (owner?.proxy) {
4541
+ const owners = openScopes.get(scope) ?? /* @__PURE__ */ new Set();
4542
+ owners.add(owner);
4543
+ openScopes.set(scope, owners);
4544
+ }
4545
+ return scope;
4546
+ }
4547
+ async function forgetSessionScopes(target) {
4548
+ for (const [scope, owners] of openScopes) {
4549
+ if ("sessionId" in target && !scope.startsWith(`${target.sessionId}\0`)) continue;
4550
+ for (const owner of [...owners]) {
4551
+ if ("owner" in target && owner !== target.owner) continue;
4552
+ if (![...owners].some((other) => other !== owner && other.proxy === owner.proxy)) await owner.proxy.forget(scope);
4553
+ owners.delete(owner);
4554
+ }
4555
+ if (owners.size === 0) openScopes.delete(scope);
4556
+ }
4557
+ }
4558
+ //#endregion
4559
+ export { ACP_MIN_PROTOCOL_VERSION, ACP_PROTOCOL_VERSION, AcpProviderAdapterBase, CODEX_DISABLED_FEATURE_ARGS, CliProviderAdapterBase, DEFAULT_INTERACTION_MCP_SERVER_NAME, EXECUTION_TRACE_HEADER, InteractionBroker, InteractionHttpBridge, InteractionMcpServer, InvalidAttachmentError, MANAGED_QUOTE_VERSION_HEADER, MAX_INLINE_IMAGE_BYTES, MAX_TOTAL_INLINE_IMAGE_BYTES, McpInteractionSupport, NATIVE_SESSION_CONTRACTS, PERMISSION_DECISION_TIMEOUT_MS, PROMPT_SECTION_SEPARATOR, SEALED_NATIVE_HOME_LAYOUT, SECRET_REFERENCE, SECRET_REFERENCE_PREFIX, SessionRehydrationError, TerminalPlanSupport, accumulateCliTokenUsage, acpContentText, assertSealedNativeHome, assertSealedNativeSearchPath, brokerInteractionTools, buildFileUrl, codexDeveloperInstructionArgs, createExecutionState, createRepeatedToolFailureGuard, deriveFilePart, deterministicFilePartId, enabledInteractionKinds, executionCorrelationHeaders, finalTextEvent, forgetSessionScopes, homeXdgEnv, inferMediaType, interactionMcpToolName, interactionToggleEnabled, isLocalMcpConfig, isLoopbackEndpoint, isPlainRecord, isTrustedStoreBinary, joinQueue, jsonEventParser, kimiThinkingArgs, localMcpArgv, localMcpEnv, nativeEffortArgs, parseCliUsageLine, partitionImageAttachments, plaintextResponseParser, preparePromptInputParts, prependPromptSection, provenStoreBinary, readCliTokenUsage, readEphemeralContext, reasoningEvents, recordCliTokenUsage, refuseSecretLaunch, rejectNotLaunched, requireRecord, resolveExecutableRealpath, resolveReferences, runtimeSecretsOverlay, secretMcpScope, secretSessionTurnQueue, servableMcpServer, servableStdioServer, sessionObservedEvents, spawnAcpBridge, streamPartBase, textDeltaEvents, textSnapshotEvents, toolResultEvents, toolStartEvents, trackFilePart, trackReasoningBlock, turnEndReason, turnInteractionEnabled, untilSettledOrAborted, wasNeverLaunched, withEphemeralContext, withTurnInteractions };
4393
4560
 
4394
4561
  //# sourceMappingURL=index.mjs.map