@cabane/companion 0.6.69 → 0.6.71
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 +240 -88
- package/dist/runtime.js +240 -88
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2284,6 +2284,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
2284
2284
|
// src/api.ts
|
|
2285
2285
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
2286
2286
|
var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
2287
|
+
var LEASE_CHECK_TIMEOUT_MS = 1e4;
|
|
2287
2288
|
function activeRunOutboxKey(conversationId, agentId) {
|
|
2288
2289
|
return `activerun:${conversationId}:${agentId}`;
|
|
2289
2290
|
}
|
|
@@ -2473,6 +2474,52 @@ var CabaneApi = class {
|
|
|
2473
2474
|
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-intent?${q}`
|
|
2474
2475
|
);
|
|
2475
2476
|
}
|
|
2477
|
+
// CT1292: RENEW THE TURN'S LEASE — ask the server whether this turn is still
|
|
2478
|
+
// running. The dispatcher calls this on a cadence for the life of the SDK loop,
|
|
2479
|
+
// and out of cadence the moment a commit is refused, so a loop whose turn was
|
|
2480
|
+
// terminalized underneath it stops instead of running to the six-hour cap.
|
|
2481
|
+
//
|
|
2482
|
+
// Single-shot and NEVER retried, on purpose. A renewal is a question about
|
|
2483
|
+
// right now, not a durable write: a failed one has no answer worth replaying,
|
|
2484
|
+
// and the next tick asks again in a few seconds anyway. Retrying would only
|
|
2485
|
+
// stretch one tick over the next one's slot.
|
|
2486
|
+
//
|
|
2487
|
+
// ALL FAILURE IS `unknown`, and this is the safety property the whole feature
|
|
2488
|
+
// rests on. The only thing that may stop a running loop is the server SAYING
|
|
2489
|
+
// the turn is over; a 500, a dropped connection, an expired token or a body we
|
|
2490
|
+
// cannot parse says nothing about the turn, and reading any of them as death
|
|
2491
|
+
// would kill healthy turns every time the API hiccups — strictly worse than the
|
|
2492
|
+
// gap this closes. So the failure direction is fixed here, at the boundary,
|
|
2493
|
+
// rather than left to each caller to remember.
|
|
2494
|
+
//
|
|
2495
|
+
// AND IT IS DEADLINED (Codo's blocking finding #1). `fetch` has no default
|
|
2496
|
+
// timeout, so a request that neither resolves nor rejects — a black-holed
|
|
2497
|
+
// connection, a half-open socket surviving a network change — would hang this
|
|
2498
|
+
// promise forever. That is not merely a slow probe: the caller single-flights
|
|
2499
|
+
// on it, so one stalled request latches the check permanently, every later
|
|
2500
|
+
// cadence tick returns immediately, and the loop this exists to stop runs to
|
|
2501
|
+
// the six-hour cap even after connectivity comes back — the exact defect the
|
|
2502
|
+
// task is about, reintroduced through the mechanism meant to close it.
|
|
2503
|
+
//
|
|
2504
|
+
// The deadline turns that into an ordinary `unknown`: the request rejects, the
|
|
2505
|
+
// in-flight latch clears, and the NEXT tick asks again. Well under the renewal
|
|
2506
|
+
// cadence so a stalled probe is finished with before its successor is due.
|
|
2507
|
+
async checkTurnLease(workspaceId, conversationId, agentId, turnId) {
|
|
2508
|
+
const q = `turnId=${encodeURIComponent(turnId)}`;
|
|
2509
|
+
try {
|
|
2510
|
+
const res = await this.request(
|
|
2511
|
+
"GET",
|
|
2512
|
+
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-lease?${q}`,
|
|
2513
|
+
void 0,
|
|
2514
|
+
{
|
|
2515
|
+
signal: AbortSignal.timeout(this.opts.leaseCheckTimeoutMs ?? LEASE_CHECK_TIMEOUT_MS)
|
|
2516
|
+
}
|
|
2517
|
+
);
|
|
2518
|
+
return res?.state === "running" || res?.state === "ended" ? res.state : "unknown";
|
|
2519
|
+
} catch {
|
|
2520
|
+
return "unknown";
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2476
2523
|
// Flips `active_run_started_at` and optionally captures the SDK session
|
|
2477
2524
|
// id. The dispatcher hits this twice per turn (now() before the SDK
|
|
2478
2525
|
// loop; null when it settles) plus once with the session id on the
|
|
@@ -3110,7 +3157,9 @@ var turnResultReasonSchema = z6.discriminatedUnion("kind", [
|
|
|
3110
3157
|
z6.object({ kind: z6.literal("timeout_idle") }),
|
|
3111
3158
|
z6.object({ kind: z6.literal("timeout_total") }),
|
|
3112
3159
|
z6.object({ kind: z6.literal("cancelled") }),
|
|
3160
|
+
z6.object({ kind: z6.literal("lease_lost") }),
|
|
3113
3161
|
z6.object({ kind: z6.literal("skipped") }),
|
|
3162
|
+
z6.object({ kind: z6.literal("session_start_failed") }),
|
|
3114
3163
|
z6.object({ kind: z6.literal("workspace_tools_missing") }),
|
|
3115
3164
|
z6.object({ kind: z6.literal("runtime_error") })
|
|
3116
3165
|
]);
|
|
@@ -3274,8 +3323,12 @@ function normalizeTurnResultReason(reason) {
|
|
|
3274
3323
|
return { kind: "timeout_total" };
|
|
3275
3324
|
case "cancelled":
|
|
3276
3325
|
return { kind: "cancelled" };
|
|
3326
|
+
case "lease_lost":
|
|
3327
|
+
return { kind: "lease_lost" };
|
|
3277
3328
|
case "skipped":
|
|
3278
3329
|
return { kind: "skipped" };
|
|
3330
|
+
case "session_start_failed":
|
|
3331
|
+
return { kind: "session_start_failed" };
|
|
3279
3332
|
case "workspace_tools_missing":
|
|
3280
3333
|
return { kind: "workspace_tools_missing" };
|
|
3281
3334
|
default:
|
|
@@ -3724,8 +3777,9 @@ import {
|
|
|
3724
3777
|
|
|
3725
3778
|
// packages/agent-runtime/src/claude-code/addendum.ts
|
|
3726
3779
|
var CLAUDE_CODE_ADDENDUM = [
|
|
3727
|
-
"On this harness the Cabane tools register under MCP prefixes. The workspace
|
|
3728
|
-
"`
|
|
3780
|
+
"On this harness the Cabane tools register under MCP prefixes. The workspace tools are",
|
|
3781
|
+
"`mcp__cabane__query`, `mcp__cabane__mutate` and `mcp__cabane__describe`; the ancillary",
|
|
3782
|
+
"tools are `mcp__cabane__list_workspaces`,",
|
|
3729
3783
|
"`mcp__cabane__read_binary`, `mcp__cabane__upload`, `mcp__cabane__begin_upload`,",
|
|
3730
3784
|
"`mcp__cabane__finalize_upload`, and `mcp__cabane__mint_render_token`; the turn tools are",
|
|
3731
3785
|
"`mcp__cabane_companion__ask`, `mcp__cabane_companion__reply_to`,",
|
|
@@ -4002,7 +4056,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
4002
4056
|
};
|
|
4003
4057
|
}
|
|
4004
4058
|
} else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
|
|
4005
|
-
resultReason = "
|
|
4059
|
+
resultReason = "session_start_failed";
|
|
4006
4060
|
break;
|
|
4007
4061
|
} else if (msg.type === "assistant") {
|
|
4008
4062
|
const assistantErr = msg.error;
|
|
@@ -4069,7 +4123,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
4069
4123
|
if (ctx.signal.aborted) return;
|
|
4070
4124
|
if (!workspaceProven) {
|
|
4071
4125
|
ok = false;
|
|
4072
|
-
resultReason ??= "
|
|
4126
|
+
resultReason ??= "session_start_failed";
|
|
4073
4127
|
}
|
|
4074
4128
|
await flushHeldText(buffer, emit, ok);
|
|
4075
4129
|
yield* drain(out);
|
|
@@ -4096,7 +4150,7 @@ function readMcpInventory(msg) {
|
|
|
4096
4150
|
}
|
|
4097
4151
|
function provesWorkspaceTools(msg) {
|
|
4098
4152
|
const tools = msg.tools;
|
|
4099
|
-
return Array.isArray(tools) && tools.includes("
|
|
4153
|
+
return Array.isArray(tools) && tools.includes("mcp__cabane__query");
|
|
4100
4154
|
}
|
|
4101
4155
|
function isSubscriptionWindow(value) {
|
|
4102
4156
|
return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
|
|
@@ -4150,6 +4204,47 @@ function buildQueryPrompt(req) {
|
|
|
4150
4204
|
}
|
|
4151
4205
|
|
|
4152
4206
|
// packages/agent-runtime/src/claude-code/index.ts
|
|
4207
|
+
function attachAbortController(options, signal) {
|
|
4208
|
+
if (options.abortController) return;
|
|
4209
|
+
const sdkAbort = new AbortController();
|
|
4210
|
+
if (signal.aborted) sdkAbort.abort();
|
|
4211
|
+
else signal.addEventListener("abort", () => sdkAbort.abort(), { once: true });
|
|
4212
|
+
options.abortController = sdkAbort;
|
|
4213
|
+
}
|
|
4214
|
+
function sdkEvents(queryFn, req, signal, built, degraded) {
|
|
4215
|
+
attachAbortController(built.options, signal);
|
|
4216
|
+
const prompt = buildQueryPrompt(req);
|
|
4217
|
+
const iter = queryFn({ prompt, options: built.options });
|
|
4218
|
+
return decodeSdkStream(iter, {
|
|
4219
|
+
signal,
|
|
4220
|
+
cwd: req.local.cwd,
|
|
4221
|
+
resumedSessionId: built.resume,
|
|
4222
|
+
degraded
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOptions, onWarn, degraded) {
|
|
4226
|
+
let emitted = false;
|
|
4227
|
+
for await (const event of sdkEvents(queryFn, req, signal, initial, degraded)) {
|
|
4228
|
+
const recoverResume = initial.resume !== null && !emitted && event.type === "result" && !event.ok && event.reason === "session_start_failed" && event.mcpInventory?.initReceived === false;
|
|
4229
|
+
if (recoverResume) {
|
|
4230
|
+
if (signal.aborted) return;
|
|
4231
|
+
onWarn?.(
|
|
4232
|
+
`claude-code adapter: stored session ${initial.resume} ended before system/init \u2014 starting one fresh session (CT1293 recovery)`,
|
|
4233
|
+
{
|
|
4234
|
+
cwd: req.local.cwd,
|
|
4235
|
+
reason: "session_start_failed",
|
|
4236
|
+
discardedSessionId: initial.resume
|
|
4237
|
+
}
|
|
4238
|
+
);
|
|
4239
|
+
const freshReq = { ...req, session: null };
|
|
4240
|
+
const fresh = buildClaudeCodeOptions(freshReq, augmentOptions);
|
|
4241
|
+
yield* sdkEvents(queryFn, freshReq, signal, fresh, true);
|
|
4242
|
+
return;
|
|
4243
|
+
}
|
|
4244
|
+
emitted = true;
|
|
4245
|
+
yield event;
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4153
4248
|
function createClaudeCodeAdapter(deps = {}) {
|
|
4154
4249
|
const queryFn = deps.queryFn ?? defaultQuery;
|
|
4155
4250
|
return {
|
|
@@ -4165,20 +4260,15 @@ function createClaudeCodeAdapter(deps = {}) {
|
|
|
4165
4260
|
);
|
|
4166
4261
|
}
|
|
4167
4262
|
const degraded = freshReason !== void 0 && freshReason !== "no_session";
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
else signal.addEventListener("abort", () => sdkAbort.abort(), { once: true });
|
|
4172
|
-
options.abortController = sdkAbort;
|
|
4173
|
-
}
|
|
4174
|
-
const prompt = buildQueryPrompt(req);
|
|
4175
|
-
const iter = queryFn({ prompt, options });
|
|
4176
|
-
return decodeSdkStream(iter, {
|
|
4263
|
+
return runWithResumeRecovery(
|
|
4264
|
+
queryFn,
|
|
4265
|
+
req,
|
|
4177
4266
|
signal,
|
|
4178
|
-
|
|
4179
|
-
|
|
4267
|
+
{ options, resume, freshReason },
|
|
4268
|
+
deps.augmentOptions,
|
|
4269
|
+
deps.onWarn,
|
|
4180
4270
|
degraded
|
|
4181
|
-
|
|
4271
|
+
);
|
|
4182
4272
|
}
|
|
4183
4273
|
};
|
|
4184
4274
|
}
|
|
@@ -4222,7 +4312,7 @@ var init = (sessionId, model) => ({
|
|
|
4222
4312
|
subtype: "init",
|
|
4223
4313
|
session_id: sessionId,
|
|
4224
4314
|
mcp_servers: HEALTHY_MCP_INVENTORY.servers,
|
|
4225
|
-
tools: ["
|
|
4315
|
+
tools: ["mcp__cabane__query"],
|
|
4226
4316
|
...model ? { model } : {}
|
|
4227
4317
|
});
|
|
4228
4318
|
var assistantText = (text) => ({
|
|
@@ -4700,12 +4790,14 @@ function selectAdapter(registry, runtime) {
|
|
|
4700
4790
|
|
|
4701
4791
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
4702
4792
|
var OPENCODE_ADDENDUM = [
|
|
4703
|
-
"Every tool here goes by its plain name: the workspace
|
|
4704
|
-
"`ask`, `reply_to`, `wake_me`, `send`, `skip_turn`; the
|
|
4705
|
-
"`list_workspaces`, `read_binary`, `upload`, `begin_upload`,
|
|
4706
|
-
"`mint_render_token`; and the host tools are plain verbs too (`bash`,
|
|
4707
|
-
"the collision: a bare `read` or `edit` is the HOST tool, acting on the
|
|
4708
|
-
"workspace read is `
|
|
4793
|
+
"Every tool here goes by its plain name: the workspace tools are `query`, `mutate` and",
|
|
4794
|
+
"`describe`; the turn tools are `ask`, `reply_to`, `wake_me`, `send`, `skip_turn`; the",
|
|
4795
|
+
"ancillary tools are `list_workspaces`, `read_binary`, `upload`, `begin_upload`,",
|
|
4796
|
+
"`finalize_upload`, `mint_render_token`; and the host tools are plain verbs too (`bash`,",
|
|
4797
|
+
"`read`, `edit`). Mind the collision: a bare `read` or `edit` is the HOST tool, acting on the",
|
|
4798
|
+
"local machine \u2014 a workspace read is `query` with the operation name `files.read`. The",
|
|
4799
|
+
"collision runs the other way too: `read` is a host tool here AND `files.read` is an",
|
|
4800
|
+
"operation name, so keep the bare verb and the dotted name apart. If a tool ever appears with",
|
|
4709
4801
|
"an `mcp__\u2026__` prefix, that prefix is not part of its name here. One more harness fact: you",
|
|
4710
4802
|
"can interleave narration with tool calls, but only your final message is recorded as the",
|
|
4711
4803
|
"turn's reply \u2014 write your closing answer last."
|
|
@@ -5674,15 +5766,18 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
5674
5766
|
// packages/agent-runtime/src/codex/addendum.ts
|
|
5675
5767
|
var CODEX_ADDENDUM = [
|
|
5676
5768
|
"Codex surfaces MCP tools under qualified names, and on some versions defers them. The",
|
|
5677
|
-
"workspace
|
|
5678
|
-
"
|
|
5769
|
+
"workspace tools are `mcp__cabane__query`, `mcp__cabane__mutate` and",
|
|
5770
|
+
"`mcp__cabane__describe` \u2014 if they aren't in your visible tool list, locate them in",
|
|
5771
|
+
"the deferred-tool inventory and invoke those exact qualified names; never conclude the",
|
|
5772
|
+
"workspace is",
|
|
5679
5773
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
5680
5774
|
"error. The turn tools are `mcp__cabane_companion__ask` / `reply_to` / `wake_me` /",
|
|
5681
5775
|
"`send` / `skip_turn`, and the ancillary tools",
|
|
5682
5776
|
"`mcp__cabane__list_workspaces` /",
|
|
5683
5777
|
"`read_binary` / `upload` / `begin_upload` / `finalize_upload` / `mint_render_token` \u2014 all",
|
|
5684
5778
|
"possibly deferred too. There is no Cabane `read`/`write`/`search` tool \u2014 those are",
|
|
5685
|
-
"
|
|
5779
|
+
"operation NAMES you pass to `query` / `mutate`. One more harness fact: you can interleave",
|
|
5780
|
+
"narration",
|
|
5686
5781
|
"with tool calls, but only your final message is recorded as the turn's reply \u2014 write your",
|
|
5687
5782
|
"closing answer last."
|
|
5688
5783
|
].join(" ");
|
|
@@ -5780,9 +5875,12 @@ function readUsage(ev) {
|
|
|
5780
5875
|
const usage = asRecord2(ev.usage);
|
|
5781
5876
|
if (!usage) return void 0;
|
|
5782
5877
|
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5878
|
+
return {
|
|
5879
|
+
inputTokens: num(usage.input_tokens),
|
|
5880
|
+
outputTokens: num(usage.output_tokens),
|
|
5881
|
+
cacheReadTokens: num(usage.cached_input_tokens),
|
|
5882
|
+
cacheCreationTokens: num(usage.cache_write_input_tokens)
|
|
5883
|
+
};
|
|
5786
5884
|
}
|
|
5787
5885
|
function toStatus(v) {
|
|
5788
5886
|
return v === "completed" || v === "failed" ? v : "in_progress";
|
|
@@ -6344,24 +6442,14 @@ var errorItem = (id, message) => ({
|
|
|
6344
6442
|
type: "item.completed",
|
|
6345
6443
|
item: { id, type: "error", message }
|
|
6346
6444
|
});
|
|
6347
|
-
var
|
|
6348
|
-
var
|
|
6445
|
+
var OP_SUCCESS = { ok: true, result: { source: "native" } };
|
|
6446
|
+
var OP_FAILURE = {
|
|
6349
6447
|
ok: false,
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
warnings: [],
|
|
6356
|
-
committed: {
|
|
6357
|
-
calls: [
|
|
6358
|
-
{
|
|
6359
|
-
method: "conversations.update",
|
|
6360
|
-
args: { id: "conv-1", opts: { title: "Renamed before failure" } }
|
|
6361
|
-
}
|
|
6362
|
-
]
|
|
6363
|
-
},
|
|
6364
|
-
inFlight: null
|
|
6448
|
+
error: {
|
|
6449
|
+
kind: "output_too_large",
|
|
6450
|
+
code: "output_too_large",
|
|
6451
|
+
message: "files.read returned more than the result ceiling"
|
|
6452
|
+
}
|
|
6365
6453
|
};
|
|
6366
6454
|
var sessionEvent3 = (threadId, cwd = DIR2, degraded = false, promptFingerprint = fingerprintPrompt(PROMPT)) => ({
|
|
6367
6455
|
type: "session",
|
|
@@ -6372,6 +6460,11 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6372
6460
|
{
|
|
6373
6461
|
// A plain text reply: the held agent message seals as the terminal final at
|
|
6374
6462
|
// `turn.completed`; the usage counters ride the `result`.
|
|
6463
|
+
//
|
|
6464
|
+
// CT1283: the counters NEST — `cached_input_tokens` is a slice of
|
|
6465
|
+
// `input_tokens`, `reasoning_output_tokens` a slice of `output_tokens` — so
|
|
6466
|
+
// input is 100 (not 120) and output is 50 (not 60), with the cache read broken
|
|
6467
|
+
// out beside them. This fixture pinned the double-count before CT1283.
|
|
6375
6468
|
name: "clean turn",
|
|
6376
6469
|
request: makeRequest3(),
|
|
6377
6470
|
nativeStream: [
|
|
@@ -6380,6 +6473,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6380
6473
|
turnCompleted({
|
|
6381
6474
|
input_tokens: 100,
|
|
6382
6475
|
cached_input_tokens: 20,
|
|
6476
|
+
cache_write_input_tokens: 5,
|
|
6383
6477
|
output_tokens: 50,
|
|
6384
6478
|
reasoning_output_tokens: 10
|
|
6385
6479
|
})
|
|
@@ -6387,7 +6481,16 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6387
6481
|
expected: [
|
|
6388
6482
|
sessionEvent3(NEW_THREAD_ID),
|
|
6389
6483
|
{ type: "text", body: "Hello there.", terminal: true },
|
|
6390
|
-
{
|
|
6484
|
+
{
|
|
6485
|
+
type: "result",
|
|
6486
|
+
ok: true,
|
|
6487
|
+
usage: {
|
|
6488
|
+
inputTokens: 100,
|
|
6489
|
+
outputTokens: 50,
|
|
6490
|
+
cacheReadTokens: 20,
|
|
6491
|
+
cacheCreationTokens: 5
|
|
6492
|
+
}
|
|
6493
|
+
}
|
|
6391
6494
|
]
|
|
6392
6495
|
},
|
|
6393
6496
|
{
|
|
@@ -6443,30 +6546,30 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6443
6546
|
]
|
|
6444
6547
|
},
|
|
6445
6548
|
{
|
|
6446
|
-
// CT1211: recorded
|
|
6549
|
+
// CT1211: recorded MCP success shape. Codex exposes the text compatibility
|
|
6447
6550
|
// copy and the native object together; normalization must keep the object.
|
|
6448
6551
|
name: "MCP success prefers native structured content",
|
|
6449
6552
|
request: makeRequest3(),
|
|
6450
6553
|
nativeStream: [
|
|
6451
6554
|
threadStarted(NEW_THREAD_ID),
|
|
6452
6555
|
toolFrame("item.started", {
|
|
6453
|
-
id: "
|
|
6556
|
+
id: "query-success",
|
|
6454
6557
|
type: "mcp_tool_call",
|
|
6455
6558
|
server: "cabane",
|
|
6456
|
-
tool: "
|
|
6457
|
-
arguments: {
|
|
6559
|
+
tool: "query",
|
|
6560
|
+
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6458
6561
|
status: "in_progress"
|
|
6459
6562
|
}),
|
|
6460
6563
|
toolFrame("item.completed", {
|
|
6461
|
-
id: "
|
|
6564
|
+
id: "query-success",
|
|
6462
6565
|
type: "mcp_tool_call",
|
|
6463
6566
|
server: "cabane",
|
|
6464
|
-
tool: "
|
|
6465
|
-
arguments: {
|
|
6567
|
+
tool: "query",
|
|
6568
|
+
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6466
6569
|
status: "completed",
|
|
6467
6570
|
result: {
|
|
6468
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
6469
|
-
structured_content:
|
|
6571
|
+
content: [{ type: "text", text: JSON.stringify(OP_SUCCESS) }],
|
|
6572
|
+
structured_content: OP_SUCCESS
|
|
6470
6573
|
}
|
|
6471
6574
|
}),
|
|
6472
6575
|
turnCompleted()
|
|
@@ -6475,20 +6578,20 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6475
6578
|
sessionEvent3(NEW_THREAD_ID),
|
|
6476
6579
|
{
|
|
6477
6580
|
type: "tool",
|
|
6478
|
-
id: "
|
|
6479
|
-
name: "
|
|
6581
|
+
id: "query-success",
|
|
6582
|
+
name: "query",
|
|
6480
6583
|
phase: "start",
|
|
6481
6584
|
summary: "",
|
|
6482
|
-
input: {
|
|
6585
|
+
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
6483
6586
|
},
|
|
6484
6587
|
{
|
|
6485
6588
|
type: "tool",
|
|
6486
|
-
id: "
|
|
6487
|
-
name: "
|
|
6589
|
+
id: "query-success",
|
|
6590
|
+
name: "query",
|
|
6488
6591
|
phase: "done",
|
|
6489
6592
|
summary: "",
|
|
6490
|
-
input: {
|
|
6491
|
-
result:
|
|
6593
|
+
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6594
|
+
result: OP_SUCCESS
|
|
6492
6595
|
},
|
|
6493
6596
|
{ type: "result", ok: true }
|
|
6494
6597
|
]
|
|
@@ -6496,28 +6599,28 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6496
6599
|
{
|
|
6497
6600
|
// CT1211 incident shape: MCP `isError` keeps the failed phase, while the raw
|
|
6498
6601
|
// camel-case structured payload survives instead of the generic `error` token.
|
|
6499
|
-
name: "failed
|
|
6602
|
+
name: "failed MCP call preserves structured receipt while remaining error phase",
|
|
6500
6603
|
request: makeRequest3(),
|
|
6501
6604
|
nativeStream: [
|
|
6502
6605
|
threadStarted(NEW_THREAD_ID),
|
|
6503
6606
|
toolFrame("item.started", {
|
|
6504
|
-
id: "
|
|
6607
|
+
id: "query-failed",
|
|
6505
6608
|
type: "mcp_tool_call",
|
|
6506
6609
|
server: "cabane",
|
|
6507
|
-
tool: "
|
|
6508
|
-
arguments: {
|
|
6610
|
+
tool: "query",
|
|
6611
|
+
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6509
6612
|
status: "in_progress"
|
|
6510
6613
|
}),
|
|
6511
6614
|
toolFrame("item.completed", {
|
|
6512
|
-
id: "
|
|
6615
|
+
id: "query-failed",
|
|
6513
6616
|
type: "mcp_tool_call",
|
|
6514
6617
|
server: "cabane",
|
|
6515
|
-
tool: "
|
|
6516
|
-
arguments: {
|
|
6618
|
+
tool: "query",
|
|
6619
|
+
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6517
6620
|
status: "failed",
|
|
6518
6621
|
result: {
|
|
6519
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
6520
|
-
structuredContent:
|
|
6622
|
+
content: [{ type: "text", text: JSON.stringify(OP_FAILURE) }],
|
|
6623
|
+
structuredContent: OP_FAILURE,
|
|
6521
6624
|
isError: true
|
|
6522
6625
|
},
|
|
6523
6626
|
error: { message: "error" }
|
|
@@ -6528,20 +6631,20 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6528
6631
|
sessionEvent3(NEW_THREAD_ID),
|
|
6529
6632
|
{
|
|
6530
6633
|
type: "tool",
|
|
6531
|
-
id: "
|
|
6532
|
-
name: "
|
|
6634
|
+
id: "query-failed",
|
|
6635
|
+
name: "query",
|
|
6533
6636
|
phase: "start",
|
|
6534
6637
|
summary: "",
|
|
6535
|
-
input: {
|
|
6638
|
+
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
6536
6639
|
},
|
|
6537
6640
|
{
|
|
6538
6641
|
type: "tool",
|
|
6539
|
-
id: "
|
|
6540
|
-
name: "
|
|
6642
|
+
id: "query-failed",
|
|
6643
|
+
name: "query",
|
|
6541
6644
|
phase: "error",
|
|
6542
6645
|
summary: "",
|
|
6543
|
-
input: {
|
|
6544
|
-
result:
|
|
6646
|
+
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6647
|
+
result: OP_FAILURE
|
|
6545
6648
|
},
|
|
6546
6649
|
{ type: "result", ok: true }
|
|
6547
6650
|
]
|
|
@@ -7772,6 +7875,7 @@ var TurnCommitter = class {
|
|
|
7772
7875
|
{ err: err instanceof Error ? err.message : String(err), hook },
|
|
7773
7876
|
"dispatcher: transcript callback failed"
|
|
7774
7877
|
);
|
|
7878
|
+
deps.onCommitFailed?.(err);
|
|
7775
7879
|
};
|
|
7776
7880
|
const commit = {
|
|
7777
7881
|
commitMessage: async ({ body, kind, seq }) => {
|
|
@@ -7836,6 +7940,7 @@ var TurnCommitter = class {
|
|
|
7836
7940
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
7837
7941
|
"dispatcher: progress-promotion commit failed"
|
|
7838
7942
|
);
|
|
7943
|
+
deps.onCommitFailed?.(err);
|
|
7839
7944
|
}
|
|
7840
7945
|
});
|
|
7841
7946
|
this.emit = sinkEmitter(this.pump.sink, this.onError);
|
|
@@ -7995,6 +8100,7 @@ var SILENT_MARKER_BODY = "(no reply)";
|
|
|
7995
8100
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
7996
8101
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7997
8102
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
8103
|
+
var DEFAULT_LEASE_RENEWAL_MS = 3e4;
|
|
7998
8104
|
function checkoutState(cwd) {
|
|
7999
8105
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
8000
8106
|
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
@@ -8027,13 +8133,19 @@ var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
|
8027
8133
|
"turn_already_ended",
|
|
8028
8134
|
"turn_belongs_elsewhere"
|
|
8029
8135
|
]);
|
|
8030
|
-
function
|
|
8136
|
+
function apiErrorCode(err) {
|
|
8031
8137
|
if (!(err instanceof ApiError)) return null;
|
|
8032
8138
|
const body = err.body;
|
|
8033
|
-
|
|
8139
|
+
return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
8140
|
+
}
|
|
8141
|
+
function leaseRefusal(err) {
|
|
8142
|
+
const code = apiErrorCode(err);
|
|
8034
8143
|
if (code && LEASE_REFUSALS.has(code)) return code;
|
|
8035
8144
|
return null;
|
|
8036
8145
|
}
|
|
8146
|
+
function isWriteFenceRefusal(err) {
|
|
8147
|
+
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
8148
|
+
}
|
|
8037
8149
|
var ERROR_BODY_LOG_CAP = 2e3;
|
|
8038
8150
|
function describeErrorBody(body) {
|
|
8039
8151
|
if (body === void 0 || body === null) return void 0;
|
|
@@ -8379,6 +8491,7 @@ ${reason}`,
|
|
|
8379
8491
|
if (pair2.size === 0) this.aborts.delete(key);
|
|
8380
8492
|
};
|
|
8381
8493
|
let timeoutReason = null;
|
|
8494
|
+
let leaseLost = false;
|
|
8382
8495
|
try {
|
|
8383
8496
|
await this.opts.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
|
|
8384
8497
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8526,6 +8639,8 @@ ${reason}`,
|
|
|
8526
8639
|
let latestSessionState = request.session;
|
|
8527
8640
|
let settledDiagnostics = null;
|
|
8528
8641
|
let silentMarkerEmitted = false;
|
|
8642
|
+
let noteCommitFailed = () => {
|
|
8643
|
+
};
|
|
8529
8644
|
const committer = new TurnCommitter({
|
|
8530
8645
|
api: this.opts.api,
|
|
8531
8646
|
workspaceId,
|
|
@@ -8551,7 +8666,9 @@ ${reason}`,
|
|
|
8551
8666
|
replyState,
|
|
8552
8667
|
// The ledger-derived owed reply, for the runtime's own declaration when
|
|
8553
8668
|
// the agent doesn't call `reply_to` (resolveDeclaredReplyField).
|
|
8554
|
-
owedReplyMessageId: turnContext.owedReplyMessageId ?? null
|
|
8669
|
+
owedReplyMessageId: turnContext.owedReplyMessageId ?? null,
|
|
8670
|
+
// CT1292: a commit that didn't land may mean this turn's lease is gone.
|
|
8671
|
+
onCommitFailed: (err) => noteCommitFailed(err)
|
|
8555
8672
|
});
|
|
8556
8673
|
const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
|
|
8557
8674
|
let turnControlIntentFetched = false;
|
|
@@ -8631,6 +8748,35 @@ ${reason}`,
|
|
|
8631
8748
|
const totalTimer = setTimeout(() => fireTimeout("timeout_total"), totalTimeoutMs);
|
|
8632
8749
|
totalTimer.unref?.();
|
|
8633
8750
|
armIdle();
|
|
8751
|
+
const leaseRenewalMs = this.opts.leaseRenewalMs ?? DEFAULT_LEASE_RENEWAL_MS;
|
|
8752
|
+
let leaseCheckInFlight = false;
|
|
8753
|
+
const checkLease = async (trigger) => {
|
|
8754
|
+
if (leaseLost || leaseCheckInFlight || abortController.signal.aborted) return;
|
|
8755
|
+
leaseCheckInFlight = true;
|
|
8756
|
+
try {
|
|
8757
|
+
const state = await this.opts.api.checkTurnLease(
|
|
8758
|
+
workspaceId,
|
|
8759
|
+
payload.conversationId,
|
|
8760
|
+
payload.agentId,
|
|
8761
|
+
turnId
|
|
8762
|
+
);
|
|
8763
|
+
if (state !== "ended") return;
|
|
8764
|
+
if (abortController.signal.aborted) return;
|
|
8765
|
+
leaseLost = true;
|
|
8766
|
+
turnLog.warn(
|
|
8767
|
+
{ turnId, trigger },
|
|
8768
|
+
"dispatcher: this turn is no longer running server-side \u2014 aborting the loop"
|
|
8769
|
+
);
|
|
8770
|
+
abortController.abort();
|
|
8771
|
+
} finally {
|
|
8772
|
+
leaseCheckInFlight = false;
|
|
8773
|
+
}
|
|
8774
|
+
};
|
|
8775
|
+
const leaseTimer = setInterval(() => void checkLease("cadence"), leaseRenewalMs);
|
|
8776
|
+
leaseTimer.unref?.();
|
|
8777
|
+
noteCommitFailed = (err) => {
|
|
8778
|
+
if (isWriteFenceRefusal(err)) void checkLease("commit_refused");
|
|
8779
|
+
};
|
|
8634
8780
|
try {
|
|
8635
8781
|
for await (const event of adapter.runTurn(request, abortController.signal)) {
|
|
8636
8782
|
transcript2?.write(event);
|
|
@@ -8695,7 +8841,7 @@ ${reason}`,
|
|
|
8695
8841
|
}
|
|
8696
8842
|
if (abortController.signal.aborted) {
|
|
8697
8843
|
okResult = false;
|
|
8698
|
-
resultReason = timeoutReason ?? "cancelled";
|
|
8844
|
+
resultReason = leaseLost ? "lease_lost" : timeoutReason ?? "cancelled";
|
|
8699
8845
|
}
|
|
8700
8846
|
const emptyResultReason = !skipState.skipped && classifyEmptyResult({ ok: okResult, contentBearingEvents, usage: turnUsage });
|
|
8701
8847
|
if (emptyResultReason) {
|
|
@@ -8760,7 +8906,12 @@ ${reason}`,
|
|
|
8760
8906
|
} finally {
|
|
8761
8907
|
if (idleTimer) clearTimeout(idleTimer);
|
|
8762
8908
|
clearTimeout(totalTimer);
|
|
8763
|
-
|
|
8909
|
+
clearInterval(leaseTimer);
|
|
8910
|
+
if (leaseLost) {
|
|
8911
|
+
resultReason = "lease_lost";
|
|
8912
|
+
okResult = false;
|
|
8913
|
+
}
|
|
8914
|
+
const userCancelled = abortController.signal.aborted && timeoutReason === null && !leaseLost;
|
|
8764
8915
|
if (timeoutReason !== null) {
|
|
8765
8916
|
resultReason = timeoutReason;
|
|
8766
8917
|
okResult = false;
|
|
@@ -8802,12 +8953,13 @@ ${reason}`,
|
|
|
8802
8953
|
if (turnResolvedConfig && Object.keys(turnResolvedConfig).length > 0) {
|
|
8803
8954
|
body.resolvedConfig = turnResolvedConfig;
|
|
8804
8955
|
}
|
|
8805
|
-
if (!okResult && resultReason && resultReason !== "cancelled" && !userCancelled && !skipState.skipped) {
|
|
8956
|
+
if (!okResult && resultReason && resultReason !== "cancelled" && !userCancelled && !leaseLost && !skipState.skipped) {
|
|
8806
8957
|
body.errorReason = resultReason.slice(0, 200);
|
|
8807
8958
|
}
|
|
8808
8959
|
if (okResult) {
|
|
8809
8960
|
body.lastSeenMessageId = payload.messageId;
|
|
8810
8961
|
}
|
|
8962
|
+
body.outcome = okResult ? "settled" : body.errorReason ? "failed" : "interrupted";
|
|
8811
8963
|
if (sessionDegraded) {
|
|
8812
8964
|
body.degraded = true;
|
|
8813
8965
|
}
|
|
@@ -8815,8 +8967,8 @@ ${reason}`,
|
|
|
8815
8967
|
body.sessionWriteRejected = true;
|
|
8816
8968
|
this.sessionWriteNotified.add(key);
|
|
8817
8969
|
}
|
|
8818
|
-
const outcome = skipState.skipped ? "skipped" : userCancelled ? "cancelled" : okResult ? "success" : "failure";
|
|
8819
|
-
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
8970
|
+
const outcome = skipState.skipped ? "skipped" : userCancelled || leaseLost ? "cancelled" : okResult ? "success" : "failure";
|
|
8971
|
+
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? leaseLost ? { kind: "lease_lost" } : { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
8820
8972
|
settledDiagnostics = {
|
|
8821
8973
|
outcome,
|
|
8822
8974
|
resultReason: diagnosticReason,
|
package/dist/runtime.js
CHANGED
|
@@ -1704,6 +1704,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
1704
1704
|
// src/api.ts
|
|
1705
1705
|
var RETRY_BACKOFF_MS = [250, 750];
|
|
1706
1706
|
var ACTIVE_RUN_OUTBOX_SEQ = 0;
|
|
1707
|
+
var LEASE_CHECK_TIMEOUT_MS = 1e4;
|
|
1707
1708
|
function activeRunOutboxKey(conversationId, agentId) {
|
|
1708
1709
|
return `activerun:${conversationId}:${agentId}`;
|
|
1709
1710
|
}
|
|
@@ -1893,6 +1894,52 @@ var CabaneApi = class {
|
|
|
1893
1894
|
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-intent?${q}`
|
|
1894
1895
|
);
|
|
1895
1896
|
}
|
|
1897
|
+
// CT1292: RENEW THE TURN'S LEASE — ask the server whether this turn is still
|
|
1898
|
+
// running. The dispatcher calls this on a cadence for the life of the SDK loop,
|
|
1899
|
+
// and out of cadence the moment a commit is refused, so a loop whose turn was
|
|
1900
|
+
// terminalized underneath it stops instead of running to the six-hour cap.
|
|
1901
|
+
//
|
|
1902
|
+
// Single-shot and NEVER retried, on purpose. A renewal is a question about
|
|
1903
|
+
// right now, not a durable write: a failed one has no answer worth replaying,
|
|
1904
|
+
// and the next tick asks again in a few seconds anyway. Retrying would only
|
|
1905
|
+
// stretch one tick over the next one's slot.
|
|
1906
|
+
//
|
|
1907
|
+
// ALL FAILURE IS `unknown`, and this is the safety property the whole feature
|
|
1908
|
+
// rests on. The only thing that may stop a running loop is the server SAYING
|
|
1909
|
+
// the turn is over; a 500, a dropped connection, an expired token or a body we
|
|
1910
|
+
// cannot parse says nothing about the turn, and reading any of them as death
|
|
1911
|
+
// would kill healthy turns every time the API hiccups — strictly worse than the
|
|
1912
|
+
// gap this closes. So the failure direction is fixed here, at the boundary,
|
|
1913
|
+
// rather than left to each caller to remember.
|
|
1914
|
+
//
|
|
1915
|
+
// AND IT IS DEADLINED (Codo's blocking finding #1). `fetch` has no default
|
|
1916
|
+
// timeout, so a request that neither resolves nor rejects — a black-holed
|
|
1917
|
+
// connection, a half-open socket surviving a network change — would hang this
|
|
1918
|
+
// promise forever. That is not merely a slow probe: the caller single-flights
|
|
1919
|
+
// on it, so one stalled request latches the check permanently, every later
|
|
1920
|
+
// cadence tick returns immediately, and the loop this exists to stop runs to
|
|
1921
|
+
// the six-hour cap even after connectivity comes back — the exact defect the
|
|
1922
|
+
// task is about, reintroduced through the mechanism meant to close it.
|
|
1923
|
+
//
|
|
1924
|
+
// The deadline turns that into an ordinary `unknown`: the request rejects, the
|
|
1925
|
+
// in-flight latch clears, and the NEXT tick asks again. Well under the renewal
|
|
1926
|
+
// cadence so a stalled probe is finished with before its successor is due.
|
|
1927
|
+
async checkTurnLease(workspaceId, conversationId, agentId, turnId) {
|
|
1928
|
+
const q = `turnId=${encodeURIComponent(turnId)}`;
|
|
1929
|
+
try {
|
|
1930
|
+
const res = await this.request(
|
|
1931
|
+
"GET",
|
|
1932
|
+
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-lease?${q}`,
|
|
1933
|
+
void 0,
|
|
1934
|
+
{
|
|
1935
|
+
signal: AbortSignal.timeout(this.opts.leaseCheckTimeoutMs ?? LEASE_CHECK_TIMEOUT_MS)
|
|
1936
|
+
}
|
|
1937
|
+
);
|
|
1938
|
+
return res?.state === "running" || res?.state === "ended" ? res.state : "unknown";
|
|
1939
|
+
} catch {
|
|
1940
|
+
return "unknown";
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1896
1943
|
// Flips `active_run_started_at` and optionally captures the SDK session
|
|
1897
1944
|
// id. The dispatcher hits this twice per turn (now() before the SDK
|
|
1898
1945
|
// loop; null when it settles) plus once with the session id on the
|
|
@@ -2609,7 +2656,9 @@ var turnResultReasonSchema = z6.discriminatedUnion("kind", [
|
|
|
2609
2656
|
z6.object({ kind: z6.literal("timeout_idle") }),
|
|
2610
2657
|
z6.object({ kind: z6.literal("timeout_total") }),
|
|
2611
2658
|
z6.object({ kind: z6.literal("cancelled") }),
|
|
2659
|
+
z6.object({ kind: z6.literal("lease_lost") }),
|
|
2612
2660
|
z6.object({ kind: z6.literal("skipped") }),
|
|
2661
|
+
z6.object({ kind: z6.literal("session_start_failed") }),
|
|
2613
2662
|
z6.object({ kind: z6.literal("workspace_tools_missing") }),
|
|
2614
2663
|
z6.object({ kind: z6.literal("runtime_error") })
|
|
2615
2664
|
]);
|
|
@@ -2773,8 +2822,12 @@ function normalizeTurnResultReason(reason) {
|
|
|
2773
2822
|
return { kind: "timeout_total" };
|
|
2774
2823
|
case "cancelled":
|
|
2775
2824
|
return { kind: "cancelled" };
|
|
2825
|
+
case "lease_lost":
|
|
2826
|
+
return { kind: "lease_lost" };
|
|
2776
2827
|
case "skipped":
|
|
2777
2828
|
return { kind: "skipped" };
|
|
2829
|
+
case "session_start_failed":
|
|
2830
|
+
return { kind: "session_start_failed" };
|
|
2778
2831
|
case "workspace_tools_missing":
|
|
2779
2832
|
return { kind: "workspace_tools_missing" };
|
|
2780
2833
|
default:
|
|
@@ -3223,8 +3276,9 @@ import {
|
|
|
3223
3276
|
|
|
3224
3277
|
// packages/agent-runtime/src/claude-code/addendum.ts
|
|
3225
3278
|
var CLAUDE_CODE_ADDENDUM = [
|
|
3226
|
-
"On this harness the Cabane tools register under MCP prefixes. The workspace
|
|
3227
|
-
"`
|
|
3279
|
+
"On this harness the Cabane tools register under MCP prefixes. The workspace tools are",
|
|
3280
|
+
"`mcp__cabane__query`, `mcp__cabane__mutate` and `mcp__cabane__describe`; the ancillary",
|
|
3281
|
+
"tools are `mcp__cabane__list_workspaces`,",
|
|
3228
3282
|
"`mcp__cabane__read_binary`, `mcp__cabane__upload`, `mcp__cabane__begin_upload`,",
|
|
3229
3283
|
"`mcp__cabane__finalize_upload`, and `mcp__cabane__mint_render_token`; the turn tools are",
|
|
3230
3284
|
"`mcp__cabane_companion__ask`, `mcp__cabane_companion__reply_to`,",
|
|
@@ -3501,7 +3555,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3501
3555
|
};
|
|
3502
3556
|
}
|
|
3503
3557
|
} else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
|
|
3504
|
-
resultReason = "
|
|
3558
|
+
resultReason = "session_start_failed";
|
|
3505
3559
|
break;
|
|
3506
3560
|
} else if (msg.type === "assistant") {
|
|
3507
3561
|
const assistantErr = msg.error;
|
|
@@ -3568,7 +3622,7 @@ async function* decodeSdkStream(iter, ctx) {
|
|
|
3568
3622
|
if (ctx.signal.aborted) return;
|
|
3569
3623
|
if (!workspaceProven) {
|
|
3570
3624
|
ok = false;
|
|
3571
|
-
resultReason ??= "
|
|
3625
|
+
resultReason ??= "session_start_failed";
|
|
3572
3626
|
}
|
|
3573
3627
|
await flushHeldText(buffer, emit, ok);
|
|
3574
3628
|
yield* drain(out);
|
|
@@ -3595,7 +3649,7 @@ function readMcpInventory(msg) {
|
|
|
3595
3649
|
}
|
|
3596
3650
|
function provesWorkspaceTools(msg) {
|
|
3597
3651
|
const tools = msg.tools;
|
|
3598
|
-
return Array.isArray(tools) && tools.includes("
|
|
3652
|
+
return Array.isArray(tools) && tools.includes("mcp__cabane__query");
|
|
3599
3653
|
}
|
|
3600
3654
|
function isSubscriptionWindow(value) {
|
|
3601
3655
|
return value === "five_hour" || value === "seven_day" || value === "seven_day_opus" || value === "seven_day_sonnet" || value === "overage";
|
|
@@ -3649,6 +3703,47 @@ function buildQueryPrompt(req) {
|
|
|
3649
3703
|
}
|
|
3650
3704
|
|
|
3651
3705
|
// packages/agent-runtime/src/claude-code/index.ts
|
|
3706
|
+
function attachAbortController(options, signal) {
|
|
3707
|
+
if (options.abortController) return;
|
|
3708
|
+
const sdkAbort = new AbortController();
|
|
3709
|
+
if (signal.aborted) sdkAbort.abort();
|
|
3710
|
+
else signal.addEventListener("abort", () => sdkAbort.abort(), { once: true });
|
|
3711
|
+
options.abortController = sdkAbort;
|
|
3712
|
+
}
|
|
3713
|
+
function sdkEvents(queryFn, req, signal, built, degraded) {
|
|
3714
|
+
attachAbortController(built.options, signal);
|
|
3715
|
+
const prompt = buildQueryPrompt(req);
|
|
3716
|
+
const iter = queryFn({ prompt, options: built.options });
|
|
3717
|
+
return decodeSdkStream(iter, {
|
|
3718
|
+
signal,
|
|
3719
|
+
cwd: req.local.cwd,
|
|
3720
|
+
resumedSessionId: built.resume,
|
|
3721
|
+
degraded
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOptions, onWarn, degraded) {
|
|
3725
|
+
let emitted = false;
|
|
3726
|
+
for await (const event of sdkEvents(queryFn, req, signal, initial, degraded)) {
|
|
3727
|
+
const recoverResume = initial.resume !== null && !emitted && event.type === "result" && !event.ok && event.reason === "session_start_failed" && event.mcpInventory?.initReceived === false;
|
|
3728
|
+
if (recoverResume) {
|
|
3729
|
+
if (signal.aborted) return;
|
|
3730
|
+
onWarn?.(
|
|
3731
|
+
`claude-code adapter: stored session ${initial.resume} ended before system/init \u2014 starting one fresh session (CT1293 recovery)`,
|
|
3732
|
+
{
|
|
3733
|
+
cwd: req.local.cwd,
|
|
3734
|
+
reason: "session_start_failed",
|
|
3735
|
+
discardedSessionId: initial.resume
|
|
3736
|
+
}
|
|
3737
|
+
);
|
|
3738
|
+
const freshReq = { ...req, session: null };
|
|
3739
|
+
const fresh = buildClaudeCodeOptions(freshReq, augmentOptions);
|
|
3740
|
+
yield* sdkEvents(queryFn, freshReq, signal, fresh, true);
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
emitted = true;
|
|
3744
|
+
yield event;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3652
3747
|
function createClaudeCodeAdapter(deps = {}) {
|
|
3653
3748
|
const queryFn = deps.queryFn ?? defaultQuery;
|
|
3654
3749
|
return {
|
|
@@ -3664,20 +3759,15 @@ function createClaudeCodeAdapter(deps = {}) {
|
|
|
3664
3759
|
);
|
|
3665
3760
|
}
|
|
3666
3761
|
const degraded = freshReason !== void 0 && freshReason !== "no_session";
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
else signal.addEventListener("abort", () => sdkAbort.abort(), { once: true });
|
|
3671
|
-
options.abortController = sdkAbort;
|
|
3672
|
-
}
|
|
3673
|
-
const prompt = buildQueryPrompt(req);
|
|
3674
|
-
const iter = queryFn({ prompt, options });
|
|
3675
|
-
return decodeSdkStream(iter, {
|
|
3762
|
+
return runWithResumeRecovery(
|
|
3763
|
+
queryFn,
|
|
3764
|
+
req,
|
|
3676
3765
|
signal,
|
|
3677
|
-
|
|
3678
|
-
|
|
3766
|
+
{ options, resume, freshReason },
|
|
3767
|
+
deps.augmentOptions,
|
|
3768
|
+
deps.onWarn,
|
|
3679
3769
|
degraded
|
|
3680
|
-
|
|
3770
|
+
);
|
|
3681
3771
|
}
|
|
3682
3772
|
};
|
|
3683
3773
|
}
|
|
@@ -3721,7 +3811,7 @@ var init = (sessionId, model) => ({
|
|
|
3721
3811
|
subtype: "init",
|
|
3722
3812
|
session_id: sessionId,
|
|
3723
3813
|
mcp_servers: HEALTHY_MCP_INVENTORY.servers,
|
|
3724
|
-
tools: ["
|
|
3814
|
+
tools: ["mcp__cabane__query"],
|
|
3725
3815
|
...model ? { model } : {}
|
|
3726
3816
|
});
|
|
3727
3817
|
var assistantText = (text) => ({
|
|
@@ -4199,12 +4289,14 @@ function selectAdapter(registry, runtime) {
|
|
|
4199
4289
|
|
|
4200
4290
|
// packages/agent-runtime/src/opencode/addendum.ts
|
|
4201
4291
|
var OPENCODE_ADDENDUM = [
|
|
4202
|
-
"Every tool here goes by its plain name: the workspace
|
|
4203
|
-
"`ask`, `reply_to`, `wake_me`, `send`, `skip_turn`; the
|
|
4204
|
-
"`list_workspaces`, `read_binary`, `upload`, `begin_upload`,
|
|
4205
|
-
"`mint_render_token`; and the host tools are plain verbs too (`bash`,
|
|
4206
|
-
"the collision: a bare `read` or `edit` is the HOST tool, acting on the
|
|
4207
|
-
"workspace read is `
|
|
4292
|
+
"Every tool here goes by its plain name: the workspace tools are `query`, `mutate` and",
|
|
4293
|
+
"`describe`; the turn tools are `ask`, `reply_to`, `wake_me`, `send`, `skip_turn`; the",
|
|
4294
|
+
"ancillary tools are `list_workspaces`, `read_binary`, `upload`, `begin_upload`,",
|
|
4295
|
+
"`finalize_upload`, `mint_render_token`; and the host tools are plain verbs too (`bash`,",
|
|
4296
|
+
"`read`, `edit`). Mind the collision: a bare `read` or `edit` is the HOST tool, acting on the",
|
|
4297
|
+
"local machine \u2014 a workspace read is `query` with the operation name `files.read`. The",
|
|
4298
|
+
"collision runs the other way too: `read` is a host tool here AND `files.read` is an",
|
|
4299
|
+
"operation name, so keep the bare verb and the dotted name apart. If a tool ever appears with",
|
|
4208
4300
|
"an `mcp__\u2026__` prefix, that prefix is not part of its name here. One more harness fact: you",
|
|
4209
4301
|
"can interleave narration with tool calls, but only your final message is recorded as the",
|
|
4210
4302
|
"turn's reply \u2014 write your closing answer last."
|
|
@@ -5173,15 +5265,18 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
5173
5265
|
// packages/agent-runtime/src/codex/addendum.ts
|
|
5174
5266
|
var CODEX_ADDENDUM = [
|
|
5175
5267
|
"Codex surfaces MCP tools under qualified names, and on some versions defers them. The",
|
|
5176
|
-
"workspace
|
|
5177
|
-
"
|
|
5268
|
+
"workspace tools are `mcp__cabane__query`, `mcp__cabane__mutate` and",
|
|
5269
|
+
"`mcp__cabane__describe` \u2014 if they aren't in your visible tool list, locate them in",
|
|
5270
|
+
"the deferred-tool inventory and invoke those exact qualified names; never conclude the",
|
|
5271
|
+
"workspace is",
|
|
5178
5272
|
"absent without attempting discovery and invocation, and if a call fails, report the recorded",
|
|
5179
5273
|
"error. The turn tools are `mcp__cabane_companion__ask` / `reply_to` / `wake_me` /",
|
|
5180
5274
|
"`send` / `skip_turn`, and the ancillary tools",
|
|
5181
5275
|
"`mcp__cabane__list_workspaces` /",
|
|
5182
5276
|
"`read_binary` / `upload` / `begin_upload` / `finalize_upload` / `mint_render_token` \u2014 all",
|
|
5183
5277
|
"possibly deferred too. There is no Cabane `read`/`write`/`search` tool \u2014 those are",
|
|
5184
|
-
"
|
|
5278
|
+
"operation NAMES you pass to `query` / `mutate`. One more harness fact: you can interleave",
|
|
5279
|
+
"narration",
|
|
5185
5280
|
"with tool calls, but only your final message is recorded as the turn's reply \u2014 write your",
|
|
5186
5281
|
"closing answer last."
|
|
5187
5282
|
].join(" ");
|
|
@@ -5279,9 +5374,12 @@ function readUsage(ev) {
|
|
|
5279
5374
|
const usage = asRecord2(ev.usage);
|
|
5280
5375
|
if (!usage) return void 0;
|
|
5281
5376
|
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5377
|
+
return {
|
|
5378
|
+
inputTokens: num(usage.input_tokens),
|
|
5379
|
+
outputTokens: num(usage.output_tokens),
|
|
5380
|
+
cacheReadTokens: num(usage.cached_input_tokens),
|
|
5381
|
+
cacheCreationTokens: num(usage.cache_write_input_tokens)
|
|
5382
|
+
};
|
|
5285
5383
|
}
|
|
5286
5384
|
function toStatus(v) {
|
|
5287
5385
|
return v === "completed" || v === "failed" ? v : "in_progress";
|
|
@@ -5843,24 +5941,14 @@ var errorItem = (id, message) => ({
|
|
|
5843
5941
|
type: "item.completed",
|
|
5844
5942
|
item: { id, type: "error", message }
|
|
5845
5943
|
});
|
|
5846
|
-
var
|
|
5847
|
-
var
|
|
5944
|
+
var OP_SUCCESS = { ok: true, result: { source: "native" } };
|
|
5945
|
+
var OP_FAILURE = {
|
|
5848
5946
|
ok: false,
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
warnings: [],
|
|
5855
|
-
committed: {
|
|
5856
|
-
calls: [
|
|
5857
|
-
{
|
|
5858
|
-
method: "conversations.update",
|
|
5859
|
-
args: { id: "conv-1", opts: { title: "Renamed before failure" } }
|
|
5860
|
-
}
|
|
5861
|
-
]
|
|
5862
|
-
},
|
|
5863
|
-
inFlight: null
|
|
5947
|
+
error: {
|
|
5948
|
+
kind: "output_too_large",
|
|
5949
|
+
code: "output_too_large",
|
|
5950
|
+
message: "files.read returned more than the result ceiling"
|
|
5951
|
+
}
|
|
5864
5952
|
};
|
|
5865
5953
|
var sessionEvent3 = (threadId, cwd = DIR2, degraded = false, promptFingerprint = fingerprintPrompt(PROMPT)) => ({
|
|
5866
5954
|
type: "session",
|
|
@@ -5871,6 +5959,11 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
5871
5959
|
{
|
|
5872
5960
|
// A plain text reply: the held agent message seals as the terminal final at
|
|
5873
5961
|
// `turn.completed`; the usage counters ride the `result`.
|
|
5962
|
+
//
|
|
5963
|
+
// CT1283: the counters NEST — `cached_input_tokens` is a slice of
|
|
5964
|
+
// `input_tokens`, `reasoning_output_tokens` a slice of `output_tokens` — so
|
|
5965
|
+
// input is 100 (not 120) and output is 50 (not 60), with the cache read broken
|
|
5966
|
+
// out beside them. This fixture pinned the double-count before CT1283.
|
|
5874
5967
|
name: "clean turn",
|
|
5875
5968
|
request: makeRequest3(),
|
|
5876
5969
|
nativeStream: [
|
|
@@ -5879,6 +5972,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
5879
5972
|
turnCompleted({
|
|
5880
5973
|
input_tokens: 100,
|
|
5881
5974
|
cached_input_tokens: 20,
|
|
5975
|
+
cache_write_input_tokens: 5,
|
|
5882
5976
|
output_tokens: 50,
|
|
5883
5977
|
reasoning_output_tokens: 10
|
|
5884
5978
|
})
|
|
@@ -5886,7 +5980,16 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
5886
5980
|
expected: [
|
|
5887
5981
|
sessionEvent3(NEW_THREAD_ID),
|
|
5888
5982
|
{ type: "text", body: "Hello there.", terminal: true },
|
|
5889
|
-
{
|
|
5983
|
+
{
|
|
5984
|
+
type: "result",
|
|
5985
|
+
ok: true,
|
|
5986
|
+
usage: {
|
|
5987
|
+
inputTokens: 100,
|
|
5988
|
+
outputTokens: 50,
|
|
5989
|
+
cacheReadTokens: 20,
|
|
5990
|
+
cacheCreationTokens: 5
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5890
5993
|
]
|
|
5891
5994
|
},
|
|
5892
5995
|
{
|
|
@@ -5942,30 +6045,30 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
5942
6045
|
]
|
|
5943
6046
|
},
|
|
5944
6047
|
{
|
|
5945
|
-
// CT1211: recorded
|
|
6048
|
+
// CT1211: recorded MCP success shape. Codex exposes the text compatibility
|
|
5946
6049
|
// copy and the native object together; normalization must keep the object.
|
|
5947
6050
|
name: "MCP success prefers native structured content",
|
|
5948
6051
|
request: makeRequest3(),
|
|
5949
6052
|
nativeStream: [
|
|
5950
6053
|
threadStarted(NEW_THREAD_ID),
|
|
5951
6054
|
toolFrame("item.started", {
|
|
5952
|
-
id: "
|
|
6055
|
+
id: "query-success",
|
|
5953
6056
|
type: "mcp_tool_call",
|
|
5954
6057
|
server: "cabane",
|
|
5955
|
-
tool: "
|
|
5956
|
-
arguments: {
|
|
6058
|
+
tool: "query",
|
|
6059
|
+
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
5957
6060
|
status: "in_progress"
|
|
5958
6061
|
}),
|
|
5959
6062
|
toolFrame("item.completed", {
|
|
5960
|
-
id: "
|
|
6063
|
+
id: "query-success",
|
|
5961
6064
|
type: "mcp_tool_call",
|
|
5962
6065
|
server: "cabane",
|
|
5963
|
-
tool: "
|
|
5964
|
-
arguments: {
|
|
6066
|
+
tool: "query",
|
|
6067
|
+
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
5965
6068
|
status: "completed",
|
|
5966
6069
|
result: {
|
|
5967
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
5968
|
-
structured_content:
|
|
6070
|
+
content: [{ type: "text", text: JSON.stringify(OP_SUCCESS) }],
|
|
6071
|
+
structured_content: OP_SUCCESS
|
|
5969
6072
|
}
|
|
5970
6073
|
}),
|
|
5971
6074
|
turnCompleted()
|
|
@@ -5974,20 +6077,20 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
5974
6077
|
sessionEvent3(NEW_THREAD_ID),
|
|
5975
6078
|
{
|
|
5976
6079
|
type: "tool",
|
|
5977
|
-
id: "
|
|
5978
|
-
name: "
|
|
6080
|
+
id: "query-success",
|
|
6081
|
+
name: "query",
|
|
5979
6082
|
phase: "start",
|
|
5980
6083
|
summary: "",
|
|
5981
|
-
input: {
|
|
6084
|
+
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
5982
6085
|
},
|
|
5983
6086
|
{
|
|
5984
6087
|
type: "tool",
|
|
5985
|
-
id: "
|
|
5986
|
-
name: "
|
|
6088
|
+
id: "query-success",
|
|
6089
|
+
name: "query",
|
|
5987
6090
|
phase: "done",
|
|
5988
6091
|
summary: "",
|
|
5989
|
-
input: {
|
|
5990
|
-
result:
|
|
6092
|
+
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6093
|
+
result: OP_SUCCESS
|
|
5991
6094
|
},
|
|
5992
6095
|
{ type: "result", ok: true }
|
|
5993
6096
|
]
|
|
@@ -5995,28 +6098,28 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
5995
6098
|
{
|
|
5996
6099
|
// CT1211 incident shape: MCP `isError` keeps the failed phase, while the raw
|
|
5997
6100
|
// camel-case structured payload survives instead of the generic `error` token.
|
|
5998
|
-
name: "failed
|
|
6101
|
+
name: "failed MCP call preserves structured receipt while remaining error phase",
|
|
5999
6102
|
request: makeRequest3(),
|
|
6000
6103
|
nativeStream: [
|
|
6001
6104
|
threadStarted(NEW_THREAD_ID),
|
|
6002
6105
|
toolFrame("item.started", {
|
|
6003
|
-
id: "
|
|
6106
|
+
id: "query-failed",
|
|
6004
6107
|
type: "mcp_tool_call",
|
|
6005
6108
|
server: "cabane",
|
|
6006
|
-
tool: "
|
|
6007
|
-
arguments: {
|
|
6109
|
+
tool: "query",
|
|
6110
|
+
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6008
6111
|
status: "in_progress"
|
|
6009
6112
|
}),
|
|
6010
6113
|
toolFrame("item.completed", {
|
|
6011
|
-
id: "
|
|
6114
|
+
id: "query-failed",
|
|
6012
6115
|
type: "mcp_tool_call",
|
|
6013
6116
|
server: "cabane",
|
|
6014
|
-
tool: "
|
|
6015
|
-
arguments: {
|
|
6117
|
+
tool: "query",
|
|
6118
|
+
arguments: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6016
6119
|
status: "failed",
|
|
6017
6120
|
result: {
|
|
6018
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
6019
|
-
structuredContent:
|
|
6121
|
+
content: [{ type: "text", text: JSON.stringify(OP_FAILURE) }],
|
|
6122
|
+
structuredContent: OP_FAILURE,
|
|
6020
6123
|
isError: true
|
|
6021
6124
|
},
|
|
6022
6125
|
error: { message: "error" }
|
|
@@ -6027,20 +6130,20 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6027
6130
|
sessionEvent3(NEW_THREAD_ID),
|
|
6028
6131
|
{
|
|
6029
6132
|
type: "tool",
|
|
6030
|
-
id: "
|
|
6031
|
-
name: "
|
|
6133
|
+
id: "query-failed",
|
|
6134
|
+
name: "query",
|
|
6032
6135
|
phase: "start",
|
|
6033
6136
|
summary: "",
|
|
6034
|
-
input: {
|
|
6137
|
+
input: { op: "files.read", args: { path: "/ABOUT.md" } }
|
|
6035
6138
|
},
|
|
6036
6139
|
{
|
|
6037
6140
|
type: "tool",
|
|
6038
|
-
id: "
|
|
6039
|
-
name: "
|
|
6141
|
+
id: "query-failed",
|
|
6142
|
+
name: "query",
|
|
6040
6143
|
phase: "error",
|
|
6041
6144
|
summary: "",
|
|
6042
|
-
input: {
|
|
6043
|
-
result:
|
|
6145
|
+
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6146
|
+
result: OP_FAILURE
|
|
6044
6147
|
},
|
|
6045
6148
|
{ type: "result", ok: true }
|
|
6046
6149
|
]
|
|
@@ -7271,6 +7374,7 @@ var TurnCommitter = class {
|
|
|
7271
7374
|
{ err: err instanceof Error ? err.message : String(err), hook },
|
|
7272
7375
|
"dispatcher: transcript callback failed"
|
|
7273
7376
|
);
|
|
7377
|
+
deps.onCommitFailed?.(err);
|
|
7274
7378
|
};
|
|
7275
7379
|
const commit = {
|
|
7276
7380
|
commitMessage: async ({ body, kind, seq }) => {
|
|
@@ -7335,6 +7439,7 @@ var TurnCommitter = class {
|
|
|
7335
7439
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
7336
7440
|
"dispatcher: progress-promotion commit failed"
|
|
7337
7441
|
);
|
|
7442
|
+
deps.onCommitFailed?.(err);
|
|
7338
7443
|
}
|
|
7339
7444
|
});
|
|
7340
7445
|
this.emit = sinkEmitter(this.pump.sink, this.onError);
|
|
@@ -7494,6 +7599,7 @@ var SILENT_MARKER_BODY = "(no reply)";
|
|
|
7494
7599
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
7495
7600
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7496
7601
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
7602
|
+
var DEFAULT_LEASE_RENEWAL_MS = 3e4;
|
|
7497
7603
|
function checkoutState(cwd) {
|
|
7498
7604
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
7499
7605
|
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
@@ -7526,13 +7632,19 @@ var LEASE_REFUSALS = /* @__PURE__ */ new Set([
|
|
|
7526
7632
|
"turn_already_ended",
|
|
7527
7633
|
"turn_belongs_elsewhere"
|
|
7528
7634
|
]);
|
|
7529
|
-
function
|
|
7635
|
+
function apiErrorCode(err) {
|
|
7530
7636
|
if (!(err instanceof ApiError)) return null;
|
|
7531
7637
|
const body = err.body;
|
|
7532
|
-
|
|
7638
|
+
return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : null;
|
|
7639
|
+
}
|
|
7640
|
+
function leaseRefusal(err) {
|
|
7641
|
+
const code = apiErrorCode(err);
|
|
7533
7642
|
if (code && LEASE_REFUSALS.has(code)) return code;
|
|
7534
7643
|
return null;
|
|
7535
7644
|
}
|
|
7645
|
+
function isWriteFenceRefusal(err) {
|
|
7646
|
+
return err instanceof ApiError && err.status === 409 && apiErrorCode(err) === "not_running";
|
|
7647
|
+
}
|
|
7536
7648
|
var ERROR_BODY_LOG_CAP = 2e3;
|
|
7537
7649
|
function describeErrorBody(body) {
|
|
7538
7650
|
if (body === void 0 || body === null) return void 0;
|
|
@@ -7878,6 +7990,7 @@ ${reason}`,
|
|
|
7878
7990
|
if (pair.size === 0) this.aborts.delete(key);
|
|
7879
7991
|
};
|
|
7880
7992
|
let timeoutReason = null;
|
|
7993
|
+
let leaseLost = false;
|
|
7881
7994
|
try {
|
|
7882
7995
|
await this.opts.api.setActiveRun(workspaceId, payload.conversationId, payload.agentId, {
|
|
7883
7996
|
activeRunStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8025,6 +8138,8 @@ ${reason}`,
|
|
|
8025
8138
|
let latestSessionState = request.session;
|
|
8026
8139
|
let settledDiagnostics = null;
|
|
8027
8140
|
let silentMarkerEmitted = false;
|
|
8141
|
+
let noteCommitFailed = () => {
|
|
8142
|
+
};
|
|
8028
8143
|
const committer = new TurnCommitter({
|
|
8029
8144
|
api: this.opts.api,
|
|
8030
8145
|
workspaceId,
|
|
@@ -8050,7 +8165,9 @@ ${reason}`,
|
|
|
8050
8165
|
replyState,
|
|
8051
8166
|
// The ledger-derived owed reply, for the runtime's own declaration when
|
|
8052
8167
|
// the agent doesn't call `reply_to` (resolveDeclaredReplyField).
|
|
8053
|
-
owedReplyMessageId: turnContext.owedReplyMessageId ?? null
|
|
8168
|
+
owedReplyMessageId: turnContext.owedReplyMessageId ?? null,
|
|
8169
|
+
// CT1292: a commit that didn't land may mean this turn's lease is gone.
|
|
8170
|
+
onCommitFailed: (err) => noteCommitFailed(err)
|
|
8054
8171
|
});
|
|
8055
8172
|
const usesHttpTurnControl = turnRuntime === "codex" || turnRuntime === "opencode";
|
|
8056
8173
|
let turnControlIntentFetched = false;
|
|
@@ -8130,6 +8247,35 @@ ${reason}`,
|
|
|
8130
8247
|
const totalTimer = setTimeout(() => fireTimeout("timeout_total"), totalTimeoutMs);
|
|
8131
8248
|
totalTimer.unref?.();
|
|
8132
8249
|
armIdle();
|
|
8250
|
+
const leaseRenewalMs = this.opts.leaseRenewalMs ?? DEFAULT_LEASE_RENEWAL_MS;
|
|
8251
|
+
let leaseCheckInFlight = false;
|
|
8252
|
+
const checkLease = async (trigger) => {
|
|
8253
|
+
if (leaseLost || leaseCheckInFlight || abortController.signal.aborted) return;
|
|
8254
|
+
leaseCheckInFlight = true;
|
|
8255
|
+
try {
|
|
8256
|
+
const state = await this.opts.api.checkTurnLease(
|
|
8257
|
+
workspaceId,
|
|
8258
|
+
payload.conversationId,
|
|
8259
|
+
payload.agentId,
|
|
8260
|
+
turnId
|
|
8261
|
+
);
|
|
8262
|
+
if (state !== "ended") return;
|
|
8263
|
+
if (abortController.signal.aborted) return;
|
|
8264
|
+
leaseLost = true;
|
|
8265
|
+
turnLog.warn(
|
|
8266
|
+
{ turnId, trigger },
|
|
8267
|
+
"dispatcher: this turn is no longer running server-side \u2014 aborting the loop"
|
|
8268
|
+
);
|
|
8269
|
+
abortController.abort();
|
|
8270
|
+
} finally {
|
|
8271
|
+
leaseCheckInFlight = false;
|
|
8272
|
+
}
|
|
8273
|
+
};
|
|
8274
|
+
const leaseTimer = setInterval(() => void checkLease("cadence"), leaseRenewalMs);
|
|
8275
|
+
leaseTimer.unref?.();
|
|
8276
|
+
noteCommitFailed = (err) => {
|
|
8277
|
+
if (isWriteFenceRefusal(err)) void checkLease("commit_refused");
|
|
8278
|
+
};
|
|
8133
8279
|
try {
|
|
8134
8280
|
for await (const event of adapter.runTurn(request, abortController.signal)) {
|
|
8135
8281
|
transcript?.write(event);
|
|
@@ -8194,7 +8340,7 @@ ${reason}`,
|
|
|
8194
8340
|
}
|
|
8195
8341
|
if (abortController.signal.aborted) {
|
|
8196
8342
|
okResult = false;
|
|
8197
|
-
resultReason = timeoutReason ?? "cancelled";
|
|
8343
|
+
resultReason = leaseLost ? "lease_lost" : timeoutReason ?? "cancelled";
|
|
8198
8344
|
}
|
|
8199
8345
|
const emptyResultReason = !skipState.skipped && classifyEmptyResult({ ok: okResult, contentBearingEvents, usage: turnUsage });
|
|
8200
8346
|
if (emptyResultReason) {
|
|
@@ -8259,7 +8405,12 @@ ${reason}`,
|
|
|
8259
8405
|
} finally {
|
|
8260
8406
|
if (idleTimer) clearTimeout(idleTimer);
|
|
8261
8407
|
clearTimeout(totalTimer);
|
|
8262
|
-
|
|
8408
|
+
clearInterval(leaseTimer);
|
|
8409
|
+
if (leaseLost) {
|
|
8410
|
+
resultReason = "lease_lost";
|
|
8411
|
+
okResult = false;
|
|
8412
|
+
}
|
|
8413
|
+
const userCancelled = abortController.signal.aborted && timeoutReason === null && !leaseLost;
|
|
8263
8414
|
if (timeoutReason !== null) {
|
|
8264
8415
|
resultReason = timeoutReason;
|
|
8265
8416
|
okResult = false;
|
|
@@ -8301,12 +8452,13 @@ ${reason}`,
|
|
|
8301
8452
|
if (turnResolvedConfig && Object.keys(turnResolvedConfig).length > 0) {
|
|
8302
8453
|
body.resolvedConfig = turnResolvedConfig;
|
|
8303
8454
|
}
|
|
8304
|
-
if (!okResult && resultReason && resultReason !== "cancelled" && !userCancelled && !skipState.skipped) {
|
|
8455
|
+
if (!okResult && resultReason && resultReason !== "cancelled" && !userCancelled && !leaseLost && !skipState.skipped) {
|
|
8305
8456
|
body.errorReason = resultReason.slice(0, 200);
|
|
8306
8457
|
}
|
|
8307
8458
|
if (okResult) {
|
|
8308
8459
|
body.lastSeenMessageId = payload.messageId;
|
|
8309
8460
|
}
|
|
8461
|
+
body.outcome = okResult ? "settled" : body.errorReason ? "failed" : "interrupted";
|
|
8310
8462
|
if (sessionDegraded) {
|
|
8311
8463
|
body.degraded = true;
|
|
8312
8464
|
}
|
|
@@ -8314,8 +8466,8 @@ ${reason}`,
|
|
|
8314
8466
|
body.sessionWriteRejected = true;
|
|
8315
8467
|
this.sessionWriteNotified.add(key);
|
|
8316
8468
|
}
|
|
8317
|
-
const outcome = skipState.skipped ? "skipped" : userCancelled ? "cancelled" : okResult ? "success" : "failure";
|
|
8318
|
-
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
8469
|
+
const outcome = skipState.skipped ? "skipped" : userCancelled || leaseLost ? "cancelled" : okResult ? "success" : "failure";
|
|
8470
|
+
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? leaseLost ? { kind: "lease_lost" } : { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
8319
8471
|
settledDiagnostics = {
|
|
8320
8472
|
outcome,
|
|
8321
8473
|
resultReason: diagnosticReason,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.71",
|
|
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",
|