@cabane/companion 0.6.61 → 0.6.62
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 +105 -338
- package/dist/runtime.js +73 -329
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3061,6 +3061,15 @@ var turnEventSchema = z5.discriminatedUnion("type", [
|
|
|
3061
3061
|
effort: z5.string().optional(),
|
|
3062
3062
|
thinking: z5.string().optional(),
|
|
3063
3063
|
reasoningEffort: z5.string().optional()
|
|
3064
|
+
}).optional(),
|
|
3065
|
+
// CT1275: the harness-reported MCP inventory from this turn's init frame.
|
|
3066
|
+
// This is deliberately diagnostic-only: names, statuses and a count, never
|
|
3067
|
+
// server definitions, credentials or session ids. `initReceived:false`
|
|
3068
|
+
// distinguishes a missing init frame from a real empty inventory.
|
|
3069
|
+
mcpInventory: z5.object({
|
|
3070
|
+
initReceived: z5.boolean(),
|
|
3071
|
+
servers: z5.array(z5.object({ name: z5.string(), status: z5.string() })),
|
|
3072
|
+
toolCount: z5.number().int().nonnegative()
|
|
3064
3073
|
}).optional()
|
|
3065
3074
|
})
|
|
3066
3075
|
]);
|
|
@@ -3079,6 +3088,7 @@ var turnResultReasonSchema = z6.discriminatedUnion("kind", [
|
|
|
3079
3088
|
z6.object({ kind: z6.literal("timeout_total") }),
|
|
3080
3089
|
z6.object({ kind: z6.literal("cancelled") }),
|
|
3081
3090
|
z6.object({ kind: z6.literal("skipped") }),
|
|
3091
|
+
z6.object({ kind: z6.literal("workspace_tools_missing") }),
|
|
3082
3092
|
z6.object({ kind: z6.literal("runtime_error") })
|
|
3083
3093
|
]);
|
|
3084
3094
|
var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
|
|
@@ -3104,7 +3114,12 @@ var turnDiagnosticsSchema = z6.object({
|
|
|
3104
3114
|
result: z6.number().int().nonnegative()
|
|
3105
3115
|
}),
|
|
3106
3116
|
runtimeResultKind: z6.enum(turnRuntimeResultKinds).nullable(),
|
|
3107
|
-
finalSource: z6.enum(turnFinalSources)
|
|
3117
|
+
finalSource: z6.enum(turnFinalSources),
|
|
3118
|
+
mcpInventory: z6.object({
|
|
3119
|
+
initReceived: z6.boolean(),
|
|
3120
|
+
servers: z6.array(z6.object({ name: z6.string(), status: z6.string() })),
|
|
3121
|
+
toolCount: z6.number().int().nonnegative()
|
|
3122
|
+
}).optional()
|
|
3108
3123
|
});
|
|
3109
3124
|
|
|
3110
3125
|
// packages/agent-runtime/src/failure.ts
|
|
@@ -3238,6 +3253,8 @@ function normalizeTurnResultReason(reason) {
|
|
|
3238
3253
|
return { kind: "cancelled" };
|
|
3239
3254
|
case "skipped":
|
|
3240
3255
|
return { kind: "skipped" };
|
|
3256
|
+
case "workspace_tools_missing":
|
|
3257
|
+
return { kind: "workspace_tools_missing" };
|
|
3241
3258
|
default:
|
|
3242
3259
|
return { kind: "runtime_error" };
|
|
3243
3260
|
}
|
|
@@ -3342,12 +3359,7 @@ var turnRequestSchema = z8.object({
|
|
|
3342
3359
|
// adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
|
|
3343
3360
|
// always populates it (`build-options.ts`), and the native adapter fails the
|
|
3344
3361
|
// turn loudly when it is somehow absent rather than guessing.
|
|
3345
|
-
workspaceId: z8.string().optional()
|
|
3346
|
-
// CT752: the server-resolved workspace surface this credential exposes.
|
|
3347
|
-
// Readiness uses this explicit fact to require `sdk` for code mode and the
|
|
3348
|
-
// granular floor for classic mode; inventory contents alone cannot infer it
|
|
3349
|
-
// because `sdk` is intentionally also available on the classic surface.
|
|
3350
|
-
workspaceToolSurface: z8.enum(["code", "classic"]).optional()
|
|
3362
|
+
workspaceId: z8.string().optional()
|
|
3351
3363
|
}),
|
|
3352
3364
|
// Machine-local resolution (host-filled): the checkout cwd, extra env from a
|
|
3353
3365
|
// prepare hook, and the resolved user MCP servers.
|
|
@@ -3932,6 +3944,8 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3932
3944
|
out.push(event);
|
|
3933
3945
|
};
|
|
3934
3946
|
let sessionEmitted = false;
|
|
3947
|
+
let workspaceProven = false;
|
|
3948
|
+
let mcpInventory = { initReceived: false, servers: [], toolCount: 0 };
|
|
3935
3949
|
let ok = false;
|
|
3936
3950
|
let resultReason;
|
|
3937
3951
|
let sawResult = false;
|
|
@@ -3949,6 +3963,12 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3949
3963
|
if (msg.type === "system" && msg.subtype === "init") {
|
|
3950
3964
|
const initModel = msg.model;
|
|
3951
3965
|
if (typeof initModel === "string" && initModel.length > 0) resolvedModel = initModel;
|
|
3966
|
+
mcpInventory = readMcpInventory(msg);
|
|
3967
|
+
workspaceProven = provesWorkspaceTools(msg);
|
|
3968
|
+
if (!workspaceProven) {
|
|
3969
|
+
resultReason = "workspace_tools_missing";
|
|
3970
|
+
break;
|
|
3971
|
+
}
|
|
3952
3972
|
const sdkSessionId = msg.session_id;
|
|
3953
3973
|
if (!sessionEmitted && sdkSessionId && sdkSessionId !== ctx.resumedSessionId) {
|
|
3954
3974
|
sessionEmitted = true;
|
|
@@ -3958,6 +3978,9 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3958
3978
|
...ctx.degraded ? { degraded: true } : {}
|
|
3959
3979
|
};
|
|
3960
3980
|
}
|
|
3981
|
+
} else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
|
|
3982
|
+
resultReason = "workspace_tools_missing";
|
|
3983
|
+
break;
|
|
3961
3984
|
} else if (msg.type === "assistant") {
|
|
3962
3985
|
const assistantErr = msg.error;
|
|
3963
3986
|
if (typeof assistantErr === "string" && assistantErr.length > 0) {
|
|
@@ -4021,6 +4044,10 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
4021
4044
|
sawResult = true;
|
|
4022
4045
|
}
|
|
4023
4046
|
if (ctx.signal.aborted) return;
|
|
4047
|
+
if (!workspaceProven) {
|
|
4048
|
+
ok = false;
|
|
4049
|
+
resultReason ??= "workspace_tools_missing";
|
|
4050
|
+
}
|
|
4024
4051
|
await flushHeldText(buffer, emit, ok);
|
|
4025
4052
|
yield* drain(out);
|
|
4026
4053
|
if (!ok && !resultReason && !sawResult) resultReason = "no_result";
|
|
@@ -4029,9 +4056,25 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
4029
4056
|
ok,
|
|
4030
4057
|
...resultReason ? { reason: resultReason } : {},
|
|
4031
4058
|
...usage ? { usage } : {},
|
|
4032
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
4059
|
+
...resolvedModel ? { resolvedModel } : {},
|
|
4060
|
+
mcpInventory
|
|
4061
|
+
};
|
|
4062
|
+
}
|
|
4063
|
+
function readMcpInventory(msg) {
|
|
4064
|
+
const frame = msg;
|
|
4065
|
+
const servers = Array.isArray(frame.mcp_servers) ? frame.mcp_servers.flatMap(
|
|
4066
|
+
(server) => typeof server?.name === "string" && typeof server.status === "string" ? [{ name: server.name, status: server.status }] : []
|
|
4067
|
+
) : [];
|
|
4068
|
+
return {
|
|
4069
|
+
initReceived: true,
|
|
4070
|
+
servers,
|
|
4071
|
+
toolCount: Array.isArray(frame.tools) ? frame.tools.length : 0
|
|
4033
4072
|
};
|
|
4034
4073
|
}
|
|
4074
|
+
function provesWorkspaceTools(msg) {
|
|
4075
|
+
const tools = msg.tools;
|
|
4076
|
+
return Array.isArray(tools) && tools.includes("mcp__cabane__sdk");
|
|
4077
|
+
}
|
|
4035
4078
|
function isSubscriptionWindow(value) {
|
|
4036
4079
|
return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
|
|
4037
4080
|
}
|
|
@@ -4130,6 +4173,11 @@ var CABANE_POLICY = {
|
|
|
4130
4173
|
uiPrompts: "never"
|
|
4131
4174
|
};
|
|
4132
4175
|
var CWD = "/env/here";
|
|
4176
|
+
var HEALTHY_MCP_INVENTORY = {
|
|
4177
|
+
initReceived: true,
|
|
4178
|
+
servers: [{ name: "cabane", status: "connected" }],
|
|
4179
|
+
toolCount: 1
|
|
4180
|
+
};
|
|
4133
4181
|
function makeRequest(overrides = {}) {
|
|
4134
4182
|
return {
|
|
4135
4183
|
systemPrompt: "system",
|
|
@@ -4150,7 +4198,8 @@ var init = (sessionId, model) => ({
|
|
|
4150
4198
|
type: "system",
|
|
4151
4199
|
subtype: "init",
|
|
4152
4200
|
session_id: sessionId,
|
|
4153
|
-
mcp_servers:
|
|
4201
|
+
mcp_servers: HEALTHY_MCP_INVENTORY.servers,
|
|
4202
|
+
tools: ["mcp__cabane__sdk"],
|
|
4154
4203
|
...model ? { model } : {}
|
|
4155
4204
|
});
|
|
4156
4205
|
var assistantText = (text) => ({
|
|
@@ -4211,7 +4260,7 @@ var sessionEvent = (sdkSessionId, cwd = CWD, degraded = false) => ({
|
|
|
4211
4260
|
state: encodeSession({ sdkSessionId, cwd }),
|
|
4212
4261
|
...degraded ? { degraded: true } : {}
|
|
4213
4262
|
});
|
|
4214
|
-
var
|
|
4263
|
+
var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
4215
4264
|
{
|
|
4216
4265
|
// A plain text reply: the held text-only block flushes as the terminal final.
|
|
4217
4266
|
name: "clean turn",
|
|
@@ -4591,6 +4640,12 @@ var CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4591
4640
|
]
|
|
4592
4641
|
}
|
|
4593
4642
|
];
|
|
4643
|
+
var CLAUDE_CODE_CONFORMANCE_FIXTURES = BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES.map((fixture) => ({
|
|
4644
|
+
...fixture,
|
|
4645
|
+
expected: fixture.expected.map(
|
|
4646
|
+
(event) => event.type === "result" ? { ...event, mcpInventory: HEALTHY_MCP_INVENTORY } : event
|
|
4647
|
+
)
|
|
4648
|
+
}));
|
|
4594
4649
|
|
|
4595
4650
|
// packages/agent-runtime/src/registry.ts
|
|
4596
4651
|
function createAdapterRegistry(adapters) {
|
|
@@ -7096,7 +7151,7 @@ var ConnectorHealthStore = class {
|
|
|
7096
7151
|
|
|
7097
7152
|
// src/dispatcher.ts
|
|
7098
7153
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
7099
|
-
import {
|
|
7154
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, statSync } from "fs";
|
|
7100
7155
|
import { join as join14 } from "path";
|
|
7101
7156
|
|
|
7102
7157
|
// src/turn-control-tools.ts
|
|
@@ -7433,7 +7488,6 @@ function buildCompanionTurnRequest(params) {
|
|
|
7433
7488
|
bearer: params.turnToken ?? params.agentPat,
|
|
7434
7489
|
activeConversationId: params.activeConversationId,
|
|
7435
7490
|
workspaceId: params.workspaceId,
|
|
7436
|
-
...t.workspaceToolSurface ? { workspaceToolSurface: t.workspaceToolSurface } : {},
|
|
7437
7491
|
// CT714: mount the turn-control surface ONLY when a real turn token backs
|
|
7438
7492
|
// this turn — the surface admits `turn_token` auth exclusively, so a
|
|
7439
7493
|
// PAT-fallback bearer would be rejected there. Absent it, external adapters
|
|
@@ -7909,173 +7963,6 @@ var TurnCommitter = class {
|
|
|
7909
7963
|
}
|
|
7910
7964
|
};
|
|
7911
7965
|
|
|
7912
|
-
// src/workspace-readiness.ts
|
|
7913
|
-
var CLASSIC_REQUIRED = ["read", "list", "search", "write", "edit"];
|
|
7914
|
-
var INITIALIZE_RETRY_DELAYS_MS = [250, 750, 1500];
|
|
7915
|
-
var INITIALIZE_ATTEMPT_TIMEOUT_MS = 4e3;
|
|
7916
|
-
var INITIALIZE_RETRY_BUDGET_MS = 1e4;
|
|
7917
|
-
async function proveWorkspaceTools(req, runtime, opts = {}) {
|
|
7918
|
-
const base = {
|
|
7919
|
-
ok: false,
|
|
7920
|
-
proofType: "authenticated_mcp_tools_list",
|
|
7921
|
-
runtime,
|
|
7922
|
-
harnessFingerprint: opts.harnessFingerprint ?? runtime,
|
|
7923
|
-
endpoint: safeEndpoint(req.cabane.mcpUrl),
|
|
7924
|
-
initialized: false,
|
|
7925
|
-
authenticated: false,
|
|
7926
|
-
discoveredTools: [],
|
|
7927
|
-
requiredTools: [],
|
|
7928
|
-
acceptedNames: ["sdk", "mcp__cabane__sdk"],
|
|
7929
|
-
failedCapability: null,
|
|
7930
|
-
detail: null
|
|
7931
|
-
};
|
|
7932
|
-
if (!req.cabane.mcpUrl) return fail(base, "server_not_configured", "Cabane MCP URL absent");
|
|
7933
|
-
if (!req.cabane.bearer) return fail(base, "authentication_failed", "Cabane bearer absent");
|
|
7934
|
-
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
7935
|
-
const headers = {
|
|
7936
|
-
authorization: `Bearer ${req.cabane.bearer}`,
|
|
7937
|
-
accept: "application/json, text/event-stream",
|
|
7938
|
-
"content-type": "application/json",
|
|
7939
|
-
"x-cabane-active-conversation": req.cabane.activeConversationId
|
|
7940
|
-
};
|
|
7941
|
-
try {
|
|
7942
|
-
const initialized = await initializeWithRetry(
|
|
7943
|
-
fetchImpl,
|
|
7944
|
-
req.cabane.mcpUrl,
|
|
7945
|
-
headers,
|
|
7946
|
-
{
|
|
7947
|
-
jsonrpc: "2.0",
|
|
7948
|
-
id: 1,
|
|
7949
|
-
method: "initialize",
|
|
7950
|
-
params: {
|
|
7951
|
-
protocolVersion: "2025-03-26",
|
|
7952
|
-
capabilities: {},
|
|
7953
|
-
clientInfo: { name: "cabane-companion-readiness", version: "1" }
|
|
7954
|
-
}
|
|
7955
|
-
},
|
|
7956
|
-
opts.retryDelaysMs ?? INITIALIZE_RETRY_DELAYS_MS
|
|
7957
|
-
);
|
|
7958
|
-
if (initialized.status === 401 || initialized.status === 403)
|
|
7959
|
-
return fail(base, "authentication_failed", `initialize returned HTTP ${initialized.status}`);
|
|
7960
|
-
if (initialized.transient)
|
|
7961
|
-
return fail(base, "workspace_endpoint_unreachable", initialized.detail);
|
|
7962
|
-
if (!initialized.ok) return fail(base, "initialization_failed", initialized.detail);
|
|
7963
|
-
base.initialized = true;
|
|
7964
|
-
base.authenticated = true;
|
|
7965
|
-
if (initialized.sessionId) headers["mcp-session-id"] = initialized.sessionId;
|
|
7966
|
-
const listed = await rpc(fetchImpl, req.cabane.mcpUrl, headers, {
|
|
7967
|
-
jsonrpc: "2.0",
|
|
7968
|
-
id: 2,
|
|
7969
|
-
method: "tools/list",
|
|
7970
|
-
params: {}
|
|
7971
|
-
});
|
|
7972
|
-
if (listed.status === 401 || listed.status === 403)
|
|
7973
|
-
return fail(base, "authentication_failed", `tools/list returned HTTP ${listed.status}`);
|
|
7974
|
-
if (!listed.ok) return fail(base, "tool_discovery_failed", listed.detail);
|
|
7975
|
-
const result = asRecord3(asRecord3(listed.value)?.result);
|
|
7976
|
-
const tools = Array.isArray(result?.tools) ? result.tools : null;
|
|
7977
|
-
if (!tools) return fail(base, "tool_discovery_failed", "tools/list returned no tool inventory");
|
|
7978
|
-
base.discoveredTools = tools.map(
|
|
7979
|
-
(tool2) => tool2 && typeof tool2 === "object" && typeof tool2.name === "string" ? tool2.name : null
|
|
7980
|
-
).filter((name) => name !== null).sort();
|
|
7981
|
-
if (!req.cabane.workspaceToolSurface)
|
|
7982
|
-
return fail(base, "required_tool_missing", "resolved workspace tool surface absent");
|
|
7983
|
-
base.requiredTools = req.cabane.workspaceToolSurface === "code" ? ["sdk"] : CLASSIC_REQUIRED;
|
|
7984
|
-
const missing = base.requiredTools.filter((name) => !base.discoveredTools.includes(name));
|
|
7985
|
-
if (missing.length > 0)
|
|
7986
|
-
return fail(
|
|
7987
|
-
base,
|
|
7988
|
-
"required_tool_missing",
|
|
7989
|
-
`missing initialized tools: ${missing.join(", ")}`
|
|
7990
|
-
);
|
|
7991
|
-
base.ok = true;
|
|
7992
|
-
return base;
|
|
7993
|
-
} catch (error) {
|
|
7994
|
-
return fail(
|
|
7995
|
-
base,
|
|
7996
|
-
"workspace_endpoint_unreachable",
|
|
7997
|
-
error instanceof Error ? error.message : String(error)
|
|
7998
|
-
);
|
|
7999
|
-
}
|
|
8000
|
-
}
|
|
8001
|
-
async function initializeWithRetry(fetchImpl, url, headers, body, retryDelaysMs) {
|
|
8002
|
-
let lastFailure = null;
|
|
8003
|
-
const deadline = Date.now() + INITIALIZE_RETRY_BUDGET_MS;
|
|
8004
|
-
for (let attempt = 0; attempt <= retryDelaysMs.length; attempt += 1) {
|
|
8005
|
-
try {
|
|
8006
|
-
const remainingMs = deadline - Date.now();
|
|
8007
|
-
if (remainingMs <= 0) break;
|
|
8008
|
-
const result = await rpc(
|
|
8009
|
-
fetchImpl,
|
|
8010
|
-
url,
|
|
8011
|
-
headers,
|
|
8012
|
-
body,
|
|
8013
|
-
Math.min(INITIALIZE_ATTEMPT_TIMEOUT_MS, remainingMs)
|
|
8014
|
-
);
|
|
8015
|
-
if (result.status === 401 || result.status === 403 || result.ok) return result;
|
|
8016
|
-
if (result.status < 500) return result;
|
|
8017
|
-
lastFailure = { ...result, transient: true };
|
|
8018
|
-
} catch (error) {
|
|
8019
|
-
lastFailure = {
|
|
8020
|
-
ok: false,
|
|
8021
|
-
status: 0,
|
|
8022
|
-
sessionId: null,
|
|
8023
|
-
value: null,
|
|
8024
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
8025
|
-
transient: true
|
|
8026
|
-
};
|
|
8027
|
-
}
|
|
8028
|
-
const retryDelayMs = retryDelaysMs[attempt];
|
|
8029
|
-
if (retryDelayMs === void 0 || Date.now() + retryDelayMs >= deadline) break;
|
|
8030
|
-
await delay(retryDelayMs);
|
|
8031
|
-
}
|
|
8032
|
-
return lastFailure;
|
|
8033
|
-
}
|
|
8034
|
-
function delay(ms) {
|
|
8035
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8036
|
-
}
|
|
8037
|
-
function fail(proof, capability, detail) {
|
|
8038
|
-
proof.failedCapability = capability;
|
|
8039
|
-
proof.detail = detail.slice(0, 300);
|
|
8040
|
-
return proof;
|
|
8041
|
-
}
|
|
8042
|
-
function safeEndpoint(value) {
|
|
8043
|
-
try {
|
|
8044
|
-
const url = new URL(value);
|
|
8045
|
-
return `${url.origin}${url.pathname}`;
|
|
8046
|
-
} catch {
|
|
8047
|
-
return null;
|
|
8048
|
-
}
|
|
8049
|
-
}
|
|
8050
|
-
async function rpc(fetchImpl, url, headers, body, timeoutMs) {
|
|
8051
|
-
const response = await fetchImpl(url, {
|
|
8052
|
-
method: "POST",
|
|
8053
|
-
headers,
|
|
8054
|
-
body: JSON.stringify(body),
|
|
8055
|
-
...timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}
|
|
8056
|
-
});
|
|
8057
|
-
const text = await response.text();
|
|
8058
|
-
const value = parseRpcBody(text);
|
|
8059
|
-
return {
|
|
8060
|
-
ok: response.ok && !!value && !value.error,
|
|
8061
|
-
status: response.status,
|
|
8062
|
-
sessionId: response.headers.get("mcp-session-id"),
|
|
8063
|
-
value,
|
|
8064
|
-
detail: typeof asRecord3(value?.error)?.message === "string" ? String(asRecord3(value?.error)?.message) : `HTTP ${response.status}`
|
|
8065
|
-
};
|
|
8066
|
-
}
|
|
8067
|
-
function parseRpcBody(text) {
|
|
8068
|
-
const trimmed = text.trim();
|
|
8069
|
-
if (trimmed.startsWith("{")) return JSON.parse(trimmed);
|
|
8070
|
-
for (const line of trimmed.split("\n")) {
|
|
8071
|
-
if (line.startsWith("data:")) return JSON.parse(line.slice(5).trim());
|
|
8072
|
-
}
|
|
8073
|
-
return null;
|
|
8074
|
-
}
|
|
8075
|
-
function asRecord3(value) {
|
|
8076
|
-
return value !== null && typeof value === "object" ? value : null;
|
|
8077
|
-
}
|
|
8078
|
-
|
|
8079
7966
|
// src/dispatcher.ts
|
|
8080
7967
|
var PREPARING_TOOL_NAME = "preparing";
|
|
8081
7968
|
var PREPARE_FAILED_PREFIX = "**Couldn't prepare your environment.** I wasn't able to provision a working directory for this conversation, so I can't run this turn. The provisioning command reported:";
|
|
@@ -8568,152 +8455,7 @@ ${reason}`,
|
|
|
8568
8455
|
`runtime_unavailable:${err.runtime}`
|
|
8569
8456
|
);
|
|
8570
8457
|
}
|
|
8571
|
-
let turnReceiptPath = null;
|
|
8572
8458
|
const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
|
|
8573
|
-
const closeTurnReceipt = (ok, reason) => {
|
|
8574
|
-
if (!turnReceiptPath) return;
|
|
8575
|
-
const target = turnReceiptPath;
|
|
8576
|
-
turnReceiptPath = null;
|
|
8577
|
-
try {
|
|
8578
|
-
appendFileSync2(
|
|
8579
|
-
target,
|
|
8580
|
-
`${JSON.stringify({
|
|
8581
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8582
|
-
event: "settled",
|
|
8583
|
-
turnId,
|
|
8584
|
-
agentId: payload.agentId,
|
|
8585
|
-
conversationId: payload.conversationId,
|
|
8586
|
-
ok,
|
|
8587
|
-
reason
|
|
8588
|
-
})}
|
|
8589
|
-
`,
|
|
8590
|
-
{ mode: 384 }
|
|
8591
|
-
);
|
|
8592
|
-
} catch (error) {
|
|
8593
|
-
turnLog.warn(
|
|
8594
|
-
{ err: error instanceof Error ? error.message : String(error) },
|
|
8595
|
-
"dispatcher: turn-settled diagnostic write failed"
|
|
8596
|
-
);
|
|
8597
|
-
}
|
|
8598
|
-
};
|
|
8599
|
-
if (prepareHook && hookEnv?.CABANE_TASK_ID) {
|
|
8600
|
-
const checkout = checkoutState(effectiveCwd);
|
|
8601
|
-
if (!effectiveCwd || !checkout.ok) {
|
|
8602
|
-
const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
|
|
8603
|
-
turnLog.error({ checkout: effectiveCwd ?? null, checkoutState: checkout }, reason);
|
|
8604
|
-
try {
|
|
8605
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8606
|
-
body: `**Couldn't prepare your environment.** ${reason}`,
|
|
8607
|
-
kind: "final",
|
|
8608
|
-
turnId,
|
|
8609
|
-
parentMessageId: payload.messageId
|
|
8610
|
-
});
|
|
8611
|
-
} catch (postErr) {
|
|
8612
|
-
turnLog.warn(
|
|
8613
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8614
|
-
"dispatcher: checkout-missing notice post failed"
|
|
8615
|
-
);
|
|
8616
|
-
}
|
|
8617
|
-
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
8618
|
-
}
|
|
8619
|
-
const receiptPath = join14(effectiveCwd, ".git", "cabane", "readiness.jsonl");
|
|
8620
|
-
const receiptLine = (fields) => `${JSON.stringify({
|
|
8621
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8622
|
-
taskId: hookEnv.CABANE_TASK_ID,
|
|
8623
|
-
binding: hookEnv.CABANE_TASK_BINDING ?? null,
|
|
8624
|
-
checkout: effectiveCwd,
|
|
8625
|
-
// CT1022: the two fields the environment reaper reads — which turn this
|
|
8626
|
-
// is (so its settle can be matched among interleaved agents) and how long
|
|
8627
|
-
// it may legitimately run (so an unclosed receipt expires on this turn's
|
|
8628
|
-
// real deadline, not the reaper's guess).
|
|
8629
|
-
turnId,
|
|
8630
|
-
totalTimeoutMs,
|
|
8631
|
-
// CT1062: WHOSE turn. A task env is shared — between agents, and between
|
|
8632
|
-
// an agent's own sequential conversations — so a reader asking "is a turn
|
|
8633
|
-
// of THIS agent, other than mine, running here?" (the host's run-lock
|
|
8634
|
-
// does, before it lets a second conversation into the tree) can only
|
|
8635
|
-
// answer it if the line says who. An unattributed open line has to count
|
|
8636
|
-
// for everyone, which refuses work that should have been admitted.
|
|
8637
|
-
agentId: payload.agentId,
|
|
8638
|
-
conversationId: payload.conversationId,
|
|
8639
|
-
...fields
|
|
8640
|
-
})}
|
|
8641
|
-
`;
|
|
8642
|
-
try {
|
|
8643
|
-
mkdirSync10(join14(effectiveCwd, ".git", "cabane"), { recursive: true });
|
|
8644
|
-
appendFileSync2(
|
|
8645
|
-
receiptPath,
|
|
8646
|
-
// `starting` is the honest classification before the proof has run. The
|
|
8647
|
-
// line the proof appends below carries the same `turnId`, so a reader
|
|
8648
|
-
// replaying the file sees one turn, not two.
|
|
8649
|
-
receiptLine({ classification: "starting" }),
|
|
8650
|
-
{ mode: 384 }
|
|
8651
|
-
);
|
|
8652
|
-
turnReceiptPath = receiptPath;
|
|
8653
|
-
} catch (error) {
|
|
8654
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
8655
|
-
const reason = `turn_receipt_unwritable: ${detail}; task=${hookEnv.CABANE_TASK_ID}; checkout=${effectiveCwd}; recovery=restore write access to the checkout's .git/cabane, then re-dispatch`;
|
|
8656
|
-
turnLog.error({ err: detail, receiptPath }, reason);
|
|
8657
|
-
try {
|
|
8658
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8659
|
-
body: `**Couldn't prepare your environment.** ${reason}`,
|
|
8660
|
-
kind: "final",
|
|
8661
|
-
turnId,
|
|
8662
|
-
parentMessageId: payload.messageId
|
|
8663
|
-
});
|
|
8664
|
-
} catch (postErr) {
|
|
8665
|
-
turnLog.warn(
|
|
8666
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8667
|
-
"dispatcher: turn-receipt failure notice post failed"
|
|
8668
|
-
);
|
|
8669
|
-
}
|
|
8670
|
-
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
8671
|
-
}
|
|
8672
|
-
const proof = await proveWorkspaceTools(request, adapter.name, {
|
|
8673
|
-
...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
|
|
8674
|
-
...this.opts.workspaceProofRetryDelaysMs ? { retryDelaysMs: this.opts.workspaceProofRetryDelaysMs } : {},
|
|
8675
|
-
harnessFingerprint: turnContext.runtime
|
|
8676
|
-
});
|
|
8677
|
-
turnLog[proof.ok ? "info" : "error"](
|
|
8678
|
-
{ workspaceProof: proof, checkout: effectiveCwd },
|
|
8679
|
-
`dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout usable`
|
|
8680
|
-
);
|
|
8681
|
-
try {
|
|
8682
|
-
appendFileSync2(
|
|
8683
|
-
receiptPath,
|
|
8684
|
-
receiptLine({
|
|
8685
|
-
classification: proof.ok ? "ready" : "workspace_tools_missing",
|
|
8686
|
-
failedCapability: proof.failedCapability,
|
|
8687
|
-
workspaceTools: proof
|
|
8688
|
-
}),
|
|
8689
|
-
{ mode: 384 }
|
|
8690
|
-
);
|
|
8691
|
-
} catch (error) {
|
|
8692
|
-
turnLog.warn(
|
|
8693
|
-
{ err: error instanceof Error ? error.message : String(error) },
|
|
8694
|
-
"dispatcher: workspace-proof diagnostic write failed (the turn receipt is open)"
|
|
8695
|
-
);
|
|
8696
|
-
}
|
|
8697
|
-
if (!proof.ok) {
|
|
8698
|
-
const recovery = proof.failedCapability === "workspace_endpoint_unreachable" ? "the Cabane workspace endpoint was unreachable; retry this dispatch" : "restart the connector after restoring the Cabane workspace tool mount";
|
|
8699
|
-
const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd}; runtime=${adapter.name}; recovery=${recovery}`;
|
|
8700
|
-
closeTurnReceipt(false, reason);
|
|
8701
|
-
try {
|
|
8702
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8703
|
-
body: `**Couldn't prepare your environment.** ${reason}`,
|
|
8704
|
-
kind: "final",
|
|
8705
|
-
turnId,
|
|
8706
|
-
parentMessageId: payload.messageId
|
|
8707
|
-
});
|
|
8708
|
-
} catch (postErr) {
|
|
8709
|
-
turnLog.warn(
|
|
8710
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8711
|
-
"dispatcher: workspace-proof failure notice post failed"
|
|
8712
|
-
);
|
|
8713
|
-
}
|
|
8714
|
-
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
8715
|
-
}
|
|
8716
|
-
}
|
|
8717
8459
|
const transcript2 = this.opts.transcriptDir ? new TranscriptWriter(
|
|
8718
8460
|
this.opts.transcriptDir,
|
|
8719
8461
|
{
|
|
@@ -8736,6 +8478,7 @@ ${reason}`,
|
|
|
8736
8478
|
let turnUsage;
|
|
8737
8479
|
let turnResolvedModel;
|
|
8738
8480
|
let turnResolvedConfig;
|
|
8481
|
+
let turnMcpInventory;
|
|
8739
8482
|
const eventCounts = {
|
|
8740
8483
|
session: 0,
|
|
8741
8484
|
text: 0,
|
|
@@ -8901,6 +8644,7 @@ ${reason}`,
|
|
|
8901
8644
|
turnUsage = event.usage;
|
|
8902
8645
|
turnResolvedModel = event.resolvedModel;
|
|
8903
8646
|
turnResolvedConfig = event.resolvedConfig;
|
|
8647
|
+
turnMcpInventory = event.mcpInventory;
|
|
8904
8648
|
runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
|
|
8905
8649
|
} else if (event.type === "text" && skipState.skipped) {
|
|
8906
8650
|
} else {
|
|
@@ -8977,7 +8721,6 @@ ${reason}`,
|
|
|
8977
8721
|
} finally {
|
|
8978
8722
|
if (idleTimer) clearTimeout(idleTimer);
|
|
8979
8723
|
clearTimeout(totalTimer);
|
|
8980
|
-
closeTurnReceipt(okResult, resultReason ?? null);
|
|
8981
8724
|
const userCancelled = abortController.signal.aborted && timeoutReason === null;
|
|
8982
8725
|
if (timeoutReason !== null) {
|
|
8983
8726
|
resultReason = timeoutReason;
|
|
@@ -9059,6 +8802,7 @@ ${reason}`,
|
|
|
9059
8802
|
sessionFingerprint: fingerprintSessionState(latestSessionState),
|
|
9060
8803
|
eventCounts,
|
|
9061
8804
|
runtimeResultKind,
|
|
8805
|
+
...turnMcpInventory ? { mcpInventory: turnMcpInventory } : {},
|
|
9062
8806
|
finalSource: outcome === "skipped" || outcome === "cancelled" || silentMarkerEmitted ? "marker" : committer.finalSource
|
|
9063
8807
|
};
|
|
9064
8808
|
body.diagnostics = settledDiagnostics;
|
|
@@ -9208,7 +8952,7 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
9208
8952
|
// src/outbox.ts
|
|
9209
8953
|
import {
|
|
9210
8954
|
existsSync as existsSync11,
|
|
9211
|
-
mkdirSync as
|
|
8955
|
+
mkdirSync as mkdirSync10,
|
|
9212
8956
|
readdirSync as readdirSync3,
|
|
9213
8957
|
readFileSync as readFileSync8,
|
|
9214
8958
|
renameSync as renameSync3,
|
|
@@ -9238,7 +8982,7 @@ var Outbox = class {
|
|
|
9238
8982
|
// per-workspace bounds.
|
|
9239
8983
|
persist(entry) {
|
|
9240
8984
|
const dir2 = this.dir();
|
|
9241
|
-
|
|
8985
|
+
mkdirSync10(dir2, { recursive: true });
|
|
9242
8986
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
9243
8987
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
9244
8988
|
try {
|
|
@@ -10509,14 +10253,14 @@ function handleUncaught(log, err, origin) {
|
|
|
10509
10253
|
}
|
|
10510
10254
|
|
|
10511
10255
|
// src/crash-marker.ts
|
|
10512
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
10256
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
10513
10257
|
import { join as join16 } from "path";
|
|
10514
10258
|
function crashMarkerPath() {
|
|
10515
10259
|
return join16(cabaneDir(), "last-error.json");
|
|
10516
10260
|
}
|
|
10517
10261
|
function recordCrash(rec2) {
|
|
10518
10262
|
try {
|
|
10519
|
-
|
|
10263
|
+
mkdirSync11(cabaneDir(), { recursive: true });
|
|
10520
10264
|
writeFileSync8(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
|
|
10521
10265
|
} catch {
|
|
10522
10266
|
}
|
|
@@ -10696,7 +10440,7 @@ async function closeSurfaces(control, dashboard) {
|
|
|
10696
10440
|
|
|
10697
10441
|
// src/commands/daemon.ts
|
|
10698
10442
|
import { spawn as spawn4 } from "child_process";
|
|
10699
|
-
import { closeSync as closeSync3, mkdirSync as
|
|
10443
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync12, openSync as openSync3 } from "fs";
|
|
10700
10444
|
|
|
10701
10445
|
// src/cli-entry.ts
|
|
10702
10446
|
import { existsSync as existsSync13 } from "fs";
|
|
@@ -10799,7 +10543,7 @@ Stop: cabane-companion stop
|
|
|
10799
10543
|
}
|
|
10800
10544
|
function defaultSpawnDetached(args) {
|
|
10801
10545
|
const cliPath = companionCliEntry();
|
|
10802
|
-
|
|
10546
|
+
mkdirSync12(cabaneDir(), { recursive: true });
|
|
10803
10547
|
const logFd = openSync3(companionLogPath(), "a");
|
|
10804
10548
|
try {
|
|
10805
10549
|
return spawn4(process.execPath, [cliPath, ...args], {
|
|
@@ -11439,12 +11183,12 @@ function renderTranscript(jsonlLines) {
|
|
|
11439
11183
|
case "system":
|
|
11440
11184
|
if (str2(obj.subtype) === "init") {
|
|
11441
11185
|
out.push(`[session ${str2(obj.session_id) || "?"} \xB7 model ${str2(obj.model) || "?"}]`);
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11186
|
+
out.push(
|
|
11187
|
+
...mcpInventoryLines(
|
|
11188
|
+
obj.mcp_servers,
|
|
11189
|
+
Array.isArray(obj.tools) ? obj.tools.length : void 0
|
|
11190
|
+
)
|
|
11191
|
+
);
|
|
11448
11192
|
out.push("");
|
|
11449
11193
|
}
|
|
11450
11194
|
break;
|
|
@@ -11477,11 +11221,24 @@ function renderTranscript(jsonlLines) {
|
|
|
11477
11221
|
break;
|
|
11478
11222
|
}
|
|
11479
11223
|
case "result": {
|
|
11480
|
-
const isErr = obj.is_error === true || str2(obj.subtype) !== "success";
|
|
11224
|
+
const isErr = typeof obj.ok === "boolean" ? obj.ok !== true : obj.is_error === true || str2(obj.subtype) !== "success";
|
|
11481
11225
|
const dur = typeof obj.duration_ms === "number" ? ` \xB7 ${obj.duration_ms}ms` : "";
|
|
11482
11226
|
out.push(
|
|
11483
11227
|
`[result ${isErr ? "error" : "ok"}${str2(obj.subtype) ? ` \xB7 ${str2(obj.subtype)}` : ""}${dur}]`
|
|
11484
11228
|
);
|
|
11229
|
+
const inventory = rec(obj.mcpInventory);
|
|
11230
|
+
if (inventory) {
|
|
11231
|
+
if (inventory.initReceived === false) {
|
|
11232
|
+
out.push(" MCP init: missing");
|
|
11233
|
+
} else {
|
|
11234
|
+
out.push(
|
|
11235
|
+
...mcpInventoryLines(
|
|
11236
|
+
inventory.servers,
|
|
11237
|
+
typeof inventory.toolCount === "number" ? inventory.toolCount : void 0
|
|
11238
|
+
)
|
|
11239
|
+
);
|
|
11240
|
+
}
|
|
11241
|
+
}
|
|
11485
11242
|
if (isErr && str2(obj.result).trim()) out.push(` ${indent(str2(obj.result))}`);
|
|
11486
11243
|
break;
|
|
11487
11244
|
}
|
|
@@ -11496,6 +11253,16 @@ function renderTranscript(jsonlLines) {
|
|
|
11496
11253
|
}
|
|
11497
11254
|
return out.join("\n");
|
|
11498
11255
|
}
|
|
11256
|
+
function mcpInventoryLines(serversValue, toolCount) {
|
|
11257
|
+
const servers = Array.isArray(serversValue) ? serversValue.map((server) => {
|
|
11258
|
+
const value = rec(server);
|
|
11259
|
+
return value ? `${str2(value.name) || "?"}${str2(value.status) ? `(${str2(value.status)})` : ""}` : "";
|
|
11260
|
+
}).filter(Boolean).join(", ") : "";
|
|
11261
|
+
return [
|
|
11262
|
+
...servers ? [` MCP servers: ${servers}`] : [],
|
|
11263
|
+
...typeof toolCount === "number" ? [` tools: ${toolCount} available`] : []
|
|
11264
|
+
];
|
|
11265
|
+
}
|
|
11499
11266
|
function safeParse(s) {
|
|
11500
11267
|
try {
|
|
11501
11268
|
return JSON.parse(s);
|
package/dist/runtime.js
CHANGED
|
@@ -2560,6 +2560,15 @@ var turnEventSchema = z5.discriminatedUnion("type", [
|
|
|
2560
2560
|
effort: z5.string().optional(),
|
|
2561
2561
|
thinking: z5.string().optional(),
|
|
2562
2562
|
reasoningEffort: z5.string().optional()
|
|
2563
|
+
}).optional(),
|
|
2564
|
+
// CT1275: the harness-reported MCP inventory from this turn's init frame.
|
|
2565
|
+
// This is deliberately diagnostic-only: names, statuses and a count, never
|
|
2566
|
+
// server definitions, credentials or session ids. `initReceived:false`
|
|
2567
|
+
// distinguishes a missing init frame from a real empty inventory.
|
|
2568
|
+
mcpInventory: z5.object({
|
|
2569
|
+
initReceived: z5.boolean(),
|
|
2570
|
+
servers: z5.array(z5.object({ name: z5.string(), status: z5.string() })),
|
|
2571
|
+
toolCount: z5.number().int().nonnegative()
|
|
2563
2572
|
}).optional()
|
|
2564
2573
|
})
|
|
2565
2574
|
]);
|
|
@@ -2578,6 +2587,7 @@ var turnResultReasonSchema = z6.discriminatedUnion("kind", [
|
|
|
2578
2587
|
z6.object({ kind: z6.literal("timeout_total") }),
|
|
2579
2588
|
z6.object({ kind: z6.literal("cancelled") }),
|
|
2580
2589
|
z6.object({ kind: z6.literal("skipped") }),
|
|
2590
|
+
z6.object({ kind: z6.literal("workspace_tools_missing") }),
|
|
2581
2591
|
z6.object({ kind: z6.literal("runtime_error") })
|
|
2582
2592
|
]);
|
|
2583
2593
|
var turnOutcomes = ["success", "failure", "cancelled", "skipped"];
|
|
@@ -2603,7 +2613,12 @@ var turnDiagnosticsSchema = z6.object({
|
|
|
2603
2613
|
result: z6.number().int().nonnegative()
|
|
2604
2614
|
}),
|
|
2605
2615
|
runtimeResultKind: z6.enum(turnRuntimeResultKinds).nullable(),
|
|
2606
|
-
finalSource: z6.enum(turnFinalSources)
|
|
2616
|
+
finalSource: z6.enum(turnFinalSources),
|
|
2617
|
+
mcpInventory: z6.object({
|
|
2618
|
+
initReceived: z6.boolean(),
|
|
2619
|
+
servers: z6.array(z6.object({ name: z6.string(), status: z6.string() })),
|
|
2620
|
+
toolCount: z6.number().int().nonnegative()
|
|
2621
|
+
}).optional()
|
|
2607
2622
|
});
|
|
2608
2623
|
|
|
2609
2624
|
// packages/agent-runtime/src/failure.ts
|
|
@@ -2737,6 +2752,8 @@ function normalizeTurnResultReason(reason) {
|
|
|
2737
2752
|
return { kind: "cancelled" };
|
|
2738
2753
|
case "skipped":
|
|
2739
2754
|
return { kind: "skipped" };
|
|
2755
|
+
case "workspace_tools_missing":
|
|
2756
|
+
return { kind: "workspace_tools_missing" };
|
|
2740
2757
|
default:
|
|
2741
2758
|
return { kind: "runtime_error" };
|
|
2742
2759
|
}
|
|
@@ -2841,12 +2858,7 @@ var turnRequestSchema = z8.object({
|
|
|
2841
2858
|
// adapters' conformance fixtures, tests) keeps parsing unchanged — the companion
|
|
2842
2859
|
// always populates it (`build-options.ts`), and the native adapter fails the
|
|
2843
2860
|
// turn loudly when it is somehow absent rather than guessing.
|
|
2844
|
-
workspaceId: z8.string().optional()
|
|
2845
|
-
// CT752: the server-resolved workspace surface this credential exposes.
|
|
2846
|
-
// Readiness uses this explicit fact to require `sdk` for code mode and the
|
|
2847
|
-
// granular floor for classic mode; inventory contents alone cannot infer it
|
|
2848
|
-
// because `sdk` is intentionally also available on the classic surface.
|
|
2849
|
-
workspaceToolSurface: z8.enum(["code", "classic"]).optional()
|
|
2861
|
+
workspaceId: z8.string().optional()
|
|
2850
2862
|
}),
|
|
2851
2863
|
// Machine-local resolution (host-filled): the checkout cwd, extra env from a
|
|
2852
2864
|
// prepare hook, and the resolved user MCP servers.
|
|
@@ -3431,6 +3443,8 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3431
3443
|
out.push(event);
|
|
3432
3444
|
};
|
|
3433
3445
|
let sessionEmitted = false;
|
|
3446
|
+
let workspaceProven = false;
|
|
3447
|
+
let mcpInventory = { initReceived: false, servers: [], toolCount: 0 };
|
|
3434
3448
|
let ok = false;
|
|
3435
3449
|
let resultReason;
|
|
3436
3450
|
let sawResult = false;
|
|
@@ -3448,6 +3462,12 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3448
3462
|
if (msg.type === "system" && msg.subtype === "init") {
|
|
3449
3463
|
const initModel = msg.model;
|
|
3450
3464
|
if (typeof initModel === "string" && initModel.length > 0) resolvedModel = initModel;
|
|
3465
|
+
mcpInventory = readMcpInventory(msg);
|
|
3466
|
+
workspaceProven = provesWorkspaceTools(msg);
|
|
3467
|
+
if (!workspaceProven) {
|
|
3468
|
+
resultReason = "workspace_tools_missing";
|
|
3469
|
+
break;
|
|
3470
|
+
}
|
|
3451
3471
|
const sdkSessionId = msg.session_id;
|
|
3452
3472
|
if (!sessionEmitted && sdkSessionId && sdkSessionId !== ctx.resumedSessionId) {
|
|
3453
3473
|
sessionEmitted = true;
|
|
@@ -3457,6 +3477,9 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3457
3477
|
...ctx.degraded ? { degraded: true } : {}
|
|
3458
3478
|
};
|
|
3459
3479
|
}
|
|
3480
|
+
} else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
|
|
3481
|
+
resultReason = "workspace_tools_missing";
|
|
3482
|
+
break;
|
|
3460
3483
|
} else if (msg.type === "assistant") {
|
|
3461
3484
|
const assistantErr = msg.error;
|
|
3462
3485
|
if (typeof assistantErr === "string" && assistantErr.length > 0) {
|
|
@@ -3520,6 +3543,10 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3520
3543
|
sawResult = true;
|
|
3521
3544
|
}
|
|
3522
3545
|
if (ctx.signal.aborted) return;
|
|
3546
|
+
if (!workspaceProven) {
|
|
3547
|
+
ok = false;
|
|
3548
|
+
resultReason ??= "workspace_tools_missing";
|
|
3549
|
+
}
|
|
3523
3550
|
await flushHeldText(buffer, emit, ok);
|
|
3524
3551
|
yield* drain(out);
|
|
3525
3552
|
if (!ok && !resultReason && !sawResult) resultReason = "no_result";
|
|
@@ -3528,9 +3555,25 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3528
3555
|
ok,
|
|
3529
3556
|
...resultReason ? { reason: resultReason } : {},
|
|
3530
3557
|
...usage ? { usage } : {},
|
|
3531
|
-
...resolvedModel ? { resolvedModel } : {}
|
|
3558
|
+
...resolvedModel ? { resolvedModel } : {},
|
|
3559
|
+
mcpInventory
|
|
3532
3560
|
};
|
|
3533
3561
|
}
|
|
3562
|
+
function readMcpInventory(msg) {
|
|
3563
|
+
const frame = msg;
|
|
3564
|
+
const servers = Array.isArray(frame.mcp_servers) ? frame.mcp_servers.flatMap(
|
|
3565
|
+
(server) => typeof server?.name === "string" && typeof server.status === "string" ? [{ name: server.name, status: server.status }] : []
|
|
3566
|
+
) : [];
|
|
3567
|
+
return {
|
|
3568
|
+
initReceived: true,
|
|
3569
|
+
servers,
|
|
3570
|
+
toolCount: Array.isArray(frame.tools) ? frame.tools.length : 0
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3573
|
+
function provesWorkspaceTools(msg) {
|
|
3574
|
+
const tools = msg.tools;
|
|
3575
|
+
return Array.isArray(tools) && tools.includes("mcp__cabane__sdk");
|
|
3576
|
+
}
|
|
3534
3577
|
function isSubscriptionWindow(value) {
|
|
3535
3578
|
return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
|
|
3536
3579
|
}
|
|
@@ -3629,6 +3672,11 @@ var CABANE_POLICY = {
|
|
|
3629
3672
|
uiPrompts: "never"
|
|
3630
3673
|
};
|
|
3631
3674
|
var CWD = "/env/here";
|
|
3675
|
+
var HEALTHY_MCP_INVENTORY = {
|
|
3676
|
+
initReceived: true,
|
|
3677
|
+
servers: [{ name: "cabane", status: "connected" }],
|
|
3678
|
+
toolCount: 1
|
|
3679
|
+
};
|
|
3632
3680
|
function makeRequest(overrides = {}) {
|
|
3633
3681
|
return {
|
|
3634
3682
|
systemPrompt: "system",
|
|
@@ -3649,7 +3697,8 @@ var init = (sessionId, model) => ({
|
|
|
3649
3697
|
type: "system",
|
|
3650
3698
|
subtype: "init",
|
|
3651
3699
|
session_id: sessionId,
|
|
3652
|
-
mcp_servers:
|
|
3700
|
+
mcp_servers: HEALTHY_MCP_INVENTORY.servers,
|
|
3701
|
+
tools: ["mcp__cabane__sdk"],
|
|
3653
3702
|
...model ? { model } : {}
|
|
3654
3703
|
});
|
|
3655
3704
|
var assistantText = (text) => ({
|
|
@@ -3710,7 +3759,7 @@ var sessionEvent = (sdkSessionId, cwd = CWD, degraded = false) => ({
|
|
|
3710
3759
|
state: encodeSession({ sdkSessionId, cwd }),
|
|
3711
3760
|
...degraded ? { degraded: true } : {}
|
|
3712
3761
|
});
|
|
3713
|
-
var
|
|
3762
|
+
var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
3714
3763
|
{
|
|
3715
3764
|
// A plain text reply: the held text-only block flushes as the terminal final.
|
|
3716
3765
|
name: "clean turn",
|
|
@@ -4090,6 +4139,12 @@ var CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4090
4139
|
]
|
|
4091
4140
|
}
|
|
4092
4141
|
];
|
|
4142
|
+
var CLAUDE_CODE_CONFORMANCE_FIXTURES = BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES.map((fixture) => ({
|
|
4143
|
+
...fixture,
|
|
4144
|
+
expected: fixture.expected.map(
|
|
4145
|
+
(event) => event.type === "result" ? { ...event, mcpInventory: HEALTHY_MCP_INVENTORY } : event
|
|
4146
|
+
)
|
|
4147
|
+
}));
|
|
4093
4148
|
|
|
4094
4149
|
// packages/agent-runtime/src/registry.ts
|
|
4095
4150
|
function createAdapterRegistry(adapters) {
|
|
@@ -6595,7 +6650,7 @@ var ConnectorHealthStore = class {
|
|
|
6595
6650
|
|
|
6596
6651
|
// src/dispatcher.ts
|
|
6597
6652
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
6598
|
-
import {
|
|
6653
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, statSync } from "fs";
|
|
6599
6654
|
import { join as join14 } from "path";
|
|
6600
6655
|
|
|
6601
6656
|
// src/turn-control-tools.ts
|
|
@@ -6932,7 +6987,6 @@ function buildCompanionTurnRequest(params) {
|
|
|
6932
6987
|
bearer: params.turnToken ?? params.agentPat,
|
|
6933
6988
|
activeConversationId: params.activeConversationId,
|
|
6934
6989
|
workspaceId: params.workspaceId,
|
|
6935
|
-
...t.workspaceToolSurface ? { workspaceToolSurface: t.workspaceToolSurface } : {},
|
|
6936
6990
|
// CT714: mount the turn-control surface ONLY when a real turn token backs
|
|
6937
6991
|
// this turn — the surface admits `turn_token` auth exclusively, so a
|
|
6938
6992
|
// PAT-fallback bearer would be rejected there. Absent it, external adapters
|
|
@@ -7408,173 +7462,6 @@ var TurnCommitter = class {
|
|
|
7408
7462
|
}
|
|
7409
7463
|
};
|
|
7410
7464
|
|
|
7411
|
-
// src/workspace-readiness.ts
|
|
7412
|
-
var CLASSIC_REQUIRED = ["read", "list", "search", "write", "edit"];
|
|
7413
|
-
var INITIALIZE_RETRY_DELAYS_MS = [250, 750, 1500];
|
|
7414
|
-
var INITIALIZE_ATTEMPT_TIMEOUT_MS = 4e3;
|
|
7415
|
-
var INITIALIZE_RETRY_BUDGET_MS = 1e4;
|
|
7416
|
-
async function proveWorkspaceTools(req, runtime, opts = {}) {
|
|
7417
|
-
const base = {
|
|
7418
|
-
ok: false,
|
|
7419
|
-
proofType: "authenticated_mcp_tools_list",
|
|
7420
|
-
runtime,
|
|
7421
|
-
harnessFingerprint: opts.harnessFingerprint ?? runtime,
|
|
7422
|
-
endpoint: safeEndpoint(req.cabane.mcpUrl),
|
|
7423
|
-
initialized: false,
|
|
7424
|
-
authenticated: false,
|
|
7425
|
-
discoveredTools: [],
|
|
7426
|
-
requiredTools: [],
|
|
7427
|
-
acceptedNames: ["sdk", "mcp__cabane__sdk"],
|
|
7428
|
-
failedCapability: null,
|
|
7429
|
-
detail: null
|
|
7430
|
-
};
|
|
7431
|
-
if (!req.cabane.mcpUrl) return fail(base, "server_not_configured", "Cabane MCP URL absent");
|
|
7432
|
-
if (!req.cabane.bearer) return fail(base, "authentication_failed", "Cabane bearer absent");
|
|
7433
|
-
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
7434
|
-
const headers = {
|
|
7435
|
-
authorization: `Bearer ${req.cabane.bearer}`,
|
|
7436
|
-
accept: "application/json, text/event-stream",
|
|
7437
|
-
"content-type": "application/json",
|
|
7438
|
-
"x-cabane-active-conversation": req.cabane.activeConversationId
|
|
7439
|
-
};
|
|
7440
|
-
try {
|
|
7441
|
-
const initialized = await initializeWithRetry(
|
|
7442
|
-
fetchImpl,
|
|
7443
|
-
req.cabane.mcpUrl,
|
|
7444
|
-
headers,
|
|
7445
|
-
{
|
|
7446
|
-
jsonrpc: "2.0",
|
|
7447
|
-
id: 1,
|
|
7448
|
-
method: "initialize",
|
|
7449
|
-
params: {
|
|
7450
|
-
protocolVersion: "2025-03-26",
|
|
7451
|
-
capabilities: {},
|
|
7452
|
-
clientInfo: { name: "cabane-companion-readiness", version: "1" }
|
|
7453
|
-
}
|
|
7454
|
-
},
|
|
7455
|
-
opts.retryDelaysMs ?? INITIALIZE_RETRY_DELAYS_MS
|
|
7456
|
-
);
|
|
7457
|
-
if (initialized.status === 401 || initialized.status === 403)
|
|
7458
|
-
return fail(base, "authentication_failed", `initialize returned HTTP ${initialized.status}`);
|
|
7459
|
-
if (initialized.transient)
|
|
7460
|
-
return fail(base, "workspace_endpoint_unreachable", initialized.detail);
|
|
7461
|
-
if (!initialized.ok) return fail(base, "initialization_failed", initialized.detail);
|
|
7462
|
-
base.initialized = true;
|
|
7463
|
-
base.authenticated = true;
|
|
7464
|
-
if (initialized.sessionId) headers["mcp-session-id"] = initialized.sessionId;
|
|
7465
|
-
const listed = await rpc(fetchImpl, req.cabane.mcpUrl, headers, {
|
|
7466
|
-
jsonrpc: "2.0",
|
|
7467
|
-
id: 2,
|
|
7468
|
-
method: "tools/list",
|
|
7469
|
-
params: {}
|
|
7470
|
-
});
|
|
7471
|
-
if (listed.status === 401 || listed.status === 403)
|
|
7472
|
-
return fail(base, "authentication_failed", `tools/list returned HTTP ${listed.status}`);
|
|
7473
|
-
if (!listed.ok) return fail(base, "tool_discovery_failed", listed.detail);
|
|
7474
|
-
const result = asRecord3(asRecord3(listed.value)?.result);
|
|
7475
|
-
const tools = Array.isArray(result?.tools) ? result.tools : null;
|
|
7476
|
-
if (!tools) return fail(base, "tool_discovery_failed", "tools/list returned no tool inventory");
|
|
7477
|
-
base.discoveredTools = tools.map(
|
|
7478
|
-
(tool2) => tool2 && typeof tool2 === "object" && typeof tool2.name === "string" ? tool2.name : null
|
|
7479
|
-
).filter((name) => name !== null).sort();
|
|
7480
|
-
if (!req.cabane.workspaceToolSurface)
|
|
7481
|
-
return fail(base, "required_tool_missing", "resolved workspace tool surface absent");
|
|
7482
|
-
base.requiredTools = req.cabane.workspaceToolSurface === "code" ? ["sdk"] : CLASSIC_REQUIRED;
|
|
7483
|
-
const missing = base.requiredTools.filter((name) => !base.discoveredTools.includes(name));
|
|
7484
|
-
if (missing.length > 0)
|
|
7485
|
-
return fail(
|
|
7486
|
-
base,
|
|
7487
|
-
"required_tool_missing",
|
|
7488
|
-
`missing initialized tools: ${missing.join(", ")}`
|
|
7489
|
-
);
|
|
7490
|
-
base.ok = true;
|
|
7491
|
-
return base;
|
|
7492
|
-
} catch (error) {
|
|
7493
|
-
return fail(
|
|
7494
|
-
base,
|
|
7495
|
-
"workspace_endpoint_unreachable",
|
|
7496
|
-
error instanceof Error ? error.message : String(error)
|
|
7497
|
-
);
|
|
7498
|
-
}
|
|
7499
|
-
}
|
|
7500
|
-
async function initializeWithRetry(fetchImpl, url, headers, body, retryDelaysMs) {
|
|
7501
|
-
let lastFailure = null;
|
|
7502
|
-
const deadline = Date.now() + INITIALIZE_RETRY_BUDGET_MS;
|
|
7503
|
-
for (let attempt = 0; attempt <= retryDelaysMs.length; attempt += 1) {
|
|
7504
|
-
try {
|
|
7505
|
-
const remainingMs = deadline - Date.now();
|
|
7506
|
-
if (remainingMs <= 0) break;
|
|
7507
|
-
const result = await rpc(
|
|
7508
|
-
fetchImpl,
|
|
7509
|
-
url,
|
|
7510
|
-
headers,
|
|
7511
|
-
body,
|
|
7512
|
-
Math.min(INITIALIZE_ATTEMPT_TIMEOUT_MS, remainingMs)
|
|
7513
|
-
);
|
|
7514
|
-
if (result.status === 401 || result.status === 403 || result.ok) return result;
|
|
7515
|
-
if (result.status < 500) return result;
|
|
7516
|
-
lastFailure = { ...result, transient: true };
|
|
7517
|
-
} catch (error) {
|
|
7518
|
-
lastFailure = {
|
|
7519
|
-
ok: false,
|
|
7520
|
-
status: 0,
|
|
7521
|
-
sessionId: null,
|
|
7522
|
-
value: null,
|
|
7523
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
7524
|
-
transient: true
|
|
7525
|
-
};
|
|
7526
|
-
}
|
|
7527
|
-
const retryDelayMs = retryDelaysMs[attempt];
|
|
7528
|
-
if (retryDelayMs === void 0 || Date.now() + retryDelayMs >= deadline) break;
|
|
7529
|
-
await delay(retryDelayMs);
|
|
7530
|
-
}
|
|
7531
|
-
return lastFailure;
|
|
7532
|
-
}
|
|
7533
|
-
function delay(ms) {
|
|
7534
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7535
|
-
}
|
|
7536
|
-
function fail(proof, capability, detail) {
|
|
7537
|
-
proof.failedCapability = capability;
|
|
7538
|
-
proof.detail = detail.slice(0, 300);
|
|
7539
|
-
return proof;
|
|
7540
|
-
}
|
|
7541
|
-
function safeEndpoint(value) {
|
|
7542
|
-
try {
|
|
7543
|
-
const url = new URL(value);
|
|
7544
|
-
return `${url.origin}${url.pathname}`;
|
|
7545
|
-
} catch {
|
|
7546
|
-
return null;
|
|
7547
|
-
}
|
|
7548
|
-
}
|
|
7549
|
-
async function rpc(fetchImpl, url, headers, body, timeoutMs) {
|
|
7550
|
-
const response = await fetchImpl(url, {
|
|
7551
|
-
method: "POST",
|
|
7552
|
-
headers,
|
|
7553
|
-
body: JSON.stringify(body),
|
|
7554
|
-
...timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}
|
|
7555
|
-
});
|
|
7556
|
-
const text = await response.text();
|
|
7557
|
-
const value = parseRpcBody(text);
|
|
7558
|
-
return {
|
|
7559
|
-
ok: response.ok && !!value && !value.error,
|
|
7560
|
-
status: response.status,
|
|
7561
|
-
sessionId: response.headers.get("mcp-session-id"),
|
|
7562
|
-
value,
|
|
7563
|
-
detail: typeof asRecord3(value?.error)?.message === "string" ? String(asRecord3(value?.error)?.message) : `HTTP ${response.status}`
|
|
7564
|
-
};
|
|
7565
|
-
}
|
|
7566
|
-
function parseRpcBody(text) {
|
|
7567
|
-
const trimmed = text.trim();
|
|
7568
|
-
if (trimmed.startsWith("{")) return JSON.parse(trimmed);
|
|
7569
|
-
for (const line of trimmed.split("\n")) {
|
|
7570
|
-
if (line.startsWith("data:")) return JSON.parse(line.slice(5).trim());
|
|
7571
|
-
}
|
|
7572
|
-
return null;
|
|
7573
|
-
}
|
|
7574
|
-
function asRecord3(value) {
|
|
7575
|
-
return value !== null && typeof value === "object" ? value : null;
|
|
7576
|
-
}
|
|
7577
|
-
|
|
7578
7465
|
// src/dispatcher.ts
|
|
7579
7466
|
var PREPARING_TOOL_NAME = "preparing";
|
|
7580
7467
|
var PREPARE_FAILED_PREFIX = "**Couldn't prepare your environment.** I wasn't able to provision a working directory for this conversation, so I can't run this turn. The provisioning command reported:";
|
|
@@ -8067,152 +7954,7 @@ ${reason}`,
|
|
|
8067
7954
|
`runtime_unavailable:${err.runtime}`
|
|
8068
7955
|
);
|
|
8069
7956
|
}
|
|
8070
|
-
let turnReceiptPath = null;
|
|
8071
7957
|
const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
|
|
8072
|
-
const closeTurnReceipt = (ok, reason) => {
|
|
8073
|
-
if (!turnReceiptPath) return;
|
|
8074
|
-
const target = turnReceiptPath;
|
|
8075
|
-
turnReceiptPath = null;
|
|
8076
|
-
try {
|
|
8077
|
-
appendFileSync2(
|
|
8078
|
-
target,
|
|
8079
|
-
`${JSON.stringify({
|
|
8080
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8081
|
-
event: "settled",
|
|
8082
|
-
turnId,
|
|
8083
|
-
agentId: payload.agentId,
|
|
8084
|
-
conversationId: payload.conversationId,
|
|
8085
|
-
ok,
|
|
8086
|
-
reason
|
|
8087
|
-
})}
|
|
8088
|
-
`,
|
|
8089
|
-
{ mode: 384 }
|
|
8090
|
-
);
|
|
8091
|
-
} catch (error) {
|
|
8092
|
-
turnLog.warn(
|
|
8093
|
-
{ err: error instanceof Error ? error.message : String(error) },
|
|
8094
|
-
"dispatcher: turn-settled diagnostic write failed"
|
|
8095
|
-
);
|
|
8096
|
-
}
|
|
8097
|
-
};
|
|
8098
|
-
if (prepareHook && hookEnv?.CABANE_TASK_ID) {
|
|
8099
|
-
const checkout = checkoutState(effectiveCwd);
|
|
8100
|
-
if (!effectiveCwd || !checkout.ok) {
|
|
8101
|
-
const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
|
|
8102
|
-
turnLog.error({ checkout: effectiveCwd ?? null, checkoutState: checkout }, reason);
|
|
8103
|
-
try {
|
|
8104
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8105
|
-
body: `**Couldn't prepare your environment.** ${reason}`,
|
|
8106
|
-
kind: "final",
|
|
8107
|
-
turnId,
|
|
8108
|
-
parentMessageId: payload.messageId
|
|
8109
|
-
});
|
|
8110
|
-
} catch (postErr) {
|
|
8111
|
-
turnLog.warn(
|
|
8112
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8113
|
-
"dispatcher: checkout-missing notice post failed"
|
|
8114
|
-
);
|
|
8115
|
-
}
|
|
8116
|
-
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
8117
|
-
}
|
|
8118
|
-
const receiptPath = join14(effectiveCwd, ".git", "cabane", "readiness.jsonl");
|
|
8119
|
-
const receiptLine = (fields) => `${JSON.stringify({
|
|
8120
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8121
|
-
taskId: hookEnv.CABANE_TASK_ID,
|
|
8122
|
-
binding: hookEnv.CABANE_TASK_BINDING ?? null,
|
|
8123
|
-
checkout: effectiveCwd,
|
|
8124
|
-
// CT1022: the two fields the environment reaper reads — which turn this
|
|
8125
|
-
// is (so its settle can be matched among interleaved agents) and how long
|
|
8126
|
-
// it may legitimately run (so an unclosed receipt expires on this turn's
|
|
8127
|
-
// real deadline, not the reaper's guess).
|
|
8128
|
-
turnId,
|
|
8129
|
-
totalTimeoutMs,
|
|
8130
|
-
// CT1062: WHOSE turn. A task env is shared — between agents, and between
|
|
8131
|
-
// an agent's own sequential conversations — so a reader asking "is a turn
|
|
8132
|
-
// of THIS agent, other than mine, running here?" (the host's run-lock
|
|
8133
|
-
// does, before it lets a second conversation into the tree) can only
|
|
8134
|
-
// answer it if the line says who. An unattributed open line has to count
|
|
8135
|
-
// for everyone, which refuses work that should have been admitted.
|
|
8136
|
-
agentId: payload.agentId,
|
|
8137
|
-
conversationId: payload.conversationId,
|
|
8138
|
-
...fields
|
|
8139
|
-
})}
|
|
8140
|
-
`;
|
|
8141
|
-
try {
|
|
8142
|
-
mkdirSync10(join14(effectiveCwd, ".git", "cabane"), { recursive: true });
|
|
8143
|
-
appendFileSync2(
|
|
8144
|
-
receiptPath,
|
|
8145
|
-
// `starting` is the honest classification before the proof has run. The
|
|
8146
|
-
// line the proof appends below carries the same `turnId`, so a reader
|
|
8147
|
-
// replaying the file sees one turn, not two.
|
|
8148
|
-
receiptLine({ classification: "starting" }),
|
|
8149
|
-
{ mode: 384 }
|
|
8150
|
-
);
|
|
8151
|
-
turnReceiptPath = receiptPath;
|
|
8152
|
-
} catch (error) {
|
|
8153
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
8154
|
-
const reason = `turn_receipt_unwritable: ${detail}; task=${hookEnv.CABANE_TASK_ID}; checkout=${effectiveCwd}; recovery=restore write access to the checkout's .git/cabane, then re-dispatch`;
|
|
8155
|
-
turnLog.error({ err: detail, receiptPath }, reason);
|
|
8156
|
-
try {
|
|
8157
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8158
|
-
body: `**Couldn't prepare your environment.** ${reason}`,
|
|
8159
|
-
kind: "final",
|
|
8160
|
-
turnId,
|
|
8161
|
-
parentMessageId: payload.messageId
|
|
8162
|
-
});
|
|
8163
|
-
} catch (postErr) {
|
|
8164
|
-
turnLog.warn(
|
|
8165
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8166
|
-
"dispatcher: turn-receipt failure notice post failed"
|
|
8167
|
-
);
|
|
8168
|
-
}
|
|
8169
|
-
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
8170
|
-
}
|
|
8171
|
-
const proof = await proveWorkspaceTools(request, adapter.name, {
|
|
8172
|
-
...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
|
|
8173
|
-
...this.opts.workspaceProofRetryDelaysMs ? { retryDelaysMs: this.opts.workspaceProofRetryDelaysMs } : {},
|
|
8174
|
-
harnessFingerprint: turnContext.runtime
|
|
8175
|
-
});
|
|
8176
|
-
turnLog[proof.ok ? "info" : "error"](
|
|
8177
|
-
{ workspaceProof: proof, checkout: effectiveCwd },
|
|
8178
|
-
`dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout usable`
|
|
8179
|
-
);
|
|
8180
|
-
try {
|
|
8181
|
-
appendFileSync2(
|
|
8182
|
-
receiptPath,
|
|
8183
|
-
receiptLine({
|
|
8184
|
-
classification: proof.ok ? "ready" : "workspace_tools_missing",
|
|
8185
|
-
failedCapability: proof.failedCapability,
|
|
8186
|
-
workspaceTools: proof
|
|
8187
|
-
}),
|
|
8188
|
-
{ mode: 384 }
|
|
8189
|
-
);
|
|
8190
|
-
} catch (error) {
|
|
8191
|
-
turnLog.warn(
|
|
8192
|
-
{ err: error instanceof Error ? error.message : String(error) },
|
|
8193
|
-
"dispatcher: workspace-proof diagnostic write failed (the turn receipt is open)"
|
|
8194
|
-
);
|
|
8195
|
-
}
|
|
8196
|
-
if (!proof.ok) {
|
|
8197
|
-
const recovery = proof.failedCapability === "workspace_endpoint_unreachable" ? "the Cabane workspace endpoint was unreachable; retry this dispatch" : "restart the connector after restoring the Cabane workspace tool mount";
|
|
8198
|
-
const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd}; runtime=${adapter.name}; recovery=${recovery}`;
|
|
8199
|
-
closeTurnReceipt(false, reason);
|
|
8200
|
-
try {
|
|
8201
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8202
|
-
body: `**Couldn't prepare your environment.** ${reason}`,
|
|
8203
|
-
kind: "final",
|
|
8204
|
-
turnId,
|
|
8205
|
-
parentMessageId: payload.messageId
|
|
8206
|
-
});
|
|
8207
|
-
} catch (postErr) {
|
|
8208
|
-
turnLog.warn(
|
|
8209
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8210
|
-
"dispatcher: workspace-proof failure notice post failed"
|
|
8211
|
-
);
|
|
8212
|
-
}
|
|
8213
|
-
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
8214
|
-
}
|
|
8215
|
-
}
|
|
8216
7958
|
const transcript = this.opts.transcriptDir ? new TranscriptWriter(
|
|
8217
7959
|
this.opts.transcriptDir,
|
|
8218
7960
|
{
|
|
@@ -8235,6 +7977,7 @@ ${reason}`,
|
|
|
8235
7977
|
let turnUsage;
|
|
8236
7978
|
let turnResolvedModel;
|
|
8237
7979
|
let turnResolvedConfig;
|
|
7980
|
+
let turnMcpInventory;
|
|
8238
7981
|
const eventCounts = {
|
|
8239
7982
|
session: 0,
|
|
8240
7983
|
text: 0,
|
|
@@ -8400,6 +8143,7 @@ ${reason}`,
|
|
|
8400
8143
|
turnUsage = event.usage;
|
|
8401
8144
|
turnResolvedModel = event.resolvedModel;
|
|
8402
8145
|
turnResolvedConfig = event.resolvedConfig;
|
|
8146
|
+
turnMcpInventory = event.mcpInventory;
|
|
8403
8147
|
runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
|
|
8404
8148
|
} else if (event.type === "text" && skipState.skipped) {
|
|
8405
8149
|
} else {
|
|
@@ -8476,7 +8220,6 @@ ${reason}`,
|
|
|
8476
8220
|
} finally {
|
|
8477
8221
|
if (idleTimer) clearTimeout(idleTimer);
|
|
8478
8222
|
clearTimeout(totalTimer);
|
|
8479
|
-
closeTurnReceipt(okResult, resultReason ?? null);
|
|
8480
8223
|
const userCancelled = abortController.signal.aborted && timeoutReason === null;
|
|
8481
8224
|
if (timeoutReason !== null) {
|
|
8482
8225
|
resultReason = timeoutReason;
|
|
@@ -8558,6 +8301,7 @@ ${reason}`,
|
|
|
8558
8301
|
sessionFingerprint: fingerprintSessionState(latestSessionState),
|
|
8559
8302
|
eventCounts,
|
|
8560
8303
|
runtimeResultKind,
|
|
8304
|
+
...turnMcpInventory ? { mcpInventory: turnMcpInventory } : {},
|
|
8561
8305
|
finalSource: outcome === "skipped" || outcome === "cancelled" || silentMarkerEmitted ? "marker" : committer.finalSource
|
|
8562
8306
|
};
|
|
8563
8307
|
body.diagnostics = settledDiagnostics;
|
|
@@ -8707,7 +8451,7 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
8707
8451
|
// src/outbox.ts
|
|
8708
8452
|
import {
|
|
8709
8453
|
existsSync as existsSync11,
|
|
8710
|
-
mkdirSync as
|
|
8454
|
+
mkdirSync as mkdirSync10,
|
|
8711
8455
|
readdirSync as readdirSync3,
|
|
8712
8456
|
readFileSync as readFileSync8,
|
|
8713
8457
|
renameSync as renameSync3,
|
|
@@ -8737,7 +8481,7 @@ var Outbox = class {
|
|
|
8737
8481
|
// per-workspace bounds.
|
|
8738
8482
|
persist(entry) {
|
|
8739
8483
|
const dir2 = this.dir();
|
|
8740
|
-
|
|
8484
|
+
mkdirSync10(dir2, { recursive: true });
|
|
8741
8485
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
8742
8486
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
8743
8487
|
try {
|
|
@@ -10008,14 +9752,14 @@ function handleUncaught(log, err, origin) {
|
|
|
10008
9752
|
}
|
|
10009
9753
|
|
|
10010
9754
|
// src/crash-marker.ts
|
|
10011
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
9755
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
10012
9756
|
import { join as join16 } from "path";
|
|
10013
9757
|
function crashMarkerPath() {
|
|
10014
9758
|
return join16(cabaneDir(), "last-error.json");
|
|
10015
9759
|
}
|
|
10016
9760
|
function recordCrash(rec) {
|
|
10017
9761
|
try {
|
|
10018
|
-
|
|
9762
|
+
mkdirSync11(cabaneDir(), { recursive: true });
|
|
10019
9763
|
writeFileSync8(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
|
|
10020
9764
|
} catch {
|
|
10021
9765
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.62",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|