@cabane/companion 0.6.143 → 0.6.145

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 +133 -38
  2. package/dist/runtime.js +132 -37
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1816,14 +1816,11 @@ var init_turn_event = __esm({
1816
1816
  // claude-code/codex it's the runtime's CUMULATIVE total summed across every model
1817
1817
  // request in the agentic turn, so it grows with the tool-call count and is NOT
1818
1818
  // "how full is the window." `contextTokens` is the distinct CONTEXT-OCCUPANCY
1819
- // read: the input the model saw on its FINAL request of the turn (uncached +
1820
- // cache, since cached tokens still occupy the window) — the number the composer
1821
- // gauge wants. `contextWindow` is the model's true window in tokens when the
1822
- // runtime reports it (claude-code's SDK does, per model) — a real denominator so
1823
- // the gauge can show a fraction. Both optional: a runtime that can't source a
1824
- // clean final-request figure (codex's cumulative-only usage) omits `contextTokens`
1825
- // and the gauge falls back to the raw count; `contextWindow` falls back to the
1826
- // model catalog.
1819
+ // read and `contextWindow` the window it is measured against, both as the
1820
+ // harness reported them (claude-code's `getContextUsage`, or its final
1821
+ // request's input and `modelUsage` window when that call gives nothing). A
1822
+ // runtime that can't source them (codex's cumulative-only usage) omits both,
1823
+ // and no surface shows a reading for that turn.
1827
1824
  //
1828
1825
  // CT601: two more optional carry-homes on the terminal result, alongside
1829
1826
  // `usage`. `resolvedModel` is the CONCRETE model the runtime actually ran —
@@ -1846,7 +1843,25 @@ var init_turn_event = __esm({
1846
1843
  cacheReadTokens: z6.number().optional(),
1847
1844
  cacheCreationTokens: z6.number().optional(),
1848
1845
  contextTokens: z6.number().optional(),
1849
- contextWindow: z6.number().optional()
1846
+ contextWindow: z6.number().optional(),
1847
+ // CT1751: the rest of the harness's own context reading, when it gave
1848
+ // one: where it will compact, its `/context` breakdown, and every
1849
+ // compaction it reported during the turn. Claude Code only.
1850
+ contextCompactAt: z6.number().optional(),
1851
+ contextCategories: z6.array(
1852
+ z6.object({
1853
+ name: z6.string(),
1854
+ tokens: z6.number(),
1855
+ kind: z6.enum(["used", "free", "buffer", "deferred"])
1856
+ })
1857
+ ).optional(),
1858
+ contextCompactions: z6.array(
1859
+ z6.object({
1860
+ trigger: z6.enum(["manual", "auto"]),
1861
+ preTokens: z6.number(),
1862
+ postTokens: z6.number().optional()
1863
+ })
1864
+ ).optional()
1850
1865
  }).optional(),
1851
1866
  resolvedModel: z6.string().optional(),
1852
1867
  resolvedConfig: z6.object({
@@ -2244,24 +2259,18 @@ function summarizeToolInput(input) {
2244
2259
  }
2245
2260
  return "";
2246
2261
  }
2247
- function summarizeToolIo(input) {
2262
+ function renderToolIo(input) {
2248
2263
  if (input === void 0 || input === null) return "";
2249
- if (typeof input === "string") {
2250
- const trimmed = input.trim();
2251
- return trimmed.length > 0 ? truncate(trimmed, TOOL_IO_MAX_CHARS) : "";
2252
- }
2264
+ if (typeof input === "string") return input.trim();
2253
2265
  try {
2254
- return truncate(JSON.stringify(input), TOOL_IO_MAX_CHARS);
2266
+ return JSON.stringify(input) ?? "";
2255
2267
  } catch {
2256
2268
  return "";
2257
2269
  }
2258
2270
  }
2259
- function summarizeToolResult(content) {
2271
+ function renderToolResult(content) {
2260
2272
  if (content === void 0 || content === null) return "";
2261
- if (typeof content === "string") {
2262
- const trimmed = content.trim();
2263
- return trimmed.length > 0 ? truncate(trimmed, TOOL_IO_MAX_CHARS) : "";
2264
- }
2273
+ if (typeof content === "string") return content.trim();
2265
2274
  if (Array.isArray(content)) {
2266
2275
  const parts = [];
2267
2276
  let sawContentEntry = false;
@@ -2276,12 +2285,12 @@ function summarizeToolResult(content) {
2276
2285
  }
2277
2286
  }
2278
2287
  const joined = parts.join("\n").trim();
2279
- if (joined.length > 0) return truncate(joined, TOOL_IO_MAX_CHARS);
2288
+ if (joined.length > 0) return joined;
2280
2289
  if (sawContentEntry || content.length === 0) return "";
2281
2290
  }
2282
2291
  if (typeof content !== "object") return "";
2283
2292
  if (!Array.isArray(content) && Object.keys(content).length === 0) return "";
2284
- return summarizeToolIo(content);
2293
+ return renderToolIo(content);
2285
2294
  }
2286
2295
  function truncate(s, max) {
2287
2296
  if (s.length <= max) return s;
@@ -2305,14 +2314,19 @@ function sinkEmitter(sink, _onError) {
2305
2314
  return;
2306
2315
  case "tool":
2307
2316
  if (sink.onToolActivity) {
2317
+ const input = renderToolIo(event.input);
2318
+ const result = event.result === void 0 ? null : renderToolResult(event.result);
2308
2319
  await sink.onToolActivity({
2309
2320
  toolUseId: event.id,
2310
2321
  toolName: event.name,
2311
2322
  summary: event.summary,
2312
- // SJ529: render the raw payloads for the tool log here — the single
2313
- // truncation site (CT255 consumer concern).
2314
- input: summarizeToolIo(event.input),
2315
- result: event.result === void 0 ? "" : summarizeToolResult(event.result),
2323
+ input: truncate(input, TOOL_IO_MAX_CHARS),
2324
+ result: result === null ? "" : truncate(result, TOOL_IO_MAX_CHARS),
2325
+ // CT1752: only when the event carried an input — a completion whose
2326
+ // start this process never saw has none, and a zero would overwrite
2327
+ // the size its start recorded.
2328
+ ...event.input === void 0 ? {} : { inputChars: input.length },
2329
+ ...result === null ? {} : { resultChars: result.length },
2316
2330
  // CT496: carry the originating MCP server (null when absent / a host tool).
2317
2331
  mcpServer: event.mcpServer ?? null,
2318
2332
  phase: event.phase
@@ -2385,6 +2399,8 @@ var init_pump = __esm({
2385
2399
  summary: evt.summary,
2386
2400
  input: evt.input,
2387
2401
  result: evt.result,
2402
+ ...evt.inputChars !== void 0 ? { inputChars: evt.inputChars } : {},
2403
+ ...evt.resultChars !== void 0 ? { resultChars: evt.resultChars } : {},
2388
2404
  mcpServer: evt.mcpServer,
2389
2405
  phase: evt.phase,
2390
2406
  seq
@@ -2902,6 +2918,7 @@ async function* decodeSdkStream(iter, ctx) {
2902
2918
  let resultReason;
2903
2919
  let sawResult = false;
2904
2920
  let usage;
2921
+ const compactions = [];
2905
2922
  let lastRequestContextTokens;
2906
2923
  let resolvedModel;
2907
2924
  const signals = { sawRejectedLimit: false };
@@ -2927,6 +2944,9 @@ async function* decodeSdkStream(iter, ctx) {
2927
2944
  ...ctx.degraded ? { degraded: true } : {}
2928
2945
  };
2929
2946
  }
2947
+ } else if (msg.type === "system" && msg.subtype === "compact_boundary") {
2948
+ const compaction = readCompaction(msg);
2949
+ if (compaction) compactions.push(compaction);
2930
2950
  } else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
2931
2951
  if (preInitToolActivity(msg) && ctx.startEvidence) ctx.startEvidence.toolActivity = true;
2932
2952
  const failure = classifyPreInitFrame(msg, signals);
@@ -2971,6 +2991,9 @@ async function* decodeSdkStream(iter, ctx) {
2971
2991
  usage.contextTokens = lastRequestContextTokens;
2972
2992
  const window = readContextWindow(msg, resolvedModel);
2973
2993
  if (window !== void 0) usage.contextWindow = window;
2994
+ if (compactions.length > 0) usage.contextCompactions = compactions;
2995
+ const reading = await readHarnessContext(ctx.readContextUsage);
2996
+ if (reading) Object.assign(usage, reading);
2974
2997
  }
2975
2998
  const isError = msg.is_error === true;
2976
2999
  if (msg.subtype === "success" && !isError) {
@@ -3121,6 +3144,56 @@ function readRequestContextTokens(msg) {
3121
3144
  const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
3122
3145
  return num(usage.input_tokens) + num(usage.cache_read_input_tokens) + num(usage.cache_creation_input_tokens);
3123
3146
  }
3147
+ async function readHarnessContext(read) {
3148
+ if (!read) return void 0;
3149
+ let timer;
3150
+ const timeout = new Promise((resolve2) => {
3151
+ timer = setTimeout(() => resolve2(void 0), CONTEXT_READING_TIMEOUT_MS);
3152
+ });
3153
+ try {
3154
+ return parseContextUsage(await Promise.race([read(), timeout]));
3155
+ } catch {
3156
+ return void 0;
3157
+ } finally {
3158
+ clearTimeout(timer);
3159
+ }
3160
+ }
3161
+ function parseContextUsage(response) {
3162
+ const r = response;
3163
+ if (!r || typeof r !== "object") return void 0;
3164
+ const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
3165
+ const total = count(r.totalTokens);
3166
+ const window = count(r.maxTokens);
3167
+ if (total === void 0 || window === void 0 || window === 0) return void 0;
3168
+ const categories = [];
3169
+ for (const c of Array.isArray(r.categories) ? r.categories : []) {
3170
+ const row = c;
3171
+ const tokens = count(row?.tokens);
3172
+ if (typeof row?.name === "string" && tokens !== void 0 && typeof row.kind === "string" && CATEGORY_KINDS.has(row.kind)) {
3173
+ categories.push({ name: row.name, tokens, kind: row.kind });
3174
+ }
3175
+ }
3176
+ const buffer = categories.filter((c) => c.kind === "buffer").reduce((sum, c) => sum + c.tokens, 0);
3177
+ return {
3178
+ contextTokens: total,
3179
+ contextWindow: window,
3180
+ ...buffer > 0 && buffer < window ? { contextCompactAt: window - buffer } : {},
3181
+ ...categories.length > 0 ? { contextCategories: categories } : {}
3182
+ };
3183
+ }
3184
+ function readCompaction(msg) {
3185
+ const meta = msg.compact_metadata;
3186
+ const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
3187
+ const preTokens = count(meta?.pre_tokens);
3188
+ if (!meta || preTokens === void 0) return void 0;
3189
+ if (meta.trigger !== "manual" && meta.trigger !== "auto") return void 0;
3190
+ const postTokens = count(meta.post_tokens);
3191
+ return {
3192
+ trigger: meta.trigger,
3193
+ preTokens,
3194
+ ...postTokens !== void 0 ? { postTokens } : {}
3195
+ };
3196
+ }
3124
3197
  function readContextWindow(msg, resolvedModel) {
3125
3198
  const modelUsage = msg.modelUsage;
3126
3199
  if (!modelUsage || typeof modelUsage !== "object") return void 0;
@@ -3136,7 +3209,7 @@ function readContextWindow(msg, resolvedModel) {
3136
3209
  }
3137
3210
  return max;
3138
3211
  }
3139
- var HARNESS_PLACEHOLDERS;
3212
+ var HARNESS_PLACEHOLDERS, CONTEXT_READING_TIMEOUT_MS, CATEGORY_KINDS;
3140
3213
  var init_loop = __esm({
3141
3214
  "packages/agent-runtime/src/claude-code/loop.ts"() {
3142
3215
  "use strict";
@@ -3144,11 +3217,13 @@ var init_loop = __esm({
3144
3217
  init_failure();
3145
3218
  init_session();
3146
3219
  HARNESS_PLACEHOLDERS = /* @__PURE__ */ new Set(["No response requested.", "(no content)"]);
3220
+ CONTEXT_READING_TIMEOUT_MS = 2e3;
3221
+ CATEGORY_KINDS = /* @__PURE__ */ new Set(["used", "free", "buffer", "deferred"]);
3147
3222
  }
3148
3223
  });
3149
3224
 
3150
3225
  // packages/agent-runtime/src/claude-code/prompt-input.ts
3151
- function buildQueryPrompt(req) {
3226
+ function buildQueryPrompt(req, openUntil) {
3152
3227
  const content = req.content;
3153
3228
  return (async function* () {
3154
3229
  yield {
@@ -3157,6 +3232,7 @@ function buildQueryPrompt(req) {
3157
3232
  message: { role: "user", content },
3158
3233
  parent_tool_use_id: null
3159
3234
  };
3235
+ if (openUntil) await openUntil;
3160
3236
  })();
3161
3237
  }
3162
3238
  var init_prompt_input = __esm({
@@ -3176,19 +3252,34 @@ function attachAbortController(options, signal) {
3176
3252
  else signal.addEventListener("abort", () => sdkAbort.abort(), { once: true });
3177
3253
  options.abortController = sdkAbort;
3178
3254
  }
3179
- function sdkEvents(queryFn, req, signal, built, degraded, startEvidence) {
3255
+ async function* sdkEvents(queryFn, req, signal, built, degraded, startEvidence) {
3180
3256
  attachAbortController(built.options, signal);
3181
- const prompt = buildQueryPrompt(req);
3182
- const iter = {
3183
- [Symbol.asyncIterator]: () => queryFn({ prompt, options: built.options })[Symbol.asyncIterator]()
3257
+ let release = () => {
3184
3258
  };
3185
- return decodeSdkStream(iter, {
3186
- signal,
3187
- cwd: req.local.cwd,
3188
- resumedSessionId: built.resume,
3189
- degraded,
3190
- ...startEvidence ? { startEvidence } : {}
3259
+ const held = new Promise((resolve2) => {
3260
+ release = resolve2;
3191
3261
  });
3262
+ const prompt = buildQueryPrompt(req, held);
3263
+ let active;
3264
+ const iter = {
3265
+ [Symbol.asyncIterator]: () => {
3266
+ const q = queryFn({ prompt, options: built.options });
3267
+ active = q;
3268
+ return q[Symbol.asyncIterator]();
3269
+ }
3270
+ };
3271
+ try {
3272
+ yield* decodeSdkStream(iter, {
3273
+ signal,
3274
+ readContextUsage: async () => typeof active?.getContextUsage === "function" ? active.getContextUsage({ detail: "summary" }) : void 0,
3275
+ cwd: req.local.cwd,
3276
+ resumedSessionId: built.resume,
3277
+ degraded,
3278
+ ...startEvidence ? { startEvidence } : {}
3279
+ });
3280
+ } finally {
3281
+ release();
3282
+ }
3192
3283
  }
3193
3284
  async function freshRequest(req, reason, hooks) {
3194
3285
  const recomposed = hooks?.fresh ? await hooks.fresh(reason) : null;
@@ -8883,6 +8974,8 @@ var init_turn_committer = __esm({
8883
8974
  summary,
8884
8975
  input,
8885
8976
  result,
8977
+ inputChars,
8978
+ resultChars,
8886
8979
  mcpServer,
8887
8980
  phase,
8888
8981
  seq
@@ -8898,6 +8991,8 @@ var init_turn_committer = __esm({
8898
8991
  summary,
8899
8992
  input,
8900
8993
  result,
8994
+ ...inputChars !== void 0 ? { inputChars } : {},
8995
+ ...resultChars !== void 0 ? { resultChars } : {},
8901
8996
  mcpServer,
8902
8997
  phase,
8903
8998
  seq,
package/dist/runtime.js CHANGED
@@ -3309,14 +3309,11 @@ var turnEventSchema = z6.discriminatedUnion("type", [
3309
3309
  // claude-code/codex it's the runtime's CUMULATIVE total summed across every model
3310
3310
  // request in the agentic turn, so it grows with the tool-call count and is NOT
3311
3311
  // "how full is the window." `contextTokens` is the distinct CONTEXT-OCCUPANCY
3312
- // read: the input the model saw on its FINAL request of the turn (uncached +
3313
- // cache, since cached tokens still occupy the window) — the number the composer
3314
- // gauge wants. `contextWindow` is the model's true window in tokens when the
3315
- // runtime reports it (claude-code's SDK does, per model) — a real denominator so
3316
- // the gauge can show a fraction. Both optional: a runtime that can't source a
3317
- // clean final-request figure (codex's cumulative-only usage) omits `contextTokens`
3318
- // and the gauge falls back to the raw count; `contextWindow` falls back to the
3319
- // model catalog.
3312
+ // read and `contextWindow` the window it is measured against, both as the
3313
+ // harness reported them (claude-code's `getContextUsage`, or its final
3314
+ // request's input and `modelUsage` window when that call gives nothing). A
3315
+ // runtime that can't source them (codex's cumulative-only usage) omits both,
3316
+ // and no surface shows a reading for that turn.
3320
3317
  //
3321
3318
  // CT601: two more optional carry-homes on the terminal result, alongside
3322
3319
  // `usage`. `resolvedModel` is the CONCRETE model the runtime actually ran —
@@ -3339,7 +3336,25 @@ var turnEventSchema = z6.discriminatedUnion("type", [
3339
3336
  cacheReadTokens: z6.number().optional(),
3340
3337
  cacheCreationTokens: z6.number().optional(),
3341
3338
  contextTokens: z6.number().optional(),
3342
- contextWindow: z6.number().optional()
3339
+ contextWindow: z6.number().optional(),
3340
+ // CT1751: the rest of the harness's own context reading, when it gave
3341
+ // one: where it will compact, its `/context` breakdown, and every
3342
+ // compaction it reported during the turn. Claude Code only.
3343
+ contextCompactAt: z6.number().optional(),
3344
+ contextCategories: z6.array(
3345
+ z6.object({
3346
+ name: z6.string(),
3347
+ tokens: z6.number(),
3348
+ kind: z6.enum(["used", "free", "buffer", "deferred"])
3349
+ })
3350
+ ).optional(),
3351
+ contextCompactions: z6.array(
3352
+ z6.object({
3353
+ trigger: z6.enum(["manual", "auto"]),
3354
+ preTokens: z6.number(),
3355
+ postTokens: z6.number().optional()
3356
+ })
3357
+ ).optional()
3343
3358
  }).optional(),
3344
3359
  resolvedModel: z6.string().optional(),
3345
3360
  resolvedConfig: z6.object({
@@ -3704,24 +3719,18 @@ function summarizeToolInput(input) {
3704
3719
  return "";
3705
3720
  }
3706
3721
  var TOOL_IO_MAX_CHARS = 1e4;
3707
- function summarizeToolIo(input) {
3722
+ function renderToolIo(input) {
3708
3723
  if (input === void 0 || input === null) return "";
3709
- if (typeof input === "string") {
3710
- const trimmed = input.trim();
3711
- return trimmed.length > 0 ? truncate(trimmed, TOOL_IO_MAX_CHARS) : "";
3712
- }
3724
+ if (typeof input === "string") return input.trim();
3713
3725
  try {
3714
- return truncate(JSON.stringify(input), TOOL_IO_MAX_CHARS);
3726
+ return JSON.stringify(input) ?? "";
3715
3727
  } catch {
3716
3728
  return "";
3717
3729
  }
3718
3730
  }
3719
- function summarizeToolResult(content) {
3731
+ function renderToolResult(content) {
3720
3732
  if (content === void 0 || content === null) return "";
3721
- if (typeof content === "string") {
3722
- const trimmed = content.trim();
3723
- return trimmed.length > 0 ? truncate(trimmed, TOOL_IO_MAX_CHARS) : "";
3724
- }
3733
+ if (typeof content === "string") return content.trim();
3725
3734
  if (Array.isArray(content)) {
3726
3735
  const parts = [];
3727
3736
  let sawContentEntry = false;
@@ -3736,12 +3745,12 @@ function summarizeToolResult(content) {
3736
3745
  }
3737
3746
  }
3738
3747
  const joined = parts.join("\n").trim();
3739
- if (joined.length > 0) return truncate(joined, TOOL_IO_MAX_CHARS);
3748
+ if (joined.length > 0) return joined;
3740
3749
  if (sawContentEntry || content.length === 0) return "";
3741
3750
  }
3742
3751
  if (typeof content !== "object") return "";
3743
3752
  if (!Array.isArray(content) && Object.keys(content).length === 0) return "";
3744
- return summarizeToolIo(content);
3753
+ return renderToolIo(content);
3745
3754
  }
3746
3755
  function truncate(s, max) {
3747
3756
  if (s.length <= max) return s;
@@ -3758,14 +3767,19 @@ function sinkEmitter(sink, _onError) {
3758
3767
  return;
3759
3768
  case "tool":
3760
3769
  if (sink.onToolActivity) {
3770
+ const input = renderToolIo(event.input);
3771
+ const result = event.result === void 0 ? null : renderToolResult(event.result);
3761
3772
  await sink.onToolActivity({
3762
3773
  toolUseId: event.id,
3763
3774
  toolName: event.name,
3764
3775
  summary: event.summary,
3765
- // SJ529: render the raw payloads for the tool log here — the single
3766
- // truncation site (CT255 consumer concern).
3767
- input: summarizeToolIo(event.input),
3768
- result: event.result === void 0 ? "" : summarizeToolResult(event.result),
3776
+ input: truncate(input, TOOL_IO_MAX_CHARS),
3777
+ result: result === null ? "" : truncate(result, TOOL_IO_MAX_CHARS),
3778
+ // CT1752: only when the event carried an input — a completion whose
3779
+ // start this process never saw has none, and a zero would overwrite
3780
+ // the size its start recorded.
3781
+ ...event.input === void 0 ? {} : { inputChars: input.length },
3782
+ ...result === null ? {} : { resultChars: result.length },
3769
3783
  // CT496: carry the originating MCP server (null when absent / a host tool).
3770
3784
  mcpServer: event.mcpServer ?? null,
3771
3785
  phase: event.phase
@@ -3826,6 +3840,8 @@ var TurnPump = class {
3826
3840
  summary: evt.summary,
3827
3841
  input: evt.input,
3828
3842
  result: evt.result,
3843
+ ...evt.inputChars !== void 0 ? { inputChars: evt.inputChars } : {},
3844
+ ...evt.resultChars !== void 0 ? { resultChars: evt.resultChars } : {},
3829
3845
  mcpServer: evt.mcpServer,
3830
3846
  phase: evt.phase,
3831
3847
  seq
@@ -4294,6 +4310,7 @@ async function* decodeSdkStream(iter, ctx) {
4294
4310
  let resultReason;
4295
4311
  let sawResult = false;
4296
4312
  let usage;
4313
+ const compactions = [];
4297
4314
  let lastRequestContextTokens;
4298
4315
  let resolvedModel;
4299
4316
  const signals = { sawRejectedLimit: false };
@@ -4319,6 +4336,9 @@ async function* decodeSdkStream(iter, ctx) {
4319
4336
  ...ctx.degraded ? { degraded: true } : {}
4320
4337
  };
4321
4338
  }
4339
+ } else if (msg.type === "system" && msg.subtype === "compact_boundary") {
4340
+ const compaction = readCompaction(msg);
4341
+ if (compaction) compactions.push(compaction);
4322
4342
  } else if (!workspaceProven && (msg.type === "assistant" || msg.type === "user" || msg.type === "result")) {
4323
4343
  if (preInitToolActivity(msg) && ctx.startEvidence) ctx.startEvidence.toolActivity = true;
4324
4344
  const failure = classifyPreInitFrame(msg, signals);
@@ -4363,6 +4383,9 @@ async function* decodeSdkStream(iter, ctx) {
4363
4383
  usage.contextTokens = lastRequestContextTokens;
4364
4384
  const window = readContextWindow(msg, resolvedModel);
4365
4385
  if (window !== void 0) usage.contextWindow = window;
4386
+ if (compactions.length > 0) usage.contextCompactions = compactions;
4387
+ const reading = await readHarnessContext(ctx.readContextUsage);
4388
+ if (reading) Object.assign(usage, reading);
4366
4389
  }
4367
4390
  const isError = msg.is_error === true;
4368
4391
  if (msg.subtype === "success" && !isError) {
@@ -4514,6 +4537,58 @@ function readRequestContextTokens(msg) {
4514
4537
  const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
4515
4538
  return num(usage.input_tokens) + num(usage.cache_read_input_tokens) + num(usage.cache_creation_input_tokens);
4516
4539
  }
4540
+ var CONTEXT_READING_TIMEOUT_MS = 2e3;
4541
+ async function readHarnessContext(read) {
4542
+ if (!read) return void 0;
4543
+ let timer;
4544
+ const timeout = new Promise((resolve2) => {
4545
+ timer = setTimeout(() => resolve2(void 0), CONTEXT_READING_TIMEOUT_MS);
4546
+ });
4547
+ try {
4548
+ return parseContextUsage(await Promise.race([read(), timeout]));
4549
+ } catch {
4550
+ return void 0;
4551
+ } finally {
4552
+ clearTimeout(timer);
4553
+ }
4554
+ }
4555
+ var CATEGORY_KINDS = /* @__PURE__ */ new Set(["used", "free", "buffer", "deferred"]);
4556
+ function parseContextUsage(response) {
4557
+ const r = response;
4558
+ if (!r || typeof r !== "object") return void 0;
4559
+ const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
4560
+ const total = count(r.totalTokens);
4561
+ const window = count(r.maxTokens);
4562
+ if (total === void 0 || window === void 0 || window === 0) return void 0;
4563
+ const categories = [];
4564
+ for (const c of Array.isArray(r.categories) ? r.categories : []) {
4565
+ const row = c;
4566
+ const tokens = count(row?.tokens);
4567
+ if (typeof row?.name === "string" && tokens !== void 0 && typeof row.kind === "string" && CATEGORY_KINDS.has(row.kind)) {
4568
+ categories.push({ name: row.name, tokens, kind: row.kind });
4569
+ }
4570
+ }
4571
+ const buffer = categories.filter((c) => c.kind === "buffer").reduce((sum, c) => sum + c.tokens, 0);
4572
+ return {
4573
+ contextTokens: total,
4574
+ contextWindow: window,
4575
+ ...buffer > 0 && buffer < window ? { contextCompactAt: window - buffer } : {},
4576
+ ...categories.length > 0 ? { contextCategories: categories } : {}
4577
+ };
4578
+ }
4579
+ function readCompaction(msg) {
4580
+ const meta = msg.compact_metadata;
4581
+ const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : void 0;
4582
+ const preTokens = count(meta?.pre_tokens);
4583
+ if (!meta || preTokens === void 0) return void 0;
4584
+ if (meta.trigger !== "manual" && meta.trigger !== "auto") return void 0;
4585
+ const postTokens = count(meta.post_tokens);
4586
+ return {
4587
+ trigger: meta.trigger,
4588
+ preTokens,
4589
+ ...postTokens !== void 0 ? { postTokens } : {}
4590
+ };
4591
+ }
4517
4592
  function readContextWindow(msg, resolvedModel) {
4518
4593
  const modelUsage = msg.modelUsage;
4519
4594
  if (!modelUsage || typeof modelUsage !== "object") return void 0;
@@ -4531,7 +4606,7 @@ function readContextWindow(msg, resolvedModel) {
4531
4606
  }
4532
4607
 
4533
4608
  // packages/agent-runtime/src/claude-code/prompt-input.ts
4534
- function buildQueryPrompt(req) {
4609
+ function buildQueryPrompt(req, openUntil) {
4535
4610
  const content = req.content;
4536
4611
  return (async function* () {
4537
4612
  yield {
@@ -4540,6 +4615,7 @@ function buildQueryPrompt(req) {
4540
4615
  message: { role: "user", content },
4541
4616
  parent_tool_use_id: null
4542
4617
  };
4618
+ if (openUntil) await openUntil;
4543
4619
  })();
4544
4620
  }
4545
4621
 
@@ -4551,19 +4627,34 @@ function attachAbortController(options, signal) {
4551
4627
  else signal.addEventListener("abort", () => sdkAbort.abort(), { once: true });
4552
4628
  options.abortController = sdkAbort;
4553
4629
  }
4554
- function sdkEvents(queryFn, req, signal, built, degraded, startEvidence) {
4630
+ async function* sdkEvents(queryFn, req, signal, built, degraded, startEvidence) {
4555
4631
  attachAbortController(built.options, signal);
4556
- const prompt = buildQueryPrompt(req);
4557
- const iter = {
4558
- [Symbol.asyncIterator]: () => queryFn({ prompt, options: built.options })[Symbol.asyncIterator]()
4632
+ let release = () => {
4559
4633
  };
4560
- return decodeSdkStream(iter, {
4561
- signal,
4562
- cwd: req.local.cwd,
4563
- resumedSessionId: built.resume,
4564
- degraded,
4565
- ...startEvidence ? { startEvidence } : {}
4634
+ const held = new Promise((resolve2) => {
4635
+ release = resolve2;
4566
4636
  });
4637
+ const prompt = buildQueryPrompt(req, held);
4638
+ let active;
4639
+ const iter = {
4640
+ [Symbol.asyncIterator]: () => {
4641
+ const q = queryFn({ prompt, options: built.options });
4642
+ active = q;
4643
+ return q[Symbol.asyncIterator]();
4644
+ }
4645
+ };
4646
+ try {
4647
+ yield* decodeSdkStream(iter, {
4648
+ signal,
4649
+ readContextUsage: async () => typeof active?.getContextUsage === "function" ? active.getContextUsage({ detail: "summary" }) : void 0,
4650
+ cwd: req.local.cwd,
4651
+ resumedSessionId: built.resume,
4652
+ degraded,
4653
+ ...startEvidence ? { startEvidence } : {}
4654
+ });
4655
+ } finally {
4656
+ release();
4657
+ }
4567
4658
  }
4568
4659
  async function freshRequest(req, reason, hooks) {
4569
4660
  const recomposed = hooks?.fresh ? await hooks.fresh(reason) : null;
@@ -9902,6 +9993,8 @@ var TurnCommitter = class {
9902
9993
  summary,
9903
9994
  input,
9904
9995
  result,
9996
+ inputChars,
9997
+ resultChars,
9905
9998
  mcpServer,
9906
9999
  phase,
9907
10000
  seq
@@ -9917,6 +10010,8 @@ var TurnCommitter = class {
9917
10010
  summary,
9918
10011
  input,
9919
10012
  result,
10013
+ ...inputChars !== void 0 ? { inputChars } : {},
10014
+ ...resultChars !== void 0 ? { resultChars } : {},
9920
10015
  mcpServer,
9921
10016
  phase,
9922
10017
  seq,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.143",
3
+ "version": "0.6.145",
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",