@cabane/companion 0.6.48 → 0.6.49
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 +56 -24
- package/dist/runtime.js +56 -24
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3599,7 +3599,7 @@ var TurnPump = class {
|
|
|
3599
3599
|
};
|
|
3600
3600
|
}
|
|
3601
3601
|
opts;
|
|
3602
|
-
// `finalEmitted` gates the end-of-turn
|
|
3602
|
+
// `finalEmitted` gates the end-of-turn progress promotion;
|
|
3603
3603
|
// `lastProgressBody` is what we promote to `final` when a clean turn ended on
|
|
3604
3604
|
// a tool call with no closing text.
|
|
3605
3605
|
emittedFinal = false;
|
|
@@ -3614,28 +3614,28 @@ var TurnPump = class {
|
|
|
3614
3614
|
// frozen committer test — drive. Each callback applies the choreography and
|
|
3615
3615
|
// commits through the injected sink.
|
|
3616
3616
|
sink;
|
|
3617
|
-
// End-of-turn
|
|
3618
|
-
//
|
|
3619
|
-
//
|
|
3620
|
-
//
|
|
3621
|
-
//
|
|
3622
|
-
//
|
|
3623
|
-
// this runs.
|
|
3617
|
+
// End-of-turn progress promotion. A clean turn that emitted interim text but
|
|
3618
|
+
// no closing text promotes the agent's own last progress body (the drawer
|
|
3619
|
+
// collapses the duplicate). If the agent emitted no words, emit no message:
|
|
3620
|
+
// Cabane never attributes host-authored text to an agent, in any turn shape,
|
|
3621
|
+
// for any reason. The host closes that wordless turn with a marker instead.
|
|
3622
|
+
// Skipped when cancelled or already final. The held-text flush that precedes
|
|
3623
|
+
// this is a classification concern, driven by the caller before this runs.
|
|
3624
3624
|
async finalize(ok) {
|
|
3625
|
-
if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
|
|
3626
|
-
const body = this.lastProgressBody
|
|
3625
|
+
if (!ok || this.opts.signal.aborted || this.emittedFinal || !this.lastProgressBody) return;
|
|
3626
|
+
const body = this.lastProgressBody;
|
|
3627
3627
|
const seq = this.opts.nextSeq();
|
|
3628
3628
|
try {
|
|
3629
3629
|
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
3630
3630
|
this.emittedFinal = true;
|
|
3631
3631
|
this.finalReplyBody = body;
|
|
3632
|
-
this.emittedFinalSource =
|
|
3632
|
+
this.emittedFinalSource = "progress_promotion";
|
|
3633
3633
|
} catch (err) {
|
|
3634
|
-
this.opts.onError?.(err, "
|
|
3634
|
+
this.opts.onError?.(err, "progress-promotion");
|
|
3635
3635
|
}
|
|
3636
3636
|
}
|
|
3637
3637
|
// Whether the turn has committed its `final` row — read by the host to decide
|
|
3638
|
-
// whether
|
|
3638
|
+
// whether a marker is owed.
|
|
3639
3639
|
get finalEmitted() {
|
|
3640
3640
|
return this.emittedFinal;
|
|
3641
3641
|
}
|
|
@@ -7274,7 +7274,6 @@ function pruneOld(dir2, retain) {
|
|
|
7274
7274
|
}
|
|
7275
7275
|
|
|
7276
7276
|
// src/turn-committer.ts
|
|
7277
|
-
var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
|
|
7278
7277
|
var TurnCommitter = class {
|
|
7279
7278
|
constructor(deps) {
|
|
7280
7279
|
this.deps = deps;
|
|
@@ -7295,9 +7294,7 @@ var TurnCommitter = class {
|
|
|
7295
7294
|
turnId: deps.turnId,
|
|
7296
7295
|
seq,
|
|
7297
7296
|
parentMessageId: deps.parentMessageId,
|
|
7298
|
-
...kind === "final" ? this.
|
|
7299
|
-
...kind === "final" ? this.askField() : {},
|
|
7300
|
-
...kind === "final" ? this.wakeField() : {}
|
|
7297
|
+
...kind === "final" ? this.turnControlFields() : {}
|
|
7301
7298
|
},
|
|
7302
7299
|
deps.signal
|
|
7303
7300
|
);
|
|
@@ -7344,11 +7341,10 @@ var TurnCommitter = class {
|
|
|
7344
7341
|
commit,
|
|
7345
7342
|
signal: deps.signal,
|
|
7346
7343
|
nextSeq: deps.nextSeq,
|
|
7347
|
-
emptyFinalBody: EMPTY_FINAL_BODY,
|
|
7348
7344
|
onError: (err) => {
|
|
7349
7345
|
deps.log.warn(
|
|
7350
7346
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
7351
|
-
"dispatcher:
|
|
7347
|
+
"dispatcher: progress-promotion commit failed"
|
|
7352
7348
|
);
|
|
7353
7349
|
}
|
|
7354
7350
|
});
|
|
@@ -7374,11 +7370,11 @@ var TurnCommitter = class {
|
|
|
7374
7370
|
this.onError(err, event.type);
|
|
7375
7371
|
}
|
|
7376
7372
|
}
|
|
7377
|
-
// End-of-turn
|
|
7373
|
+
// End-of-turn progress promotion. The held-text flush is now the adapter's
|
|
7378
7374
|
// job (it emits the closing reply as a `text` event before `result`), so this
|
|
7379
|
-
// only
|
|
7380
|
-
//
|
|
7381
|
-
// on cancel no `final` is forced.
|
|
7375
|
+
// only asks the pump to promote agent-authored interim text. A wordless turn
|
|
7376
|
+
// stays wordless and the dispatcher closes it with a marker. Guards on the
|
|
7377
|
+
// abort signal, so on cancel no `final` is forced.
|
|
7382
7378
|
async finalize(okResult) {
|
|
7383
7379
|
await this.pump.finalize(okResult);
|
|
7384
7380
|
}
|
|
@@ -7389,6 +7385,19 @@ var TurnCommitter = class {
|
|
|
7389
7385
|
get finalSource() {
|
|
7390
7386
|
return this.pump.finalSource;
|
|
7391
7387
|
}
|
|
7388
|
+
get finalEmitted() {
|
|
7389
|
+
return this.pump.finalEmitted;
|
|
7390
|
+
}
|
|
7391
|
+
// A closing textual reply and a wordless terminal marker carry the same
|
|
7392
|
+
// per-turn control state. Keep this one projection so ask/wake/summon cannot
|
|
7393
|
+
// silently diverge when the agent ends without words.
|
|
7394
|
+
turnControlFields() {
|
|
7395
|
+
return {
|
|
7396
|
+
...this.summonField(),
|
|
7397
|
+
...this.askField(),
|
|
7398
|
+
...this.wakeField()
|
|
7399
|
+
};
|
|
7400
|
+
}
|
|
7392
7401
|
// CT183: resolve the in-thread summon into the `dispatch` field for a `final`
|
|
7393
7402
|
// commit. A self-target is stripped here (mirror of the in-app self-strip); the
|
|
7394
7403
|
// server strips it again and resolves / ignores an unknown id.
|
|
@@ -7617,6 +7626,7 @@ var STOPPED_MARKER_BODY = "(stopped)";
|
|
|
7617
7626
|
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:";
|
|
7618
7627
|
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`;
|
|
7619
7628
|
var SKIPPED_MARKER_BODY = "(skipped)";
|
|
7629
|
+
var SILENT_MARKER_BODY = "(no reply)";
|
|
7620
7630
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
7621
7631
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7622
7632
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
@@ -8274,6 +8284,7 @@ ${reason}`,
|
|
|
8274
8284
|
let contentBearingEvents = 0;
|
|
8275
8285
|
let latestSessionState = request.session;
|
|
8276
8286
|
let settledDiagnostics = null;
|
|
8287
|
+
let silentMarkerEmitted = false;
|
|
8277
8288
|
const committer = new TurnCommitter({
|
|
8278
8289
|
api: this.opts.api,
|
|
8279
8290
|
workspaceId,
|
|
@@ -8465,6 +8476,27 @@ ${reason}`,
|
|
|
8465
8476
|
}
|
|
8466
8477
|
} else {
|
|
8467
8478
|
await committer.finalize(okResult);
|
|
8479
|
+
if (!abortController.signal.aborted && okResult && !committer.finalEmitted) {
|
|
8480
|
+
try {
|
|
8481
|
+
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
8482
|
+
body: SILENT_MARKER_BODY,
|
|
8483
|
+
kind: "silent",
|
|
8484
|
+
turnId,
|
|
8485
|
+
seq: nextSeq(),
|
|
8486
|
+
parentMessageId: payload.messageId,
|
|
8487
|
+
// ask, wake and summon must survive a wordless turn exactly as
|
|
8488
|
+
// they survive a textual final; dropping one can strand a person
|
|
8489
|
+
// or the next actor with no visible failure.
|
|
8490
|
+
...committer.turnControlFields()
|
|
8491
|
+
});
|
|
8492
|
+
silentMarkerEmitted = true;
|
|
8493
|
+
} catch (err) {
|
|
8494
|
+
turnLog.warn(
|
|
8495
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8496
|
+
"dispatcher: silent-marker commit failed"
|
|
8497
|
+
);
|
|
8498
|
+
}
|
|
8499
|
+
}
|
|
8468
8500
|
}
|
|
8469
8501
|
} catch (err) {
|
|
8470
8502
|
okResult = false;
|
|
@@ -8555,7 +8587,7 @@ ${reason}`,
|
|
|
8555
8587
|
sessionFingerprint: fingerprintSessionState(latestSessionState),
|
|
8556
8588
|
eventCounts,
|
|
8557
8589
|
runtimeResultKind,
|
|
8558
|
-
finalSource: outcome === "skipped" || outcome === "cancelled" ? "marker" : committer.finalSource
|
|
8590
|
+
finalSource: outcome === "skipped" || outcome === "cancelled" || silentMarkerEmitted ? "marker" : committer.finalSource
|
|
8559
8591
|
};
|
|
8560
8592
|
body.diagnostics = settledDiagnostics;
|
|
8561
8593
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
package/dist/runtime.js
CHANGED
|
@@ -3107,7 +3107,7 @@ var TurnPump = class {
|
|
|
3107
3107
|
};
|
|
3108
3108
|
}
|
|
3109
3109
|
opts;
|
|
3110
|
-
// `finalEmitted` gates the end-of-turn
|
|
3110
|
+
// `finalEmitted` gates the end-of-turn progress promotion;
|
|
3111
3111
|
// `lastProgressBody` is what we promote to `final` when a clean turn ended on
|
|
3112
3112
|
// a tool call with no closing text.
|
|
3113
3113
|
emittedFinal = false;
|
|
@@ -3122,28 +3122,28 @@ var TurnPump = class {
|
|
|
3122
3122
|
// frozen committer test — drive. Each callback applies the choreography and
|
|
3123
3123
|
// commits through the injected sink.
|
|
3124
3124
|
sink;
|
|
3125
|
-
// End-of-turn
|
|
3126
|
-
//
|
|
3127
|
-
//
|
|
3128
|
-
//
|
|
3129
|
-
//
|
|
3130
|
-
//
|
|
3131
|
-
// this runs.
|
|
3125
|
+
// End-of-turn progress promotion. A clean turn that emitted interim text but
|
|
3126
|
+
// no closing text promotes the agent's own last progress body (the drawer
|
|
3127
|
+
// collapses the duplicate). If the agent emitted no words, emit no message:
|
|
3128
|
+
// Cabane never attributes host-authored text to an agent, in any turn shape,
|
|
3129
|
+
// for any reason. The host closes that wordless turn with a marker instead.
|
|
3130
|
+
// Skipped when cancelled or already final. The held-text flush that precedes
|
|
3131
|
+
// this is a classification concern, driven by the caller before this runs.
|
|
3132
3132
|
async finalize(ok) {
|
|
3133
|
-
if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
|
|
3134
|
-
const body = this.lastProgressBody
|
|
3133
|
+
if (!ok || this.opts.signal.aborted || this.emittedFinal || !this.lastProgressBody) return;
|
|
3134
|
+
const body = this.lastProgressBody;
|
|
3135
3135
|
const seq = this.opts.nextSeq();
|
|
3136
3136
|
try {
|
|
3137
3137
|
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
3138
3138
|
this.emittedFinal = true;
|
|
3139
3139
|
this.finalReplyBody = body;
|
|
3140
|
-
this.emittedFinalSource =
|
|
3140
|
+
this.emittedFinalSource = "progress_promotion";
|
|
3141
3141
|
} catch (err) {
|
|
3142
|
-
this.opts.onError?.(err, "
|
|
3142
|
+
this.opts.onError?.(err, "progress-promotion");
|
|
3143
3143
|
}
|
|
3144
3144
|
}
|
|
3145
3145
|
// Whether the turn has committed its `final` row — read by the host to decide
|
|
3146
|
-
// whether
|
|
3146
|
+
// whether a marker is owed.
|
|
3147
3147
|
get finalEmitted() {
|
|
3148
3148
|
return this.emittedFinal;
|
|
3149
3149
|
}
|
|
@@ -6782,7 +6782,6 @@ function pruneOld(dir2, retain) {
|
|
|
6782
6782
|
}
|
|
6783
6783
|
|
|
6784
6784
|
// src/turn-committer.ts
|
|
6785
|
-
var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
|
|
6786
6785
|
var TurnCommitter = class {
|
|
6787
6786
|
constructor(deps) {
|
|
6788
6787
|
this.deps = deps;
|
|
@@ -6803,9 +6802,7 @@ var TurnCommitter = class {
|
|
|
6803
6802
|
turnId: deps.turnId,
|
|
6804
6803
|
seq,
|
|
6805
6804
|
parentMessageId: deps.parentMessageId,
|
|
6806
|
-
...kind === "final" ? this.
|
|
6807
|
-
...kind === "final" ? this.askField() : {},
|
|
6808
|
-
...kind === "final" ? this.wakeField() : {}
|
|
6805
|
+
...kind === "final" ? this.turnControlFields() : {}
|
|
6809
6806
|
},
|
|
6810
6807
|
deps.signal
|
|
6811
6808
|
);
|
|
@@ -6852,11 +6849,10 @@ var TurnCommitter = class {
|
|
|
6852
6849
|
commit,
|
|
6853
6850
|
signal: deps.signal,
|
|
6854
6851
|
nextSeq: deps.nextSeq,
|
|
6855
|
-
emptyFinalBody: EMPTY_FINAL_BODY,
|
|
6856
6852
|
onError: (err) => {
|
|
6857
6853
|
deps.log.warn(
|
|
6858
6854
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
6859
|
-
"dispatcher:
|
|
6855
|
+
"dispatcher: progress-promotion commit failed"
|
|
6860
6856
|
);
|
|
6861
6857
|
}
|
|
6862
6858
|
});
|
|
@@ -6882,11 +6878,11 @@ var TurnCommitter = class {
|
|
|
6882
6878
|
this.onError(err, event.type);
|
|
6883
6879
|
}
|
|
6884
6880
|
}
|
|
6885
|
-
// End-of-turn
|
|
6881
|
+
// End-of-turn progress promotion. The held-text flush is now the adapter's
|
|
6886
6882
|
// job (it emits the closing reply as a `text` event before `result`), so this
|
|
6887
|
-
// only
|
|
6888
|
-
//
|
|
6889
|
-
// on cancel no `final` is forced.
|
|
6883
|
+
// only asks the pump to promote agent-authored interim text. A wordless turn
|
|
6884
|
+
// stays wordless and the dispatcher closes it with a marker. Guards on the
|
|
6885
|
+
// abort signal, so on cancel no `final` is forced.
|
|
6890
6886
|
async finalize(okResult) {
|
|
6891
6887
|
await this.pump.finalize(okResult);
|
|
6892
6888
|
}
|
|
@@ -6897,6 +6893,19 @@ var TurnCommitter = class {
|
|
|
6897
6893
|
get finalSource() {
|
|
6898
6894
|
return this.pump.finalSource;
|
|
6899
6895
|
}
|
|
6896
|
+
get finalEmitted() {
|
|
6897
|
+
return this.pump.finalEmitted;
|
|
6898
|
+
}
|
|
6899
|
+
// A closing textual reply and a wordless terminal marker carry the same
|
|
6900
|
+
// per-turn control state. Keep this one projection so ask/wake/summon cannot
|
|
6901
|
+
// silently diverge when the agent ends without words.
|
|
6902
|
+
turnControlFields() {
|
|
6903
|
+
return {
|
|
6904
|
+
...this.summonField(),
|
|
6905
|
+
...this.askField(),
|
|
6906
|
+
...this.wakeField()
|
|
6907
|
+
};
|
|
6908
|
+
}
|
|
6900
6909
|
// CT183: resolve the in-thread summon into the `dispatch` field for a `final`
|
|
6901
6910
|
// commit. A self-target is stripped here (mirror of the in-app self-strip); the
|
|
6902
6911
|
// server strips it again and resolves / ignores an unknown id.
|
|
@@ -7125,6 +7134,7 @@ var STOPPED_MARKER_BODY = "(stopped)";
|
|
|
7125
7134
|
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:";
|
|
7126
7135
|
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`;
|
|
7127
7136
|
var SKIPPED_MARKER_BODY = "(skipped)";
|
|
7137
|
+
var SILENT_MARKER_BODY = "(no reply)";
|
|
7128
7138
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
7129
7139
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7130
7140
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
@@ -7782,6 +7792,7 @@ ${reason}`,
|
|
|
7782
7792
|
let contentBearingEvents = 0;
|
|
7783
7793
|
let latestSessionState = request.session;
|
|
7784
7794
|
let settledDiagnostics = null;
|
|
7795
|
+
let silentMarkerEmitted = false;
|
|
7785
7796
|
const committer = new TurnCommitter({
|
|
7786
7797
|
api: this.opts.api,
|
|
7787
7798
|
workspaceId,
|
|
@@ -7973,6 +7984,27 @@ ${reason}`,
|
|
|
7973
7984
|
}
|
|
7974
7985
|
} else {
|
|
7975
7986
|
await committer.finalize(okResult);
|
|
7987
|
+
if (!abortController.signal.aborted && okResult && !committer.finalEmitted) {
|
|
7988
|
+
try {
|
|
7989
|
+
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
7990
|
+
body: SILENT_MARKER_BODY,
|
|
7991
|
+
kind: "silent",
|
|
7992
|
+
turnId,
|
|
7993
|
+
seq: nextSeq(),
|
|
7994
|
+
parentMessageId: payload.messageId,
|
|
7995
|
+
// ask, wake and summon must survive a wordless turn exactly as
|
|
7996
|
+
// they survive a textual final; dropping one can strand a person
|
|
7997
|
+
// or the next actor with no visible failure.
|
|
7998
|
+
...committer.turnControlFields()
|
|
7999
|
+
});
|
|
8000
|
+
silentMarkerEmitted = true;
|
|
8001
|
+
} catch (err) {
|
|
8002
|
+
turnLog.warn(
|
|
8003
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
8004
|
+
"dispatcher: silent-marker commit failed"
|
|
8005
|
+
);
|
|
8006
|
+
}
|
|
8007
|
+
}
|
|
7976
8008
|
}
|
|
7977
8009
|
} catch (err) {
|
|
7978
8010
|
okResult = false;
|
|
@@ -8063,7 +8095,7 @@ ${reason}`,
|
|
|
8063
8095
|
sessionFingerprint: fingerprintSessionState(latestSessionState),
|
|
8064
8096
|
eventCounts,
|
|
8065
8097
|
runtimeResultKind,
|
|
8066
|
-
finalSource: outcome === "skipped" || outcome === "cancelled" ? "marker" : committer.finalSource
|
|
8098
|
+
finalSource: outcome === "skipped" || outcome === "cancelled" || silentMarkerEmitted ? "marker" : committer.finalSource
|
|
8067
8099
|
};
|
|
8068
8100
|
body.diagnostics = settledDiagnostics;
|
|
8069
8101
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.49",
|
|
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",
|