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