@cabane/companion 0.6.47 → 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 +67 -29
- package/dist/runtime.js +67 -29
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -149,6 +149,7 @@ var runPrepareHook = (hook, input) => {
|
|
|
149
149
|
CABANE_AGENT_ID: input.agentId,
|
|
150
150
|
CABANE_AGENT_USERNAME: input.agentUsername,
|
|
151
151
|
CABANE_RUNTIME: input.runtime ?? "",
|
|
152
|
+
CABANE_HOST_ACCESS: input.hostAccess ? "1" : "0",
|
|
152
153
|
// CT319: the conversation anchor is gone. These are kept as DEPRECATED
|
|
153
154
|
// back-compat constants so an old user prepare script that still reads
|
|
154
155
|
// them doesn't crash; a hook should key off CABANE_TRIGGER_ENTRY_PATHS.
|
|
@@ -3598,7 +3599,7 @@ var TurnPump = class {
|
|
|
3598
3599
|
};
|
|
3599
3600
|
}
|
|
3600
3601
|
opts;
|
|
3601
|
-
// `finalEmitted` gates the end-of-turn
|
|
3602
|
+
// `finalEmitted` gates the end-of-turn progress promotion;
|
|
3602
3603
|
// `lastProgressBody` is what we promote to `final` when a clean turn ended on
|
|
3603
3604
|
// a tool call with no closing text.
|
|
3604
3605
|
emittedFinal = false;
|
|
@@ -3613,28 +3614,28 @@ var TurnPump = class {
|
|
|
3613
3614
|
// frozen committer test — drive. Each callback applies the choreography and
|
|
3614
3615
|
// commits through the injected sink.
|
|
3615
3616
|
sink;
|
|
3616
|
-
// End-of-turn
|
|
3617
|
-
//
|
|
3618
|
-
//
|
|
3619
|
-
//
|
|
3620
|
-
//
|
|
3621
|
-
//
|
|
3622
|
-
// 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.
|
|
3623
3624
|
async finalize(ok) {
|
|
3624
|
-
if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
|
|
3625
|
-
const body = this.lastProgressBody
|
|
3625
|
+
if (!ok || this.opts.signal.aborted || this.emittedFinal || !this.lastProgressBody) return;
|
|
3626
|
+
const body = this.lastProgressBody;
|
|
3626
3627
|
const seq = this.opts.nextSeq();
|
|
3627
3628
|
try {
|
|
3628
3629
|
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
3629
3630
|
this.emittedFinal = true;
|
|
3630
3631
|
this.finalReplyBody = body;
|
|
3631
|
-
this.emittedFinalSource =
|
|
3632
|
+
this.emittedFinalSource = "progress_promotion";
|
|
3632
3633
|
} catch (err) {
|
|
3633
|
-
this.opts.onError?.(err, "
|
|
3634
|
+
this.opts.onError?.(err, "progress-promotion");
|
|
3634
3635
|
}
|
|
3635
3636
|
}
|
|
3636
3637
|
// Whether the turn has committed its `final` row — read by the host to decide
|
|
3637
|
-
// whether
|
|
3638
|
+
// whether a marker is owed.
|
|
3638
3639
|
get finalEmitted() {
|
|
3639
3640
|
return this.emittedFinal;
|
|
3640
3641
|
}
|
|
@@ -5777,7 +5778,8 @@ var ENV_ENVELOPE_KEYS = [
|
|
|
5777
5778
|
"CABANE_PLAYGROUND_BIN",
|
|
5778
5779
|
"CABANE_CONVERSATION_ID",
|
|
5779
5780
|
"CABANE_AGENT_ID",
|
|
5780
|
-
"CABANE_CONVERSATION_TITLE"
|
|
5781
|
+
"CABANE_CONVERSATION_TITLE",
|
|
5782
|
+
"CABANE_HOST_ACCESS"
|
|
5781
5783
|
];
|
|
5782
5784
|
function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null) {
|
|
5783
5785
|
const { policy, config } = req;
|
|
@@ -7272,7 +7274,6 @@ function pruneOld(dir2, retain) {
|
|
|
7272
7274
|
}
|
|
7273
7275
|
|
|
7274
7276
|
// src/turn-committer.ts
|
|
7275
|
-
var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
|
|
7276
7277
|
var TurnCommitter = class {
|
|
7277
7278
|
constructor(deps) {
|
|
7278
7279
|
this.deps = deps;
|
|
@@ -7293,9 +7294,7 @@ var TurnCommitter = class {
|
|
|
7293
7294
|
turnId: deps.turnId,
|
|
7294
7295
|
seq,
|
|
7295
7296
|
parentMessageId: deps.parentMessageId,
|
|
7296
|
-
...kind === "final" ? this.
|
|
7297
|
-
...kind === "final" ? this.askField() : {},
|
|
7298
|
-
...kind === "final" ? this.wakeField() : {}
|
|
7297
|
+
...kind === "final" ? this.turnControlFields() : {}
|
|
7299
7298
|
},
|
|
7300
7299
|
deps.signal
|
|
7301
7300
|
);
|
|
@@ -7342,11 +7341,10 @@ var TurnCommitter = class {
|
|
|
7342
7341
|
commit,
|
|
7343
7342
|
signal: deps.signal,
|
|
7344
7343
|
nextSeq: deps.nextSeq,
|
|
7345
|
-
emptyFinalBody: EMPTY_FINAL_BODY,
|
|
7346
7344
|
onError: (err) => {
|
|
7347
7345
|
deps.log.warn(
|
|
7348
7346
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
7349
|
-
"dispatcher:
|
|
7347
|
+
"dispatcher: progress-promotion commit failed"
|
|
7350
7348
|
);
|
|
7351
7349
|
}
|
|
7352
7350
|
});
|
|
@@ -7372,11 +7370,11 @@ var TurnCommitter = class {
|
|
|
7372
7370
|
this.onError(err, event.type);
|
|
7373
7371
|
}
|
|
7374
7372
|
}
|
|
7375
|
-
// End-of-turn
|
|
7373
|
+
// End-of-turn progress promotion. The held-text flush is now the adapter's
|
|
7376
7374
|
// job (it emits the closing reply as a `text` event before `result`), so this
|
|
7377
|
-
// only
|
|
7378
|
-
//
|
|
7379
|
-
// 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.
|
|
7380
7378
|
async finalize(okResult) {
|
|
7381
7379
|
await this.pump.finalize(okResult);
|
|
7382
7380
|
}
|
|
@@ -7387,6 +7385,19 @@ var TurnCommitter = class {
|
|
|
7387
7385
|
get finalSource() {
|
|
7388
7386
|
return this.pump.finalSource;
|
|
7389
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
|
+
}
|
|
7390
7401
|
// CT183: resolve the in-thread summon into the `dispatch` field for a `final`
|
|
7391
7402
|
// commit. A self-target is stripped here (mirror of the in-app self-strip); the
|
|
7392
7403
|
// server strips it again and resolves / ignores an unknown id.
|
|
@@ -7615,6 +7626,7 @@ var STOPPED_MARKER_BODY = "(stopped)";
|
|
|
7615
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:";
|
|
7616
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`;
|
|
7617
7628
|
var SKIPPED_MARKER_BODY = "(skipped)";
|
|
7629
|
+
var SILENT_MARKER_BODY = "(no reply)";
|
|
7618
7630
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
7619
7631
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7620
7632
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
@@ -7861,6 +7873,7 @@ var Dispatcher = class {
|
|
|
7861
7873
|
agentId: payload.agentId,
|
|
7862
7874
|
agentUsername: this.opts.agentUsername,
|
|
7863
7875
|
runtime: turnContext.runtime,
|
|
7876
|
+
hostAccess: turnContext.policy.hostFs,
|
|
7864
7877
|
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
7865
7878
|
title: turnContext.conversation.title,
|
|
7866
7879
|
messageBody: message.body,
|
|
@@ -7922,6 +7935,7 @@ ${reason}`,
|
|
|
7922
7935
|
agentId: payload.agentId,
|
|
7923
7936
|
agentUsername: this.opts.agentUsername,
|
|
7924
7937
|
runtime: turnContext.runtime,
|
|
7938
|
+
hostAccess: turnContext.policy.hostFs,
|
|
7925
7939
|
// CT317/CT319: the trigger message's referenced-entry paths — what the
|
|
7926
7940
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
7927
7941
|
// an older API. The conversation anchor is gone (CT319).
|
|
@@ -7965,7 +7979,10 @@ ${reason}`,
|
|
|
7965
7979
|
}
|
|
7966
7980
|
}
|
|
7967
7981
|
}
|
|
7968
|
-
const turnEnv =
|
|
7982
|
+
const turnEnv = {
|
|
7983
|
+
...hookEnv,
|
|
7984
|
+
CABANE_HOST_ACCESS: turnContext.policy.hostFs ? "1" : "0"
|
|
7985
|
+
};
|
|
7969
7986
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
7970
7987
|
const abortController = new AbortController();
|
|
7971
7988
|
this.aborts.set(key, abortController);
|
|
@@ -8028,9 +8045,8 @@ ${reason}`,
|
|
|
8028
8045
|
...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
|
|
8029
8046
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
8030
8047
|
...effectiveCwd ? { cwd: effectiveCwd } : {},
|
|
8031
|
-
//
|
|
8032
|
-
|
|
8033
|
-
...turnEnv ? { env: turnEnv } : {},
|
|
8048
|
+
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
8049
|
+
env: turnEnv,
|
|
8034
8050
|
mcpServers: resolvedMcpServers,
|
|
8035
8051
|
summonServer,
|
|
8036
8052
|
// CT238: this turn's conversation, forwarded as the active-conversation
|
|
@@ -8268,6 +8284,7 @@ ${reason}`,
|
|
|
8268
8284
|
let contentBearingEvents = 0;
|
|
8269
8285
|
let latestSessionState = request.session;
|
|
8270
8286
|
let settledDiagnostics = null;
|
|
8287
|
+
let silentMarkerEmitted = false;
|
|
8271
8288
|
const committer = new TurnCommitter({
|
|
8272
8289
|
api: this.opts.api,
|
|
8273
8290
|
workspaceId,
|
|
@@ -8459,6 +8476,27 @@ ${reason}`,
|
|
|
8459
8476
|
}
|
|
8460
8477
|
} else {
|
|
8461
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
|
+
}
|
|
8462
8500
|
}
|
|
8463
8501
|
} catch (err) {
|
|
8464
8502
|
okResult = false;
|
|
@@ -8549,7 +8587,7 @@ ${reason}`,
|
|
|
8549
8587
|
sessionFingerprint: fingerprintSessionState(latestSessionState),
|
|
8550
8588
|
eventCounts,
|
|
8551
8589
|
runtimeResultKind,
|
|
8552
|
-
finalSource: outcome === "skipped" || outcome === "cancelled" ? "marker" : committer.finalSource
|
|
8590
|
+
finalSource: outcome === "skipped" || outcome === "cancelled" || silentMarkerEmitted ? "marker" : committer.finalSource
|
|
8553
8591
|
};
|
|
8554
8592
|
body.diagnostics = settledDiagnostics;
|
|
8555
8593
|
if (diagnosticReason && !["usage_capped", "rate_limited", "auth_expired", "cancelled", "skipped"].includes(
|
package/dist/runtime.js
CHANGED
|
@@ -141,6 +141,7 @@ var runPrepareHook = (hook, input) => {
|
|
|
141
141
|
CABANE_AGENT_ID: input.agentId,
|
|
142
142
|
CABANE_AGENT_USERNAME: input.agentUsername,
|
|
143
143
|
CABANE_RUNTIME: input.runtime ?? "",
|
|
144
|
+
CABANE_HOST_ACCESS: input.hostAccess ? "1" : "0",
|
|
144
145
|
// CT319: the conversation anchor is gone. These are kept as DEPRECATED
|
|
145
146
|
// back-compat constants so an old user prepare script that still reads
|
|
146
147
|
// them doesn't crash; a hook should key off CABANE_TRIGGER_ENTRY_PATHS.
|
|
@@ -3106,7 +3107,7 @@ var TurnPump = class {
|
|
|
3106
3107
|
};
|
|
3107
3108
|
}
|
|
3108
3109
|
opts;
|
|
3109
|
-
// `finalEmitted` gates the end-of-turn
|
|
3110
|
+
// `finalEmitted` gates the end-of-turn progress promotion;
|
|
3110
3111
|
// `lastProgressBody` is what we promote to `final` when a clean turn ended on
|
|
3111
3112
|
// a tool call with no closing text.
|
|
3112
3113
|
emittedFinal = false;
|
|
@@ -3121,28 +3122,28 @@ var TurnPump = class {
|
|
|
3121
3122
|
// frozen committer test — drive. Each callback applies the choreography and
|
|
3122
3123
|
// commits through the injected sink.
|
|
3123
3124
|
sink;
|
|
3124
|
-
// End-of-turn
|
|
3125
|
-
//
|
|
3126
|
-
//
|
|
3127
|
-
//
|
|
3128
|
-
//
|
|
3129
|
-
//
|
|
3130
|
-
// 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.
|
|
3131
3132
|
async finalize(ok) {
|
|
3132
|
-
if (!ok || this.opts.signal.aborted || this.emittedFinal) return;
|
|
3133
|
-
const body = this.lastProgressBody
|
|
3133
|
+
if (!ok || this.opts.signal.aborted || this.emittedFinal || !this.lastProgressBody) return;
|
|
3134
|
+
const body = this.lastProgressBody;
|
|
3134
3135
|
const seq = this.opts.nextSeq();
|
|
3135
3136
|
try {
|
|
3136
3137
|
await this.opts.commit.commitMessage({ body, kind: "final", seq });
|
|
3137
3138
|
this.emittedFinal = true;
|
|
3138
3139
|
this.finalReplyBody = body;
|
|
3139
|
-
this.emittedFinalSource =
|
|
3140
|
+
this.emittedFinalSource = "progress_promotion";
|
|
3140
3141
|
} catch (err) {
|
|
3141
|
-
this.opts.onError?.(err, "
|
|
3142
|
+
this.opts.onError?.(err, "progress-promotion");
|
|
3142
3143
|
}
|
|
3143
3144
|
}
|
|
3144
3145
|
// Whether the turn has committed its `final` row — read by the host to decide
|
|
3145
|
-
// whether
|
|
3146
|
+
// whether a marker is owed.
|
|
3146
3147
|
get finalEmitted() {
|
|
3147
3148
|
return this.emittedFinal;
|
|
3148
3149
|
}
|
|
@@ -5285,7 +5286,8 @@ var ENV_ENVELOPE_KEYS = [
|
|
|
5285
5286
|
"CABANE_PLAYGROUND_BIN",
|
|
5286
5287
|
"CABANE_CONVERSATION_ID",
|
|
5287
5288
|
"CABANE_AGENT_ID",
|
|
5288
|
-
"CABANE_CONVERSATION_TITLE"
|
|
5289
|
+
"CABANE_CONVERSATION_TITLE",
|
|
5290
|
+
"CABANE_HOST_ACCESS"
|
|
5289
5291
|
];
|
|
5290
5292
|
function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromptFingerprint = null) {
|
|
5291
5293
|
const { policy, config } = req;
|
|
@@ -6780,7 +6782,6 @@ function pruneOld(dir2, retain) {
|
|
|
6780
6782
|
}
|
|
6781
6783
|
|
|
6782
6784
|
// src/turn-committer.ts
|
|
6783
|
-
var EMPTY_FINAL_BODY = "Done \u2014 see the changes above.";
|
|
6784
6785
|
var TurnCommitter = class {
|
|
6785
6786
|
constructor(deps) {
|
|
6786
6787
|
this.deps = deps;
|
|
@@ -6801,9 +6802,7 @@ var TurnCommitter = class {
|
|
|
6801
6802
|
turnId: deps.turnId,
|
|
6802
6803
|
seq,
|
|
6803
6804
|
parentMessageId: deps.parentMessageId,
|
|
6804
|
-
...kind === "final" ? this.
|
|
6805
|
-
...kind === "final" ? this.askField() : {},
|
|
6806
|
-
...kind === "final" ? this.wakeField() : {}
|
|
6805
|
+
...kind === "final" ? this.turnControlFields() : {}
|
|
6807
6806
|
},
|
|
6808
6807
|
deps.signal
|
|
6809
6808
|
);
|
|
@@ -6850,11 +6849,10 @@ var TurnCommitter = class {
|
|
|
6850
6849
|
commit,
|
|
6851
6850
|
signal: deps.signal,
|
|
6852
6851
|
nextSeq: deps.nextSeq,
|
|
6853
|
-
emptyFinalBody: EMPTY_FINAL_BODY,
|
|
6854
6852
|
onError: (err) => {
|
|
6855
6853
|
deps.log.warn(
|
|
6856
6854
|
{ err: err instanceof Error ? err.message : String(err) },
|
|
6857
|
-
"dispatcher:
|
|
6855
|
+
"dispatcher: progress-promotion commit failed"
|
|
6858
6856
|
);
|
|
6859
6857
|
}
|
|
6860
6858
|
});
|
|
@@ -6880,11 +6878,11 @@ var TurnCommitter = class {
|
|
|
6880
6878
|
this.onError(err, event.type);
|
|
6881
6879
|
}
|
|
6882
6880
|
}
|
|
6883
|
-
// End-of-turn
|
|
6881
|
+
// End-of-turn progress promotion. The held-text flush is now the adapter's
|
|
6884
6882
|
// job (it emits the closing reply as a `text` event before `result`), so this
|
|
6885
|
-
// only
|
|
6886
|
-
//
|
|
6887
|
-
// 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.
|
|
6888
6886
|
async finalize(okResult) {
|
|
6889
6887
|
await this.pump.finalize(okResult);
|
|
6890
6888
|
}
|
|
@@ -6895,6 +6893,19 @@ var TurnCommitter = class {
|
|
|
6895
6893
|
get finalSource() {
|
|
6896
6894
|
return this.pump.finalSource;
|
|
6897
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
|
+
}
|
|
6898
6909
|
// CT183: resolve the in-thread summon into the `dispatch` field for a `final`
|
|
6899
6910
|
// commit. A self-target is stripped here (mirror of the in-app self-strip); the
|
|
6900
6911
|
// server strips it again and resolves / ignores an unknown id.
|
|
@@ -7123,6 +7134,7 @@ var STOPPED_MARKER_BODY = "(stopped)";
|
|
|
7123
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:";
|
|
7124
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`;
|
|
7125
7136
|
var SKIPPED_MARKER_BODY = "(skipped)";
|
|
7137
|
+
var SILENT_MARKER_BODY = "(no reply)";
|
|
7126
7138
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
7127
7139
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7128
7140
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
@@ -7369,6 +7381,7 @@ var Dispatcher = class {
|
|
|
7369
7381
|
agentId: payload.agentId,
|
|
7370
7382
|
agentUsername: this.opts.agentUsername,
|
|
7371
7383
|
runtime: turnContext.runtime,
|
|
7384
|
+
hostAccess: turnContext.policy.hostFs,
|
|
7372
7385
|
triggerEntryPaths: turnContext.conversation.triggerEntryPaths ?? [],
|
|
7373
7386
|
title: turnContext.conversation.title,
|
|
7374
7387
|
messageBody: message.body,
|
|
@@ -7430,6 +7443,7 @@ ${reason}`,
|
|
|
7430
7443
|
agentId: payload.agentId,
|
|
7431
7444
|
agentUsername: this.opts.agentUsername,
|
|
7432
7445
|
runtime: turnContext.runtime,
|
|
7446
|
+
hostAccess: turnContext.policy.hostFs,
|
|
7433
7447
|
// CT317/CT319: the trigger message's referenced-entry paths — what the
|
|
7434
7448
|
// tasker prepare hook keys its per-task env off. Defaults to `[]` for
|
|
7435
7449
|
// an older API. The conversation anchor is gone (CT319).
|
|
@@ -7473,7 +7487,10 @@ ${reason}`,
|
|
|
7473
7487
|
}
|
|
7474
7488
|
}
|
|
7475
7489
|
}
|
|
7476
|
-
const turnEnv =
|
|
7490
|
+
const turnEnv = {
|
|
7491
|
+
...hookEnv,
|
|
7492
|
+
CABANE_HOST_ACCESS: turnContext.policy.hostFs ? "1" : "0"
|
|
7493
|
+
};
|
|
7477
7494
|
const key = runKey(payload.conversationId, payload.agentId);
|
|
7478
7495
|
const abortController = new AbortController();
|
|
7479
7496
|
this.aborts.set(key, abortController);
|
|
@@ -7536,9 +7553,8 @@ ${reason}`,
|
|
|
7536
7553
|
...turnContext.turnToken ? { turnToken: turnContext.turnToken } : {},
|
|
7537
7554
|
// SJ524: the hook-resolved cwd overrides the static local cwd.
|
|
7538
7555
|
...effectiveCwd ? { cwd: effectiveCwd } : {},
|
|
7539
|
-
//
|
|
7540
|
-
|
|
7541
|
-
...turnEnv ? { env: turnEnv } : {},
|
|
7556
|
+
// CT1103: the prepare-hook env plus the authoritative host-access token.
|
|
7557
|
+
env: turnEnv,
|
|
7542
7558
|
mcpServers: resolvedMcpServers,
|
|
7543
7559
|
summonServer,
|
|
7544
7560
|
// CT238: this turn's conversation, forwarded as the active-conversation
|
|
@@ -7776,6 +7792,7 @@ ${reason}`,
|
|
|
7776
7792
|
let contentBearingEvents = 0;
|
|
7777
7793
|
let latestSessionState = request.session;
|
|
7778
7794
|
let settledDiagnostics = null;
|
|
7795
|
+
let silentMarkerEmitted = false;
|
|
7779
7796
|
const committer = new TurnCommitter({
|
|
7780
7797
|
api: this.opts.api,
|
|
7781
7798
|
workspaceId,
|
|
@@ -7967,6 +7984,27 @@ ${reason}`,
|
|
|
7967
7984
|
}
|
|
7968
7985
|
} else {
|
|
7969
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
|
+
}
|
|
7970
8008
|
}
|
|
7971
8009
|
} catch (err) {
|
|
7972
8010
|
okResult = false;
|
|
@@ -8057,7 +8095,7 @@ ${reason}`,
|
|
|
8057
8095
|
sessionFingerprint: fingerprintSessionState(latestSessionState),
|
|
8058
8096
|
eventCounts,
|
|
8059
8097
|
runtimeResultKind,
|
|
8060
|
-
finalSource: outcome === "skipped" || outcome === "cancelled" ? "marker" : committer.finalSource
|
|
8098
|
+
finalSource: outcome === "skipped" || outcome === "cancelled" || silentMarkerEmitted ? "marker" : committer.finalSource
|
|
8061
8099
|
};
|
|
8062
8100
|
body.diagnostics = settledDiagnostics;
|
|
8063
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",
|