@cabane/companion 0.6.79 → 0.6.80

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.
Files changed (3) hide show
  1. package/dist/cli.js +176 -13
  2. package/dist/runtime.js +176 -13
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5100,6 +5100,10 @@ async function* decodeOpencodeStream(events, ctx) {
5100
5100
  if (sealed) yield sealed;
5101
5101
  ok = false;
5102
5102
  const errorText = readSessionError(ev.properties);
5103
+ const notice = errorText.trim().slice(0, 200).trimEnd();
5104
+ if (notice && !/^[\w:-]+$/.test(notice)) {
5105
+ yield { type: "runtime_notice", body: notice };
5106
+ }
5103
5107
  const failure = classifyErrorText(errorText);
5104
5108
  reason = failure ? encodeFailureReason(failure) : `error:${errorText.slice(0, 200)}`;
5105
5109
  settled = true;
@@ -5509,6 +5513,7 @@ var opencodeAdapter = createOpencodeAdapter();
5509
5513
  // packages/agent-runtime/src/opencode/conformance.ts
5510
5514
  var ABORT_SENTINEL2 = { __abortHere: true };
5511
5515
  var NEW_SESSION_ID = "sess_new";
5516
+ var BOUNDED_NOTICE = "You've hit your usage limit \xB7 ".padEnd(200, "x");
5512
5517
  var COMPANION_POLICY = {
5513
5518
  hostFs: false,
5514
5519
  web: true,
@@ -5700,6 +5705,7 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5700
5705
  expected: [
5701
5706
  sessionEvent2(NEW_SESSION_ID),
5702
5707
  { type: "text", body: "Partial work.", terminal: false },
5708
+ { type: "runtime_notice", body: "provider exploded" },
5703
5709
  { type: "result", ok: false, reason: "error:provider exploded" }
5704
5710
  ]
5705
5711
  },
@@ -5716,7 +5722,11 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5716
5722
  properties: { error: { name: "RateLimitError", message: "rate limit exceeded (429)" } }
5717
5723
  }
5718
5724
  ],
5719
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "rate_limited" }]
5725
+ expected: [
5726
+ sessionEvent2(NEW_SESSION_ID),
5727
+ { type: "runtime_notice", body: "RateLimitError: rate limit exceeded (429)" },
5728
+ { type: "result", ok: false, reason: "rate_limited" }
5729
+ ]
5720
5730
  },
5721
5731
  {
5722
5732
  // CT592: a subscription cap surfaced as text ("usage limit reached") → the cap
@@ -5730,7 +5740,11 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5730
5740
  properties: { error: { name: "Error", message: "Claude usage limit reached" } }
5731
5741
  }
5732
5742
  ],
5733
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "usage_capped" }]
5743
+ expected: [
5744
+ sessionEvent2(NEW_SESSION_ID),
5745
+ { type: "runtime_notice", body: "Claude usage limit reached" },
5746
+ { type: "result", ok: false, reason: "usage_capped" }
5747
+ ]
5734
5748
  },
5735
5749
  {
5736
5750
  // CT592: a transient provider outage (5xx / overload) surfaced as text → the
@@ -5746,7 +5760,11 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5746
5760
  }
5747
5761
  }
5748
5762
  ],
5749
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "server_error" }]
5763
+ expected: [
5764
+ sessionEvent2(NEW_SESSION_ID),
5765
+ { type: "runtime_notice", body: "AI_APICallError: overloaded_error (529)" },
5766
+ { type: "result", ok: false, reason: "server_error" }
5767
+ ]
5750
5768
  },
5751
5769
  {
5752
5770
  // CT558: an expired/invalid credential classifies as `auth_expired`.
@@ -5758,7 +5776,66 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5758
5776
  properties: { error: { name: "AuthenticationError", message: "HTTP 401 Unauthorized" } }
5759
5777
  }
5760
5778
  ],
5761
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "auth_expired" }]
5779
+ expected: [
5780
+ sessionEvent2(NEW_SESSION_ID),
5781
+ { type: "runtime_notice", body: "AuthenticationError: HTTP 401 Unauthorized" },
5782
+ { type: "result", ok: false, reason: "auth_expired" }
5783
+ ]
5784
+ },
5785
+ {
5786
+ // CT1351: an unclassified failure remains as informative as a classified one:
5787
+ // its provider text is visible while the raw machine reason stays unchanged.
5788
+ name: "unclassified failure keeps a runtime notice and raw reason",
5789
+ request: makeRequest2(),
5790
+ nativeStream: [errored("something local broke")],
5791
+ expected: [
5792
+ sessionEvent2(NEW_SESSION_ID),
5793
+ { type: "runtime_notice", body: "something local broke" },
5794
+ { type: "result", ok: false, reason: "error:something local broke" }
5795
+ ]
5796
+ },
5797
+ {
5798
+ name: "runtime notice is trimmed and bounded to 200 characters",
5799
+ request: makeRequest2(),
5800
+ nativeStream: [errored(` ${BOUNDED_NOTICE}${"y".repeat(20)} `)],
5801
+ expected: [
5802
+ sessionEvent2(NEW_SESSION_ID),
5803
+ { type: "runtime_notice", body: BOUNDED_NOTICE },
5804
+ { type: "result", ok: false, reason: "usage_capped" }
5805
+ ]
5806
+ },
5807
+ {
5808
+ // CT1351: empty, whitespace-only, and bare status-token failures still set
5809
+ // the machine reason but do not add an empty/unhelpful transcript notice.
5810
+ name: "empty failure text emits no runtime notice",
5811
+ request: makeRequest2(),
5812
+ nativeStream: [errored("")],
5813
+ expected: [
5814
+ sessionEvent2(NEW_SESSION_ID),
5815
+ { type: "result", ok: false, reason: "error:unknown" }
5816
+ ]
5817
+ },
5818
+ {
5819
+ name: "whitespace-only failure text emits no runtime notice",
5820
+ request: makeRequest2(),
5821
+ nativeStream: [errored(" ")],
5822
+ expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "error: " }]
5823
+ },
5824
+ {
5825
+ name: "bare status token emits no runtime notice",
5826
+ request: makeRequest2(),
5827
+ nativeStream: [errored("429")],
5828
+ expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "rate_limited" }]
5829
+ },
5830
+ {
5831
+ name: "single-word provider sentence emits a runtime notice",
5832
+ request: makeRequest2(),
5833
+ nativeStream: [errored("Unavailable.")],
5834
+ expected: [
5835
+ sessionEvent2(NEW_SESSION_ID),
5836
+ { type: "runtime_notice", body: "Unavailable." },
5837
+ { type: "result", ok: false, reason: "error:Unavailable." }
5838
+ ]
5762
5839
  },
5763
5840
  {
5764
5841
  // CT1144: the same zero-content success the claude-code suite pins, in
@@ -6180,6 +6257,10 @@ async function* decodeCodexStream(events, ctx) {
6180
6257
  yield* flushInterim();
6181
6258
  ok = false;
6182
6259
  const text = readErrorMessage(ev);
6260
+ const notice = text.trim().slice(0, 200).trimEnd();
6261
+ if (notice && !/^[\w:-]+$/.test(notice)) {
6262
+ yield { type: "runtime_notice", body: notice };
6263
+ }
6183
6264
  const failure = classifyErrorText(text);
6184
6265
  reason = failure ? encodeFailureReason(failure) : `error:${text.slice(0, 200)}`;
6185
6266
  settled = true;
@@ -6498,6 +6579,7 @@ var codexAdapter = createCodexAdapter();
6498
6579
  // packages/agent-runtime/src/codex/conformance.ts
6499
6580
  var ABORT_SENTINEL3 = { __abortHere: true };
6500
6581
  var NEW_THREAD_ID = "th_new";
6582
+ var BOUNDED_NOTICE2 = "You've hit your usage limit \xB7 ".padEnd(200, "x");
6501
6583
  var COMPANION_POLICY2 = {
6502
6584
  hostFs: false,
6503
6585
  web: true,
@@ -7027,6 +7109,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7027
7109
  expected: [
7028
7110
  sessionEvent3(NEW_THREAD_ID),
7029
7111
  { type: "text", body: "Partial work.", terminal: false },
7112
+ { type: "runtime_notice", body: "provider exploded" },
7030
7113
  { type: "result", ok: false, reason: "error:provider exploded" }
7031
7114
  ]
7032
7115
  },
@@ -7043,6 +7126,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
7043
7126
  expected: [
7044
7127
  sessionEvent3(NEW_THREAD_ID),
7045
7128
  { type: "text", body: "Starting.", terminal: false },
7129
+ { type: "runtime_notice", body: "stream died" },
7046
7130
  { type: "result", ok: false, reason: "error:stream died" }
7047
7131
  ]
7048
7132
  },
@@ -7055,25 +7139,47 @@ var CODEX_CONFORMANCE_FIXTURES = [
7055
7139
  name: "turn.failed 429 \u2192 rate_limited (throttle)",
7056
7140
  request: makeRequest3(),
7057
7141
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
7058
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "rate_limited" }]
7142
+ expected: [
7143
+ sessionEvent3(NEW_THREAD_ID),
7144
+ { type: "runtime_notice", body: "429 Too Many Requests" },
7145
+ { type: "result", ok: false, reason: "rate_limited" }
7146
+ ]
7059
7147
  },
7060
7148
  {
7061
7149
  name: "turn.failed overload \u2192 server_error",
7062
7150
  request: makeRequest3(),
7063
7151
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("503 Service Unavailable")],
7064
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "server_error" }]
7152
+ expected: [
7153
+ sessionEvent3(NEW_THREAD_ID),
7154
+ { type: "runtime_notice", body: "503 Service Unavailable" },
7155
+ { type: "result", ok: false, reason: "server_error" }
7156
+ ]
7065
7157
  },
7066
7158
  {
7067
7159
  name: "turn.failed usage cap \u2192 usage_capped",
7068
7160
  request: makeRequest3(),
7069
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("You have hit your usage limit")],
7070
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "usage_capped" }]
7161
+ nativeStream: [
7162
+ threadStarted(NEW_THREAD_ID),
7163
+ turnFailed("You've hit your session limit \xB7 resets 8:20pm (UTC)")
7164
+ ],
7165
+ expected: [
7166
+ sessionEvent3(NEW_THREAD_ID),
7167
+ {
7168
+ type: "runtime_notice",
7169
+ body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
7170
+ },
7171
+ { type: "result", ok: false, reason: "usage_capped" }
7172
+ ]
7071
7173
  },
7072
7174
  {
7073
7175
  name: "turn.failed auth \u2192 auth_expired",
7074
7176
  request: makeRequest3(),
7075
7177
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("401 Unauthorized: invalid api key")],
7076
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "auth_expired" }]
7178
+ expected: [
7179
+ sessionEvent3(NEW_THREAD_ID),
7180
+ { type: "runtime_notice", body: "401 Unauthorized: invalid api key" },
7181
+ { type: "result", ok: false, reason: "auth_expired" }
7182
+ ]
7077
7183
  },
7078
7184
  {
7079
7185
  // CT592: a Codex failure text the classifier can't place still keeps today's
@@ -7083,9 +7189,53 @@ var CODEX_CONFORMANCE_FIXTURES = [
7083
7189
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
7084
7190
  expected: [
7085
7191
  sessionEvent3(NEW_THREAD_ID),
7192
+ { type: "runtime_notice", body: "something local broke" },
7086
7193
  { type: "result", ok: false, reason: "error:something local broke" }
7087
7194
  ]
7088
7195
  },
7196
+ {
7197
+ name: "runtime notice is trimmed and bounded to 200 characters",
7198
+ request: makeRequest3(),
7199
+ nativeStream: [
7200
+ threadStarted(NEW_THREAD_ID),
7201
+ turnFailed(` ${BOUNDED_NOTICE2}${"y".repeat(20)} `)
7202
+ ],
7203
+ expected: [
7204
+ sessionEvent3(NEW_THREAD_ID),
7205
+ { type: "runtime_notice", body: BOUNDED_NOTICE2 },
7206
+ { type: "result", ok: false, reason: "usage_capped" }
7207
+ ]
7208
+ },
7209
+ {
7210
+ // CT1351: empty, whitespace-only, and bare status-token failures still set
7211
+ // the machine reason but do not add an empty/unhelpful transcript notice.
7212
+ name: "empty failure text emits no runtime notice",
7213
+ request: makeRequest3(),
7214
+ nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
7215
+ expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "error:unknown" }]
7216
+ },
7217
+ {
7218
+ name: "whitespace-only failure text emits no runtime notice",
7219
+ request: makeRequest3(),
7220
+ nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed(" ")],
7221
+ expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "error: " }]
7222
+ },
7223
+ {
7224
+ name: "bare status token emits no runtime notice",
7225
+ request: makeRequest3(),
7226
+ nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429")],
7227
+ expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "rate_limited" }]
7228
+ },
7229
+ {
7230
+ name: "single-word provider sentence emits a runtime notice",
7231
+ request: makeRequest3(),
7232
+ nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("Unavailable.")],
7233
+ expected: [
7234
+ sessionEvent3(NEW_THREAD_ID),
7235
+ { type: "runtime_notice", body: "Unavailable." },
7236
+ { type: "result", ok: false, reason: "error:Unavailable." }
7237
+ ]
7238
+ },
7089
7239
  {
7090
7240
  // CT481: fail LOUD on unknown model metadata. An unknown `--model` makes Codex
7091
7241
  // emit an `error` item ("… Defaulting to fallback metadata …") and then go
@@ -7430,7 +7580,14 @@ function createSkipState() {
7430
7580
  return { skipped: false, reason: null };
7431
7581
  }
7432
7582
  function createAskState() {
7433
- return { targetUserId: null, question: null, headline: null, options: null, questions: null };
7583
+ return {
7584
+ targetUserId: null,
7585
+ question: null,
7586
+ headline: null,
7587
+ options: null,
7588
+ questions: null,
7589
+ order: null
7590
+ };
7434
7591
  }
7435
7592
  function createWakeState() {
7436
7593
  return { afterSeconds: null, at: null, note: null, cancelled: false };
@@ -7576,6 +7733,7 @@ function createTurnControlMcpServer(sendState, skipState, askState, wakeState, r
7576
7733
  };
7577
7734
  }
7578
7735
  askState.targetUserId = args.targetUserId;
7736
+ askState.order = controlOrder?.next() ?? null;
7579
7737
  if (hasArray) {
7580
7738
  askState.questions = args.questions;
7581
7739
  askState.question = null;
@@ -8123,9 +8281,11 @@ var TurnCommitter = class {
8123
8281
  // the server sorts it after every recorded one — it binds the turn's closing
8124
8282
  // words.
8125
8283
  //
8126
- // The ask is absent by design, not by omission: its carrier queues last among
8127
- // the turn's rows however early the tool fired, so there is no position to
8128
- // report.
8284
+ // CT1347: the ask has a position too. Its carrier used to queue last however
8285
+ // early the tool fired, because a landed open ask held the conversation and an
8286
+ // ask ahead of its own turn's send would have stranded that send behind a
8287
+ // person's response time. Nothing holds now, so the ask is an addressed intent
8288
+ // like the other two and queues where it was called.
8129
8289
  orderField(fields) {
8130
8290
  const order = {};
8131
8291
  const replyOrder = this.deps.replyState.order;
@@ -8133,6 +8293,9 @@ var TurnCommitter = class {
8133
8293
  if (fields.dispatch !== void 0 && this.deps.sendState.order !== null) {
8134
8294
  order.send = this.deps.sendState.order;
8135
8295
  }
8296
+ if (fields.ask !== void 0 && this.deps.askState.order !== null) {
8297
+ order.ask = this.deps.askState.order;
8298
+ }
8136
8299
  return Object.keys(order).length > 0 ? { turnControlOrder: order } : {};
8137
8300
  }
8138
8301
  // The declared reply: explicit `reply_to` first, else the runtime's own
package/dist/runtime.js CHANGED
@@ -4599,6 +4599,10 @@ async function* decodeOpencodeStream(events, ctx) {
4599
4599
  if (sealed) yield sealed;
4600
4600
  ok = false;
4601
4601
  const errorText = readSessionError(ev.properties);
4602
+ const notice = errorText.trim().slice(0, 200).trimEnd();
4603
+ if (notice && !/^[\w:-]+$/.test(notice)) {
4604
+ yield { type: "runtime_notice", body: notice };
4605
+ }
4602
4606
  const failure = classifyErrorText(errorText);
4603
4607
  reason = failure ? encodeFailureReason(failure) : `error:${errorText.slice(0, 200)}`;
4604
4608
  settled = true;
@@ -5008,6 +5012,7 @@ var opencodeAdapter = createOpencodeAdapter();
5008
5012
  // packages/agent-runtime/src/opencode/conformance.ts
5009
5013
  var ABORT_SENTINEL2 = { __abortHere: true };
5010
5014
  var NEW_SESSION_ID = "sess_new";
5015
+ var BOUNDED_NOTICE = "You've hit your usage limit \xB7 ".padEnd(200, "x");
5011
5016
  var COMPANION_POLICY = {
5012
5017
  hostFs: false,
5013
5018
  web: true,
@@ -5199,6 +5204,7 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5199
5204
  expected: [
5200
5205
  sessionEvent2(NEW_SESSION_ID),
5201
5206
  { type: "text", body: "Partial work.", terminal: false },
5207
+ { type: "runtime_notice", body: "provider exploded" },
5202
5208
  { type: "result", ok: false, reason: "error:provider exploded" }
5203
5209
  ]
5204
5210
  },
@@ -5215,7 +5221,11 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5215
5221
  properties: { error: { name: "RateLimitError", message: "rate limit exceeded (429)" } }
5216
5222
  }
5217
5223
  ],
5218
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "rate_limited" }]
5224
+ expected: [
5225
+ sessionEvent2(NEW_SESSION_ID),
5226
+ { type: "runtime_notice", body: "RateLimitError: rate limit exceeded (429)" },
5227
+ { type: "result", ok: false, reason: "rate_limited" }
5228
+ ]
5219
5229
  },
5220
5230
  {
5221
5231
  // CT592: a subscription cap surfaced as text ("usage limit reached") → the cap
@@ -5229,7 +5239,11 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5229
5239
  properties: { error: { name: "Error", message: "Claude usage limit reached" } }
5230
5240
  }
5231
5241
  ],
5232
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "usage_capped" }]
5242
+ expected: [
5243
+ sessionEvent2(NEW_SESSION_ID),
5244
+ { type: "runtime_notice", body: "Claude usage limit reached" },
5245
+ { type: "result", ok: false, reason: "usage_capped" }
5246
+ ]
5233
5247
  },
5234
5248
  {
5235
5249
  // CT592: a transient provider outage (5xx / overload) surfaced as text → the
@@ -5245,7 +5259,11 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5245
5259
  }
5246
5260
  }
5247
5261
  ],
5248
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "server_error" }]
5262
+ expected: [
5263
+ sessionEvent2(NEW_SESSION_ID),
5264
+ { type: "runtime_notice", body: "AI_APICallError: overloaded_error (529)" },
5265
+ { type: "result", ok: false, reason: "server_error" }
5266
+ ]
5249
5267
  },
5250
5268
  {
5251
5269
  // CT558: an expired/invalid credential classifies as `auth_expired`.
@@ -5257,7 +5275,66 @@ var OPENCODE_CONFORMANCE_FIXTURES = [
5257
5275
  properties: { error: { name: "AuthenticationError", message: "HTTP 401 Unauthorized" } }
5258
5276
  }
5259
5277
  ],
5260
- expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "auth_expired" }]
5278
+ expected: [
5279
+ sessionEvent2(NEW_SESSION_ID),
5280
+ { type: "runtime_notice", body: "AuthenticationError: HTTP 401 Unauthorized" },
5281
+ { type: "result", ok: false, reason: "auth_expired" }
5282
+ ]
5283
+ },
5284
+ {
5285
+ // CT1351: an unclassified failure remains as informative as a classified one:
5286
+ // its provider text is visible while the raw machine reason stays unchanged.
5287
+ name: "unclassified failure keeps a runtime notice and raw reason",
5288
+ request: makeRequest2(),
5289
+ nativeStream: [errored("something local broke")],
5290
+ expected: [
5291
+ sessionEvent2(NEW_SESSION_ID),
5292
+ { type: "runtime_notice", body: "something local broke" },
5293
+ { type: "result", ok: false, reason: "error:something local broke" }
5294
+ ]
5295
+ },
5296
+ {
5297
+ name: "runtime notice is trimmed and bounded to 200 characters",
5298
+ request: makeRequest2(),
5299
+ nativeStream: [errored(` ${BOUNDED_NOTICE}${"y".repeat(20)} `)],
5300
+ expected: [
5301
+ sessionEvent2(NEW_SESSION_ID),
5302
+ { type: "runtime_notice", body: BOUNDED_NOTICE },
5303
+ { type: "result", ok: false, reason: "usage_capped" }
5304
+ ]
5305
+ },
5306
+ {
5307
+ // CT1351: empty, whitespace-only, and bare status-token failures still set
5308
+ // the machine reason but do not add an empty/unhelpful transcript notice.
5309
+ name: "empty failure text emits no runtime notice",
5310
+ request: makeRequest2(),
5311
+ nativeStream: [errored("")],
5312
+ expected: [
5313
+ sessionEvent2(NEW_SESSION_ID),
5314
+ { type: "result", ok: false, reason: "error:unknown" }
5315
+ ]
5316
+ },
5317
+ {
5318
+ name: "whitespace-only failure text emits no runtime notice",
5319
+ request: makeRequest2(),
5320
+ nativeStream: [errored(" ")],
5321
+ expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "error: " }]
5322
+ },
5323
+ {
5324
+ name: "bare status token emits no runtime notice",
5325
+ request: makeRequest2(),
5326
+ nativeStream: [errored("429")],
5327
+ expected: [sessionEvent2(NEW_SESSION_ID), { type: "result", ok: false, reason: "rate_limited" }]
5328
+ },
5329
+ {
5330
+ name: "single-word provider sentence emits a runtime notice",
5331
+ request: makeRequest2(),
5332
+ nativeStream: [errored("Unavailable.")],
5333
+ expected: [
5334
+ sessionEvent2(NEW_SESSION_ID),
5335
+ { type: "runtime_notice", body: "Unavailable." },
5336
+ { type: "result", ok: false, reason: "error:Unavailable." }
5337
+ ]
5261
5338
  },
5262
5339
  {
5263
5340
  // CT1144: the same zero-content success the claude-code suite pins, in
@@ -5679,6 +5756,10 @@ async function* decodeCodexStream(events, ctx) {
5679
5756
  yield* flushInterim();
5680
5757
  ok = false;
5681
5758
  const text = readErrorMessage(ev);
5759
+ const notice = text.trim().slice(0, 200).trimEnd();
5760
+ if (notice && !/^[\w:-]+$/.test(notice)) {
5761
+ yield { type: "runtime_notice", body: notice };
5762
+ }
5682
5763
  const failure = classifyErrorText(text);
5683
5764
  reason = failure ? encodeFailureReason(failure) : `error:${text.slice(0, 200)}`;
5684
5765
  settled = true;
@@ -5997,6 +6078,7 @@ var codexAdapter = createCodexAdapter();
5997
6078
  // packages/agent-runtime/src/codex/conformance.ts
5998
6079
  var ABORT_SENTINEL3 = { __abortHere: true };
5999
6080
  var NEW_THREAD_ID = "th_new";
6081
+ var BOUNDED_NOTICE2 = "You've hit your usage limit \xB7 ".padEnd(200, "x");
6000
6082
  var COMPANION_POLICY2 = {
6001
6083
  hostFs: false,
6002
6084
  web: true,
@@ -6526,6 +6608,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6526
6608
  expected: [
6527
6609
  sessionEvent3(NEW_THREAD_ID),
6528
6610
  { type: "text", body: "Partial work.", terminal: false },
6611
+ { type: "runtime_notice", body: "provider exploded" },
6529
6612
  { type: "result", ok: false, reason: "error:provider exploded" }
6530
6613
  ]
6531
6614
  },
@@ -6542,6 +6625,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
6542
6625
  expected: [
6543
6626
  sessionEvent3(NEW_THREAD_ID),
6544
6627
  { type: "text", body: "Starting.", terminal: false },
6628
+ { type: "runtime_notice", body: "stream died" },
6545
6629
  { type: "result", ok: false, reason: "error:stream died" }
6546
6630
  ]
6547
6631
  },
@@ -6554,25 +6638,47 @@ var CODEX_CONFORMANCE_FIXTURES = [
6554
6638
  name: "turn.failed 429 \u2192 rate_limited (throttle)",
6555
6639
  request: makeRequest3(),
6556
6640
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
6557
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "rate_limited" }]
6641
+ expected: [
6642
+ sessionEvent3(NEW_THREAD_ID),
6643
+ { type: "runtime_notice", body: "429 Too Many Requests" },
6644
+ { type: "result", ok: false, reason: "rate_limited" }
6645
+ ]
6558
6646
  },
6559
6647
  {
6560
6648
  name: "turn.failed overload \u2192 server_error",
6561
6649
  request: makeRequest3(),
6562
6650
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("503 Service Unavailable")],
6563
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "server_error" }]
6651
+ expected: [
6652
+ sessionEvent3(NEW_THREAD_ID),
6653
+ { type: "runtime_notice", body: "503 Service Unavailable" },
6654
+ { type: "result", ok: false, reason: "server_error" }
6655
+ ]
6564
6656
  },
6565
6657
  {
6566
6658
  name: "turn.failed usage cap \u2192 usage_capped",
6567
6659
  request: makeRequest3(),
6568
- nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("You have hit your usage limit")],
6569
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "usage_capped" }]
6660
+ nativeStream: [
6661
+ threadStarted(NEW_THREAD_ID),
6662
+ turnFailed("You've hit your session limit \xB7 resets 8:20pm (UTC)")
6663
+ ],
6664
+ expected: [
6665
+ sessionEvent3(NEW_THREAD_ID),
6666
+ {
6667
+ type: "runtime_notice",
6668
+ body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
6669
+ },
6670
+ { type: "result", ok: false, reason: "usage_capped" }
6671
+ ]
6570
6672
  },
6571
6673
  {
6572
6674
  name: "turn.failed auth \u2192 auth_expired",
6573
6675
  request: makeRequest3(),
6574
6676
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("401 Unauthorized: invalid api key")],
6575
- expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "auth_expired" }]
6677
+ expected: [
6678
+ sessionEvent3(NEW_THREAD_ID),
6679
+ { type: "runtime_notice", body: "401 Unauthorized: invalid api key" },
6680
+ { type: "result", ok: false, reason: "auth_expired" }
6681
+ ]
6576
6682
  },
6577
6683
  {
6578
6684
  // CT592: a Codex failure text the classifier can't place still keeps today's
@@ -6582,9 +6688,53 @@ var CODEX_CONFORMANCE_FIXTURES = [
6582
6688
  nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
6583
6689
  expected: [
6584
6690
  sessionEvent3(NEW_THREAD_ID),
6691
+ { type: "runtime_notice", body: "something local broke" },
6585
6692
  { type: "result", ok: false, reason: "error:something local broke" }
6586
6693
  ]
6587
6694
  },
6695
+ {
6696
+ name: "runtime notice is trimmed and bounded to 200 characters",
6697
+ request: makeRequest3(),
6698
+ nativeStream: [
6699
+ threadStarted(NEW_THREAD_ID),
6700
+ turnFailed(` ${BOUNDED_NOTICE2}${"y".repeat(20)} `)
6701
+ ],
6702
+ expected: [
6703
+ sessionEvent3(NEW_THREAD_ID),
6704
+ { type: "runtime_notice", body: BOUNDED_NOTICE2 },
6705
+ { type: "result", ok: false, reason: "usage_capped" }
6706
+ ]
6707
+ },
6708
+ {
6709
+ // CT1351: empty, whitespace-only, and bare status-token failures still set
6710
+ // the machine reason but do not add an empty/unhelpful transcript notice.
6711
+ name: "empty failure text emits no runtime notice",
6712
+ request: makeRequest3(),
6713
+ nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
6714
+ expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "error:unknown" }]
6715
+ },
6716
+ {
6717
+ name: "whitespace-only failure text emits no runtime notice",
6718
+ request: makeRequest3(),
6719
+ nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed(" ")],
6720
+ expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "error: " }]
6721
+ },
6722
+ {
6723
+ name: "bare status token emits no runtime notice",
6724
+ request: makeRequest3(),
6725
+ nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429")],
6726
+ expected: [sessionEvent3(NEW_THREAD_ID), { type: "result", ok: false, reason: "rate_limited" }]
6727
+ },
6728
+ {
6729
+ name: "single-word provider sentence emits a runtime notice",
6730
+ request: makeRequest3(),
6731
+ nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("Unavailable.")],
6732
+ expected: [
6733
+ sessionEvent3(NEW_THREAD_ID),
6734
+ { type: "runtime_notice", body: "Unavailable." },
6735
+ { type: "result", ok: false, reason: "error:Unavailable." }
6736
+ ]
6737
+ },
6588
6738
  {
6589
6739
  // CT481: fail LOUD on unknown model metadata. An unknown `--model` makes Codex
6590
6740
  // emit an `error` item ("… Defaulting to fallback metadata …") and then go
@@ -6929,7 +7079,14 @@ function createSkipState() {
6929
7079
  return { skipped: false, reason: null };
6930
7080
  }
6931
7081
  function createAskState() {
6932
- return { targetUserId: null, question: null, headline: null, options: null, questions: null };
7082
+ return {
7083
+ targetUserId: null,
7084
+ question: null,
7085
+ headline: null,
7086
+ options: null,
7087
+ questions: null,
7088
+ order: null
7089
+ };
6933
7090
  }
6934
7091
  function createWakeState() {
6935
7092
  return { afterSeconds: null, at: null, note: null, cancelled: false };
@@ -7075,6 +7232,7 @@ function createTurnControlMcpServer(sendState, skipState, askState, wakeState, r
7075
7232
  };
7076
7233
  }
7077
7234
  askState.targetUserId = args.targetUserId;
7235
+ askState.order = controlOrder?.next() ?? null;
7078
7236
  if (hasArray) {
7079
7237
  askState.questions = args.questions;
7080
7238
  askState.question = null;
@@ -7622,9 +7780,11 @@ var TurnCommitter = class {
7622
7780
  // the server sorts it after every recorded one — it binds the turn's closing
7623
7781
  // words.
7624
7782
  //
7625
- // The ask is absent by design, not by omission: its carrier queues last among
7626
- // the turn's rows however early the tool fired, so there is no position to
7627
- // report.
7783
+ // CT1347: the ask has a position too. Its carrier used to queue last however
7784
+ // early the tool fired, because a landed open ask held the conversation and an
7785
+ // ask ahead of its own turn's send would have stranded that send behind a
7786
+ // person's response time. Nothing holds now, so the ask is an addressed intent
7787
+ // like the other two and queues where it was called.
7628
7788
  orderField(fields) {
7629
7789
  const order = {};
7630
7790
  const replyOrder = this.deps.replyState.order;
@@ -7632,6 +7792,9 @@ var TurnCommitter = class {
7632
7792
  if (fields.dispatch !== void 0 && this.deps.sendState.order !== null) {
7633
7793
  order.send = this.deps.sendState.order;
7634
7794
  }
7795
+ if (fields.ask !== void 0 && this.deps.askState.order !== null) {
7796
+ order.ask = this.deps.askState.order;
7797
+ }
7635
7798
  return Object.keys(order).length > 0 ? { turnControlOrder: order } : {};
7636
7799
  }
7637
7800
  // The declared reply: explicit `reply_to` first, else the runtime's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.79",
3
+ "version": "0.6.80",
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",