@cabane/companion 0.6.92 → 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 +238 -340
- package/dist/runtime.js +238 -340
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2707,8 +2707,12 @@ var CabaneApi = class {
|
|
|
2707
2707
|
// it), so it is passed down rather than inferred here. It gates the COST of
|
|
2708
2708
|
// the aggregate, never the tenancy scoping, which the route applies
|
|
2709
2709
|
// unconditionally.
|
|
2710
|
-
|
|
2711
|
-
|
|
2710
|
+
// `fresh` asks for the context a session that holds NOTHING needs: the read
|
|
2711
|
+
// from the start of the conversation, the agent's own words included, and
|
|
2712
|
+
// the first-turn material — what the adapter runs on when it refused to
|
|
2713
|
+
// resume the stored session and is starting a fresh one.
|
|
2714
|
+
getTurnContext(conversationId, messageId2, turnId, resumed, fresh) {
|
|
2715
|
+
const q = `conversationId=${encodeURIComponent(conversationId)}&messageId=${encodeURIComponent(messageId2)}` + (turnId ? `&turnId=${encodeURIComponent(turnId)}` : "") + (resumed ? "&resumed=1" : "") + (fresh ? "&fresh=1" : "");
|
|
2712
2716
|
return this.request("GET", `/api/agent/turn-context?${q}`);
|
|
2713
2717
|
}
|
|
2714
2718
|
// CT1380: the local half of a resumed turn's seq floor (see
|
|
@@ -2717,6 +2721,18 @@ var CabaneApi = class {
|
|
|
2717
2721
|
outboxMaxSeqForTurn(turnId) {
|
|
2718
2722
|
return this.opts.outbox?.maxSeqForTurn(turnId) ?? 0;
|
|
2719
2723
|
}
|
|
2724
|
+
// Whether this turn's speech is still waiting in the outbox — the half of
|
|
2725
|
+
// "did the span speak" the server cannot see: a first message absorbed by a
|
|
2726
|
+
// transient failure before the crash is on disk here, spent and undelivered,
|
|
2727
|
+
// and the resumed run must not commit its next note as first words again.
|
|
2728
|
+
// A runtime notice queued under the turn is not speech.
|
|
2729
|
+
outboxHasSpeechForTurn(turnId) {
|
|
2730
|
+
const outbox = this.opts.outbox;
|
|
2731
|
+
if (!outbox) return false;
|
|
2732
|
+
return outbox.list().some(
|
|
2733
|
+
(e) => e.turnId === turnId && e.kind === "message" && e.body?.kind !== "runtime_notice"
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2720
2736
|
// CT714: read a turn's recorded turn-control intent. An EXTERNAL adapter
|
|
2721
2737
|
// (Codex / opencode) records `reply_to` / `skip_turn` into `turn_intents`
|
|
2722
2738
|
// server-side (the URL MCP surface) rather than the dispatcher's in-memory
|
|
@@ -2842,6 +2858,23 @@ var CabaneApi = class {
|
|
|
2842
2858
|
async durableActiveRunWrite(path, conversationId, agentId, body) {
|
|
2843
2859
|
const outbox = this.opts.outbox;
|
|
2844
2860
|
const key = activeRunOutboxKey(conversationId, agentId);
|
|
2861
|
+
const turnId = body?.turnId;
|
|
2862
|
+
if (outbox && typeof turnId === "string" && outbox.maxSeqForTurn(turnId) > 0) {
|
|
2863
|
+
outbox.persist({
|
|
2864
|
+
enqueuedAt: Date.now(),
|
|
2865
|
+
turnId: key,
|
|
2866
|
+
seq: ACTIVE_RUN_OUTBOX_SEQ,
|
|
2867
|
+
method: "PATCH",
|
|
2868
|
+
path,
|
|
2869
|
+
body,
|
|
2870
|
+
kind: "active-run"
|
|
2871
|
+
});
|
|
2872
|
+
this.opts.log?.warn(
|
|
2873
|
+
{ conversationId, agentId, turnId },
|
|
2874
|
+
"companion: settle queued behind this turn's undelivered commits (drains in order)"
|
|
2875
|
+
);
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2845
2878
|
try {
|
|
2846
2879
|
await this.request("PATCH", path, body, { retry: true });
|
|
2847
2880
|
outbox?.remove(key, ACTIVE_RUN_OUTBOX_SEQ);
|
|
@@ -2883,16 +2916,11 @@ var CabaneApi = class {
|
|
|
2883
2916
|
// (transport errors + 5xx), never a 4xx, so no `(turnId, kind)` row is
|
|
2884
2917
|
// double-posted. The caller threads its turn abort signal so a cancel
|
|
2885
2918
|
// mid-commit aborts the in-flight POST rather than letting it land.
|
|
2886
|
-
//
|
|
2887
|
-
//
|
|
2888
|
-
//
|
|
2889
|
-
// companion no longer WRITES that kind — the server does, in the transaction
|
|
2890
|
-
// that terminalizes the stopped turn — so the value survives here only as
|
|
2891
|
-
// wire vocabulary the server also speaks. `seq` is the companion's per-turn
|
|
2919
|
+
// Workzone: a commit is a message — the agent's first or last words — or the
|
|
2920
|
+
// runtime's own line (`runtime_notice`). The words between tool calls go
|
|
2921
|
+
// through `reportText`, the trail's door. `seq` is the companion's per-turn
|
|
2892
2922
|
// monotonic counter, stamped on the row so the merged timeline orders the
|
|
2893
|
-
// commit deterministically against the persisted tool/thinking rows.
|
|
2894
|
-
// fields are optional on the wire — an older companion that didn't mint seq
|
|
2895
|
-
// still validates (the server defaults to 0).
|
|
2923
|
+
// commit deterministically against the persisted tool/thinking rows.
|
|
2896
2924
|
postTurnMessage(workspaceId, conversationId, body, signal) {
|
|
2897
2925
|
return this.durableCommit(
|
|
2898
2926
|
"message",
|
|
@@ -2903,35 +2931,6 @@ var CabaneApi = class {
|
|
|
2903
2931
|
signal
|
|
2904
2932
|
);
|
|
2905
2933
|
}
|
|
2906
|
-
// CT1357: post ONE ordinary addressed message — not turn speech, no `turnId`
|
|
2907
|
-
// — so `dispatch` is honoured and the target agent is woken. The turn-commit
|
|
2908
|
-
// fork ignores `dispatch` (an act rides its own call now), which leaves this
|
|
2909
|
-
// as the only way a companion can address a peer OUTSIDE a running turn.
|
|
2910
|
-
//
|
|
2911
|
-
// The prepare-failure handback is exactly that case and the reason this
|
|
2912
|
-
// exists. It fires before the turn is ever admitted — the active-run PATCH
|
|
2913
|
-
// that admits it comes after the prepare hook — so `turnAct` is not available:
|
|
2914
|
-
// its route resolves the turn first and 404s when there is none. The failure
|
|
2915
|
-
// notice's own commit is what admits the turn, and by then it has also settled
|
|
2916
|
-
// it. An ordinary addressed post depends on no lease at all, which is what
|
|
2917
|
-
// makes it the right shape here.
|
|
2918
|
-
//
|
|
2919
|
-
// Retried like any durable write, and therefore REQUIRING `idempotencyKey`:
|
|
2920
|
-
// this is not outbox-queued (there is no `(turnId, seq)` for a non-turn row),
|
|
2921
|
-
// so without a key a committed request whose response was lost — or a
|
|
2922
|
-
// redelivered dispatch event — writes a second row and wakes the peer twice.
|
|
2923
|
-
// The server dedupes on `(conversation_id, idempotency_key)`: the repeat
|
|
2924
|
-
// returns the existing row and raises no second dispatch (CT239). Required,
|
|
2925
|
-
// not optional, so a future caller cannot omit it and silently lose the
|
|
2926
|
-
// guarantee.
|
|
2927
|
-
postAddressedMessage(workspaceId, conversationId, body, signal) {
|
|
2928
|
-
return this.request(
|
|
2929
|
-
"POST",
|
|
2930
|
-
`/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`,
|
|
2931
|
-
body,
|
|
2932
|
-
signal ? { retry: true, signal } : { retry: true }
|
|
2933
|
-
);
|
|
2934
|
-
}
|
|
2935
2934
|
// SJ477: report one tool-activity transition (start / done / error) for the
|
|
2936
2935
|
// live activity cards. Transient — the server publishes an `agent_activity`
|
|
2937
2936
|
// SSE and writes no row. Agent-PAT authed; the URL `:agentId` must match the
|
|
@@ -2961,6 +2960,20 @@ var CabaneApi = class {
|
|
|
2961
2960
|
signal
|
|
2962
2961
|
);
|
|
2963
2962
|
}
|
|
2963
|
+
// Workzone: report one text the agent wrote between tool calls — the trail's
|
|
2964
|
+
// door. Durable like a thinking row (same outbox, same `(turnId, seq)`
|
|
2965
|
+
// replay), and never a message: the server writes it to `message_activity`
|
|
2966
|
+
// under the span and nothing lands, stamps or wakes on it.
|
|
2967
|
+
reportText(workspaceId, conversationId, agentId, body, signal) {
|
|
2968
|
+
return this.durableCommit(
|
|
2969
|
+
"text",
|
|
2970
|
+
`/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/text`,
|
|
2971
|
+
body,
|
|
2972
|
+
body.turnId,
|
|
2973
|
+
body.seq,
|
|
2974
|
+
signal
|
|
2975
|
+
);
|
|
2976
|
+
}
|
|
2964
2977
|
// SJ493: fetch the agent's self-view — identity + operating context. The
|
|
2965
2978
|
// companion calls this on each dispatch to get its `systemPrompt` (composed
|
|
2966
2979
|
// server-side from the bundled default + the agent's charter) rather than
|
|
@@ -4014,20 +4027,24 @@ var THINKING_TEXT_MAX_CHARS = 3900;
|
|
|
4014
4027
|
var TurnPump = class {
|
|
4015
4028
|
constructor(opts) {
|
|
4016
4029
|
this.opts = opts;
|
|
4030
|
+
this.hasSpoken = opts.spoke ?? false;
|
|
4017
4031
|
this.sink = {
|
|
4018
4032
|
onAssistantText: async (evt) => {
|
|
4019
4033
|
if (opts.signal.aborted) return;
|
|
4020
4034
|
const body = normalizeCommitText(evt.text);
|
|
4021
4035
|
if (!body) return;
|
|
4022
|
-
const kind = evt.final ? "final" : "progress";
|
|
4023
4036
|
const seq = opts.nextSeq();
|
|
4024
|
-
|
|
4037
|
+
if (!evt.final && this.hasSpoken) {
|
|
4038
|
+
await opts.commit.reportText({ text: body, seq });
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
await opts.commit.commitMessage({ body, kind: "message", seq });
|
|
4042
|
+
this.hasSpoken = true;
|
|
4025
4043
|
if (evt.final) {
|
|
4026
4044
|
this.emittedFinal = true;
|
|
4045
|
+
this.lastSeq = seq;
|
|
4027
4046
|
this.finalReplyBody = body;
|
|
4028
4047
|
this.emittedFinalSource = "runtime_text";
|
|
4029
|
-
} else {
|
|
4030
|
-
this.lastProgressBody = body;
|
|
4031
4048
|
}
|
|
4032
4049
|
},
|
|
4033
4050
|
onToolActivity: async (evt) => {
|
|
@@ -4064,11 +4081,12 @@ var TurnPump = class {
|
|
|
4064
4081
|
};
|
|
4065
4082
|
}
|
|
4066
4083
|
opts;
|
|
4067
|
-
//
|
|
4068
|
-
//
|
|
4069
|
-
|
|
4084
|
+
// A message has landed under this span — its first words. Every later
|
|
4085
|
+
// non-terminal text is the trail. Seeded from the host on a resume.
|
|
4086
|
+
hasSpoken;
|
|
4087
|
+
// The last words landed, and their `seq` — what the settle declares.
|
|
4070
4088
|
emittedFinal = false;
|
|
4071
|
-
|
|
4089
|
+
lastSeq = null;
|
|
4072
4090
|
// The turn's final reply text, captured for the dashboard feed / logging.
|
|
4073
4091
|
finalReplyBody = null;
|
|
4074
4092
|
emittedFinalSource = "none";
|
|
@@ -4079,31 +4097,20 @@ var TurnPump = class {
|
|
|
4079
4097
|
// frozen committer test — drive. Each callback applies the choreography and
|
|
4080
4098
|
// commits through the injected sink.
|
|
4081
4099
|
sink;
|
|
4082
|
-
//
|
|
4083
|
-
//
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
// for any reason. The host closes that wordless turn with a marker instead.
|
|
4087
|
-
// Skipped when cancelled or already final. The held-text flush that precedes
|
|
4088
|
-
// this is a classification concern, driven by the caller before this runs.
|
|
4089
|
-
async finalize(ok) {
|
|
4090
|
-
if (!ok || this.opts.signal.aborted || this.emittedFinal || !this.lastProgressBody) return;
|
|
4091
|
-
const body = this.lastProgressBody;
|
|
4092
|
-
const seq = this.opts.nextSeq();
|
|
4093
|
-
try {
|
|
4094
|
-
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
4095
|
-
this.emittedFinal = true;
|
|
4096
|
-
this.finalReplyBody = body;
|
|
4097
|
-
this.emittedFinalSource = "progress_promotion";
|
|
4098
|
-
} catch (err) {
|
|
4099
|
-
this.opts.onError?.(err, "progress-promotion");
|
|
4100
|
-
}
|
|
4100
|
+
// Whether a message has landed under this span — first words, at least. A
|
|
4101
|
+
// span that spoke settles; one that never did is a pass.
|
|
4102
|
+
get spoke() {
|
|
4103
|
+
return this.hasSpoken;
|
|
4101
4104
|
}
|
|
4102
|
-
// Whether the turn has committed its
|
|
4103
|
-
// whether a marker is owed.
|
|
4105
|
+
// Whether the turn has committed its last words.
|
|
4104
4106
|
get finalEmitted() {
|
|
4105
4107
|
return this.emittedFinal;
|
|
4106
4108
|
}
|
|
4109
|
+
// The `seq` of the last words, for the settle to declare — or null when the
|
|
4110
|
+
// span ended without any. Nothing is put in their place.
|
|
4111
|
+
get lastMessageSeq() {
|
|
4112
|
+
return this.lastSeq;
|
|
4113
|
+
}
|
|
4107
4114
|
// The turn's final reply text, or null if none landed (failure / cancel /
|
|
4108
4115
|
// empty). Used for the dashboard feed's reply preview.
|
|
4109
4116
|
get replyBody() {
|
|
@@ -4572,7 +4579,11 @@ function sdkEvents(queryFn, req, signal, built, degraded) {
|
|
|
4572
4579
|
degraded
|
|
4573
4580
|
});
|
|
4574
4581
|
}
|
|
4575
|
-
async function
|
|
4582
|
+
async function freshRequest(req, reason, hooks) {
|
|
4583
|
+
const recomposed = hooks?.fresh ? await hooks.fresh(reason) : null;
|
|
4584
|
+
return recomposed ? { req: { ...req, ...recomposed, session: null }, degraded: false } : { req: { ...req, session: null }, degraded: true };
|
|
4585
|
+
}
|
|
4586
|
+
async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOptions, onWarn, degraded, hooks) {
|
|
4576
4587
|
let emitted = false;
|
|
4577
4588
|
for await (const event of sdkEvents(queryFn, req, signal, initial, degraded)) {
|
|
4578
4589
|
const recoverResume = initial.resume !== null && !emitted && event.type === "result" && !event.ok && event.reason === "session_start_failed" && event.mcpInventory?.initReceived === false;
|
|
@@ -4586,9 +4597,10 @@ async function* runWithResumeRecovery(queryFn, req, signal, initial, augmentOpti
|
|
|
4586
4597
|
discardedSessionId: initial.resume
|
|
4587
4598
|
}
|
|
4588
4599
|
);
|
|
4589
|
-
const
|
|
4590
|
-
|
|
4591
|
-
|
|
4600
|
+
const fresh = await freshRequest(req, "session_start_failed", hooks);
|
|
4601
|
+
if (signal.aborted) return;
|
|
4602
|
+
const options = buildClaudeCodeOptions(fresh.req, augmentOptions);
|
|
4603
|
+
yield* sdkEvents(queryFn, fresh.req, signal, options, fresh.degraded);
|
|
4592
4604
|
return;
|
|
4593
4605
|
}
|
|
4594
4606
|
emitted = true;
|
|
@@ -4601,23 +4613,32 @@ function createClaudeCodeAdapter(deps = {}) {
|
|
|
4601
4613
|
name: "claude-code",
|
|
4602
4614
|
promptAddendum: () => CLAUDE_CODE_ADDENDUM,
|
|
4603
4615
|
dialectSchema: claudeCodeDialectSchema,
|
|
4604
|
-
runTurn(
|
|
4605
|
-
|
|
4606
|
-
|
|
4616
|
+
async *runTurn(request, signal, hooks) {
|
|
4617
|
+
let req = request;
|
|
4618
|
+
let built = buildClaudeCodeOptions(req, deps.augmentOptions);
|
|
4619
|
+
if (built.freshReason === "cwd_mismatch") {
|
|
4607
4620
|
deps.onWarn?.(
|
|
4608
4621
|
"claude-code adapter: stored session cwd no longer matches the current environment \u2014 starting a fresh session (SJ527 guard)",
|
|
4609
4622
|
{ cwd: req.local.cwd }
|
|
4610
4623
|
);
|
|
4611
4624
|
}
|
|
4612
|
-
|
|
4613
|
-
|
|
4625
|
+
let degraded = false;
|
|
4626
|
+
if (built.freshReason !== void 0 && built.freshReason !== "no_session") {
|
|
4627
|
+
const fresh = await freshRequest(req, built.freshReason, hooks);
|
|
4628
|
+
if (signal.aborted) return;
|
|
4629
|
+
req = fresh.req;
|
|
4630
|
+
degraded = fresh.degraded;
|
|
4631
|
+
built = buildClaudeCodeOptions(req, deps.augmentOptions);
|
|
4632
|
+
}
|
|
4633
|
+
yield* runWithResumeRecovery(
|
|
4614
4634
|
queryFn,
|
|
4615
4635
|
req,
|
|
4616
4636
|
signal,
|
|
4617
|
-
|
|
4637
|
+
built,
|
|
4618
4638
|
deps.augmentOptions,
|
|
4619
4639
|
deps.onWarn,
|
|
4620
|
-
degraded
|
|
4640
|
+
degraded,
|
|
4641
|
+
hooks
|
|
4621
4642
|
);
|
|
4622
4643
|
}
|
|
4623
4644
|
};
|
|
@@ -5779,7 +5800,8 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
5779
5800
|
// argument went with the named tool surface it selected against.
|
|
5780
5801
|
promptAddendum: () => OPENCODE_ADDENDUM,
|
|
5781
5802
|
dialectSchema: opencodeDialectSchema,
|
|
5782
|
-
async *runTurn(
|
|
5803
|
+
async *runTurn(request, signal, hooks) {
|
|
5804
|
+
let req = request;
|
|
5783
5805
|
if (!transport) {
|
|
5784
5806
|
yield {
|
|
5785
5807
|
type: "result",
|
|
@@ -5797,7 +5819,15 @@ function createOpencodeAdapter(deps = {}) {
|
|
|
5797
5819
|
{ directory }
|
|
5798
5820
|
);
|
|
5799
5821
|
}
|
|
5800
|
-
|
|
5822
|
+
let degraded = "fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session";
|
|
5823
|
+
if ("fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session" && hooks?.fresh) {
|
|
5824
|
+
const recomposed = await hooks.fresh(decision.reason);
|
|
5825
|
+
if (signal.aborted) return;
|
|
5826
|
+
if (recomposed) {
|
|
5827
|
+
req = { ...req, ...recomposed, session: null };
|
|
5828
|
+
degraded = false;
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5801
5831
|
const spec = buildRunSpec(req, resume);
|
|
5802
5832
|
const result = await transport.run(spec, signal);
|
|
5803
5833
|
if (signal.aborted) return;
|
|
@@ -6804,7 +6834,8 @@ function createCodexAdapter(deps = {}) {
|
|
|
6804
6834
|
// argument went with the named tool surface it selected against.
|
|
6805
6835
|
promptAddendum: () => CODEX_ADDENDUM,
|
|
6806
6836
|
dialectSchema: codexDialectSchema,
|
|
6807
|
-
async *runTurn(
|
|
6837
|
+
async *runTurn(request, signal, hooks) {
|
|
6838
|
+
let req = request;
|
|
6808
6839
|
if (!transport) {
|
|
6809
6840
|
yield {
|
|
6810
6841
|
type: "result",
|
|
@@ -6823,7 +6854,15 @@ function createCodexAdapter(deps = {}) {
|
|
|
6823
6854
|
{ directory }
|
|
6824
6855
|
);
|
|
6825
6856
|
}
|
|
6826
|
-
|
|
6857
|
+
let degraded = "fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session";
|
|
6858
|
+
if ("fresh" in decision && decision.reason !== void 0 && decision.reason !== "no_session" && hooks?.fresh) {
|
|
6859
|
+
const recomposed = await hooks.fresh(decision.reason);
|
|
6860
|
+
if (signal.aborted) return;
|
|
6861
|
+
if (recomposed) {
|
|
6862
|
+
req = { ...req, ...recomposed, session: null };
|
|
6863
|
+
degraded = false;
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6827
6866
|
let instructions = null;
|
|
6828
6867
|
if (!req.policy.hostFs) {
|
|
6829
6868
|
if (!deps.writeInstructionsFile) {
|
|
@@ -7865,14 +7904,11 @@ var MIN_ASK_OPTIONS = 2;
|
|
|
7865
7904
|
var MAX_ASK_OPTIONS = 4;
|
|
7866
7905
|
var MAX_WAKE_NOTE_BYTES = 2e3;
|
|
7867
7906
|
var MAX_WITHDRAW_REASON_BYTES = 400;
|
|
7868
|
-
var MAX_SEND_MESSAGE_BYTES = 65536;
|
|
7869
7907
|
var MAX_SKIP_REASON_BYTES = 500;
|
|
7870
7908
|
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
7871
|
-
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
|
|
7909
|
+
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.`,
|
|
7872
7910
|
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.*\`).`,
|
|
7873
|
-
|
|
7874
|
-
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.`,
|
|
7875
|
-
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.`,
|
|
7911
|
+
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.`,
|
|
7876
7912
|
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.`,
|
|
7877
7913
|
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.`
|
|
7878
7914
|
};
|
|
@@ -7891,13 +7927,6 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
7891
7927
|
"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.`
|
|
7892
7928
|
},
|
|
7893
7929
|
cancel_wake: {},
|
|
7894
|
-
reply_to: {
|
|
7895
|
-
messageId: "The owed ask message id from this turn context."
|
|
7896
|
-
},
|
|
7897
|
-
send: {
|
|
7898
|
-
agentId: "The peer agent to address \u2014 a workspace agent id, from your turn context's roster.",
|
|
7899
|
-
message: `The complete new request the peer should receive and act on. Up to ${MAX_SEND_MESSAGE_BYTES} characters.`
|
|
7900
|
-
},
|
|
7901
7930
|
skip_turn: {
|
|
7902
7931
|
reason: `Short reason you are declining \u2014 used for telemetry/debugging, never posted. Up to ${MAX_SKIP_REASON_BYTES} characters.`
|
|
7903
7932
|
},
|
|
@@ -7915,34 +7944,20 @@ var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
|
7915
7944
|
// src/turn-control-tools.ts
|
|
7916
7945
|
import { z as z13 } from "zod";
|
|
7917
7946
|
var COMPANION_LOCAL_MCP_SERVER = "cabane_companion";
|
|
7918
|
-
var SEND_TOOL = "send";
|
|
7919
|
-
var SEND_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SEND_TOOL}`;
|
|
7920
7947
|
var COMPANION_LOCAL_TOOL_GLOB = `mcp__${COMPANION_LOCAL_MCP_SERVER}__*`;
|
|
7921
7948
|
var SKIP_TURN_TOOL = "skip_turn";
|
|
7922
7949
|
var ASK_TOOL = "ask";
|
|
7923
7950
|
var ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${ASK_TOOL}`;
|
|
7924
7951
|
var WITHDRAW_ASK_TOOL = "withdraw_ask";
|
|
7925
7952
|
var WITHDRAW_ASK_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WITHDRAW_ASK_TOOL}`;
|
|
7926
|
-
var REPLY_TO_TOOL = "reply_to";
|
|
7927
|
-
var REPLY_TO_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${REPLY_TO_TOOL}`;
|
|
7928
7953
|
var WAKE_ME_TOOL = "wake_me";
|
|
7929
7954
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
7930
7955
|
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
7931
7956
|
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
7932
7957
|
var TURN_ACT_TIMEOUT_MS = 45e3;
|
|
7933
|
-
function createReplyState() {
|
|
7934
|
-
return { answersMessageId: null };
|
|
7935
|
-
}
|
|
7936
7958
|
function createSkipState() {
|
|
7937
7959
|
return { skipped: false, reason: null };
|
|
7938
7960
|
}
|
|
7939
|
-
function resolveDeclaredReplyField(input) {
|
|
7940
|
-
const explicit = input.replyState.answersMessageId;
|
|
7941
|
-
if (explicit) return { answersMessageId: explicit };
|
|
7942
|
-
if (input.kind !== "final") return {};
|
|
7943
|
-
if (!input.owedReplyMessageId) return {};
|
|
7944
|
-
return { answersMessageId: input.owedReplyMessageId, answersAutoDeclared: true };
|
|
7945
|
-
}
|
|
7946
7961
|
function toolError(text) {
|
|
7947
7962
|
return { isError: true, content: [{ type: "text", text }] };
|
|
7948
7963
|
}
|
|
@@ -7972,7 +7987,7 @@ async function performAct(acts, act) {
|
|
|
7972
7987
|
if (!result.ok) return toolError(JSON.stringify(result));
|
|
7973
7988
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
7974
7989
|
}
|
|
7975
|
-
function createTurnControlMcpServer(acts, skipState
|
|
7990
|
+
function createTurnControlMcpServer(acts, skipState) {
|
|
7976
7991
|
const ACT_TOOL = { readOnlyHint: false, destructiveHint: false, openWorldHint: false };
|
|
7977
7992
|
const CANCEL_TOOL = { ...ACT_TOOL, idempotentHint: true };
|
|
7978
7993
|
const RECORDER_TOOL = { readOnlyHint: true, openWorldHint: false };
|
|
@@ -7980,42 +7995,6 @@ function createTurnControlMcpServer(acts, skipState, replyState) {
|
|
|
7980
7995
|
name: COMPANION_LOCAL_MCP_SERVER,
|
|
7981
7996
|
version: "0.0.0",
|
|
7982
7997
|
tools: [
|
|
7983
|
-
tool(
|
|
7984
|
-
SEND_TOOL,
|
|
7985
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.send,
|
|
7986
|
-
{
|
|
7987
|
-
agentId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.send.agentId),
|
|
7988
|
-
message: z13.string().min(1).max(MAX_SEND_MESSAGE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.send.message)
|
|
7989
|
-
},
|
|
7990
|
-
async (args) => performAct(acts, {
|
|
7991
|
-
kind: "send",
|
|
7992
|
-
callId: randomUUID(),
|
|
7993
|
-
agentId: args.agentId,
|
|
7994
|
-
message: args.message
|
|
7995
|
-
}),
|
|
7996
|
-
{ annotations: ACT_TOOL, alwaysLoad: true }
|
|
7997
|
-
),
|
|
7998
|
-
...replyState ? [
|
|
7999
|
-
tool(
|
|
8000
|
-
REPLY_TO_TOOL,
|
|
8001
|
-
TURN_CONTROL_TOOL_DESCRIPTIONS.reply_to,
|
|
8002
|
-
{
|
|
8003
|
-
messageId: z13.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.reply_to.messageId)
|
|
8004
|
-
},
|
|
8005
|
-
async (args) => {
|
|
8006
|
-
replyState.answersMessageId = args.messageId;
|
|
8007
|
-
return {
|
|
8008
|
-
content: [
|
|
8009
|
-
{
|
|
8010
|
-
type: "text",
|
|
8011
|
-
text: JSON.stringify({ answersMessageId: args.messageId })
|
|
8012
|
-
}
|
|
8013
|
-
]
|
|
8014
|
-
};
|
|
8015
|
-
},
|
|
8016
|
-
{ annotations: RECORDER_TOOL, alwaysLoad: true }
|
|
8017
|
-
)
|
|
8018
|
-
] : [],
|
|
8019
7998
|
...skipState ? [
|
|
8020
7999
|
tool(
|
|
8021
8000
|
SKIP_TURN_TOOL,
|
|
@@ -8542,8 +8521,7 @@ var TurnCommitter = class {
|
|
|
8542
8521
|
kind,
|
|
8543
8522
|
turnId: deps.turnId,
|
|
8544
8523
|
seq,
|
|
8545
|
-
parentMessageId: deps.parentMessageId
|
|
8546
|
-
...kind === "final" ? this.turnControlFields("final") : {}
|
|
8524
|
+
parentMessageId: deps.parentMessageId
|
|
8547
8525
|
},
|
|
8548
8526
|
deps.signal
|
|
8549
8527
|
);
|
|
@@ -8584,19 +8562,22 @@ var TurnCommitter = class {
|
|
|
8584
8562
|
{ turnId: deps.turnId, text, seq },
|
|
8585
8563
|
deps.signal
|
|
8586
8564
|
);
|
|
8565
|
+
},
|
|
8566
|
+
reportText: async ({ text, seq }) => {
|
|
8567
|
+
await deps.api.reportText(
|
|
8568
|
+
deps.workspaceId,
|
|
8569
|
+
deps.conversationId,
|
|
8570
|
+
deps.agentId,
|
|
8571
|
+
{ turnId: deps.turnId, text, seq },
|
|
8572
|
+
deps.signal
|
|
8573
|
+
);
|
|
8587
8574
|
}
|
|
8588
8575
|
};
|
|
8589
8576
|
this.pump = new TurnPump({
|
|
8590
8577
|
commit,
|
|
8591
8578
|
signal: deps.signal,
|
|
8592
8579
|
nextSeq: deps.nextSeq,
|
|
8593
|
-
|
|
8594
|
-
deps.log.warn(
|
|
8595
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
8596
|
-
"dispatcher: progress-promotion commit failed"
|
|
8597
|
-
);
|
|
8598
|
-
deps.onCommitFailed?.(err);
|
|
8599
|
-
}
|
|
8580
|
+
...deps.spoke !== void 0 ? { spoke: deps.spoke } : {}
|
|
8600
8581
|
});
|
|
8601
8582
|
this.emit = sinkEmitter(this.pump.sink, this.onError);
|
|
8602
8583
|
}
|
|
@@ -8620,15 +8601,17 @@ var TurnCommitter = class {
|
|
|
8620
8601
|
this.onError(err, event.type);
|
|
8621
8602
|
}
|
|
8622
8603
|
}
|
|
8623
|
-
//
|
|
8624
|
-
//
|
|
8625
|
-
//
|
|
8626
|
-
//
|
|
8627
|
-
//
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8604
|
+
// A message has landed under this span — its first words at least. A span
|
|
8605
|
+
// that spoke settles; one that never did is a pass. Nothing is promoted at
|
|
8606
|
+
// the end of a turn: the held-text flush is the adapter's (it emits the
|
|
8607
|
+
// closing reply as a terminal `text` event before `result`), and a span
|
|
8608
|
+
// that ended on working text ends without last words.
|
|
8609
|
+
get spoke() {
|
|
8610
|
+
return this.pump.spoke;
|
|
8611
|
+
}
|
|
8612
|
+
// The `seq` of the last words, for the settle to declare; null when none.
|
|
8613
|
+
get lastMessageSeq() {
|
|
8614
|
+
return this.pump.lastMessageSeq;
|
|
8632
8615
|
}
|
|
8633
8616
|
// The turn's final reply text, or null if none landed.
|
|
8634
8617
|
get replyBody() {
|
|
@@ -8640,26 +8623,6 @@ var TurnCommitter = class {
|
|
|
8640
8623
|
get finalEmitted() {
|
|
8641
8624
|
return this.pump.finalEmitted;
|
|
8642
8625
|
}
|
|
8643
|
-
// A closing textual reply and a wordless terminal marker carry the same
|
|
8644
|
-
// declared-reply projection. The KIND matters: the auto-declared reply binds
|
|
8645
|
-
// only a textual `final` — a wordless turn has no answer to bind, and the
|
|
8646
|
-
// server's mute-settle notice speaks for it.
|
|
8647
|
-
turnControlFields(kind) {
|
|
8648
|
-
return this.answersField(kind);
|
|
8649
|
-
}
|
|
8650
|
-
// The declared reply: explicit `reply_to` first, else the runtime's own
|
|
8651
|
-
// declaration for the turn that just answers — see
|
|
8652
|
-
// `resolveDeclaredReplyField` for the whole rule and its reasons. CT1224: an
|
|
8653
|
-
// auto-declaration is flagged as such on the wire, because the server drops
|
|
8654
|
-
// it when the turn performed an outward act (CT1354: any of them, read off
|
|
8655
|
-
// the ledger). An explicit `reply_to` carries no flag and always stands.
|
|
8656
|
-
answersField(kind) {
|
|
8657
|
-
return resolveDeclaredReplyField({
|
|
8658
|
-
kind,
|
|
8659
|
-
owedReplyMessageId: this.deps.owedReplyMessageId,
|
|
8660
|
-
replyState: this.deps.replyState
|
|
8661
|
-
});
|
|
8662
|
-
}
|
|
8663
8626
|
};
|
|
8664
8627
|
|
|
8665
8628
|
// src/turn-runtime-integrity.ts
|
|
@@ -8673,7 +8636,7 @@ async function postIncompleteNotice(ctx, harness, resolution) {
|
|
|
8673
8636
|
try {
|
|
8674
8637
|
await ctx.postTurnMessage(ctx.workspaceId, ctx.conversationId, {
|
|
8675
8638
|
body: turnIncompleteCopy(harness),
|
|
8676
|
-
kind: "
|
|
8639
|
+
kind: "runtime_notice",
|
|
8677
8640
|
turnId: ctx.turnId,
|
|
8678
8641
|
parentMessageId: ctx.parentMessageId
|
|
8679
8642
|
});
|
|
@@ -8742,12 +8705,9 @@ async function absorbBundledLoss(ctx, aborted, outcome) {
|
|
|
8742
8705
|
|
|
8743
8706
|
// src/turn-execution.ts
|
|
8744
8707
|
var PREPARING_TOOL_NAME = "preparing";
|
|
8745
|
-
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:";
|
|
8746
8708
|
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:";
|
|
8747
8709
|
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:";
|
|
8748
8710
|
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`;
|
|
8749
|
-
var SKIPPED_MARKER_BODY = "(skipped)";
|
|
8750
|
-
var SILENT_MARKER_BODY = "(no reply)";
|
|
8751
8711
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
8752
8712
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
8753
8713
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
@@ -8768,7 +8728,6 @@ function initialOutcome() {
|
|
|
8768
8728
|
contentBearingEvents: 0,
|
|
8769
8729
|
latestSessionState: null,
|
|
8770
8730
|
settledDiagnostics: null,
|
|
8771
|
-
silentMarkerEmitted: false,
|
|
8772
8731
|
timeoutReason: null,
|
|
8773
8732
|
leaseLost: false,
|
|
8774
8733
|
runtimeIncomplete: false
|
|
@@ -8817,13 +8776,15 @@ var TurnExecution = class {
|
|
|
8817
8776
|
// `turnId` is set on every dispatch. `resumedFromSeq` is what it seeded from.
|
|
8818
8777
|
resumed;
|
|
8819
8778
|
resumedFromSeq;
|
|
8779
|
+
// Whether the span this resumed run continues already has first words —
|
|
8780
|
+
// from the server (`turnSpoke`) or from speech still queued in the outbox.
|
|
8781
|
+
resumedSpoke = false;
|
|
8820
8782
|
turnContext;
|
|
8821
8783
|
resolvedMcpServers;
|
|
8822
8784
|
effectiveCwd;
|
|
8823
8785
|
hookEnv;
|
|
8824
8786
|
turnEnv;
|
|
8825
8787
|
skipState;
|
|
8826
|
-
replyState;
|
|
8827
8788
|
turnControlServer;
|
|
8828
8789
|
request;
|
|
8829
8790
|
adapter;
|
|
@@ -8841,9 +8802,9 @@ var TurnExecution = class {
|
|
|
8841
8802
|
disarmWatchdogs = () => {
|
|
8842
8803
|
};
|
|
8843
8804
|
// Codo's stack review, blocking finding #2: set the moment `acquireLease`'s
|
|
8844
|
-
// PATCH returns — the
|
|
8845
|
-
//
|
|
8846
|
-
//
|
|
8805
|
+
// PATCH returns — the lease is granted and this is the pair's running span,
|
|
8806
|
+
// so every exit after this point must settle THE SPAN, not just clear the
|
|
8807
|
+
// participant flag.
|
|
8847
8808
|
admitted = false;
|
|
8848
8809
|
concluded(reason, errorReason) {
|
|
8849
8810
|
return new TurnConcluded(reason, errorReason);
|
|
@@ -8881,14 +8842,16 @@ var TurnExecution = class {
|
|
|
8881
8842
|
return this.report();
|
|
8882
8843
|
}
|
|
8883
8844
|
async concludeBeforeRun(reason, errorReason) {
|
|
8884
|
-
const { payload, turnLog, startedAt } = this;
|
|
8885
|
-
const body = {
|
|
8886
|
-
activeRunStartedAt: null,
|
|
8887
|
-
// CT1046: this dispatch is over before it ran — retire its evidence so it
|
|
8888
|
-
// can't authorize a turn-token mint afterwards.
|
|
8889
|
-
settledMessageId: payload.messageId
|
|
8890
|
-
};
|
|
8845
|
+
const { payload, turnLog, startedAt, turnId } = this;
|
|
8846
|
+
const body = { activeRunStartedAt: null, turnId, outcome: "failed" };
|
|
8891
8847
|
if (errorReason) body.errorReason = errorReason.slice(0, 200);
|
|
8848
|
+
const context = this.turnContext;
|
|
8849
|
+
if (context?.readThrough !== void 0) {
|
|
8850
|
+
body.readThrough = context.readThrough;
|
|
8851
|
+
if (context.readRevision !== void 0) body.readRevision = context.readRevision;
|
|
8852
|
+
} else {
|
|
8853
|
+
body.readThrough = payload.messageId;
|
|
8854
|
+
}
|
|
8892
8855
|
try {
|
|
8893
8856
|
await this.opts.api.setActiveRun(
|
|
8894
8857
|
this.opts.workspaceId,
|
|
@@ -8907,21 +8870,18 @@ var TurnExecution = class {
|
|
|
8907
8870
|
return { ok: false, durationMs, reason };
|
|
8908
8871
|
}
|
|
8909
8872
|
// The post-admission analogue of `concludeBeforeRun` (Codo's stack review,
|
|
8910
|
-
// blocking finding #2): this turn WAS admitted —
|
|
8911
|
-
//
|
|
8912
|
-
//
|
|
8913
|
-
//
|
|
8914
|
-
//
|
|
8915
|
-
//
|
|
8916
|
-
// the turn
|
|
8917
|
-
// `final` before concluding, so the turn genuinely produced its terminal
|
|
8918
|
-
// output and `settled` releases the queue honestly.
|
|
8873
|
+
// blocking finding #2): this turn WAS admitted — it holds the conversation's
|
|
8874
|
+
// lease — so the clear must NAME the turn and DECLARE its terminal outcome.
|
|
8875
|
+
// A bare pre-run clear here would reset the participant flag while the span
|
|
8876
|
+
// stayed running until the lost sweep. `outcome` follows the settle wire's
|
|
8877
|
+
// rule: a failure names its reason; without one the turn settled — the
|
|
8878
|
+
// runtime-unavailable exit posts its user-facing `final` before concluding,
|
|
8879
|
+
// so the turn genuinely produced its terminal output.
|
|
8919
8880
|
async concludeAdmittedRun(reason, errorReason) {
|
|
8920
8881
|
const { payload, turnLog, startedAt, turnId } = this;
|
|
8921
8882
|
const body = {
|
|
8922
8883
|
activeRunStartedAt: null,
|
|
8923
8884
|
turnId,
|
|
8924
|
-
settledMessageId: payload.messageId,
|
|
8925
8885
|
// CT1379: `runtimeIncomplete` is a real failure that deliberately carries
|
|
8926
8886
|
// no `errorReason`, so the inference alone would read it as settled.
|
|
8927
8887
|
outcome: errorReason || this.outcome.runtimeIncomplete ? "failed" : "settled"
|
|
@@ -8947,6 +8907,7 @@ var TurnExecution = class {
|
|
|
8947
8907
|
async fetchContext() {
|
|
8948
8908
|
const { payload, turnId, turnLog } = this;
|
|
8949
8909
|
const outboxFloor = this.resumed ? readOutboxFloor((id) => this.opts.api.outboxMaxSeqForTurn(id), turnId, turnLog) : 0;
|
|
8910
|
+
const outboxSpoke = this.resumed && this.opts.api.outboxHasSpeechForTurn(turnId);
|
|
8950
8911
|
let turnContext;
|
|
8951
8912
|
try {
|
|
8952
8913
|
turnContext = await this.opts.api.getTurnContext(
|
|
@@ -8974,12 +8935,14 @@ var TurnExecution = class {
|
|
|
8974
8935
|
const sources = { serverFloor: turnContext.turnSeqFloor, outboxFloor };
|
|
8975
8936
|
this.resumedFromSeq = resolveSeqFloor(sources, { turnId, log: turnLog });
|
|
8976
8937
|
this.seqCounter = Math.max(this.seqCounter, this.resumedFromSeq);
|
|
8938
|
+
this.resumedSpoke = turnContext.turnSpoke === true || outboxSpoke;
|
|
8939
|
+
if (this.resumedSpoke) turnLog.info({ turnId }, "dispatcher: resumed span already spoke");
|
|
8977
8940
|
}
|
|
8978
8941
|
}
|
|
8979
8942
|
gateTrigger() {
|
|
8980
8943
|
const { payload, turnLog } = this;
|
|
8981
8944
|
const message = this.turnContext.message;
|
|
8982
|
-
const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system"
|
|
8945
|
+
const isDispatchableTrigger = message.role === "user" || message.role === "agent" || message.role === "system";
|
|
8983
8946
|
if (!isDispatchableTrigger) {
|
|
8984
8947
|
turnLog.warn({ role: message.role }, "dispatcher: trigger role not dispatchable \u2014 skipping");
|
|
8985
8948
|
throw this.concluded("unexpected_role", UNEXPECTED_ROLE_REASON);
|
|
@@ -9005,7 +8968,7 @@ var TurnExecution = class {
|
|
|
9005
8968
|
try {
|
|
9006
8969
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
9007
8970
|
body: `${MISSING_SECRET_PREFIX} ${list}`,
|
|
9008
|
-
kind: "
|
|
8971
|
+
kind: "runtime_notice",
|
|
9009
8972
|
turnId: randomUUID2(),
|
|
9010
8973
|
// CT113: even a turn that fails before it runs answers a message.
|
|
9011
8974
|
parentMessageId: payload.messageId
|
|
@@ -9035,32 +8998,6 @@ var TurnExecution = class {
|
|
|
9035
8998
|
effectiveCwd = void 0;
|
|
9036
8999
|
}
|
|
9037
9000
|
let hookEnv;
|
|
9038
|
-
const triggerIsPrepareFailure = this.turnContext.message.body.startsWith(PREPARE_FAILED_PREFIX);
|
|
9039
|
-
const sendPrepareFailureHandback = async () => {
|
|
9040
|
-
if (!this.turnContext.dispatchedByAgentId || triggerIsPrepareFailure) return;
|
|
9041
|
-
try {
|
|
9042
|
-
await this.opts.api.postAddressedMessage(workspaceId, payload.conversationId, {
|
|
9043
|
-
body: `${PREPARE_FAILED_PREFIX}
|
|
9044
|
-
|
|
9045
|
-
The dispatched turn could not start. Re-dispatch it after repairing the preparation failure shown in this conversation.`,
|
|
9046
|
-
dispatch: this.turnContext.dispatchedByAgentId,
|
|
9047
|
-
// Keyed on the TRIGGER, not the turn: a redelivered dispatch event is
|
|
9048
|
-
// the replay this has to survive, and it mints a fresh `turnId` while
|
|
9049
|
-
// carrying the same `messageId`. One handback per failed pickup of a
|
|
9050
|
-
// given trigger — which is also CT992's rule, that the first failure
|
|
9051
|
-
// is the one carrying information. A genuine re-dispatch after a
|
|
9052
|
-
// repair authors a NEW trigger message, so it keys differently and
|
|
9053
|
-
// hands back again.
|
|
9054
|
-
idempotencyKey: `prepare-failure-handback:${payload.messageId}`,
|
|
9055
|
-
parentMessageId: payload.messageId
|
|
9056
|
-
});
|
|
9057
|
-
} catch (err) {
|
|
9058
|
-
turnLog.warn(
|
|
9059
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
9060
|
-
"dispatcher: prepare-failure handback failed"
|
|
9061
|
-
);
|
|
9062
|
-
}
|
|
9063
|
-
};
|
|
9064
9001
|
if (prepareHook) {
|
|
9065
9002
|
let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
9066
9003
|
if (cached2 && !checkoutState(cached2.cwd).ok) {
|
|
@@ -9092,23 +9029,8 @@ The dispatched turn could not start. Re-dispatch it after repairing the preparat
|
|
|
9092
9029
|
} catch (err) {
|
|
9093
9030
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9094
9031
|
turnLog.error({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
body: `${PREPARE_FAILED_PREFIX}
|
|
9098
|
-
|
|
9099
|
-
${reason}`,
|
|
9100
|
-
kind: "final",
|
|
9101
|
-
turnId,
|
|
9102
|
-
parentMessageId: payload.messageId
|
|
9103
|
-
});
|
|
9104
|
-
} catch (postErr) {
|
|
9105
|
-
turnLog.warn(
|
|
9106
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
9107
|
-
"dispatcher: prepare-rejection post failed"
|
|
9108
|
-
);
|
|
9109
|
-
}
|
|
9110
|
-
await sendPrepareFailureHandback();
|
|
9111
|
-
throw this.concluded(`prepare_failed: ${reason}`);
|
|
9032
|
+
const failReason = `prepare_failed: ${reason}`;
|
|
9033
|
+
throw this.concluded(failReason, failReason);
|
|
9112
9034
|
}
|
|
9113
9035
|
}
|
|
9114
9036
|
} else {
|
|
@@ -9167,25 +9089,8 @@ ${reason}`,
|
|
|
9167
9089
|
if (preparingStarted) reportPreparing("error");
|
|
9168
9090
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9169
9091
|
turnLog.error({ err: reason }, "dispatcher: prepare hook failed");
|
|
9170
|
-
try {
|
|
9171
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
9172
|
-
body: `${PREPARE_FAILED_PREFIX}
|
|
9173
|
-
|
|
9174
|
-
${reason}`,
|
|
9175
|
-
kind: "final",
|
|
9176
|
-
turnId,
|
|
9177
|
-
// CT113: stamp the parent even on the prepare-failed close.
|
|
9178
|
-
parentMessageId: payload.messageId
|
|
9179
|
-
});
|
|
9180
|
-
} catch (postErr) {
|
|
9181
|
-
turnLog.warn(
|
|
9182
|
-
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
9183
|
-
"dispatcher: prepare-failure post failed"
|
|
9184
|
-
);
|
|
9185
|
-
}
|
|
9186
|
-
await sendPrepareFailureHandback();
|
|
9187
9092
|
const failReason = `prepare_failed: ${reason}`;
|
|
9188
|
-
throw this.concluded(failReason);
|
|
9093
|
+
throw this.concluded(failReason, failReason);
|
|
9189
9094
|
}
|
|
9190
9095
|
}
|
|
9191
9096
|
}
|
|
@@ -9212,7 +9117,12 @@ ${reason}`,
|
|
|
9212
9117
|
// already emitted a "preparing" activity row for this turn above (it
|
|
9213
9118
|
// runs before this flip), which would otherwise make the sweep mistake
|
|
9214
9119
|
// this live turn for an abandoned one and close it with a `stopped`.
|
|
9215
|
-
turnId
|
|
9120
|
+
turnId,
|
|
9121
|
+
// The newest message this turn's read covered: the lease records it,
|
|
9122
|
+
// and settle moves the cursor to it. Omitted when the server sent
|
|
9123
|
+
// none (an older API), so the cursor stays where it was.
|
|
9124
|
+
...this.turnContext.readThrough !== void 0 ? { readThrough: this.turnContext.readThrough } : {},
|
|
9125
|
+
...this.turnContext.readRevision !== void 0 ? { readRevision: this.turnContext.readRevision } : {}
|
|
9216
9126
|
});
|
|
9217
9127
|
} catch (err) {
|
|
9218
9128
|
const refusal = leaseRefusal(err);
|
|
@@ -9235,11 +9145,9 @@ ${reason}`,
|
|
|
9235
9145
|
buildRequest() {
|
|
9236
9146
|
const { payload, workspaceId } = this;
|
|
9237
9147
|
const skipState = this.skipState = createSkipState();
|
|
9238
|
-
const replyState = this.replyState = createReplyState();
|
|
9239
9148
|
const turnControlServer = createTurnControlMcpServer(
|
|
9240
9149
|
{ api: this.opts.api, workspaceId, turnId: this.turnId },
|
|
9241
|
-
skipState
|
|
9242
|
-
replyState
|
|
9150
|
+
skipState
|
|
9243
9151
|
);
|
|
9244
9152
|
this.request = buildCompanionTurnRequest({
|
|
9245
9153
|
turnContext: this.turnContext,
|
|
@@ -9308,7 +9216,7 @@ ${reason}`,
|
|
|
9308
9216
|
try {
|
|
9309
9217
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
9310
9218
|
body: `${RUNTIME_UNAVAILABLE_PREFIX} ${err.message}`,
|
|
9311
|
-
kind: "
|
|
9219
|
+
kind: "runtime_notice",
|
|
9312
9220
|
turnId,
|
|
9313
9221
|
parentMessageId: payload.messageId
|
|
9314
9222
|
});
|
|
@@ -9323,6 +9231,43 @@ ${reason}`,
|
|
|
9323
9231
|
throw this.concluded(`runtime_unavailable:${err.runtime}`);
|
|
9324
9232
|
}
|
|
9325
9233
|
}
|
|
9234
|
+
// The adapter refused to resume this pair's stored session and is starting a
|
|
9235
|
+
// fresh one. The request it holds was composed for a session that had the
|
|
9236
|
+
// conversation so far — the read is what landed since the cursor, the
|
|
9237
|
+
// agent's own words left out because the session held them. A fresh session
|
|
9238
|
+
// holds none of it, so fetch the context again as a first turn on an empty
|
|
9239
|
+
// session (the whole conversation, own words included, the first-turn
|
|
9240
|
+
// material) and hand the adapter that to run on — before it speaks or acts.
|
|
9241
|
+
// The lease already granted stands; the read reaches at least its mark. A
|
|
9242
|
+
// fetch that fails returns null and the adapter falls back to the request
|
|
9243
|
+
// it has, flagged degraded, so the server rewinds the cursor at settle.
|
|
9244
|
+
async freshRequest(reason) {
|
|
9245
|
+
const { payload, turnId, turnLog } = this;
|
|
9246
|
+
try {
|
|
9247
|
+
const context = await this.opts.api.getTurnContext(
|
|
9248
|
+
payload.conversationId,
|
|
9249
|
+
payload.messageId,
|
|
9250
|
+
turnId,
|
|
9251
|
+
false,
|
|
9252
|
+
true
|
|
9253
|
+
);
|
|
9254
|
+
turnLog.warn(
|
|
9255
|
+
{ reason },
|
|
9256
|
+
"dispatcher: stored session not resumed \u2014 the turn context was recomposed for a fresh session"
|
|
9257
|
+
);
|
|
9258
|
+
return {
|
|
9259
|
+
systemPrompt: context.systemPrompt,
|
|
9260
|
+
prompt: context.prompt,
|
|
9261
|
+
content: context.content
|
|
9262
|
+
};
|
|
9263
|
+
} catch (err) {
|
|
9264
|
+
turnLog.error(
|
|
9265
|
+
{ reason, err: err instanceof Error ? err.message : String(err) },
|
|
9266
|
+
"dispatcher: could not recompose the turn context for a fresh session \u2014 running on the incremental one, degraded"
|
|
9267
|
+
);
|
|
9268
|
+
return null;
|
|
9269
|
+
}
|
|
9270
|
+
}
|
|
9326
9271
|
async execute() {
|
|
9327
9272
|
const { payload, workspaceId, turnId, turnLog, startedAt } = this;
|
|
9328
9273
|
const o = this.outcome;
|
|
@@ -9354,10 +9299,7 @@ ${reason}`,
|
|
|
9354
9299
|
signal: abortController.signal,
|
|
9355
9300
|
log: turnLog,
|
|
9356
9301
|
nextSeq: this.nextSeq,
|
|
9357
|
-
|
|
9358
|
-
// The ledger-derived owed reply, for the runtime's own declaration when
|
|
9359
|
-
// the agent doesn't call `reply_to` (resolveDeclaredReplyField).
|
|
9360
|
-
owedReplyMessageId: this.turnContext.owedReplyMessageId ?? null,
|
|
9302
|
+
spoke: this.resumedSpoke,
|
|
9361
9303
|
// CT1292: a commit that didn't land may mean this turn's lease is gone.
|
|
9362
9304
|
// `noteCommitFailed` is a field slot the lease watchdog fills when it
|
|
9363
9305
|
// arms (it starts as a no-op) — the committer only fires it from inside
|
|
@@ -9376,9 +9318,6 @@ ${reason}`,
|
|
|
9376
9318
|
payload.agentId,
|
|
9377
9319
|
turnId
|
|
9378
9320
|
);
|
|
9379
|
-
if (intent.answersMessageId) {
|
|
9380
|
-
this.replyState.answersMessageId = intent.answersMessageId;
|
|
9381
|
-
}
|
|
9382
9321
|
if (intent.skipped) {
|
|
9383
9322
|
this.skipState.skipped = true;
|
|
9384
9323
|
this.skipState.reason = intent.skipReason;
|
|
@@ -9386,7 +9325,7 @@ ${reason}`,
|
|
|
9386
9325
|
} catch (err) {
|
|
9387
9326
|
turnLog.warn(
|
|
9388
9327
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
9389
|
-
"dispatcher: turn-control intent fetch failed; the
|
|
9328
|
+
"dispatcher: turn-control intent fetch failed; the skip for this turn is dropped"
|
|
9390
9329
|
);
|
|
9391
9330
|
}
|
|
9392
9331
|
};
|
|
@@ -9444,7 +9383,9 @@ ${reason}`,
|
|
|
9444
9383
|
clearInterval(leaseTimer);
|
|
9445
9384
|
};
|
|
9446
9385
|
try {
|
|
9447
|
-
for await (const event of this.adapter.runTurn(this.request, abortController.signal
|
|
9386
|
+
for await (const event of this.adapter.runTurn(this.request, abortController.signal, {
|
|
9387
|
+
fresh: (reason) => this.freshRequest(reason)
|
|
9388
|
+
})) {
|
|
9448
9389
|
transcript2?.write(event);
|
|
9449
9390
|
o.eventCounts[event.type] += 1;
|
|
9450
9391
|
if (isContentBearingEvent(event)) o.contentBearingEvents += 1;
|
|
@@ -9529,43 +9470,6 @@ ${reason}`,
|
|
|
9529
9470
|
{ reason: this.skipState.reason, turnId, ok: o.okResult },
|
|
9530
9471
|
"agent skipped turn (skip_turn)"
|
|
9531
9472
|
);
|
|
9532
|
-
try {
|
|
9533
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
9534
|
-
body: SKIPPED_MARKER_BODY,
|
|
9535
|
-
kind: "skipped",
|
|
9536
|
-
turnId,
|
|
9537
|
-
seq: this.nextSeq(),
|
|
9538
|
-
parentMessageId: payload.messageId
|
|
9539
|
-
});
|
|
9540
|
-
} catch (err) {
|
|
9541
|
-
turnLog.warn(
|
|
9542
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
9543
|
-
"dispatcher: skipped-marker commit failed"
|
|
9544
|
-
);
|
|
9545
|
-
}
|
|
9546
|
-
} else {
|
|
9547
|
-
await committer.finalize(o.okResult);
|
|
9548
|
-
if (!abortController.signal.aborted && o.okResult && !committer.finalEmitted) {
|
|
9549
|
-
try {
|
|
9550
|
-
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
9551
|
-
body: SILENT_MARKER_BODY,
|
|
9552
|
-
kind: "silent",
|
|
9553
|
-
turnId,
|
|
9554
|
-
seq: this.nextSeq(),
|
|
9555
|
-
parentMessageId: payload.messageId,
|
|
9556
|
-
// An explicit `reply_to` survives a wordless turn exactly as it
|
|
9557
|
-
// survives a textual final (the auto-declaration does not — a
|
|
9558
|
-
// marker has no answer to bind).
|
|
9559
|
-
...committer.turnControlFields("silent")
|
|
9560
|
-
});
|
|
9561
|
-
o.silentMarkerEmitted = true;
|
|
9562
|
-
} catch (err) {
|
|
9563
|
-
turnLog.warn(
|
|
9564
|
-
{ err: err instanceof Error ? err.message : String(err) },
|
|
9565
|
-
"dispatcher: silent-marker commit failed"
|
|
9566
|
-
);
|
|
9567
|
-
}
|
|
9568
|
-
}
|
|
9569
9473
|
}
|
|
9570
9474
|
} catch (err) {
|
|
9571
9475
|
o.okResult = false;
|
|
@@ -9608,12 +9512,7 @@ ${reason}`,
|
|
|
9608
9512
|
// CT277: the server keys the turn-end metadata UPDATE (ended_at + tokens)
|
|
9609
9513
|
// off this turn id, so the settle PATCH must carry it — the start PATCH
|
|
9610
9514
|
// isn't enough. Always sent (every settle closes a real turn).
|
|
9611
|
-
turnId
|
|
9612
|
-
// CT1046: and the trigger this turn answered, so the server retires the
|
|
9613
|
-
// dispatch that authorized it. Always sent, unlike `lastSeenMessageId`
|
|
9614
|
-
// below (which is a success-only catch-up advance): a failed or cancelled
|
|
9615
|
-
// turn is just as over, and its evidence must die with it.
|
|
9616
|
-
settledMessageId: payload.messageId
|
|
9515
|
+
turnId
|
|
9617
9516
|
};
|
|
9618
9517
|
if (o.turnUsage) {
|
|
9619
9518
|
body.usage = o.turnUsage;
|
|
@@ -9627,10 +9526,8 @@ ${reason}`,
|
|
|
9627
9526
|
if (!o.okResult && o.resultReason && o.resultReason !== "cancelled" && !userCancelled && !o.leaseLost && !o.runtimeIncomplete && !this.skipState.skipped) {
|
|
9628
9527
|
body.errorReason = o.resultReason.slice(0, 200);
|
|
9629
9528
|
}
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
}
|
|
9633
|
-
body.outcome = o.okResult ? "settled" : body.errorReason || o.runtimeIncomplete ? "failed" : "interrupted";
|
|
9529
|
+
body.outcome = o.okResult ? committer.spoke ? "settled" : "passed" : body.errorReason || o.runtimeIncomplete ? "failed" : "interrupted";
|
|
9530
|
+
if (body.outcome === "settled") body.lastMessageSeq = committer.lastMessageSeq;
|
|
9634
9531
|
if (o.sessionDegraded) {
|
|
9635
9532
|
body.degraded = true;
|
|
9636
9533
|
}
|
|
@@ -9647,7 +9544,7 @@ ${reason}`,
|
|
|
9647
9544
|
eventCounts: o.eventCounts,
|
|
9648
9545
|
runtimeResultKind: o.runtimeResultKind,
|
|
9649
9546
|
...o.turnMcpInventory ? { mcpInventory: o.turnMcpInventory } : {},
|
|
9650
|
-
finalSource: outcome === "skipped" || outcome === "cancelled" ||
|
|
9547
|
+
finalSource: outcome === "skipped" || outcome === "cancelled" || !committer.finalEmitted ? "none" : committer.finalSource
|
|
9651
9548
|
};
|
|
9652
9549
|
body.diagnostics = o.settledDiagnostics;
|
|
9653
9550
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
|
@@ -9861,7 +9758,7 @@ function mapOpencodeProviders(json) {
|
|
|
9861
9758
|
const modelId = typeof raw?.id === "string" ? raw.id : null;
|
|
9862
9759
|
const prov = typeof raw?.providerID === "string" ? raw.providerID : providerId;
|
|
9863
9760
|
if (!modelId || !prov) continue;
|
|
9864
|
-
const context = raw.limit && typeof raw.limit === "object" && typeof raw.limit.context === "number" ? raw.limit.context : null;
|
|
9761
|
+
const context = raw.limit && typeof raw.limit === "object" && typeof raw.limit.context === "number" && raw.limit.context > 0 ? raw.limit.context : null;
|
|
9865
9762
|
models.push({
|
|
9866
9763
|
// The namespaced id storage holds + opencode addresses a model by
|
|
9867
9764
|
// (`deepseek/deepseek-chat` → { providerID, modelID }).
|
|
@@ -9941,8 +9838,8 @@ var Outbox = class {
|
|
|
9941
9838
|
this.enforceBounds();
|
|
9942
9839
|
}
|
|
9943
9840
|
// Every queued entry, oldest-first by `(enqueuedAt, turnId, seq)` so the
|
|
9944
|
-
// drainer delivers
|
|
9945
|
-
// before newer ones. Malformed files are dropped (logged) rather than
|
|
9841
|
+
// drainer delivers a turn's rows in the order they were minted, and older
|
|
9842
|
+
// turns before newer ones. Malformed files are dropped (logged) rather than
|
|
9946
9843
|
// wedging the drain.
|
|
9947
9844
|
list() {
|
|
9948
9845
|
const dir2 = this.dir();
|
|
@@ -9968,8 +9865,9 @@ var Outbox = class {
|
|
|
9968
9865
|
this.dropCorrupt(full);
|
|
9969
9866
|
}
|
|
9970
9867
|
}
|
|
9868
|
+
const rank = (e) => e.kind === "active-run" ? 1 : 0;
|
|
9971
9869
|
entries.sort(
|
|
9972
|
-
(a, b) => a.enqueuedAt - b.enqueuedAt || (a.turnId < b.turnId ? -1 : a.turnId > b.turnId ? 1 : 0) || a.seq - b.seq
|
|
9870
|
+
(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
|
|
9973
9871
|
);
|
|
9974
9872
|
return entries;
|
|
9975
9873
|
}
|