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