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