@cabane/companion 0.6.73 → 0.6.75

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
@@ -3048,6 +3048,12 @@ var turnEventSchema = z5.discriminatedUnion("type", [
3048
3048
  // `progress` row); `terminal: true` is the turn's closing reply (commits as
3049
3049
  // the `final` row).
3050
3050
  z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
3051
+ // Runtime/provider-authored prose surfaced alongside a failed turn. Unlike
3052
+ // `text`, this is not the agent's narration or reply: the pump persists it as
3053
+ // `runtime_notice`, and the transcript renders it in the shared SystemNote
3054
+ // voice. Adapters emit it only from positive runtime evidence; the web never
3055
+ // classifies English strings.
3056
+ z5.object({ type: z5.literal("runtime_notice"), body: z5.string() }),
3051
3057
  // A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
3052
3058
  // `toolName`→`name` (already prefix-stripped: `cabane_read`, not
3053
3059
  // `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
@@ -3181,6 +3187,7 @@ var turnDiagnosticsSchema = z6.object({
3181
3187
  eventCounts: z6.object({
3182
3188
  session: z6.number().int().nonnegative(),
3183
3189
  text: z6.number().int().nonnegative(),
3190
+ runtime_notice: z6.number().int().nonnegative(),
3184
3191
  thinking: z6.number().int().nonnegative(),
3185
3192
  tool: z6.number().int().nonnegative(),
3186
3193
  result: z6.number().int().nonnegative()
@@ -3279,6 +3286,17 @@ function classifyErrorText(text) {
3279
3286
  if (BARE_LIMIT.test(t)) return { kind: "usage_capped" };
3280
3287
  return null;
3281
3288
  }
3289
+ function classifyRuntimeNoticeText(text) {
3290
+ if (!text) return null;
3291
+ const normalized = text.trim().replace(/\s+/g, " ");
3292
+ if (!normalized) return null;
3293
+ if (/^you(?:'|’)ve hit your (?:session|weekly|usage) limit(?:\s*[·—-]\s*resets?\s+.+)?$/i.test(
3294
+ normalized
3295
+ )) {
3296
+ return { kind: "usage_capped" };
3297
+ }
3298
+ return null;
3299
+ }
3282
3300
  var AUTH_PATTERNS = [
3283
3301
  /authentication[_ ]error/,
3284
3302
  /invalid[_ ]?(x-)?api[_ ]?key/,
@@ -3346,6 +3364,8 @@ function isContentBearingEvent(event) {
3346
3364
  return normalizeCommitText(event.body) !== "";
3347
3365
  case "thinking":
3348
3366
  return normalizeCommitText(event.text) !== "";
3367
+ case "runtime_notice":
3368
+ return normalizeCommitText(event.body) !== "";
3349
3369
  case "tool":
3350
3370
  return true;
3351
3371
  case "session":
@@ -3542,6 +3562,12 @@ async function flushHeldText(buffer, emit, terminal, onError) {
3542
3562
  buffer.held = null;
3543
3563
  await safeEmit(emit, { type: "text", body, terminal }, onError);
3544
3564
  }
3565
+ async function flushHeldRuntimeNotice(buffer, emit, onError) {
3566
+ if (buffer.held === null) return;
3567
+ const body = buffer.held;
3568
+ buffer.held = null;
3569
+ await safeEmit(emit, { type: "runtime_notice", body }, onError);
3570
+ }
3545
3571
  function extractAssistantBlocks(msg) {
3546
3572
  const texts = [];
3547
3573
  const toolUses = [];
@@ -3663,6 +3689,9 @@ function sinkEmitter(sink, _onError) {
3663
3689
  case "thinking":
3664
3690
  if (sink.onThinking) await sink.onThinking({ text: event.text });
3665
3691
  return;
3692
+ case "runtime_notice":
3693
+ if (sink.onRuntimeNotice) await sink.onRuntimeNotice({ text: event.body });
3694
+ return;
3666
3695
  // `session` / `result` are host/pump territory — never produced by the
3667
3696
  // transcript classification, so they have no callback to render onto.
3668
3697
  case "session":
@@ -3716,6 +3745,13 @@ var TurnPump = class {
3716
3745
  const text = truncate(trimmed, THINKING_TEXT_MAX_CHARS);
3717
3746
  const seq = opts.nextSeq();
3718
3747
  await opts.commit.reportThinking({ text, seq });
3748
+ },
3749
+ onRuntimeNotice: async (evt) => {
3750
+ if (opts.signal.aborted) return;
3751
+ const body = normalizeCommitText(evt.text);
3752
+ if (!body) return;
3753
+ const seq = opts.nextSeq();
3754
+ await opts.commit.commitMessage({ body, kind: "runtime_notice", seq });
3719
3755
  }
3720
3756
  };
3721
3757
  }
@@ -4125,7 +4161,13 @@ async function* decodeSdkStream(iter, ctx) {
4125
4161
  ok = false;
4126
4162
  resultReason ??= "session_start_failed";
4127
4163
  }
4128
- await flushHeldText(buffer, emit, ok);
4164
+ const heldFailure = !ok ? classifyRuntimeNoticeText(buffer.held) : null;
4165
+ const terminalFailure = !ok ? decodeFailureReason(resultReason) : null;
4166
+ if (heldFailure && terminalFailure && heldFailure.kind === terminalFailure.kind) {
4167
+ await flushHeldRuntimeNotice(buffer, emit);
4168
+ } else {
4169
+ await flushHeldText(buffer, emit, ok);
4170
+ }
4129
4171
  yield* drain(out);
4130
4172
  if (!ok && !resultReason && !sawResult) resultReason = "no_result";
4131
4173
  yield {
@@ -4647,22 +4689,92 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
4647
4689
  {
4648
4690
  // CT558/CT592/CT731: a subscription cap. The SDK emits a rejected
4649
4691
  // `rate_limit_event` with a named subscription window; the terminal error result
4650
- // then classifies as the structured `usage_capped` reason. Partial narration
4651
- // lands as `progress`. The cap event is distinct from a provider 429 throttle.
4692
+ // then classifies as the structured `usage_capped` reason. The held provider
4693
+ // line independently classifies to the same kind, so it lands as a runtime
4694
+ // notice rather than agent-authored progress.
4652
4695
  name: "subscription cap \u2192 usage_capped",
4653
4696
  request: makeRequest(),
4654
4697
  nativeStream: [
4655
4698
  init("s1"),
4656
- assistantText("Let me work on that."),
4699
+ assistantText("You've hit your session limit \xB7 resets 8:20pm (UTC)"),
4700
+ rateLimitEvent("rejected", void 0, "five_hour"),
4701
+ resultError("error_during_execution")
4702
+ ],
4703
+ expected: [
4704
+ sessionEvent("s1"),
4705
+ {
4706
+ type: "runtime_notice",
4707
+ body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
4708
+ },
4709
+ { type: "result", ok: false, reason: "usage_capped" }
4710
+ ]
4711
+ },
4712
+ {
4713
+ // CT1336: a failed turn can still contain the agent's own partial narration.
4714
+ // No text classification means no authorship rewrite, even though the
4715
+ // structured result is the same usage cap.
4716
+ name: "partial narration before subscription cap stays progress",
4717
+ request: makeRequest(),
4718
+ nativeStream: [
4719
+ init("s1"),
4720
+ assistantText("Partial work."),
4657
4721
  rateLimitEvent("rejected", void 0, "five_hour"),
4658
4722
  resultError("error_during_execution")
4659
4723
  ],
4660
4724
  expected: [
4661
4725
  sessionEvent("s1"),
4662
- { type: "text", body: "Let me work on that.", terminal: false },
4726
+ { type: "text", body: "Partial work.", terminal: false },
4663
4727
  { type: "result", ok: false, reason: "usage_capped" }
4664
4728
  ]
4665
4729
  },
4730
+ {
4731
+ // CT1336: even recognized runtime prose stays authored when its kind
4732
+ // disagrees with the structured terminal failure.
4733
+ name: "cap notice before server failure stays progress",
4734
+ request: makeRequest(),
4735
+ nativeStream: [
4736
+ init("s1"),
4737
+ assistantText("You've hit your session limit \xB7 resets 8:20pm (UTC)"),
4738
+ resultErrorFull("error_during_execution", {
4739
+ is_error: true,
4740
+ result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
4741
+ })
4742
+ ],
4743
+ expected: [
4744
+ sessionEvent("s1"),
4745
+ {
4746
+ type: "text",
4747
+ body: "You've hit your session limit \xB7 resets 8:20pm (UTC)",
4748
+ terminal: false
4749
+ },
4750
+ { type: "result", ok: false, reason: "server_error" }
4751
+ ]
4752
+ },
4753
+ {
4754
+ // CT1336 review: `classifyErrorText` intentionally searches arbitrary error
4755
+ // blobs for status tokens. Assistant narration needs the stricter whole-
4756
+ // notice classifier, or an incidental count is de-authored when the turn
4757
+ // later fails with the matching provider kind.
4758
+ name: "incidental 5xx-shaped narration before server failure stays progress",
4759
+ request: makeRequest(),
4760
+ nativeStream: [
4761
+ init("s1"),
4762
+ assistantText("Indexed 500 files. Writing the summary next."),
4763
+ resultErrorFull("error_during_execution", {
4764
+ is_error: true,
4765
+ result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
4766
+ })
4767
+ ],
4768
+ expected: [
4769
+ sessionEvent("s1"),
4770
+ {
4771
+ type: "text",
4772
+ body: "Indexed 500 files. Writing the summary next.",
4773
+ terminal: false
4774
+ },
4775
+ { type: "result", ok: false, reason: "server_error" }
4776
+ ]
4777
+ },
4666
4778
  {
4667
4779
  // CT558/CT592: a cap signalled only by the result's
4668
4780
  // `terminal_reason:'blocking_limit'` (no discrete rate-limit event) still
@@ -8149,7 +8261,7 @@ function initialOutcome() {
8149
8261
  turnResolvedModel: void 0,
8150
8262
  turnResolvedConfig: void 0,
8151
8263
  turnMcpInventory: void 0,
8152
- eventCounts: { session: 0, text: 0, thinking: 0, tool: 0, result: 0 },
8264
+ eventCounts: { session: 0, text: 0, runtime_notice: 0, thinking: 0, tool: 0, result: 0 },
8153
8265
  runtimeResultKind: null,
8154
8266
  contentBearingEvents: 0,
8155
8267
  latestSessionState: null,
package/dist/runtime.js CHANGED
@@ -2547,6 +2547,12 @@ var turnEventSchema = z5.discriminatedUnion("type", [
2547
2547
  // `progress` row); `terminal: true` is the turn's closing reply (commits as
2548
2548
  // the `final` row).
2549
2549
  z5.object({ type: z5.literal("text"), body: z5.string(), terminal: z5.boolean() }),
2550
+ // Runtime/provider-authored prose surfaced alongside a failed turn. Unlike
2551
+ // `text`, this is not the agent's narration or reply: the pump persists it as
2552
+ // `runtime_notice`, and the transcript renders it in the shared SystemNote
2553
+ // voice. Adapters emit it only from positive runtime evidence; the web never
2554
+ // classifies English strings.
2555
+ z5.object({ type: z5.literal("runtime_notice"), body: z5.string() }),
2550
2556
  // A tool-activity transition. Maps `onToolActivity` — `toolUseId`→`id`,
2551
2557
  // `toolName`→`name` (already prefix-stripped: `cabane_read`, not
2552
2558
  // `mcp__cabane__cabane_read`), `summary` is the short card label. `phase`
@@ -2680,6 +2686,7 @@ var turnDiagnosticsSchema = z6.object({
2680
2686
  eventCounts: z6.object({
2681
2687
  session: z6.number().int().nonnegative(),
2682
2688
  text: z6.number().int().nonnegative(),
2689
+ runtime_notice: z6.number().int().nonnegative(),
2683
2690
  thinking: z6.number().int().nonnegative(),
2684
2691
  tool: z6.number().int().nonnegative(),
2685
2692
  result: z6.number().int().nonnegative()
@@ -2778,6 +2785,17 @@ function classifyErrorText(text) {
2778
2785
  if (BARE_LIMIT.test(t)) return { kind: "usage_capped" };
2779
2786
  return null;
2780
2787
  }
2788
+ function classifyRuntimeNoticeText(text) {
2789
+ if (!text) return null;
2790
+ const normalized = text.trim().replace(/\s+/g, " ");
2791
+ if (!normalized) return null;
2792
+ if (/^you(?:'|’)ve hit your (?:session|weekly|usage) limit(?:\s*[·—-]\s*resets?\s+.+)?$/i.test(
2793
+ normalized
2794
+ )) {
2795
+ return { kind: "usage_capped" };
2796
+ }
2797
+ return null;
2798
+ }
2781
2799
  var AUTH_PATTERNS = [
2782
2800
  /authentication[_ ]error/,
2783
2801
  /invalid[_ ]?(x-)?api[_ ]?key/,
@@ -2845,6 +2863,8 @@ function isContentBearingEvent(event) {
2845
2863
  return normalizeCommitText(event.body) !== "";
2846
2864
  case "thinking":
2847
2865
  return normalizeCommitText(event.text) !== "";
2866
+ case "runtime_notice":
2867
+ return normalizeCommitText(event.body) !== "";
2848
2868
  case "tool":
2849
2869
  return true;
2850
2870
  case "session":
@@ -3041,6 +3061,12 @@ async function flushHeldText(buffer, emit, terminal, onError) {
3041
3061
  buffer.held = null;
3042
3062
  await safeEmit(emit, { type: "text", body, terminal }, onError);
3043
3063
  }
3064
+ async function flushHeldRuntimeNotice(buffer, emit, onError) {
3065
+ if (buffer.held === null) return;
3066
+ const body = buffer.held;
3067
+ buffer.held = null;
3068
+ await safeEmit(emit, { type: "runtime_notice", body }, onError);
3069
+ }
3044
3070
  function extractAssistantBlocks(msg) {
3045
3071
  const texts = [];
3046
3072
  const toolUses = [];
@@ -3162,6 +3188,9 @@ function sinkEmitter(sink, _onError) {
3162
3188
  case "thinking":
3163
3189
  if (sink.onThinking) await sink.onThinking({ text: event.text });
3164
3190
  return;
3191
+ case "runtime_notice":
3192
+ if (sink.onRuntimeNotice) await sink.onRuntimeNotice({ text: event.body });
3193
+ return;
3165
3194
  // `session` / `result` are host/pump territory — never produced by the
3166
3195
  // transcript classification, so they have no callback to render onto.
3167
3196
  case "session":
@@ -3215,6 +3244,13 @@ var TurnPump = class {
3215
3244
  const text = truncate(trimmed, THINKING_TEXT_MAX_CHARS);
3216
3245
  const seq = opts.nextSeq();
3217
3246
  await opts.commit.reportThinking({ text, seq });
3247
+ },
3248
+ onRuntimeNotice: async (evt) => {
3249
+ if (opts.signal.aborted) return;
3250
+ const body = normalizeCommitText(evt.text);
3251
+ if (!body) return;
3252
+ const seq = opts.nextSeq();
3253
+ await opts.commit.commitMessage({ body, kind: "runtime_notice", seq });
3218
3254
  }
3219
3255
  };
3220
3256
  }
@@ -3624,7 +3660,13 @@ async function* decodeSdkStream(iter, ctx) {
3624
3660
  ok = false;
3625
3661
  resultReason ??= "session_start_failed";
3626
3662
  }
3627
- await flushHeldText(buffer, emit, ok);
3663
+ const heldFailure = !ok ? classifyRuntimeNoticeText(buffer.held) : null;
3664
+ const terminalFailure = !ok ? decodeFailureReason(resultReason) : null;
3665
+ if (heldFailure && terminalFailure && heldFailure.kind === terminalFailure.kind) {
3666
+ await flushHeldRuntimeNotice(buffer, emit);
3667
+ } else {
3668
+ await flushHeldText(buffer, emit, ok);
3669
+ }
3628
3670
  yield* drain(out);
3629
3671
  if (!ok && !resultReason && !sawResult) resultReason = "no_result";
3630
3672
  yield {
@@ -4146,22 +4188,92 @@ var BASE_CLAUDE_CODE_CONFORMANCE_FIXTURES = [
4146
4188
  {
4147
4189
  // CT558/CT592/CT731: a subscription cap. The SDK emits a rejected
4148
4190
  // `rate_limit_event` with a named subscription window; the terminal error result
4149
- // then classifies as the structured `usage_capped` reason. Partial narration
4150
- // lands as `progress`. The cap event is distinct from a provider 429 throttle.
4191
+ // then classifies as the structured `usage_capped` reason. The held provider
4192
+ // line independently classifies to the same kind, so it lands as a runtime
4193
+ // notice rather than agent-authored progress.
4151
4194
  name: "subscription cap \u2192 usage_capped",
4152
4195
  request: makeRequest(),
4153
4196
  nativeStream: [
4154
4197
  init("s1"),
4155
- assistantText("Let me work on that."),
4198
+ assistantText("You've hit your session limit \xB7 resets 8:20pm (UTC)"),
4199
+ rateLimitEvent("rejected", void 0, "five_hour"),
4200
+ resultError("error_during_execution")
4201
+ ],
4202
+ expected: [
4203
+ sessionEvent("s1"),
4204
+ {
4205
+ type: "runtime_notice",
4206
+ body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
4207
+ },
4208
+ { type: "result", ok: false, reason: "usage_capped" }
4209
+ ]
4210
+ },
4211
+ {
4212
+ // CT1336: a failed turn can still contain the agent's own partial narration.
4213
+ // No text classification means no authorship rewrite, even though the
4214
+ // structured result is the same usage cap.
4215
+ name: "partial narration before subscription cap stays progress",
4216
+ request: makeRequest(),
4217
+ nativeStream: [
4218
+ init("s1"),
4219
+ assistantText("Partial work."),
4156
4220
  rateLimitEvent("rejected", void 0, "five_hour"),
4157
4221
  resultError("error_during_execution")
4158
4222
  ],
4159
4223
  expected: [
4160
4224
  sessionEvent("s1"),
4161
- { type: "text", body: "Let me work on that.", terminal: false },
4225
+ { type: "text", body: "Partial work.", terminal: false },
4162
4226
  { type: "result", ok: false, reason: "usage_capped" }
4163
4227
  ]
4164
4228
  },
4229
+ {
4230
+ // CT1336: even recognized runtime prose stays authored when its kind
4231
+ // disagrees with the structured terminal failure.
4232
+ name: "cap notice before server failure stays progress",
4233
+ request: makeRequest(),
4234
+ nativeStream: [
4235
+ init("s1"),
4236
+ assistantText("You've hit your session limit \xB7 resets 8:20pm (UTC)"),
4237
+ resultErrorFull("error_during_execution", {
4238
+ is_error: true,
4239
+ result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
4240
+ })
4241
+ ],
4242
+ expected: [
4243
+ sessionEvent("s1"),
4244
+ {
4245
+ type: "text",
4246
+ body: "You've hit your session limit \xB7 resets 8:20pm (UTC)",
4247
+ terminal: false
4248
+ },
4249
+ { type: "result", ok: false, reason: "server_error" }
4250
+ ]
4251
+ },
4252
+ {
4253
+ // CT1336 review: `classifyErrorText` intentionally searches arbitrary error
4254
+ // blobs for status tokens. Assistant narration needs the stricter whole-
4255
+ // notice classifier, or an incidental count is de-authored when the turn
4256
+ // later fails with the matching provider kind.
4257
+ name: "incidental 5xx-shaped narration before server failure stays progress",
4258
+ request: makeRequest(),
4259
+ nativeStream: [
4260
+ init("s1"),
4261
+ assistantText("Indexed 500 files. Writing the summary next."),
4262
+ resultErrorFull("error_during_execution", {
4263
+ is_error: true,
4264
+ result: '529 {"type":"error","error":{"type":"overloaded_error"}}'
4265
+ })
4266
+ ],
4267
+ expected: [
4268
+ sessionEvent("s1"),
4269
+ {
4270
+ type: "text",
4271
+ body: "Indexed 500 files. Writing the summary next.",
4272
+ terminal: false
4273
+ },
4274
+ { type: "result", ok: false, reason: "server_error" }
4275
+ ]
4276
+ },
4165
4277
  {
4166
4278
  // CT558/CT592: a cap signalled only by the result's
4167
4279
  // `terminal_reason:'blocking_limit'` (no discrete rate-limit event) still
@@ -7648,7 +7760,7 @@ function initialOutcome() {
7648
7760
  turnResolvedModel: void 0,
7649
7761
  turnResolvedConfig: void 0,
7650
7762
  turnMcpInventory: void 0,
7651
- eventCounts: { session: 0, text: 0, thinking: 0, tool: 0, result: 0 },
7763
+ eventCounts: { session: 0, text: 0, runtime_notice: 0, thinking: 0, tool: 0, result: 0 },
7652
7764
  runtimeResultKind: null,
7653
7765
  contentBearingEvents: 0,
7654
7766
  latestSessionState: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.73",
3
+ "version": "0.6.75",
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",