@cabane/companion 0.6.36 → 0.6.38

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/cli.js +339 -120
  2. package/dist/runtime.js +339 -120
  3. package/package.json +1 -1
package/dist/runtime.js CHANGED
@@ -2506,13 +2506,55 @@ var turnEventSchema = z5.discriminatedUnion("type", [
2506
2506
  })
2507
2507
  ]);
2508
2508
 
2509
- // packages/agent-runtime/src/failure.ts
2509
+ // packages/agent-runtime/src/turn-diagnostics.ts
2510
2510
  import { z as z6 } from "zod";
2511
- var turnFailureSchema = z6.discriminatedUnion("kind", [
2511
+ var turnResultReasonSchema = z6.discriminatedUnion("kind", [
2512
2512
  z6.object({ kind: z6.literal("usage_capped"), resetsAt: z6.string().optional() }),
2513
2513
  z6.object({ kind: z6.literal("rate_limited") }),
2514
2514
  z6.object({ kind: z6.literal("server_error") }),
2515
- z6.object({ kind: z6.literal("auth_expired") })
2515
+ z6.object({ kind: z6.literal("auth_expired") }),
2516
+ z6.object({ kind: z6.literal("no_result") }),
2517
+ z6.object({ kind: z6.literal("empty_result") }),
2518
+ z6.object({ kind: z6.literal("empty_result_unverified") }),
2519
+ z6.object({ kind: z6.literal("timeout_idle") }),
2520
+ z6.object({ kind: z6.literal("timeout_total") }),
2521
+ z6.object({ kind: z6.literal("cancelled") }),
2522
+ z6.object({ kind: z6.literal("skipped") }),
2523
+ z6.object({ kind: z6.literal("runtime_error") })
2524
+ ]);
2525
+ var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
2526
+ var turnSessionModes = ["fresh", "resumed", "degraded"];
2527
+ var turnRuntimeResultKinds = ["success", "error", "no_terminal"];
2528
+ var turnFinalSources = [
2529
+ "runtime_text",
2530
+ "progress_promotion",
2531
+ "host_fallback",
2532
+ "marker",
2533
+ "none"
2534
+ ];
2535
+ var turnDiagnosticsSchema = z6.object({
2536
+ outcome: z6.enum(turnOutcomes),
2537
+ resultReason: turnResultReasonSchema.nullable(),
2538
+ sessionMode: z6.enum(turnSessionModes),
2539
+ sessionFingerprint: z6.string().regex(/^[a-f0-9]{16}$/).nullable(),
2540
+ eventCounts: z6.object({
2541
+ session: z6.number().int().nonnegative(),
2542
+ text: z6.number().int().nonnegative(),
2543
+ thinking: z6.number().int().nonnegative(),
2544
+ tool: z6.number().int().nonnegative(),
2545
+ result: z6.number().int().nonnegative()
2546
+ }),
2547
+ runtimeResultKind: z6.enum(turnRuntimeResultKinds).nullable(),
2548
+ finalSource: z6.enum(turnFinalSources)
2549
+ });
2550
+
2551
+ // packages/agent-runtime/src/failure.ts
2552
+ import { z as z7 } from "zod";
2553
+ var turnFailureSchema = z7.discriminatedUnion("kind", [
2554
+ z7.object({ kind: z7.literal("usage_capped"), resetsAt: z7.string().optional() }),
2555
+ z7.object({ kind: z7.literal("rate_limited") }),
2556
+ z7.object({ kind: z7.literal("server_error") }),
2557
+ z7.object({ kind: z7.literal("auth_expired") })
2516
2558
  ]);
2517
2559
  var USAGE_CAPPED = "usage_capped";
2518
2560
  var RATE_LIMITED = "rate_limited";
@@ -2617,64 +2659,111 @@ var NEGATED_CAP = /not (your|a) usage limit/g;
2617
2659
  var RATE_PATTERNS = [/rate[_ ]?limit/, /\b429\b/, /too many requests/];
2618
2660
  var BARE_LIMIT = /\blimit (reached|exceeded)\b/;
2619
2661
 
2662
+ // packages/agent-runtime/src/turn-diagnostics-normalize.ts
2663
+ function normalizeTurnResultReason(reason) {
2664
+ const classified = decodeFailureReason(reason);
2665
+ if (classified) return classified;
2666
+ switch (reason) {
2667
+ case "no_result":
2668
+ case "no_terminal":
2669
+ return { kind: "no_result" };
2670
+ case "empty_result":
2671
+ return { kind: "empty_result" };
2672
+ case "empty_result_unverified":
2673
+ return { kind: "empty_result_unverified" };
2674
+ case "timeout_idle":
2675
+ return { kind: "timeout_idle" };
2676
+ case "timeout_total":
2677
+ return { kind: "timeout_total" };
2678
+ case "cancelled":
2679
+ return { kind: "cancelled" };
2680
+ case "skipped":
2681
+ return { kind: "skipped" };
2682
+ default:
2683
+ return { kind: "runtime_error" };
2684
+ }
2685
+ }
2686
+
2687
+ // packages/agent-runtime/src/empty-result.ts
2688
+ function normalizeCommitText(text) {
2689
+ return text.trim();
2690
+ }
2691
+ function isContentBearingEvent(event) {
2692
+ switch (event.type) {
2693
+ case "text":
2694
+ return normalizeCommitText(event.body) !== "";
2695
+ case "thinking":
2696
+ return normalizeCommitText(event.text) !== "";
2697
+ case "tool":
2698
+ return true;
2699
+ case "session":
2700
+ case "result":
2701
+ return false;
2702
+ }
2703
+ }
2704
+ function classifyEmptyResult(input) {
2705
+ if (!input.ok || input.contentBearingEvents > 0) return null;
2706
+ return input.usage?.inputTokens === 0 && input.usage.outputTokens === 0 ? "empty_result" : "empty_result_unverified";
2707
+ }
2708
+
2620
2709
  // packages/agent-runtime/src/turn-request.ts
2621
- import { z as z7 } from "zod";
2622
- var contentBlockSchema = z7.discriminatedUnion("type", [
2623
- z7.object({ type: z7.literal("text"), text: z7.string() }),
2624
- z7.object({
2625
- type: z7.literal("image"),
2626
- source: z7.object({ type: z7.literal("url"), url: z7.string() })
2710
+ import { z as z8 } from "zod";
2711
+ var contentBlockSchema = z8.discriminatedUnion("type", [
2712
+ z8.object({ type: z8.literal("text"), text: z8.string() }),
2713
+ z8.object({
2714
+ type: z8.literal("image"),
2715
+ source: z8.object({ type: z8.literal("url"), url: z8.string() })
2627
2716
  }),
2628
- z7.object({
2629
- type: z7.literal("document"),
2630
- source: z7.object({ type: z7.literal("url"), url: z7.string() })
2717
+ z8.object({
2718
+ type: z8.literal("document"),
2719
+ source: z8.object({ type: z8.literal("url"), url: z8.string() })
2631
2720
  })
2632
2721
  ]);
2633
- var effortLevelSchema = z7.enum(["low", "medium", "high", "xhigh", "max"]);
2634
- var resolvedRunConfigSchema = z7.object({
2635
- model: z7.string().nullable(),
2722
+ var effortLevelSchema = z8.enum(["low", "medium", "high", "xhigh", "max"]);
2723
+ var resolvedRunConfigSchema = z8.object({
2724
+ model: z8.string().nullable(),
2636
2725
  effort: effortLevelSchema.optional(),
2637
- runtimeOptions: z7.record(z7.string(), z7.unknown()).optional()
2726
+ runtimeOptions: z8.record(z8.string(), z8.unknown()).optional()
2638
2727
  });
2639
- var resolvedMcpServerSchema = z7.union([
2640
- z7.object({
2641
- type: z7.literal("stdio").optional(),
2642
- command: z7.string(),
2643
- args: z7.array(z7.string()).optional(),
2644
- env: z7.record(z7.string(), z7.string()).optional()
2728
+ var resolvedMcpServerSchema = z8.union([
2729
+ z8.object({
2730
+ type: z8.literal("stdio").optional(),
2731
+ command: z8.string(),
2732
+ args: z8.array(z8.string()).optional(),
2733
+ env: z8.record(z8.string(), z8.string()).optional()
2645
2734
  }),
2646
- z7.object({
2647
- type: z7.literal("http"),
2648
- url: z7.string(),
2649
- headers: z7.record(z7.string(), z7.string()).optional()
2735
+ z8.object({
2736
+ type: z8.literal("http"),
2737
+ url: z8.string(),
2738
+ headers: z8.record(z8.string(), z8.string()).optional()
2650
2739
  }),
2651
- z7.object({
2652
- type: z7.literal("sse"),
2653
- url: z7.string(),
2654
- headers: z7.record(z7.string(), z7.string()).optional()
2740
+ z8.object({
2741
+ type: z8.literal("sse"),
2742
+ url: z8.string(),
2743
+ headers: z8.record(z8.string(), z8.string()).optional()
2655
2744
  })
2656
2745
  ]);
2657
- var resolvedMcpServersSchema = z7.record(z7.string(), resolvedMcpServerSchema);
2658
- var hostInjectedServersSchema = z7.record(z7.string(), z7.unknown());
2659
- var turnRequestSchema = z7.object({
2746
+ var resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
2747
+ var hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
2748
+ var turnRequestSchema = z8.object({
2660
2749
  // Server-composed system prompt (core + capability prose + adapter addendum +
2661
2750
  // charter). One string to the adapter.
2662
- systemPrompt: z7.string(),
2751
+ systemPrompt: z8.string(),
2663
2752
  // Server-composed per-turn user text (anchor reminder + the triggering message).
2664
- prompt: z7.string(),
2753
+ prompt: z8.string(),
2665
2754
  // The multi-block user-message body (text + vision).
2666
- content: z7.array(contentBlockSchema),
2755
+ content: z8.array(contentBlockSchema),
2667
2756
  // Portable-or-dialect run-config (above).
2668
2757
  config: resolvedRunConfigSchema,
2669
2758
  // Abstract capability grants; the adapter maps them to tool names.
2670
2759
  policy: hostPolicySchema,
2671
2760
  // Prior opaque session state, or null for a fresh session.
2672
- session: z7.string().nullable(),
2761
+ session: z8.string().nullable(),
2673
2762
  // The cabane control-plane coordinates for this turn's MCP + post-back.
2674
- cabane: z7.object({
2675
- mcpUrl: z7.string(),
2676
- bearer: z7.string(),
2677
- activeConversationId: z7.string(),
2763
+ cabane: z8.object({
2764
+ mcpUrl: z8.string(),
2765
+ bearer: z8.string(),
2766
+ activeConversationId: z8.string(),
2678
2767
  // CT714: the scoped TURN-CONTROL MCP endpoint (`/api/turn-control`). The
2679
2768
  // EXTERNAL adapters (Codex / opencode) mount it by URL under the key
2680
2769
  // `cabane_companion` — using the same `bearer` (the turn token) and the same
@@ -2684,7 +2773,7 @@ var turnRequestSchema = z7.object({
2684
2773
  // claude-code ignores it (it mounts the in-process instance instead), and
2685
2774
  // every existing `cabane`-block fixture keeps parsing unchanged; the
2686
2775
  // companion always populates it (`build-options.ts`).
2687
- turnControlUrl: z7.string().optional(),
2776
+ turnControlUrl: z8.string().optional(),
2688
2777
  // CT598: the workspace this turn runs in. The claude-code/opencode/codex
2689
2778
  // adapters never need it (they reach Cabane through the `cabane` MCP server,
2690
2779
  // which takes `workspaceId` as a per-tool arg the model supplies); the
@@ -2694,39 +2783,39 @@ var turnRequestSchema = z7.object({
2694
2783
  // adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
2695
2784
  // always populates it (`build-options.ts`), and the native adapter fails the
2696
2785
  // turn loudly when it is somehow absent rather than guessing.
2697
- workspaceId: z7.string().optional(),
2786
+ workspaceId: z8.string().optional(),
2698
2787
  // CT752: the server-resolved workspace surface this credential exposes.
2699
2788
  // Readiness uses this explicit fact to require `sdk` for code mode and the
2700
2789
  // granular floor for classic mode; inventory contents alone cannot infer it
2701
2790
  // because `sdk` is intentionally also available on the classic surface.
2702
- workspaceToolSurface: z7.enum(["code", "classic"]).optional()
2791
+ workspaceToolSurface: z8.enum(["code", "classic"]).optional()
2703
2792
  }),
2704
2793
  // Machine-local resolution (host-filled): the checkout cwd, extra env from a
2705
2794
  // prepare hook, and the resolved user MCP servers.
2706
- local: z7.object({
2707
- cwd: z7.string().optional(),
2708
- env: z7.record(z7.string(), z7.string()).optional(),
2795
+ local: z8.object({
2796
+ cwd: z8.string().optional(),
2797
+ env: z8.record(z8.string(), z8.string()).optional(),
2709
2798
  mcpServers: resolvedMcpServersSchema.optional(),
2710
2799
  // CT289: machine-local claude-code adapter knobs the operator sets on a
2711
2800
  // companion they run themselves — the auto-memory escape hatch. `autoMemory:
2712
2801
  // true` opts back into Claude Code's auto-memory (governed by the operator's
2713
2802
  // own `.claude/settings.json`); absent/false leaves the adapter's force-off
2714
2803
  // default in place (see `buildClaudeCodeOptions`).
2715
- claudeCode: z7.object({ autoMemory: z7.boolean().optional() }).optional()
2804
+ claudeCode: z8.object({ autoMemory: z8.boolean().optional() }).optional()
2716
2805
  }),
2717
2806
  // Host-owned injected servers (host-filled) — e.g. the summon server.
2718
- extra: z7.object({
2807
+ extra: z8.object({
2719
2808
  mcpServers: hostInjectedServersSchema
2720
2809
  })
2721
2810
  });
2722
2811
 
2723
2812
  // packages/agent-runtime/src/conformance.ts
2724
- import { z as z8 } from "zod";
2725
- var conformanceFixtureSchema = z8.object({
2726
- name: z8.string(),
2813
+ import { z as z9 } from "zod";
2814
+ var conformanceFixtureSchema = z9.object({
2815
+ name: z9.string(),
2727
2816
  request: turnRequestSchema,
2728
- nativeStream: z8.array(z8.unknown()),
2729
- expected: z8.array(turnEventSchema)
2817
+ nativeStream: z9.array(z9.unknown()),
2818
+ expected: z9.array(turnEventSchema)
2730
2819
  });
2731
2820
 
2732
2821
  // packages/agent-runtime/src/transcript.ts
@@ -2938,7 +3027,7 @@ var TurnPump = class {
2938
3027
  this.sink = {
2939
3028
  onAssistantText: async (evt) => {
2940
3029
  if (opts.signal.aborted) return;
2941
- const body = evt.text.trim();
3030
+ const body = normalizeCommitText(evt.text);
2942
3031
  if (!body) return;
2943
3032
  const kind = evt.final ? "final" : "progress";
2944
3033
  const seq = opts.nextSeq();
@@ -2946,6 +3035,7 @@ var TurnPump = class {
2946
3035
  if (evt.final) {
2947
3036
  this.emittedFinal = true;
2948
3037
  this.finalReplyBody = body;
3038
+ this.emittedFinalSource = "runtime_text";
2949
3039
  } else {
2950
3040
  this.lastProgressBody = body;
2951
3041
  }
@@ -2968,7 +3058,7 @@ var TurnPump = class {
2968
3058
  },
2969
3059
  onThinking: async (evt) => {
2970
3060
  if (opts.signal.aborted) return;
2971
- const trimmed = evt.text.trim();
3061
+ const trimmed = normalizeCommitText(evt.text);
2972
3062
  if (!trimmed) return;
2973
3063
  const text = truncate(trimmed, THINKING_TEXT_MAX_CHARS);
2974
3064
  const seq = opts.nextSeq();
@@ -2984,6 +3074,7 @@ var TurnPump = class {
2984
3074
  lastProgressBody = null;
2985
3075
  // The turn's final reply text, captured for the dashboard feed / logging.
2986
3076
  finalReplyBody = null;
3077
+ emittedFinalSource = "none";
2987
3078
  // CT11: each tool's persisted seq so the `done`/`error` frame republishes the
2988
3079
  // same seq the `start` row got (the host's upsert keeps the original seq).
2989
3080
  toolSeqByUseId = /* @__PURE__ */ new Map();
@@ -3006,6 +3097,7 @@ var TurnPump = class {
3006
3097
  await this.opts.commit.commitMessage({ body, kind: "final", seq });
3007
3098
  this.emittedFinal = true;
3008
3099
  this.finalReplyBody = body;
3100
+ this.emittedFinalSource = this.lastProgressBody ? "progress_promotion" : "host_fallback";
3009
3101
  } catch (err) {
3010
3102
  this.opts.onError?.(err, "empty-final");
3011
3103
  }
@@ -3020,6 +3112,9 @@ var TurnPump = class {
3020
3112
  get replyBody() {
3021
3113
  return this.finalReplyBody;
3022
3114
  }
3115
+ get finalSource() {
3116
+ return this.emittedFinalSource;
3117
+ }
3023
3118
  };
3024
3119
 
3025
3120
  // packages/agent-runtime/src/claude-code/index.ts
@@ -3041,7 +3136,7 @@ var CLAUDE_CODE_ADDENDUM = [
3041
3136
  ].join(" ");
3042
3137
 
3043
3138
  // packages/agent-runtime/src/claude-code/policy.ts
3044
- import { z as z9 } from "zod";
3139
+ import { z as z10 } from "zod";
3045
3140
  var HOST_FS_TOOLS = [
3046
3141
  // shell + local filesystem
3047
3142
  "Bash",
@@ -3091,18 +3186,18 @@ function withThinkingSummaries(thinking) {
3091
3186
  if (thinking.type === "disabled") return thinking;
3092
3187
  return { display: "summarized", ...thinking };
3093
3188
  }
3094
- var claudeCodeDialectSchema = z9.object({
3095
- thinking: z9.discriminatedUnion("type", [
3096
- z9.object({
3097
- type: z9.literal("adaptive"),
3098
- display: z9.enum(["summarized", "omitted"]).optional()
3189
+ var claudeCodeDialectSchema = z10.object({
3190
+ thinking: z10.discriminatedUnion("type", [
3191
+ z10.object({
3192
+ type: z10.literal("adaptive"),
3193
+ display: z10.enum(["summarized", "omitted"]).optional()
3099
3194
  }),
3100
- z9.object({
3101
- type: z9.literal("enabled"),
3102
- budgetTokens: z9.number().int().positive().optional(),
3103
- display: z9.enum(["summarized", "omitted"]).optional()
3195
+ z10.object({
3196
+ type: z10.literal("enabled"),
3197
+ budgetTokens: z10.number().int().positive().optional(),
3198
+ display: z10.enum(["summarized", "omitted"]).optional()
3104
3199
  }),
3105
- z9.object({ type: z9.literal("disabled") })
3200
+ z10.object({ type: z10.literal("disabled") })
3106
3201
  ]).optional()
3107
3202
  }).loose();
3108
3203
  function readThinking(runtimeOptions) {
@@ -3641,6 +3736,19 @@ var CLAUDE_CODE_CONFORMANCE_FIXTURES = [
3641
3736
  { type: "result", ok: false, reason: "result_error:error_max_turns" }
3642
3737
  ]
3643
3738
  },
3739
+ {
3740
+ // CT1144: the empty-runtime anomaly's exact captured trace — `system/init`
3741
+ // then a successful `result`, with nothing in between. Distinct from
3742
+ // `empty-final` below: THAT turn did work (a tool ran) and just didn't speak;
3743
+ // this one produced nothing at all. The adapter's job is unchanged either way
3744
+ // — report faithfully what the runtime said — so it must still emit
3745
+ // `result{ok:true}` here; the host is what decides that a success with no
3746
+ // content-bearing event is a failed turn.
3747
+ name: "zero-content success (empty runtime result)",
3748
+ request: makeRequest(),
3749
+ nativeStream: [init("s1"), resultSuccess("s1")],
3750
+ expected: [sessionEvent("s1"), { type: "result", ok: true }]
3751
+ },
3644
3752
  {
3645
3753
  // Empty-final: a clean turn that ended on a tool call with no closing text.
3646
3754
  // The adapter emits NO final text — empty-final promotion is host/pump
@@ -4044,7 +4152,7 @@ function sealHeld(held, terminal) {
4044
4152
  }
4045
4153
 
4046
4154
  // packages/agent-runtime/src/opencode/policy.ts
4047
- import { z as z10 } from "zod";
4155
+ import { z as z11 } from "zod";
4048
4156
  var OPENCODE_HOST_TOOLS = [
4049
4157
  "bash",
4050
4158
  "edit",
@@ -4071,8 +4179,8 @@ function opencodeToolPolicy(policy) {
4071
4179
  deny(OPENCODE_UI_PROMPT_TOOLS);
4072
4180
  return { tools, allowAllHostTools: policy.hostFs };
4073
4181
  }
4074
- var opencodeDialectSchema = z10.object({
4075
- agent: z10.string().min(1).optional()
4182
+ var opencodeDialectSchema = z11.object({
4183
+ agent: z11.string().min(1).optional()
4076
4184
  }).loose();
4077
4185
  function readOpencodeDialect(runtimeOptions) {
4078
4186
  const parsed = opencodeDialectSchema.safeParse(runtimeOptions?.["opencode"] ?? {});
@@ -4677,6 +4785,18 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
4677
4785
  ],
4678
4786
  expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "auth_expired" }]
4679
4787
  },
4788
+ {
4789
+ // CT1144: the same zero-content success the claude-code suite pins, in
4790
+ // opencode's vocabulary — the session goes idle having emitted no part at
4791
+ // all. No opencode sample of the anomaly has been seen in the wild; the
4792
+ // fixture exists because the host's classification is runtime-neutral, so a
4793
+ // runtime that CAN produce this envelope must be shown producing it in the
4794
+ // shape the classifier reads.
4795
+ name: "zero-content success (empty runtime result)",
4796
+ request: makeRequest2(),
4797
+ nativeStream: [idle()],
4798
+ expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: true }]
4799
+ },
4680
4800
  {
4681
4801
  // Empty-final: a clean turn that ended on a tool call with no closing text.
4682
4802
  // The adapter emits NO final text — empty-final promotion is host/pump
@@ -5082,7 +5202,7 @@ function sealHeld2(held, terminal) {
5082
5202
  }
5083
5203
 
5084
5204
  // packages/agent-runtime/src/codex/policy.ts
5085
- import { z as z11 } from "zod";
5205
+ import { z as z12 } from "zod";
5086
5206
  function codexToolPolicy(policy) {
5087
5207
  return policy.hostFs ? {
5088
5208
  sandboxMode: "danger-full-access",
@@ -5101,8 +5221,8 @@ function codexToolPolicy(policy) {
5101
5221
  };
5102
5222
  }
5103
5223
  var CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
5104
- var codexDialectSchema = z11.object({
5105
- modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
5224
+ var codexDialectSchema = z12.object({
5225
+ modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
5106
5226
  }).loose();
5107
5227
  function readCodexDialect(runtimeOptions) {
5108
5228
  const parsed = codexDialectSchema.safeParse(runtimeOptions?.["codex"] ?? {});
@@ -5791,6 +5911,18 @@ var CODEX_CONFORMANCE_FIXTURES = [
5791
5911
  { type: "result", ok: true }
5792
5912
  ]
5793
5913
  },
5914
+ {
5915
+ // CT1144: the same zero-content success the claude-code suite pins, in
5916
+ // Codex's vocabulary — the thread starts, the turn completes, and nothing is
5917
+ // emitted in between. No Codex sample of the anomaly has been seen in the
5918
+ // wild; the fixture exists because the host's classification is runtime-
5919
+ // neutral, so a runtime that CAN produce this envelope must be shown
5920
+ // producing it in the shape the classifier reads.
5921
+ name: "zero-content success (empty runtime result)",
5922
+ request: makeRequest3(),
5923
+ nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
5924
+ expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: true }]
5925
+ },
5794
5926
  {
5795
5927
  // Empty-final: a clean turn that ended on a tool call with no closing message.
5796
5928
  // The adapter emits NO final text — empty-final promotion is host/pump territory
@@ -6031,12 +6163,12 @@ var ConnectorHealthStore = class {
6031
6163
  };
6032
6164
 
6033
6165
  // src/dispatcher.ts
6034
- import { randomUUID } from "crypto";
6166
+ import { createHash as createHash2, randomUUID } from "crypto";
6035
6167
  import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync10, readdirSync as readdirSync2, statSync } from "fs";
6036
6168
  import { join as join14 } from "path";
6037
6169
 
6038
6170
  // src/summon.ts
6039
- import { z as z12 } from "zod";
6171
+ import { z as z13 } from "zod";
6040
6172
  var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
6041
6173
  var SUMMON_AGENT_TOOL = "summon_agent";
6042
6174
  var SUMMON_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUMMON_AGENT_TOOL}`;
@@ -6084,7 +6216,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6084
6216
  SUMMON_AGENT_TOOL,
6085
6217
  "Summon another agent into THIS conversation \u2014 dispatch a peer to reply here on your turn. Use it to hand part of the work to a teammate, or pull in an expert, without leaving the conversation. Pass the peer's `agentId` \u2014 every agent's handle and id is on the roster in your turn context. The peer is dispatched on your turn's final reply, so write the context/ask into that reply first \u2014 it receives your message + this conversation to work from. Writing `@handle` in your prose does NOT summon anyone (agent prose never dispatches); this tool is the only in-thread lever. Single target \u2014 the last call wins. Summoning yourself is a no-op. Reach for it when the human wants the peer's answer right HERE, in front of them \u2014 the reply lands in this thread, so there's no return to wire (a return is for work YOU consume, never a courtesy notification). A handoff to a DIFFERENT conversation is `cabane.conversations.create` / `cabane.conversations.post` with their `dispatch` field instead.",
6086
6218
  {
6087
- agentId: z12.string().uuid().describe(
6219
+ agentId: z13.string().uuid().describe(
6088
6220
  "The peer agent to summon \u2014 a workspace agent id, from your turn context's roster."
6089
6221
  )
6090
6222
  },
@@ -6101,7 +6233,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6101
6233
  SKIP_TURN_TOOL,
6102
6234
  `End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or a pile-on where someone else has it. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
6103
6235
  {
6104
- reason: z12.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
6236
+ reason: z13.string().min(1).max(500).describe("Short reason you are declining \u2014 used for telemetry/debugging.")
6105
6237
  },
6106
6238
  async (args) => {
6107
6239
  skipState.skipped = true;
@@ -6118,23 +6250,23 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6118
6250
  ASK_TOOL,
6119
6251
  "Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass `targetUserId` (a workspace member's user id \u2014 every person's id is on the roster in your turn context). Two forms: a SINGLE question \u2014 a `headline` (the actual question as one clear, capitalized sentence ending in `?`, \"Do we go to prod?\") plus a short `question` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a `questions` array of 1\u20135 items, each `{ headline, body?, options? }`. **Prefer the list over cramming the extra decisions into prose or dropping them** \u2014 end the turn with one ask carrying every question, never pick one and bury the rest. Each question keeps the same form rules: a one-sentence `headline`, a short `body` frame (NOT a report \u2014 your status, links, and detail go in your REPLY, and the body renders inline markdown only: links/emphasis/inline code, no bulleted lists or headings), and 2\u20134 `options` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. An option can be a short button label or a whole sentence. Provide EITHER `question` (single) or `questions` (array), never both. The ask is a first-class attention item aimed at that person; your final reply carries the surrounding CONTEXT (what you found, why you're stuck), the ask carries the QUESTION(S). An open ask marks you as blocked until EVERY question is answered, so raise one only when you truly can't proceed \u2014 never ceremonially. One ask per turn (last call wins). After asking, stop \u2014 when the person replies addressed to you, the ask resolves and you resume; other people's or agents' messages may wake you but leave it open. Targets a human only; to hand work to another AGENT use summon/dispatch instead.",
6120
6252
  {
6121
- targetUserId: z12.string().uuid().describe(
6253
+ targetUserId: z13.string().uuid().describe(
6122
6254
  "The workspace member (human) to ask \u2014 a user id, from your turn context's roster."
6123
6255
  ),
6124
- question: z12.string().min(1).max(400).optional().describe(
6256
+ question: z13.string().min(1).max(400).optional().describe(
6125
6257
  "SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (capped, inline markdown only). Provide EITHER this or `questions`, not both. Put the crisp one-sentence question in `headline`."
6126
6258
  ),
6127
- headline: z12.string().min(1).max(120).optional().describe(
6259
+ headline: z13.string().min(1).max(120).optional().describe(
6128
6260
  'SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in `?` ("Do we go to prod?"). What the human reads first in the inbox and the chip \u2014 one scannable question, no elaboration (that goes in `question`). Strongly encouraged.'
6129
6261
  ),
6130
- options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
6131
- questions: z12.array(
6132
- z12.object({
6133
- headline: z12.string().min(1).max(120).describe(
6262
+ options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("SINGLE-question form: optional 2\u20134 suggested one-click answers."),
6263
+ questions: z13.array(
6264
+ z13.object({
6265
+ headline: z13.string().min(1).max(120).describe(
6134
6266
  'The one-sentence question ("Do we go to prod?") \u2014 required for each item.'
6135
6267
  ),
6136
- body: z12.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
6137
- options: z12.array(z12.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
6268
+ body: z13.string().min(1).max(400).optional().describe("Optional short framing beneath the headline. NOT a report."),
6269
+ options: z13.array(z13.string().min(1).max(200)).min(2).max(4).optional().describe("Optional 2\u20134 one-click answers for this question.")
6138
6270
  })
6139
6271
  ).min(1).max(5).optional().describe(
6140
6272
  "MULTI-question form: 1\u20135 questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or `question`/`headline`/`options`, not both."
@@ -6191,13 +6323,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6191
6323
  SUB_AGENT_TOOL,
6192
6324
  "Spawn a sub-agent \u2014 hand a piece of work to a private worker with a fresh context window, whose result comes back to you automatically. Modeled on the Task tool, with ONE deliberate difference: it does NOT return the result inline. A callee's turn can run for minutes and no turn may hold an unbounded wait, so the shape is spawn-now, results-on-wake \u2014 this returns immediately with the child's `conversationId`, and the outcome lands LATER as a message in THIS conversation; you're woken once every sub-agent you have out in this conversation has returned. So DON'T wait for it: after spawning, finish whatever else this turn can do and end your turn (never poll the child with reads in a loop \u2014 the wake is automatic). Parallel fan-out = call this N times in one turn (they run concurrently; ONE wake when all are in); series = one call per turn. `prompt` is the child's opening instruction \u2014 make it self-contained (the sub-agent starts fresh, with only this prompt + the thread it lands in). `agentId` (optional) dispatches a PEER instead of yourself \u2014 same mechanics, a different mind (use for capability/context you lack); default (self) is the pure sub-worker with a clean context window. `title` (optional) names the child thread (results link it, so a legible title helps). A single sub-agent has no wall-clock advantage (you idle either way) \u2014 it pays when the callee has capability/context you lack, or to isolate a big read from your own session; the real win is fan-out. Don't spawn one for a lookup you can do in-turn with your own tools. The result returns to YOU to act on \u2014 reach for it when you're the consumer of the output, not as a way to notify a human: if a person just wants to read the result, dispatch a plain (no-return) conversation and link it instead of spawning a sub-agent.",
6193
6325
  {
6194
- prompt: z12.string().min(1).max(65536).describe(
6326
+ prompt: z13.string().min(1).max(65536).describe(
6195
6327
  "The sub-agent's opening instruction \u2014 self-contained (it starts with a fresh context window; only this prompt + the thread it lands in)."
6196
6328
  ),
6197
- agentId: z12.string().uuid().optional().describe(
6329
+ agentId: z13.string().uuid().optional().describe(
6198
6330
  "Optional peer to run the sub-agent as (a workspace agent id, from your turn context's roster); omit to spawn yourself with a fresh context window."
6199
6331
  ),
6200
- title: z12.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
6332
+ title: z13.string().max(200).optional().describe("Optional title for the child thread (result chips link it).")
6201
6333
  },
6202
6334
  async (args) => {
6203
6335
  const result = await subAgentCreate(args);
@@ -6227,13 +6359,13 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
6227
6359
  WAKE_ME_TOOL,
6228
6360
  "Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for \"wait until X\": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an `ask`. Pass EXACTLY ONE of `afterSeconds` (a relative delay \u2014 `300` for five minutes) or `at` (an absolute ISO-8601 timestamp WITH a zone, e.g. `2026-07-16T09:00:00-07:00` \u2014 YOU compute it from a phrase like \"tomorrow morning\"; the system never parses natural-language time). `note` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check (\"check whether CT441 merged yet\"). The wake is armed when your turn SETTLES, not now, so the delay counts from the turn ending; one wake per turn (last call wins). This is the sanctioned way to schedule your own continuation \u2014 the ONLY one; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an `ask` to the human instead. If a wake can't be armed you're re-dispatched with a note explaining why \u2014 never a silent drop.",
6229
6361
  {
6230
- afterSeconds: z12.number().int().positive().optional().describe(
6362
+ afterSeconds: z13.number().int().positive().optional().describe(
6231
6363
  "Relative delay in seconds from when this turn ends (e.g. 300 = five minutes). Provide EITHER this or `at`, not both. Floor 60s, horizon 14 days \u2014 enforced server-side."
6232
6364
  ),
6233
- at: z12.string().datetime({ offset: true }).optional().describe(
6365
+ at: z13.string().datetime({ offset: true }).optional().describe(
6234
6366
  "Absolute ISO-8601 timestamp WITH a zone (`Z` or `\xB1HH:MM`), e.g. `2026-07-16T09:00:00-07:00`. YOU compute it from a natural-language phrase using the current datetime in your turn context. Provide EITHER this or `afterSeconds`, not both."
6235
6367
  ),
6236
- note: z12.string().min(1).max(2e3).describe(
6368
+ note: z13.string().min(1).max(2e3).describe(
6237
6369
  'A note to your future self \u2014 becomes the body of the wake message that re-dispatches you. Write the condition to re-check ("check whether the PR merged").'
6238
6370
  )
6239
6371
  },
@@ -6423,12 +6555,12 @@ function clearPrepared(workspaceId, conversationId, agentId) {
6423
6555
  // src/secrets.ts
6424
6556
  import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
6425
6557
  import { join as join12 } from "path";
6426
- import { z as z13 } from "zod";
6558
+ import { z as z14 } from "zod";
6427
6559
  var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
6428
6560
  function secretsPath() {
6429
6561
  return join12(cabaneDir(), "secrets.json");
6430
6562
  }
6431
- var secretStoreSchema = z13.record(z13.string(), z13.string());
6563
+ var secretStoreSchema = z14.record(z14.string(), z14.string());
6432
6564
  function loadSecretStore() {
6433
6565
  const path = secretsPath();
6434
6566
  if (!existsSync9(path)) return makeStore({});
@@ -6512,12 +6644,13 @@ function resolveMcpSecrets(mcpServers, store) {
6512
6644
  }
6513
6645
 
6514
6646
  // src/transcript-writer.ts
6515
- import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
6516
- import { join as join13 } from "path";
6647
+ import { appendFileSync, chmodSync as chmodSync3, copyFileSync, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
6648
+ import { basename, dirname as dirname5, join as join13 } from "path";
6517
6649
  function transcriptsDir() {
6518
6650
  return join13(cabaneDir(), "transcripts");
6519
6651
  }
6520
6652
  var RETAIN = 200;
6653
+ var ANOMALY_RETAIN = 50;
6521
6654
  var TranscriptWriter = class {
6522
6655
  path;
6523
6656
  broken = false;
@@ -6545,6 +6678,25 @@ var TranscriptWriter = class {
6545
6678
  close(outcome) {
6546
6679
  this.line({ type: "_outcome", ...outcome });
6547
6680
  }
6681
+ // CT1145: keep an independent copy of an empty successful envelope after the
6682
+ // footer has landed. The ordinary 200-file FIFO keeps rotating as before;
6683
+ // this bucket retains the newest 50 anomalies regardless of ordinary traffic.
6684
+ // Best-effort like every other transcript operation: observability can never
6685
+ // fail the turn.
6686
+ preserveAnomaly() {
6687
+ if (this.broken) return;
6688
+ try {
6689
+ const dir2 = join13(dirname5(this.path), "anomalies");
6690
+ mkdirSync9(dir2, { recursive: true, mode: 448 });
6691
+ chmodSync3(dir2, 448);
6692
+ const target = join13(dir2, basename(this.path));
6693
+ copyFileSync(this.path, target);
6694
+ chmodSync3(target, 384);
6695
+ pruneOld(dir2, ANOMALY_RETAIN);
6696
+ } catch (err) {
6697
+ this.fail(err);
6698
+ }
6699
+ }
6548
6700
  line(obj) {
6549
6701
  if (this.broken) return;
6550
6702
  try {
@@ -6702,6 +6854,9 @@ var TurnCommitter = class {
6702
6854
  get replyBody() {
6703
6855
  return this.pump.replyBody;
6704
6856
  }
6857
+ get finalSource() {
6858
+ return this.pump.finalSource;
6859
+ }
6705
6860
  // CT183: resolve the in-thread summon into the `dispatch` field for a `final`
6706
6861
  // commit. A self-target is stripped here (mirror of the in-app self-strip); the
6707
6862
  // server strips it again and resolves / ignores an unknown id.
@@ -7565,6 +7720,17 @@ ${reason}`,
7565
7720
  let turnUsage;
7566
7721
  let turnResolvedModel;
7567
7722
  let turnResolvedConfig;
7723
+ const eventCounts = {
7724
+ session: 0,
7725
+ text: 0,
7726
+ thinking: 0,
7727
+ tool: 0,
7728
+ result: 0
7729
+ };
7730
+ let runtimeResultKind = null;
7731
+ let contentBearingEvents = 0;
7732
+ let latestSessionState = request.session;
7733
+ let settledDiagnostics = null;
7568
7734
  const committer = new TurnCommitter({
7569
7735
  api: this.opts.api,
7570
7736
  workspaceId,
@@ -7660,6 +7826,8 @@ ${reason}`,
7660
7826
  try {
7661
7827
  for await (const event of adapter.runTurn(request, abortController.signal)) {
7662
7828
  transcript?.write(event);
7829
+ eventCounts[event.type] += 1;
7830
+ if (isContentBearingEvent(event)) contentBearingEvents += 1;
7663
7831
  armIdle();
7664
7832
  if (abortController.signal.aborted) {
7665
7833
  turnLog.info("dispatcher: aborted mid-turn");
@@ -7668,6 +7836,7 @@ ${reason}`,
7668
7836
  break;
7669
7837
  }
7670
7838
  if (event.type === "session") {
7839
+ latestSessionState = event.state;
7671
7840
  if (event.degraded) sessionDegraded = true;
7672
7841
  if (!sessionWritten) {
7673
7842
  sessionWritten = true;
@@ -7706,6 +7875,7 @@ ${reason}`,
7706
7875
  turnUsage = event.usage;
7707
7876
  turnResolvedModel = event.resolvedModel;
7708
7877
  turnResolvedConfig = event.resolvedConfig;
7878
+ runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
7709
7879
  } else if (event.type === "text" && skipState.skipped) {
7710
7880
  } else {
7711
7881
  if (event.type === "text" && event.terminal) {
@@ -7718,6 +7888,11 @@ ${reason}`,
7718
7888
  okResult = false;
7719
7889
  resultReason = timeoutReason ?? "cancelled";
7720
7890
  }
7891
+ const emptyResultReason = !skipState.skipped && classifyEmptyResult({ ok: okResult, contentBearingEvents, usage: turnUsage });
7892
+ if (emptyResultReason) {
7893
+ okResult = false;
7894
+ resultReason = emptyResultReason;
7895
+ }
7721
7896
  if (!okResult && !resultReason) {
7722
7897
  resultReason = "no_result";
7723
7898
  }
@@ -7828,6 +8003,36 @@ ${reason}`,
7828
8003
  body.sessionWriteRejected = true;
7829
8004
  this.sessionWriteNotified.add(key);
7830
8005
  }
8006
+ const outcome = skipState.skipped ? "skipped" : userCancelled ? "cancelled" : okResult ? "success" : "failure";
8007
+ const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
8008
+ settledDiagnostics = {
8009
+ outcome,
8010
+ resultReason: diagnosticReason,
8011
+ sessionMode: sessionDegraded ? "degraded" : request.session ? "resumed" : "fresh",
8012
+ sessionFingerprint: fingerprintSessionState(latestSessionState),
8013
+ eventCounts,
8014
+ runtimeResultKind,
8015
+ finalSource: outcome === "skipped" || outcome === "cancelled" ? "marker" : committer.finalSource
8016
+ };
8017
+ body.diagnostics = settledDiagnostics;
8018
+ if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
8019
+ diagnosticReason.kind
8020
+ )) {
8021
+ turnLog.warn(
8022
+ {
8023
+ workspaceId,
8024
+ turnId,
8025
+ conversationId: payload.conversationId,
8026
+ agentId: payload.agentId,
8027
+ runtime: turnRuntime,
8028
+ model: turnResolvedModel ?? null,
8029
+ usage: turnUsage ?? null,
8030
+ hadStoredSession: request.session != null,
8031
+ diagnostics: settledDiagnostics
8032
+ },
8033
+ "dispatcher: anomalous turn settled"
8034
+ );
8035
+ }
7831
8036
  this.opts.connectorHealth?.recordSettle(turnRuntime, {
7832
8037
  ok: okResult,
7833
8038
  errorReason: body.errorReason ?? null
@@ -7860,6 +8065,9 @@ ${reason}`,
7860
8065
  if (!okResult && resultReason !== "cancelled") {
7861
8066
  turnLog.info(`turn failed \u2014 full transcript: ${transcript.path}`);
7862
8067
  }
8068
+ if (settledDiagnostics?.resultReason?.kind === "empty_result" || settledDiagnostics?.resultReason?.kind === "empty_result_unverified") {
8069
+ transcript.preserveAnomaly();
8070
+ }
7863
8071
  }
7864
8072
  if (okResult) {
7865
8073
  turnLog.debug({ durationMs }, "dispatcher: turn end (ok)");
@@ -7897,6 +8105,17 @@ ${reason}`,
7897
8105
  return true;
7898
8106
  }
7899
8107
  };
8108
+ function fingerprintSessionState(state) {
8109
+ if (!state) return null;
8110
+ let opaqueId = state;
8111
+ try {
8112
+ const parsed = JSON.parse(state);
8113
+ const candidate = parsed.sdkSessionId ?? parsed.threadId ?? parsed.sessionId;
8114
+ if (typeof candidate === "string" && candidate.length > 0) opaqueId = candidate;
8115
+ } catch {
8116
+ }
8117
+ return createHash2("sha256").update(opaqueId).digest("hex").slice(0, 16);
8118
+ }
7900
8119
 
7901
8120
  // src/opencode-models.ts
7902
8121
  var OPENCODE_RUNTIME = "opencode";
@@ -8081,43 +8300,43 @@ var Outbox = class {
8081
8300
  };
8082
8301
 
8083
8302
  // src/run-config.ts
8084
- import { z as z14 } from "zod";
8085
- var mcpStdioServerSchema = z14.object({
8086
- type: z14.literal("stdio").optional(),
8087
- command: z14.string().min(1),
8088
- args: z14.array(z14.string()).optional(),
8089
- env: z14.record(z14.string(), z14.string()).optional()
8303
+ import { z as z15 } from "zod";
8304
+ var mcpStdioServerSchema = z15.object({
8305
+ type: z15.literal("stdio").optional(),
8306
+ command: z15.string().min(1),
8307
+ args: z15.array(z15.string()).optional(),
8308
+ env: z15.record(z15.string(), z15.string()).optional()
8090
8309
  });
8091
- var mcpHttpServerSchema = z14.object({
8092
- type: z14.literal("http"),
8093
- url: z14.string().url(),
8094
- headers: z14.record(z14.string(), z14.string()).optional()
8310
+ var mcpHttpServerSchema = z15.object({
8311
+ type: z15.literal("http"),
8312
+ url: z15.string().url(),
8313
+ headers: z15.record(z15.string(), z15.string()).optional()
8095
8314
  });
8096
- var mcpSseServerSchema = z14.object({
8097
- type: z14.literal("sse"),
8098
- url: z14.string().url(),
8099
- headers: z14.record(z14.string(), z14.string()).optional()
8315
+ var mcpSseServerSchema = z15.object({
8316
+ type: z15.literal("sse"),
8317
+ url: z15.string().url(),
8318
+ headers: z15.record(z15.string(), z15.string()).optional()
8100
8319
  });
8101
- var mcpServerDefSchema = z14.union([
8320
+ var mcpServerDefSchema = z15.union([
8102
8321
  mcpHttpServerSchema,
8103
8322
  mcpSseServerSchema,
8104
8323
  mcpStdioServerSchema
8105
8324
  ]);
8106
- var thinkingConfigSchema = z14.discriminatedUnion("type", [
8107
- z14.object({ type: z14.literal("adaptive") }),
8108
- z14.object({ type: z14.literal("enabled"), budgetTokens: z14.number().int().positive().optional() }),
8109
- z14.object({ type: z14.literal("disabled") })
8325
+ var thinkingConfigSchema = z15.discriminatedUnion("type", [
8326
+ z15.object({ type: z15.literal("adaptive") }),
8327
+ z15.object({ type: z15.literal("enabled"), budgetTokens: z15.number().int().positive().optional() }),
8328
+ z15.object({ type: z15.literal("disabled") })
8110
8329
  ]);
8111
- var effortSchema = z14.enum(["low", "medium", "high", "xhigh", "max"]);
8112
- var runConfigSchema = z14.object({
8330
+ var effortSchema = z15.enum(["low", "medium", "high", "xhigh", "max"]);
8331
+ var runConfigSchema = z15.object({
8113
8332
  // CT788: the host-access binary replaced the `assistant`/`coding`/`custom` mode
8114
8333
  // trio + its custom tool lists — `true` grants the host filesystem/shell, absent
8115
8334
  // is the locked surface. Kept in lockstep with `@cabane/shared`'s
8116
8335
  // `agentRunConfigSchema` (independent zod, forward-compatible: unknown keys are
8117
8336
  // stripped, so an older companion riding a newer server never rejects the config).
8118
- hostAccess: z14.boolean().optional(),
8119
- mcpServers: z14.record(z14.string(), mcpServerDefSchema).optional(),
8120
- model: z14.string().min(1).optional(),
8337
+ hostAccess: z15.boolean().optional(),
8338
+ mcpServers: z15.record(z15.string(), mcpServerDefSchema).optional(),
8339
+ model: z15.string().min(1).optional(),
8121
8340
  thinking: thinkingConfigSchema.optional(),
8122
8341
  effort: effortSchema.optional()
8123
8342
  });