@cabane/companion 0.6.93 → 0.6.94
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 +237 -339
- package/dist/runtime.js +237 -339
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -2125,8 +2125,12 @@ var CabaneApi = class {
|
|
|
2125
2125
|
// it), so it is passed down rather than inferred here. It gates the COST of
|
|
2126
2126
|
// the aggregate, never the tenancy scoping, which the route applies
|
|
2127
2127
|
// unconditionally.
|
|
2128
|
-
|
|
2129
|
-
|
|
2128
|
+
// `fresh` asks for the context a session that holds NOTHING needs: the read
|
|
2129
|
+
// from the start of the conversation, the agent's own words included, and
|
|
2130
|
+
// the first-turn material — what the adapter runs on when it refused to
|
|
2131
|
+
// resume the stored session and is starting a fresh one.
|
|
2132
|
+
getTurnContext(conversationId, messageId2, turnId, resumed, fresh) {
|
|
2133
|
+
const q = `conversationId=${encodeURIComponent(conversationId)}&messageId=${encodeURIComponent(messageId2)}` + (turnId ? `&turnId=${encodeURIComponent(turnId)}` : "") + (resumed ? "&resumed=1" : "") + (fresh ? "&fresh=1" : "");
|
|
2130
2134
|
return this.request("GET", `/api/agent/turn-context?${q}`);
|
|
2131
2135
|
}
|
|
2132
2136
|
// CT1380: the local half of a resumed turn's seq floor (see
|
|
@@ -2135,6 +2139,18 @@ var CabaneApi = class {
|
|
|
2135
2139
|
outboxMaxSeqForTurn(turnId) {
|
|
2136
2140
|
return this.opts.outbox?.maxSeqForTurn(turnId) ?? 0;
|
|
2137
2141
|
}
|
|
2142
|
+
// Whether this turn's speech is still waiting in the outbox — the half of
|
|
2143
|
+
// "did the span speak" the server cannot see: a first message absorbed by a
|
|
2144
|
+
// transient failure before the crash is on disk here, spent and undelivered,
|
|
2145
|
+
// and the resumed run must not commit its next note as first words again.
|
|
2146
|
+
// A runtime notice queued under the turn is not speech.
|
|
2147
|
+
outboxHasSpeechForTurn(turnId) {
|
|
2148
|
+
const outbox = this.opts.outbox;
|
|
2149
|
+
if (!outbox) return false;
|
|
2150
|
+
return outbox.list().some(
|
|
2151
|
+
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2138
2154
|
// CT714: read a turn's recorded turn-control intent. An EXTERNAL adapter
|
|
2139
2155
|
// (Codex / opencode) records `reply_to` / `skip_turn` into `turn_intents`
|
|
2140
2156
|
// server-side (the URL MCP surface) rather than the dispatcher's in-memory
|
|
@@ -2260,6 +2276,23 @@ var CabaneApi = class {
|
|
|
2260
2276
|
async durableActiveRunWrite(path, conversationId, agentId, body) {
|
|
2261
2277
|
const outbox = this.opts.outbox;
|
|
2262
2278
|
const key = activeRunOutboxKey(conversationId, agentId);
|
|
2279
|
+
const turnId = body?.turnId;
|
|
2280
|
+
if (outbox && typeof turnId === "string" && outbox.maxSeqForTurn(turnId) > 0) {
|
|
2281
|
+
outbox.persist({
|
|
2282
|
+
enqueuedAt: Date.now(),
|
|
2283
|
+
turnId: key,
|
|
2284
|
+
seq: ACTIVE_RUN_OUTBOX_SEQ,
|
|
2285
|
+
method: "PATCH",
|
|
2286
|
+
path,
|
|
2287
|
+
body,
|
|
2288
|
+
kind: "active-run"
|
|
2289
|
+
});
|
|
2290
|
+
this.opts.log?.warn(
|
|
2291
|
+
{ conversationId, agentId, turnId },
|
|
2292
|
+
"companion: settle queued behind this turn's undelivered commits (drains in order)"
|
|
2293
|
+
);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2263
2296
|
try {
|
|
2264
2297
|
await this.request("PATCH", path, body, { retry: true });
|
|
2265
2298
|
outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
@@ -2301,16 +2334,11 @@ var CabaneApi = class {
|
|
|
2301
2334
|
// (transport errors + 5xx), never a 4xx, so no `(turnId, kind)` row is
|
|
2302
2335
|
// double-posted. The caller threads its turn abort signal so a cancel
|
|
2303
2336
|
// mid-commit aborts the in-flight POST rather than letting it land.
|
|
2304
|
-
//
|
|
2305
|
-
//
|
|
2306
|
-
//
|
|
2307
|
-
// companion no longer WRITES that kind — the server does, in the transaction
|
|
2308
|
-
// that terminalizes the stopped turn — so the value survives here only as
|
|
2309
|
-
// wire vocabulary the server also speaks. `seq` is the companion's per-turn
|
|
2337
|
+
// Workzone: a commit is a message — the agent's first or last words — or the
|
|
2338
|
+
// runtime's own line (`runtime_notice`). The words between tool calls go
|
|
2339
|
+
// through `reportText`, the trail's door. `seq` is the companion's per-turn
|
|
2310
2340
|
// monotonic counter, stamped on the row so the merged timeline orders the
|
|
2311
|
-
// commit deterministically against the persisted tool/thinking rows.
|
|
2312
|
-
// fields are optional on the wire — an older companion that didn't mint seq
|
|
2313
|
-
// still validates (the server defaults to 0).
|
|
2341
|
+
// commit deterministically against the persisted tool/thinking rows.
|
|
2314
2342
|
postTurnMessage(workspaceId, conversationId, body, signal) {
|
|
2315
2343
|
return this.durableCommit(
|
|
2316
2344
|
"message",
|
|
@@ -2321,35 +2349,6 @@ var CabaneApi = class {
|
|
|
2321
2349
|
signal
|
|
2322
2350
|
);
|
|
2323
2351
|
}
|
|
2324
|
-
// CT1357: post ONE ordinary addressed message — not turn speech, no `turnId`
|
|
2325
|
-
// — so `dispatch` is honoured and the target agent is woken. The turn-commit
|
|
2326
|
-
// fork ignores `dispatch` (an act rides its own call now), which leaves this
|
|
2327
|
-
// as the only way a companion can address a peer OUTSIDE a running turn.
|
|
2328
|
-
//
|
|
2329
|
-
// The prepare-failure handback is exactly that case and the reason this
|
|
2330
|
-
// exists. It fires before the turn is ever admitted — the active-run PATCH
|
|
2331
|
-
// that admits it comes after the prepare hook — so `turnAct` is not available:
|
|
2332
|
-
// its route resolves the turn first and 404s when there is none. The failure
|
|
2333
|
-
// notice's own commit is what admits the turn, and by then it has also settled
|
|
2334
|
-
// it. An ordinary addressed post depends on no lease at all, which is what
|
|
2335
|
-
// makes it the right shape here.
|
|
2336
|
-
//
|
|
2337
|
-
// Retried like any durable write, and therefore REQUIRING `idempotencyKey`:
|
|
2338
|
-
// this is not outbox-queued (there is no `(turnId, seq)` for a non-turn row),
|
|
2339
|
-
// so without a key a committed request whose response was lost — or a
|
|
2340
|
-
// redelivered dispatch event — writes a second row and wakes the peer twice.
|
|
2341
|
-
// The server dedupes on `(conversation_id, idempotency_key)`: the repeat
|
|
2342
|
-
// returns the existing row and raises no second dispatch (CT239). Required,
|
|
2343
|
-
// not optional, so a future caller cannot omit it and silently lose the
|
|
2344
|
-
// guarantee.
|
|
2345
|
-
postAddressedMessage(workspaceId, conversationId, body, signal) {
|
|
2346
|
-
return this.request(
|
|
2347
|
-
"POST",
|
|
2348
|
-
`/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`,
|
|
2349
|
-
body,
|
|
2350
|
-
signal ? { retry: true, signal } : { retry: true }
|
|
2351
|
-
);
|
|
2352
|
-
}
|
|
2353
2352
|
// SJ477: report one tool-activity transition (start / done / error) for the
|
|
2354
2353
|
// live activity cards. Transient — the server publishes an `agent_activity`
|
|
2355
2354
|
// SSE and writes no row. Agent-PAT authed; the URL `:agentId` must match the
|
|
@@ -2379,6 +2378,20 @@ var CabaneApi = class {
|
|
|
2379
2378
|
signal
|
|
2380
2379
|
);
|
|
2381
2380
|
}
|
|
2381
|
+
// Workzone: report one text the agent wrote between tool calls — the trail's
|
|
2382
|
+
// door. Durable like a thinking row (same outbox, same `(turnId, seq)`
|
|
2383
|
+
// replay), and never a message: the server writes it to `message_activity`
|
|
2384
|
+
// under the span and nothing lands, stamps or wakes on it.
|
|
2385
|
+
reportText(workspaceId, conversationId, agentId, body, signal) {
|
|
2386
|
+
return this.durableCommit(
|
|
2387
|
+
"text",
|
|
2388
|
+
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/text`,
|
|
2389
|
+
body,
|
|
2390
|
+
body.turnId,
|
|
2391
|
+
body.seq,
|
|
2392
|
+
signal
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2382
2395
|
// SJ493: fetch the agent's self-view — identity + operating context. The
|
|
2383
2396
|
// companion calls this on each dispatch to get its `systemPrompt` (composed
|
|
2384
2397
|
// server-side from the bundled default + the agent's charter) rather than
|
|
@@ -3511,20 +3524,24 @@ var THINKING_TEXT_MAX_CHARS = 3900;
|
|
|
3511
3524
|
var TurnPump = class {
|
|
3512
3525
|
constructor(opts) {
|
|
3513
3526
|
this.opts = opts;
|
|
3527
|
+
this.hasSpoken = opts.spoke ?? false;
|
|
3514
3528
|
this.sink = {
|
|
3515
3529
|
onAssistantText: async (evt) => {
|
|
3516
3530
|
if (opts.signal.aborted) return;
|
|
3517
3531
|
const body = normalizeCommitText(evt.text);
|
|
3518
3532
|
if (!body) return;
|
|
3519
|
-
const kind = evt.final ? "final" : "progress";
|
|
3520
3533
|
const seq = opts.nextSeq();
|
|
3521
|
-
|
|
3534
|
+
if (!evt.final && this.hasSpoken) {
|
|
3535
|
+
await opts.commit.reportText({ text: body, seq });
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
await opts.commit.commitMessage({ body, kind: "message", seq });
|
|
3539
|
+
this.hasSpoken = true;
|
|
3522
3540
|
if (evt.final) {
|
|
3523
3541
|
this.emittedFinal = true;
|
|
3542
|
+
this.lastSeq = seq;
|
|
3524
3543
|
this.finalReplyBody = body;
|
|
3525
3544
|
this.emittedFinalSource = "runtime_text";
|
|
3526
|
-
} else {
|
|
3527
|
-
this.lastProgressBody = body;
|
|
3528
3545
|
}
|
|
3529
3546
|
},
|
|
3530
3547
|
onToolActivity: async (evt) => {
|
|
@@ -3561,11 +3578,12 @@ var TurnPump = class {
|
|
|
3561
3578
|
};
|
|
3562
3579
|
}
|
|
3563
3580
|
opts;
|
|
3564
|
-
//
|
|
3565
|
-
//
|
|
3566
|
-
|
|
3581
|
+
// A message has landed under this span — its first words. Every later
|
|
3582
|
+
// non-terminal text is the trail. Seeded from the host on a resume.
|
|
3583
|
+
hasSpoken;
|
|
3584
|
+
// The last words landed, and their `seq` — what the settle declares.
|
|
3567
3585
|
emittedFinal = false;
|
|
3568
|
-
|
|
3586
|
+
lastSeq = null;
|
|
3569
3587
|
// The turn's final reply text, captured for the dashboard feed / logging.
|
|
3570
3588
|
finalReplyBody = null;
|
|
3571
3589
|
emittedFinalSource = "none";
|
|
@@ -3576,31 +3594,20 @@ var TurnPump = class {
|
|
|
3576
3594
|
// frozen committer test — drive. Each callback applies the choreography and
|
|
3577
3595
|
// commits through the injected sink.
|
|
3578
3596
|
sink;
|
|
3579
|
-
//
|
|
3580
|
-
//
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
// for any reason. The host closes that wordless turn with a marker instead.
|
|
3584
|
-
// Skipped when cancelled or already final. The held-text flush that precedes
|
|
3585
|
-
// this is a classification concern, driven by the caller before this runs.
|
|
3586
|
-
async finalize(ok) {
|
|
3587
|
-
if (!ok || this.opts.signal.aborted || this.emittedFinal || !this.lastProgressBody) return;
|
|
3588
|
-
const body = this.lastProgressBody;
|
|
3589
|
-
const seq = this.opts.nextSeq();
|
|
3590
|
-
try {
|
|
3591
|
-
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
3592
|
-
this.emittedFinal = true;
|
|
3593
|
-
this.finalReplyBody = body;
|
|
3594
|
-
this.emittedFinalSource = "progress_promotion";
|
|
3595
|
-
} catch (err) {
|
|
3596
|
-
this.opts.onError?.(err, "progress-promotion");
|
|
3597
|
-
}
|
|
3597
|
+
// Whether a message has landed under this span — first words, at least. A
|
|
3598
|
+
// span that spoke settles; one that never did is a pass.
|
|
3599
|
+
get spoke() {
|
|
3600
|
+
return this.hasSpoken;
|
|
3598
3601
|
}
|
|
3599
|
-
// Whether the turn has committed its
|
|
3600
|
-
// whether a marker is owed.
|
|
3602
|
+
// Whether the turn has committed its last words.
|
|
3601
3603
|
get finalEmitted() {
|
|
3602
3604
|
return this.emittedFinal;
|
|
3603
3605
|
}
|
|
3606
|
+
// The `seq` of the last words, for the settle to declare — or null when the
|
|
3607
|
+
// span ended without any. Nothing is put in their place.
|
|
3608
|
+
get lastMessageSeq() {
|
|
3609
|
+
return this.lastSeq;
|
|
3610
|
+
}
|
|
3604
3611
|
// The turn's final reply text, or null if none landed (failure / cancel /
|
|
3605
3612
|
// empty). Used for the dashboard feed's reply preview.
|
|
3606
3613
|
get replyBody() {
|
|
@@ -4069,7 +4076,11 @@ function sdkEvents(queryFn, req, signal, built, degraded) {
|
|
|
4069
4076
|
degraded
|
|
4070
4077
|
});
|
|
4071
4078
|
}
|
|
4072
|
-
async function
|
|
4079
|
+
async function freshRequest(req, reason, hooks) {
|
|
4080
|
+
const recomposed = hooks?.fresh ? await hooks.fresh(reason) : null;
|
|
4081
|
+
return recomposed ? { req: { ...req, ...recomposed, session: null }, degraded: false } : { req: { ...req, session: null }, degraded: true };
|
|
4082
|
+
}
|
|
4083
|
+
async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOptions, onWarn, degraded, hooks) {
|
|
4073
4084
|
let emitted = false;
|
|
4074
4085
|
for await (const event of sdkEvents(queryFn, req, signal, initial, degraded)) {
|
|
4075
4086
|
const recoverResume = initial.resume !== null && !emitted && event.type === "result" && !event.ok && event.reason === "session_start_failed" && event.mcpInventory?.initReceived === false;
|
|
@@ -4083,9 +4094,10 @@ async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOpti
|
|
|
4083
4094
|
discardedSessionId: initial.resume
|
|
4084
4095
|
}
|
|
4085
4096
|
);
|
|
4086
|
-
const
|
|
4087
|
-
|
|
4088
|
-
|
|
4097
|
+
const fresh = await freshRequest(req, "session_start_failed", hooks);
|
|
4098
|
+
if (signal.aborted) return;
|
|
4099
|
+
const options = buildClaudeCodeOptions(fresh.req, augmentOptions);
|
|
4100
|
+
yield* sdkEvents(queryFn, fresh.req, signal, options, fresh.degraded);
|
|
4089
4101
|
return;
|
|
4090
4102
|
}
|
|
4091
4103
|
emitted = true;
|
|
@@ -4098,23 +4110,32 @@ function createClaudeCodeAdapter(deps = {}) {
|
|
|
4098
4110
|
name: "claude-code",
|
|
4099
4111
|
promptAddendum: () => CLAUDE_CODE_ADDENDUM,
|
|
4100
4112
|
dialectSchema: claudeCodeDialectSchema,
|
|
4101
|
-
runTurn(
|
|
4102
|
-
|
|
4103
|
-
|
|
4113
|
+
async *runTurn(request, signal, hooks) {
|
|
4114
|
+
let req = request;
|
|
4115
|
+
let built = buildClaudeCodeOptions(req, deps.augmentOptions);
|
|
4116
|
+
if (built.freshReason === "cwd_mismatch") {
|
|
4104
4117
|
deps.onWarn?.(
|
|
4105
4118
|
"claude-code adapter: stored session cwd no longer matches the current environment \u2014 starting a fresh session (SJ527 guard)",
|
|
4106
4119
|
{ cwd: req.local.cwd }
|
|
4107
4120
|
);
|
|
4108
4121
|
}
|
|
4109
|
-
|
|
4110
|
-
|
|
4122
|
+
let degraded = false;
|
|
4123
|
+
if (built.freshReason !== void 0 && built.freshReason !== "no_session") {
|
|
4124
|
+
const fresh = await freshRequest(req, built.freshReason, hooks);
|
|
4125
|
+
if (signal.aborted) return;
|
|
4126
|
+
req = fresh.req;
|
|
4127
|
+
degraded = fresh.degraded;
|
|
4128
|
+
built = buildClaudeCodeOptions(req, deps.augmentOptions);
|
|
4129
|
+
}
|
|
4130
|
+
yield* runWithResumeRecovery(
|
|
4111
4131
|
queryFn,
|
|
4112
4132
|
req,
|
|
4113
4133
|
signal,
|
|
4114
|
-
|
|
4134
|
+
built,
|
|
4115
4135
|
deps.augmentOptions,
|
|
4116
4136
|
deps.onWarn,
|
|
4117
|
-
degraded
|
|
4137
|
+
degraded,
|
|
4138
|
+
hooks
|
|
4118
4139
|
);
|
|
4119
4140
|
}
|
|
4120
4141
|
};
|
|
@@ -5276,7 +5297,8 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
5276
5297
|
// argument went with the named tool surface it selected against.
|
|
5277
5298
|
promptAddendum: () => OPENCODE_ADDENDUM,
|
|
5278
5299
|
dialectSchema: opencodeDialectSchema,
|
|
5279
|
-
async *runTurn(
|
|
5300
|
+
async *runTurn(request, signal, hooks) {
|
|
5301
|
+
let req = request;
|
|
5280
5302
|
if (!transport) {
|
|
5281
5303
|
yield {
|
|
5282
5304
|
type: "result",
|
|
@@ -5294,7 +5316,15 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
5294
5316
|
{ directory }
|
|
5295
5317
|
);
|
|
5296
5318
|
}
|
|
5297
|
-
|
|
5319
|
+
let degraded = "fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session";
|
|
5320
|
+
if ("fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session" && hooks?.fresh) {
|
|
5321
|
+
const recomposed = await hooks.fresh(decision.reason);
|
|
5322
|
+
if (signal.aborted) return;
|
|
5323
|
+
if (recomposed) {
|
|
5324
|
+
req = { ...req, ...recomposed, session: null };
|
|
5325
|
+
degraded = false;
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5298
5328
|
const spec = buildRunSpec(req, resume);
|
|
5299
5329
|
const result = await transport.run(spec, signal);
|
|
5300
5330
|
if (signal.aborted) return;
|
|
@@ -6301,7 +6331,8 @@ function createCodexAdapter(deps = {}) {
|
|
|
6301
6331
|
// argument went with the named tool surface it selected against.
|
|
6302
6332
|
promptAddendum: () => CODEX_ADDENDUM,
|
|
6303
6333
|
dialectSchema: codexDialectSchema,
|
|
6304
|
-
async *runTurn(
|
|
6334
|
+
async *runTurn(request, signal, hooks) {
|
|
6335
|
+
let req = request;
|
|
6305
6336
|
if (!transport) {
|
|
6306
6337
|
yield {
|
|
6307
6338
|
type: "result",
|
|
@@ -6320,7 +6351,15 @@ function createCodexAdapter(deps = {}) {
|
|
|
6320
6351
|
{ directory }
|
|
6321
6352
|
);
|
|
6322
6353
|
}
|
|
6323
|
-
|
|
6354
|
+
let degraded = "fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session";
|
|
6355
|
+
if ("fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session" && hooks?.fresh) {
|
|
6356
|
+
const recomposed = await hooks.fresh(decision.reason);
|
|
6357
|
+
if (signal.aborted) return;
|
|
6358
|
+
if (recomposed) {
|
|
6359
|
+
req = { ...req, ...recomposed, session: null };
|
|
6360
|
+
degraded = false;
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6324
6363
|
let instructions = null;
|
|
6325
6364
|
if (!req.policy.hostFs) {
|
|
6326
6365
|
if (!deps.writeInstructionsFile) {
|
|
@@ -7362,14 +7401,11 @@ var MIN_ASK_OPTIONS = 2;
|
|
|
7362
7401
|
var MAX_ASK_OPTIONS = 4;
|
|
7363
7402
|
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
7364
7403
|
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
7365
|
-
var MAX_SEND_MESSAGE_BYTES = 65536;
|
|
7366
7404
|
var MAX_SKIP_REASON_BYTES = 500;
|
|
7367
7405
|
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
7368
|
-
ask: `Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass \`targetUserId\` (a workspace member's user id \u2014 every person's id is on the roster in your turn context). Two forms: a SINGLE question \u2014 a \`headline\` (the actual question as one clear, capitalized sentence ending in \`?\`, "Do we go to prod?") plus a short \`question\` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a \`questions\` array of 1\u2013${MAX_ASK_ITEMS} items, each \`{ headline, body?, options? }\`. Provide EITHER \`question\` (single) or \`questions\` (array), never both. **Several decisions landing together are ONE call with \`questions\`** \u2014 never cram the extras into prose, drop all but one, or fire an ask each: a second \`ask\` is a second inbox item competing with the first, never an edit to it. Each question keeps the same form: a one-sentence \`headline\`, a short \`body\` frame (NOT a report \u2014 your status, links and detail go in your REPLY; inline markdown only, no lists or headings), and ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} \`options\` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. The ask is a first-class attention item aimed at that person; your final reply carries the CONTEXT, the ask carries the QUESTION(S). It is written and delivered THE MOMENT you call this \u2014 the person sees it in their inbox while your turn is still running \u2014 so call it once you know the question. The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. Ask only when blocked \u2014 never ceremonially. \`replaces\` withdraws a stale ask of yours (or anyone's) in this conversation in the same write, with the reason shown to the person. Targets a human only; to
|
|
7406
|
+
ask: `Ask a HUMAN a structured question (or a short LIST of them) you need answered to continue, then END your turn \u2014 don't wait for the reply. Use it when you genuinely can't proceed without a person's input (a decision only they can make, a missing fact). Pass \`targetUserId\` (a workspace member's user id \u2014 every person's id is on the roster in your turn context). Two forms: a SINGLE question \u2014 a \`headline\` (the actual question as one clear, capitalized sentence ending in \`?\`, "Do we go to prod?") plus a short \`question\` body for the framing the headline can't hold \u2014 OR, when a plan ends with SEVERAL bounded decisions at once, a \`questions\` array of 1\u2013${MAX_ASK_ITEMS} items, each \`{ headline, body?, options? }\`. Provide EITHER \`question\` (single) or \`questions\` (array), never both. **Several decisions landing together are ONE call with \`questions\`** \u2014 never cram the extras into prose, drop all but one, or fire an ask each: a second \`ask\` is a second inbox item competing with the first, never an edit to it. Each question keeps the same form: a one-sentence \`headline\`, a short \`body\` frame (NOT a report \u2014 your status, links and detail go in your REPLY; inline markdown only, no lists or headings), and ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} \`options\` when the answer is a bounded choice \u2014 for a yes/no go-ahead always pass them, so it's one click, not a typed reply. The ask is a first-class attention item aimed at that person; your final reply carries the CONTEXT, the ask carries the QUESTION(S). It is written and delivered THE MOMENT you call this \u2014 the person sees it in their inbox while your turn is still running \u2014 so call it once you know the question. The human sends ONE response: supplied answers are recorded, omissions become terminally Unanswered. Ask only when blocked \u2014 never ceremonially. \`replaces\` withdraws a stale ask of yours (or anyone's) in this conversation in the same write, with the reason shown to the person. Targets a human only; to bring a peer AGENT in, name them by \`@handle\` in your reply.`,
|
|
7369
7407
|
cancel_wake: `Stand down your own wake \u2014 the inverse of \`wake_me\`. No arguments: it cancels the wake you have armed in this conversation, NOW, when you call it. Reach for it when the thing you armed a wake to check has already happened, or the work it was watching is over \u2014 an armed wake you no longer need fires into a turn with nothing to do, and a supervision loop with no off switch is one you can only end by leaving it running. Whether you have one armed is printed in your turn context ("Wake armed: \u2026"); calling this with nothing armed is a clean no-op, no error. Each call acts as it arrives: \`cancel_wake\` then \`wake_me\` leaves the NEW wake armed; \`wake_me\` then \`cancel_wake\` leaves nothing armed. It reaches only your own wake in this conversation \u2014 never a peer's, and never a recurring schedule (those are \`schedules.*\`).`,
|
|
7370
|
-
|
|
7371
|
-
send: `Send one addressed message to another agent in THIS conversation. Pass the peer's \`agentId\` and the complete \`message\` they should act on. The message is WRITTEN the moment you call this \u2014 durable, in the conversation's queue, in the order you called \u2014 but it is DELIVERED only after your own turn ends: the peer is not running yet and cannot answer you inside this turn. Each call is one message; two calls are two messages. Writing \`@handle\` in prose dispatches nobody. An unknown, deactivated, or self target is refused here, in the result. A handoff to a DIFFERENT conversation is \`conversations.create\` / \`conversations.post\` with their \`dispatch\` field instead. Never use send to hand an answer back to the requester \u2014 declare that with reply_to.`,
|
|
7372
|
-
skip_turn: `End your current turn WITHOUT posting a reply. Call this when you've been dispatched but the message genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or a pile-on where someone else has it. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
|
|
7408
|
+
skip_turn: `End your current turn WITHOUT posting a reply. Call this when you've been woken to read a message that genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or something another participant clearly owns. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
|
|
7373
7409
|
wake_me: `Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for "wait until X": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an \`ask\`. Pass EXACTLY ONE of \`afterSeconds\` (a relative delay \u2014 \`300\` for five minutes) or \`at\` (an absolute ISO-8601 timestamp WITH a zone, e.g. \`2026-07-16T09:00:00-07:00\` \u2014 YOU compute it; the system never parses natural-language time). \`note\` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check ("check whether CT441 merged yet"). The wake arms NOW, when you call this, and the delay counts from this call; a later \`wake_me\` in the same turn replaces it, and \`cancel_wake\` stands it down. A wake that comes due while your turn is still running waits and is delivered after your reply. This is the ONLY sanctioned way to schedule your own continuation; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days \u2014 a wake that can't be armed returns an error here, in this call, so you can recompute; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an \`ask\` to the human instead.`,
|
|
7374
7410
|
withdraw_ask: `Withdraw an open ask \u2014 yours or anyone's in this conversation \u2014 because the question is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't your ask, to the agent who asked it. If there is a current question to put in its place, don't call this \u2014 call \`ask\` with \`replaces\` so the two land as one and the person never sees neither or both. Never withdraw to hurry an answer; to act on a deadline, arm a \`wake_me\` and decide then.`
|
|
7375
7411
|
};
|
|
@@ -7388,13 +7424,6 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
7388
7424
|
"replaces.reason": `Why the old question is no longer the question \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
7389
7425
|
},
|
|
7390
7426
|
cancel_wake: {},
|
|
7391
|
-
reply_to: {
|
|
7392
|
-
messageId: "The owed ask message id from this turn context."
|
|
7393
|
-
},
|
|
7394
|
-
send: {
|
|
7395
|
-
agentId: "The peer agent to address \u2014 a workspace agent id, from your turn context's roster.",
|
|
7396
|
-
message: `The complete new request the peer should receive and act on. Up to ${MAX_SEND_MESSAGE_BYTES} characters.`
|
|
7397
|
-
},
|
|
7398
7427
|
skip_turn: {
|
|
7399
7428
|
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
7400
7429
|
},
|
|
@@ -7412,34 +7441,20 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
7412
7441
|
// src/turn-control-tools.ts
|
|
7413
7442
|
import { z as z13 } from "zod";
|
|
7414
7443
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
7415
|
-
var SEND_TOOL = "send";
|
|
7416
|
-
var SEND_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SEND_TOOL}`;
|
|
7417
7444
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
7418
7445
|
var SKIP_TURN_TOOL = "skip_turn";
|
|
7419
7446
|
var ASK_TOOL = "ask";
|
|
7420
7447
|
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
7421
7448
|
var WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
7422
7449
|
var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
7423
|
-
var REPLY_TO_TOOL = "reply_to";
|
|
7424
|
-
var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
|
|
7425
7450
|
var WAKE_ME_TOOL = "wake_me";
|
|
7426
7451
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
7427
7452
|
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
7428
7453
|
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
7429
7454
|
var TURN_ACT_TIMEOUT_MS = 45e3;
|
|
7430
|
-
function createReplyState() {
|
|
7431
|
-
return { answersMessageId: null };
|
|
7432
|
-
}
|
|
7433
7455
|
function createSkipState() {
|
|
7434
7456
|
return { skipped: false, reason: null };
|
|
7435
7457
|
}
|
|
7436
|
-
function resolveDeclaredReplyField(input) {
|
|
7437
|
-
const explicit = input.replyState.answersMessageId;
|
|
7438
|
-
if (explicit) return { answersMessageId: explicit };
|
|
7439
|
-
if (input.kind !== "final") return {};
|
|
7440
|
-
if (!input.owedReplyMessageId) return {};
|
|
7441
|
-
return { answersMessageId: input.owedReplyMessageId, answersAutoDeclared: true };
|
|
7442
|
-
}
|
|
7443
7458
|
function toolError(text) {
|
|
7444
7459
|
return { isError: true, content: [{ type: "text", text }] };
|
|
7445
7460
|
}
|
|
@@ -7469,7 +7484,7 @@ async function performAct(acts, act) {
|
|
|
7469
7484
|
if (!result.ok) return toolError(JSON.stringify(result));
|
|
7470
7485
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
7471
7486
|
}
|
|
7472
|
-
function createTurnControlMcpServer(acts, skipState
|
|
7487
|
+
function createTurnControlMcpServer(acts, skipState) {
|
|
7473
7488
|
const ACT_TOOL = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
|
|
7474
7489
|
const CANCEL_TOOL = { ...ACT_TOOL, idempotentHint: true };
|
|
7475
7490
|
const RECORDER_TOOL = { readOnlyHint: true, openWorldHint: false };
|
|
@@ -7477,42 +7492,6 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7477
7492
|
name: COMPANION_LOCAL_MCP_SERVER,
|
|
7478
7493
|
version: "0.0.0",
|
|
7479
7494
|
tools: [
|
|
7480
|
-
tool(
|
|
7481
|
-
SEND_TOOL,
|
|
7482
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.send,
|
|
7483
|
-
{
|
|
7484
|
-
agentId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.send.agentId),
|
|
7485
|
-
message: z13.string().min(1).max(MAX_SEND_MESSAGE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.send.message)
|
|
7486
|
-
},
|
|
7487
|
-
async (args) => performAct(acts, {
|
|
7488
|
-
kind: "send",
|
|
7489
|
-
callId: randomUUID(),
|
|
7490
|
-
agentId: args.agentId,
|
|
7491
|
-
message: args.message
|
|
7492
|
-
}),
|
|
7493
|
-
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
7494
|
-
),
|
|
7495
|
-
...replyState ? [
|
|
7496
|
-
tool(
|
|
7497
|
-
REPLY_TO_TOOL,
|
|
7498
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.reply_to,
|
|
7499
|
-
{
|
|
7500
|
-
messageId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.reply_to.messageId)
|
|
7501
|
-
},
|
|
7502
|
-
async (args) => {
|
|
7503
|
-
replyState.answersMessageId = args.messageId;
|
|
7504
|
-
return {
|
|
7505
|
-
content: [
|
|
7506
|
-
{
|
|
7507
|
-
type: "text",
|
|
7508
|
-
text: JSON.stringify({ answersMessageId: args.messageId })
|
|
7509
|
-
}
|
|
7510
|
-
]
|
|
7511
|
-
};
|
|
7512
|
-
},
|
|
7513
|
-
{ annotations: RECORDER_TOOL, alwaysLoad: true }
|
|
7514
|
-
)
|
|
7515
|
-
] : [],
|
|
7516
7495
|
...skipState ? [
|
|
7517
7496
|
tool(
|
|
7518
7497
|
SKIP_TURN_TOOL,
|
|
@@ -8039,8 +8018,7 @@ var TurnCommitter = class {
|
|
|
8039
8018
|
kind,
|
|
8040
8019
|
turnId: deps.turnId,
|
|
8041
8020
|
seq,
|
|
8042
|
-
parentMessageId: deps.parentMessageId
|
|
8043
|
-
...kind === "final" ? this.turnControlFields("final") : {}
|
|
8021
|
+
parentMessageId: deps.parentMessageId
|
|
8044
8022
|
},
|
|
8045
8023
|
deps.signal
|
|
8046
8024
|
);
|
|
@@ -8081,19 +8059,22 @@ var TurnCommitter = class {
|
|
|
8081
8059
|
{ turnId: deps.turnId, text, seq },
|
|
8082
8060
|
deps.signal
|
|
8083
8061
|
);
|
|
8062
|
+
},
|
|
8063
|
+
reportText: async ({ text, seq }) => {
|
|
8064
|
+
await deps.api.reportText(
|
|
8065
|
+
deps.workspaceId,
|
|
8066
|
+
deps.conversationId,
|
|
8067
|
+
deps.agentId,
|
|
8068
|
+
{ turnId: deps.turnId, text, seq },
|
|
8069
|
+
deps.signal
|
|
8070
|
+
);
|
|
8084
8071
|
}
|
|
8085
8072
|
};
|
|
8086
8073
|
this.pump = new TurnPump({
|
|
8087
8074
|
commit,
|
|
8088
8075
|
signal: deps.signal,
|
|
8089
8076
|
nextSeq: deps.nextSeq,
|
|
8090
|
-
|
|
8091
|
-
deps.log.warn(
|
|
8092
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
8093
|
-
"dispatcher: progress-promotion commit failed"
|
|
8094
|
-
);
|
|
8095
|
-
deps.onCommitFailed?.(err);
|
|
8096
|
-
}
|
|
8077
|
+
...deps.spoke !== void 0 ? { spoke: deps.spoke } : {}
|
|
8097
8078
|
});
|
|
8098
8079
|
this.emit = sinkEmitter(this.pump.sink, this.onError);
|
|
8099
8080
|
}
|
|
@@ -8117,15 +8098,17 @@ var TurnCommitter = class {
|
|
|
8117
8098
|
this.onError(err, event.type);
|
|
8118
8099
|
}
|
|
8119
8100
|
}
|
|
8120
|
-
//
|
|
8121
|
-
//
|
|
8122
|
-
//
|
|
8123
|
-
//
|
|
8124
|
-
//
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8101
|
+
// A message has landed under this span — its first words at least. A span
|
|
8102
|
+
// that spoke settles; one that never did is a pass. Nothing is promoted at
|
|
8103
|
+
// the end of a turn: the held-text flush is the adapter's (it emits the
|
|
8104
|
+
// closing reply as a terminal `text` event before `result`), and a span
|
|
8105
|
+
// that ended on working text ends without last words.
|
|
8106
|
+
get spoke() {
|
|
8107
|
+
return this.pump.spoke;
|
|
8108
|
+
}
|
|
8109
|
+
// The `seq` of the last words, for the settle to declare; null when none.
|
|
8110
|
+
get lastMessageSeq() {
|
|
8111
|
+
return this.pump.lastMessageSeq;
|
|
8129
8112
|
}
|
|
8130
8113
|
// The turn's final reply text, or null if none landed.
|
|
8131
8114
|
get replyBody() {
|
|
@@ -8137,26 +8120,6 @@ var TurnCommitter = class {
|
|
|
8137
8120
|
get finalEmitted() {
|
|
8138
8121
|
return this.pump.finalEmitted;
|
|
8139
8122
|
}
|
|
8140
|
-
// A closing textual reply and a wordless terminal marker carry the same
|
|
8141
|
-
// declared-reply projection. The KIND matters: the auto-declared reply binds
|
|
8142
|
-
// only a textual `final` — a wordless turn has no answer to bind, and the
|
|
8143
|
-
// server's mute-settle notice speaks for it.
|
|
8144
|
-
turnControlFields(kind) {
|
|
8145
|
-
return this.answersField(kind);
|
|
8146
|
-
}
|
|
8147
|
-
// The declared reply: explicit `reply_to` first, else the runtime's own
|
|
8148
|
-
// declaration for the turn that just answers — see
|
|
8149
|
-
// `resolveDeclaredReplyField` for the whole rule and its reasons. CT1224: an
|
|
8150
|
-
// auto-declaration is flagged as such on the wire, because the server drops
|
|
8151
|
-
// it when the turn performed an outward act (CT1354: any of them, read off
|
|
8152
|
-
// the ledger). An explicit `reply_to` carries no flag and always stands.
|
|
8153
|
-
answersField(kind) {
|
|
8154
|
-
return resolveDeclaredReplyField({
|
|
8155
|
-
kind,
|
|
8156
|
-
owedReplyMessageId: this.deps.owedReplyMessageId,
|
|
8157
|
-
replyState: this.deps.replyState
|
|
8158
|
-
});
|
|
8159
|
-
}
|
|
8160
8123
|
};
|
|
8161
8124
|
|
|
8162
8125
|
// src/turn-runtime-integrity.ts
|
|
@@ -8170,7 +8133,7 @@ async function postIncompleteNotice(ctx, harness, resolution) {
|
|
|
8170
8133
|
try {
|
|
8171
8134
|
await ctx.postTurnMessage(ctx.workspaceId, ctx.conversationId, {
|
|
8172
8135
|
body: turnIncompleteCopy(harness),
|
|
8173
|
-
kind: "
|
|
8136
|
+
kind: "runtime_notice",
|
|
8174
8137
|
turnId: ctx.turnId,
|
|
8175
8138
|
parentMessageId: ctx.parentMessageId
|
|
8176
8139
|
});
|
|
@@ -8239,12 +8202,9 @@ async function absorbBundledLoss(ctx, aborted, outcome) {
|
|
|
8239
8202
|
|
|
8240
8203
|
// src/turn-execution.ts
|
|
8241
8204
|
var PREPARING_TOOL_NAME = "preparing";
|
|
8242
|
-
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:";
|
|
8243
8205
|
var MISSING_SECRET_PREFIX = "**Missing secret on this companion.** This agent's tools need a credential this device hasn't been given, so I can't run this turn safely. Declare it in this companion\u2019s secret store (`~/.cabane/secrets.json`) and try again. Missing:";
|
|
8244
8206
|
var RUNTIME_UNAVAILABLE_PREFIX = "**This agent's runtime isn't available on this companion.** The model this agent uses needs a runtime this device isn't running, so I can't run this turn here. Details:";
|
|
8245
8207
|
var UNEXPECTED_ROLE_REASON = `refused the wake trigger (role "system" isn't dispatchable on this companion) \u2014 the companion is likely running outdated code; refresh it, then re-address the agent`;
|
|
8246
|
-
var SKIPPED_MARKER_BODY = "(skipped)";
|
|
8247
|
-
var SILENT_MARKER_BODY = "(no reply)";
|
|
8248
8208
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
8249
8209
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
8250
8210
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
@@ -8265,7 +8225,6 @@ function initialOutcome() {
|
|
|
8265
8225
|
contentBearingEvents: 0,
|
|
8266
8226
|
latestSessionState: null,
|
|
8267
8227
|
settledDiagnostics: null,
|
|
8268
|
-
silentMarkerEmitted: false,
|
|
8269
8228
|
timeoutReason: null,
|
|
8270
8229
|
leaseLost: false,
|
|
8271
8230
|
runtimeIncomplete: false
|
|
@@ -8314,13 +8273,15 @@ var TurnExecution = class {
|
|
|
8314
8273
|
// `turnId` is set on every dispatch. `resumedFromSeq` is what it seeded from.
|
|
8315
8274
|
resumed;
|
|
8316
8275
|
resumedFromSeq;
|
|
8276
|
+
// Whether the span this resumed run continues already has first words —
|
|
8277
|
+
// from the server (`turnSpoke`) or from speech still queued in the outbox.
|
|
8278
|
+
resumedSpoke = false;
|
|
8317
8279
|
turnContext;
|
|
8318
8280
|
resolvedMcpServers;
|
|
8319
8281
|
effectiveCwd;
|
|
8320
8282
|
hookEnv;
|
|
8321
8283
|
turnEnv;
|
|
8322
8284
|
skipState;
|
|
8323
|
-
replyState;
|
|
8324
8285
|
turnControlServer;
|
|
8325
8286
|
request;
|
|
8326
8287
|
adapter;
|
|
@@ -8338,9 +8299,9 @@ var TurnExecution = class {
|
|
|
8338
8299
|
disarmWatchdogs = () => {
|
|
8339
8300
|
};
|
|
8340
8301
|
// Codo's stack review, blocking finding #2: set the moment `acquireLease`'s
|
|
8341
|
-
// PATCH returns — the
|
|
8342
|
-
//
|
|
8343
|
-
//
|
|
8302
|
+
// PATCH returns — the lease is granted and this is the pair's running span,
|
|
8303
|
+
// so every exit after this point must settle THE SPAN, not just clear the
|
|
8304
|
+
// participant flag.
|
|
8344
8305
|
admitted = false;
|
|
8345
8306
|
concluded(reason, errorReason) {
|
|
8346
8307
|
return new TurnConcluded(reason, errorReason);
|
|
@@ -8378,14 +8339,16 @@ var TurnExecution = class {
|
|
|
8378
8339
|
return this.report();
|
|
8379
8340
|
}
|
|
8380
8341
|
async concludeBeforeRun(reason, errorReason) {
|
|
8381
|
-
const { payload, turnLog, startedAt } = this;
|
|
8382
|
-
const body = {
|
|
8383
|
-
activeRunStartedAt: null,
|
|
8384
|
-
// CT1046: this dispatch is over before it ran — retire its evidence so it
|
|
8385
|
-
// can't authorize a turn-token mint afterwards.
|
|
8386
|
-
settledMessageId: payload.messageId
|
|
8387
|
-
};
|
|
8342
|
+
const { payload, turnLog, startedAt, turnId } = this;
|
|
8343
|
+
const body = { activeRunStartedAt: null, turnId, outcome: "failed" };
|
|
8388
8344
|
if (errorReason) body.errorReason = errorReason.slice(0, 200);
|
|
8345
|
+
const context = this.turnContext;
|
|
8346
|
+
if (context?.readThrough !== void 0) {
|
|
8347
|
+
body.readThrough = context.readThrough;
|
|
8348
|
+
if (context.readRevision !== void 0) body.readRevision = context.readRevision;
|
|
8349
|
+
} else {
|
|
8350
|
+
body.readThrough = payload.messageId;
|
|
8351
|
+
}
|
|
8389
8352
|
try {
|
|
8390
8353
|
await this.opts.api.setActiveRun(
|
|
8391
8354
|
this.opts.workspaceId,
|
|
@@ -8404,21 +8367,18 @@ var TurnExecution = class {
|
|
|
8404
8367
|
return { ok: false, durationMs, reason };
|
|
8405
8368
|
}
|
|
8406
8369
|
// The post-admission analogue of `concludeBeforeRun` (Codo's stack review,
|
|
8407
|
-
// blocking finding #2): this turn WAS admitted —
|
|
8408
|
-
//
|
|
8409
|
-
//
|
|
8410
|
-
//
|
|
8411
|
-
//
|
|
8412
|
-
//
|
|
8413
|
-
// the turn
|
|
8414
|
-
// `final` before concluding, so the turn genuinely produced its terminal
|
|
8415
|
-
// output and `settled` releases the queue honestly.
|
|
8370
|
+
// blocking finding #2): this turn WAS admitted — it holds the conversation's
|
|
8371
|
+
// lease — so the clear must NAME the turn and DECLARE its terminal outcome.
|
|
8372
|
+
// A bare pre-run clear here would reset the participant flag while the span
|
|
8373
|
+
// stayed running until the lost sweep. `outcome` follows the settle wire's
|
|
8374
|
+
// rule: a failure names its reason; without one the turn settled — the
|
|
8375
|
+
// runtime-unavailable exit posts its user-facing `final` before concluding,
|
|
8376
|
+
// so the turn genuinely produced its terminal output.
|
|
8416
8377
|
async concludeAdmittedRun(reason, errorReason) {
|
|
8417
8378
|
const { payload, turnLog, startedAt, turnId } = this;
|
|
8418
8379
|
const body = {
|
|
8419
8380
|
activeRunStartedAt: null,
|
|
8420
8381
|
turnId,
|
|
8421
|
-
settledMessageId: payload.messageId,
|
|
8422
8382
|
// CT1379: `runtimeIncomplete` is a real failure that deliberately carries
|
|
8423
8383
|
// no `errorReason`, so the inference alone would read it as settled.
|
|
8424
8384
|
outcome: errorReason || this.outcome.runtimeIncomplete ? "failed" : "settled"
|
|
@@ -8444,6 +8404,7 @@ var TurnExecution = class {
|
|
|
8444
8404
|
async fetchContext() {
|
|
8445
8405
|
const { payload, turnId, turnLog } = this;
|
|
8446
8406
|
const outboxFloor = this.resumed ? readOutboxFloor((id) => this.opts.api.outboxMaxSeqForTurn(id), turnId, turnLog) : 0;
|
|
8407
|
+
const outboxSpoke = this.resumed && this.opts.api.outboxHasSpeechForTurn(turnId);
|
|
8447
8408
|
let turnContext;
|
|
8448
8409
|
try {
|
|
8449
8410
|
turnContext = await this.opts.api.getTurnContext(
|
|
@@ -8471,12 +8432,14 @@ var TurnExecution = class {
|
|
|
8471
8432
|
const sources = { serverFloor: turnContext.turnSeqFloor, outboxFloor };
|
|
8472
8433
|
this.resumedFromSeq = resolveSeqFloor(sources, { turnId, log: turnLog });
|
|
8473
8434
|
this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
|
|
8435
|
+
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
8436
|
+
if (this.resumedSpoke) turnLog.info({ turnId }, "dispatcher: resumed span already spoke");
|
|
8474
8437
|
}
|
|
8475
8438
|
}
|
|
8476
8439
|
gateTrigger() {
|
|
8477
8440
|
const { payload, turnLog } = this;
|
|
8478
8441
|
const message = this.turnContext.message;
|
|
8479
|
-
const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system"
|
|
8442
|
+
const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system";
|
|
8480
8443
|
if (!isDispatchableTrigger) {
|
|
8481
8444
|
turnLog.warn({ role: message.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
|
|
8482
8445
|
throw this.concluded("unexpected_role", UNEXPECTED_ROLE_REASON);
|
|
@@ -8502,7 +8465,7 @@ var TurnExecution = class {
|
|
|
8502
8465
|
try {
|
|
8503
8466
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8504
8467
|
body: `${MISSING_SECRET_PREFIX} ${list}`,
|
|
8505
|
-
kind: "
|
|
8468
|
+
kind: "runtime_notice",
|
|
8506
8469
|
turnId: randomUUID2(),
|
|
8507
8470
|
// CT113: even a turn that fails before it runs answers a message.
|
|
8508
8471
|
parentMessageId: payload.messageId
|
|
@@ -8532,32 +8495,6 @@ var TurnExecution = class {
|
|
|
8532
8495
|
effectiveCwd = void 0;
|
|
8533
8496
|
}
|
|
8534
8497
|
let hookEnv;
|
|
8535
|
-
const triggerIsPrepareFailure = this.turnContext.message.body.startsWith(PREPARE_FAILED_PREFIX);
|
|
8536
|
-
const sendPrepareFailureHandback = async () => {
|
|
8537
|
-
if (!this.turnContext.dispatchedByAgentId || triggerIsPrepareFailure) return;
|
|
8538
|
-
try {
|
|
8539
|
-
await this.opts.api.postAddressedMessage(workspaceId, payload.conversationId, {
|
|
8540
|
-
body: `${PREPARE_FAILED_PREFIX}
|
|
8541
|
-
|
|
8542
|
-
The dispatched turn could not start. Re-dispatch it after repairing the preparation failure shown in this conversation.`,
|
|
8543
|
-
dispatch: this.turnContext.dispatchedByAgentId,
|
|
8544
|
-
// Keyed on the TRIGGER, not the turn: a redelivered dispatch event is
|
|
8545
|
-
// the replay this has to survive, and it mints a fresh `turnId` while
|
|
8546
|
-
// carrying the same `messageId`. One handback per failed pickup of a
|
|
8547
|
-
// given trigger — which is also CT992's rule, that the first failure
|
|
8548
|
-
// is the one carrying information. A genuine re-dispatch after a
|
|
8549
|
-
// repair authors a NEW trigger message, so it keys differently and
|
|
8550
|
-
// hands back again.
|
|
8551
|
-
idempotencyKey: `prepare-failure-handback:${payload.messageId}`,
|
|
8552
|
-
parentMessageId: payload.messageId
|
|
8553
|
-
});
|
|
8554
|
-
} catch (err) {
|
|
8555
|
-
turnLog.warn(
|
|
8556
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
8557
|
-
"dispatcher: prepare-failure handback failed"
|
|
8558
|
-
);
|
|
8559
|
-
}
|
|
8560
|
-
};
|
|
8561
8498
|
if (prepareHook) {
|
|
8562
8499
|
let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
8563
8500
|
if (cached2 && !checkoutState(cached2.cwd).ok) {
|
|
@@ -8589,23 +8526,8 @@ The dispatched turn could not start. Re-dispatch it after repairing the preparat
|
|
|
8589
8526
|
} catch (err) {
|
|
8590
8527
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8591
8528
|
turnLog.error({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
body: `${PREPARE_FAILED_PREFIX}
|
|
8595
|
-
|
|
8596
|
-
${reason}`,
|
|
8597
|
-
kind: "final",
|
|
8598
|
-
turnId,
|
|
8599
|
-
parentMessageId: payload.messageId
|
|
8600
|
-
});
|
|
8601
|
-
} catch (postErr) {
|
|
8602
|
-
turnLog.warn(
|
|
8603
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8604
|
-
"dispatcher: prepare-rejection post failed"
|
|
8605
|
-
);
|
|
8606
|
-
}
|
|
8607
|
-
await sendPrepareFailureHandback();
|
|
8608
|
-
throw this.concluded(`prepare_failed: ${reason}`);
|
|
8529
|
+
const failReason = `prepare_failed: ${reason}`;
|
|
8530
|
+
throw this.concluded(failReason, failReason);
|
|
8609
8531
|
}
|
|
8610
8532
|
}
|
|
8611
8533
|
} else {
|
|
@@ -8664,25 +8586,8 @@ ${reason}`,
|
|
|
8664
8586
|
if (preparingStarted) reportPreparing("error");
|
|
8665
8587
|
const reason = err instanceof Error ? err.message : String(err);
|
|
8666
8588
|
turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
|
|
8667
|
-
try {
|
|
8668
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8669
|
-
body: `${PREPARE_FAILED_PREFIX}
|
|
8670
|
-
|
|
8671
|
-
${reason}`,
|
|
8672
|
-
kind: "final",
|
|
8673
|
-
turnId,
|
|
8674
|
-
// CT113: stamp the parent even on the prepare-failed close.
|
|
8675
|
-
parentMessageId: payload.messageId
|
|
8676
|
-
});
|
|
8677
|
-
} catch (postErr) {
|
|
8678
|
-
turnLog.warn(
|
|
8679
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
8680
|
-
"dispatcher: prepare-failure post failed"
|
|
8681
|
-
);
|
|
8682
|
-
}
|
|
8683
|
-
await sendPrepareFailureHandback();
|
|
8684
8589
|
const failReason = `prepare_failed: ${reason}`;
|
|
8685
|
-
throw this.concluded(failReason);
|
|
8590
|
+
throw this.concluded(failReason, failReason);
|
|
8686
8591
|
}
|
|
8687
8592
|
}
|
|
8688
8593
|
}
|
|
@@ -8709,7 +8614,12 @@ ${reason}`,
|
|
|
8709
8614
|
// already emitted a "preparing" activity row for this turn above (it
|
|
8710
8615
|
// runs before this flip), which would otherwise make the sweep mistake
|
|
8711
8616
|
// this live turn for an abandoned one and close it with a `stopped`.
|
|
8712
|
-
turnId
|
|
8617
|
+
turnId,
|
|
8618
|
+
// The newest message this turn's read covered: the lease records it,
|
|
8619
|
+
// and settle moves the cursor to it. Omitted when the server sent
|
|
8620
|
+
// none (an older API), so the cursor stays where it was.
|
|
8621
|
+
...this.turnContext.readThrough !== void 0 ? { readThrough: this.turnContext.readThrough } : {},
|
|
8622
|
+
...this.turnContext.readRevision !== void 0 ? { readRevision: this.turnContext.readRevision } : {}
|
|
8713
8623
|
});
|
|
8714
8624
|
} catch (err) {
|
|
8715
8625
|
const refusal = leaseRefusal(err);
|
|
@@ -8732,11 +8642,9 @@ ${reason}`,
|
|
|
8732
8642
|
buildRequest() {
|
|
8733
8643
|
const { payload, workspaceId } = this;
|
|
8734
8644
|
const skipState = this.skipState = createSkipState();
|
|
8735
|
-
const replyState = this.replyState = createReplyState();
|
|
8736
8645
|
const turnControlServer = createTurnControlMcpServer(
|
|
8737
8646
|
{ api: this.opts.api, workspaceId, turnId: this.turnId },
|
|
8738
|
-
skipState
|
|
8739
|
-
replyState
|
|
8647
|
+
skipState
|
|
8740
8648
|
);
|
|
8741
8649
|
this.request = buildCompanionTurnRequest({
|
|
8742
8650
|
turnContext: this.turnContext,
|
|
@@ -8805,7 +8713,7 @@ ${reason}`,
|
|
|
8805
8713
|
try {
|
|
8806
8714
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8807
8715
|
body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
|
|
8808
|
-
kind: "
|
|
8716
|
+
kind: "runtime_notice",
|
|
8809
8717
|
turnId,
|
|
8810
8718
|
parentMessageId: payload.messageId
|
|
8811
8719
|
});
|
|
@@ -8820,6 +8728,43 @@ ${reason}`,
|
|
|
8820
8728
|
throw this.concluded(`runtime_unavailable:${err.runtime}`);
|
|
8821
8729
|
}
|
|
8822
8730
|
}
|
|
8731
|
+
// The adapter refused to resume this pair's stored session and is starting a
|
|
8732
|
+
// fresh one. The request it holds was composed for a session that had the
|
|
8733
|
+
// conversation so far — the read is what landed since the cursor, the
|
|
8734
|
+
// agent's own words left out because the session held them. A fresh session
|
|
8735
|
+
// holds none of it, so fetch the context again as a first turn on an empty
|
|
8736
|
+
// session (the whole conversation, own words included, the first-turn
|
|
8737
|
+
// material) and hand the adapter that to run on — before it speaks or acts.
|
|
8738
|
+
// The lease already granted stands; the read reaches at least its mark. A
|
|
8739
|
+
// fetch that fails returns null and the adapter falls back to the request
|
|
8740
|
+
// it has, flagged degraded, so the server rewinds the cursor at settle.
|
|
8741
|
+
async freshRequest(reason) {
|
|
8742
|
+
const { payload, turnId, turnLog } = this;
|
|
8743
|
+
try {
|
|
8744
|
+
const context = await this.opts.api.getTurnContext(
|
|
8745
|
+
payload.conversationId,
|
|
8746
|
+
payload.messageId,
|
|
8747
|
+
turnId,
|
|
8748
|
+
false,
|
|
8749
|
+
true
|
|
8750
|
+
);
|
|
8751
|
+
turnLog.warn(
|
|
8752
|
+
{ reason },
|
|
8753
|
+
"dispatcher: stored session not resumed \u2014 the turn context was recomposed for a fresh session"
|
|
8754
|
+
);
|
|
8755
|
+
return {
|
|
8756
|
+
systemPrompt: context.systemPrompt,
|
|
8757
|
+
prompt: context.prompt,
|
|
8758
|
+
content: context.content
|
|
8759
|
+
};
|
|
8760
|
+
} catch (err) {
|
|
8761
|
+
turnLog.error(
|
|
8762
|
+
{ reason, err: err instanceof Error ? err.message : String(err) },
|
|
8763
|
+
"dispatcher: could not recompose the turn context for a fresh session \u2014 running on the incremental one, degraded"
|
|
8764
|
+
);
|
|
8765
|
+
return null;
|
|
8766
|
+
}
|
|
8767
|
+
}
|
|
8823
8768
|
async execute() {
|
|
8824
8769
|
const { payload, workspaceId, turnId, turnLog, startedAt } = this;
|
|
8825
8770
|
const o = this.outcome;
|
|
@@ -8851,10 +8796,7 @@ ${reason}`,
|
|
|
8851
8796
|
signal: abortController.signal,
|
|
8852
8797
|
log: turnLog,
|
|
8853
8798
|
nextSeq: this.nextSeq,
|
|
8854
|
-
|
|
8855
|
-
// The ledger-derived owed reply, for the runtime's own declaration when
|
|
8856
|
-
// the agent doesn't call `reply_to` (resolveDeclaredReplyField).
|
|
8857
|
-
owedReplyMessageId: this.turnContext.owedReplyMessageId ?? null,
|
|
8799
|
+
spoke: this.resumedSpoke,
|
|
8858
8800
|
// CT1292: a commit that didn't land may mean this turn's lease is gone.
|
|
8859
8801
|
// `noteCommitFailed` is a field slot the lease watchdog fills when it
|
|
8860
8802
|
// arms (it starts as a no-op) — the committer only fires it from inside
|
|
@@ -8873,9 +8815,6 @@ ${reason}`,
|
|
|
8873
8815
|
payload.agentId,
|
|
8874
8816
|
turnId
|
|
8875
8817
|
);
|
|
8876
|
-
if (intent.answersMessageId) {
|
|
8877
|
-
this.replyState.answersMessageId = intent.answersMessageId;
|
|
8878
|
-
}
|
|
8879
8818
|
if (intent.skipped) {
|
|
8880
8819
|
this.skipState.skipped = true;
|
|
8881
8820
|
this.skipState.reason = intent.skipReason;
|
|
@@ -8883,7 +8822,7 @@ ${reason}`,
|
|
|
8883
8822
|
} catch (err) {
|
|
8884
8823
|
turnLog.warn(
|
|
8885
8824
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
8886
|
-
"dispatcher: turn-control intent fetch failed; the
|
|
8825
|
+
"dispatcher: turn-control intent fetch failed; the skip for this turn is dropped"
|
|
8887
8826
|
);
|
|
8888
8827
|
}
|
|
8889
8828
|
};
|
|
@@ -8941,7 +8880,9 @@ ${reason}`,
|
|
|
8941
8880
|
clearInterval(leaseTimer);
|
|
8942
8881
|
};
|
|
8943
8882
|
try {
|
|
8944
|
-
for await (const event of this.adapter.runTurn(this.request, abortController.signal
|
|
8883
|
+
for await (const event of this.adapter.runTurn(this.request, abortController.signal, {
|
|
8884
|
+
fresh: (reason) => this.freshRequest(reason)
|
|
8885
|
+
})) {
|
|
8945
8886
|
transcript?.write(event);
|
|
8946
8887
|
o.eventCounts[event.type] += 1;
|
|
8947
8888
|
if (isContentBearingEvent(event)) o.contentBearingEvents += 1;
|
|
@@ -9026,43 +8967,6 @@ ${reason}`,
|
|
|
9026
8967
|
{ reason: this.skipState.reason, turnId, ok: o.okResult },
|
|
9027
8968
|
"agent skipped turn (skip_turn)"
|
|
9028
8969
|
);
|
|
9029
|
-
try {
|
|
9030
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
9031
|
-
body: SKIPPED_MARKER_BODY,
|
|
9032
|
-
kind: "skipped",
|
|
9033
|
-
turnId,
|
|
9034
|
-
seq: this.nextSeq(),
|
|
9035
|
-
parentMessageId: payload.messageId
|
|
9036
|
-
});
|
|
9037
|
-
} catch (err) {
|
|
9038
|
-
turnLog.warn(
|
|
9039
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
9040
|
-
"dispatcher: skipped-marker commit failed"
|
|
9041
|
-
);
|
|
9042
|
-
}
|
|
9043
|
-
} else {
|
|
9044
|
-
await committer.finalize(o.okResult);
|
|
9045
|
-
if (!abortController.signal.aborted && o.okResult && !committer.finalEmitted) {
|
|
9046
|
-
try {
|
|
9047
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
9048
|
-
body: SILENT_MARKER_BODY,
|
|
9049
|
-
kind: "silent",
|
|
9050
|
-
turnId,
|
|
9051
|
-
seq: this.nextSeq(),
|
|
9052
|
-
parentMessageId: payload.messageId,
|
|
9053
|
-
// An explicit `reply_to` survives a wordless turn exactly as it
|
|
9054
|
-
// survives a textual final (the auto-declaration does not — a
|
|
9055
|
-
// marker has no answer to bind).
|
|
9056
|
-
...committer.turnControlFields("silent")
|
|
9057
|
-
});
|
|
9058
|
-
o.silentMarkerEmitted = true;
|
|
9059
|
-
} catch (err) {
|
|
9060
|
-
turnLog.warn(
|
|
9061
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
9062
|
-
"dispatcher: silent-marker commit failed"
|
|
9063
|
-
);
|
|
9064
|
-
}
|
|
9065
|
-
}
|
|
9066
8970
|
}
|
|
9067
8971
|
} catch (err) {
|
|
9068
8972
|
o.okResult = false;
|
|
@@ -9105,12 +9009,7 @@ ${reason}`,
|
|
|
9105
9009
|
// CT277: the server keys the turn-end metadata UPDATE (ended_at + tokens)
|
|
9106
9010
|
// off this turn id, so the settle PATCH must carry it — the start PATCH
|
|
9107
9011
|
// isn't enough. Always sent (every settle closes a real turn).
|
|
9108
|
-
turnId
|
|
9109
|
-
// CT1046: and the trigger this turn answered, so the server retires the
|
|
9110
|
-
// dispatch that authorized it. Always sent, unlike `lastSeenMessageId`
|
|
9111
|
-
// below (which is a success-only catch-up advance): a failed or cancelled
|
|
9112
|
-
// turn is just as over, and its evidence must die with it.
|
|
9113
|
-
settledMessageId: payload.messageId
|
|
9012
|
+
turnId
|
|
9114
9013
|
};
|
|
9115
9014
|
if (o.turnUsage) {
|
|
9116
9015
|
body.usage = o.turnUsage;
|
|
@@ -9124,10 +9023,8 @@ ${reason}`,
|
|
|
9124
9023
|
if (!o.okResult && o.resultReason && o.resultReason !== "cancelled" && !userCancelled && !o.leaseLost && !o.runtimeIncomplete && !this.skipState.skipped) {
|
|
9125
9024
|
body.errorReason = o.resultReason.slice(0, 200);
|
|
9126
9025
|
}
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
}
|
|
9130
|
-
body.outcome = o.okResult ? "settled" : body.errorReason || o.runtimeIncomplete ? "failed" : "interrupted";
|
|
9026
|
+
body.outcome = o.okResult ? committer.spoke ? "settled" : "passed" : body.errorReason || o.runtimeIncomplete ? "failed" : "interrupted";
|
|
9027
|
+
if (body.outcome === "settled") body.lastMessageSeq = committer.lastMessageSeq;
|
|
9131
9028
|
if (o.sessionDegraded) {
|
|
9132
9029
|
body.degraded = true;
|
|
9133
9030
|
}
|
|
@@ -9144,7 +9041,7 @@ ${reason}`,
|
|
|
9144
9041
|
eventCounts: o.eventCounts,
|
|
9145
9042
|
runtimeResultKind: o.runtimeResultKind,
|
|
9146
9043
|
...o.turnMcpInventory ? { mcpInventory: o.turnMcpInventory } : {},
|
|
9147
|
-
finalSource: outcome === "skipped" || outcome === "cancelled" ||
|
|
9044
|
+
finalSource: outcome === "skipped" || outcome === "cancelled" || !committer.finalEmitted ? "none" : committer.finalSource
|
|
9148
9045
|
};
|
|
9149
9046
|
body.diagnostics = o.settledDiagnostics;
|
|
9150
9047
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
|
@@ -9438,8 +9335,8 @@ var Outbox = class {
|
|
|
9438
9335
|
this.enforceBounds();
|
|
9439
9336
|
}
|
|
9440
9337
|
// Every queued entry, oldest-first by `(enqueuedAt, turnId, seq)` so the
|
|
9441
|
-
// drainer delivers
|
|
9442
|
-
// before newer ones. Malformed files are dropped (logged) rather than
|
|
9338
|
+
// drainer delivers a turn's rows in the order they were minted, and older
|
|
9339
|
+
// turns before newer ones. Malformed files are dropped (logged) rather than
|
|
9443
9340
|
// wedging the drain.
|
|
9444
9341
|
list() {
|
|
9445
9342
|
const dir2 = this.dir();
|
|
@@ -9465,8 +9362,9 @@ var Outbox = class {
|
|
|
9465
9362
|
this.dropCorrupt(full);
|
|
9466
9363
|
}
|
|
9467
9364
|
}
|
|
9365
|
+
const rank = (e) => e.kind === "active-run" ? 1 : 0;
|
|
9468
9366
|
entries.sort(
|
|
9469
|
-
(a, b) => a.enqueuedAt - b.enqueuedAt || (a.turnId < b.turnId ? -1 : a.turnId > b.turnId ? 1 : 0) || a.seq - b.seq
|
|
9367
|
+
(a, b) => a.enqueuedAt - b.enqueuedAt || rank(a) - rank(b) || (a.turnId < b.turnId ? -1 : a.turnId > b.turnId ? 1 : 0) || a.seq - b.seq
|
|
9470
9368
|
);
|
|
9471
9369
|
return entries;
|
|
9472
9370
|
}
|