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