@cabane/companion 0.6.11 → 0.6.13

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 CHANGED
@@ -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
  ]
@@ -6941,16 +6981,13 @@ var TurnCommitter = class {
6941
6981
  // null = no wake this turn → attach nothing). The server computes `fire_at` and
6942
6982
  // arms the CT441 schedule. Kept independent of summon/ask — a turn could
6943
6983
  // conceivably ask AND arm a wake.
6984
+ //
6985
+ // CT990: the field now carries three states, and the third can't be spelled by
6986
+ // absence — attaching NOTHING means "leave the standing wake alone", which is
6987
+ // exactly what a stand-down must not do. So a `cancel_wake` turn attaches the
6988
+ // discriminated `{ cancel: true }` payload instead.
6944
6989
  wakeField() {
6945
- const { afterSeconds, at, note } = this.deps.wakeState;
6946
- if (!note || afterSeconds === null && at === null) return {};
6947
- return {
6948
- wake: {
6949
- ...afterSeconds !== null ? { afterSeconds } : {},
6950
- ...at !== null ? { at } : {},
6951
- note
6952
- }
6953
- };
6990
+ return wakeCommitField(this.deps.wakeState);
6954
6991
  }
6955
6992
  };
6956
6993
 
@@ -7246,6 +7283,8 @@ var Dispatcher = class {
7246
7283
  effectiveCwd = void 0;
7247
7284
  }
7248
7285
  let hookEnv;
7286
+ const triggerIsPrepareFailure = message.body.startsWith(PREPARE_FAILED_PREFIX);
7287
+ const prepareFailureDispatch = turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? { dispatch: turnContext.dispatchedByAgentId } : {};
7249
7288
  if (prepareHook) {
7250
7289
  const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
7251
7290
  if (cached2) {
@@ -7275,7 +7314,8 @@ var Dispatcher = class {
7275
7314
  ${reason}`,
7276
7315
  kind: "final",
7277
7316
  turnId,
7278
- parentMessageId: payload.messageId
7317
+ parentMessageId: payload.messageId,
7318
+ ...prepareFailureDispatch
7279
7319
  });
7280
7320
  } catch (postErr) {
7281
7321
  turnLog.warn(
@@ -7349,7 +7389,8 @@ ${reason}`,
7349
7389
  kind: "final",
7350
7390
  turnId,
7351
7391
  // CT113: stamp the parent even on the prepare-failed close.
7352
- parentMessageId: payload.messageId
7392
+ parentMessageId: payload.messageId,
7393
+ ...prepareFailureDispatch
7353
7394
  });
7354
7395
  } catch (postErr) {
7355
7396
  turnLog.warn(
@@ -7623,9 +7664,17 @@ ${reason}`,
7623
7664
  }
7624
7665
  }
7625
7666
  if (intent.wake) {
7626
- wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
7627
- wakeState.at = intent.wake.at ?? null;
7628
- wakeState.note = intent.wake.note;
7667
+ if ("cancel" in intent.wake) {
7668
+ wakeState.cancelled = true;
7669
+ wakeState.afterSeconds = null;
7670
+ wakeState.at = null;
7671
+ wakeState.note = null;
7672
+ } else {
7673
+ wakeState.cancelled = false;
7674
+ wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
7675
+ wakeState.at = intent.wake.at ?? null;
7676
+ wakeState.note = intent.wake.note;
7677
+ }
7629
7678
  }
7630
7679
  if (intent.summonAgentId) summonState.agentId = intent.summonAgentId;
7631
7680
  if (intent.skipped) {
@@ -7716,12 +7765,7 @@ ${reason}`,
7716
7765
  { reason: skipState.reason, turnId, ok: okResult },
7717
7766
  "agent skipped turn (skip_turn)"
7718
7767
  );
7719
- const { afterSeconds: wakeAfter, at: wakeAt, note: wakeNote } = wakeState;
7720
- const skipWake = wakeNote && (wakeAfter !== null || wakeAt !== null) ? {
7721
- ...wakeAfter !== null ? { afterSeconds: wakeAfter } : {},
7722
- ...wakeAt !== null ? { at: wakeAt } : {},
7723
- note: wakeNote
7724
- } : void 0;
7768
+ const { wake: skipWake } = wakeCommitField(wakeState);
7725
7769
  try {
7726
7770
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7727
7771
  body: SKIPPED_MARKER_BODY,
package/dist/runtime.js CHANGED
@@ -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
  ]
@@ -6594,16 +6634,13 @@ var TurnCommitter = class {
6594
6634
  // null = no wake this turn → attach nothing). The server computes `fire_at` and
6595
6635
  // arms the CT441 schedule. Kept independent of summon/ask — a turn could
6596
6636
  // conceivably ask AND arm a wake.
6637
+ //
6638
+ // CT990: the field now carries three states, and the third can't be spelled by
6639
+ // absence — attaching NOTHING means "leave the standing wake alone", which is
6640
+ // exactly what a stand-down must not do. So a `cancel_wake` turn attaches the
6641
+ // discriminated `{ cancel: true }` payload instead.
6597
6642
  wakeField() {
6598
- const { afterSeconds, at, note } = this.deps.wakeState;
6599
- if (!note || afterSeconds === null && at === null) return {};
6600
- return {
6601
- wake: {
6602
- ...afterSeconds !== null ? { afterSeconds } : {},
6603
- ...at !== null ? { at } : {},
6604
- note
6605
- }
6606
- };
6643
+ return wakeCommitField(this.deps.wakeState);
6607
6644
  }
6608
6645
  };
6609
6646
 
@@ -6899,6 +6936,8 @@ var Dispatcher = class {
6899
6936
  effectiveCwd = void 0;
6900
6937
  }
6901
6938
  let hookEnv;
6939
+ const triggerIsPrepareFailure = message.body.startsWith(PREPARE_FAILED_PREFIX);
6940
+ const prepareFailureDispatch = turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? { dispatch: turnContext.dispatchedByAgentId } : {};
6902
6941
  if (prepareHook) {
6903
6942
  const cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
6904
6943
  if (cached2) {
@@ -6928,7 +6967,8 @@ var Dispatcher = class {
6928
6967
  ${reason}`,
6929
6968
  kind: "final",
6930
6969
  turnId,
6931
- parentMessageId: payload.messageId
6970
+ parentMessageId: payload.messageId,
6971
+ ...prepareFailureDispatch
6932
6972
  });
6933
6973
  } catch (postErr) {
6934
6974
  turnLog.warn(
@@ -7002,7 +7042,8 @@ ${reason}`,
7002
7042
  kind: "final",
7003
7043
  turnId,
7004
7044
  // CT113: stamp the parent even on the prepare-failed close.
7005
- parentMessageId: payload.messageId
7045
+ parentMessageId: payload.messageId,
7046
+ ...prepareFailureDispatch
7006
7047
  });
7007
7048
  } catch (postErr) {
7008
7049
  turnLog.warn(
@@ -7276,9 +7317,17 @@ ${reason}`,
7276
7317
  }
7277
7318
  }
7278
7319
  if (intent.wake) {
7279
- wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
7280
- wakeState.at = intent.wake.at ?? null;
7281
- wakeState.note = intent.wake.note;
7320
+ if ("cancel" in intent.wake) {
7321
+ wakeState.cancelled = true;
7322
+ wakeState.afterSeconds = null;
7323
+ wakeState.at = null;
7324
+ wakeState.note = null;
7325
+ } else {
7326
+ wakeState.cancelled = false;
7327
+ wakeState.afterSeconds = intent.wake.afterSeconds ?? null;
7328
+ wakeState.at = intent.wake.at ?? null;
7329
+ wakeState.note = intent.wake.note;
7330
+ }
7282
7331
  }
7283
7332
  if (intent.summonAgentId) summonState.agentId = intent.summonAgentId;
7284
7333
  if (intent.skipped) {
@@ -7369,12 +7418,7 @@ ${reason}`,
7369
7418
  { reason: skipState.reason, turnId, ok: okResult },
7370
7419
  "agent skipped turn (skip_turn)"
7371
7420
  );
7372
- const { afterSeconds: wakeAfter, at: wakeAt, note: wakeNote } = wakeState;
7373
- const skipWake = wakeNote && (wakeAfter !== null || wakeAt !== null) ? {
7374
- ...wakeAfter !== null ? { afterSeconds: wakeAfter } : {},
7375
- ...wakeAt !== null ? { at: wakeAt } : {},
7376
- note: wakeNote
7377
- } : void 0;
7421
+ const { wake: skipWake } = wakeCommitField(wakeState);
7378
7422
  try {
7379
7423
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7380
7424
  body: SKIPPED_MARKER_BODY,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.11",
3
+ "version": "0.6.13",
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",