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