@cabane/companion 0.6.12 → 0.6.14
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 +118 -34
- package/dist/runtime.js +118 -34
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6278,7 +6278,7 @@ var ConnectorHealthStore = class {
|
|
|
6278
6278
|
|
|
6279
6279
|
// src/dispatcher.ts
|
|
6280
6280
|
import { randomUUID } from "crypto";
|
|
6281
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10 } from "fs";
|
|
6281
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync10, statSync as statSync2 } from "fs";
|
|
6282
6282
|
import { join as join13 } from "path";
|
|
6283
6283
|
|
|
6284
6284
|
// src/summon.ts
|
|
@@ -6295,6 +6295,8 @@ var SUB_AGENT_TOOL = "sub_agent";
|
|
|
6295
6295
|
var SUB_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUB_AGENT_TOOL}`;
|
|
6296
6296
|
var WAKE_ME_TOOL = "wake_me";
|
|
6297
6297
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
6298
|
+
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
6299
|
+
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
6298
6300
|
function createSummonState() {
|
|
6299
6301
|
return { agentId: null };
|
|
6300
6302
|
}
|
|
@@ -6305,7 +6307,19 @@ function createAskState() {
|
|
|
6305
6307
|
return { targetUserId: null, question: null, headline: null, options: null, questions: null };
|
|
6306
6308
|
}
|
|
6307
6309
|
function createWakeState() {
|
|
6308
|
-
return { afterSeconds: null, at: null, note: null };
|
|
6310
|
+
return { afterSeconds: null, at: null, note: null, cancelled: false };
|
|
6311
|
+
}
|
|
6312
|
+
function wakeCommitField(state) {
|
|
6313
|
+
if (state.cancelled) return { wake: { cancel: true } };
|
|
6314
|
+
const { afterSeconds, at, note } = state;
|
|
6315
|
+
if (!note || afterSeconds === null && at === null) return {};
|
|
6316
|
+
return {
|
|
6317
|
+
wake: {
|
|
6318
|
+
...afterSeconds !== null ? { afterSeconds } : {},
|
|
6319
|
+
...at !== null ? { at } : {},
|
|
6320
|
+
note
|
|
6321
|
+
}
|
|
6322
|
+
};
|
|
6309
6323
|
}
|
|
6310
6324
|
function createSummonMcpServer(summonState, skipState, askState, subAgentCreate, wakeState) {
|
|
6311
6325
|
return createSdkMcpServer({
|
|
@@ -6492,6 +6506,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6492
6506
|
wakeState.afterSeconds = args.afterSeconds ?? null;
|
|
6493
6507
|
wakeState.at = args.at ?? null;
|
|
6494
6508
|
wakeState.note = args.note;
|
|
6509
|
+
wakeState.cancelled = false;
|
|
6495
6510
|
return {
|
|
6496
6511
|
content: [
|
|
6497
6512
|
{
|
|
@@ -6505,6 +6520,31 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6505
6520
|
};
|
|
6506
6521
|
},
|
|
6507
6522
|
{ annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
|
|
6523
|
+
),
|
|
6524
|
+
// CT990: the inverse. No arguments — it can only ever stand down the
|
|
6525
|
+
// caller's own wake in the conversation it's holding a turn in.
|
|
6526
|
+
tool(
|
|
6527
|
+
CANCEL_WAKE_TOOL,
|
|
6528
|
+
'Stand down your own wake \u2014 the inverse of `wake_me`. No arguments: it means "when this turn settles, leave no wake armed for me in this conversation." 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. It writes the same per-turn slot as `wake_me`, so the two are last-call-wins against each other: `cancel_wake` then `wake_me` leaves the NEW wake armed, `wake_me` then `cancel_wake` leaves nothing armed. Like `wake_me` it takes effect when the turn SETTLES, not now. It reaches only your own wake in this conversation \u2014 never a peer\'s, and never a recurring schedule (those are `cabane.schedules.*`).',
|
|
6529
|
+
{},
|
|
6530
|
+
async () => {
|
|
6531
|
+
wakeState.cancelled = true;
|
|
6532
|
+
wakeState.afterSeconds = null;
|
|
6533
|
+
wakeState.at = null;
|
|
6534
|
+
wakeState.note = null;
|
|
6535
|
+
return {
|
|
6536
|
+
content: [
|
|
6537
|
+
{
|
|
6538
|
+
type: "text",
|
|
6539
|
+
text: JSON.stringify({
|
|
6540
|
+
cancelled: true,
|
|
6541
|
+
note: "Recorded. Any wake you have armed in this conversation is stood down when this turn ends. If you had none, nothing happens."
|
|
6542
|
+
})
|
|
6543
|
+
}
|
|
6544
|
+
]
|
|
6545
|
+
};
|
|
6546
|
+
},
|
|
6547
|
+
{ annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
|
|
6508
6548
|
)
|
|
6509
6549
|
] : []
|
|
6510
6550
|
]
|
|
@@ -6580,7 +6620,7 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
6580
6620
|
}
|
|
6581
6621
|
|
|
6582
6622
|
// src/prepared.ts
|
|
6583
|
-
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
|
|
6623
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
|
|
6584
6624
|
import { join as join10 } from "path";
|
|
6585
6625
|
function dirFor(workspaceId) {
|
|
6586
6626
|
return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
@@ -6615,6 +6655,9 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
|
6615
6655
|
"utf8"
|
|
6616
6656
|
);
|
|
6617
6657
|
}
|
|
6658
|
+
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
6659
|
+
rmSync4(pathFor3(workspaceId, conversationId, agentId), { force: true });
|
|
6660
|
+
}
|
|
6618
6661
|
|
|
6619
6662
|
// src/secrets.ts
|
|
6620
6663
|
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
@@ -6708,7 +6751,7 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
6708
6751
|
}
|
|
6709
6752
|
|
|
6710
6753
|
// src/transcript-writer.ts
|
|
6711
|
-
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as
|
|
6754
|
+
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync5 } from "fs";
|
|
6712
6755
|
import { join as join12 } from "path";
|
|
6713
6756
|
function transcriptsDir() {
|
|
6714
6757
|
return join12(cabaneDir(), "transcripts");
|
|
@@ -6779,7 +6822,7 @@ function pruneOld(dir2, retain) {
|
|
|
6779
6822
|
const drop = files.sort().slice(0, files.length - retain);
|
|
6780
6823
|
for (const f of drop) {
|
|
6781
6824
|
try {
|
|
6782
|
-
|
|
6825
|
+
rmSync5(join12(dir2, f), { force: true });
|
|
6783
6826
|
} catch {
|
|
6784
6827
|
}
|
|
6785
6828
|
}
|
|
@@ -6941,16 +6984,13 @@ var TurnCommitter = class {
|
|
|
6941
6984
|
// null = no wake this turn → attach nothing). The server computes `fire_at` and
|
|
6942
6985
|
// arms the CT441 schedule. Kept independent of summon/ask — a turn could
|
|
6943
6986
|
// conceivably ask AND arm a wake.
|
|
6987
|
+
//
|
|
6988
|
+
// CT990: the field now carries three states, and the third can't be spelled by
|
|
6989
|
+
// absence — attaching NOTHING means "leave the standing wake alone", which is
|
|
6990
|
+
// exactly what a stand-down must not do. So a `cancel_wake` turn attaches the
|
|
6991
|
+
// discriminated `{ cancel: true }` payload instead.
|
|
6944
6992
|
wakeField() {
|
|
6945
|
-
|
|
6946
|
-
if (!note || afterSeconds === null && at === null) return {};
|
|
6947
|
-
return {
|
|
6948
|
-
wake: {
|
|
6949
|
-
...afterSeconds !== null ? { afterSeconds } : {},
|
|
6950
|
-
...at !== null ? { at } : {},
|
|
6951
|
-
note
|
|
6952
|
-
}
|
|
6953
|
-
};
|
|
6993
|
+
return wakeCommitField(this.deps.wakeState);
|
|
6954
6994
|
}
|
|
6955
6995
|
};
|
|
6956
6996
|
|
|
@@ -7080,6 +7120,21 @@ var SKIPPED_MARKER_BODY = "(skipped)";
|
|
|
7080
7120
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
7081
7121
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
7082
7122
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
7123
|
+
function checkoutState(cwd) {
|
|
7124
|
+
if (!cwd) return { ok: false, reason: "no checkout was resolved for this turn" };
|
|
7125
|
+
if (!existsSync9(cwd)) return { ok: false, reason: `the checkout directory is gone (${cwd})` };
|
|
7126
|
+
const gitPath = join13(cwd, ".git");
|
|
7127
|
+
if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
|
|
7128
|
+
let stat;
|
|
7129
|
+
try {
|
|
7130
|
+
stat = statSync2(gitPath);
|
|
7131
|
+
} catch (error) {
|
|
7132
|
+
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
7133
|
+
}
|
|
7134
|
+
if (stat.isDirectory() && !existsSync9(join13(gitPath, "HEAD")))
|
|
7135
|
+
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
7136
|
+
return { ok: true, reason: "usable" };
|
|
7137
|
+
}
|
|
7083
7138
|
function runKey(conversationId, agentId) {
|
|
7084
7139
|
return `${conversationId}|${agentId}`;
|
|
7085
7140
|
}
|
|
@@ -7249,7 +7304,15 @@ var Dispatcher = class {
|
|
|
7249
7304
|
const triggerIsPrepareFailure = message.body.startsWith(PREPARE_FAILED_PREFIX);
|
|
7250
7305
|
const prepareFailureDispatch = turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? { dispatch: turnContext.dispatchedByAgentId } : {};
|
|
7251
7306
|
if (prepareHook) {
|
|
7252
|
-
|
|
7307
|
+
let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
7308
|
+
if (cached2 && !checkoutState(cached2.cwd).ok) {
|
|
7309
|
+
turnLog.warn(
|
|
7310
|
+
{ cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
|
|
7311
|
+
"dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
|
|
7312
|
+
);
|
|
7313
|
+
clearPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
7314
|
+
cached2 = null;
|
|
7315
|
+
}
|
|
7253
7316
|
if (cached2) {
|
|
7254
7317
|
effectiveCwd = cached2.cwd;
|
|
7255
7318
|
hookEnv = cached2.env;
|
|
@@ -7510,11 +7573,12 @@ ${reason}`,
|
|
|
7510
7573
|
...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
|
|
7511
7574
|
harnessFingerprint: turnContext.runtime
|
|
7512
7575
|
});
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7576
|
+
const checkout = checkoutState(effectiveCwd);
|
|
7577
|
+
turnLog[proof.ok && checkout.ok ? "info" : "error"](
|
|
7578
|
+
{ workspaceProof: proof, checkout: effectiveCwd ?? null, checkoutState: checkout },
|
|
7579
|
+
`dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout ${checkout.ok ? "usable" : checkout.reason}`
|
|
7516
7580
|
);
|
|
7517
|
-
if (effectiveCwd) {
|
|
7581
|
+
if (effectiveCwd && checkout.ok) {
|
|
7518
7582
|
try {
|
|
7519
7583
|
const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
|
|
7520
7584
|
mkdirSync10(diagnosticDir, { recursive: true });
|
|
@@ -7539,6 +7603,23 @@ ${reason}`,
|
|
|
7539
7603
|
);
|
|
7540
7604
|
}
|
|
7541
7605
|
}
|
|
7606
|
+
if (!checkout.ok) {
|
|
7607
|
+
const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
|
|
7608
|
+
try {
|
|
7609
|
+
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
7610
|
+
body: `**Couldn't prepare your environment.** ${reason}`,
|
|
7611
|
+
kind: "final",
|
|
7612
|
+
turnId,
|
|
7613
|
+
parentMessageId: payload.messageId
|
|
7614
|
+
});
|
|
7615
|
+
} catch (postErr) {
|
|
7616
|
+
turnLog.warn(
|
|
7617
|
+
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
7618
|
+
"dispatcher: checkout-missing notice post failed"
|
|
7619
|
+
);
|
|
7620
|
+
}
|
|
7621
|
+
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
7622
|
+
}
|
|
7542
7623
|
if (!proof.ok) {
|
|
7543
7624
|
const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd ?? "unknown"}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
|
|
7544
7625
|
try {
|
|
@@ -7627,9 +7708,17 @@ ${reason}`,
|
|
|
7627
7708
|
}
|
|
7628
7709
|
}
|
|
7629
7710
|
if (intent.wake) {
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7711
|
+
if ("cancel" in intent.wake) {
|
|
7712
|
+
wakeState.cancelled = true;
|
|
7713
|
+
wakeState.afterSeconds = null;
|
|
7714
|
+
wakeState.at = null;
|
|
7715
|
+
wakeState.note = null;
|
|
7716
|
+
} else {
|
|
7717
|
+
wakeState.cancelled = false;
|
|
7718
|
+
wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
|
|
7719
|
+
wakeState.at = intent.wake.at ?? null;
|
|
7720
|
+
wakeState.note = intent.wake.note;
|
|
7721
|
+
}
|
|
7633
7722
|
}
|
|
7634
7723
|
if (intent.summonAgentId) summonState.agentId = intent.summonAgentId;
|
|
7635
7724
|
if (intent.skipped) {
|
|
@@ -7720,12 +7809,7 @@ ${reason}`,
|
|
|
7720
7809
|
{ reason: skipState.reason, turnId, ok: okResult },
|
|
7721
7810
|
"agent skipped turn (skip_turn)"
|
|
7722
7811
|
);
|
|
7723
|
-
const {
|
|
7724
|
-
const skipWake = wakeNote && (wakeAfter !== null || wakeAt !== null) ? {
|
|
7725
|
-
...wakeAfter !== null ? { afterSeconds: wakeAfter } : {},
|
|
7726
|
-
...wakeAt !== null ? { at: wakeAt } : {},
|
|
7727
|
-
note: wakeNote
|
|
7728
|
-
} : void 0;
|
|
7812
|
+
const { wake: skipWake } = wakeCommitField(wakeState);
|
|
7729
7813
|
try {
|
|
7730
7814
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
7731
7815
|
body: SKIPPED_MARKER_BODY,
|
|
@@ -8100,7 +8184,7 @@ import {
|
|
|
8100
8184
|
readdirSync as readdirSync2,
|
|
8101
8185
|
readFileSync as readFileSync8,
|
|
8102
8186
|
renameSync as renameSync3,
|
|
8103
|
-
rmSync as
|
|
8187
|
+
rmSync as rmSync6,
|
|
8104
8188
|
writeFileSync as writeFileSync7
|
|
8105
8189
|
} from "fs";
|
|
8106
8190
|
import { join as join14 } from "path";
|
|
@@ -8134,7 +8218,7 @@ var Outbox = class {
|
|
|
8134
8218
|
renameSync3(tmp, target);
|
|
8135
8219
|
} catch (err) {
|
|
8136
8220
|
try {
|
|
8137
|
-
|
|
8221
|
+
rmSync6(tmp, { force: true });
|
|
8138
8222
|
} catch {
|
|
8139
8223
|
}
|
|
8140
8224
|
this.log?.warn(
|
|
@@ -8181,7 +8265,7 @@ var Outbox = class {
|
|
|
8181
8265
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
8182
8266
|
remove(turnId, seq) {
|
|
8183
8267
|
try {
|
|
8184
|
-
|
|
8268
|
+
rmSync6(this.fileFor(turnId, seq), { force: true });
|
|
8185
8269
|
} catch {
|
|
8186
8270
|
}
|
|
8187
8271
|
}
|
|
@@ -8200,7 +8284,7 @@ var Outbox = class {
|
|
|
8200
8284
|
"companion outbox: dropping unreadable entry"
|
|
8201
8285
|
);
|
|
8202
8286
|
try {
|
|
8203
|
-
|
|
8287
|
+
rmSync6(full, { force: true });
|
|
8204
8288
|
} catch {
|
|
8205
8289
|
}
|
|
8206
8290
|
}
|
|
@@ -9267,7 +9351,7 @@ function handleUncaught(log, err, origin) {
|
|
|
9267
9351
|
}
|
|
9268
9352
|
|
|
9269
9353
|
// src/crash-marker.ts
|
|
9270
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as
|
|
9354
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
9271
9355
|
import { join as join15 } from "path";
|
|
9272
9356
|
function crashMarkerPath() {
|
|
9273
9357
|
return join15(cabaneDir(), "last-error.json");
|
|
@@ -9282,7 +9366,7 @@ function recordCrash(rec2) {
|
|
|
9282
9366
|
function clearCrash() {
|
|
9283
9367
|
try {
|
|
9284
9368
|
const path3 = crashMarkerPath();
|
|
9285
|
-
if (existsSync11(path3))
|
|
9369
|
+
if (existsSync11(path3)) rmSync7(path3, { force: true });
|
|
9286
9370
|
} catch {
|
|
9287
9371
|
}
|
|
9288
9372
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -5931,7 +5931,7 @@ var ConnectorHealthStore = class {
|
|
|
5931
5931
|
|
|
5932
5932
|
// src/dispatcher.ts
|
|
5933
5933
|
import { randomUUID } from "crypto";
|
|
5934
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9 } from "fs";
|
|
5934
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync9, statSync as statSync2 } from "fs";
|
|
5935
5935
|
import { join as join13 } from "path";
|
|
5936
5936
|
|
|
5937
5937
|
// src/summon.ts
|
|
@@ -5948,6 +5948,8 @@ var SUB_AGENT_TOOL = "sub_agent";
|
|
|
5948
5948
|
var SUB_AGENT_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${SUB_AGENT_TOOL}`;
|
|
5949
5949
|
var WAKE_ME_TOOL = "wake_me";
|
|
5950
5950
|
var WAKE_ME_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${WAKE_ME_TOOL}`;
|
|
5951
|
+
var CANCEL_WAKE_TOOL = "cancel_wake";
|
|
5952
|
+
var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_TOOL}`;
|
|
5951
5953
|
function createSummonState() {
|
|
5952
5954
|
return { agentId: null };
|
|
5953
5955
|
}
|
|
@@ -5958,7 +5960,19 @@ function createAskState() {
|
|
|
5958
5960
|
return { targetUserId: null, question: null, headline: null, options: null, questions: null };
|
|
5959
5961
|
}
|
|
5960
5962
|
function createWakeState() {
|
|
5961
|
-
return { afterSeconds: null, at: null, note: null };
|
|
5963
|
+
return { afterSeconds: null, at: null, note: null, cancelled: false };
|
|
5964
|
+
}
|
|
5965
|
+
function wakeCommitField(state) {
|
|
5966
|
+
if (state.cancelled) return { wake: { cancel: true } };
|
|
5967
|
+
const { afterSeconds, at, note } = state;
|
|
5968
|
+
if (!note || afterSeconds === null && at === null) return {};
|
|
5969
|
+
return {
|
|
5970
|
+
wake: {
|
|
5971
|
+
...afterSeconds !== null ? { afterSeconds } : {},
|
|
5972
|
+
...at !== null ? { at } : {},
|
|
5973
|
+
note
|
|
5974
|
+
}
|
|
5975
|
+
};
|
|
5962
5976
|
}
|
|
5963
5977
|
function createSummonMcpServer(summonState, skipState, askState, subAgentCreate, wakeState) {
|
|
5964
5978
|
return createSdkMcpServer({
|
|
@@ -6145,6 +6159,7 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6145
6159
|
wakeState.afterSeconds = args.afterSeconds ?? null;
|
|
6146
6160
|
wakeState.at = args.at ?? null;
|
|
6147
6161
|
wakeState.note = args.note;
|
|
6162
|
+
wakeState.cancelled = false;
|
|
6148
6163
|
return {
|
|
6149
6164
|
content: [
|
|
6150
6165
|
{
|
|
@@ -6158,6 +6173,31 @@ function createSummonMcpServer(summonState, skipState, askState, subAgentCreate,
|
|
|
6158
6173
|
};
|
|
6159
6174
|
},
|
|
6160
6175
|
{ annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
|
|
6176
|
+
),
|
|
6177
|
+
// CT990: the inverse. No arguments — it can only ever stand down the
|
|
6178
|
+
// caller's own wake in the conversation it's holding a turn in.
|
|
6179
|
+
tool(
|
|
6180
|
+
CANCEL_WAKE_TOOL,
|
|
6181
|
+
'Stand down your own wake \u2014 the inverse of `wake_me`. No arguments: it means "when this turn settles, leave no wake armed for me in this conversation." 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. It writes the same per-turn slot as `wake_me`, so the two are last-call-wins against each other: `cancel_wake` then `wake_me` leaves the NEW wake armed, `wake_me` then `cancel_wake` leaves nothing armed. Like `wake_me` it takes effect when the turn SETTLES, not now. It reaches only your own wake in this conversation \u2014 never a peer\'s, and never a recurring schedule (those are `cabane.schedules.*`).',
|
|
6182
|
+
{},
|
|
6183
|
+
async () => {
|
|
6184
|
+
wakeState.cancelled = true;
|
|
6185
|
+
wakeState.afterSeconds = null;
|
|
6186
|
+
wakeState.at = null;
|
|
6187
|
+
wakeState.note = null;
|
|
6188
|
+
return {
|
|
6189
|
+
content: [
|
|
6190
|
+
{
|
|
6191
|
+
type: "text",
|
|
6192
|
+
text: JSON.stringify({
|
|
6193
|
+
cancelled: true,
|
|
6194
|
+
note: "Recorded. Any wake you have armed in this conversation is stood down when this turn ends. If you had none, nothing happens."
|
|
6195
|
+
})
|
|
6196
|
+
}
|
|
6197
|
+
]
|
|
6198
|
+
};
|
|
6199
|
+
},
|
|
6200
|
+
{ annotations: { readOnlyHint: true, openWorldHint: false }, alwaysLoad: true }
|
|
6161
6201
|
)
|
|
6162
6202
|
] : []
|
|
6163
6203
|
]
|
|
@@ -6233,7 +6273,7 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
6233
6273
|
}
|
|
6234
6274
|
|
|
6235
6275
|
// src/prepared.ts
|
|
6236
|
-
import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
|
|
6276
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
|
|
6237
6277
|
import { join as join10 } from "path";
|
|
6238
6278
|
function dirFor(workspaceId) {
|
|
6239
6279
|
return join10(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
@@ -6268,6 +6308,9 @@ function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
|
6268
6308
|
"utf8"
|
|
6269
6309
|
);
|
|
6270
6310
|
}
|
|
6311
|
+
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
6312
|
+
rmSync4(pathFor3(workspaceId, conversationId, agentId), { force: true });
|
|
6313
|
+
}
|
|
6271
6314
|
|
|
6272
6315
|
// src/secrets.ts
|
|
6273
6316
|
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
@@ -6361,7 +6404,7 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
6361
6404
|
}
|
|
6362
6405
|
|
|
6363
6406
|
// src/transcript-writer.ts
|
|
6364
|
-
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync8, readdirSync, rmSync as
|
|
6407
|
+
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync5 } from "fs";
|
|
6365
6408
|
import { join as join12 } from "path";
|
|
6366
6409
|
function transcriptsDir() {
|
|
6367
6410
|
return join12(cabaneDir(), "transcripts");
|
|
@@ -6432,7 +6475,7 @@ function pruneOld(dir2, retain) {
|
|
|
6432
6475
|
const drop = files.sort().slice(0, files.length - retain);
|
|
6433
6476
|
for (const f of drop) {
|
|
6434
6477
|
try {
|
|
6435
|
-
|
|
6478
|
+
rmSync5(join12(dir2, f), { force: true });
|
|
6436
6479
|
} catch {
|
|
6437
6480
|
}
|
|
6438
6481
|
}
|
|
@@ -6594,16 +6637,13 @@ var TurnCommitter = class {
|
|
|
6594
6637
|
// null = no wake this turn → attach nothing). The server computes `fire_at` and
|
|
6595
6638
|
// arms the CT441 schedule. Kept independent of summon/ask — a turn could
|
|
6596
6639
|
// conceivably ask AND arm a wake.
|
|
6640
|
+
//
|
|
6641
|
+
// CT990: the field now carries three states, and the third can't be spelled by
|
|
6642
|
+
// absence — attaching NOTHING means "leave the standing wake alone", which is
|
|
6643
|
+
// exactly what a stand-down must not do. So a `cancel_wake` turn attaches the
|
|
6644
|
+
// discriminated `{ cancel: true }` payload instead.
|
|
6597
6645
|
wakeField() {
|
|
6598
|
-
|
|
6599
|
-
if (!note || afterSeconds === null && at === null) return {};
|
|
6600
|
-
return {
|
|
6601
|
-
wake: {
|
|
6602
|
-
...afterSeconds !== null ? { afterSeconds } : {},
|
|
6603
|
-
...at !== null ? { at } : {},
|
|
6604
|
-
note
|
|
6605
|
-
}
|
|
6606
|
-
};
|
|
6646
|
+
return wakeCommitField(this.deps.wakeState);
|
|
6607
6647
|
}
|
|
6608
6648
|
};
|
|
6609
6649
|
|
|
@@ -6733,6 +6773,21 @@ var SKIPPED_MARKER_BODY = "(skipped)";
|
|
|
6733
6773
|
var DEFAULT_PREPARING_ROW_DELAY_MS = 1500;
|
|
6734
6774
|
var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
6735
6775
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
6776
|
+
function checkoutState(cwd) {
|
|
6777
|
+
if (!cwd) return { ok: false, reason: "no checkout was resolved for this turn" };
|
|
6778
|
+
if (!existsSync9(cwd)) return { ok: false, reason: `the checkout directory is gone (${cwd})` };
|
|
6779
|
+
const gitPath = join13(cwd, ".git");
|
|
6780
|
+
if (!existsSync9(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
|
|
6781
|
+
let stat;
|
|
6782
|
+
try {
|
|
6783
|
+
stat = statSync2(gitPath);
|
|
6784
|
+
} catch (error) {
|
|
6785
|
+
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
6786
|
+
}
|
|
6787
|
+
if (stat.isDirectory() && !existsSync9(join13(gitPath, "HEAD")))
|
|
6788
|
+
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
6789
|
+
return { ok: true, reason: "usable" };
|
|
6790
|
+
}
|
|
6736
6791
|
function runKey(conversationId, agentId) {
|
|
6737
6792
|
return `${conversationId}|${agentId}`;
|
|
6738
6793
|
}
|
|
@@ -6902,7 +6957,15 @@ var Dispatcher = class {
|
|
|
6902
6957
|
const triggerIsPrepareFailure = message.body.startsWith(PREPARE_FAILED_PREFIX);
|
|
6903
6958
|
const prepareFailureDispatch = turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? { dispatch: turnContext.dispatchedByAgentId } : {};
|
|
6904
6959
|
if (prepareHook) {
|
|
6905
|
-
|
|
6960
|
+
let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
6961
|
+
if (cached2 && !checkoutState(cached2.cwd).ok) {
|
|
6962
|
+
turnLog.warn(
|
|
6963
|
+
{ cwd: cached2.cwd, reason: checkoutState(cached2.cwd).reason },
|
|
6964
|
+
"dispatcher: the prepared checkout is no longer usable \u2014 re-running the prepare hook to restore it"
|
|
6965
|
+
);
|
|
6966
|
+
clearPrepared(workspaceId, payload.conversationId, payload.agentId);
|
|
6967
|
+
cached2 = null;
|
|
6968
|
+
}
|
|
6906
6969
|
if (cached2) {
|
|
6907
6970
|
effectiveCwd = cached2.cwd;
|
|
6908
6971
|
hookEnv = cached2.env;
|
|
@@ -7163,11 +7226,12 @@ ${reason}`,
|
|
|
7163
7226
|
...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
|
|
7164
7227
|
harnessFingerprint: turnContext.runtime
|
|
7165
7228
|
});
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7229
|
+
const checkout = checkoutState(effectiveCwd);
|
|
7230
|
+
turnLog[proof.ok && checkout.ok ? "info" : "error"](
|
|
7231
|
+
{ workspaceProof: proof, checkout: effectiveCwd ?? null, checkoutState: checkout },
|
|
7232
|
+
`dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout ${checkout.ok ? "usable" : checkout.reason}`
|
|
7169
7233
|
);
|
|
7170
|
-
if (effectiveCwd) {
|
|
7234
|
+
if (effectiveCwd && checkout.ok) {
|
|
7171
7235
|
try {
|
|
7172
7236
|
const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
|
|
7173
7237
|
mkdirSync9(diagnosticDir, { recursive: true });
|
|
@@ -7192,6 +7256,23 @@ ${reason}`,
|
|
|
7192
7256
|
);
|
|
7193
7257
|
}
|
|
7194
7258
|
}
|
|
7259
|
+
if (!checkout.ok) {
|
|
7260
|
+
const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
|
|
7261
|
+
try {
|
|
7262
|
+
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
7263
|
+
body: `**Couldn't prepare your environment.** ${reason}`,
|
|
7264
|
+
kind: "final",
|
|
7265
|
+
turnId,
|
|
7266
|
+
parentMessageId: payload.messageId
|
|
7267
|
+
});
|
|
7268
|
+
} catch (postErr) {
|
|
7269
|
+
turnLog.warn(
|
|
7270
|
+
{ err: postErr instanceof Error ? postErr.message : String(postErr) },
|
|
7271
|
+
"dispatcher: checkout-missing notice post failed"
|
|
7272
|
+
);
|
|
7273
|
+
}
|
|
7274
|
+
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
7275
|
+
}
|
|
7195
7276
|
if (!proof.ok) {
|
|
7196
7277
|
const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd ?? "unknown"}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
|
|
7197
7278
|
try {
|
|
@@ -7280,9 +7361,17 @@ ${reason}`,
|
|
|
7280
7361
|
}
|
|
7281
7362
|
}
|
|
7282
7363
|
if (intent.wake) {
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7364
|
+
if ("cancel" in intent.wake) {
|
|
7365
|
+
wakeState.cancelled = true;
|
|
7366
|
+
wakeState.afterSeconds = null;
|
|
7367
|
+
wakeState.at = null;
|
|
7368
|
+
wakeState.note = null;
|
|
7369
|
+
} else {
|
|
7370
|
+
wakeState.cancelled = false;
|
|
7371
|
+
wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
|
|
7372
|
+
wakeState.at = intent.wake.at ?? null;
|
|
7373
|
+
wakeState.note = intent.wake.note;
|
|
7374
|
+
}
|
|
7286
7375
|
}
|
|
7287
7376
|
if (intent.summonAgentId) summonState.agentId = intent.summonAgentId;
|
|
7288
7377
|
if (intent.skipped) {
|
|
@@ -7373,12 +7462,7 @@ ${reason}`,
|
|
|
7373
7462
|
{ reason: skipState.reason, turnId, ok: okResult },
|
|
7374
7463
|
"agent skipped turn (skip_turn)"
|
|
7375
7464
|
);
|
|
7376
|
-
const {
|
|
7377
|
-
const skipWake = wakeNote && (wakeAfter !== null || wakeAt !== null) ? {
|
|
7378
|
-
...wakeAfter !== null ? { afterSeconds: wakeAfter } : {},
|
|
7379
|
-
...wakeAt !== null ? { at: wakeAt } : {},
|
|
7380
|
-
note: wakeNote
|
|
7381
|
-
} : void 0;
|
|
7465
|
+
const { wake: skipWake } = wakeCommitField(wakeState);
|
|
7382
7466
|
try {
|
|
7383
7467
|
await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
|
|
7384
7468
|
body: SKIPPED_MARKER_BODY,
|
|
@@ -7753,7 +7837,7 @@ import {
|
|
|
7753
7837
|
readdirSync as readdirSync2,
|
|
7754
7838
|
readFileSync as readFileSync8,
|
|
7755
7839
|
renameSync as renameSync3,
|
|
7756
|
-
rmSync as
|
|
7840
|
+
rmSync as rmSync6,
|
|
7757
7841
|
writeFileSync as writeFileSync7
|
|
7758
7842
|
} from "fs";
|
|
7759
7843
|
import { join as join14 } from "path";
|
|
@@ -7787,7 +7871,7 @@ var Outbox = class {
|
|
|
7787
7871
|
renameSync3(tmp, target);
|
|
7788
7872
|
} catch (err) {
|
|
7789
7873
|
try {
|
|
7790
|
-
|
|
7874
|
+
rmSync6(tmp, { force: true });
|
|
7791
7875
|
} catch {
|
|
7792
7876
|
}
|
|
7793
7877
|
this.log?.warn(
|
|
@@ -7834,7 +7918,7 @@ var Outbox = class {
|
|
|
7834
7918
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
7835
7919
|
remove(turnId, seq) {
|
|
7836
7920
|
try {
|
|
7837
|
-
|
|
7921
|
+
rmSync6(this.fileFor(turnId, seq), { force: true });
|
|
7838
7922
|
} catch {
|
|
7839
7923
|
}
|
|
7840
7924
|
}
|
|
@@ -7853,7 +7937,7 @@ var Outbox = class {
|
|
|
7853
7937
|
"companion outbox: dropping unreadable entry"
|
|
7854
7938
|
);
|
|
7855
7939
|
try {
|
|
7856
|
-
|
|
7940
|
+
rmSync6(full, { force: true });
|
|
7857
7941
|
} catch {
|
|
7858
7942
|
}
|
|
7859
7943
|
}
|
|
@@ -8920,7 +9004,7 @@ function handleUncaught(log, err, origin) {
|
|
|
8920
9004
|
}
|
|
8921
9005
|
|
|
8922
9006
|
// src/crash-marker.ts
|
|
8923
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as
|
|
9007
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
8924
9008
|
import { join as join15 } from "path";
|
|
8925
9009
|
function crashMarkerPath() {
|
|
8926
9010
|
return join15(cabaneDir(), "last-error.json");
|
|
@@ -8935,7 +9019,7 @@ function recordCrash(rec) {
|
|
|
8935
9019
|
function clearCrash() {
|
|
8936
9020
|
try {
|
|
8937
9021
|
const path3 = crashMarkerPath();
|
|
8938
|
-
if (existsSync11(path3))
|
|
9022
|
+
if (existsSync11(path3)) rmSync7(path3, { force: true });
|
|
8939
9023
|
} catch {
|
|
8940
9024
|
}
|
|
8941
9025
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.14",
|
|
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",
|