@cabane/companion 0.6.87 → 0.6.88

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
@@ -2647,6 +2647,35 @@ var CabaneApi = class {
2647
2647
  signal
2648
2648
  );
2649
2649
  }
2650
+ // CT1357: post ONE ordinary addressed message — not turn speech, no `turnId`
2651
+ // — so `dispatch` is honoured and the target agent is woken. The turn-commit
2652
+ // fork ignores `dispatch` (an act rides its own call now), which leaves this
2653
+ // as the only way a companion can address a peer OUTSIDE a running turn.
2654
+ //
2655
+ // The prepare-failure handback is exactly that case and the reason this
2656
+ // exists. It fires before the turn is ever admitted — the active-run PATCH
2657
+ // that admits it comes after the prepare hook — so `turnAct` is not available:
2658
+ // its route resolves the turn first and 404s when there is none. The failure
2659
+ // notice's own commit is what admits the turn, and by then it has also settled
2660
+ // it. An ordinary addressed post depends on no lease at all, which is what
2661
+ // makes it the right shape here.
2662
+ //
2663
+ // Retried like any durable write, and therefore REQUIRING `idempotencyKey`:
2664
+ // this is not outbox-queued (there is no `(turnId, seq)` for a non-turn row),
2665
+ // so without a key a committed request whose response was lost — or a
2666
+ // redelivered dispatch event — writes a second row and wakes the peer twice.
2667
+ // The server dedupes on `(conversation_id, idempotency_key)`: the repeat
2668
+ // returns the existing row and raises no second dispatch (CT239). Required,
2669
+ // not optional, so a future caller cannot omit it and silently lose the
2670
+ // guarantee.
2671
+ postAddressedMessage(workspaceId, conversationId, body, signal) {
2672
+ return this.request(
2673
+ "POST",
2674
+ `/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`,
2675
+ body,
2676
+ signal ? { retry: true, signal } : { retry: true }
2677
+ );
2678
+ }
2650
2679
  // SJ477: report one tool-activity transition (start / done / error) for the
2651
2680
  // live activity cards. Transient — the server publishes an `agent_activity`
2652
2681
  // SSE and writes no row. Agent-PAT authed; the URL `:agentId` must match the
@@ -8617,12 +8646,31 @@ var TurnExecution = class {
8617
8646
  }
8618
8647
  let hookEnv;
8619
8648
  const triggerIsPrepareFailure = this.turnContext.message.body.startsWith(PREPARE_FAILED_PREFIX);
8620
- const prepareFailureDispatch = this.turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? {
8621
- dispatch: this.turnContext.dispatchedByAgentId,
8622
- dispatchBody: `${PREPARE_FAILED_PREFIX}
8649
+ const sendPrepareFailureHandback = async () => {
8650
+ if (!this.turnContext.dispatchedByAgentId || triggerIsPrepareFailure) return;
8651
+ try {
8652
+ await this.opts.api.postAddressedMessage(workspaceId, payload.conversationId, {
8653
+ body: `${PREPARE_FAILED_PREFIX}
8623
8654
 
8624
- The dispatched turn could not start. Re-dispatch it after repairing the preparation failure shown in this conversation.`
8625
- } : {};
8655
+ The dispatched turn could not start. Re-dispatch it after repairing the preparation failure shown in this conversation.`,
8656
+ dispatch: this.turnContext.dispatchedByAgentId,
8657
+ // Keyed on the TRIGGER, not the turn: a redelivered dispatch event is
8658
+ // the replay this has to survive, and it mints a fresh `turnId` while
8659
+ // carrying the same `messageId`. One handback per failed pickup of a
8660
+ // given trigger — which is also CT992's rule, that the first failure
8661
+ // is the one carrying information. A genuine re-dispatch after a
8662
+ // repair authors a NEW trigger message, so it keys differently and
8663
+ // hands back again.
8664
+ idempotencyKey: `prepare-failure-handback:${payload.messageId}`,
8665
+ parentMessageId: payload.messageId
8666
+ });
8667
+ } catch (err) {
8668
+ turnLog.warn(
8669
+ { err: err instanceof Error ? err.message : String(err) },
8670
+ "dispatcher: prepare-failure handback failed"
8671
+ );
8672
+ }
8673
+ };
8626
8674
  if (prepareHook) {
8627
8675
  let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
8628
8676
  if (cached2 && !checkoutState(cached2.cwd).ok) {
@@ -8661,8 +8709,7 @@ The dispatched turn could not start. Re-dispatch it after repairing the preparat
8661
8709
  ${reason}`,
8662
8710
  kind: "final",
8663
8711
  turnId,
8664
- parentMessageId: payload.messageId,
8665
- ...prepareFailureDispatch
8712
+ parentMessageId: payload.messageId
8666
8713
  });
8667
8714
  } catch (postErr) {
8668
8715
  turnLog.warn(
@@ -8670,6 +8717,7 @@ ${reason}`,
8670
8717
  "dispatcher: prepare-rejection post failed"
8671
8718
  );
8672
8719
  }
8720
+ await sendPrepareFailureHandback();
8673
8721
  throw this.concluded(`prepare_failed: ${reason}`);
8674
8722
  }
8675
8723
  }
@@ -8737,8 +8785,7 @@ ${reason}`,
8737
8785
  kind: "final",
8738
8786
  turnId,
8739
8787
  // CT113: stamp the parent even on the prepare-failed close.
8740
- parentMessageId: payload.messageId,
8741
- ...prepareFailureDispatch
8788
+ parentMessageId: payload.messageId
8742
8789
  });
8743
8790
  } catch (postErr) {
8744
8791
  turnLog.warn(
@@ -8746,6 +8793,7 @@ ${reason}`,
8746
8793
  "dispatcher: prepare-failure post failed"
8747
8794
  );
8748
8795
  }
8796
+ await sendPrepareFailureHandback();
8749
8797
  const failReason = `prepare_failed: ${reason}`;
8750
8798
  throw this.concluded(failReason);
8751
8799
  }
package/dist/runtime.js CHANGED
@@ -2065,6 +2065,35 @@ var CabaneApi = class {
2065
2065
  signal
2066
2066
  );
2067
2067
  }
2068
+ // CT1357: post ONE ordinary addressed message — not turn speech, no `turnId`
2069
+ // — so `dispatch` is honoured and the target agent is woken. The turn-commit
2070
+ // fork ignores `dispatch` (an act rides its own call now), which leaves this
2071
+ // as the only way a companion can address a peer OUTSIDE a running turn.
2072
+ //
2073
+ // The prepare-failure handback is exactly that case and the reason this
2074
+ // exists. It fires before the turn is ever admitted — the active-run PATCH
2075
+ // that admits it comes after the prepare hook — so `turnAct` is not available:
2076
+ // its route resolves the turn first and 404s when there is none. The failure
2077
+ // notice's own commit is what admits the turn, and by then it has also settled
2078
+ // it. An ordinary addressed post depends on no lease at all, which is what
2079
+ // makes it the right shape here.
2080
+ //
2081
+ // Retried like any durable write, and therefore REQUIRING `idempotencyKey`:
2082
+ // this is not outbox-queued (there is no `(turnId, seq)` for a non-turn row),
2083
+ // so without a key a committed request whose response was lost — or a
2084
+ // redelivered dispatch event — writes a second row and wakes the peer twice.
2085
+ // The server dedupes on `(conversation_id, idempotency_key)`: the repeat
2086
+ // returns the existing row and raises no second dispatch (CT239). Required,
2087
+ // not optional, so a future caller cannot omit it and silently lose the
2088
+ // guarantee.
2089
+ postAddressedMessage(workspaceId, conversationId, body, signal) {
2090
+ return this.request(
2091
+ "POST",
2092
+ `/api/workspaces/${workspaceId}/conversations/${conversationId}/messages`,
2093
+ body,
2094
+ signal ? { retry: true, signal } : { retry: true }
2095
+ );
2096
+ }
2068
2097
  // SJ477: report one tool-activity transition (start / done / error) for the
2069
2098
  // live activity cards. Transient — the server publishes an `agent_activity`
2070
2099
  // SSE and writes no row. Agent-PAT authed; the URL `:agentId` must match the
@@ -8114,12 +8143,31 @@ var TurnExecution = class {
8114
8143
  }
8115
8144
  let hookEnv;
8116
8145
  const triggerIsPrepareFailure = this.turnContext.message.body.startsWith(PREPARE_FAILED_PREFIX);
8117
- const prepareFailureDispatch = this.turnContext.dispatchedByAgentId && !triggerIsPrepareFailure ? {
8118
- dispatch: this.turnContext.dispatchedByAgentId,
8119
- dispatchBody: `${PREPARE_FAILED_PREFIX}
8146
+ const sendPrepareFailureHandback = async () => {
8147
+ if (!this.turnContext.dispatchedByAgentId || triggerIsPrepareFailure) return;
8148
+ try {
8149
+ await this.opts.api.postAddressedMessage(workspaceId, payload.conversationId, {
8150
+ body: `${PREPARE_FAILED_PREFIX}
8120
8151
 
8121
- The dispatched turn could not start. Re-dispatch it after repairing the preparation failure shown in this conversation.`
8122
- } : {};
8152
+ The dispatched turn could not start. Re-dispatch it after repairing the preparation failure shown in this conversation.`,
8153
+ dispatch: this.turnContext.dispatchedByAgentId,
8154
+ // Keyed on the TRIGGER, not the turn: a redelivered dispatch event is
8155
+ // the replay this has to survive, and it mints a fresh `turnId` while
8156
+ // carrying the same `messageId`. One handback per failed pickup of a
8157
+ // given trigger — which is also CT992's rule, that the first failure
8158
+ // is the one carrying information. A genuine re-dispatch after a
8159
+ // repair authors a NEW trigger message, so it keys differently and
8160
+ // hands back again.
8161
+ idempotencyKey: `prepare-failure-handback:${payload.messageId}`,
8162
+ parentMessageId: payload.messageId
8163
+ });
8164
+ } catch (err) {
8165
+ turnLog.warn(
8166
+ { err: err instanceof Error ? err.message : String(err) },
8167
+ "dispatcher: prepare-failure handback failed"
8168
+ );
8169
+ }
8170
+ };
8123
8171
  if (prepareHook) {
8124
8172
  let cached2 = readPrepared(workspaceId, payload.conversationId, payload.agentId);
8125
8173
  if (cached2 && !checkoutState(cached2.cwd).ok) {
@@ -8158,8 +8206,7 @@ The dispatched turn could not start. Re-dispatch it after repairing the preparat
8158
8206
  ${reason}`,
8159
8207
  kind: "final",
8160
8208
  turnId,
8161
- parentMessageId: payload.messageId,
8162
- ...prepareFailureDispatch
8209
+ parentMessageId: payload.messageId
8163
8210
  });
8164
8211
  } catch (postErr) {
8165
8212
  turnLog.warn(
@@ -8167,6 +8214,7 @@ ${reason}`,
8167
8214
  "dispatcher: prepare-rejection post failed"
8168
8215
  );
8169
8216
  }
8217
+ await sendPrepareFailureHandback();
8170
8218
  throw this.concluded(`prepare_failed: ${reason}`);
8171
8219
  }
8172
8220
  }
@@ -8234,8 +8282,7 @@ ${reason}`,
8234
8282
  kind: "final",
8235
8283
  turnId,
8236
8284
  // CT113: stamp the parent even on the prepare-failed close.
8237
- parentMessageId: payload.messageId,
8238
- ...prepareFailureDispatch
8285
+ parentMessageId: payload.messageId
8239
8286
  });
8240
8287
  } catch (postErr) {
8241
8288
  turnLog.warn(
@@ -8243,6 +8290,7 @@ ${reason}`,
8243
8290
  "dispatcher: prepare-failure post failed"
8244
8291
  );
8245
8292
  }
8293
+ await sendPrepareFailureHandback();
8246
8294
  const failReason = `prepare_failed: ${reason}`;
8247
8295
  throw this.concluded(failReason);
8248
8296
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.87",
3
+ "version": "0.6.88",
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",