@cabane/companion 0.6.37 → 0.6.38
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 +73 -5
- package/dist/runtime.js +73 -5
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3177,6 +3177,28 @@ function normalizeTurnResultReason(reason) {
|
|
|
3177
3177
|
}
|
|
3178
3178
|
}
|
|
3179
3179
|
|
|
3180
|
+
// packages/agent-runtime/src/empty-result.ts
|
|
3181
|
+
function normalizeCommitText(text) {
|
|
3182
|
+
return text.trim();
|
|
3183
|
+
}
|
|
3184
|
+
function isContentBearingEvent(event) {
|
|
3185
|
+
switch (event.type) {
|
|
3186
|
+
case "text":
|
|
3187
|
+
return normalizeCommitText(event.body) !== "";
|
|
3188
|
+
case "thinking":
|
|
3189
|
+
return normalizeCommitText(event.text) !== "";
|
|
3190
|
+
case "tool":
|
|
3191
|
+
return true;
|
|
3192
|
+
case "session":
|
|
3193
|
+
case "result":
|
|
3194
|
+
return false;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
function classifyEmptyResult(input) {
|
|
3198
|
+
if (!input.ok || input.contentBearingEvents > 0) return null;
|
|
3199
|
+
return input.usage?.inputTokens === 0 && input.usage.outputTokens === 0 ? "empty_result" : "empty_result_unverified";
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3180
3202
|
// packages/agent-runtime/src/turn-request.ts
|
|
3181
3203
|
import { z as z8 } from "zod";
|
|
3182
3204
|
var contentBlockSchema = z8.discriminatedUnion("type", [
|
|
@@ -3498,7 +3520,7 @@ var TurnPump = class {
|
|
|
3498
3520
|
this.sink = {
|
|
3499
3521
|
onAssistantText: async (evt) => {
|
|
3500
3522
|
if (opts.signal.aborted) return;
|
|
3501
|
-
const body = evt.text
|
|
3523
|
+
const body = normalizeCommitText(evt.text);
|
|
3502
3524
|
if (!body) return;
|
|
3503
3525
|
const kind = evt.final ? "final" : "progress";
|
|
3504
3526
|
const seq = opts.nextSeq();
|
|
@@ -3529,7 +3551,7 @@ var TurnPump = class {
|
|
|
3529
3551
|
},
|
|
3530
3552
|
onThinking: async (evt) => {
|
|
3531
3553
|
if (opts.signal.aborted) return;
|
|
3532
|
-
const trimmed = evt.text
|
|
3554
|
+
const trimmed = normalizeCommitText(evt.text);
|
|
3533
3555
|
if (!trimmed) return;
|
|
3534
3556
|
const text = truncate(trimmed, THINKING_TEXT_MAX_CHARS);
|
|
3535
3557
|
const seq = opts.nextSeq();
|
|
@@ -4207,6 +4229,19 @@ var CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
4207
4229
|
{ type: "result", ok: false, reason: "result_error:error_max_turns" }
|
|
4208
4230
|
]
|
|
4209
4231
|
},
|
|
4232
|
+
{
|
|
4233
|
+
// CT1144: the empty-runtime anomaly's exact captured trace — `system/init`
|
|
4234
|
+
// then a successful `result`, with nothing in between. Distinct from
|
|
4235
|
+
// `empty-final` below: THAT turn did work (a tool ran) and just didn't speak;
|
|
4236
|
+
// this one produced nothing at all. The adapter's job is unchanged either way
|
|
4237
|
+
// — report faithfully what the runtime said — so it must still emit
|
|
4238
|
+
// `result{ok:true}` here; the host is what decides that a success with no
|
|
4239
|
+
// content-bearing event is a failed turn.
|
|
4240
|
+
name: "zero-content success (empty runtime result)",
|
|
4241
|
+
request: makeRequest(),
|
|
4242
|
+
nativeStream: [init("s1"), resultSuccess("s1")],
|
|
4243
|
+
expected: [sessionEvent("s1"), { type: "result", ok: true }]
|
|
4244
|
+
},
|
|
4210
4245
|
{
|
|
4211
4246
|
// Empty-final: a clean turn that ended on a tool call with no closing text.
|
|
4212
4247
|
// The adapter emits NO final text — empty-final promotion is host/pump
|
|
@@ -5243,6 +5278,18 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
5243
5278
|
],
|
|
5244
5279
|
expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "auth_expired" }]
|
|
5245
5280
|
},
|
|
5281
|
+
{
|
|
5282
|
+
// CT1144: the same zero-content success the claude-code suite pins, in
|
|
5283
|
+
// opencode's vocabulary — the session goes idle having emitted no part at
|
|
5284
|
+
// all. No opencode sample of the anomaly has been seen in the wild; the
|
|
5285
|
+
// fixture exists because the host's classification is runtime-neutral, so a
|
|
5286
|
+
// runtime that CAN produce this envelope must be shown producing it in the
|
|
5287
|
+
// shape the classifier reads.
|
|
5288
|
+
name: "zero-content success (empty runtime result)",
|
|
5289
|
+
request: makeRequest2(),
|
|
5290
|
+
nativeStream: [idle()],
|
|
5291
|
+
expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: true }]
|
|
5292
|
+
},
|
|
5246
5293
|
{
|
|
5247
5294
|
// Empty-final: a clean turn that ended on a tool call with no closing text.
|
|
5248
5295
|
// The adapter emits NO final text — empty-final promotion is host/pump
|
|
@@ -6357,6 +6404,18 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6357
6404
|
{ type: "result", ok: true }
|
|
6358
6405
|
]
|
|
6359
6406
|
},
|
|
6407
|
+
{
|
|
6408
|
+
// CT1144: the same zero-content success the claude-code suite pins, in
|
|
6409
|
+
// Codex's vocabulary — the thread starts, the turn completes, and nothing is
|
|
6410
|
+
// emitted in between. No Codex sample of the anomaly has been seen in the
|
|
6411
|
+
// wild; the fixture exists because the host's classification is runtime-
|
|
6412
|
+
// neutral, so a runtime that CAN produce this envelope must be shown
|
|
6413
|
+
// producing it in the shape the classifier reads.
|
|
6414
|
+
name: "zero-content success (empty runtime result)",
|
|
6415
|
+
request: makeRequest3(),
|
|
6416
|
+
nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
|
|
6417
|
+
expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: true }]
|
|
6418
|
+
},
|
|
6360
6419
|
{
|
|
6361
6420
|
// Empty-final: a clean turn that ended on a tool call with no closing message.
|
|
6362
6421
|
// The adapter emits NO final text — empty-final promotion is host/pump territory
|
|
@@ -8162,6 +8221,7 @@ ${reason}`,
|
|
|
8162
8221
|
result: 0
|
|
8163
8222
|
};
|
|
8164
8223
|
let runtimeResultKind = null;
|
|
8224
|
+
let contentBearingEvents = 0;
|
|
8165
8225
|
let latestSessionState = request.session;
|
|
8166
8226
|
let settledDiagnostics = null;
|
|
8167
8227
|
const committer = new TurnCommitter({
|
|
@@ -8260,6 +8320,7 @@ ${reason}`,
|
|
|
8260
8320
|
for await (const event of adapter.runTurn(request, abortController.signal)) {
|
|
8261
8321
|
transcript2?.write(event);
|
|
8262
8322
|
eventCounts[event.type] += 1;
|
|
8323
|
+
if (isContentBearingEvent(event)) contentBearingEvents += 1;
|
|
8263
8324
|
armIdle();
|
|
8264
8325
|
if (abortController.signal.aborted) {
|
|
8265
8326
|
turnLog.info("dispatcher: aborted mid-turn");
|
|
@@ -8320,6 +8381,11 @@ ${reason}`,
|
|
|
8320
8381
|
okResult = false;
|
|
8321
8382
|
resultReason = timeoutReason ?? "cancelled";
|
|
8322
8383
|
}
|
|
8384
|
+
const emptyResultReason = !skipState.skipped && classifyEmptyResult({ ok: okResult, contentBearingEvents, usage: turnUsage });
|
|
8385
|
+
if (emptyResultReason) {
|
|
8386
|
+
okResult = false;
|
|
8387
|
+
resultReason = emptyResultReason;
|
|
8388
|
+
}
|
|
8323
8389
|
if (!okResult && !resultReason) {
|
|
8324
8390
|
resultReason = "no_result";
|
|
8325
8391
|
}
|
|
@@ -8431,9 +8497,7 @@ ${reason}`,
|
|
|
8431
8497
|
this.sessionWriteNotified.add(key);
|
|
8432
8498
|
}
|
|
8433
8499
|
const outcome = skipState.skipped ? "skipped" : userCancelled ? "cancelled" : okResult ? "success" : "failure";
|
|
8434
|
-
const
|
|
8435
|
-
const emptySuccessfulEnvelope = outcome === "success" && eventCounts.result > 0 && noContentEvents;
|
|
8436
|
-
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? { kind: "cancelled" } : emptySuccessfulEnvelope ? turnUsage?.inputTokens === 0 && turnUsage.outputTokens === 0 ? { kind: "empty_result" } : { kind: "empty_result_unverified" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
8500
|
+
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
8437
8501
|
settledDiagnostics = {
|
|
8438
8502
|
outcome,
|
|
8439
8503
|
resultReason: diagnosticReason,
|
|
@@ -8449,10 +8513,14 @@ ${reason}`,
|
|
|
8449
8513
|
)) {
|
|
8450
8514
|
turnLog.warn(
|
|
8451
8515
|
{
|
|
8516
|
+
workspaceId,
|
|
8452
8517
|
turnId,
|
|
8453
8518
|
conversationId: payload.conversationId,
|
|
8454
8519
|
agentId: payload.agentId,
|
|
8455
8520
|
runtime: turnRuntime,
|
|
8521
|
+
model: turnResolvedModel ?? null,
|
|
8522
|
+
usage: turnUsage ?? null,
|
|
8523
|
+
hadStoredSession: request.session != null,
|
|
8456
8524
|
diagnostics: settledDiagnostics
|
|
8457
8525
|
},
|
|
8458
8526
|
"dispatcher: anomalous turn settled"
|
package/dist/runtime.js
CHANGED
|
@@ -2684,6 +2684,28 @@ function normalizeTurnResultReason(reason) {
|
|
|
2684
2684
|
}
|
|
2685
2685
|
}
|
|
2686
2686
|
|
|
2687
|
+
// packages/agent-runtime/src/empty-result.ts
|
|
2688
|
+
function normalizeCommitText(text) {
|
|
2689
|
+
return text.trim();
|
|
2690
|
+
}
|
|
2691
|
+
function isContentBearingEvent(event) {
|
|
2692
|
+
switch (event.type) {
|
|
2693
|
+
case "text":
|
|
2694
|
+
return normalizeCommitText(event.body) !== "";
|
|
2695
|
+
case "thinking":
|
|
2696
|
+
return normalizeCommitText(event.text) !== "";
|
|
2697
|
+
case "tool":
|
|
2698
|
+
return true;
|
|
2699
|
+
case "session":
|
|
2700
|
+
case "result":
|
|
2701
|
+
return false;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
function classifyEmptyResult(input) {
|
|
2705
|
+
if (!input.ok || input.contentBearingEvents > 0) return null;
|
|
2706
|
+
return input.usage?.inputTokens === 0 && input.usage.outputTokens === 0 ? "empty_result" : "empty_result_unverified";
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2687
2709
|
// packages/agent-runtime/src/turn-request.ts
|
|
2688
2710
|
import { z as z8 } from "zod";
|
|
2689
2711
|
var contentBlockSchema = z8.discriminatedUnion("type", [
|
|
@@ -3005,7 +3027,7 @@ var TurnPump = class {
|
|
|
3005
3027
|
this.sink = {
|
|
3006
3028
|
onAssistantText: async (evt) => {
|
|
3007
3029
|
if (opts.signal.aborted) return;
|
|
3008
|
-
const body = evt.text
|
|
3030
|
+
const body = normalizeCommitText(evt.text);
|
|
3009
3031
|
if (!body) return;
|
|
3010
3032
|
const kind = evt.final ? "final" : "progress";
|
|
3011
3033
|
const seq = opts.nextSeq();
|
|
@@ -3036,7 +3058,7 @@ var TurnPump = class {
|
|
|
3036
3058
|
},
|
|
3037
3059
|
onThinking: async (evt) => {
|
|
3038
3060
|
if (opts.signal.aborted) return;
|
|
3039
|
-
const trimmed = evt.text
|
|
3061
|
+
const trimmed = normalizeCommitText(evt.text);
|
|
3040
3062
|
if (!trimmed) return;
|
|
3041
3063
|
const text = truncate(trimmed, THINKING_TEXT_MAX_CHARS);
|
|
3042
3064
|
const seq = opts.nextSeq();
|
|
@@ -3714,6 +3736,19 @@ var CLAUDE_CODE_CONFORMANCE_FIXTURES = [
|
|
|
3714
3736
|
{ type: "result", ok: false, reason: "result_error:error_max_turns" }
|
|
3715
3737
|
]
|
|
3716
3738
|
},
|
|
3739
|
+
{
|
|
3740
|
+
// CT1144: the empty-runtime anomaly's exact captured trace — `system/init`
|
|
3741
|
+
// then a successful `result`, with nothing in between. Distinct from
|
|
3742
|
+
// `empty-final` below: THAT turn did work (a tool ran) and just didn't speak;
|
|
3743
|
+
// this one produced nothing at all. The adapter's job is unchanged either way
|
|
3744
|
+
// — report faithfully what the runtime said — so it must still emit
|
|
3745
|
+
// `result{ok:true}` here; the host is what decides that a success with no
|
|
3746
|
+
// content-bearing event is a failed turn.
|
|
3747
|
+
name: "zero-content success (empty runtime result)",
|
|
3748
|
+
request: makeRequest(),
|
|
3749
|
+
nativeStream: [init("s1"), resultSuccess("s1")],
|
|
3750
|
+
expected: [sessionEvent("s1"), { type: "result", ok: true }]
|
|
3751
|
+
},
|
|
3717
3752
|
{
|
|
3718
3753
|
// Empty-final: a clean turn that ended on a tool call with no closing text.
|
|
3719
3754
|
// The adapter emits NO final text — empty-final promotion is host/pump
|
|
@@ -4750,6 +4785,18 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
|
|
|
4750
4785
|
],
|
|
4751
4786
|
expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "auth_expired" }]
|
|
4752
4787
|
},
|
|
4788
|
+
{
|
|
4789
|
+
// CT1144: the same zero-content success the claude-code suite pins, in
|
|
4790
|
+
// opencode's vocabulary — the session goes idle having emitted no part at
|
|
4791
|
+
// all. No opencode sample of the anomaly has been seen in the wild; the
|
|
4792
|
+
// fixture exists because the host's classification is runtime-neutral, so a
|
|
4793
|
+
// runtime that CAN produce this envelope must be shown producing it in the
|
|
4794
|
+
// shape the classifier reads.
|
|
4795
|
+
name: "zero-content success (empty runtime result)",
|
|
4796
|
+
request: makeRequest2(),
|
|
4797
|
+
nativeStream: [idle()],
|
|
4798
|
+
expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: true }]
|
|
4799
|
+
},
|
|
4753
4800
|
{
|
|
4754
4801
|
// Empty-final: a clean turn that ended on a tool call with no closing text.
|
|
4755
4802
|
// The adapter emits NO final text — empty-final promotion is host/pump
|
|
@@ -5864,6 +5911,18 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
5864
5911
|
{ type: "result", ok: true }
|
|
5865
5912
|
]
|
|
5866
5913
|
},
|
|
5914
|
+
{
|
|
5915
|
+
// CT1144: the same zero-content success the claude-code suite pins, in
|
|
5916
|
+
// Codex's vocabulary — the thread starts, the turn completes, and nothing is
|
|
5917
|
+
// emitted in between. No Codex sample of the anomaly has been seen in the
|
|
5918
|
+
// wild; the fixture exists because the host's classification is runtime-
|
|
5919
|
+
// neutral, so a runtime that CAN produce this envelope must be shown
|
|
5920
|
+
// producing it in the shape the classifier reads.
|
|
5921
|
+
name: "zero-content success (empty runtime result)",
|
|
5922
|
+
request: makeRequest3(),
|
|
5923
|
+
nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
|
|
5924
|
+
expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: true }]
|
|
5925
|
+
},
|
|
5867
5926
|
{
|
|
5868
5927
|
// Empty-final: a clean turn that ended on a tool call with no closing message.
|
|
5869
5928
|
// The adapter emits NO final text — empty-final promotion is host/pump territory
|
|
@@ -7669,6 +7728,7 @@ ${reason}`,
|
|
|
7669
7728
|
result: 0
|
|
7670
7729
|
};
|
|
7671
7730
|
let runtimeResultKind = null;
|
|
7731
|
+
let contentBearingEvents = 0;
|
|
7672
7732
|
let latestSessionState = request.session;
|
|
7673
7733
|
let settledDiagnostics = null;
|
|
7674
7734
|
const committer = new TurnCommitter({
|
|
@@ -7767,6 +7827,7 @@ ${reason}`,
|
|
|
7767
7827
|
for await (const event of adapter.runTurn(request, abortController.signal)) {
|
|
7768
7828
|
transcript?.write(event);
|
|
7769
7829
|
eventCounts[event.type] += 1;
|
|
7830
|
+
if (isContentBearingEvent(event)) contentBearingEvents += 1;
|
|
7770
7831
|
armIdle();
|
|
7771
7832
|
if (abortController.signal.aborted) {
|
|
7772
7833
|
turnLog.info("dispatcher: aborted mid-turn");
|
|
@@ -7827,6 +7888,11 @@ ${reason}`,
|
|
|
7827
7888
|
okResult = false;
|
|
7828
7889
|
resultReason = timeoutReason ?? "cancelled";
|
|
7829
7890
|
}
|
|
7891
|
+
const emptyResultReason = !skipState.skipped && classifyEmptyResult({ ok: okResult, contentBearingEvents, usage: turnUsage });
|
|
7892
|
+
if (emptyResultReason) {
|
|
7893
|
+
okResult = false;
|
|
7894
|
+
resultReason = emptyResultReason;
|
|
7895
|
+
}
|
|
7830
7896
|
if (!okResult && !resultReason) {
|
|
7831
7897
|
resultReason = "no_result";
|
|
7832
7898
|
}
|
|
@@ -7938,9 +8004,7 @@ ${reason}`,
|
|
|
7938
8004
|
this.sessionWriteNotified.add(key);
|
|
7939
8005
|
}
|
|
7940
8006
|
const outcome = skipState.skipped ? "skipped" : userCancelled ? "cancelled" : okResult ? "success" : "failure";
|
|
7941
|
-
const
|
|
7942
|
-
const emptySuccessfulEnvelope = outcome === "success" && eventCounts.result > 0 && noContentEvents;
|
|
7943
|
-
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? { kind: "cancelled" } : emptySuccessfulEnvelope ? turnUsage?.inputTokens === 0 && turnUsage.outputTokens === 0 ? { kind: "empty_result" } : { kind: "empty_result_unverified" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
8007
|
+
const diagnosticReason = outcome === "skipped" ? { kind: "skipped" } : outcome === "cancelled" ? { kind: "cancelled" } : outcome === "failure" ? normalizeTurnResultReason(resultReason) : null;
|
|
7944
8008
|
settledDiagnostics = {
|
|
7945
8009
|
outcome,
|
|
7946
8010
|
resultReason: diagnosticReason,
|
|
@@ -7956,10 +8020,14 @@ ${reason}`,
|
|
|
7956
8020
|
)) {
|
|
7957
8021
|
turnLog.warn(
|
|
7958
8022
|
{
|
|
8023
|
+
workspaceId,
|
|
7959
8024
|
turnId,
|
|
7960
8025
|
conversationId: payload.conversationId,
|
|
7961
8026
|
agentId: payload.agentId,
|
|
7962
8027
|
runtime: turnRuntime,
|
|
8028
|
+
model: turnResolvedModel ?? null,
|
|
8029
|
+
usage: turnUsage ?? null,
|
|
8030
|
+
hadStoredSession: request.session != null,
|
|
7963
8031
|
diagnostics: settledDiagnostics
|
|
7964
8032
|
},
|
|
7965
8033
|
"dispatcher: anomalous turn settled"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|