@cabane/companion 0.6.110 → 0.6.112

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
@@ -2901,9 +2901,27 @@ function describeErrorBody(body) {
2901
2901
  if (text.length === 0) return void 0;
2902
2902
  return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
2903
2903
  }
2904
+ var CAUSE_TEXT_CAP = 200;
2905
+ function describeError(err) {
2906
+ const message = err instanceof Error ? err.message : String(err);
2907
+ const cause = describeCause(err);
2908
+ return cause === void 0 ? message : `${message}: ${cause}`;
2909
+ }
2910
+ function describeCause(err) {
2911
+ if (!(err instanceof Error)) return void 0;
2912
+ const cause = err.cause;
2913
+ if (cause === void 0 || cause === null) return void 0;
2914
+ if (typeof cause === "object") {
2915
+ const code = cause.code;
2916
+ if (typeof code === "string" && code.length > 0) return code;
2917
+ }
2918
+ const text = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : void 0;
2919
+ if (text === void 0 || text.length === 0) return void 0;
2920
+ return text.length > CAUSE_TEXT_CAP ? `${text.slice(0, CAUSE_TEXT_CAP)}\u2026` : text;
2921
+ }
2904
2922
  function apiErrorLogFields(err) {
2905
2923
  const fields = {
2906
- err: err instanceof Error ? err.message : String(err)
2924
+ err: describeError(err)
2907
2925
  };
2908
2926
  if (err instanceof ApiError) {
2909
2927
  fields.status = err.status;
@@ -2922,6 +2940,7 @@ import { randomUUID as randomUUID4 } from "crypto";
2922
2940
 
2923
2941
  // src/api.ts
2924
2942
  var RETRY_BACKOFF_MS = [250, 750];
2943
+ var TURN_CONTEXT_BACKOFF_MS = [250, 750, 2e3, 5e3];
2925
2944
  var ACTIVE_RUN_OUTBOX_SEQ = 0;
2926
2945
  var LEASE_CHECK_TIMEOUT_MS = 1e4;
2927
2946
  function activeRunOutboxKey(conversationId, agentId) {
@@ -2972,14 +2991,26 @@ var CabaneApi = class {
2972
2991
  return parsed;
2973
2992
  }
2974
2993
  async request(method, path, body, opts = {}) {
2975
- const { signal, retry = false } = opts;
2976
- const maxAttempts = retry ? RETRY_BACKOFF_MS.length + 1 : 1;
2994
+ const { signal, retry = method === "GET", backoffMs = RETRY_BACKOFF_MS } = opts;
2995
+ const maxAttempts = retry ? backoffMs.length + 1 : 1;
2977
2996
  for (let attempt = 1; ; attempt++) {
2978
2997
  try {
2979
2998
  return await this.attempt(method, path, body, signal);
2980
2999
  } catch (err) {
2981
3000
  if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
2982
- await sleep2(RETRY_BACKOFF_MS[attempt - 1], signal);
3001
+ const delayMs = backoffMs[attempt - 1];
3002
+ this.opts.log?.info(
3003
+ {
3004
+ method,
3005
+ path: pathForLog(path),
3006
+ attempt,
3007
+ maxAttempts,
3008
+ delayMs,
3009
+ err: describeError(err)
3010
+ },
3011
+ "companion: API call failed transiently; retrying"
3012
+ );
3013
+ await sleep2(delayMs, signal);
2983
3014
  if (signal?.aborted) throw err;
2984
3015
  }
2985
3016
  }
@@ -3117,9 +3148,16 @@ var CabaneApi = class {
3117
3148
  // CT1543: `episode` is the dispatch event's own, echoed back so the server can
3118
3149
  // tell this frame from a later wake on the same message. Omitted when the
3119
3150
  // event didn't carry one, which is the only thing an older event can mean.
3120
- getTurnContext(conversationId, messageId2, turnId, resumed, fresh, episode) {
3151
+ // CT1657: `signal` is the turn's own abort. It matters here more than on any
3152
+ // other read, because this call now waits up to ~8s across five attempts: a
3153
+ // Stop landing in that window used to leave the whole schedule running, and
3154
+ // the person watched a turn they had already stopped keep trying.
3155
+ getTurnContext(conversationId, messageId2, turnId, resumed, fresh, episode, signal) {
3121
3156
  const q = `conversationId=${encodeURIComponent(conversationId)}&messageId=${encodeURIComponent(messageId2)}` + (turnId ? `&turnId=${encodeURIComponent(turnId)}` : "") + (resumed ? "&resumed=1" : "") + (fresh ? "&fresh=1" : "") + (episode !== void 0 ? `&episode=${encodeURIComponent(String(episode))}` : "");
3122
- return this.request("GET", `/api/agent/turn-context?${q}`);
3157
+ return this.request("GET", `/api/agent/turn-context?${q}`, void 0, {
3158
+ backoffMs: this.opts.turnContextBackoffMs ?? TURN_CONTEXT_BACKOFF_MS,
3159
+ ...signal ? { signal } : {}
3160
+ });
3123
3161
  }
3124
3162
  // CT1380: the local half of a resumed turn's seq floor (see
3125
3163
  // `Outbox.maxSeqForTurn`). 0 when no outbox is configured — a one-shot CLI or
@@ -3204,6 +3242,12 @@ var CabaneApi = class {
3204
3242
  `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-lease?${q}`,
3205
3243
  void 0,
3206
3244
  {
3245
+ // CT1657: the one GET that opts OUT of the default retry. The reasoning
3246
+ // is right above — a failed probe has no answer worth replaying and the
3247
+ // next tick asks again in seconds, so retrying inside this one would
3248
+ // stretch a tick over its successor's slot, which is what the deadline
3249
+ // exists to prevent.
3250
+ retry: false,
3207
3251
  signal: AbortSignal.timeout(this.opts.leaseCheckTimeoutMs ?? LEASE_CHECK_TIMEOUT_MS)
3208
3252
  }
3209
3253
  );
@@ -3395,6 +3439,10 @@ function isRetryable(err) {
3395
3439
  function isAbortError(err) {
3396
3440
  return err instanceof Error && err.name === "AbortError";
3397
3441
  }
3442
+ function pathForLog(path) {
3443
+ const q = path.indexOf("?");
3444
+ return q === -1 ? path : path.slice(0, q);
3445
+ }
3398
3446
  function sleep2(ms, signal) {
3399
3447
  return new Promise((resolve2) => {
3400
3448
  if (signal?.aborted) return resolve2();
@@ -9569,6 +9617,7 @@ var TurnExecution = class {
9569
9617
  // identity-checked at the supervisor — then clear the active-run flag);
9570
9618
  // everything past the lease settles in `execute()`'s one `finally`.
9571
9619
  async run() {
9620
+ this.supervisor.registerAbort(this.turnId, this.abortController);
9572
9621
  try {
9573
9622
  await this.fetchContext();
9574
9623
  this.gateTrigger();
@@ -9590,6 +9639,8 @@ var TurnExecution = class {
9590
9639
  this.supervisor.releaseAbort(this.turnId, this.abortController);
9591
9640
  const msg = err instanceof Error ? err.message : String(err);
9592
9641
  await this.concludeAdmittedRun("setup_failed", `setup_failed: ${msg}`);
9642
+ } else {
9643
+ this.supervisor.releaseAbort(this.turnId, this.abortController);
9593
9644
  }
9594
9645
  throw err;
9595
9646
  }
@@ -9671,9 +9722,14 @@ var TurnExecution = class {
9671
9722
  turnId,
9672
9723
  this.resumed,
9673
9724
  void 0,
9674
- payload.episode
9725
+ payload.episode,
9726
+ this.abortController.signal
9675
9727
  );
9676
9728
  } catch (err) {
9729
+ if (this.abortController.signal.aborted) {
9730
+ turnLog.debug("dispatcher: stopped while loading the turn context");
9731
+ throw this.concluded("cancelled");
9732
+ }
9677
9733
  const status2 = err instanceof ApiError ? err.status : 0;
9678
9734
  if (status2 === 404) {
9679
9735
  turnLog.debug(
@@ -9688,9 +9744,8 @@ var TurnExecution = class {
9688
9744
  );
9689
9745
  throw this.concluded("turn_context_not_admitted");
9690
9746
  }
9691
- const reason = err instanceof Error ? err.message : String(err);
9692
- turnLog.debug({ err: reason }, "dispatcher: failed to fetch turn context");
9693
- const fetchReason = `fetch_failed: ${reason}`;
9747
+ turnLog.warn(apiErrorLogFields(err), "dispatcher: failed to fetch turn context");
9748
+ const fetchReason = `fetch_failed: ${describeError(err)}`;
9694
9749
  throw this.concluded(fetchReason, fetchReason);
9695
9750
  }
9696
9751
  this.turnContext = turnContext;
@@ -10053,7 +10108,12 @@ var TurnExecution = class {
10053
10108
  payload.messageId,
10054
10109
  turnId,
10055
10110
  false,
10056
- true
10111
+ true,
10112
+ void 0,
10113
+ // CT1657: the turn's abort. This recompose happens mid-run with the
10114
+ // adapter waiting on it, so a Stop here must end the wait rather than
10115
+ // hold the turn open for the rest of the retry schedule.
10116
+ this.abortController.signal
10057
10117
  );
10058
10118
  turnLog.debug(
10059
10119
  { reason },
@@ -10065,6 +10125,10 @@ var TurnExecution = class {
10065
10125
  content: context.content
10066
10126
  };
10067
10127
  } catch (err) {
10128
+ if (this.abortController.signal.aborted) {
10129
+ turnLog.debug({ reason }, "dispatcher: stopped while recomposing the turn context");
10130
+ return null;
10131
+ }
10068
10132
  turnLog.error(
10069
10133
  { reason, ...apiErrorLogFields(err) },
10070
10134
  "Couldn't load the full conversation; continuing with the recent messages."
package/dist/runtime.js CHANGED
@@ -2235,9 +2235,27 @@ function describeErrorBody(body) {
2235
2235
  if (text.length === 0) return void 0;
2236
2236
  return text.length > ERROR_BODY_LOG_CAP ? `${text.slice(0, ERROR_BODY_LOG_CAP)}\u2026` : text;
2237
2237
  }
2238
+ var CAUSE_TEXT_CAP = 200;
2239
+ function describeError(err) {
2240
+ const message = err instanceof Error ? err.message : String(err);
2241
+ const cause = describeCause(err);
2242
+ return cause === void 0 ? message : `${message}: ${cause}`;
2243
+ }
2244
+ function describeCause(err) {
2245
+ if (!(err instanceof Error)) return void 0;
2246
+ const cause = err.cause;
2247
+ if (cause === void 0 || cause === null) return void 0;
2248
+ if (typeof cause === "object") {
2249
+ const code = cause.code;
2250
+ if (typeof code === "string" && code.length > 0) return code;
2251
+ }
2252
+ const text = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : void 0;
2253
+ if (text === void 0 || text.length === 0) return void 0;
2254
+ return text.length > CAUSE_TEXT_CAP ? `${text.slice(0, CAUSE_TEXT_CAP)}\u2026` : text;
2255
+ }
2238
2256
  function apiErrorLogFields(err) {
2239
2257
  const fields = {
2240
- err: err instanceof Error ? err.message : String(err)
2258
+ err: describeError(err)
2241
2259
  };
2242
2260
  if (err instanceof ApiError) {
2243
2261
  fields.status = err.status;
@@ -2256,6 +2274,7 @@ import { randomUUID as randomUUID4 } from "crypto";
2256
2274
 
2257
2275
  // src/api.ts
2258
2276
  var RETRY_BACKOFF_MS = [250, 750];
2277
+ var TURN_CONTEXT_BACKOFF_MS = [250, 750, 2e3, 5e3];
2259
2278
  var ACTIVE_RUN_OUTBOX_SEQ = 0;
2260
2279
  var LEASE_CHECK_TIMEOUT_MS = 1e4;
2261
2280
  function activeRunOutboxKey(conversationId, agentId) {
@@ -2306,14 +2325,26 @@ var CabaneApi = class {
2306
2325
  return parsed;
2307
2326
  }
2308
2327
  async request(method, path, body, opts = {}) {
2309
- const { signal, retry = false } = opts;
2310
- const maxAttempts = retry ? RETRY_BACKOFF_MS.length + 1 : 1;
2328
+ const { signal, retry = method === "GET", backoffMs = RETRY_BACKOFF_MS } = opts;
2329
+ const maxAttempts = retry ? backoffMs.length + 1 : 1;
2311
2330
  for (let attempt = 1; ; attempt++) {
2312
2331
  try {
2313
2332
  return await this.attempt(method, path, body, signal);
2314
2333
  } catch (err) {
2315
2334
  if (attempt >= maxAttempts || signal?.aborted || !isRetryable(err)) throw err;
2316
- await sleep(RETRY_BACKOFF_MS[attempt - 1], signal);
2335
+ const delayMs = backoffMs[attempt - 1];
2336
+ this.opts.log?.info(
2337
+ {
2338
+ method,
2339
+ path: pathForLog(path),
2340
+ attempt,
2341
+ maxAttempts,
2342
+ delayMs,
2343
+ err: describeError(err)
2344
+ },
2345
+ "companion: API call failed transiently; retrying"
2346
+ );
2347
+ await sleep(delayMs, signal);
2317
2348
  if (signal?.aborted) throw err;
2318
2349
  }
2319
2350
  }
@@ -2451,9 +2482,16 @@ var CabaneApi = class {
2451
2482
  // CT1543: `episode` is the dispatch event's own, echoed back so the server can
2452
2483
  // tell this frame from a later wake on the same message. Omitted when the
2453
2484
  // event didn't carry one, which is the only thing an older event can mean.
2454
- getTurnContext(conversationId, messageId2, turnId, resumed, fresh, episode) {
2485
+ // CT1657: `signal` is the turn's own abort. It matters here more than on any
2486
+ // other read, because this call now waits up to ~8s across five attempts: a
2487
+ // Stop landing in that window used to leave the whole schedule running, and
2488
+ // the person watched a turn they had already stopped keep trying.
2489
+ getTurnContext(conversationId, messageId2, turnId, resumed, fresh, episode, signal) {
2455
2490
  const q = `conversationId=${encodeURIComponent(conversationId)}&messageId=${encodeURIComponent(messageId2)}` + (turnId ? `&turnId=${encodeURIComponent(turnId)}` : "") + (resumed ? "&resumed=1" : "") + (fresh ? "&fresh=1" : "") + (episode !== void 0 ? `&episode=${encodeURIComponent(String(episode))}` : "");
2456
- return this.request("GET", `/api/agent/turn-context?${q}`);
2491
+ return this.request("GET", `/api/agent/turn-context?${q}`, void 0, {
2492
+ backoffMs: this.opts.turnContextBackoffMs ?? TURN_CONTEXT_BACKOFF_MS,
2493
+ ...signal ? { signal } : {}
2494
+ });
2457
2495
  }
2458
2496
  // CT1380: the local half of a resumed turn's seq floor (see
2459
2497
  // `Outbox.maxSeqForTurn`). 0 when no outbox is configured — a one-shot CLI or
@@ -2538,6 +2576,12 @@ var CabaneApi = class {
2538
2576
  `/api/workspaces/${workspaceId}/conversations/${conversationId}/participants/agents/${agentId}/turn-lease?${q}`,
2539
2577
  void 0,
2540
2578
  {
2579
+ // CT1657: the one GET that opts OUT of the default retry. The reasoning
2580
+ // is right above — a failed probe has no answer worth replaying and the
2581
+ // next tick asks again in seconds, so retrying inside this one would
2582
+ // stretch a tick over its successor's slot, which is what the deadline
2583
+ // exists to prevent.
2584
+ retry: false,
2541
2585
  signal: AbortSignal.timeout(this.opts.leaseCheckTimeoutMs ?? LEASE_CHECK_TIMEOUT_MS)
2542
2586
  }
2543
2587
  );
@@ -2729,6 +2773,10 @@ function isRetryable(err) {
2729
2773
  function isAbortError(err) {
2730
2774
  return err instanceof Error && err.name === "AbortError";
2731
2775
  }
2776
+ function pathForLog(path) {
2777
+ const q = path.indexOf("?");
2778
+ return q === -1 ? path : path.slice(0, q);
2779
+ }
2732
2780
  function sleep(ms, signal) {
2733
2781
  return new Promise((resolve2) => {
2734
2782
  if (signal?.aborted) return resolve2();
@@ -8992,6 +9040,7 @@ var TurnExecution = class {
8992
9040
  // identity-checked at the supervisor — then clear the active-run flag);
8993
9041
  // everything past the lease settles in `execute()`'s one `finally`.
8994
9042
  async run() {
9043
+ this.supervisor.registerAbort(this.turnId, this.abortController);
8995
9044
  try {
8996
9045
  await this.fetchContext();
8997
9046
  this.gateTrigger();
@@ -9013,6 +9062,8 @@ var TurnExecution = class {
9013
9062
  this.supervisor.releaseAbort(this.turnId, this.abortController);
9014
9063
  const msg = err instanceof Error ? err.message : String(err);
9015
9064
  await this.concludeAdmittedRun("setup_failed", `setup_failed: ${msg}`);
9065
+ } else {
9066
+ this.supervisor.releaseAbort(this.turnId, this.abortController);
9016
9067
  }
9017
9068
  throw err;
9018
9069
  }
@@ -9094,9 +9145,14 @@ var TurnExecution = class {
9094
9145
  turnId,
9095
9146
  this.resumed,
9096
9147
  void 0,
9097
- payload.episode
9148
+ payload.episode,
9149
+ this.abortController.signal
9098
9150
  );
9099
9151
  } catch (err) {
9152
+ if (this.abortController.signal.aborted) {
9153
+ turnLog.debug("dispatcher: stopped while loading the turn context");
9154
+ throw this.concluded("cancelled");
9155
+ }
9100
9156
  const status = err instanceof ApiError ? err.status : 0;
9101
9157
  if (status === 404) {
9102
9158
  turnLog.debug(
@@ -9111,9 +9167,8 @@ var TurnExecution = class {
9111
9167
  );
9112
9168
  throw this.concluded("turn_context_not_admitted");
9113
9169
  }
9114
- const reason = err instanceof Error ? err.message : String(err);
9115
- turnLog.debug({ err: reason }, "dispatcher: failed to fetch turn context");
9116
- const fetchReason = `fetch_failed: ${reason}`;
9170
+ turnLog.warn(apiErrorLogFields(err), "dispatcher: failed to fetch turn context");
9171
+ const fetchReason = `fetch_failed: ${describeError(err)}`;
9117
9172
  throw this.concluded(fetchReason, fetchReason);
9118
9173
  }
9119
9174
  this.turnContext = turnContext;
@@ -9476,7 +9531,12 @@ var TurnExecution = class {
9476
9531
  payload.messageId,
9477
9532
  turnId,
9478
9533
  false,
9479
- true
9534
+ true,
9535
+ void 0,
9536
+ // CT1657: the turn's abort. This recompose happens mid-run with the
9537
+ // adapter waiting on it, so a Stop here must end the wait rather than
9538
+ // hold the turn open for the rest of the retry schedule.
9539
+ this.abortController.signal
9480
9540
  );
9481
9541
  turnLog.debug(
9482
9542
  { reason },
@@ -9488,6 +9548,10 @@ var TurnExecution = class {
9488
9548
  content: context.content
9489
9549
  };
9490
9550
  } catch (err) {
9551
+ if (this.abortController.signal.aborted) {
9552
+ turnLog.debug({ reason }, "dispatcher: stopped while recomposing the turn context");
9553
+ return null;
9554
+ }
9491
9555
  turnLog.error(
9492
9556
  { reason, ...apiErrorLogFields(err) },
9493
9557
  "Couldn't load the full conversation; continuing with the recent messages."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.110",
3
+ "version": "0.6.112",
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",