@cabane/companion 0.6.215 → 0.6.217

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -738,9 +738,9 @@ var init_config = __esm({
738
738
  // silent roster: an agent nobody listed gets no hook and no error, and starts
739
739
  // in the operator's `$HOME`.
740
740
  prepareHook: prepareHookSchema.optional(),
741
- // `dashboardPort` and `autoOpen` are IGNORED by the CLI (no port, no browser)
742
- // but stay in the schema so an existing file still parses; the Electron shell
743
- // reads them. `logLevel` is pino's level, live-editable.
741
+ // The CLI ignores both dashboard settings. Desktop uses `dashboardPort`;
742
+ // `autoOpen` remains parseable for older config files but no longer controls
743
+ // startup. `logLevel` is pino's level, live-editable.
744
744
  dashboardPort: z2.number().int().min(1).max(65535).optional(),
745
745
  autoOpen: z2.boolean().optional(),
746
746
  logLevel: z2.enum(["warn", "info", "debug"]).optional(),
@@ -778,7 +778,10 @@ var init_config = __esm({
778
778
  // credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
779
779
  // what a wrapper can rely on.
780
780
  runner: z2.object({
781
- command: z2.array(z2.string().min(1)).min(1)
781
+ command: z2.array(z2.string().min(1)).min(1),
782
+ // The external wrapper supplies only broker placeholders and removes
783
+ // credential mounts. Do not hand provider logins to that runner on stdin.
784
+ brokeredCredentials: z2.boolean().optional()
782
785
  }).strict().optional()
783
786
  });
784
787
  RETIRED_ROW_KEYS = ["sandbox"];
@@ -2105,7 +2108,7 @@ var init_turn_diagnostics = __esm({
2105
2108
  turnSessionModes = ["fresh", "resumed", "degraded"];
2106
2109
  turnRuntimeResultKinds = ["success", "error", "no_terminal"];
2107
2110
  turnFinalSources = [
2108
- // The agent declared its answer with the `answer` turn act (CT1973).
2111
+ // The agent declared its answer with the `answer` turn act.
2109
2112
  "declared",
2110
2113
  "runtime_text",
2111
2114
  "progress_promotion",
@@ -2327,23 +2330,6 @@ var init_standing = __esm({
2327
2330
  }
2328
2331
  });
2329
2332
 
2330
- // packages/agent-runtime/src/conformance.ts
2331
- import { z as z9 } from "zod";
2332
- var conformanceFixtureSchema;
2333
- var init_conformance = __esm({
2334
- "packages/agent-runtime/src/conformance.ts"() {
2335
- "use strict";
2336
- init_turn_event();
2337
- init_turn_request();
2338
- conformanceFixtureSchema = z9.object({
2339
- name: z9.string(),
2340
- request: turnRequestSchema,
2341
- nativeStream: z9.array(z9.unknown()),
2342
- expected: z9.array(turnEventSchema)
2343
- });
2344
- }
2345
- });
2346
-
2347
2333
  // packages/agent-runtime/src/transcript.ts
2348
2334
  function createTerminalTextBuffer() {
2349
2335
  return { held: null };
@@ -2902,7 +2888,7 @@ var init_mcp_secret_env = __esm({
2902
2888
  });
2903
2889
 
2904
2890
  // packages/agent-runtime/src/claude-code/policy.ts
2905
- import { z as z10 } from "zod";
2891
+ import { z as z9 } from "zod";
2906
2892
  function withThinkingSummaries(thinking) {
2907
2893
  if (!thinking) return { type: "adaptive", display: "summarized" };
2908
2894
  if (thinking.type === "disabled") return thinking;
@@ -2939,18 +2925,18 @@ var init_policy = __esm({
2939
2925
  "Workflow"
2940
2926
  ];
2941
2927
  DEFAULT_WEB_TOOLS = ["WebSearch", "WebFetch"];
2942
- claudeCodeDialectSchema = z10.object({
2943
- thinking: z10.discriminatedUnion("type", [
2944
- z10.object({
2945
- type: z10.literal("adaptive"),
2946
- display: z10.enum(["summarized", "omitted"]).optional()
2928
+ claudeCodeDialectSchema = z9.object({
2929
+ thinking: z9.discriminatedUnion("type", [
2930
+ z9.object({
2931
+ type: z9.literal("adaptive"),
2932
+ display: z9.enum(["summarized", "omitted"]).optional()
2947
2933
  }),
2948
- z10.object({
2949
- type: z10.literal("enabled"),
2950
- budgetTokens: z10.number().int().positive().optional(),
2951
- display: z10.enum(["summarized", "omitted"]).optional()
2934
+ z9.object({
2935
+ type: z9.literal("enabled"),
2936
+ budgetTokens: z9.number().int().positive().optional(),
2937
+ display: z9.enum(["summarized", "omitted"]).optional()
2952
2938
  }),
2953
- z10.object({ type: z10.literal("disabled") })
2939
+ z9.object({ type: z9.literal("disabled") })
2954
2940
  ]).optional()
2955
2941
  }).loose();
2956
2942
  }
@@ -3018,7 +3004,7 @@ async function preToolUseHook(input) {
3018
3004
  hookSpecificOutput: {
3019
3005
  hookEventName: "PreToolUse",
3020
3006
  permissionDecision: "allow",
3021
- permissionDecisionReason: "headless never-prompt (CT680)",
3007
+ permissionDecisionReason: "headless never-prompt",
3022
3008
  updatedInput: toolInput
3023
3009
  }
3024
3010
  };
@@ -3586,7 +3572,7 @@ async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOpti
3586
3572
  if (recoverResume) {
3587
3573
  if (signal.aborted) return;
3588
3574
  onWarn?.(
3589
- `claude-code adapter: stored session ${initial.resume} ended before system/init \u2014 starting one fresh session (CT1293 recovery)`,
3575
+ `claude-code adapter: stored session ${initial.resume} ended before system/init \u2014 starting one fresh session`,
3590
3576
  {
3591
3577
  cwd: req.local.cwd,
3592
3578
  reason: "session_start_failed",
@@ -3615,7 +3601,7 @@ function createClaudeCodeAdapter(deps = {}) {
3615
3601
  let built = buildClaudeCodeOptions(req, deps.augmentOptions);
3616
3602
  if (built.freshReason === "cwd_mismatch") {
3617
3603
  deps.onWarn?.(
3618
- "claude-code adapter: stored session cwd no longer matches the current environment \u2014 starting a fresh session (SJ527 guard)",
3604
+ "claude-code adapter: stored session cwd no longer matches the current environment \u2014 starting a fresh session",
3619
3605
  { cwd: req.local.cwd }
3620
3606
  );
3621
3607
  }
@@ -3665,546 +3651,6 @@ var init_claude_code = __esm({
3665
3651
  }
3666
3652
  });
3667
3653
 
3668
- // packages/agent-runtime/src/claude-code/conformance.ts
3669
- function makeRequest(overrides2 = {}) {
3670
- return {
3671
- systemPrompt: "system",
3672
- prompt: "hi there",
3673
- content: [{ type: "text", text: "hi there" }],
3674
- // The adapter rejects a null model, so the fixture carries a real bare SDK id.
3675
- config: { model: "claude-opus-4-8" },
3676
- session: null,
3677
- cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
3678
- local: { cwd: CWD },
3679
- extra: { mcpServers: {} },
3680
- ...overrides2
3681
- };
3682
- }
3683
- var ABORT_SENTINEL, CWD, HEALTHY_MCP_INVENTORY, init, assistantText, assistantTextTool, toolResult, resultSuccess, resultSuccessFull, resultError, resultErrorFull, rateLimitEvent, authStatus, sessionEvent, BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES, CLAUDE_CODE_CONFORMANCE_FIXTURES;
3684
- var init_conformance2 = __esm({
3685
- "packages/agent-runtime/src/claude-code/conformance.ts"() {
3686
- "use strict";
3687
- init_claude_code();
3688
- init_session();
3689
- ABORT_SENTINEL = { __abortHere: true };
3690
- CWD = "/env/here";
3691
- HEALTHY_MCP_INVENTORY = {
3692
- initReceived: true,
3693
- servers: [{ name: "cabane", status: "connected" }],
3694
- toolCount: 1
3695
- };
3696
- init = (sessionId, model) => ({
3697
- type: "system",
3698
- subtype: "init",
3699
- session_id: sessionId,
3700
- mcp_servers: HEALTHY_MCP_INVENTORY.servers,
3701
- tools: ["mcp__cabane__query"],
3702
- ...model ? { model } : {}
3703
- });
3704
- assistantText = (text) => ({
3705
- type: "assistant",
3706
- message: { content: [{ type: "text", text }] }
3707
- });
3708
- assistantTextTool = (text, tool2) => ({
3709
- type: "assistant",
3710
- message: {
3711
- content: [
3712
- { type: "text", text },
3713
- { type: "tool_use", id: tool2.id, name: tool2.name, input: tool2.input }
3714
- ]
3715
- }
3716
- });
3717
- toolResult = (id, content, isError = false) => ({
3718
- type: "user",
3719
- message: { content: [{ type: "tool_result", tool_use_id: id, is_error: isError, content }] }
3720
- });
3721
- resultSuccess = (sessionId) => ({
3722
- type: "result",
3723
- subtype: "success",
3724
- session_id: sessionId
3725
- });
3726
- resultSuccessFull = (sessionId, extra) => ({
3727
- ...resultSuccess(sessionId),
3728
- ...extra
3729
- });
3730
- resultError = (subtype) => ({
3731
- type: "result",
3732
- subtype,
3733
- session_id: "s"
3734
- });
3735
- resultErrorFull = (subtype, extra = {}) => ({
3736
- type: "result",
3737
- subtype,
3738
- session_id: "s",
3739
- ...extra
3740
- });
3741
- rateLimitEvent = (status2, resetsAt, rateLimitType) => ({
3742
- type: "rate_limit_event",
3743
- rate_limit_info: {
3744
- status: status2,
3745
- ...resetsAt !== void 0 ? { resetsAt } : {},
3746
- ...rateLimitType !== void 0 ? { rateLimitType } : {}
3747
- },
3748
- session_id: "s"
3749
- });
3750
- authStatus = (error) => ({
3751
- type: "auth_status",
3752
- isAuthenticating: false,
3753
- output: [],
3754
- error,
3755
- session_id: "s"
3756
- });
3757
- sessionEvent = (sdkSessionId, cwd = CWD, degraded = false) => ({
3758
- type: "session",
3759
- state: encodeSession({ sdkSessionId, cwd }),
3760
- ...degraded ? { degraded: true } : {}
3761
- });
3762
- BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
3763
- {
3764
- // A plain text reply: the held text-only block flushes as the terminal final.
3765
- name: "clean turn",
3766
- request: makeRequest(),
3767
- nativeStream: [init("s1"), assistantText("Hello there."), resultSuccess("s1")],
3768
- expected: [
3769
- sessionEvent("s1"),
3770
- { type: "text", body: "Hello there.", terminal: true },
3771
- { type: "result", ok: true }
3772
- ]
3773
- },
3774
- {
3775
- // Narration + a tool call + result + closing reply.
3776
- name: "tool turn",
3777
- request: makeRequest(),
3778
- nativeStream: [
3779
- init("s1"),
3780
- assistantTextTool("Reading your notes.", {
3781
- id: "tu1",
3782
- name: "mcp__cabane__cabane_read",
3783
- input: { path: "sleep.md" }
3784
- }),
3785
- toolResult("tu1", [{ type: "text", text: "sleep early" }]),
3786
- assistantText("Here's the summary."),
3787
- resultSuccess("s1")
3788
- ],
3789
- expected: [
3790
- sessionEvent("s1"),
3791
- { type: "text", body: "Reading your notes.", terminal: false },
3792
- {
3793
- type: "tool",
3794
- id: "tu1",
3795
- name: "cabane_read",
3796
- phase: "start",
3797
- summary: "sleep.md",
3798
- input: { path: "sleep.md" },
3799
- // Derived from the raw `mcp__cabane__` name.
3800
- mcpServer: "cabane"
3801
- },
3802
- {
3803
- type: "tool",
3804
- id: "tu1",
3805
- name: "cabane_read",
3806
- phase: "done",
3807
- summary: "sleep.md",
3808
- input: { path: "sleep.md" },
3809
- result: [{ type: "text", text: "sleep early" }],
3810
- mcpServer: "cabane"
3811
- },
3812
- { type: "text", body: "Here's the summary.", terminal: true },
3813
- { type: "result", ok: true }
3814
- ]
3815
- },
3816
- {
3817
- // Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops
3818
- // before the closing reply — no final text, no `result` event (the server writes the stopped
3819
- // marker).
3820
- name: "cancel mid-stream",
3821
- request: makeRequest(),
3822
- nativeStream: [
3823
- init("s1"),
3824
- assistantTextTool("Working on it.", {
3825
- id: "tu1",
3826
- name: "mcp__cabane__cabane_list",
3827
- input: {}
3828
- }),
3829
- toolResult("tu1"),
3830
- ABORT_SENTINEL,
3831
- assistantText("This reply must never be emitted."),
3832
- resultSuccess("s1")
3833
- ],
3834
- expected: [
3835
- sessionEvent("s1"),
3836
- { type: "text", body: "Working on it.", terminal: false },
3837
- {
3838
- type: "tool",
3839
- id: "tu1",
3840
- name: "cabane_list",
3841
- phase: "start",
3842
- summary: "",
3843
- input: {},
3844
- mcpServer: "cabane"
3845
- },
3846
- {
3847
- type: "tool",
3848
- id: "tu1",
3849
- name: "cabane_list",
3850
- phase: "done",
3851
- summary: "",
3852
- input: {},
3853
- result: void 0,
3854
- mcpServer: "cabane"
3855
- }
3856
- ]
3857
- },
3858
- {
3859
- // An error result: partial narration lands as `progress` (terminal:false),
3860
- // and the `result` event carries the machine reason.
3861
- name: "error result",
3862
- request: makeRequest(),
3863
- nativeStream: [init("s1"), assistantText("Partial work."), resultError("error_max_turns")],
3864
- expected: [
3865
- sessionEvent("s1"),
3866
- { type: "text", body: "Partial work.", terminal: false },
3867
- { type: "result", ok: false, reason: "result_error:error_max_turns" }
3868
- ]
3869
- },
3870
- {
3871
- // The empty-runtime anomaly's captured trace: `system/init` then a successful `result`, nothing
3872
- // in between. Distinct from `empty-final` (a tool ran, no speech); this produced nothing. The
3873
- // adapter reports faithfully — `result{ok:true}` — and the host decides that a success with no
3874
- // content-bearing event is a failed turn.
3875
- name: "zero-content success (empty runtime result)",
3876
- request: makeRequest(),
3877
- nativeStream: [init("s1"), resultSuccess("s1")],
3878
- expected: [sessionEvent("s1"), { type: "result", ok: true }]
3879
- },
3880
- {
3881
- // On resume, a completed background task can emit a notification and its own zero-token result
3882
- // before the prompt's frames. That result is not the turn boundary; decoding continues through
3883
- // the reply and real usage.
3884
- name: "task-notification result does not end a resumed turn",
3885
- request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
3886
- nativeStream: [
3887
- init("s1"),
3888
- {
3889
- type: "system",
3890
- subtype: "task_notification",
3891
- task_id: "task-1",
3892
- status: "completed",
3893
- output_file: "/tmp/task-1.output",
3894
- summary: "Background work completed.",
3895
- session_id: "s1"
3896
- },
3897
- resultSuccessFull("s1", {
3898
- is_error: false,
3899
- origin: { kind: "task-notification" },
3900
- usage: {
3901
- input_tokens: 0,
3902
- output_tokens: 0,
3903
- cache_read_input_tokens: 0,
3904
- cache_creation_input_tokens: 0
3905
- }
3906
- }),
3907
- assistantTextTool("Checking the returned work.", {
3908
- id: "tu1",
3909
- name: "mcp__cabane__cabane_read",
3910
- input: { path: "result.md" }
3911
- }),
3912
- toolResult("tu1", [{ type: "text", text: "ready" }]),
3913
- assistantText("The prompt completed normally."),
3914
- resultSuccessFull("s1", {
3915
- is_error: false,
3916
- usage: {
3917
- input_tokens: 120,
3918
- output_tokens: 30,
3919
- cache_read_input_tokens: 80,
3920
- cache_creation_input_tokens: 10
3921
- }
3922
- })
3923
- ],
3924
- expected: [
3925
- { type: "text", body: "Checking the returned work.", terminal: false },
3926
- {
3927
- type: "tool",
3928
- id: "tu1",
3929
- name: "cabane_read",
3930
- phase: "start",
3931
- summary: "result.md",
3932
- input: { path: "result.md" },
3933
- mcpServer: "cabane"
3934
- },
3935
- {
3936
- type: "tool",
3937
- id: "tu1",
3938
- name: "cabane_read",
3939
- phase: "done",
3940
- summary: "result.md",
3941
- input: { path: "result.md" },
3942
- result: [{ type: "text", text: "ready" }],
3943
- mcpServer: "cabane"
3944
- },
3945
- { type: "text", body: "The prompt completed normally.", terminal: true },
3946
- {
3947
- type: "result",
3948
- ok: true,
3949
- usage: {
3950
- inputTokens: 210,
3951
- outputTokens: 30,
3952
- cacheReadTokens: 80,
3953
- cacheCreationTokens: 10
3954
- }
3955
- }
3956
- ]
3957
- },
3958
- {
3959
- // The filter is deliberately narrow: an origin-less zero-token result still ends the turn, even
3960
- // if more frames follow.
3961
- name: "origin-less result still ends a resumed turn",
3962
- request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
3963
- nativeStream: [
3964
- init("s1"),
3965
- resultSuccessFull("s1", {
3966
- is_error: false,
3967
- usage: {
3968
- input_tokens: 0,
3969
- output_tokens: 0,
3970
- cache_read_input_tokens: 0,
3971
- cache_creation_input_tokens: 0
3972
- }
3973
- }),
3974
- assistantText("This must not be emitted."),
3975
- resultSuccess("s1")
3976
- ],
3977
- expected: [
3978
- {
3979
- type: "result",
3980
- ok: true,
3981
- usage: {
3982
- inputTokens: 0,
3983
- outputTokens: 0,
3984
- cacheReadTokens: 0,
3985
- cacheCreationTokens: 0
3986
- }
3987
- }
3988
- ]
3989
- },
3990
- {
3991
- // Empty-final: a clean turn that ended on a tool call with no closing text.
3992
- // The adapter emits NO final text — empty-final promotion is host/pump
3993
- // territory (this fixture pins that boundary).
3994
- name: "empty-final (no closing text)",
3995
- request: makeRequest(),
3996
- nativeStream: [
3997
- init("s1"),
3998
- assistantTextTool("", {
3999
- id: "tu1",
4000
- name: "mcp__cabane__cabane_write",
4001
- input: { path: "a.md" }
4002
- }),
4003
- toolResult("tu1"),
4004
- resultSuccess("s1")
4005
- ],
4006
- expected: [
4007
- sessionEvent("s1"),
4008
- {
4009
- type: "tool",
4010
- id: "tu1",
4011
- name: "cabane_write",
4012
- phase: "start",
4013
- summary: "a.md",
4014
- input: { path: "a.md" },
4015
- mcpServer: "cabane"
4016
- },
4017
- {
4018
- type: "tool",
4019
- id: "tu1",
4020
- name: "cabane_write",
4021
- phase: "done",
4022
- summary: "a.md",
4023
- input: { path: "a.md" },
4024
- result: void 0,
4025
- mcpServer: "cabane"
4026
- },
4027
- { type: "result", ok: true }
4028
- ]
4029
- },
4030
- {
4031
- // A subscription cap. The SDK emits a rejected `rate_limit_event` with a named subscription
4032
- // window; the terminal error result classifies as `usage_capped`. The held provider line
4033
- // independently classifies to the same kind, so it lands as a runtime notice, not agent
4034
- // progress.
4035
- name: "subscription cap \u2192 usage_capped",
4036
- request: makeRequest(),
4037
- nativeStream: [
4038
- init("s1"),
4039
- assistantText("You've hit your session limit \xB7 resets 8:20pm (UTC)"),
4040
- rateLimitEvent("rejected", void 0, "five_hour"),
4041
- resultError("error_during_execution")
4042
- ],
4043
- expected: [
4044
- sessionEvent("s1"),
4045
- {
4046
- type: "runtime_notice",
4047
- body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
4048
- },
4049
- { type: "result", ok: false, reason: "usage_capped" }
4050
- ]
4051
- },
4052
- {
4053
- // A failed turn can still contain the agent's own partial narration. No text classification
4054
- // means no authorship rewrite, even though the structured result is the same usage cap.
4055
- name: "partial narration before subscription cap stays progress",
4056
- request: makeRequest(),
4057
- nativeStream: [
4058
- init("s1"),
4059
- assistantText("Partial work."),
4060
- rateLimitEvent("rejected", void 0, "five_hour"),
4061
- resultError("error_during_execution")
4062
- ],
4063
- expected: [
4064
- sessionEvent("s1"),
4065
- { type: "text", body: "Partial work.", terminal: false },
4066
- { type: "result", ok: false, reason: "usage_capped" }
4067
- ]
4068
- },
4069
- {
4070
- // Recognized runtime prose stays authored when its kind disagrees with the structured terminal
4071
- // failure.
4072
- name: "cap notice before server failure stays progress",
4073
- request: makeRequest(),
4074
- nativeStream: [
4075
- init("s1"),
4076
- assistantText("You've hit your session limit \xB7 resets 8:20pm (UTC)"),
4077
- resultErrorFull("error_during_execution", {
4078
- is_error: true,
4079
- result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
4080
- })
4081
- ],
4082
- expected: [
4083
- sessionEvent("s1"),
4084
- {
4085
- type: "text",
4086
- body: "You've hit your session limit \xB7 resets 8:20pm (UTC)",
4087
- terminal: false
4088
- },
4089
- { type: "result", ok: false, reason: "server_error" }
4090
- ]
4091
- },
4092
- {
4093
- // `classifyErrorText` deliberately searches arbitrary error blobs for status tokens. Assistant
4094
- // narration needs the stricter whole-notice classifier, or an incidental count would be
4095
- // de-authored when the turn later fails with the matching provider kind.
4096
- name: "incidental 5xx-shaped narration before server failure stays progress",
4097
- request: makeRequest(),
4098
- nativeStream: [
4099
- init("s1"),
4100
- assistantText("Indexed 500 files. Writing the summary next."),
4101
- resultErrorFull("error_during_execution", {
4102
- is_error: true,
4103
- result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
4104
- })
4105
- ],
4106
- expected: [
4107
- sessionEvent("s1"),
4108
- {
4109
- type: "text",
4110
- body: "Indexed 500 files. Writing the summary next.",
4111
- terminal: false
4112
- },
4113
- { type: "result", ok: false, reason: "server_error" }
4114
- ]
4115
- },
4116
- {
4117
- // A cap signalled only by the result's `terminal_reason:'blocking_limit'` (no rate-limit event)
4118
- // still classifies as `usage_capped`.
4119
- name: "blocking_limit terminal reason \u2192 usage_capped",
4120
- request: makeRequest(),
4121
- nativeStream: [
4122
- init("s1"),
4123
- resultErrorFull("error_during_execution", {
4124
- is_error: true,
4125
- terminal_reason: "blocking_limit",
4126
- result: "stopped"
4127
- })
4128
- ],
4129
- expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "usage_capped" }]
4130
- },
4131
- {
4132
- // An expired login surfaces as an `auth_status` error frame; the failing result classifies as
4133
- // `auth_expired`.
4134
- name: "expired login \u2192 auth_expired",
4135
- request: makeRequest(),
4136
- nativeStream: [
4137
- init("s1"),
4138
- authStatus("OAuth token has expired, please run /login"),
4139
- resultErrorFull("error_during_execution", {
4140
- is_error: true,
4141
- result: "authentication failed"
4142
- })
4143
- ],
4144
- expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "auth_expired" }]
4145
- },
4146
- {
4147
- // A transient provider outage in the terminal result text (no structured event or enum) — a 529
4148
- // overload — classifies as `server_error` via the text fallback. It must not read as the cap or
4149
- // stamp the connector `limited`.
4150
- name: "provider overload text \u2192 server_error",
4151
- request: makeRequest(),
4152
- nativeStream: [
4153
- init("s1"),
4154
- resultErrorFull("error_during_execution", {
4155
- is_error: true,
4156
- result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
4157
- })
4158
- ],
4159
- expected: [sessionEvent("s1"), { type: "result", ok: false, reason: "server_error" }]
4160
- },
4161
- {
4162
- // The resolved model is captured off `system/init.model` and attached to the terminal `result`
4163
- // even on a plain resume (this fixture resumes, so no `session` event fires). What's asserted is
4164
- // that the concrete id the harness reported comes home.
4165
- name: "resolved model captured from system/init",
4166
- request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
4167
- nativeStream: [init("s1", "claude-opus-4-8"), assistantText("Done."), resultSuccess("s1")],
4168
- expected: [
4169
- { type: "text", body: "Done.", terminal: true },
4170
- { type: "result", ok: true, resolvedModel: "claude-opus-4-8" }
4171
- ]
4172
- },
4173
- {
4174
- // Session resume echo: the stored state matches the cwd, so its SDK id is resumed and
4175
- // `system/init` echoes the same id — no session event fires.
4176
- name: "session resume (no re-emit on echo)",
4177
- request: makeRequest({ session: encodeSession({ sdkSessionId: "s1", cwd: CWD }) }),
4178
- nativeStream: [init("s1"), assistantText("Back again."), resultSuccess("s1")],
4179
- expected: [
4180
- { type: "text", body: "Back again.", terminal: true },
4181
- { type: "result", ok: true }
4182
- ]
4183
- },
4184
- {
4185
- // Cwd-mismatch degrade: the stored state was created under another cwd, so the adapter starts
4186
- // fresh — `system/init` reports a new id, and the session event records the current cwd.
4187
- name: "session cwd-mismatch degrades to fresh",
4188
- request: makeRequest({ session: encodeSession({ sdkSessionId: "old", cwd: "/env/gone" }) }),
4189
- nativeStream: [init("fresh1"), assistantText("Fresh start."), resultSuccess("fresh1")],
4190
- expected: [
4191
- // The degrade is flagged on the session event so the host relays it on settle and the server
4192
- // rewinds the catch-up mark (recovering context the discarded session no longer carries).
4193
- sessionEvent("fresh1", CWD, true),
4194
- { type: "text", body: "Fresh start.", terminal: true },
4195
- { type: "result", ok: true }
4196
- ]
4197
- }
4198
- ];
4199
- CLAUDE_CODE_CONFORMANCE_FIXTURES = BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES.map((fixture) => ({
4200
- ...fixture,
4201
- expected: fixture.expected.map(
4202
- (event) => event.type === "result" ? { ...event, mcpInventory: HEALTHY_MCP_INVENTORY } : event
4203
- )
4204
- }));
4205
- }
4206
- });
4207
-
4208
3654
  // packages/agent-runtime/src/registry.ts
4209
3655
  function createAdapterRegistry(adapters) {
4210
3656
  const byName = /* @__PURE__ */ new Map();
@@ -4501,7 +3947,7 @@ var init_loop2 = __esm({
4501
3947
  });
4502
3948
 
4503
3949
  // packages/agent-runtime/src/opencode/policy.ts
4504
- import { z as z11 } from "zod";
3950
+ import { z as z10 } from "zod";
4505
3951
  function opencodeTools() {
4506
3952
  const tools = {};
4507
3953
  for (const name of [...OPENCODE_SCHEDULING_TOOLS, ...OPENCODE_UI_PROMPT_TOOLS])
@@ -4518,8 +3964,8 @@ var init_policy2 = __esm({
4518
3964
  "use strict";
4519
3965
  OPENCODE_SCHEDULING_TOOLS = [];
4520
3966
  OPENCODE_UI_PROMPT_TOOLS = ["question"];
4521
- opencodeDialectSchema = z11.object({
4522
- agent: z11.string().min(1).optional()
3967
+ opencodeDialectSchema = z10.object({
3968
+ agent: z10.string().min(1).optional()
4523
3969
  }).loose();
4524
3970
  }
4525
3971
  });
@@ -4927,7 +4373,7 @@ function createOpencodeAdapter(deps = {}) {
4927
4373
  const resume = "resume" in decision ? decision.resume : null;
4928
4374
  if ("fresh" in decision && decision.reason === "directory_mismatch") {
4929
4375
  deps.onWarn?.(
4930
- "opencode adapter: stored session directory no longer matches the current environment \u2014 starting a fresh session (SJ527-analogue guard)",
4376
+ "opencode adapter: stored session directory no longer matches the current environment \u2014 starting a fresh session",
4931
4377
  { directory }
4932
4378
  );
4933
4379
  }
@@ -4982,490 +4428,6 @@ var init_opencode = __esm({
4982
4428
  }
4983
4429
  });
4984
4430
 
4985
- // packages/agent-runtime/src/opencode/conformance.ts
4986
- function makeRequest2(overrides2 = {}) {
4987
- return {
4988
- systemPrompt: "system",
4989
- prompt: "hi there",
4990
- content: [{ type: "text", text: "hi there" }],
4991
- config: { model: "deepseek/deepseek-chat" },
4992
- session: null,
4993
- cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
4994
- local: { cwd: DIR },
4995
- extra: { mcpServers: {} },
4996
- ...overrides2
4997
- };
4998
- }
4999
- var ABORT_SENTINEL2, NEW_SESSION_ID, BOUNDED_NOTICE, DIR, textPart, reasoningPart, toolPart, messageUpdated, echoedUserPart, idle, errored, sessionEvent2, OPENCODE_CONFORMANCE_FIXTURES;
5000
- var init_conformance3 = __esm({
5001
- "packages/agent-runtime/src/opencode/conformance.ts"() {
5002
- "use strict";
5003
- init_opencode();
5004
- init_session2();
5005
- ABORT_SENTINEL2 = { __abortHere: true };
5006
- NEW_SESSION_ID = "sess_new";
5007
- BOUNDED_NOTICE = "You've hit your usage limit \xB7 ".padEnd(200, "x");
5008
- DIR = "/env/here";
5009
- textPart = (id, text) => ({
5010
- type: "message.part.updated",
5011
- properties: { part: { type: "text", id, text } }
5012
- });
5013
- reasoningPart = (id, text) => ({
5014
- type: "message.part.updated",
5015
- properties: { part: { type: "reasoning", id, text } }
5016
- });
5017
- toolPart = (id, tool2, state, callID) => ({
5018
- type: "message.part.updated",
5019
- properties: { part: { type: "tool", id, tool: tool2, state, ...callID ? { callID } : {} } }
5020
- });
5021
- messageUpdated = (role, id) => ({
5022
- type: "message.updated",
5023
- properties: { info: { role, id } }
5024
- });
5025
- echoedUserPart = (partId, messageID, text) => ({
5026
- type: "message.part.updated",
5027
- properties: { part: { type: "text", id: partId, messageID, text } }
5028
- });
5029
- idle = () => ({ type: "session.idle", properties: {} });
5030
- errored = (message2) => ({
5031
- type: "session.error",
5032
- properties: { error: { message: message2 } }
5033
- });
5034
- sessionEvent2 = (sessionId, directory = DIR, degraded = false) => ({
5035
- type: "session",
5036
- state: encodeSession2({ sessionId, directory }),
5037
- ...degraded ? { degraded: true } : {}
5038
- });
5039
- OPENCODE_CONFORMANCE_FIXTURES = [
5040
- {
5041
- // A plain text reply: the held text part seals as the terminal final at idle.
5042
- name: "clean turn",
5043
- request: makeRequest2(),
5044
- nativeStream: [textPart("p1", "Hello there."), idle()],
5045
- expected: [
5046
- sessionEvent2(NEW_SESSION_ID),
5047
- { type: "text", body: "Hello there.", terminal: true },
5048
- { type: "result", ok: true }
5049
- ]
5050
- },
5051
- {
5052
- // Narration + a tool call + result + closing reply. The narration part is
5053
- // sealed as interim (a tool follows it); the trailing text is the final.
5054
- name: "tool turn",
5055
- request: makeRequest2(),
5056
- nativeStream: [
5057
- textPart("p1", "Reading your notes."),
5058
- toolPart("t1", "read", { status: "running", input: { path: "sleep.md" } }, "call_1"),
5059
- toolPart(
5060
- "t1",
5061
- "read",
5062
- { status: "completed", input: { path: "sleep.md" }, output: "sleep early" },
5063
- "call_1"
5064
- ),
5065
- textPart("p2", "Here's the summary."),
5066
- idle()
5067
- ],
5068
- expected: [
5069
- sessionEvent2(NEW_SESSION_ID),
5070
- { type: "text", body: "Reading your notes.", terminal: false },
5071
- {
5072
- type: "tool",
5073
- id: "call_1",
5074
- name: "read",
5075
- phase: "start",
5076
- summary: "sleep.md",
5077
- input: { path: "sleep.md" }
5078
- },
5079
- {
5080
- type: "tool",
5081
- id: "call_1",
5082
- name: "read",
5083
- phase: "done",
5084
- summary: "sleep.md",
5085
- input: { path: "sleep.md" },
5086
- result: "sleep early"
5087
- },
5088
- { type: "text", body: "Here's the summary.", terminal: true },
5089
- { type: "result", ok: true }
5090
- ]
5091
- },
5092
- {
5093
- // A streaming provider's tool input arrives late. opencode re-emits the tool part as a full
5094
- // snapshot as the args parse: DeepSeek sends `input: {}` on the early `pending`/`running` frame
5095
- // and the full args on a later one (captured on opencode 1.17: `pending {}` → `running {full}` →
5096
- // `completed {full}`). `start` fires off the first (empty) frame — the live card — but `done`
5097
- // (and the row it upserts) must carry the latest non-empty input and its summary. Every other
5098
- // tool fixture bakes the full input into the first snapshot, so only this one catches it.
5099
- name: "streaming tool input \u2014 late args refresh the done event (CT301)",
5100
- request: makeRequest2(),
5101
- nativeStream: [
5102
- toolPart("t1", "cabane_list", { status: "pending", input: {} }, "call_1"),
5103
- toolPart(
5104
- "t1",
5105
- "cabane_list",
5106
- { status: "running", input: { path: "/product/tasks" } },
5107
- "call_1"
5108
- ),
5109
- toolPart(
5110
- "t1",
5111
- "cabane_list",
5112
- { status: "completed", input: { path: "/product/tasks" }, output: "CT301.md" },
5113
- "call_1"
5114
- ),
5115
- idle()
5116
- ],
5117
- expected: [
5118
- sessionEvent2(NEW_SESSION_ID),
5119
- // start: fires off the first (empty) snapshot — empty input + summary, by design.
5120
- {
5121
- type: "tool",
5122
- id: "call_1",
5123
- name: "cabane_list",
5124
- phase: "start",
5125
- summary: "",
5126
- input: {},
5127
- mcpServer: "cabane"
5128
- },
5129
- // done: carries the full args captured from the later snapshot, and the
5130
- // summary recomputed from them — NOT the first-seen `{}`.
5131
- {
5132
- type: "tool",
5133
- id: "call_1",
5134
- name: "cabane_list",
5135
- phase: "done",
5136
- summary: "/product/tasks",
5137
- input: { path: "/product/tasks" },
5138
- result: "CT301.md",
5139
- mcpServer: "cabane"
5140
- },
5141
- { type: "result", ok: true }
5142
- ]
5143
- },
5144
- {
5145
- // The turn-control server's `answer` (CT1973). opencode names an MCP tool `<server>_<tool>`,
5146
- // so the installed keys split the raw id: `cabane_companion_answer` is the turn-control
5147
- // server's, never the workspace server's `companion_answer`. The pump binds on that.
5148
- name: "turn-control answer is read as the turn-control server\u2019s, not cabane\u2019s",
5149
- request: makeRequest2({
5150
- cabane: {
5151
- mcpUrl: "https://cabane.test/api/mcp",
5152
- turnControlUrl: "https://cabane.test/api/turn-control",
5153
- bearer: "tok",
5154
- activeConversationId: "conv"
5155
- }
5156
- }),
5157
- nativeStream: [
5158
- textPart("p1", "On it."),
5159
- toolPart("t1", "cabane_companion_answer", { status: "running", input: {} }, "call_1"),
5160
- toolPart(
5161
- "t1",
5162
- "cabane_companion_answer",
5163
- { status: "completed", input: {}, output: '{"ok":true,"kind":"answer"}' },
5164
- "call_1"
5165
- ),
5166
- textPart("p2", "The answer."),
5167
- idle()
5168
- ],
5169
- expected: [
5170
- sessionEvent2(NEW_SESSION_ID),
5171
- { type: "text", body: "On it.", terminal: false },
5172
- {
5173
- type: "tool",
5174
- id: "call_1",
5175
- name: "cabane_companion_answer",
5176
- phase: "start",
5177
- summary: "",
5178
- input: {},
5179
- mcpServer: "cabane_companion"
5180
- },
5181
- {
5182
- type: "tool",
5183
- id: "call_1",
5184
- name: "cabane_companion_answer",
5185
- phase: "done",
5186
- summary: "",
5187
- input: {},
5188
- result: '{"ok":true,"kind":"answer"}',
5189
- mcpServer: "cabane_companion"
5190
- },
5191
- { type: "text", body: "The answer.", terminal: true },
5192
- { type: "result", ok: true }
5193
- ]
5194
- },
5195
- {
5196
- // Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
5197
- // the closing reply — no final text, no `result` event (the server writes the stopped marker).
5198
- name: "cancel mid-stream",
5199
- request: makeRequest2(),
5200
- nativeStream: [
5201
- textPart("p1", "Working on it."),
5202
- toolPart("t1", "list", { status: "running", input: {} }, "call_1"),
5203
- toolPart("t1", "list", { status: "completed", input: {}, output: "ok" }, "call_1"),
5204
- ABORT_SENTINEL2,
5205
- textPart("p2", "This reply must never be emitted."),
5206
- idle()
5207
- ],
5208
- expected: [
5209
- sessionEvent2(NEW_SESSION_ID),
5210
- { type: "text", body: "Working on it.", terminal: false },
5211
- { type: "tool", id: "call_1", name: "list", phase: "start", summary: "", input: {} },
5212
- {
5213
- type: "tool",
5214
- id: "call_1",
5215
- name: "list",
5216
- phase: "done",
5217
- summary: "",
5218
- input: {},
5219
- result: "ok"
5220
- }
5221
- ]
5222
- },
5223
- {
5224
- // An error result: partial narration lands as `progress` (terminal:false),
5225
- // and the `result` event carries the machine reason.
5226
- name: "error result",
5227
- request: makeRequest2(),
5228
- nativeStream: [textPart("p1", "Partial work."), errored("provider exploded")],
5229
- expected: [
5230
- sessionEvent2(NEW_SESSION_ID),
5231
- { type: "text", body: "Partial work.", terminal: false },
5232
- { type: "runtime_notice", body: "provider exploded" },
5233
- { type: "result", ok: false, reason: "error:provider exploded" }
5234
- ]
5235
- },
5236
- {
5237
- // A provider 429 throttle. The wrapped `RateLimitError` is unwrapped by `readSessionError` and
5238
- // text-classified: a bare "rate limit (429)" with no cap noun is the throttle (`rate_limited`),
5239
- // not the account cap.
5240
- name: "provider 429 \u2192 rate_limited (throttle, no reset)",
5241
- request: makeRequest2(),
5242
- nativeStream: [
5243
- {
5244
- type: "session.error",
5245
- properties: { error: { name: "RateLimitError", message: "rate limit exceeded (429)" } }
5246
- }
5247
- ],
5248
- expected: [
5249
- sessionEvent2(NEW_SESSION_ID),
5250
- { type: "runtime_notice", body: "RateLimitError: rate limit exceeded (429)" },
5251
- { type: "result", ok: false, reason: "rate_limited" }
5252
- ]
5253
- },
5254
- {
5255
- // A subscription cap surfaced as text ("usage limit reached") → `usage_capped`. opencode carries
5256
- // no reset hint, so it's reset-less (the server says "try again later").
5257
- name: "usage cap text \u2192 usage_capped (no reset)",
5258
- request: makeRequest2(),
5259
- nativeStream: [
5260
- {
5261
- type: "session.error",
5262
- properties: { error: { name: "Error", message: "Claude usage limit reached" } }
5263
- }
5264
- ],
5265
- expected: [
5266
- sessionEvent2(NEW_SESSION_ID),
5267
- { type: "runtime_notice", body: "Claude usage limit reached" },
5268
- { type: "result", ok: false, reason: "usage_capped" }
5269
- ]
5270
- },
5271
- {
5272
- // A transient provider outage (5xx / overload) surfaced as text → `server_error`, never the cap.
5273
- name: "provider overload text \u2192 server_error",
5274
- request: makeRequest2(),
5275
- nativeStream: [
5276
- {
5277
- type: "session.error",
5278
- properties: {
5279
- error: { name: "Error", message: "AI_APICallError: overloaded_error (529)" }
5280
- }
5281
- }
5282
- ],
5283
- expected: [
5284
- sessionEvent2(NEW_SESSION_ID),
5285
- { type: "runtime_notice", body: "AI_APICallError: overloaded_error (529)" },
5286
- { type: "result", ok: false, reason: "server_error" }
5287
- ]
5288
- },
5289
- {
5290
- // An expired or invalid credential classifies as `auth_expired`.
5291
- name: "auth failure \u2192 auth_expired",
5292
- request: makeRequest2(),
5293
- nativeStream: [
5294
- {
5295
- type: "session.error",
5296
- properties: { error: { name: "AuthenticationError", message: "HTTP 401 Unauthorized" } }
5297
- }
5298
- ],
5299
- expected: [
5300
- sessionEvent2(NEW_SESSION_ID),
5301
- { type: "runtime_notice", body: "AuthenticationError: HTTP 401 Unauthorized" },
5302
- { type: "result", ok: false, reason: "auth_expired" }
5303
- ]
5304
- },
5305
- {
5306
- // An unclassified failure stays informative: its provider text is visible, and the raw machine
5307
- // reason is unchanged.
5308
- name: "unclassified failure keeps a runtime notice and raw reason",
5309
- request: makeRequest2(),
5310
- nativeStream: [errored("something local broke")],
5311
- expected: [
5312
- sessionEvent2(NEW_SESSION_ID),
5313
- { type: "runtime_notice", body: "something local broke" },
5314
- { type: "result", ok: false, reason: "error:something local broke" }
5315
- ]
5316
- },
5317
- {
5318
- name: "runtime notice is trimmed and bounded to 200 characters",
5319
- request: makeRequest2(),
5320
- nativeStream: [errored(` ${BOUNDED_NOTICE}${"y".repeat(20)} `)],
5321
- expected: [
5322
- sessionEvent2(NEW_SESSION_ID),
5323
- { type: "runtime_notice", body: BOUNDED_NOTICE },
5324
- { type: "result", ok: false, reason: "usage_capped" }
5325
- ]
5326
- },
5327
- {
5328
- // Empty, whitespace-only and bare status-token failures still set the machine reason but add no
5329
- // empty or unhelpful transcript notice.
5330
- name: "empty failure text emits no runtime notice",
5331
- request: makeRequest2(),
5332
- nativeStream: [errored("")],
5333
- expected: [
5334
- sessionEvent2(NEW_SESSION_ID),
5335
- { type: "result", ok: false, reason: "error:unknown" }
5336
- ]
5337
- },
5338
- {
5339
- name: "whitespace-only failure text emits no runtime notice",
5340
- request: makeRequest2(),
5341
- nativeStream: [errored(" ")],
5342
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "error: " }]
5343
- },
5344
- {
5345
- name: "bare status token emits no runtime notice",
5346
- request: makeRequest2(),
5347
- nativeStream: [errored("429")],
5348
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "rate_limited" }]
5349
- },
5350
- {
5351
- name: "single-word provider sentence emits a runtime notice",
5352
- request: makeRequest2(),
5353
- nativeStream: [errored("Unavailable.")],
5354
- expected: [
5355
- sessionEvent2(NEW_SESSION_ID),
5356
- { type: "runtime_notice", body: "Unavailable." },
5357
- { type: "result", ok: false, reason: "error:Unavailable." }
5358
- ]
5359
- },
5360
- {
5361
- // The zero-content success the claude-code suite pins, in opencode's vocabulary — the session
5362
- // goes idle having emitted no part. No opencode sample has been seen; the fixture exists because
5363
- // the host's classification is runtime-neutral, so a runtime that can produce this envelope must
5364
- // be shown producing it in the shape the classifier reads.
5365
- name: "zero-content success (empty runtime result)",
5366
- request: makeRequest2(),
5367
- nativeStream: [idle()],
5368
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: true }]
5369
- },
5370
- {
5371
- // Empty-final: a clean turn that ended on a tool call with no closing text.
5372
- // The adapter emits NO final text — empty-final promotion is host/pump
5373
- // territory (this fixture pins that boundary).
5374
- name: "empty-final (no closing text)",
5375
- request: makeRequest2(),
5376
- nativeStream: [
5377
- toolPart("t1", "write", { status: "running", input: { path: "a.md" } }, "call_1"),
5378
- toolPart("t1", "write", { status: "completed", input: { path: "a.md" } }, "call_1"),
5379
- idle()
5380
- ],
5381
- expected: [
5382
- sessionEvent2(NEW_SESSION_ID),
5383
- {
5384
- type: "tool",
5385
- id: "call_1",
5386
- name: "write",
5387
- phase: "start",
5388
- summary: "a.md",
5389
- input: { path: "a.md" }
5390
- },
5391
- {
5392
- type: "tool",
5393
- id: "call_1",
5394
- name: "write",
5395
- phase: "done",
5396
- summary: "a.md",
5397
- input: { path: "a.md" },
5398
- result: void 0
5399
- },
5400
- { type: "result", ok: true }
5401
- ]
5402
- },
5403
- {
5404
- // Thinking: a reasoning part seals as a transient `thinking` when the reply
5405
- // text supersedes it; the text is the final.
5406
- name: "thinking then reply",
5407
- request: makeRequest2(),
5408
- nativeStream: [
5409
- reasoningPart("r1", "Let me check the notes."),
5410
- textPart("p1", "All set."),
5411
- idle()
5412
- ],
5413
- expected: [
5414
- sessionEvent2(NEW_SESSION_ID),
5415
- { type: "thinking", text: "Let me check the notes." },
5416
- { type: "text", body: "All set.", terminal: true },
5417
- { type: "result", ok: true }
5418
- ]
5419
- },
5420
- {
5421
- // The stored state matches the directory, so the adapter resumes its id and the transport reports
5422
- // `created:false` on the same id — no session event fires. Resume is what carries cross-turn
5423
- // memory.
5424
- name: "session resume (no re-emit on echo)",
5425
- request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
5426
- nativeStream: [textPart("p1", "Back again."), idle()],
5427
- expected: [
5428
- { type: "text", body: "Back again.", terminal: true },
5429
- { type: "result", ok: true }
5430
- ]
5431
- },
5432
- {
5433
- // On resume, as on a fresh turn, the decoder learns the user-message id from the stream:
5434
- // `message.updated` user frame precedes the echoed part — and drops the echo, so the injected
5435
- // per-turn context can't leak into Steps. Only the reply surfaces; no session event.
5436
- name: "session resume \u2014 prompt echo filtered via stream-derived user id",
5437
- request: makeRequest2({ session: encodeSession2({ sessionId: "s1", directory: DIR }) }),
5438
- nativeStream: [
5439
- messageUpdated("user", "msg_u1"),
5440
- echoedUserPart("up1", "msg_u1", "INJECTED CONTEXT \u2014 must not leak"),
5441
- messageUpdated("assistant", "msg_a1"),
5442
- echoedUserPart("ap1", "msg_a1", "The real reply."),
5443
- idle()
5444
- ],
5445
- expected: [
5446
- { type: "text", body: "The real reply.", terminal: true },
5447
- { type: "result", ok: true }
5448
- ]
5449
- },
5450
- {
5451
- // Directory-mismatch degrade: the stored state was created under another directory, so the
5452
- // adapter starts fresh — the transport creates a new session, and the session event records the
5453
- // current directory.
5454
- name: "session directory-mismatch degrades to fresh",
5455
- request: makeRequest2({ session: encodeSession2({ sessionId: "old", directory: "/env/gone" }) }),
5456
- nativeStream: [textPart("p1", "Fresh start."), idle()],
5457
- expected: [
5458
- // The degrade is flagged on the session event so the host relays it on settle and the server
5459
- // rewinds the catch-up mark, recovering context the discarded session dropped.
5460
- sessionEvent2(NEW_SESSION_ID, DIR, true),
5461
- { type: "text", body: "Fresh start.", terminal: true },
5462
- { type: "result", ok: true }
5463
- ]
5464
- }
5465
- ];
5466
- }
5467
- });
5468
-
5469
4431
  // packages/agent-runtime/src/codex/addendum.ts
5470
4432
  var CODEX_ADDENDUM;
5471
4433
  var init_addendum3 = __esm({
@@ -5903,7 +4865,7 @@ var init_loop3 = __esm({
5903
4865
  });
5904
4866
 
5905
4867
  // packages/agent-runtime/src/codex/policy.ts
5906
- import { z as z12 } from "zod";
4868
+ import { z as z11 } from "zod";
5907
4869
  function codexSandboxMode(local) {
5908
4870
  return local.codex?.configSetsSandboxMode ? null : "danger-full-access";
5909
4871
  }
@@ -5916,8 +4878,8 @@ var init_policy3 = __esm({
5916
4878
  "packages/agent-runtime/src/codex/policy.ts"() {
5917
4879
  "use strict";
5918
4880
  CODEX_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
5919
- codexDialectSchema = z12.object({
5920
- modelReasoningEffort: z12.enum(CODEX_REASONING_EFFORTS).optional()
4881
+ codexDialectSchema = z11.object({
4882
+ modelReasoningEffort: z11.enum(CODEX_REASONING_EFFORTS).optional()
5921
4883
  }).loose();
5922
4884
  }
5923
4885
  });
@@ -6245,7 +5207,7 @@ function createCodexAdapter(deps = {}) {
6245
5207
  let threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
6246
5208
  if ("fresh" in decision && decision.reason === "cwd_mismatch") {
6247
5209
  deps.onWarn?.(
6248
- "codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread (SJ527-analogue guard)",
5210
+ "codex adapter: stored session directory no longer matches the current environment \u2014 starting a fresh thread",
6249
5211
  { directory }
6250
5212
  );
6251
5213
  }
@@ -6290,7 +5252,7 @@ function createCodexAdapter(deps = {}) {
6290
5252
  if (imageUrls.length > 0) {
6291
5253
  if (!deps.prepareImages) {
6292
5254
  deps.onWarn?.(
6293
- "codex adapter: no image preparer wired on this host \u2014 the turn runs without its images (CT1757)",
5255
+ "codex adapter: no image preparer wired on this host \u2014 the turn runs without its images",
6294
5256
  { images: imageUrls.length }
6295
5257
  );
6296
5258
  } else {
@@ -6299,7 +5261,7 @@ function createCodexAdapter(deps = {}) {
6299
5261
  } catch (err) {
6300
5262
  if (!signal.aborted) {
6301
5263
  deps.onWarn?.(
6302
- "codex adapter: preparing the images failed \u2014 the turn runs without them (CT1757)",
5264
+ "codex adapter: preparing the images failed \u2014 the turn runs without them",
6303
5265
  { err: err instanceof Error ? err.message : String(err) }
6304
5266
  );
6305
5267
  }
@@ -6308,1096 +5270,101 @@ function createCodexAdapter(deps = {}) {
6308
5270
  if (signal.aborted) return;
6309
5271
  }
6310
5272
  for (; ; ) {
6311
- const spec = buildRunSpec2(req, resumeThreadId, threadStanding, images?.paths ?? []);
6312
- if (spec.model === null) {
6313
- deps.onWarn?.("Codex is using its default model because no model was resolved", {
6314
- model: req.config.model
6315
- });
6316
- }
6317
- let emitted = false;
6318
- try {
6319
- const result = await transport.run(spec, signal);
6320
- if (signal.aborted) return;
6321
- for await (const event of decodeCodexStream(result.events, {
6322
- signal,
6323
- cwd: req.local.cwd,
6324
- resumedThreadId: resumeThreadId,
6325
- degraded,
6326
- // Which segments the thread carries once this turn lands. A segment sent is that one —
6327
- // the later copy is what the model reads. One withheld is what the thread had. Read off
6328
- // the spec, so the record can't disagree with what was sent.
6329
- promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
6330
- conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
6331
- compactions,
6332
- // What the thread held before this turn, for the write at `thread.started`.
6333
- heldStanding: {
6334
- promptFingerprint: threadStanding?.core ?? null,
6335
- conversationFingerprint: threadStanding?.conversation ?? null,
6336
- compactions: threadStanding ? compactions : null
6337
- },
6338
- storedState: req.session,
6339
- usageBaseline: threadUsageBaseline,
6340
- // Codex reports no model in-stream, so record what the thread was started with (null on the
6341
- // default token). Same for the reasoning effort.
6342
- resolvedModel: spec.model,
6343
- resolvedReasoningEffort: spec.modelReasoningEffort ?? null
6344
- })) {
6345
- emitted = true;
6346
- yield event;
6347
- }
6348
- return;
6349
- } catch (err) {
6350
- if (resumeThreadId === null || emitted || signal.aborted || !isMissingRollout(err)) {
6351
- throw err;
6352
- }
6353
- deps.onWarn?.(
6354
- "codex adapter: the stored thread has no rollout on this host \u2014 starting a fresh thread",
6355
- { threadId: resumeThreadId }
6356
- );
6357
- resumeThreadId = null;
6358
- threadUsageBaseline = null;
6359
- threadStanding = null;
6360
- compactions = 0;
6361
- req = { ...req, session: null };
6362
- degraded = true;
6363
- if (hooks?.fresh) {
6364
- const recomposed = await hooks.fresh("session_start_failed");
6365
- if (signal.aborted) return;
6366
- if (recomposed) {
6367
- req = { ...req, ...recomposed, session: null };
6368
- degraded = false;
6369
- }
6370
- }
6371
- }
6372
- }
6373
- } finally {
6374
- if (images) {
6375
- try {
6376
- await images.cleanup();
6377
- } catch (err) {
6378
- deps.onWarn?.("codex adapter: image cleanup failed (CT1757)", {
6379
- err: err instanceof Error ? err.message : String(err)
6380
- });
6381
- }
6382
- }
6383
- }
6384
- }
6385
- };
6386
- }
6387
- var codexAdapter;
6388
- var init_codex = __esm({
6389
- "packages/agent-runtime/src/codex/index.ts"() {
6390
- "use strict";
6391
- init_addendum3();
6392
- init_loop3();
6393
- init_policy3();
6394
- init_run_spec2();
6395
- init_session3();
6396
- init_transport2();
6397
- init_addendum3();
6398
- init_loop3();
6399
- init_policy3();
6400
- init_model3();
6401
- init_run_spec2();
6402
- init_transport2();
6403
- init_session3();
6404
- init_events2();
6405
- codexAdapter = createCodexAdapter();
6406
- }
6407
- });
6408
-
6409
- // packages/agent-runtime/src/codex/conformance.ts
6410
- function makeRequest3(overrides2 = {}) {
6411
- return {
6412
- systemPrompt: PROMPT,
6413
- prompt: "hi there",
6414
- content: [{ type: "text", text: "hi there" }],
6415
- // The resolved model and reasoning effort ride the terminal `result` off the run spec's model.
6416
- // Behavioural fixtures use the model-less token (no `resolvedModel` emitted); a dedicated capture
6417
- // fixture sets a real model and effort.
6418
- config: { model: null },
6419
- session: null,
6420
- cabane: { mcpUrl: "https://cabane.test/api/mcp", bearer: "tok", activeConversationId: "conv" },
6421
- local: { cwd: DIR2 },
6422
- extra: { mcpServers: {} },
6423
- ...overrides2
6424
- };
6425
- }
6426
- var ABORT_SENTINEL3, NEW_THREAD_ID, BOUNDED_NOTICE2, DIR2, PROMPT, threadStarted, agentMessage, reasoning, toolFrame, turnCompleted, turnFailed, fatalError, errorItem, OP_SUCCESS, OP_FAILURE, startedSession, closingSession, CODEX_CONFORMANCE_FIXTURES;
6427
- var init_conformance4 = __esm({
6428
- "packages/agent-runtime/src/codex/conformance.ts"() {
6429
- "use strict";
6430
- init_codex();
6431
- init_session3();
6432
- ABORT_SENTINEL3 = { __abortHere: true };
6433
- NEW_THREAD_ID = "th_new";
6434
- BOUNDED_NOTICE2 = "You've hit your usage limit \xB7 ".padEnd(200, "x");
6435
- DIR2 = "/env/here";
6436
- PROMPT = "system";
6437
- threadStarted = (threadId) => ({
6438
- type: "thread.started",
6439
- thread_id: threadId
6440
- });
6441
- agentMessage = (id, text) => ({
6442
- type: "item.completed",
6443
- item: { id, type: "agent_message", text }
6444
- });
6445
- reasoning = (id, text) => ({
6446
- type: "item.completed",
6447
- item: { id, type: "reasoning", text }
6448
- });
6449
- toolFrame = (evType, item) => ({ type: evType, item });
6450
- turnCompleted = (usage) => ({
6451
- type: "turn.completed",
6452
- ...usage ? { usage } : {}
6453
- });
6454
- turnFailed = (message2) => ({
6455
- type: "turn.failed",
6456
- error: { message: message2 }
6457
- });
6458
- fatalError = (message2) => ({ type: "error", message: message2 });
6459
- errorItem = (id, message2) => ({
6460
- type: "item.completed",
6461
- item: { id, type: "error", message: message2 }
6462
- });
6463
- OP_SUCCESS = { ok: true, result: { source: "native" } };
6464
- OP_FAILURE = {
6465
- ok: false,
6466
- error: {
6467
- kind: "output_too_large",
6468
- code: "output_too_large",
6469
- message: "files.read returned more than the result ceiling"
6470
- }
6471
- };
6472
- startedSession = (threadId, cwd = DIR2, degraded = false) => ({
6473
- type: "session",
6474
- state: encodeSession3({ threadId, cwd, promptFingerprint: null, usageBaseline: null }),
6475
- ...degraded ? { degraded: true } : {}
6476
- });
6477
- closingSession = (threadId, usageBaseline = null) => ({
6478
- type: "session",
6479
- state: encodeSession3({
6480
- threadId,
6481
- cwd: DIR2,
6482
- promptFingerprint: fingerprintPrompt(PROMPT),
6483
- usageBaseline,
6484
- compactions: 0
6485
- })
6486
- });
6487
- CODEX_CONFORMANCE_FIXTURES = [
6488
- {
6489
- // A plain text reply: the held agent message seals as the terminal final at `turn.completed`;
6490
- // usage rides the `result`.
6491
- //
6492
- // The counters nest — `cached_input_tokens` is a slice of `input_tokens`,
6493
- // `reasoning_output_tokens` a slice of `output_tokens` — so input is 100 (not 120) and output 50
6494
- // (not 60), with the cache read broken out beside them.
6495
- name: "clean turn",
6496
- request: makeRequest3(),
6497
- nativeStream: [
6498
- threadStarted(NEW_THREAD_ID),
6499
- agentMessage("m1", "Hello there."),
6500
- turnCompleted({
6501
- input_tokens: 100,
6502
- cached_input_tokens: 20,
6503
- cache_write_input_tokens: 5,
6504
- output_tokens: 50,
6505
- reasoning_output_tokens: 10
6506
- })
6507
- ],
6508
- expected: [
6509
- startedSession(NEW_THREAD_ID),
6510
- { type: "text", body: "Hello there.", terminal: true },
6511
- // The closing session write — the thread's ending totals, recorded as the baseline the next turn
6512
- // subtracts. A fresh thread, so the `result` usage below is the frame verbatim.
6513
- closingSession(NEW_THREAD_ID, {
6514
- inputTokens: 100,
6515
- outputTokens: 50,
6516
- cacheReadTokens: 20,
6517
- cacheCreationTokens: 5
6518
- }),
6519
- {
6520
- type: "result",
6521
- ok: true,
6522
- usage: {
6523
- inputTokens: 100,
6524
- outputTokens: 50,
6525
- cacheReadTokens: 20,
6526
- cacheCreationTokens: 5
6527
- }
6528
- }
6529
- ]
6530
- },
6531
- {
6532
- // Narration + an MCP tool call + result + closing reply. The narration message
6533
- // is sealed as interim (a tool follows it); the trailing message is the final.
6534
- name: "tool turn",
6535
- request: makeRequest3(),
6536
- nativeStream: [
6537
- threadStarted(NEW_THREAD_ID),
6538
- agentMessage("m1", "Reading your notes."),
6539
- toolFrame("item.started", {
6540
- id: "t1",
6541
- type: "mcp_tool_call",
6542
- server: "cabane",
6543
- tool: "read",
6544
- arguments: { path: "sleep.md" },
6545
- status: "in_progress"
6546
- }),
6547
- toolFrame("item.completed", {
6548
- id: "t1",
6549
- type: "mcp_tool_call",
6550
- server: "cabane",
6551
- tool: "read",
6552
- arguments: { path: "sleep.md" },
6553
- status: "completed",
6554
- result: { content: [{ type: "text", text: "sleep early" }] }
6555
- }),
6556
- agentMessage("m2", "Here's the summary."),
6557
- turnCompleted()
6558
- ],
6559
- expected: [
6560
- startedSession(NEW_THREAD_ID),
6561
- { type: "text", body: "Reading your notes.", terminal: false },
6562
- {
6563
- type: "tool",
6564
- id: "t1",
6565
- name: "read",
6566
- mcpServer: "cabane",
6567
- phase: "start",
6568
- summary: "sleep.md",
6569
- input: { path: "sleep.md" }
6570
- },
6571
- {
6572
- type: "tool",
6573
- id: "t1",
6574
- name: "read",
6575
- mcpServer: "cabane",
6576
- phase: "done",
6577
- summary: "sleep.md",
6578
- input: { path: "sleep.md" },
6579
- result: [{ type: "text", text: "sleep early" }]
6580
- },
6581
- { type: "text", body: "Here's the summary.", terminal: true },
6582
- closingSession(NEW_THREAD_ID),
6583
- { type: "result", ok: true }
6584
- ]
6585
- },
6586
- {
6587
- // The recorded MCP success shape: Codex exposes the text copy and the native object together;
6588
- // normalization must keep the object.
6589
- name: "MCP success prefers native structured content",
6590
- request: makeRequest3(),
6591
- nativeStream: [
6592
- threadStarted(NEW_THREAD_ID),
6593
- toolFrame("item.started", {
6594
- id: "query-success",
6595
- type: "mcp_tool_call",
6596
- server: "cabane",
6597
- tool: "query",
6598
- arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
6599
- status: "in_progress"
6600
- }),
6601
- toolFrame("item.completed", {
6602
- id: "query-success",
6603
- type: "mcp_tool_call",
6604
- server: "cabane",
6605
- tool: "query",
6606
- arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
6607
- status: "completed",
6608
- result: {
6609
- content: [{ type: "text", text: JSON.stringify(OP_SUCCESS) }],
6610
- structured_content: OP_SUCCESS
6611
- }
6612
- }),
6613
- turnCompleted()
6614
- ],
6615
- expected: [
6616
- startedSession(NEW_THREAD_ID),
6617
- {
6618
- type: "tool",
6619
- id: "query-success",
6620
- name: "query",
6621
- mcpServer: "cabane",
6622
- phase: "start",
6623
- summary: "",
6624
- input: { op: "files.read", args: { path: "/ABOUT.md" } }
6625
- },
6626
- {
6627
- type: "tool",
6628
- id: "query-success",
6629
- name: "query",
6630
- mcpServer: "cabane",
6631
- phase: "done",
6632
- summary: "",
6633
- input: { op: "files.read", args: { path: "/ABOUT.md" } },
6634
- result: OP_SUCCESS
6635
- },
6636
- closingSession(NEW_THREAD_ID),
6637
- { type: "result", ok: true }
6638
- ]
6639
- },
6640
- {
6641
- // MCP `isError` keeps the failed phase, while the raw camel-case structured payload survives
6642
- // instead of the generic `error` token.
6643
- name: "failed MCP call preserves structured receipt while remaining error phase",
6644
- request: makeRequest3(),
6645
- nativeStream: [
6646
- threadStarted(NEW_THREAD_ID),
6647
- toolFrame("item.started", {
6648
- id: "query-failed",
6649
- type: "mcp_tool_call",
6650
- server: "cabane",
6651
- tool: "query",
6652
- arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
6653
- status: "in_progress"
6654
- }),
6655
- toolFrame("item.completed", {
6656
- id: "query-failed",
6657
- type: "mcp_tool_call",
6658
- server: "cabane",
6659
- tool: "query",
6660
- arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
6661
- status: "failed",
6662
- result: {
6663
- content: [{ type: "text", text: JSON.stringify(OP_FAILURE) }],
6664
- structuredContent: OP_FAILURE,
6665
- isError: true
6666
- },
6667
- error: { message: "error" }
6668
- }),
6669
- turnCompleted()
6670
- ],
6671
- expected: [
6672
- startedSession(NEW_THREAD_ID),
6673
- {
6674
- type: "tool",
6675
- id: "query-failed",
6676
- name: "query",
6677
- mcpServer: "cabane",
6678
- phase: "start",
6679
- summary: "",
6680
- input: { op: "files.read", args: { path: "/ABOUT.md" } }
6681
- },
6682
- {
6683
- type: "tool",
6684
- id: "query-failed",
6685
- name: "query",
6686
- mcpServer: "cabane",
6687
- phase: "error",
6688
- summary: "",
6689
- input: { op: "files.read", args: { path: "/ABOUT.md" } },
6690
- result: OP_FAILURE
6691
- },
6692
- closingSession(NEW_THREAD_ID),
6693
- { type: "result", ok: true }
6694
- ]
6695
- },
6696
- {
6697
- // The turn-control server's `answer` (CT1973): the tool event carries the server Codex names
6698
- // beside the tool, which is what the pump binds the declaration to. A non-cabane server keeps
6699
- // its prefix on the card as before.
6700
- name: "turn-control answer carries its server",
6701
- request: makeRequest3(),
6702
- nativeStream: [
6703
- threadStarted(NEW_THREAD_ID),
6704
- agentMessage("m1", "On it."),
6705
- toolFrame("item.completed", {
6706
- id: "answer-1",
6707
- type: "mcp_tool_call",
6708
- server: "cabane_companion",
6709
- tool: "answer",
6710
- arguments: {},
6711
- status: "completed",
6712
- result: { content: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }] }
6713
- }),
6714
- agentMessage("m2", "The answer."),
6715
- turnCompleted()
6716
- ],
6717
- expected: [
6718
- startedSession(NEW_THREAD_ID),
6719
- { type: "text", body: "On it.", terminal: false },
6720
- {
6721
- type: "tool",
6722
- id: "answer-1",
6723
- name: "cabane_companion.answer",
6724
- mcpServer: "cabane_companion",
6725
- phase: "start",
6726
- summary: "",
6727
- input: {}
6728
- },
6729
- {
6730
- type: "tool",
6731
- id: "answer-1",
6732
- name: "cabane_companion.answer",
6733
- mcpServer: "cabane_companion",
6734
- phase: "done",
6735
- summary: "",
6736
- input: {},
6737
- result: [{ type: "text", text: '{"ok":true,"kind":"answer"}' }]
6738
- },
6739
- { type: "text", body: "The answer.", terminal: true },
6740
- closingSession(NEW_THREAD_ID),
6741
- { type: "result", ok: true }
6742
- ]
6743
- },
6744
- {
6745
- // A transport/tool failure with no MCP result payload still uses the real adapter error message,
6746
- // never `undefined`.
6747
- name: "failed MCP call without result keeps error message fallback",
6748
- request: makeRequest3(),
6749
- nativeStream: [
6750
- threadStarted(NEW_THREAD_ID),
6751
- toolFrame("item.completed", {
6752
- id: "mcp-failed",
6753
- type: "mcp_tool_call",
6754
- server: "other",
6755
- tool: "lookup",
6756
- arguments: { q: "receipt" },
6757
- status: "failed",
6758
- result: { content: [], isError: true },
6759
- error: { message: "connection closed" }
6760
- }),
6761
- turnCompleted()
6762
- ],
6763
- expected: [
6764
- startedSession(NEW_THREAD_ID),
6765
- {
6766
- type: "tool",
6767
- id: "mcp-failed",
6768
- name: "other.lookup",
6769
- mcpServer: "other",
6770
- phase: "start",
6771
- summary: "receipt",
6772
- input: { q: "receipt" }
6773
- },
6774
- {
6775
- type: "tool",
6776
- id: "mcp-failed",
6777
- name: "other.lookup",
6778
- mcpServer: "other",
6779
- phase: "error",
6780
- summary: "receipt",
6781
- input: { q: "receipt" },
6782
- result: "connection closed"
6783
- },
6784
- closingSession(NEW_THREAD_ID),
6785
- { type: "result", ok: true }
6786
- ]
6787
- },
6788
- {
6789
- // A connector's structured metadata beside an image-only MCP response: no image/base64 payload
6790
- // enters activity receipts.
6791
- name: "image-only MCP result remains empty despite structured metadata",
6792
- request: makeRequest3(),
6793
- nativeStream: [
6794
- threadStarted(NEW_THREAD_ID),
6795
- toolFrame("item.completed", {
6796
- id: "mcp-image",
6797
- type: "mcp_tool_call",
6798
- server: "other",
6799
- tool: "render",
6800
- arguments: {},
6801
- status: "completed",
6802
- result: {
6803
- content: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }],
6804
- structured_content: { image: "base64-bytes" }
6805
- }
6806
- }),
6807
- turnCompleted()
6808
- ],
6809
- expected: [
6810
- startedSession(NEW_THREAD_ID),
6811
- {
6812
- type: "tool",
6813
- id: "mcp-image",
6814
- name: "other.render",
6815
- mcpServer: "other",
6816
- phase: "start",
6817
- summary: "",
6818
- input: {}
6819
- },
6820
- {
6821
- type: "tool",
6822
- id: "mcp-image",
6823
- name: "other.render",
6824
- mcpServer: "other",
6825
- phase: "done",
6826
- summary: "",
6827
- input: {},
6828
- result: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }]
6829
- },
6830
- closingSession(NEW_THREAD_ID),
6831
- { type: "result", ok: true }
6832
- ]
6833
- },
6834
- {
6835
- // A streaming tool whose args arrive on a later snapshot: `item.started` carries
6836
- // empty args, `item.updated` fills them, `item.completed` finishes. The `start`
6837
- // fires off the first (empty) frame — that's the live card — but the `done` must
6838
- // carry the LATEST non-empty input + its recomputed summary, not the first `{}`.
6839
- name: "streaming tool input \u2014 late args refresh the done event",
6840
- request: makeRequest3(),
6841
- nativeStream: [
6842
- threadStarted(NEW_THREAD_ID),
6843
- toolFrame("item.started", {
6844
- id: "t1",
6845
- type: "mcp_tool_call",
6846
- server: "cabane",
6847
- tool: "list",
6848
- arguments: {},
6849
- status: "in_progress"
6850
- }),
6851
- toolFrame("item.updated", {
6852
- id: "t1",
6853
- type: "mcp_tool_call",
6854
- server: "cabane",
6855
- tool: "list",
6856
- arguments: { path: "/product/tasks" },
6857
- status: "in_progress"
6858
- }),
6859
- toolFrame("item.completed", {
6860
- id: "t1",
6861
- type: "mcp_tool_call",
6862
- server: "cabane",
6863
- tool: "list",
6864
- arguments: { path: "/product/tasks" },
6865
- status: "completed",
6866
- result: { content: [{ type: "text", text: "CT481.md" }] }
6867
- }),
6868
- turnCompleted()
6869
- ],
6870
- expected: [
6871
- startedSession(NEW_THREAD_ID),
6872
- {
6873
- type: "tool",
6874
- id: "t1",
6875
- name: "list",
6876
- mcpServer: "cabane",
6877
- phase: "start",
6878
- summary: "",
6879
- input: {}
6880
- },
6881
- {
6882
- type: "tool",
6883
- id: "t1",
6884
- name: "list",
6885
- mcpServer: "cabane",
6886
- phase: "done",
6887
- summary: "/product/tasks",
6888
- input: { path: "/product/tasks" },
6889
- result: [{ type: "text", text: "CT481.md" }]
6890
- },
6891
- closingSession(NEW_THREAD_ID),
6892
- { type: "result", ok: true }
6893
- ]
6894
- },
6895
- {
6896
- // web_search lifecycle. A `web_search` item carries no `status` — completion is signalled by the
6897
- // frame type (`item.started` → `item.completed`). The `start` fires off the first frame (empty
6898
- // query); the `done` must resolve off `item.completed` with the populated query, not stay
6899
- // "running".
6900
- name: "web_search lifecycle \u2014 completes off frame type, populated query on done",
6901
- request: makeRequest3(),
6902
- nativeStream: [
6903
- threadStarted(NEW_THREAD_ID),
6904
- toolFrame("item.started", { id: "ws1", type: "web_search", query: "" }),
6905
- toolFrame("item.completed", { id: "ws1", type: "web_search", query: "best pizza in nyc" }),
6906
- turnCompleted()
6907
- ],
6908
- expected: [
6909
- startedSession(NEW_THREAD_ID),
6910
- {
6911
- type: "tool",
6912
- id: "ws1",
6913
- name: "web_search",
6914
- phase: "start",
6915
- summary: "",
6916
- input: { query: "" }
6917
- },
6918
- {
6919
- type: "tool",
6920
- id: "ws1",
6921
- name: "web_search",
6922
- phase: "done",
6923
- summary: "best pizza in nyc",
6924
- input: { query: "best pizza in nyc" }
6925
- },
6926
- closingSession(NEW_THREAD_ID),
6927
- { type: "result", ok: true }
6928
- ]
6929
- },
6930
- {
6931
- // A non-text web action (`action.type: "other"`) legitimately completes with an empty query —
6932
- // Codex's own data. It must still resolve to `done`.
6933
- name: "web_search lifecycle \u2014 empty-query completion still resolves to done",
6934
- request: makeRequest3(),
6935
- nativeStream: [
6936
- threadStarted(NEW_THREAD_ID),
6937
- toolFrame("item.started", { id: "ws2", type: "web_search", query: "" }),
6938
- toolFrame("item.completed", { id: "ws2", type: "web_search", query: "" }),
6939
- turnCompleted()
6940
- ],
6941
- expected: [
6942
- startedSession(NEW_THREAD_ID),
6943
- {
6944
- type: "tool",
6945
- id: "ws2",
6946
- name: "web_search",
6947
- phase: "start",
6948
- summary: "",
6949
- input: { query: "" }
6950
- },
6951
- {
6952
- type: "tool",
6953
- id: "ws2",
6954
- name: "web_search",
6955
- phase: "done",
6956
- summary: "",
6957
- input: { query: "" }
6958
- },
6959
- closingSession(NEW_THREAD_ID),
6960
- { type: "result", ok: true }
6961
- ]
6962
- },
6963
- {
6964
- // Highest risk: cancel mid-stream. The abort lands after the tool result; the adapter stops before
6965
- // the closing reply — no final text, no `result` event (the server writes the stopped marker).
6966
- name: "cancel mid-stream",
6967
- request: makeRequest3(),
6968
- nativeStream: [
6969
- threadStarted(NEW_THREAD_ID),
6970
- agentMessage("m1", "Working on it."),
6971
- toolFrame("item.started", {
6972
- id: "t1",
6973
- type: "command_execution",
6974
- command: "ls",
6975
- status: "in_progress"
6976
- }),
6977
- toolFrame("item.completed", {
6978
- id: "t1",
6979
- type: "command_execution",
6980
- command: "ls",
6981
- aggregated_output: "ok",
6982
- exit_code: 0,
6983
- status: "completed"
6984
- }),
6985
- ABORT_SENTINEL3,
6986
- agentMessage("m2", "This reply must never be emitted."),
6987
- turnCompleted()
6988
- ],
6989
- expected: [
6990
- startedSession(NEW_THREAD_ID),
6991
- { type: "text", body: "Working on it.", terminal: false },
6992
- {
6993
- type: "tool",
6994
- id: "t1",
6995
- name: "shell",
6996
- phase: "start",
6997
- summary: "ls",
6998
- input: { command: "ls" }
6999
- },
7000
- {
7001
- type: "tool",
7002
- id: "t1",
7003
- name: "shell",
7004
- phase: "done",
7005
- summary: "ls",
7006
- input: { command: "ls" },
7007
- result: { output: "ok", exit_code: 0 }
7008
- }
7009
- ]
7010
- },
7011
- {
7012
- // A failed turn: partial narration lands as `progress` (terminal:false), and the
7013
- // `result` event carries the machine reason off `turn.failed`.
7014
- name: "turn.failed result",
7015
- request: makeRequest3(),
7016
- nativeStream: [
7017
- threadStarted(NEW_THREAD_ID),
7018
- agentMessage("m1", "Partial work."),
7019
- turnFailed("provider exploded")
7020
- ],
7021
- expected: [
7022
- startedSession(NEW_THREAD_ID),
7023
- { type: "text", body: "Partial work.", terminal: false },
7024
- { type: "runtime_notice", body: "provider exploded" },
7025
- { type: "result", ok: false, reason: "error:provider exploded" }
7026
- ]
7027
- },
7028
- {
7029
- // A fatal top-level `error` frame (distinct from `turn.failed`): held narration
7030
- // seals as interim, then a failed `result`.
7031
- name: "fatal stream error",
7032
- request: makeRequest3(),
7033
- nativeStream: [
7034
- threadStarted(NEW_THREAD_ID),
7035
- agentMessage("m1", "Starting."),
7036
- fatalError("stream died")
7037
- ],
7038
- expected: [
7039
- startedSession(NEW_THREAD_ID),
7040
- { type: "text", body: "Starting.", terminal: false },
7041
- { type: "runtime_notice", body: "stream died" },
7042
- { type: "result", ok: false, reason: "error:stream died" }
7043
- ]
7044
- },
7045
- {
7046
- // The failure text goes through the shared classifier (Codex is text-only), so a provider 429 /
7047
- // outage / auth / cap failure splits into the honest taxonomy. One fixture per kind:
7048
- name: "turn.failed 429 \u2192 rate_limited (throttle)",
7049
- request: makeRequest3(),
7050
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
7051
- expected: [
7052
- startedSession(NEW_THREAD_ID),
7053
- { type: "runtime_notice", body: "429 Too Many Requests" },
7054
- { type: "result", ok: false, reason: "rate_limited" }
7055
- ]
7056
- },
7057
- {
7058
- name: "turn.failed overload \u2192 server_error",
7059
- request: makeRequest3(),
7060
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("503 Service Unavailable")],
7061
- expected: [
7062
- startedSession(NEW_THREAD_ID),
7063
- { type: "runtime_notice", body: "503 Service Unavailable" },
7064
- { type: "result", ok: false, reason: "server_error" }
7065
- ]
7066
- },
7067
- {
7068
- name: "turn.failed usage cap \u2192 usage_capped",
7069
- request: makeRequest3(),
7070
- nativeStream: [
7071
- threadStarted(NEW_THREAD_ID),
7072
- turnFailed("You've hit your session limit \xB7 resets 8:20pm (UTC)")
7073
- ],
7074
- expected: [
7075
- startedSession(NEW_THREAD_ID),
7076
- {
7077
- type: "runtime_notice",
7078
- body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
7079
- },
7080
- { type: "result", ok: false, reason: "usage_capped" }
7081
- ]
7082
- },
7083
- {
7084
- name: "turn.failed auth \u2192 auth_expired",
7085
- request: makeRequest3(),
7086
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("401 Unauthorized: invalid api key")],
7087
- expected: [
7088
- startedSession(NEW_THREAD_ID),
7089
- { type: "runtime_notice", body: "401 Unauthorized: invalid api key" },
7090
- { type: "result", ok: false, reason: "auth_expired" }
7091
- ]
7092
- },
7093
- {
7094
- // A failure text the classifier can't place keeps the raw `error:<text>` reason (the `unknown`
7095
- // fallback).
7096
- name: "turn.failed unclassified \u2192 raw error reason (unchanged)",
7097
- request: makeRequest3(),
7098
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
7099
- expected: [
7100
- startedSession(NEW_THREAD_ID),
7101
- { type: "runtime_notice", body: "something local broke" },
7102
- { type: "result", ok: false, reason: "error:something local broke" }
7103
- ]
7104
- },
7105
- {
7106
- name: "runtime notice is trimmed and bounded to 200 characters",
7107
- request: makeRequest3(),
7108
- nativeStream: [
7109
- threadStarted(NEW_THREAD_ID),
7110
- turnFailed(` ${BOUNDED_NOTICE2}${"y".repeat(20)} `)
7111
- ],
7112
- expected: [
7113
- startedSession(NEW_THREAD_ID),
7114
- { type: "runtime_notice", body: BOUNDED_NOTICE2 },
7115
- { type: "result", ok: false, reason: "usage_capped" }
7116
- ]
7117
- },
7118
- {
7119
- // Empty, whitespace-only and bare status-token failures still set the machine reason but add no
7120
- // empty or unhelpful transcript notice.
7121
- name: "empty failure text emits no runtime notice",
7122
- request: makeRequest3(),
7123
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
7124
- expected: [
7125
- startedSession(NEW_THREAD_ID),
7126
- { type: "result", ok: false, reason: "error:unknown" }
7127
- ]
7128
- },
7129
- {
7130
- name: "whitespace-only failure text emits no runtime notice",
7131
- request: makeRequest3(),
7132
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed(" ")],
7133
- expected: [startedSession(NEW_THREAD_ID), { type: "result", ok: false, reason: "error: " }]
7134
- },
7135
- {
7136
- name: "bare status token emits no runtime notice",
7137
- request: makeRequest3(),
7138
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429")],
7139
- expected: [
7140
- startedSession(NEW_THREAD_ID),
7141
- { type: "result", ok: false, reason: "rate_limited" }
7142
- ]
7143
- },
7144
- {
7145
- name: "single-word provider sentence emits a runtime notice",
7146
- request: makeRequest3(),
7147
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("Unavailable.")],
7148
- expected: [
7149
- startedSession(NEW_THREAD_ID),
7150
- { type: "runtime_notice", body: "Unavailable." },
7151
- { type: "result", ok: false, reason: "error:Unavailable." }
7152
- ]
7153
- },
7154
- {
7155
- // Fail loud on unknown model metadata. An unknown `--model` makes Codex emit an `error` item
7156
- // ("… Defaulting to fallback metadata …") and misbehave (the spurious tool call scripted after it
7157
- // must never be emitted). The adapter ends the turn when the signal lands, with a machine reason.
7158
- name: "unknown model metadata \u2192 fail loud (guard)",
7159
- request: makeRequest3(),
7160
- nativeStream: [
7161
- threadStarted(NEW_THREAD_ID),
7162
- errorItem(
7163
- "e0",
7164
- "Model metadata for `gpt-5-codex` not found. Defaulting to fallback metadata; this can degrade performance and cause issues."
7165
- ),
7166
- // Everything after the guard fires must be dropped (the haywire the guard prevents).
7167
- toolFrame("item.started", {
7168
- id: "t1",
7169
- type: "web_search",
7170
- query: "anything",
7171
- status: "in_progress"
7172
- }),
7173
- agentMessage("m1", "This degraded reply must never be emitted."),
7174
- turnCompleted()
7175
- ],
7176
- expected: [
7177
- startedSession(NEW_THREAD_ID),
7178
- {
7179
- type: "result",
7180
- ok: false,
7181
- reason: "model_unavailable:Model metadata for `gpt-5-codex` not found. Defaulting to fallback metadata; this can degrade performance and cause issues."
7182
- }
7183
- ]
7184
- },
7185
- {
7186
- // A NON-metadata error item is non-fatal — it's skipped, and the turn proceeds
7187
- // to its normal reply (guard specificity: only the fallback-metadata phrase trips).
7188
- name: "non-metadata error item is skipped (non-fatal)",
7189
- request: makeRequest3(),
7190
- nativeStream: [
7191
- threadStarted(NEW_THREAD_ID),
7192
- errorItem("e0", "a transient hiccup"),
7193
- agentMessage("m1", "All good now."),
7194
- turnCompleted()
7195
- ],
7196
- expected: [
7197
- startedSession(NEW_THREAD_ID),
7198
- { type: "text", body: "All good now.", terminal: true },
7199
- closingSession(NEW_THREAD_ID),
7200
- { type: "result", ok: true }
7201
- ]
7202
- },
7203
- {
7204
- // The zero-content success the claude-code suite pins, in Codex's vocabulary — the thread
7205
- // starts, the turn completes, nothing in between. No Codex sample has been seen; the fixture
7206
- // exists because the host's classification is runtime-neutral, so a runtime that can produce
7207
- // this envelope must be shown producing it in the shape the classifier reads.
7208
- name: "zero-content success (empty runtime result)",
7209
- request: makeRequest3(),
7210
- nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
7211
- expected: [
7212
- startedSession(NEW_THREAD_ID),
7213
- closingSession(NEW_THREAD_ID),
7214
- { type: "result", ok: true }
7215
- ]
7216
- },
7217
- {
7218
- // Empty-final: a clean turn that ended on a tool call with no closing message.
7219
- // The adapter emits NO final text — empty-final promotion is host/pump territory
7220
- // (this fixture pins that boundary).
7221
- name: "empty-final (no closing message)",
7222
- request: makeRequest3(),
7223
- nativeStream: [
7224
- threadStarted(NEW_THREAD_ID),
7225
- toolFrame("item.started", {
7226
- id: "t1",
7227
- type: "file_change",
7228
- changes: [{ path: "a.md", kind: "add" }],
7229
- status: "in_progress"
7230
- }),
7231
- toolFrame("item.completed", {
7232
- id: "t1",
7233
- type: "file_change",
7234
- changes: [{ path: "a.md", kind: "add" }],
7235
- status: "completed"
7236
- }),
7237
- turnCompleted()
7238
- ],
7239
- expected: [
7240
- startedSession(NEW_THREAD_ID),
7241
- {
7242
- type: "tool",
7243
- id: "t1",
7244
- name: "apply_patch",
7245
- phase: "start",
7246
- summary: "",
7247
- input: { changes: [{ path: "a.md", kind: "add" }] }
7248
- },
7249
- {
7250
- type: "tool",
7251
- id: "t1",
7252
- name: "apply_patch",
7253
- phase: "done",
7254
- summary: "",
7255
- input: { changes: [{ path: "a.md", kind: "add" }] },
7256
- result: [{ path: "a.md", kind: "add" }]
7257
- },
7258
- closingSession(NEW_THREAD_ID),
7259
- { type: "result", ok: true }
7260
- ]
7261
- },
7262
- {
7263
- // Thinking: a reasoning item seals as a transient `thinking`, then the reply
7264
- // message is the final.
7265
- name: "thinking then reply",
7266
- request: makeRequest3(),
7267
- nativeStream: [
7268
- threadStarted(NEW_THREAD_ID),
7269
- reasoning("r1", "Let me check the notes."),
7270
- agentMessage("m1", "All set."),
7271
- turnCompleted()
7272
- ],
7273
- expected: [
7274
- startedSession(NEW_THREAD_ID),
7275
- { type: "thinking", text: "Let me check the notes." },
7276
- { type: "text", body: "All set.", terminal: true },
7277
- closingSession(NEW_THREAD_ID),
7278
- { type: "result", ok: true }
7279
- ]
7280
- },
7281
- {
7282
- // Codex reports no model in-stream, so the resolved model and reasoning effort come from what the
7283
- // thread was started with (the run spec) and ride the terminal `result`: a real model and a
7284
- // `runtimeOptions.codex.modelReasoningEffort` are asserted through to `resolvedModel` (bare,
7285
- // `openai/`-stripped) and `resolvedConfig.reasoningEffort`.
7286
- name: "resolved model + reasoning effort captured from run spec",
7287
- request: makeRequest3({
7288
- config: {
7289
- model: "openai/gpt-5-codex",
7290
- runtimeOptions: { codex: { modelReasoningEffort: "high" } }
5273
+ const spec = buildRunSpec2(req, resumeThreadId, threadStanding, images?.paths ?? []);
5274
+ if (spec.model === null) {
5275
+ deps.onWarn?.("Codex is using its default model because no model was resolved", {
5276
+ model: req.config.model
5277
+ });
7291
5278
  }
7292
- }),
7293
- nativeStream: [threadStarted(NEW_THREAD_ID), agentMessage("m1", "Done."), turnCompleted()],
7294
- expected: [
7295
- startedSession(NEW_THREAD_ID),
7296
- { type: "text", body: "Done.", terminal: true },
7297
- closingSession(NEW_THREAD_ID),
7298
- {
7299
- type: "result",
7300
- ok: true,
7301
- resolvedModel: "gpt-5-codex",
7302
- resolvedConfig: { reasoningEffort: "high" }
5279
+ let emitted = false;
5280
+ try {
5281
+ const result = await transport.run(spec, signal);
5282
+ if (signal.aborted) return;
5283
+ for await (const event of decodeCodexStream(result.events, {
5284
+ signal,
5285
+ cwd: req.local.cwd,
5286
+ resumedThreadId: resumeThreadId,
5287
+ degraded,
5288
+ // Which segments the thread carries once this turn lands. A segment sent is that one —
5289
+ // the later copy is what the model reads. One withheld is what the thread had. Read off
5290
+ // the spec, so the record can't disagree with what was sent.
5291
+ promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
5292
+ conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
5293
+ compactions,
5294
+ // What the thread held before this turn, for the write at `thread.started`.
5295
+ heldStanding: {
5296
+ promptFingerprint: threadStanding?.core ?? null,
5297
+ conversationFingerprint: threadStanding?.conversation ?? null,
5298
+ compactions: threadStanding ? compactions : null
5299
+ },
5300
+ storedState: req.session,
5301
+ usageBaseline: threadUsageBaseline,
5302
+ // Codex reports no model in-stream, so record what the thread was started with (null on the
5303
+ // default token). Same for the reasoning effort.
5304
+ resolvedModel: spec.model,
5305
+ resolvedReasoningEffort: spec.modelReasoningEffort ?? null
5306
+ })) {
5307
+ emitted = true;
5308
+ yield event;
5309
+ }
5310
+ return;
5311
+ } catch (err) {
5312
+ if (resumeThreadId === null || emitted || signal.aborted || !isMissingRollout(err)) {
5313
+ throw err;
5314
+ }
5315
+ deps.onWarn?.(
5316
+ "codex adapter: the stored thread has no rollout on this host \u2014 starting a fresh thread",
5317
+ { threadId: resumeThreadId }
5318
+ );
5319
+ resumeThreadId = null;
5320
+ threadUsageBaseline = null;
5321
+ threadStanding = null;
5322
+ compactions = 0;
5323
+ req = { ...req, session: null };
5324
+ degraded = true;
5325
+ if (hooks?.fresh) {
5326
+ const recomposed = await hooks.fresh("session_start_failed");
5327
+ if (signal.aborted) return;
5328
+ if (recomposed) {
5329
+ req = { ...req, ...recomposed, session: null };
5330
+ degraded = false;
5331
+ }
5332
+ }
7303
5333
  }
7304
- ]
7305
- },
7306
- {
7307
- // Session resume: the stored state matches the directory, so the thread id is resumed and
7308
- // `thread.started` echoes it — no session event fires. This is what carries cross-turn memory.
7309
- //
7310
- // The stored state also says the thread already holds the composed prompt, so this turn
7311
- // withholds the preamble and the state it would write is byte-identical: the silence proves both
7312
- // an unchanged id and a prompt that needed no re-send.
7313
- name: "session resume (no re-emit on echo)",
7314
- request: makeRequest3({
7315
- session: encodeSession3({
7316
- threadId: "th1",
7317
- cwd: DIR2,
7318
- promptFingerprint: fingerprintPrompt(PROMPT),
7319
- usageBaseline: null,
7320
- compactions: 0
7321
- })
7322
- }),
7323
- nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
7324
- expected: [
7325
- { type: "text", body: "Back again.", terminal: true },
7326
- { type: "result", ok: true }
7327
- ]
7328
- },
7329
- {
7330
- // A resumed thread whose stored state carries no `promptFingerprint` field. The adapter reads the
7331
- // absence as "can't say which prompt this thread holds", sends the full prompt, and re-emits the
7332
- // state with the fingerprint — on the same thread id. That re-emit is the upgrade path: without
7333
- // it a long-lived thread would re-send the whole prompt forever.
7334
- //
7335
- // This fixture fails if the standing half is withheld on a thread we have no evidence about.
7336
- name: "session resume with a pre-CT1075 state re-emits with the prompt fingerprint",
7337
- request: makeRequest3({
7338
- session: JSON.stringify({ threadId: "th1", cwd: DIR2 })
7339
- }),
7340
- nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
7341
- expected: [
7342
- startedSession("th1"),
7343
- { type: "text", body: "Back again.", terminal: true },
7344
- closingSession("th1"),
7345
- { type: "result", ok: true }
7346
- ]
7347
- },
7348
- {
7349
- // The thread carries a different prompt than this turn composed — a charter edit, a renamed
7350
- // agent, a redeployed prompt body. The fingerprint doesn't match, so the new prompt is sent as on
7351
- // a fresh thread and the state re-recorded.
7352
- //
7353
- // This fixture fails if a stale prompt is treated as good enough; a boolean flag would pass the
7354
- // resume fixture above and withhold every future charter edit.
7355
- name: "session resume with a stale prompt fingerprint re-sends the prompt",
7356
- request: makeRequest3({
7357
- session: encodeSession3({
7358
- threadId: "th1",
7359
- cwd: DIR2,
7360
- promptFingerprint: fingerprintPrompt("an older composed prompt"),
7361
- usageBaseline: null
7362
- })
7363
- }),
7364
- nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
7365
- expected: [
7366
- startedSession("th1"),
7367
- { type: "text", body: "Back again.", terminal: true },
7368
- closingSession("th1"),
7369
- { type: "result", ok: true }
7370
- ]
7371
- },
7372
- {
7373
- // Directory-mismatch degrade: the stored state was created under another directory, so the
7374
- // adapter starts fresh — `thread.started` reports a new id, and the session event records the
7375
- // current directory with `degraded: true` so the host rewinds the catch-up mark.
7376
- //
7377
- // The refused resume means the new thread is empty, so the full prompt goes in even though the
7378
- // stored state vouched for a copy in a thread we're no longer in.
7379
- name: "session directory-mismatch degrades to fresh",
7380
- request: makeRequest3({
7381
- session: encodeSession3({
7382
- threadId: "old",
7383
- cwd: "/env/gone",
7384
- promptFingerprint: fingerprintPrompt(PROMPT),
7385
- usageBaseline: null
7386
- })
7387
- }),
7388
- nativeStream: [
7389
- threadStarted(NEW_THREAD_ID),
7390
- agentMessage("m1", "Fresh start."),
7391
- turnCompleted()
7392
- ],
7393
- expected: [
7394
- startedSession(NEW_THREAD_ID, DIR2, true),
7395
- { type: "text", body: "Fresh start.", terminal: true },
7396
- closingSession(NEW_THREAD_ID),
7397
- { type: "result", ok: true }
7398
- ]
5334
+ }
5335
+ } finally {
5336
+ if (images) {
5337
+ try {
5338
+ await images.cleanup();
5339
+ } catch (err) {
5340
+ deps.onWarn?.("codex adapter: image cleanup failed", {
5341
+ err: err instanceof Error ? err.message : String(err)
5342
+ });
5343
+ }
5344
+ }
7399
5345
  }
7400
- ];
5346
+ }
5347
+ };
5348
+ }
5349
+ var codexAdapter;
5350
+ var init_codex = __esm({
5351
+ "packages/agent-runtime/src/codex/index.ts"() {
5352
+ "use strict";
5353
+ init_addendum3();
5354
+ init_loop3();
5355
+ init_policy3();
5356
+ init_run_spec2();
5357
+ init_session3();
5358
+ init_transport2();
5359
+ init_addendum3();
5360
+ init_loop3();
5361
+ init_policy3();
5362
+ init_model3();
5363
+ init_run_spec2();
5364
+ init_transport2();
5365
+ init_session3();
5366
+ init_events2();
5367
+ codexAdapter = createCodexAdapter();
7401
5368
  }
7402
5369
  });
7403
5370
 
@@ -7421,17 +5388,13 @@ var init_src = __esm({
7421
5388
  init_failure();
7422
5389
  init_turn_request();
7423
5390
  init_standing();
7424
- init_conformance();
7425
5391
  init_transcript();
7426
5392
  init_transcript_sink();
7427
5393
  init_pump();
7428
5394
  init_claude_code();
7429
- init_conformance2();
7430
5395
  init_registry();
7431
5396
  init_opencode();
7432
- init_conformance3();
7433
5397
  init_codex();
7434
- init_conformance4();
7435
5398
  init_sdk();
7436
5399
  }
7437
5400
  });
@@ -7464,11 +5427,11 @@ function withDeviceOpencodeLock(inner, serverUrl, acquire) {
7464
5427
  };
7465
5428
  }
7466
5429
  function runnerOpencodeTransport(serverUrl, acquire, report) {
7467
- const fetchImpl = (input, init2) => {
5430
+ const fetchImpl = (input, init) => {
7468
5431
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
7469
5432
  const prompt = /\/session\/([^/]+)\/prompt_async$/.exec(new URL(url).pathname);
7470
5433
  if (prompt?.[1]) report(decodeURIComponent(prompt[1]));
7471
- return globalThis.fetch(input, init2);
5434
+ return globalThis.fetch(input, init);
7472
5435
  };
7473
5436
  return withDeviceOpencodeLock(
7474
5437
  createHttpOpencodeTransport({ baseUrl: serverUrl, fetchImpl }),
@@ -7809,7 +5772,7 @@ var init_turn_containment = __esm({
7809
5772
  });
7810
5773
 
7811
5774
  // src/run-config.ts
7812
- import { z as z13 } from "zod";
5775
+ import { z as z12 } from "zod";
7813
5776
  function parseRunConfig(raw, onWarn) {
7814
5777
  const result = runConfigSchema.safeParse(raw ?? {});
7815
5778
  if (result.success) return result.data;
@@ -7822,43 +5785,43 @@ var mcpStdioServerSchema, mcpHttpServerSchema, mcpSseServerSchema, mcpServerDefS
7822
5785
  var init_run_config = __esm({
7823
5786
  "src/run-config.ts"() {
7824
5787
  "use strict";
7825
- mcpStdioServerSchema = z13.object({
7826
- type: z13.literal("stdio").optional(),
7827
- command: z13.string().min(1),
7828
- args: z13.array(z13.string()).optional(),
7829
- env: z13.record(z13.string(), z13.string()).optional(),
5788
+ mcpStdioServerSchema = z12.object({
5789
+ type: z12.literal("stdio").optional(),
5790
+ command: z12.string().min(1),
5791
+ args: z12.array(z12.string()).optional(),
5792
+ env: z12.record(z12.string(), z12.string()).optional(),
7830
5793
  // Variable → the name of a `secretEnv` entry holding its value. Set by the server on a
7831
5794
  // room's shared browser, whose connect URL is a credential.
7832
- envFromEnv: z13.record(z13.string(), z13.string()).optional()
5795
+ envFromEnv: z12.record(z12.string(), z12.string()).optional()
7833
5796
  });
7834
- mcpHttpServerSchema = z13.object({
7835
- type: z13.literal("http"),
7836
- url: z13.string().url(),
7837
- headers: z13.record(z13.string(), z13.string()).optional(),
5797
+ mcpHttpServerSchema = z12.object({
5798
+ type: z12.literal("http"),
5799
+ url: z12.string().url(),
5800
+ headers: z12.record(z12.string(), z12.string()).optional(),
7838
5801
  // Header → the name of a `secretEnv` entry holding its value. Set by
7839
5802
  // the server on a room connection's entry, and by `resolveMcpSecrets` on a
7840
5803
  // header whose `${VAR}` it resolved.
7841
- headersFromEnv: z13.record(z13.string(), z13.string()).optional()
5804
+ headersFromEnv: z12.record(z12.string(), z12.string()).optional()
7842
5805
  });
7843
- mcpSseServerSchema = z13.object({
7844
- type: z13.literal("sse"),
7845
- url: z13.string().url(),
7846
- headers: z13.record(z13.string(), z13.string()).optional(),
5806
+ mcpSseServerSchema = z12.object({
5807
+ type: z12.literal("sse"),
5808
+ url: z12.string().url(),
5809
+ headers: z12.record(z12.string(), z12.string()).optional(),
7847
5810
  // Header → the name of a `secretEnv` entry holding its value. Set by
7848
5811
  // the server on a room connection's entry, and by `resolveMcpSecrets` on a
7849
5812
  // header whose `${VAR}` it resolved.
7850
- headersFromEnv: z13.record(z13.string(), z13.string()).optional()
5813
+ headersFromEnv: z12.record(z12.string(), z12.string()).optional()
7851
5814
  });
7852
- mcpServerDefSchema = z13.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
7853
- thinkingConfigSchema = z13.discriminatedUnion("type", [
7854
- z13.object({ type: z13.literal("adaptive") }),
7855
- z13.object({ type: z13.literal("enabled"), budgetTokens: z13.number().int().positive().optional() }),
7856
- z13.object({ type: z13.literal("disabled") })
5815
+ mcpServerDefSchema = z12.union([mcpHttpServerSchema, mcpSseServerSchema, mcpStdioServerSchema]);
5816
+ thinkingConfigSchema = z12.discriminatedUnion("type", [
5817
+ z12.object({ type: z12.literal("adaptive") }),
5818
+ z12.object({ type: z12.literal("enabled"), budgetTokens: z12.number().int().positive().optional() }),
5819
+ z12.object({ type: z12.literal("disabled") })
7857
5820
  ]);
7858
- effortSchema = z13.enum(["low", "medium", "high", "xhigh", "max"]);
7859
- runConfigSchema = z13.object({
7860
- mcpServers: z13.record(z13.string(), mcpServerDefSchema).optional(),
7861
- model: z13.string().min(1).optional(),
5821
+ effortSchema = z12.enum(["low", "medium", "high", "xhigh", "max"]);
5822
+ runConfigSchema = z12.object({
5823
+ mcpServers: z12.record(z12.string(), mcpServerDefSchema).optional(),
5824
+ model: z12.string().min(1).optional(),
7862
5825
  thinking: thinkingConfigSchema.optional(),
7863
5826
  effort: effortSchema.optional()
7864
5827
  });
@@ -7867,7 +5830,7 @@ var init_run_config = __esm({
7867
5830
  });
7868
5831
 
7869
5832
  // src/runner/protocol.ts
7870
- import { z as z14 } from "zod";
5833
+ import { z as z13 } from "zod";
7871
5834
  function required(env, name) {
7872
5835
  const value = env[name];
7873
5836
  if (!value) throw new Error(`runner: ${name} is not set`);
@@ -7966,61 +5929,61 @@ var init_protocol = __esm({
7966
5929
  transcriptDir: "CABANE_TRANSCRIPT_DIR",
7967
5930
  dedicatedRoot: "CABANE_WORK_ROOT_DEDICATED"
7968
5931
  };
7969
- runtimeHealthSchema = z14.object({
7970
- quotaState: z14.enum(["ok", "limited", "auth_expired", "unknown"]),
7971
- limitedUntil: z14.string().nullable(),
7972
- lastError: z14.string().nullable()
5932
+ runtimeHealthSchema = z13.object({
5933
+ quotaState: z13.enum(["ok", "limited", "auth_expired", "unknown"]),
5934
+ limitedUntil: z13.string().nullable(),
5935
+ lastError: z13.string().nullable()
7973
5936
  });
7974
- runnerSpecSchema = z14.object({
7975
- deviceId: z14.string().optional(),
5937
+ runnerSpecSchema = z13.object({
5938
+ deviceId: z13.string().optional(),
7976
5939
  // The launcher's boot id: the one its heartbeat carries, so a runner's lease
7977
5940
  // matches the device that holds it.
7978
- bootId: z14.string(),
7979
- workspaceId: z14.string(),
7980
- workspaceSlug: z14.string(),
7981
- agentId: z14.string(),
7982
- agentUsername: z14.string(),
7983
- agentName: z14.string(),
5941
+ bootId: z13.string(),
5942
+ workspaceId: z13.string(),
5943
+ workspaceSlug: z13.string(),
5944
+ agentId: z13.string(),
5945
+ agentUsername: z13.string(),
5946
+ agentName: z13.string(),
7984
5947
  local: localAgentConfigSchema,
7985
5948
  runConfig: runConfigSchema,
7986
5949
  // The device's `root`, resolved by the launcher: the pair directory the turn
7987
5950
  // stands in when nothing else names one lives under it (`work-dir.ts`).
7988
- workRoot: z14.string(),
7989
- opencodeServerUrl: z14.string().optional(),
7990
- claudeCodeAvailable: z14.boolean(),
7991
- codexAvailable: z14.boolean(),
7992
- idleTimeoutMs: z14.number().int().positive().optional(),
7993
- totalTimeoutMs: z14.number().int().positive().optional(),
7994
- leaseRenewalMs: z14.number().int().positive().optional(),
7995
- preparingRowDelayMs: z14.number().int().nonnegative().optional(),
5951
+ workRoot: z13.string(),
5952
+ opencodeServerUrl: z13.string().optional(),
5953
+ claudeCodeAvailable: z13.boolean(),
5954
+ codexAvailable: z13.boolean(),
5955
+ idleTimeoutMs: z13.number().int().positive().optional(),
5956
+ totalTimeoutMs: z13.number().int().positive().optional(),
5957
+ leaseRenewalMs: z13.number().int().positive().optional(),
5958
+ preparingRowDelayMs: z13.number().int().nonnegative().optional(),
7996
5959
  // Whether the turn writes a transcript, into the device's own directory
7997
5960
  // (`transcriptsDir()`).
7998
- transcripts: z14.boolean(),
7999
- logLevel: z14.string(),
5961
+ transcripts: z13.boolean(),
5962
+ logLevel: z13.string(),
8000
5963
  // The launcher's connector-health observations, by runtime, as of the start.
8001
- connectorHealth: z14.record(z14.string(), runtimeHealthSchema),
5964
+ connectorHealth: z13.record(z13.string(), runtimeHealthSchema),
8002
5965
  // True when this (conversation, agent) pair was already told its session
8003
5966
  // could not be saved: the notice is once per pair, not once per process.
8004
- sessionWriteNoticeClaimed: z14.boolean(),
5967
+ sessionWriteNoticeClaimed: z13.boolean(),
8005
5968
  // The runner's identity, minted with its credential. The start
8006
5969
  // PATCH carries it so the lease knows which runner holds the span.
8007
- runnerId: z14.string(),
5970
+ runnerId: z13.string(),
8008
5971
  // Whether the server took gzip request bodies at the latest heartbeat, as of
8009
5972
  // the start. The launcher writes `gzip_bodies` when that changes mid-turn.
8010
- gzipBodies: z14.boolean().optional()
5973
+ gzipBodies: z13.boolean().optional()
8011
5974
  });
8012
- dispatchSchema = z14.object({
8013
- type: z14.literal("device:dispatch_requested"),
8014
- agentId: z14.string(),
8015
- deviceId: z14.string(),
8016
- conversationId: z14.string(),
8017
- messageId: z14.string(),
8018
- episode: z14.number().int().optional()
5975
+ dispatchSchema = z13.object({
5976
+ type: z13.literal("device:dispatch_requested"),
5977
+ agentId: z13.string(),
5978
+ deviceId: z13.string(),
5979
+ conversationId: z13.string(),
5980
+ messageId: z13.string(),
5981
+ episode: z13.number().int().optional()
8019
5982
  });
8020
- credentialsSchema = z14.object({
8021
- credential: z14.string().startsWith("cabrn_"),
8022
- secrets: z14.record(z14.string(), z14.string()).optional(),
8023
- grants: z14.record(z14.string(), z14.record(z14.string(), z14.string())).optional()
5983
+ credentialsSchema = z13.object({
5984
+ credential: z13.string().startsWith("cabrn_"),
5985
+ secrets: z13.record(z13.string(), z13.string()).optional(),
5986
+ grants: z13.record(z13.string(), z13.record(z13.string(), z13.string())).optional()
8024
5987
  });
8025
5988
  }
8026
5989
  });
@@ -8079,9 +6042,9 @@ var init_turn_responsiveness = __esm({
8079
6042
  });
8080
6043
 
8081
6044
  // src/grants.ts
8082
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
8083
- import { join as join14 } from "path";
8084
- import { z as z15 } from "zod";
6045
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
6046
+ import { join as join12 } from "path";
6047
+ import { z as z14 } from "zod";
8085
6048
  function allowedGrantVariable(name) {
8086
6049
  return name.length <= 100 && VARIABLE_NAME_RE.test(name) && !name.startsWith("CABANE_");
8087
6050
  }
@@ -8089,7 +6052,7 @@ function environmentSafe(value) {
8089
6052
  return !value.includes("\0");
8090
6053
  }
8091
6054
  function grantsPath() {
8092
- return join14(cabaneDir(), "grants.json");
6055
+ return join12(cabaneDir(), "grants.json");
8093
6056
  }
8094
6057
  function readGrants(keys, report) {
8095
6058
  if (keys.length === 0) return {};
@@ -8108,7 +6071,7 @@ function readGrants(keys, report) {
8108
6071
  }
8109
6072
  function loadGrantsFile(report) {
8110
6073
  const path = grantsPath();
8111
- if (!existsSync9(path)) return {};
6074
+ if (!existsSync8(path)) return {};
8112
6075
  let raw;
8113
6076
  try {
8114
6077
  raw = readFileSync7(path, "utf8");
@@ -8164,15 +6127,15 @@ var init_grants = __esm({
8164
6127
  "use strict";
8165
6128
  init_config();
8166
6129
  VARIABLE_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
8167
- grantsFileSchema = z15.record(z15.string(), z15.record(z15.string(), z15.string()));
6130
+ grantsFileSchema = z14.record(z14.string(), z14.record(z14.string(), z14.string()));
8168
6131
  }
8169
6132
  });
8170
6133
 
8171
6134
  // src/work-dir.ts
8172
6135
  import { statSync as statSync3 } from "fs";
8173
- import { join as join15 } from "path";
6136
+ import { join as join13 } from "path";
8174
6137
  function pairDirectory(root, pair) {
8175
- return join15(root, pair.workspaceSlug, pair.agentUsername, pair.conversationId);
6138
+ return join13(root, pair.workspaceSlug, pair.agentUsername, pair.conversationId);
8176
6139
  }
8177
6140
  function chooseStandingDirectory(named, pairDir, isDirectory = directoryExists) {
8178
6141
  const skipped = [];
@@ -8197,7 +6160,7 @@ var init_work_dir = __esm({
8197
6160
 
8198
6161
  // src/turn-control-tools.ts
8199
6162
  import { randomUUID as randomUUID3 } from "crypto";
8200
- import { z as z16 } from "zod";
6163
+ import { z as z15 } from "zod";
8201
6164
  function createSkipState() {
8202
6165
  return { skipped: false, reason: null };
8203
6166
  }
@@ -8248,7 +6211,7 @@ function createTurnControlMcpServer(acts, skipState) {
8248
6211
  SKIP_TURN_TOOL,
8249
6212
  TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
8250
6213
  {
8251
- reason: z16.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
6214
+ reason: z15.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
8252
6215
  },
8253
6216
  async (args) => {
8254
6217
  skipState.skipped = true;
@@ -8264,22 +6227,22 @@ function createTurnControlMcpServer(acts, skipState) {
8264
6227
  ASK_TOOL,
8265
6228
  TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
8266
6229
  {
8267
- targetUserId: z16.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
8268
- question: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
8269
- headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
8270
- options: z16.array(z16.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
8271
- questions: z16.array(
8272
- z16.object({
8273
- headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
8274
- body: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
8275
- options: z16.array(
8276
- z16.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)
6230
+ targetUserId: z15.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
6231
+ question: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
6232
+ headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
6233
+ options: z15.array(z15.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
6234
+ questions: z15.array(
6235
+ z15.object({
6236
+ headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
6237
+ body: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
6238
+ options: z15.array(
6239
+ z15.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)
8277
6240
  ).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
8278
6241
  })
8279
6242
  ).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
8280
- replaces: z16.object({
8281
- askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
8282
- reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
6243
+ replaces: z15.object({
6244
+ askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
6245
+ reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
8283
6246
  }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
8284
6247
  },
8285
6248
  async (args) => {
@@ -8320,9 +6283,9 @@ function createTurnControlMcpServer(acts, skipState) {
8320
6283
  WAKE_ME_TOOL,
8321
6284
  TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
8322
6285
  {
8323
- afterSeconds: z16.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
8324
- at: z16.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
8325
- note: z16.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
6286
+ afterSeconds: z15.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
6287
+ at: z15.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
6288
+ note: z15.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
8326
6289
  },
8327
6290
  async (args) => {
8328
6291
  const hasAfter = args.afterSeconds !== void 0;
@@ -8356,8 +6319,8 @@ function createTurnControlMcpServer(acts, skipState) {
8356
6319
  WITHDRAW_ASK_TOOL,
8357
6320
  TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
8358
6321
  {
8359
- askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
8360
- reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
6322
+ askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
6323
+ reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
8361
6324
  },
8362
6325
  async (args) => performAct(acts, {
8363
6326
  kind: "withdraw_ask",
@@ -8461,17 +6424,17 @@ var init_build_options = __esm({
8461
6424
  });
8462
6425
 
8463
6426
  // src/codex-config.ts
8464
- import { readFile as readFile2 } from "fs/promises";
6427
+ import { readFile } from "fs/promises";
8465
6428
  import { homedir as homedir7 } from "os";
8466
- import { join as join16 } from "path";
8467
- import { parse } from "smol-toml";
6429
+ import { join as join14 } from "path";
6430
+ import { parse as parse2 } from "smol-toml";
8468
6431
  function codexConfigPath(env = process.env, home = homedir7()) {
8469
- return join16(env.CODEX_HOME || join16(home, ".codex"), "config.toml");
6432
+ return join14(env.CODEX_HOME || join14(home, ".codex"), "config.toml");
8470
6433
  }
8471
6434
  async function readCodexConfigSetsSandboxMode(path = codexConfigPath()) {
8472
6435
  let text;
8473
6436
  try {
8474
- text = await readFile2(path, "utf8");
6437
+ text = await readFile(path, "utf8");
8475
6438
  } catch {
8476
6439
  return false;
8477
6440
  }
@@ -8479,7 +6442,7 @@ async function readCodexConfigSetsSandboxMode(path = codexConfigPath()) {
8479
6442
  }
8480
6443
  function configSetsSandboxMode(text) {
8481
6444
  try {
8482
- return parse(text).sandbox_mode !== void 0;
6445
+ return parse2(text).sandbox_mode !== void 0;
8483
6446
  } catch {
8484
6447
  return false;
8485
6448
  }
@@ -8493,13 +6456,13 @@ var init_codex_config = __esm({
8493
6456
  // src/codex-images.ts
8494
6457
  import { mkdtemp, rm, writeFile } from "fs/promises";
8495
6458
  import { tmpdir } from "os";
8496
- import { join as join17 } from "path";
6459
+ import { join as join15 } from "path";
8497
6460
  function createCodexImagePreparer(opts) {
8498
6461
  const doFetch = opts.fetch ?? fetch;
8499
6462
  const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
8500
6463
  return async (urls, signal) => {
8501
6464
  signal.throwIfAborted();
8502
- const dir2 = await mkdtemp(join17(tmpdir(), `cabane-codex-images-${opts.turnId}-`));
6465
+ const dir2 = await mkdtemp(join15(tmpdir(), `cabane-codex-images-${opts.turnId}-`));
8503
6466
  const cleanup = async () => {
8504
6467
  try {
8505
6468
  await rm(dir2, { recursive: true, force: true });
@@ -8520,7 +6483,7 @@ function createCodexImagePreparer(opts) {
8520
6483
  const ext = EXTENSIONS[type];
8521
6484
  if (!ext) throw new Error(`not a vision image type: ${type || "none"}`);
8522
6485
  const bytes = new Uint8Array(await res.arrayBuffer());
8523
- const path = join17(dir2, `image-${index + 1}.${ext}`);
6486
+ const path = join15(dir2, `image-${index + 1}.${ext}`);
8524
6487
  await writeFile(path, bytes, { mode: 384 });
8525
6488
  return path;
8526
6489
  } catch (err) {
@@ -8562,9 +6525,9 @@ var init_codex_images = __esm({
8562
6525
  import { createReadStream } from "fs";
8563
6526
  import { readdir } from "fs/promises";
8564
6527
  import { homedir as homedir8 } from "os";
8565
- import { join as join18 } from "path";
6528
+ import { join as join16 } from "path";
8566
6529
  import { createInterface as createInterface2 } from "readline";
8567
- function createCodexCompactionCounter(codexHome = process.env.CODEX_HOME || join18(homedir8(), ".codex")) {
6530
+ function createCodexCompactionCounter(codexHome = process.env.CODEX_HOME || join16(homedir8(), ".codex")) {
8568
6531
  return async (threadId) => {
8569
6532
  try {
8570
6533
  const rollout = await findRollout(codexHome, threadId);
@@ -8581,7 +6544,7 @@ async function findRollout(codexHome, threadId) {
8581
6544
  const suffix = `-${threadId}.jsonl`;
8582
6545
  for (const offset of [0, -DAY_MS, DAY_MS]) {
8583
6546
  const day = new Date(createdAt + offset);
8584
- const dir2 = join18(
6547
+ const dir2 = join16(
8585
6548
  codexHome,
8586
6549
  "sessions",
8587
6550
  String(day.getUTCFullYear()),
@@ -8595,7 +6558,7 @@ async function findRollout(codexHome, threadId) {
8595
6558
  continue;
8596
6559
  }
8597
6560
  const name = names.find((n) => n.startsWith("rollout-") && n.endsWith(suffix));
8598
- if (name) return join18(dir2, name);
6561
+ if (name) return join16(dir2, name);
8599
6562
  }
8600
6563
  return null;
8601
6564
  }
@@ -8660,20 +6623,20 @@ var init_turn_seq_floor = __esm({
8660
6623
  });
8661
6624
 
8662
6625
  // src/prepared.ts
8663
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
8664
- import { join as join19 } from "path";
6626
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
6627
+ import { join as join17 } from "path";
8665
6628
  function dirFor(workspaceId) {
8666
- return join19(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
6629
+ return join17(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8667
6630
  }
8668
6631
  function conversationDir(workspaceId, conversationId) {
8669
- return join19(dirFor(workspaceId), encodeURIComponent(conversationId));
6632
+ return join17(dirFor(workspaceId), encodeURIComponent(conversationId));
8670
6633
  }
8671
6634
  function pathFor2(workspaceId, conversationId, agentId) {
8672
- return join19(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
6635
+ return join17(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8673
6636
  }
8674
6637
  function readPrepared(workspaceId, conversationId, agentId) {
8675
6638
  const path = pathFor2(workspaceId, conversationId, agentId);
8676
- if (!existsSync10(path)) return null;
6639
+ if (!existsSync9(path)) return null;
8677
6640
  try {
8678
6641
  const parsed = JSON.parse(readFileSync8(path, "utf8"));
8679
6642
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
@@ -8706,15 +6669,15 @@ var init_prepared = __esm({
8706
6669
  });
8707
6670
 
8708
6671
  // src/secrets.ts
8709
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
8710
- import { join as join20 } from "path";
8711
- import { z as z17 } from "zod";
6672
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
6673
+ import { join as join18 } from "path";
6674
+ import { z as z16 } from "zod";
8712
6675
  function secretsPath() {
8713
- return join20(cabaneDir(), "secrets.json");
6676
+ return join18(cabaneDir(), "secrets.json");
8714
6677
  }
8715
6678
  function loadSecretValues() {
8716
6679
  const path = secretsPath();
8717
- if (!existsSync11(path)) return {};
6680
+ if (!existsSync10(path)) return {};
8718
6681
  let raw;
8719
6682
  try {
8720
6683
  raw = readFileSync9(path, "utf8");
@@ -8835,7 +6798,7 @@ var init_secrets = __esm({
8835
6798
  init_config();
8836
6799
  init_errors();
8837
6800
  PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
8838
- secretStoreSchema = z17.record(z17.string(), z17.string());
6801
+ secretStoreSchema = z16.record(z16.string(), z16.string());
8839
6802
  SECRET_ENV_NAME = /^CABANE_MCP_(\d+)$/;
8840
6803
  }
8841
6804
  });
@@ -8867,9 +6830,9 @@ var init_turn_env = __esm({
8867
6830
 
8868
6831
  // src/transcript-writer.ts
8869
6832
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
8870
- import { basename as basename3, dirname as dirname7, join as join21 } from "path";
6833
+ import { basename as basename3, dirname as dirname6, join as join19 } from "path";
8871
6834
  function transcriptsDir() {
8872
- return join21(cabaneDir(), "transcripts");
6835
+ return join19(cabaneDir(), "transcripts");
8873
6836
  }
8874
6837
  function fileName(meta) {
8875
6838
  const d = new Date(meta.ts);
@@ -8895,7 +6858,7 @@ function pruneOld(dir2, retain) {
8895
6858
  const drop = files.sort().slice(0, files.length - retain);
8896
6859
  for (const f of drop) {
8897
6860
  try {
8898
- rmSync6(join21(dir2, f), { force: true });
6861
+ rmSync6(join19(dir2, f), { force: true });
8899
6862
  } catch {
8900
6863
  }
8901
6864
  }
@@ -8913,7 +6876,7 @@ var init_transcript_writer = __esm({
8913
6876
  onWarn;
8914
6877
  constructor(dir2, meta, onWarn) {
8915
6878
  this.onWarn = onWarn;
8916
- this.path = join21(dir2, fileName(meta));
6879
+ this.path = join19(dir2, fileName(meta));
8917
6880
  try {
8918
6881
  mkdirSync8(dir2, { recursive: true });
8919
6882
  try {
@@ -8942,10 +6905,10 @@ var init_transcript_writer = __esm({
8942
6905
  preserveAnomaly() {
8943
6906
  if (this.broken) return;
8944
6907
  try {
8945
- const dir2 = join21(dirname7(this.path), "anomalies");
6908
+ const dir2 = join19(dirname6(this.path), "anomalies");
8946
6909
  mkdirSync8(dir2, { recursive: true, mode: 448 });
8947
6910
  chmodSync4(dir2, 448);
8948
- const target = join21(dir2, basename3(this.path));
6911
+ const target = join19(dir2, basename3(this.path));
8949
6912
  copyFileSync(this.path, target);
8950
6913
  chmodSync4(target, 384);
8951
6914
  pruneOld(dir2, ANOMALY_RETAIN);
@@ -9202,7 +7165,7 @@ var init_turn_runtime_integrity = __esm({
9202
7165
 
9203
7166
  // src/turn-execution.ts
9204
7167
  import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
9205
- import { existsSync as existsSync12, mkdirSync as mkdirSync9 } from "fs";
7168
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9 } from "fs";
9206
7169
  function resolveIdleTimeoutMs(override) {
9207
7170
  return override ?? DEFAULT_AGENT_IDLE_TIMEOUT_MS;
9208
7171
  }
@@ -9696,7 +7659,7 @@ var init_turn_execution = __esm({
9696
7659
  let hookEnv;
9697
7660
  if (prepareHook) {
9698
7661
  let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
9699
- if (cached2 && !existsSync12(cached2.cwd)) {
7662
+ if (cached2 && !existsSync11(cached2.cwd)) {
9700
7663
  turnLog.debug(
9701
7664
  { cwd: cached2.cwd },
9702
7665
  "dispatcher: the prepared directory is gone \u2014 re-running the prepare hook"
@@ -10294,7 +8257,7 @@ var init_turn_execution = __esm({
10294
8257
  });
10295
8258
  turnLog.debug(
10296
8259
  { runtime: turnRuntime, limitedUntil: health.limitedUntil },
10297
- "dispatcher: idle-timeout on a capped runtime \u2014 reclassified as usage_capped (CT639)"
8260
+ "dispatcher: idle-timeout on a capped runtime \u2014 reclassified as usage_capped"
10298
8261
  );
10299
8262
  }
10300
8263
  }
@@ -10429,7 +8392,7 @@ var init_turn_execution = __esm({
10429
8392
 
10430
8393
  // src/outbox.ts
10431
8394
  import {
10432
- existsSync as existsSync14,
8395
+ existsSync as existsSync13,
10433
8396
  mkdirSync as mkdirSync10,
10434
8397
  readdirSync as readdirSync2,
10435
8398
  readFileSync as readFileSync11,
@@ -10437,7 +8400,7 @@ import {
10437
8400
  rmSync as rmSync7,
10438
8401
  writeFileSync as writeFileSync6
10439
8402
  } from "fs";
10440
- import { join as join22 } from "path";
8403
+ import { join as join20 } from "path";
10441
8404
  var MAX_ENTRIES, MAX_AGE_MS, Outbox;
10442
8405
  var init_outbox = __esm({
10443
8406
  "src/outbox.ts"() {
@@ -10455,10 +8418,10 @@ var init_outbox = __esm({
10455
8418
  // Resolved lazily (per call) so tests that swap HOME between
10456
8419
  // cases route writes at the right tmpdir.
10457
8420
  dir() {
10458
- return join22(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
8421
+ return join20(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
10459
8422
  }
10460
8423
  fileFor(turnId, seq) {
10461
- return join22(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
8424
+ return join20(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
10462
8425
  }
10463
8426
  // Persist a commit for later draining. Atomic (temp file + rename) so a
10464
8427
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -10490,7 +8453,7 @@ var init_outbox = __esm({
10490
8453
  // wedging the drain.
10491
8454
  list() {
10492
8455
  const dir2 = this.dir();
10493
- if (!existsSync14(dir2)) return [];
8456
+ if (!existsSync13(dir2)) return [];
10494
8457
  let names;
10495
8458
  try {
10496
8459
  names = readdirSync2(dir2);
@@ -10500,7 +8463,7 @@ var init_outbox = __esm({
10500
8463
  const entries = [];
10501
8464
  for (const name of names) {
10502
8465
  if (!name.endsWith(".json")) continue;
10503
- const full = join22(dir2, name);
8466
+ const full = join20(dir2, name);
10504
8467
  try {
10505
8468
  const parsed = JSON.parse(readFileSync11(full, "utf8"));
10506
8469
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
@@ -10536,7 +8499,7 @@ var init_outbox = __esm({
10536
8499
  // different: it is positive evidence that nothing was ever queued here.
10537
8500
  maxSeqForTurn(turnId) {
10538
8501
  const dir2 = this.dir();
10539
- if (!existsSync14(dir2)) return 0;
8502
+ if (!existsSync13(dir2)) return 0;
10540
8503
  const names = readdirSync2(dir2);
10541
8504
  const prefix = `${encodeURIComponent(turnId)}__`;
10542
8505
  let max = 0;
@@ -10556,7 +8519,7 @@ var init_outbox = __esm({
10556
8519
  }
10557
8520
  size() {
10558
8521
  const dir2 = this.dir();
10559
- if (!existsSync14(dir2)) return 0;
8522
+ if (!existsSync13(dir2)) return 0;
10560
8523
  try {
10561
8524
  return readdirSync2(dir2).filter((n) => n.endsWith(".json")).length;
10562
8525
  } catch {
@@ -10607,7 +8570,7 @@ var init_outbox = __esm({
10607
8570
 
10608
8571
  // src/runner/exec-shim.ts
10609
8572
  import { chmodSync as chmodSync6, mkdirSync as mkdirSync18, mkdtempSync, rmSync as rmSync13, writeFileSync as writeFileSync11 } from "fs";
10610
- import { join as join32 } from "path";
8573
+ import { join as join30 } from "path";
10611
8574
  function shellQuote(value) {
10612
8575
  return `'${value.replace(/'/g, `'\\''`)}'`;
10613
8576
  }
@@ -10617,10 +8580,10 @@ function createShimDir() {
10617
8580
  return {
10618
8581
  write(argv) {
10619
8582
  if (!dir2) {
10620
- mkdirSync18(join32(cabaneDir(), "runner"), { recursive: true });
10621
- dir2 = mkdtempSync(join32(cabaneDir(), "runner", "turn-"));
8583
+ mkdirSync18(join30(cabaneDir(), "runner"), { recursive: true });
8584
+ dir2 = mkdtempSync(join30(cabaneDir(), "runner", "turn-"));
10622
8585
  }
10623
- const path = join32(dir2, `exec-${++count}`);
8586
+ const path = join30(dir2, `exec-${++count}`);
10624
8587
  writeFileSync11(path, `#!/bin/sh
10625
8588
  exec ${argv.map(shellQuote).join(" ")} "$@"
10626
8589
  `);
@@ -11488,12 +9451,12 @@ async function detectHarnesses(cfg, deps = {}) {
11488
9451
  );
11489
9452
  return version !== null ? { state: "usable", via: "serve", version, url } : { state: "not_running", url };
11490
9453
  };
11491
- const [claude, codex, serve2] = await Promise.all([
9454
+ const [claude, codex, serve] = await Promise.all([
11492
9455
  bundled("claude-code"),
11493
9456
  bundled("codex"),
11494
9457
  opencode()
11495
9458
  ]);
11496
- return { "claude-code": claude, codex, opencode: serve2 };
9459
+ return { "claude-code": claude, codex, opencode: serve };
11497
9460
  }
11498
9461
  function usableRuntimes(detection) {
11499
9462
  return HARNESS_RUNTIMES.filter((runtime) => detection[runtime].state === "usable");
@@ -12288,20 +10251,21 @@ function isDevicePaired() {
12288
10251
  init_config();
12289
10252
  init_cabane_command();
12290
10253
  import { randomUUID as randomUUID7 } from "crypto";
12291
- import { fileURLToPath as fileURLToPath6 } from "url";
12292
-
12293
- // src/dashboard/server.ts
12294
- init_errors();
12295
- import { dirname as dirname6, join as join11 } from "path";
12296
- import { fileURLToPath as fileURLToPath4 } from "url";
12297
- import { serve } from "@hono/node-server";
12298
- import { Hono } from "hono";
10254
+ import { fileURLToPath as fileURLToPath5 } from "url";
12299
10255
 
12300
- // src/dashboard/routes.ts
12301
- import { openSync as openSync3, readSync, closeSync as closeSync2, fstatSync, existsSync as existsSync6 } from "fs";
12302
- import { readFile } from "fs/promises";
12303
- import { extname, join as join10, normalize } from "path";
12304
- import { streamSSE } from "hono/streaming";
10256
+ // src/prereqs.ts
10257
+ init_config();
10258
+ init_bundled_binaries();
10259
+ function warnAboutHarnessReadiness(cfg, deps = {}) {
10260
+ const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
10261
+ const warn = deps.warn ?? ((message2) => process.stderr.write(`${message2}
10262
+ `));
10263
+ for (const runtime of ["claude-code", "codex"]) {
10264
+ if (!harnessAllowed(cfg, runtime)) continue;
10265
+ const resolution = resolveBundled(runtime);
10266
+ if (resolution.status !== "present") warn(startupWarning(resolution));
10267
+ }
10268
+ }
12305
10269
 
12306
10270
  // src/state.ts
12307
10271
  import { EventEmitter } from "events";
@@ -12537,231 +10501,6 @@ var CompanionStateHub = class {
12537
10501
  }
12538
10502
  };
12539
10503
 
12540
- // src/dashboard/routes.ts
12541
- var CONTENT_TYPES = {
12542
- ".html": "text/html; charset=utf-8",
12543
- ".css": "text/css; charset=utf-8",
12544
- ".js": "text/javascript; charset=utf-8",
12545
- ".svg": "image/svg+xml",
12546
- ".ico": "image/x-icon"
12547
- };
12548
- function registerRoutes(app, deps) {
12549
- const { supervisor, hub, staticDir } = deps;
12550
- app.get("/", async (c) => {
12551
- const html = await readFile(join10(staticDir, "index.html"), "utf8");
12552
- return c.html(html);
12553
- });
12554
- app.get("/static/:file", async (c) => {
12555
- const file = c.req.param("file");
12556
- const safe2 = normalize(file).replace(/^(\.\.[/\\])+/, "");
12557
- const full = join10(staticDir, safe2);
12558
- if (!full.startsWith(staticDir) || !existsSync6(full)) return c.notFound();
12559
- const body = await readFile(full);
12560
- const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
12561
- c.header("content-type", type);
12562
- return c.body(body);
12563
- });
12564
- app.get("/api/status", (c) => c.json(hub.statusJson()));
12565
- app.get("/api/dispatches", (c) => {
12566
- const limit = clampLimit(c.req.query("limit"), 20);
12567
- return c.json({ dispatches: hub.recentDispatches(limit).map(dispatchToWire) });
12568
- });
12569
- app.get("/api/logs", async (c) => {
12570
- const lines2 = clampLimit(c.req.query("lines"), 200, 1e3);
12571
- return c.json({ lines: tailFile(companionLogPath(), lines2) });
12572
- });
12573
- app.post("/api/settings", async (c) => {
12574
- const body = await readJson(c);
12575
- const patch = {};
12576
- if (body.logLevel === "warn" || body.logLevel === "info" || body.logLevel === "debug") {
12577
- patch.logLevel = body.logLevel;
12578
- }
12579
- if (typeof body.autoOpen === "boolean") patch.autoOpen = body.autoOpen;
12580
- supervisor.applySettings(patch);
12581
- return c.json({ ok: true, status: hub.statusJson() });
12582
- });
12583
- app.post("/api/control", async (c) => {
12584
- const body = await readJson(c);
12585
- const action = body.action;
12586
- if (action === "stop") {
12587
- setTimeout(() => void supervisor.requestStop(), 50);
12588
- return c.json({ ok: true, action: "stop" });
12589
- }
12590
- if (action === "restart") {
12591
- setTimeout(() => supervisor.requestRestart(), 50);
12592
- return c.json({ ok: true, action: "restart" });
12593
- }
12594
- if (action === "reload-config") {
12595
- supervisor.reloadConfig();
12596
- return c.json({ ok: true, action: "reload-config" });
12597
- }
12598
- if (action === "refresh") {
12599
- supervisor.refresh();
12600
- return c.json({ ok: true, action: "refresh" });
12601
- }
12602
- return c.json({ error: `unknown action "${String(action)}"` }, 400);
12603
- });
12604
- app.post("/api/harnesses/recheck", async (c) => {
12605
- await supervisor.redetect();
12606
- return c.json({ ok: true, status: hub.statusJson() });
12607
- });
12608
- app.get(
12609
- "/api/events",
12610
- (c) => streamSSE(c, async (stream) => {
12611
- await stream.writeSSE({ event: "status:changed", data: JSON.stringify(hub.statusJson()) });
12612
- const queue = [];
12613
- let wake = null;
12614
- const unsubscribe = hub.on((ev) => {
12615
- queue.push(ev);
12616
- wake?.();
12617
- });
12618
- stream.onAbort(() => {
12619
- unsubscribe();
12620
- wake?.();
12621
- });
12622
- while (!stream.aborted) {
12623
- if (queue.length === 0) {
12624
- await Promise.race([
12625
- new Promise((resolve2) => {
12626
- wake = resolve2;
12627
- }),
12628
- stream.sleep(25e3)
12629
- ]);
12630
- wake = null;
12631
- }
12632
- if (stream.aborted) break;
12633
- if (queue.length === 0) {
12634
- await stream.writeSSE({ event: "ping", data: "" });
12635
- continue;
12636
- }
12637
- const ev = queue.shift();
12638
- await stream.writeSSE({ event: ev.type, data: JSON.stringify(ev.data) });
12639
- }
12640
- unsubscribe();
12641
- })
12642
- );
12643
- }
12644
- async function readJson(c) {
12645
- try {
12646
- const parsed = await c.req.json();
12647
- return parsed && typeof parsed === "object" ? parsed : {};
12648
- } catch {
12649
- return {};
12650
- }
12651
- }
12652
- function clampLimit(raw, fallback, max = 200) {
12653
- if (!raw) return fallback;
12654
- const n = Number(raw);
12655
- if (!Number.isFinite(n) || n <= 0) return fallback;
12656
- return Math.min(Math.floor(n), max);
12657
- }
12658
- function tailFile(path, lines2) {
12659
- if (!existsSync6(path)) return [];
12660
- const MAX_BYTES = 256 * 1024;
12661
- let fd;
12662
- try {
12663
- fd = openSync3(path, "r");
12664
- const size = fstatSync(fd).size;
12665
- const start2 = Math.max(0, size - MAX_BYTES);
12666
- const len = size - start2;
12667
- if (len <= 0) return [];
12668
- const buf = Buffer.alloc(len);
12669
- readSync(fd, buf, 0, len, start2);
12670
- const text = buf.toString("utf8");
12671
- const all = text.split("\n");
12672
- if (start2 > 0 && all.length > 0) all.shift();
12673
- return all.filter((l) => l.length > 0).slice(-lines2);
12674
- } catch {
12675
- return [];
12676
- } finally {
12677
- if (fd !== void 0) closeSync2(fd);
12678
- }
12679
- }
12680
-
12681
- // src/dashboard/server.ts
12682
- var DEFAULT_PORT = 7474;
12683
- var PORT_FALLBACK_SPAN = 10;
12684
- function buildDashboardApp(deps) {
12685
- const app = new Hono();
12686
- app.onError((err, c) => {
12687
- if (err instanceof ApiError) {
12688
- const status2 = err.status >= 400 && err.status < 600 ? err.status : 502;
12689
- return c.json({ error: err.message }, status2);
12690
- }
12691
- if (err instanceof CompanionError) {
12692
- return c.json({ error: err.message }, 400);
12693
- }
12694
- return c.json({ error: err instanceof Error ? err.message : "internal error" }, 500);
12695
- });
12696
- registerRoutes(app, { ...deps, staticDir: resolveStaticDir() });
12697
- return app;
12698
- }
12699
- async function startDashboard(opts) {
12700
- const app = buildDashboardApp({ supervisor: opts.supervisor, hub: opts.hub });
12701
- const preferred = opts.port ?? DEFAULT_PORT;
12702
- let lastErr;
12703
- for (let port = preferred; port < preferred + PORT_FALLBACK_SPAN; port++) {
12704
- try {
12705
- const server = await listen(app, port);
12706
- const url = `http://127.0.0.1:${port}`;
12707
- return {
12708
- url,
12709
- port,
12710
- close: () => new Promise((resolve2) => {
12711
- server.close(() => resolve2());
12712
- server.closeAllConnections?.();
12713
- })
12714
- };
12715
- } catch (err) {
12716
- if (isAddrInUse(err)) {
12717
- lastErr = err;
12718
- continue;
12719
- }
12720
- throw err;
12721
- }
12722
- }
12723
- throw new CompanionError(
12724
- `couldn't bind the dashboard to any port in ${preferred}\u2013${preferred + PORT_FALLBACK_SPAN - 1} (all in use). Free one up or pass --port. (last error: ${lastErr instanceof Error ? lastErr.message : String(lastErr)})`
12725
- );
12726
- }
12727
- function listen(app, port) {
12728
- return new Promise((resolve2, reject) => {
12729
- let settled = false;
12730
- const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, () => {
12731
- if (!settled) {
12732
- settled = true;
12733
- resolve2(server);
12734
- }
12735
- });
12736
- server.on("error", (err) => {
12737
- if (!settled) {
12738
- settled = true;
12739
- reject(err);
12740
- }
12741
- });
12742
- });
12743
- }
12744
- function isAddrInUse(err) {
12745
- return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
12746
- }
12747
- function resolveStaticDir() {
12748
- return join11(dirname6(fileURLToPath4(import.meta.url)), "static");
12749
- }
12750
-
12751
- // src/prereqs.ts
12752
- init_config();
12753
- init_bundled_binaries();
12754
- function warnAboutHarnessReadiness(cfg, deps = {}) {
12755
- const resolveBundled = deps.resolveBundled ?? ((runtime) => resolveBundledBinary(runtime));
12756
- const warn = deps.warn ?? ((message2) => process.stderr.write(`${message2}
12757
- `));
12758
- for (const runtime of ["claude-code", "codex"]) {
12759
- if (!harnessAllowed(cfg, runtime)) continue;
12760
- const resolution = resolveBundled(runtime);
12761
- if (resolution.status !== "present") warn(startupWarning(resolution));
12762
- }
12763
- }
12764
-
12765
10504
  // src/supervisor.ts
12766
10505
  init_errors();
12767
10506
 
@@ -12964,27 +10703,27 @@ init_config();
12964
10703
  init_config();
12965
10704
  import {
12966
10705
  chmodSync as chmodSync3,
12967
- existsSync as existsSync7,
10706
+ existsSync as existsSync6,
12968
10707
  mkdirSync as mkdirSync5,
12969
10708
  readFileSync as readFileSync4,
12970
10709
  renameSync as renameSync3,
12971
10710
  rmSync as rmSync4,
12972
10711
  writeFileSync as writeFileSync3
12973
10712
  } from "fs";
12974
- import { join as join12 } from "path";
10713
+ import { join as join10 } from "path";
12975
10714
  import { z as z5 } from "zod";
12976
10715
  var credentialMapSchema = z5.record(z5.string(), z5.string());
12977
10716
  function credentialsDir() {
12978
- return join12(cabaneDir(), "credentials");
10717
+ return join10(cabaneDir(), "credentials");
12979
10718
  }
12980
10719
  function legacyPath() {
12981
- return join12(cabaneDir(), "credentials.json");
10720
+ return join10(cabaneDir(), "credentials.json");
12982
10721
  }
12983
10722
  function devicePath(deviceId) {
12984
- return join12(credentialsDir(), `${deviceId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
10723
+ return join10(credentialsDir(), `${deviceId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
12985
10724
  }
12986
10725
  function read(path) {
12987
- if (!existsSync7(path)) return null;
10726
+ if (!existsSync6(path)) return null;
12988
10727
  let raw;
12989
10728
  try {
12990
10729
  raw = readFileSync4(path, "utf8");
@@ -13049,23 +10788,23 @@ function pruneCredentials(deviceId, keepAgentIds) {
13049
10788
  }
13050
10789
  function clearCredentials() {
13051
10790
  rmSync4(credentialsDir(), { recursive: true, force: true });
13052
- if (existsSync7(legacyPath())) writeFileSync3(legacyPath(), "", { mode: 384 });
10791
+ if (existsSync6(legacyPath())) writeFileSync3(legacyPath(), "", { mode: 384 });
13053
10792
  }
13054
10793
 
13055
10794
  // src/dispatch-dedupe.ts
13056
10795
  init_config();
13057
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync8 } from "fs";
13058
- import { join as join13 } from "path";
10796
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync7 } from "fs";
10797
+ import { join as join11 } from "path";
13059
10798
  var MAX_IDS = 256;
13060
10799
  function dir(log2) {
13061
- return join13(cabaneDir(), log2);
10800
+ return join11(cabaneDir(), log2);
13062
10801
  }
13063
10802
  function pathFor(log2, workspaceId) {
13064
- return join13(dir(log2), encodeURIComponent(workspaceId));
10803
+ return join11(dir(log2), encodeURIComponent(workspaceId));
13065
10804
  }
13066
10805
  function readIds(log2, workspaceId) {
13067
10806
  const path = pathFor(log2, workspaceId);
13068
- if (!existsSync8(path)) return [];
10807
+ if (!existsSync7(path)) return [];
13069
10808
  try {
13070
10809
  return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
13071
10810
  } catch {
@@ -13098,15 +10837,15 @@ function markCompleted(workspaceId, eventId) {
13098
10837
  }
13099
10838
  var MAX_RESUME_ATTEMPTS = 3;
13100
10839
  function resumeDir() {
13101
- return join13(cabaneDir(), "resume-attempts");
10840
+ return join11(cabaneDir(), "resume-attempts");
13102
10841
  }
13103
10842
  function resumePathFor(workspaceId) {
13104
- return join13(resumeDir(), encodeURIComponent(workspaceId));
10843
+ return join11(resumeDir(), encodeURIComponent(workspaceId));
13105
10844
  }
13106
10845
  function readResumeCounts(workspaceId) {
13107
10846
  const out = /* @__PURE__ */ new Map();
13108
10847
  const path = resumePathFor(workspaceId);
13109
- if (!existsSync8(path)) return out;
10848
+ if (!existsSync7(path)) return out;
13110
10849
  try {
13111
10850
  for (const line of readFileSync5(path, "utf8").split("\n")) {
13112
10851
  const trimmed = line.trim();
@@ -13137,16 +10876,16 @@ function bumpResumeAttempt(workspaceId, eventId) {
13137
10876
  return next;
13138
10877
  }
13139
10878
  function turnDir() {
13140
- return join13(cabaneDir(), "turns");
10879
+ return join11(cabaneDir(), "turns");
13141
10880
  }
13142
10881
  function turnPathFor(workspaceId) {
13143
- return join13(turnDir(), encodeURIComponent(workspaceId));
10882
+ return join11(turnDir(), encodeURIComponent(workspaceId));
13144
10883
  }
13145
10884
  var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13146
10885
  function readTurnIds(workspaceId) {
13147
10886
  const out = /* @__PURE__ */ new Map();
13148
10887
  const path = turnPathFor(workspaceId);
13149
- if (!existsSync8(path)) return out;
10888
+ if (!existsSync7(path)) return out;
13150
10889
  try {
13151
10890
  for (const line of readFileSync5(path, "utf8").split("\n")) {
13152
10891
  const trimmed = line.trim();
@@ -13255,6 +10994,32 @@ init_boot_id();
13255
10994
  init_turn_containment();
13256
10995
  init_protocol();
13257
10996
  import { createInterface } from "readline";
10997
+
10998
+ // src/credential-broker/client-config.ts
10999
+ import { parse, stringify } from "smol-toml";
11000
+ var BROKERED_CREDENTIAL_NAMES = /* @__PURE__ */ new Set([
11001
+ "ANTHROPIC_API_KEY",
11002
+ "ANTHROPIC_AUTH_TOKEN",
11003
+ "CLAUDE_CODE_OAUTH_TOKEN",
11004
+ "CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
11005
+ "CLAUDE_CODE_SESSION_ACCESS_TOKEN",
11006
+ "CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
11007
+ "CLAUDE_CODE_HOST_CREDS_FILE",
11008
+ "CODEX_API_KEY",
11009
+ "OPENAI_API_KEY",
11010
+ "OPENAI_ACCESS_TOKEN",
11011
+ "GH_TOKEN",
11012
+ "GITHUB_TOKEN",
11013
+ "GH_ENTERPRISE_TOKEN",
11014
+ "GITHUB_ENTERPRISE_TOKEN"
11015
+ ]);
11016
+ function withoutBrokeredCredentials(values) {
11017
+ return Object.fromEntries(
11018
+ Object.entries(values).filter(([name]) => !BROKERED_CREDENTIAL_NAMES.has(name))
11019
+ );
11020
+ }
11021
+
11022
+ // src/runner/launch.ts
13258
11023
  function gzipBodiesSignal() {
13259
11024
  let on = false;
13260
11025
  const listeners = /* @__PURE__ */ new Set();
@@ -13517,12 +11282,22 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
13517
11282
  }
13518
11283
  try {
13519
11284
  const wrapped = launch.wrapped === true;
13520
- const env = runnerExecEnv(launch.env ?? process.env, wrapped);
13521
- const grants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
11285
+ if (launch.brokeredCredentials && !wrapped)
11286
+ throw new Error("credential broker requires a wrapped runner");
11287
+ const inherited = runnerExecEnv(launch.env ?? process.env, wrapped);
11288
+ const env = launch.brokeredCredentials ? withoutBrokeredCredentials(inherited) : inherited;
11289
+ const directGrants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
11290
+ const grants = launch.brokeredCredentials ? Object.fromEntries(
11291
+ Object.entries(directGrants).map(([key, values]) => [
11292
+ key,
11293
+ withoutBrokeredCredentials(values)
11294
+ ])
11295
+ ) : directGrants;
11296
+ const secrets = wrapped ? launch.secrets?.() ?? {} : void 0;
13522
11297
  const input = {
13523
11298
  baseUrl: opts.baseUrl,
13524
11299
  credential: minted.credential,
13525
- ...wrapped ? { secrets: launch.secrets?.() ?? {} } : {},
11300
+ ...secrets ? { secrets: launch.brokeredCredentials ? withoutBrokeredCredentials(secrets) : secrets } : {},
13526
11301
  ...Object.keys(grants).length > 0 ? { grants } : {},
13527
11302
  dispatch: payload,
13528
11303
  turnId,
@@ -13788,7 +11563,7 @@ var Dispatcher = class {
13788
11563
  };
13789
11564
 
13790
11565
  // src/host-report.ts
13791
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
11566
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
13792
11567
  import { userInfo as userInfo2 } from "os";
13793
11568
  function inContainer(signals) {
13794
11569
  if (signals.dockerenv) return true;
@@ -13803,7 +11578,7 @@ function readContainerSignals() {
13803
11578
  } catch {
13804
11579
  cgroup = null;
13805
11580
  }
13806
- return { dockerenv: existsSync13("/.dockerenv"), cgroup, env: process.env };
11581
+ return { dockerenv: existsSync12("/.dockerenv"), cgroup, env: process.env };
13807
11582
  }
13808
11583
  function osUser() {
13809
11584
  try {
@@ -14213,8 +11988,8 @@ init_transcript_writer();
14213
11988
 
14214
11989
  // src/updater.ts
14215
11990
  import { spawn as spawn6 } from "child_process";
14216
- import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
14217
- import { dirname as dirname11, join as join26 } from "path";
11991
+ import { copyFileSync as copyFileSync2, existsSync as existsSync15, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
11992
+ import { dirname as dirname10, join as join24 } from "path";
14218
11993
 
14219
11994
  // src/boot-rule.ts
14220
11995
  import {
@@ -14226,7 +12001,7 @@ import {
14226
12001
  symlinkSync,
14227
12002
  writeFileSync as writeFileSync7
14228
12003
  } from "fs";
14229
- import { dirname as dirname8, join as join23 } from "path";
12004
+ import { dirname as dirname7, join as join21 } from "path";
14230
12005
  var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
14231
12006
  var MAX_STAGE_ATTEMPTS = 3;
14232
12007
  function superseded(pending, ...running) {
@@ -14250,7 +12025,7 @@ function rolledBackState(state, pending, reason, now) {
14250
12025
  // src/install-layout.ts
14251
12026
  init_config();
14252
12027
  import {
14253
- existsSync as existsSync15,
12028
+ existsSync as existsSync14,
14254
12029
  lstatSync,
14255
12030
  mkdirSync as mkdirSync12,
14256
12031
  readFileSync as readFileSync13,
@@ -14260,32 +12035,32 @@ import {
14260
12035
  rmSync as rmSync9,
14261
12036
  symlinkSync as symlinkSync2
14262
12037
  } from "fs";
14263
- import { dirname as dirname9, join as join24 } from "path";
14264
- import { fileURLToPath as fileURLToPath5 } from "url";
12038
+ import { dirname as dirname8, join as join22 } from "path";
12039
+ import { fileURLToPath as fileURLToPath4 } from "url";
14265
12040
  function versionsDir() {
14266
- return join24(cabaneDir(), "versions");
12041
+ return join22(cabaneDir(), "versions");
14267
12042
  }
14268
12043
  function currentLink() {
14269
- return join24(cabaneDir(), "current");
12044
+ return join22(cabaneDir(), "current");
14270
12045
  }
14271
12046
  function bootGuardPath() {
14272
- return join24(cabaneDir(), "boot-guard.mjs");
12047
+ return join22(cabaneDir(), "boot-guard.mjs");
14273
12048
  }
14274
12049
  function pinnedNodeDir() {
14275
- return join24(cabaneDir(), "node");
12050
+ return join22(cabaneDir(), "node");
14276
12051
  }
14277
12052
  function pinnedNodePath() {
14278
- return join24(pinnedNodeDir(), "bin", "node");
12053
+ return join22(pinnedNodeDir(), "bin", "node");
14279
12054
  }
14280
12055
  function versionPackageRoot(version) {
14281
- return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
12056
+ return join22(versionsDir(), version, "node_modules", "@cabane", "companion");
14282
12057
  }
14283
12058
  function ownPackageRoot(moduleUrl = import.meta.url) {
14284
- return dirname9(dirname9(fileURLToPath5(moduleUrl)));
12059
+ return dirname8(dirname8(fileURLToPath4(moduleUrl)));
14285
12060
  }
14286
12061
  function packageVersion(root) {
14287
12062
  try {
14288
- const pkg2 = JSON.parse(readFileSync13(join24(root, "package.json"), "utf8"));
12063
+ const pkg2 = JSON.parse(readFileSync13(join22(root, "package.json"), "utf8"));
14289
12064
  return typeof pkg2.version === "string" ? pkg2.version : null;
14290
12065
  } catch {
14291
12066
  return null;
@@ -14294,14 +12069,14 @@ function packageVersion(root) {
14294
12069
  function currentRoot() {
14295
12070
  try {
14296
12071
  const root = realpathSync5(currentLink());
14297
- return existsSync15(join24(root, "dist", "cli.js")) ? root : null;
12072
+ return existsSync14(join22(root, "dist", "cli.js")) ? root : null;
14298
12073
  } catch {
14299
12074
  return null;
14300
12075
  }
14301
12076
  }
14302
12077
  function pointCurrentAt(root) {
14303
12078
  const link = currentLink();
14304
- mkdirSync12(dirname9(link), { recursive: true });
12079
+ mkdirSync12(dirname8(link), { recursive: true });
14305
12080
  const fresh = `${link}.${process.pid}.${Date.now()}`;
14306
12081
  symlinkSync2(root, fresh);
14307
12082
  try {
@@ -14313,11 +12088,11 @@ function pointCurrentAt(root) {
14313
12088
  }
14314
12089
  function pruneVersions(keep) {
14315
12090
  const dir2 = versionsDir();
14316
- if (!existsSync15(dir2)) return [];
12091
+ if (!existsSync14(dir2)) return [];
14317
12092
  const removed = [];
14318
12093
  for (const name of readdirSync3(dir2)) {
14319
12094
  if (keep.includes(name)) continue;
14320
- rmSync9(join24(dir2, name), { recursive: true, force: true });
12095
+ rmSync9(join22(dir2, name), { recursive: true, force: true });
14321
12096
  removed.push(name);
14322
12097
  }
14323
12098
  return removed;
@@ -14337,31 +12112,31 @@ function compareVersions(a, b) {
14337
12112
  // src/update-state.ts
14338
12113
  init_config();
14339
12114
  import { mkdirSync as mkdirSync13, readFileSync as readFileSync14, renameSync as renameSync8, writeFileSync as writeFileSync8 } from "fs";
14340
- import { dirname as dirname10, join as join25 } from "path";
14341
- import { z as z18 } from "zod";
14342
- var resultSchema = z18.object({
14343
- kind: z18.enum(["updated", "rolled_back", "failed"]),
14344
- from: z18.string(),
14345
- to: z18.string(),
14346
- at: z18.string(),
14347
- reason: z18.string().optional()
12115
+ import { dirname as dirname9, join as join23 } from "path";
12116
+ import { z as z17 } from "zod";
12117
+ var resultSchema = z17.object({
12118
+ kind: z17.enum(["updated", "rolled_back", "failed"]),
12119
+ from: z17.string(),
12120
+ to: z17.string(),
12121
+ at: z17.string(),
12122
+ reason: z17.string().optional()
14348
12123
  });
14349
- var pendingSchema = z18.object({
14350
- from: z18.string(),
12124
+ var pendingSchema = z17.object({
12125
+ from: z17.string(),
14351
12126
  // The package root `current` pointed at before the swap, so a rollback can point it back.
14352
- fromRoot: z18.string(),
14353
- to: z18.string(),
14354
- at: z18.string(),
14355
- boots: z18.number().int().nonnegative()
12127
+ fromRoot: z17.string(),
12128
+ to: z17.string(),
12129
+ at: z17.string(),
12130
+ boots: z17.number().int().nonnegative()
14356
12131
  });
14357
- var stateSchema = z18.object({
14358
- server: z18.object({ latest: z18.string(), minimum: z18.string(), at: z18.string() }).optional(),
12132
+ var stateSchema = z17.object({
12133
+ server: z17.object({ latest: z17.string(), minimum: z17.string(), at: z17.string() }).optional(),
14359
12134
  pending: pendingSchema.optional(),
14360
12135
  lastResult: resultSchema.optional(),
14361
- failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
12136
+ failed: z17.object({ version: z17.string(), attempts: z17.number().int(), at: z17.string() }).optional()
14362
12137
  });
14363
12138
  function updateStatePath(dir2 = cabaneDir()) {
14364
- return join25(dir2, "update.json");
12139
+ return join23(dir2, "update.json");
14365
12140
  }
14366
12141
  function readUpdateState(path = updateStatePath()) {
14367
12142
  try {
@@ -14372,7 +12147,7 @@ function readUpdateState(path = updateStatePath()) {
14372
12147
  }
14373
12148
  }
14374
12149
  function writeUpdateState(state, path = updateStatePath()) {
14375
- mkdirSync13(dirname10(path), { recursive: true });
12150
+ mkdirSync13(dirname9(path), { recursive: true });
14376
12151
  const tmp = `${path}.${process.pid}.tmp`;
14377
12152
  writeFileSync8(tmp, `${JSON.stringify(state, null, 2)}
14378
12153
  `, { mode: 384 });
@@ -14406,14 +12181,14 @@ function versionToStage(input) {
14406
12181
  return latest;
14407
12182
  }
14408
12183
  function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
14409
- if (existsSync16(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
12184
+ if (existsSync15(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
14410
12185
  return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
14411
12186
  }
14412
12187
  function writeBootGuard(root = ownPackageRoot()) {
14413
- const built = join26(root, "dist", "boot-guard.js");
14414
- if (!existsSync16(built)) return false;
12188
+ const built = join24(root, "dist", "boot-guard.js");
12189
+ if (!existsSync15(built)) return false;
14415
12190
  const target = bootGuardPath();
14416
- mkdirSync14(dirname11(target), { recursive: true });
12191
+ mkdirSync14(dirname10(target), { recursive: true });
14417
12192
  const tmp = `${target}.${process.pid}.tmp`;
14418
12193
  copyFileSync2(built, tmp);
14419
12194
  renameSync9(tmp, target);
@@ -14450,17 +12225,17 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
14450
12225
  async function stageVersion(version, harnesses, deps = {}) {
14451
12226
  const run = deps.run ?? runProcess;
14452
12227
  const node = deps.execPath ?? process.execPath;
14453
- const target = join26(versionsDir(), version);
12228
+ const target = join24(versionsDir(), version);
14454
12229
  const placed = versionPackageRoot(version);
14455
- if (existsSync16(placed)) {
12230
+ if (existsSync15(placed)) {
14456
12231
  if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
14457
12232
  return { ok: true, root: placed };
14458
12233
  }
14459
- const npm = join26(dirname11(node), "npm");
14460
- if (!existsSync16(npm))
14461
- return { ok: false, reason: `there is no npm beside Node at ${dirname11(node)}` };
12234
+ const npm = join24(dirname10(node), "npm");
12235
+ if (!existsSync15(npm))
12236
+ return { ok: false, reason: `there is no npm beside Node at ${dirname10(node)}` };
14462
12237
  mkdirSync14(versionsDir(), { recursive: true });
14463
- const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
12238
+ const staging = join24(versionsDir(), `.${version}.staging-${process.pid}`);
14464
12239
  rmSync10(staging, { recursive: true, force: true });
14465
12240
  const installed = await run(
14466
12241
  npm,
@@ -14477,7 +12252,7 @@ async function stageVersion(version, harnesses, deps = {}) {
14477
12252
  ],
14478
12253
  {
14479
12254
  // npm's install scripts run on this Node, not whichever `node` is first on PATH.
14480
- env: { ...process.env, PATH: `${dirname11(node)}:${process.env.PATH ?? ""}` },
12255
+ env: { ...process.env, PATH: `${dirname10(node)}:${process.env.PATH ?? ""}` },
14481
12256
  timeoutMs: 15 * 6e4
14482
12257
  }
14483
12258
  );
@@ -14489,7 +12264,7 @@ async function stageVersion(version, harnesses, deps = {}) {
14489
12264
  return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
14490
12265
  return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
14491
12266
  }
14492
- const root = join26(staging, "node_modules", "@cabane", "companion");
12267
+ const root = join24(staging, "node_modules", "@cabane", "companion");
14493
12268
  const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
14494
12269
  if (verdict) {
14495
12270
  rmSync10(staging, { recursive: true, force: true });
@@ -14501,7 +12276,7 @@ async function stageVersion(version, harnesses, deps = {}) {
14501
12276
  }
14502
12277
  async function verifyInstall(root, version, harnesses, deps) {
14503
12278
  if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
14504
- const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
12279
+ const answered = await deps.run(deps.execPath, [join24(root, "dist", "bin.js"), "--version"], {
14505
12280
  timeoutMs: 6e4,
14506
12281
  env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
14507
12282
  });
@@ -14509,7 +12284,7 @@ async function verifyInstall(root, version, harnesses, deps) {
14509
12284
  return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
14510
12285
  }
14511
12286
  if (harnesses.length === 0) return null;
14512
- const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
12287
+ const probed = await deps.run(deps.execPath, [join24(root, "dist", "postinstall.js"), "--json"], {
14513
12288
  timeoutMs: 6e4
14514
12289
  });
14515
12290
  let resolutions = {};
@@ -15526,6 +13301,7 @@ var CompanionSupervisor = class {
15526
13301
  ),
15527
13302
  ...this.config.runner?.command ? {
15528
13303
  wrapped: true,
13304
+ brokeredCredentials: this.config.runner.brokeredCredentials,
15529
13305
  secrets: () => loadSecretValuesTolerant(
15530
13306
  (m) => this.log.warn({ err: m }, "Could not read the secrets file for a wrapped turn.")
15531
13307
  )
@@ -16283,10 +14059,10 @@ function handleUncaught(log2, err, origin) {
16283
14059
 
16284
14060
  // src/crash-marker.ts
16285
14061
  init_config();
16286
- import { existsSync as existsSync17, mkdirSync as mkdirSync15, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync9 } from "fs";
16287
- import { join as join27 } from "path";
14062
+ import { existsSync as existsSync16, mkdirSync as mkdirSync15, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync9 } from "fs";
14063
+ import { join as join25 } from "path";
16288
14064
  function crashMarkerPath() {
16289
- return join27(cabaneDir(), "last-error.json");
14065
+ return join25(cabaneDir(), "last-error.json");
16290
14066
  }
16291
14067
  function recordCrash(rec2) {
16292
14068
  try {
@@ -16298,7 +14074,7 @@ function recordCrash(rec2) {
16298
14074
  function clearCrash() {
16299
14075
  try {
16300
14076
  const path = crashMarkerPath();
16301
- if (existsSync17(path)) rmSync11(path, { force: true });
14077
+ if (existsSync16(path)) rmSync11(path, { force: true });
16302
14078
  } catch {
16303
14079
  }
16304
14080
  }
@@ -16307,7 +14083,7 @@ function clearCrash() {
16307
14083
  init_config();
16308
14084
  import { spawnSync } from "child_process";
16309
14085
  import { readFileSync as readFileSync16 } from "fs";
16310
- import { join as join28 } from "path";
14086
+ import { join as join26 } from "path";
16311
14087
  var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
16312
14088
  var START = /\sstart(\s|$)/;
16313
14089
  function companionStarts(psOutput) {
@@ -16324,7 +14100,7 @@ function companionStarts(psOutput) {
16324
14100
  }
16325
14101
  function stateDirOf(env) {
16326
14102
  const home = env.CABANE_COMPANION_HOME || env.HOME;
16327
- return home ? join28(home, ".cabane") : null;
14103
+ return home ? join26(home, ".cabane") : null;
16328
14104
  }
16329
14105
  function envFromPsE(output, command) {
16330
14106
  const line = output.replace(/\n$/, "");
@@ -16394,9 +14170,10 @@ function strayCompanionLines(strays, retry) {
16394
14170
  }
16395
14171
 
16396
14172
  // src/runtime.ts
14173
+ init_errors();
16397
14174
  function embeddedRunner(host) {
16398
14175
  if (!host.electron) return null;
16399
- const cli = fileURLToPath6(new URL("./cli.js", host.moduleUrl));
14176
+ const cli = fileURLToPath5(new URL("./cli.js", host.moduleUrl));
16400
14177
  return {
16401
14178
  runnerCommand: [host.execPath, cli, "run-turn"],
16402
14179
  runnerEnv: { ...host.env, ELECTRON_RUN_AS_NODE: "1" }
@@ -16491,11 +14268,12 @@ async function createCompanionRuntime(opts = {}) {
16491
14268
  update: (auto) => active2.update(auto),
16492
14269
  stop: () => void stop2().then(() => active2.requestStop())
16493
14270
  });
16494
- if (opts.dashboard) {
16495
- const preferredPort = opts.port ?? cfg.dashboardPort;
16496
- dashboard = await startDashboard({
14271
+ if (opts.startDashboard) {
14272
+ const preferredPort = cfg.dashboardPort;
14273
+ dashboard = await opts.startDashboard({
16497
14274
  supervisor,
16498
14275
  hub,
14276
+ logPath: companionLogPath(),
16499
14277
  ...preferredPort !== void 0 ? { port: preferredPort } : {}
16500
14278
  });
16501
14279
  hub.setDashboardUrl(dashboard.url);
@@ -16591,7 +14369,7 @@ import { createHash as createHash3 } from "crypto";
16591
14369
  import {
16592
14370
  chmodSync as chmodSync5,
16593
14371
  cpSync,
16594
- existsSync as existsSync18,
14372
+ existsSync as existsSync17,
16595
14373
  mkdirSync as mkdirSync16,
16596
14374
  readFileSync as readFileSync17,
16597
14375
  renameSync as renameSync10,
@@ -16599,7 +14377,7 @@ import {
16599
14377
  writeFileSync as writeFileSync10
16600
14378
  } from "fs";
16601
14379
  import { homedir as homedir9, userInfo as userInfo3 } from "os";
16602
- import { dirname as dirname12, join as join29, sep as sep3 } from "path";
14380
+ import { dirname as dirname11, join as join27, sep as sep3 } from "path";
16603
14381
  function servicePlatform(platform = process.platform) {
16604
14382
  if (platform === "linux") return "systemd";
16605
14383
  if (platform === "darwin") return "launchd";
@@ -16614,18 +14392,18 @@ function launchdLabel(env = process.env) {
16614
14392
  return serviceName(env).replace(/^cabane-companion/, "ai.cabane.companion").replace("-", ".");
16615
14393
  }
16616
14394
  function serviceNode(execPath = process.execPath) {
16617
- return existsSync18(pinnedNodePath()) ? pinnedNodePath() : execPath;
14395
+ return existsSync17(pinnedNodePath()) ? pinnedNodePath() : execPath;
16618
14396
  }
16619
14397
  function servicePaths(platform, env = process.env, home = homedir9(), node = serviceNode()) {
16620
- const unitFile = platform === "systemd" ? join29(
16621
- env.XDG_CONFIG_HOME || join29(home, ".config"),
14398
+ const unitFile = platform === "systemd" ? join27(
14399
+ env.XDG_CONFIG_HOME || join27(home, ".config"),
16622
14400
  "systemd",
16623
14401
  "user",
16624
14402
  `${serviceName(env)}.service`
16625
- ) : join29(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
14403
+ ) : join27(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
16626
14404
  return {
16627
14405
  unitFile,
16628
- envFile: join29(cabaneDir(), "service.env"),
14406
+ envFile: join27(cabaneDir(), "service.env"),
16629
14407
  node,
16630
14408
  entry: bootGuardPath()
16631
14409
  };
@@ -16692,7 +14470,7 @@ function renderLaunchdPlist(paths, env = process.env, servicePath) {
16692
14470
  " <key>ThrottleInterval</key><integer>5</integer>",
16693
14471
  // The companion writes its own log; the console copy would only grow a second one.
16694
14472
  " <key>StandardOutPath</key><string>/dev/null</string>",
16695
- ` <key>StandardErrorPath</key><string>${xml(join29(cabaneDir(), "service-error.log"))}</string>`,
14473
+ ` <key>StandardErrorPath</key><string>${xml(join27(cabaneDir(), "service-error.log"))}</string>`,
16696
14474
  "</dict>",
16697
14475
  "</plist>",
16698
14476
  ""
@@ -16747,11 +14525,11 @@ async function installService(deps = {}) {
16747
14525
  rewriteScriptWrappers();
16748
14526
  const paths = servicePaths(platform, env, deps.home, node);
16749
14527
  const servicePath = await loginShellPath(run, env);
16750
- mkdirSync16(dirname12(paths.envFile), { recursive: true });
16751
- const existing = existsSync18(paths.envFile) ? readFileSync17(paths.envFile, "utf8") : "";
14528
+ mkdirSync16(dirname11(paths.envFile), { recursive: true });
14529
+ const existing = existsSync17(paths.envFile) ? readFileSync17(paths.envFile, "utf8") : "";
16752
14530
  writeFileSync10(paths.envFile, withServicePath(existing, servicePath), { mode: 384 });
16753
14531
  chmodSync5(paths.envFile, 384);
16754
- mkdirSync16(dirname12(paths.unitFile), { recursive: true });
14532
+ mkdirSync16(dirname11(paths.unitFile), { recursive: true });
16755
14533
  const outcome = await writeAndStartUnit(platform, paths, env, servicePath, run);
16756
14534
  if (!outcome.ok) await removeUnit(platform, paths, env, run);
16757
14535
  return outcome;
@@ -16804,7 +14582,7 @@ async function uninstallService(deps = {}) {
16804
14582
  const platform = servicePlatform(deps.platform);
16805
14583
  if (!platform) return { ok: true, lines: [] };
16806
14584
  const paths = servicePaths(platform, env, deps.home);
16807
- if (!existsSync18(paths.unitFile)) return { ok: true, lines: ["No Cabane service is installed."] };
14585
+ if (!existsSync17(paths.unitFile)) return { ok: true, lines: ["No Cabane service is installed."] };
16808
14586
  await removeUnit(platform, paths, env, run);
16809
14587
  return { ok: true, lines: [] };
16810
14588
  }
@@ -16831,16 +14609,16 @@ async function seedManagedInstall(deps) {
16831
14609
  return { ok: true, root: own };
16832
14610
  }
16833
14611
  const target = versionPackageRoot(version);
16834
- if (!existsSync18(target)) {
16835
- const staging = join29(versionsDir(), `.${version}.seed-${process.pid}`);
14612
+ if (!existsSync17(target)) {
14613
+ const staging = join27(versionsDir(), `.${version}.seed-${process.pid}`);
16836
14614
  rmSync12(staging, { recursive: true, force: true });
16837
14615
  try {
16838
- cpSync(own, join29(staging, "node_modules", "@cabane", "companion"), {
14616
+ cpSync(own, join27(staging, "node_modules", "@cabane", "companion"), {
16839
14617
  recursive: true,
16840
14618
  verbatimSymlinks: true
16841
14619
  });
16842
- rmSync12(join29(versionsDir(), version), { recursive: true, force: true });
16843
- renameSync10(staging, join29(versionsDir(), version));
14620
+ rmSync12(join27(versionsDir(), version), { recursive: true, force: true });
14621
+ renameSync10(staging, join27(versionsDir(), version));
16844
14622
  } catch (err) {
16845
14623
  rmSync12(staging, { recursive: true, force: true });
16846
14624
  return {
@@ -16851,7 +14629,7 @@ async function seedManagedInstall(deps) {
16851
14629
  }
16852
14630
  const unrunnable = await verifyInstall(target, version, [], deps);
16853
14631
  if (unrunnable) {
16854
- rmSync12(join29(versionsDir(), version), { recursive: true, force: true });
14632
+ rmSync12(join27(versionsDir(), version), { recursive: true, force: true });
16855
14633
  return { ok: false, reason: `The copy of Cabane for the service can't run: ${unrunnable}.` };
16856
14634
  }
16857
14635
  pointCurrentAt(target);
@@ -16885,9 +14663,9 @@ async function serviceState(deps = {}) {
16885
14663
  };
16886
14664
  if (!platform) return none;
16887
14665
  const paths = servicePaths(platform, env, deps.home);
16888
- if (!existsSync18(paths.unitFile)) return none;
14666
+ if (!existsSync17(paths.unitFile)) return none;
16889
14667
  const node = unitNode(platform, paths.unitFile);
16890
- const nodeOk = node === null || existsSync18(node);
14668
+ const nodeOk = node === null || existsSync17(node);
16891
14669
  if (platform === "systemd") {
16892
14670
  const r2 = await run(
16893
14671
  "systemctl",
@@ -16919,7 +14697,7 @@ async function serviceState(deps = {}) {
16919
14697
  function serviceInstalled(deps = {}) {
16920
14698
  const platform = servicePlatform(deps.platform);
16921
14699
  if (!platform) return false;
16922
- return existsSync18(servicePaths(platform, deps.env ?? process.env, deps.home).unitFile);
14700
+ return existsSync17(servicePaths(platform, deps.env ?? process.env, deps.home).unitFile);
16923
14701
  }
16924
14702
  function writeAtomic(path, content) {
16925
14703
  const tmp = `${path}.${process.pid}.tmp`;
@@ -16933,9 +14711,9 @@ function userName() {
16933
14711
  return process.env.USER ?? "";
16934
14712
  }
16935
14713
  }
16936
- function rewriteScriptWrappers(dir2 = join29(cabaneDir(), "bin")) {
14714
+ function rewriteScriptWrappers(dir2 = join27(cabaneDir(), "bin")) {
16937
14715
  for (const name of ["cabane", "cabane-companion"]) {
16938
- const path = join29(dir2, name);
14716
+ const path = join27(dir2, name);
16939
14717
  let text;
16940
14718
  try {
16941
14719
  text = readFileSync17(path, "utf8");
@@ -16986,7 +14764,7 @@ init_cabane_command();
16986
14764
  init_cli_entry();
16987
14765
  init_config();
16988
14766
  import { spawn as spawn7 } from "child_process";
16989
- import { closeSync as closeSync3, mkdirSync as mkdirSync17, openSync as openSync4 } from "fs";
14767
+ import { closeSync as closeSync2, mkdirSync as mkdirSync17, openSync as openSync3 } from "fs";
16990
14768
  init_config();
16991
14769
  async function startDaemon(opts = {}, deps = {}) {
16992
14770
  const ensureRuntime = deps.ensureRuntime ?? warnAboutHarnessReadiness;
@@ -17065,7 +14843,7 @@ function alreadyRunningLines(pid) {
17065
14843
  function defaultSpawnDetached(args) {
17066
14844
  const cliPath = companionCliEntry();
17067
14845
  mkdirSync17(cabaneDir(), { recursive: true });
17068
- const logFd = openSync4(companionLogPath(), "a");
14846
+ const logFd = openSync3(companionLogPath(), "a");
17069
14847
  try {
17070
14848
  const child = spawn7(process.execPath, [cliPath, ...args], {
17071
14849
  detached: true,
@@ -17081,7 +14859,7 @@ function defaultSpawnDetached(args) {
17081
14859
  });
17082
14860
  return { pid: child.pid, unref: () => child.unref(), failure: () => failure };
17083
14861
  } finally {
17084
- closeSync3(logFd);
14862
+ closeSync2(logFd);
17085
14863
  }
17086
14864
  }
17087
14865
 
@@ -17592,9 +15370,9 @@ async function logs(opts = {}) {
17592
15370
  init_cabane_command();
17593
15371
  init_install_kind();
17594
15372
  init_cabane_command();
17595
- import { readdirSync as readdirSync4, realpathSync as realpathSync6, existsSync as existsSync19 } from "fs";
15373
+ import { readdirSync as readdirSync4, realpathSync as realpathSync6, existsSync as existsSync18 } from "fs";
17596
15374
  import { homedir as homedir10 } from "os";
17597
- import { dirname as dirname13, join as join30 } from "path";
15375
+ import { dirname as dirname12, join as join28 } from "path";
17598
15376
  init_config();
17599
15377
  init_secrets();
17600
15378
  init_term();
@@ -17878,10 +15656,10 @@ function versionLine() {
17878
15656
  const install = companionInstall();
17879
15657
  const own = installedCommandPath({ install });
17880
15658
  if (install === "script") {
17881
- return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join30(homedir10(), ".cabane"))})`;
15659
+ return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join28(homedir10(), ".cabane"))})`;
17882
15660
  }
17883
15661
  if (own)
17884
- return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname13(dirname13(own)))})`;
15662
+ return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname12(dirname12(own)))})`;
17885
15663
  return `Cabane ${COMPANION_VERSION}`;
17886
15664
  }
17887
15665
  function otherInstallLines(running) {
@@ -17904,7 +15682,7 @@ function otherInstall(install, own) {
17904
15682
  }
17905
15683
  };
17906
15684
  if (install === "npm") {
17907
- const script = join30(
15685
+ const script = join28(
17908
15686
  homedir10(),
17909
15687
  ".cabane",
17910
15688
  "node",
@@ -17913,12 +15691,12 @@ function otherInstall(install, own) {
17913
15691
  "@cabane",
17914
15692
  "companion"
17915
15693
  );
17916
- return existsSync19(script) ? join30(homedir10(), ".cabane") : null;
15694
+ return existsSync18(script) ? join28(homedir10(), ".cabane") : null;
17917
15695
  }
17918
15696
  const found = firstOnPath("cabane-companion", process.env.PATH);
17919
15697
  if (!found) return null;
17920
- const ownAlias = own ? real(join30(dirname13(own), "cabane-companion")) : null;
17921
- return real(found) === ownAlias ? null : dirname13(found);
15698
+ const ownAlias = own ? real(join28(dirname12(own), "cabane-companion")) : null;
15699
+ return real(found) === ownAlias ? null : dirname12(found);
17922
15700
  }
17923
15701
  function runningMode(running, service2) {
17924
15702
  if (service2.installed) {
@@ -18123,8 +15901,8 @@ init_cabane_command();
18123
15901
  init_errors();
18124
15902
  init_src();
18125
15903
  init_transcript_writer();
18126
- import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "fs";
18127
- import { basename as basename4, isAbsolute as isAbsolute2, join as join31 } from "path";
15904
+ import { existsSync as existsSync19, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "fs";
15905
+ import { basename as basename4, isAbsolute as isAbsolute2, join as join29 } from "path";
18128
15906
  async function transcript(opts = {}) {
18129
15907
  const dir2 = transcriptsDir();
18130
15908
  if (opts.follow) {
@@ -18141,7 +15919,7 @@ async function transcript(opts = {}) {
18141
15919
  process.stdout.write(emptyMessage(dir2));
18142
15920
  return;
18143
15921
  }
18144
- process.stdout.write(renderFile(join31(dir2, newest)) + "\n");
15922
+ process.stdout.write(renderFile(join29(dir2, newest)) + "\n");
18145
15923
  return;
18146
15924
  }
18147
15925
  printList(dir2);
@@ -18224,7 +16002,7 @@ function isComplete(content) {
18224
16002
  async function followTranscripts(dir2) {
18225
16003
  const follower = new TranscriptFollower({
18226
16004
  listFiles: () => listFiles(dir2),
18227
- read: (f) => readFileSync18(join31(dir2, f), "utf8"),
16005
+ read: (f) => readFileSync18(join29(dir2, f), "utf8"),
18228
16006
  write: (s) => process.stdout.write(s),
18229
16007
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
18230
16008
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -18260,7 +16038,7 @@ function listTranscriptFiles(dir2) {
18260
16038
  if (!entry.isDirectory() || entry.name === "anomalies") continue;
18261
16039
  let inner;
18262
16040
  try {
18263
- inner = readdirSync5(join31(dir2, entry.name));
16041
+ inner = readdirSync5(join29(dir2, entry.name));
18264
16042
  } catch {
18265
16043
  continue;
18266
16044
  }
@@ -18279,7 +16057,7 @@ function printList(dir2) {
18279
16057
 
18280
16058
  `);
18281
16059
  for (const f of files.slice(0, 20)) {
18282
- const { meta, outcome } = peek(join31(dir2, f));
16060
+ const { meta, outcome } = peek(join29(dir2, f));
18283
16061
  const when = fmtTime(rec(meta)?.ts);
18284
16062
  const ws = str2(rec(meta)?.workspaceSlug);
18285
16063
  const o = rec(outcome);
@@ -18314,14 +16092,14 @@ function peek(path) {
18314
16092
  }
18315
16093
  function resolveTarget(dir2, target) {
18316
16094
  if (isAbsolute2(target) || target.includes("/")) {
18317
- if (existsSync20(target)) return target;
18318
- if (!isAbsolute2(target) && existsSync20(join31(dir2, target))) return join31(dir2, target);
16095
+ if (existsSync19(target)) return target;
16096
+ if (!isAbsolute2(target) && existsSync19(join29(dir2, target))) return join29(dir2, target);
18319
16097
  throw new CompanionError(`no transcript at ${target}.`);
18320
16098
  }
18321
- const exact = join31(dir2, target);
18322
- if (existsSync20(exact)) return exact;
16099
+ const exact = join29(dir2, target);
16100
+ if (existsSync19(exact)) return exact;
18323
16101
  const matches = listFiles(dir2).filter((f) => f.includes(target));
18324
- if (matches.length === 1) return join31(dir2, matches[0]);
16102
+ if (matches.length === 1) return join29(dir2, matches[0]);
18325
16103
  if (matches.length === 0) {
18326
16104
  throw new CompanionError(
18327
16105
  `no transcript matching "${target}" in ${dir2}. Run \`${cabane("transcript")}\` to list them.`