@cabane/companion 0.6.61 → 0.6.63
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 +162 -349
- package/dist/runtime.js +130 -340
- package/package.json +1 -1
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
|
|
3560
|
+
};
|
|
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
|
|
3532
3571
|
};
|
|
3533
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
|
|
@@ -6617,16 +6672,32 @@ var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
|
6617
6672
|
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
6618
6673
|
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
6619
6674
|
function createReplyState() {
|
|
6620
|
-
return { answersMessageId: null };
|
|
6675
|
+
return { answersMessageId: null, order: null };
|
|
6621
6676
|
}
|
|
6622
6677
|
function createSendState() {
|
|
6623
|
-
return { agentId: null, message: null };
|
|
6678
|
+
return { agentId: null, message: null, order: null };
|
|
6679
|
+
}
|
|
6680
|
+
function createTurnControlOrder() {
|
|
6681
|
+
let calls = 0;
|
|
6682
|
+
return {
|
|
6683
|
+
next: () => {
|
|
6684
|
+
calls += 1;
|
|
6685
|
+
return calls;
|
|
6686
|
+
}
|
|
6687
|
+
};
|
|
6624
6688
|
}
|
|
6625
6689
|
function createSkipState() {
|
|
6626
6690
|
return { skipped: false, reason: null };
|
|
6627
6691
|
}
|
|
6628
6692
|
function createAskState() {
|
|
6629
|
-
return {
|
|
6693
|
+
return {
|
|
6694
|
+
targetUserId: null,
|
|
6695
|
+
question: null,
|
|
6696
|
+
headline: null,
|
|
6697
|
+
options: null,
|
|
6698
|
+
questions: null,
|
|
6699
|
+
order: null
|
|
6700
|
+
};
|
|
6630
6701
|
}
|
|
6631
6702
|
function createWakeState() {
|
|
6632
6703
|
return { afterSeconds: null, at: null, note: null, cancelled: false };
|
|
@@ -6656,7 +6727,7 @@ function wakeCommitField(state) {
|
|
|
6656
6727
|
}
|
|
6657
6728
|
};
|
|
6658
6729
|
}
|
|
6659
|
-
function createTurnControlMcpServer(sendState, skipState, askState, subAgentCreate, wakeState, replyState) {
|
|
6730
|
+
function createTurnControlMcpServer(sendState, skipState, askState, subAgentCreate, wakeState, replyState, controlOrder) {
|
|
6660
6731
|
return createSdkMcpServer({
|
|
6661
6732
|
name: COMPANION_LOCAL_MCP_SERVER,
|
|
6662
6733
|
version: "0.0.0",
|
|
@@ -6673,6 +6744,7 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
|
|
|
6673
6744
|
async (args) => {
|
|
6674
6745
|
sendState.agentId = args.agentId;
|
|
6675
6746
|
sendState.message = args.message;
|
|
6747
|
+
sendState.order = controlOrder?.next() ?? null;
|
|
6676
6748
|
return {
|
|
6677
6749
|
content: [{ type: "text", text: JSON.stringify({ sent: args.agentId }) }]
|
|
6678
6750
|
};
|
|
@@ -6688,6 +6760,7 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
|
|
|
6688
6760
|
},
|
|
6689
6761
|
async (args) => {
|
|
6690
6762
|
replyState.answersMessageId = args.messageId;
|
|
6763
|
+
replyState.order = controlOrder?.next() ?? null;
|
|
6691
6764
|
return {
|
|
6692
6765
|
content: [
|
|
6693
6766
|
{
|
|
@@ -6770,6 +6843,7 @@ function createTurnControlMcpServer(sendState, skipState, askState, subAgentCrea
|
|
|
6770
6843
|
};
|
|
6771
6844
|
}
|
|
6772
6845
|
askState.targetUserId = args.targetUserId;
|
|
6846
|
+
askState.order = controlOrder?.next() ?? null;
|
|
6773
6847
|
if (hasArray) {
|
|
6774
6848
|
askState.questions = args.questions;
|
|
6775
6849
|
askState.question = null;
|
|
@@ -6932,7 +7006,6 @@ function buildCompanionTurnRequest(params) {
|
|
|
6932
7006
|
bearer: params.turnToken ?? params.agentPat,
|
|
6933
7007
|
activeConversationId: params.activeConversationId,
|
|
6934
7008
|
workspaceId: params.workspaceId,
|
|
6935
|
-
...t.workspaceToolSurface ? { workspaceToolSurface: t.workspaceToolSurface } : {},
|
|
6936
7009
|
// CT714: mount the turn-control surface ONLY when a real turn token backs
|
|
6937
7010
|
// this turn — the surface admits `turn_token` auth exclusively, so a
|
|
6938
7011
|
// PAT-fallback bearer would be rejected there. Absent it, external adapters
|
|
@@ -7334,12 +7407,31 @@ var TurnCommitter = class {
|
|
|
7334
7407
|
// a wordless turn has no answer to bind, and the server's mute-settle
|
|
7335
7408
|
// notice speaks for it.
|
|
7336
7409
|
turnControlFields(kind) {
|
|
7337
|
-
|
|
7410
|
+
const fields = {
|
|
7338
7411
|
...this.answersField(kind),
|
|
7339
7412
|
...this.sendField(),
|
|
7340
7413
|
...this.askField(),
|
|
7341
7414
|
...this.wakeField()
|
|
7342
7415
|
};
|
|
7416
|
+
return { ...fields, ...this.orderField(fields) };
|
|
7417
|
+
}
|
|
7418
|
+
// CT1281: the order the control tools were CALLED, for the addressed rows the
|
|
7419
|
+
// server writes from this one commit. It reports a position only for an intent
|
|
7420
|
+
// that actually made it into the commit — an ask whose payload was incomplete,
|
|
7421
|
+
// or a self-send the committer stripped, contributes no row and so has no place
|
|
7422
|
+
// in the line. An intent with no recorded call (the runtime's auto-declared
|
|
7423
|
+
// reply) is deliberately absent: the server sorts it last, which is what it is.
|
|
7424
|
+
orderField(fields) {
|
|
7425
|
+
const order = {};
|
|
7426
|
+
const replyOrder = this.deps.replyState.order;
|
|
7427
|
+
if (fields.answersMessageId !== void 0 && replyOrder !== null) order.reply = replyOrder;
|
|
7428
|
+
if (fields.dispatch !== void 0 && this.deps.sendState.order !== null) {
|
|
7429
|
+
order.send = this.deps.sendState.order;
|
|
7430
|
+
}
|
|
7431
|
+
if (fields.ask !== void 0 && this.deps.askState.order !== null) {
|
|
7432
|
+
order.ask = this.deps.askState.order;
|
|
7433
|
+
}
|
|
7434
|
+
return Object.keys(order).length > 0 ? { turnControlOrder: order } : {};
|
|
7343
7435
|
}
|
|
7344
7436
|
// The declared reply: explicit `reply_to` first, else the runtime's own
|
|
7345
7437
|
// declaration for the turn that just answers — see
|
|
@@ -7408,173 +7500,6 @@ var TurnCommitter = class {
|
|
|
7408
7500
|
}
|
|
7409
7501
|
};
|
|
7410
7502
|
|
|
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
7503
|
// src/dispatcher.ts
|
|
7579
7504
|
var PREPARING_TOOL_NAME = "preparing";
|
|
7580
7505
|
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:";
|
|
@@ -7965,6 +7890,7 @@ ${reason}`,
|
|
|
7965
7890
|
const askState = createAskState();
|
|
7966
7891
|
const wakeState = createWakeState();
|
|
7967
7892
|
const replyState = createReplyState();
|
|
7893
|
+
const controlOrder = createTurnControlOrder();
|
|
7968
7894
|
let spawnedSubAgent = false;
|
|
7969
7895
|
const subAgentCreate = async (args) => {
|
|
7970
7896
|
const dispatchTarget = args.agentId ?? payload.agentId;
|
|
@@ -7996,7 +7922,8 @@ ${reason}`,
|
|
|
7996
7922
|
askState,
|
|
7997
7923
|
subAgentCreate,
|
|
7998
7924
|
wakeState,
|
|
7999
|
-
replyState
|
|
7925
|
+
replyState,
|
|
7926
|
+
controlOrder
|
|
8000
7927
|
);
|
|
8001
7928
|
const request = buildCompanionTurnRequest({
|
|
8002
7929
|
turnContext,
|
|
@@ -8067,152 +7994,7 @@ ${reason}`,
|
|
|
8067
7994
|
`runtime_unavailable:${err.runtime}`
|
|
8068
7995
|
);
|
|
8069
7996
|
}
|
|
8070
|
-
let turnReceiptPath = null;
|
|
8071
7997
|
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
7998
|
const transcript = this.opts.transcriptDir ? new TranscriptWriter(
|
|
8217
7999
|
this.opts.transcriptDir,
|
|
8218
8000
|
{
|
|
@@ -8235,6 +8017,7 @@ ${reason}`,
|
|
|
8235
8017
|
let turnUsage;
|
|
8236
8018
|
let turnResolvedModel;
|
|
8237
8019
|
let turnResolvedConfig;
|
|
8020
|
+
let turnMcpInventory;
|
|
8238
8021
|
const eventCounts = {
|
|
8239
8022
|
session: 0,
|
|
8240
8023
|
text: 0,
|
|
@@ -8259,11 +8042,12 @@ ${reason}`,
|
|
|
8259
8042
|
signal: abortController.signal,
|
|
8260
8043
|
log: turnLog,
|
|
8261
8044
|
nextSeq,
|
|
8262
|
-
// The committer reads this at commit to
|
|
8263
|
-
//
|
|
8045
|
+
// The committer reads this at commit to carry the addressed send on the
|
|
8046
|
+
// terminal row; the server writes the send itself as a distinct message.
|
|
8264
8047
|
sendState,
|
|
8265
|
-
// CT326: likewise the ask payload
|
|
8266
|
-
//
|
|
8048
|
+
// CT326: likewise the ask payload. CT1281: the server writes the ask as its
|
|
8049
|
+
// own addressed message to the human — which ENQUEUES like any other send —
|
|
8050
|
+
// and creates the `asks` row against that carrier, not against turn speech.
|
|
8267
8051
|
askState,
|
|
8268
8052
|
// CT442: likewise the wake payload — attached to the `final` row so the
|
|
8269
8053
|
// server arms the wake schedule atomically with the reply it rode on.
|
|
@@ -8288,6 +8072,7 @@ ${reason}`,
|
|
|
8288
8072
|
);
|
|
8289
8073
|
if (intent.ask) {
|
|
8290
8074
|
askState.targetUserId = intent.ask.targetUserId;
|
|
8075
|
+
askState.order = intent.askOrder ?? null;
|
|
8291
8076
|
if (intent.ask.questions && intent.ask.questions.length > 0) {
|
|
8292
8077
|
askState.questions = intent.ask.questions;
|
|
8293
8078
|
askState.question = null;
|
|
@@ -8316,8 +8101,12 @@ ${reason}`,
|
|
|
8316
8101
|
if (intent.sendAgentId && intent.sendBody) {
|
|
8317
8102
|
sendState.agentId = intent.sendAgentId;
|
|
8318
8103
|
sendState.message = intent.sendBody;
|
|
8104
|
+
sendState.order = intent.sendOrder ?? null;
|
|
8105
|
+
}
|
|
8106
|
+
if (intent.answersMessageId) {
|
|
8107
|
+
replyState.answersMessageId = intent.answersMessageId;
|
|
8108
|
+
replyState.order = intent.replyOrder ?? null;
|
|
8319
8109
|
}
|
|
8320
|
-
if (intent.answersMessageId) replyState.answersMessageId = intent.answersMessageId;
|
|
8321
8110
|
if (intent.skipped) {
|
|
8322
8111
|
skipState.skipped = true;
|
|
8323
8112
|
skipState.reason = intent.skipReason;
|
|
@@ -8400,6 +8189,7 @@ ${reason}`,
|
|
|
8400
8189
|
turnUsage = event.usage;
|
|
8401
8190
|
turnResolvedModel = event.resolvedModel;
|
|
8402
8191
|
turnResolvedConfig = event.resolvedConfig;
|
|
8192
|
+
turnMcpInventory = event.mcpInventory;
|
|
8403
8193
|
runtimeResultKind = event.ok ? "success" : event.reason === "no_terminal" ? "no_terminal" : "error";
|
|
8404
8194
|
} else if (event.type === "text" && skipState.skipped) {
|
|
8405
8195
|
} else {
|
|
@@ -8476,7 +8266,6 @@ ${reason}`,
|
|
|
8476
8266
|
} finally {
|
|
8477
8267
|
if (idleTimer) clearTimeout(idleTimer);
|
|
8478
8268
|
clearTimeout(totalTimer);
|
|
8479
|
-
closeTurnReceipt(okResult, resultReason ?? null);
|
|
8480
8269
|
const userCancelled = abortController.signal.aborted && timeoutReason === null;
|
|
8481
8270
|
if (timeoutReason !== null) {
|
|
8482
8271
|
resultReason = timeoutReason;
|
|
@@ -8558,6 +8347,7 @@ ${reason}`,
|
|
|
8558
8347
|
sessionFingerprint: fingerprintSessionState(latestSessionState),
|
|
8559
8348
|
eventCounts,
|
|
8560
8349
|
runtimeResultKind,
|
|
8350
|
+
...turnMcpInventory ? { mcpInventory: turnMcpInventory } : {},
|
|
8561
8351
|
finalSource: outcome === "skipped" || outcome === "cancelled" || silentMarkerEmitted ? "marker" : committer.finalSource
|
|
8562
8352
|
};
|
|
8563
8353
|
body.diagnostics = settledDiagnostics;
|
|
@@ -8707,7 +8497,7 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
8707
8497
|
// src/outbox.ts
|
|
8708
8498
|
import {
|
|
8709
8499
|
existsSync as existsSync11,
|
|
8710
|
-
mkdirSync as
|
|
8500
|
+
mkdirSync as mkdirSync10,
|
|
8711
8501
|
readdirSync as readdirSync3,
|
|
8712
8502
|
readFileSync as readFileSync8,
|
|
8713
8503
|
renameSync as renameSync3,
|
|
@@ -8737,7 +8527,7 @@ var Outbox = class {
|
|
|
8737
8527
|
// per-workspace bounds.
|
|
8738
8528
|
persist(entry) {
|
|
8739
8529
|
const dir2 = this.dir();
|
|
8740
|
-
|
|
8530
|
+
mkdirSync10(dir2, { recursive: true });
|
|
8741
8531
|
const target = this.fileFor(entry.turnId, entry.seq);
|
|
8742
8532
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
8743
8533
|
try {
|
|
@@ -10008,14 +9798,14 @@ function handleUncaught(log, err, origin) {
|
|
|
10008
9798
|
}
|
|
10009
9799
|
|
|
10010
9800
|
// src/crash-marker.ts
|
|
10011
|
-
import { existsSync as existsSync12, mkdirSync as
|
|
9801
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
10012
9802
|
import { join as join16 } from "path";
|
|
10013
9803
|
function crashMarkerPath() {
|
|
10014
9804
|
return join16(cabaneDir(), "last-error.json");
|
|
10015
9805
|
}
|
|
10016
9806
|
function recordCrash(rec) {
|
|
10017
9807
|
try {
|
|
10018
|
-
|
|
9808
|
+
mkdirSync11(cabaneDir(), { recursive: true });
|
|
10019
9809
|
writeFileSync8(crashMarkerPath(), JSON.stringify(rec, null, 2) + "\n");
|
|
10020
9810
|
} catch {
|
|
10021
9811
|
}
|