@usabledev/usable-chat 1.152.0 → 1.152.1

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 (2) hide show
  1. package/cli.js +292 -125
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -35469,10 +35469,13 @@ function contentText(content) {
35469
35469
  return content.map((p28) => {
35470
35470
  if (typeof p28.text === "string") return p28.text;
35471
35471
  if (p28.type === "tool-call") return `[tool-call: ${String(p28.toolName ?? "")}]`;
35472
- if (p28.type === "tool-result") {
35473
- const out = p28.result ?? p28.output;
35472
+ if (p28.type === "tool-result" || p28.type === "tool_result") {
35473
+ const out = p28.result ?? p28.output ?? p28.content;
35474
35474
  const s17 = typeof out === "string" ? out : JSON.stringify(out ?? "");
35475
- return `[tool-result: ${s17.slice(0, 2e3)}]`;
35475
+ const pointers = s17.match(/\[⚠️ FULL RESULT TOO LARGE[^\]]*\]/g) ?? [];
35476
+ const tail2 = pointers.length ? `
35477
+ ${pointers.join("\n")}` : "";
35478
+ return `[tool-result: ${s17.slice(0, 2e3)}${tail2}]`;
35476
35479
  }
35477
35480
  return "";
35478
35481
  }).filter(Boolean).join("\n");
@@ -35485,6 +35488,64 @@ function buildCompactionTranscript(messages4) {
35485
35488
  return text3.trim() ? `${m33.role.toUpperCase()}: ${text3}` : "";
35486
35489
  }).filter(Boolean).join("\n\n");
35487
35490
  }
35491
+ function fingerprintMessages(messages4) {
35492
+ let h25 = 5381;
35493
+ for (const m33 of messages4) {
35494
+ const s17 = m33.role + ":" + (typeof m33.content === "string" ? m33.content : JSON.stringify(m33.content)).slice(0, 200);
35495
+ for (let i18 = 0; i18 < s17.length; i18++) h25 = (h25 << 5) + h25 + s17.charCodeAt(i18) | 0;
35496
+ }
35497
+ return `${h25 >>> 0}:${messages4.length}`;
35498
+ }
35499
+ function getCachedCompaction(conversationId) {
35500
+ return compactionCache.get(conversationId);
35501
+ }
35502
+ function setCachedCompaction(conversationId, entry) {
35503
+ if (compactionCache.size >= COMPACTION_CACHE_MAX && !compactionCache.has(conversationId)) {
35504
+ const oldest = compactionCache.keys().next().value;
35505
+ if (oldest !== void 0) compactionCache.delete(oldest);
35506
+ }
35507
+ compactionCache.set(conversationId, entry);
35508
+ }
35509
+ async function generateCompactionSummaryCached(conversationId, dropped, opts) {
35510
+ const cached2 = conversationId ? getCachedCompaction(conversationId) : void 0;
35511
+ if (cached2 && dropped.length >= cached2.count) {
35512
+ const prefixFp = fingerprintMessages(dropped.slice(0, cached2.count));
35513
+ if (prefixFp === cached2.fingerprint) {
35514
+ if (dropped.length === cached2.count) {
35515
+ return { summary: cached2.summary, cacheHit: "exact" };
35516
+ }
35517
+ const delta = dropped.slice(cached2.count);
35518
+ const summary2 = await generateCompactionSummary(
35519
+ [
35520
+ {
35521
+ role: "user",
35522
+ content: `[prior checkpoint summary of the earlier conversation]
35523
+ ${cached2.summary}`
35524
+ },
35525
+ ...delta
35526
+ ],
35527
+ opts
35528
+ );
35529
+ if (summary2 && conversationId) {
35530
+ setCachedCompaction(conversationId, {
35531
+ fingerprint: fingerprintMessages(dropped),
35532
+ count: dropped.length,
35533
+ summary: summary2
35534
+ });
35535
+ }
35536
+ return { summary: summary2, cacheHit: "rolling" };
35537
+ }
35538
+ }
35539
+ const summary = await generateCompactionSummary(dropped, opts);
35540
+ if (summary && conversationId) {
35541
+ setCachedCompaction(conversationId, {
35542
+ fingerprint: fingerprintMessages(dropped),
35543
+ count: dropped.length,
35544
+ summary
35545
+ });
35546
+ }
35547
+ return { summary, cacheHit: "miss" };
35548
+ }
35488
35549
  async function viaOpenRouter(system, user, opts) {
35489
35550
  const apiKey = opts.provider === "usable" ? opts.authToken ?? "" : opts.openRouterApiKey ?? "";
35490
35551
  const provider = createOpenRouter({
@@ -35561,7 +35622,7 @@ async function generateCompactionSummary(messages4, opts) {
35561
35622
  return null;
35562
35623
  }
35563
35624
  }
35564
- var COMPACTION_SYSTEM_PROMPT, DEFAULT_CALLERS;
35625
+ var COMPACTION_SYSTEM_PROMPT, compactionCache, COMPACTION_CACHE_MAX, DEFAULT_CALLERS;
35565
35626
  var init_compaction_summary = __esm({
35566
35627
  "src/core/orchestrator/compaction-summary.ts"() {
35567
35628
  "use strict";
@@ -35580,11 +35641,15 @@ var init_compaction_summary = __esm({
35580
35641
  "- PROGRESS: what is DONE, what is IN-PROGRESS, what is BLOCKED.",
35581
35642
  "- DECISIONS: key choices, constraints, and user preferences to honor.",
35582
35643
  "- FILES: files read and files modified so far (paths).",
35644
+ '- DATA: where large tool results are preserved \u2014 copy any "[\u26A0\uFE0F FULL RESULT TOO LARGE \u2014 \u2026]"',
35645
+ " spill paths VERBATIM so the work can re-read them instead of re-calling the tools.",
35583
35646
  "- NEXT: the concrete next steps to continue from here.",
35584
35647
  "",
35585
35648
  "Be factual and specific (keep ids, paths, names verbatim \u2014 never shorten UUIDs). Do not invent",
35586
35649
  "progress that did not happen. Output the summary only \u2014 no preamble, no sign-off."
35587
35650
  ].join("\n");
35651
+ compactionCache = /* @__PURE__ */ new Map();
35652
+ COMPACTION_CACHE_MAX = 200;
35588
35653
  DEFAULT_CALLERS = {
35589
35654
  openrouter: viaOpenRouter,
35590
35655
  anthropic: viaAnthropic,
@@ -35641,6 +35706,17 @@ function extractFileOps(messages4) {
35641
35706
  }
35642
35707
  return { read: [...read], modified: [...modified] };
35643
35708
  }
35709
+ function extractSpillPointers(messages4) {
35710
+ const paths = /* @__PURE__ */ new Set();
35711
+ const re10 = /(?:\/data\/tool-results\/|[^\s"'\\]*usable-tool-results\/)[^\s"'\\\]]+/g;
35712
+ for (const msg of messages4) {
35713
+ const text3 = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
35714
+ for (const m33 of text3.match(re10) ?? []) {
35715
+ paths.add(m33.replace(/[.,;:!?]+$/, ""));
35716
+ }
35717
+ }
35718
+ return [...paths];
35719
+ }
35644
35720
  function isStepStart(messages4, i18) {
35645
35721
  if (i18 <= 0) return false;
35646
35722
  return messages4[i18].role === "assistant" && messages4[i18 - 1].role !== "assistant";
@@ -164395,6 +164471,7 @@ var init_agents2 = __esm({
164395
164471
  // src/core/utils/tool-result-summarizer.ts
164396
164472
  var tool_result_summarizer_exports = {};
164397
164473
  __export(tool_result_summarizer_exports, {
164474
+ dedupeFragmentContentFields: () => dedupeFragmentContentFields,
164398
164475
  sanitizeImageToolResult: () => sanitizeImageToolResult,
164399
164476
  summarizeToolResultForHistory: () => summarizeToolResultForHistory,
164400
164477
  truncateToolResultWithNotice: () => truncateToolResultWithNotice
@@ -164443,12 +164520,52 @@ function extractFragmentReferences(result) {
164443
164520
  }
164444
164521
  function truncateToolResultWithNotice(serialized, maxChars) {
164445
164522
  if (serialized.length <= maxChars) return serialized;
164446
- const head = serialized.slice(0, maxChars);
164447
- const keptKb = Math.round(maxChars / 1024);
164448
- const omittedKb = Math.round((serialized.length - maxChars) / 1024);
164523
+ let head = serialized.slice(0, maxChars);
164524
+ const boundary = Math.max(
164525
+ head.lastIndexOf("},"),
164526
+ head.lastIndexOf("],"),
164527
+ head.lastIndexOf('",'),
164528
+ head.lastIndexOf("\n")
164529
+ );
164530
+ if (boundary > maxChars * 0.8) head = head.slice(0, boundary + 1);
164531
+ const keptKb = Math.round(head.length / 1024);
164532
+ const omittedKb = Math.max(1, Math.round((serialized.length - head.length) / 1024));
164449
164533
  return `${head}
164450
164534
 
164451
- [tool-result truncated \u2014 kept the first ${keptKb}KB, ${omittedKb}KB omitted. The full result is preserved (see the spill path noted below) \u2014 re-read it if you need the omitted part.]`;
164535
+ [tool-result truncated \u2014 kept the first ${keptKb}KB, ~${omittedKb}KB omitted. The omitted tail is NOT in this conversation unless a spill-file path is noted below. NEVER guess or reconstruct omitted values (ids, uuids, names) \u2014 re-run the tool with narrower filters/pagination to get the part you need, or read the spill file if a path is given.]`;
164536
+ }
164537
+ function dedupeFragmentContentFields(result) {
164538
+ if (!result || typeof result !== "object") return result;
164539
+ if (Array.isArray(result)) {
164540
+ let changed = false;
164541
+ const mapped = result.map((item) => {
164542
+ if (item && typeof item === "object" && item.type === "text" && typeof item.text === "string") {
164543
+ const text3 = item.text;
164544
+ if (text3.includes('"contentWithoutFrontmatter"')) {
164545
+ try {
164546
+ const parsed = JSON.parse(text3);
164547
+ const deduped = dedupeFragmentContentFields(parsed);
164548
+ if (deduped !== parsed) {
164549
+ changed = true;
164550
+ return { ...item, text: JSON.stringify(deduped) };
164551
+ }
164552
+ } catch {
164553
+ }
164554
+ }
164555
+ }
164556
+ return item;
164557
+ });
164558
+ return changed ? mapped : result;
164559
+ }
164560
+ const r17 = result;
164561
+ if (typeof r17.content === "string" && typeof r17.contentWithoutFrontmatter === "string" && r17.contentWithoutFrontmatter.length > 1e3) {
164562
+ return { ...r17, contentWithoutFrontmatter: FRAGMENT_DUP_MARKER };
164563
+ }
164564
+ if (Array.isArray(r17.content)) {
164565
+ const deduped = dedupeFragmentContentFields(r17.content);
164566
+ if (deduped !== r17.content) return { ...r17, content: deduped };
164567
+ }
164568
+ return result;
164452
164569
  }
164453
164570
  function sanitizeImageToolResult(result) {
164454
164571
  if (!result || typeof result !== "object") {
@@ -164491,7 +164608,7 @@ function sanitizeImageToolResult(result) {
164491
164608
  return sanitized;
164492
164609
  }
164493
164610
  async function summarizeToolResultForHistory(toolName, result, _conversationContext) {
164494
- const sanitizedResult = sanitizeImageToolResult(result);
164611
+ const sanitizedResult = dedupeFragmentContentFields(sanitizeImageToolResult(result));
164495
164612
  const serialized = JSON.stringify(sanitizedResult);
164496
164613
  const originalSize = countTokens(JSON.stringify(result));
164497
164614
  const references = extractFragmentReferences(result);
@@ -164506,12 +164623,13 @@ async function summarizeToolResultForHistory(toolName, result, _conversationCont
164506
164623
  usedQwen: false
164507
164624
  };
164508
164625
  }
164509
- var MAX_TOOL_RESULT_CHARS;
164626
+ var MAX_TOOL_RESULT_CHARS, FRAGMENT_DUP_MARKER;
164510
164627
  var init_tool_result_summarizer = __esm({
164511
164628
  "src/core/utils/tool-result-summarizer.ts"() {
164512
164629
  "use strict";
164513
164630
  init_token_counter();
164514
164631
  MAX_TOOL_RESULT_CHARS = 5e4;
164632
+ FRAGMENT_DUP_MARKER = "[omitted \u2014 duplicate of `content` minus the YAML frontmatter; read `content`]";
164515
164633
  }
164516
164634
  });
164517
164635
 
@@ -248154,6 +248272,65 @@ function pruneOversizedToolResultBodies(messages4, maxCharsPerResult) {
248154
248272
  });
248155
248273
  return { messages: pruned, prunedCount, bytesReclaimed };
248156
248274
  }
248275
+ async function buildCollectionContextParts(opts) {
248276
+ const { usableApiUrl, accessToken, contextId, workspaceId } = opts;
248277
+ const parts = [];
248278
+ const fallback = () => {
248279
+ parts.length = 0;
248280
+ parts.push(`<collection id="${contextId}" name="${opts.metadataName || "Unnamed"}">`);
248281
+ if (opts.metadataSummary) parts.push(`<description>${opts.metadataSummary}</description>`);
248282
+ parts.push("</collection>");
248283
+ return parts;
248284
+ };
248285
+ if (!workspaceId) return fallback();
248286
+ const headers = {
248287
+ Authorization: `Bearer ${accessToken}`,
248288
+ "Content-Type": "application/json"
248289
+ };
248290
+ const collectionResponse = await fetch(
248291
+ `${usableApiUrl}/workspaces/${workspaceId}/collections/${contextId}`,
248292
+ { headers }
248293
+ );
248294
+ if (!collectionResponse.ok) return fallback();
248295
+ const collectionData = await collectionResponse.json();
248296
+ const collection = collectionData.collection ?? collectionData;
248297
+ let fragments = [];
248298
+ let totalCount = 0;
248299
+ try {
248300
+ const fragmentsResponse = await fetch(
248301
+ `${usableApiUrl}/workspaces/${workspaceId}/collections/${contextId}/fragments?limit=50`,
248302
+ { headers }
248303
+ );
248304
+ if (fragmentsResponse.ok) {
248305
+ const fragmentsData = await fragmentsResponse.json();
248306
+ fragments = fragmentsData.fragments || fragmentsData.data || [];
248307
+ totalCount = fragmentsData.total ?? fragments.length;
248308
+ }
248309
+ } catch {
248310
+ }
248311
+ parts.push(
248312
+ `<collection id="${contextId}" name="${collection.name || opts.metadataName || "Unnamed"}" workspace-id="${collection.workspaceId || workspaceId}">`
248313
+ );
248314
+ if (collection.description) parts.push(`<description>${collection.description}</description>`);
248315
+ parts.push(`<fragments count="${totalCount}">`);
248316
+ for (const frag of fragments.slice(0, 50)) {
248317
+ const fragId = frag.id || frag.fragmentId;
248318
+ const fragTitle = frag.title || "Untitled";
248319
+ const fragSummary = frag.summary || "";
248320
+ parts.push(`<fragment-ref id="${fragId}" title="${fragTitle}" summary="${fragSummary}" />`);
248321
+ }
248322
+ parts.push("</fragments>");
248323
+ if (totalCount > 50) {
248324
+ parts.push(
248325
+ `<note>Collection has ${totalCount} fragments total. Only showing first 50. Use list-memory-fragments with collectionId to see more.</note>`
248326
+ );
248327
+ }
248328
+ parts.push(
248329
+ "<instructions>Use get-memory-fragment-content to read full content of specific fragments. Don't add all fragments to context.</instructions>"
248330
+ );
248331
+ parts.push("</collection>");
248332
+ return parts;
248333
+ }
248157
248334
  function enforceContextLimits(messages4, systemMessage, contextTokens, toCountableMessage, modelId, toolDefinitionTokens = 0) {
248158
248335
  const model = getModelById(modelId);
248159
248336
  const contextLength = model?.capabilities.contextLength || 128e3;
@@ -249613,54 +249790,16 @@ Folder behavior:
249613
249790
  }
249614
249791
  contextParts.push("</tool-result>");
249615
249792
  } else if (item.contextType === "collection") {
249616
- const usableApiUrl = config4.mcp.url.replace("/mcp", "");
249617
- const collectionResponse = await fetch(
249618
- `${usableApiUrl}/collections/${item.contextId}?includeFragments=true&fragmentLimit=50`,
249619
- {
249620
- headers: {
249621
- Authorization: `Bearer ${context.session.user?.accessToken}`,
249622
- "Content-Type": "application/json"
249623
- }
249624
- }
249793
+ contextParts.push(
249794
+ ...await buildCollectionContextParts({
249795
+ usableApiUrl: config4.mcp.url.replace("/mcp", ""),
249796
+ accessToken: context.session.user?.accessToken,
249797
+ contextId: item.contextId,
249798
+ workspaceId: item.metadata?.workspaceId,
249799
+ metadataName: item.metadata?.name,
249800
+ metadataSummary: item.metadata?.summary
249801
+ })
249625
249802
  );
249626
- if (collectionResponse.ok) {
249627
- const collection = await collectionResponse.json();
249628
- const fragments = collection.fragments || collection.items || [];
249629
- const totalCount = collection.fragmentCount || collection.totalFragments || fragments.length;
249630
- contextParts.push(
249631
- `<collection id="${item.contextId}" name="${collection.name || item.metadata?.name || "Unnamed"}" workspace-id="${collection.workspaceId || item.metadata?.workspaceId || ""}">`
249632
- );
249633
- if (collection.description) {
249634
- contextParts.push(`<description>${collection.description}</description>`);
249635
- }
249636
- contextParts.push(`<fragments count="${totalCount}">`);
249637
- for (const frag of fragments.slice(0, 50)) {
249638
- const fragId = frag.id || frag.fragmentId;
249639
- const fragTitle = frag.title || "Untitled";
249640
- const fragSummary = frag.summary || "";
249641
- contextParts.push(
249642
- `<fragment-ref id="${fragId}" title="${fragTitle}" summary="${fragSummary}" />`
249643
- );
249644
- }
249645
- contextParts.push("</fragments>");
249646
- if (totalCount > 50) {
249647
- contextParts.push(
249648
- `<note>Collection has ${totalCount} fragments total. Only showing first 50. Use list-memory-fragments with collectionId to see more.</note>`
249649
- );
249650
- }
249651
- contextParts.push(
249652
- "<instructions>Use get-memory-fragment-content to read full content of specific fragments. Don't add all fragments to context.</instructions>"
249653
- );
249654
- contextParts.push("</collection>");
249655
- } else {
249656
- contextParts.push(
249657
- `<collection id="${item.contextId}" name="${item.metadata?.name || "Unnamed"}">`
249658
- );
249659
- if (item.metadata?.summary) {
249660
- contextParts.push(`<description>${item.metadata.summary}</description>`);
249661
- }
249662
- contextParts.push("</collection>");
249663
- }
249664
249803
  }
249665
249804
  } catch (error41) {
249666
249805
  orchestrationLogger.warn("Failed to fetch context item content", {
@@ -249942,54 +250081,16 @@ Folder behavior:
249942
250081
  }
249943
250082
  contextParts.push("</tool-result>");
249944
250083
  } else if (item.contextType === "collection") {
249945
- const usableApiUrl = config4.mcp.url.replace("/mcp", "");
249946
- const collectionResponse = await fetch(
249947
- `${usableApiUrl}/collections/${item.contextId}?includeFragments=true&fragmentLimit=50`,
249948
- {
249949
- headers: {
249950
- Authorization: `Bearer ${context.session.user?.accessToken}`,
249951
- "Content-Type": "application/json"
249952
- }
249953
- }
250084
+ contextParts.push(
250085
+ ...await buildCollectionContextParts({
250086
+ usableApiUrl: config4.mcp.url.replace("/mcp", ""),
250087
+ accessToken: context.session.user?.accessToken,
250088
+ contextId: item.contextId,
250089
+ workspaceId: item.metadata?.workspaceId,
250090
+ metadataName: item.metadata?.name,
250091
+ metadataSummary: item.metadata?.summary
250092
+ })
249954
250093
  );
249955
- if (collectionResponse.ok) {
249956
- const collection = await collectionResponse.json();
249957
- const fragments = collection.fragments || collection.items || [];
249958
- const totalCount = collection.fragmentCount || collection.totalFragments || fragments.length;
249959
- contextParts.push(
249960
- `<collection id="${item.contextId}" name="${collection.name || item.metadata?.name || "Unnamed"}" workspace-id="${collection.workspaceId || item.metadata?.workspaceId || ""}">`
249961
- );
249962
- if (collection.description) {
249963
- contextParts.push(`<description>${collection.description}</description>`);
249964
- }
249965
- contextParts.push(`<fragments count="${totalCount}">`);
249966
- for (const frag of fragments.slice(0, 50)) {
249967
- const fragId = frag.id || frag.fragmentId;
249968
- const fragTitle = frag.title || "Untitled";
249969
- const fragSummary = frag.summary || "";
249970
- contextParts.push(
249971
- `<fragment-ref id="${fragId}" title="${fragTitle}" summary="${fragSummary}" />`
249972
- );
249973
- }
249974
- contextParts.push("</fragments>");
249975
- if (totalCount > 50) {
249976
- contextParts.push(
249977
- `<note>Collection has ${totalCount} fragments total. Only showing first 50. Use list-memory-fragments with collectionId to see more.</note>`
249978
- );
249979
- }
249980
- contextParts.push(
249981
- "<instructions>Use get-memory-fragment-content to read full content of specific fragments. Don't add all fragments to context.</instructions>"
249982
- );
249983
- contextParts.push("</collection>");
249984
- } else {
249985
- contextParts.push(
249986
- `<collection id="${item.contextId}" name="${item.metadata?.name || "Unnamed"}">`
249987
- );
249988
- if (item.metadata?.summary) {
249989
- contextParts.push(`<description>${item.metadata.summary}</description>`);
249990
- }
249991
- contextParts.push("</collection>");
249992
- }
249993
250094
  }
249994
250095
  } catch (error41) {
249995
250096
  orchestrationLogger.warn("Failed to fetch context item content", {
@@ -251287,44 +251388,101 @@ ${combinedSystemMessage}` : combinedSystemMessage;
251287
251388
  const ctxLen = getModelById(selectedModelId)?.capabilities.contextLength || 128e3;
251288
251389
  const envRatio = Number(process.env.USABLE_HARNESS_COMPACT_RATIO);
251289
251390
  const compactRatio = envRatio > 0 && envRatio <= 1 ? envRatio : HARNESS_COMPACT_RATIO;
251290
- const tokensOf = (m33) => countTokens(typeof m33.content === "string" ? m33.content : JSON.stringify(m33.content));
251291
- const estTokens = conversationMessages.reduce((s17, m33) => s17 + tokensOf(m33), 0);
251391
+ const tokensOf = (m33) => countMessageTokens(toCountableMessage(m33));
251392
+ const compactToolDefTokens = estimateToolDefinitionTokens(rawTools);
251393
+ const systemTokens = systemMessage ? countTokens(systemMessage) : 0;
251394
+ const baseTokens = systemTokens + contextTokens + compactToolDefTokens;
251395
+ const messageTokens = conversationMessages.reduce((s17, m33) => s17 + tokensOf(m33), 0);
251396
+ const estTokens = baseTokens + messageTokens;
251397
+ const threshold = Math.floor(ctxLen * compactRatio);
251292
251398
  const alreadyCompacted = isCompactionCheckpoint(conversationMessages[0]);
251293
- if (!alreadyCompacted && estTokens > ctxLen * compactRatio) {
251399
+ orchestrationLogger.debug("\u{1F9F9} compaction check", {
251400
+ estTokens,
251401
+ baseTokens,
251402
+ systemTokens,
251403
+ contextTokens,
251404
+ toolDefTokens: compactToolDefTokens,
251405
+ messageTokens,
251406
+ threshold,
251407
+ ctxLen,
251408
+ compactRatio,
251409
+ messages: conversationMessages.length,
251410
+ alreadyCompacted,
251411
+ path: config3.localFilesystem ? "cli" : "web"
251412
+ });
251413
+ if (!alreadyCompacted && estTokens > threshold) {
251294
251414
  const beforeLen = conversationMessages.length;
251295
251415
  const envKeep = Number(process.env.USABLE_HARNESS_KEEP_TOKENS);
251296
- const keepRecentTokens = envKeep > 0 ? envKeep : Math.floor(ctxLen * HARNESS_KEEP_RECENT_RATIO);
251416
+ const desiredKeep = envKeep > 0 ? envKeep : Math.floor(ctxLen * HARNESS_KEEP_RECENT_RATIO);
251417
+ const roomForMessages = Math.floor(ctxLen * 0.85) - baseTokens;
251418
+ const keepRecentTokens = Math.max(2e3, Math.min(desiredKeep, roomForMessages));
251297
251419
  const cut = findCutIndex(conversationMessages, keepRecentTokens, tokensOf);
251298
251420
  if (cut >= 3) {
251421
+ const compactionStartEvent = emitter.emit("compaction-needed", {
251422
+ contextUsagePercent: Math.round(estTokens / ctxLen * 100),
251423
+ inputTokens: estTokens,
251424
+ contextLength: ctxLen,
251425
+ model: selectedModelId,
251426
+ serverCompacted: true
251427
+ });
251428
+ multiplexer.send(compactionStartEvent);
251429
+ const compactingPlan = emitter.emit("plan", {
251430
+ plan: "\u{1F9F9} Summarizing older conversation to free context\u2026",
251431
+ steps: config3.maxSteps
251432
+ });
251433
+ multiplexer.send(compactingPlan);
251299
251434
  const dropped = conversationMessages.slice(0, cut);
251300
251435
  const tail2 = conversationMessages.slice(cut);
251301
251436
  const fileOps = extractFileOps(dropped);
251302
- const summary = await generateCompactionSummary(dropped, {
251303
- provider: providerRouting?.provider,
251304
- model: selectedModelId,
251305
- openRouterApiKey: apiKey,
251306
- anthropicApiKey,
251307
- baseUrl: request.providerBaseUrl,
251308
- authToken: request.providerAuthToken,
251309
- codexAuth: request.codexAuth,
251310
- abortSignal: context.abortSignal
251311
- });
251312
- const checkpoint = summary ? buildSummaryCheckpoint(summary, dropped.length, fileOps) : buildCompactionMarker(goalText(conversationMessages), dropped.length, fileOps);
251437
+ const compactionConversationId = context.metadata?.conversationId;
251438
+ const { summary, cacheHit } = await generateCompactionSummaryCached(
251439
+ compactionConversationId,
251440
+ dropped,
251441
+ {
251442
+ provider: providerRouting?.provider,
251443
+ model: selectedModelId,
251444
+ openRouterApiKey: apiKey,
251445
+ anthropicApiKey,
251446
+ baseUrl: request.providerBaseUrl,
251447
+ authToken: request.providerAuthToken,
251448
+ codexAuth: request.codexAuth,
251449
+ abortSignal: context.abortSignal
251450
+ }
251451
+ );
251452
+ let checkpoint = summary ? buildSummaryCheckpoint(summary, dropped.length, fileOps) : buildCompactionMarker(goalText(conversationMessages), dropped.length, fileOps);
251453
+ const spillPaths = extractSpillPointers(dropped);
251454
+ if (spillPaths.length > 0) {
251455
+ checkpoint = {
251456
+ role: "user",
251457
+ content: `${checkpoint.content}
251458
+
251459
+ Large tool results from the compacted work are PRESERVED at:
251460
+ ` + spillPaths.map((p28) => `- ${p28}`).join("\n") + `
251461
+ Re-read them (working_memory on web, bash on the CLI \u2014 grep/sed projection, not a full cat) instead of re-calling the tools that produced them.`
251462
+ };
251463
+ }
251313
251464
  conversationMessages = ensureToolCallIntegrity([checkpoint, ...tail2]);
251314
251465
  orchestrationLogger.warn("\u{1F9F9} Proactive conversation compaction", {
251315
251466
  beforeLen,
251316
251467
  afterLen: conversationMessages.length,
251317
251468
  droppedCount: dropped.length,
251318
251469
  estTokens,
251470
+ baseTokens,
251471
+ messageTokens,
251472
+ keepRecentTokens,
251473
+ threshold,
251319
251474
  contextLength: ctxLen,
251320
251475
  usedLlmSummary: !!summary,
251476
+ cacheHit,
251321
251477
  path: config3.localFilesystem ? "cli" : "web"
251322
251478
  });
251323
- const compactPlan = emitter.emit("plan", {
251324
- plan: `\u{1F9F9} Compacted ${dropped.length} older messages into a summary to free context \u2014 continuing.`,
251325
- steps: config3.maxSteps
251479
+ const compactionDoneEvent = emitter.emit("compaction", {
251480
+ summary: summary ?? (typeof checkpoint.content === "string" ? checkpoint.content : ""),
251481
+ droppedCount: dropped.length,
251482
+ usedLlmSummary: !!summary,
251483
+ cacheHit
251326
251484
  });
251327
- multiplexer.send(compactPlan);
251485
+ multiplexer.send(compactionDoneEvent);
251328
251486
  if (request.onContextCompacted) {
251329
251487
  try {
251330
251488
  await request.onContextCompacted(conversationMessages);
@@ -251911,7 +252069,7 @@ ${combinedSystemMessage}` : combinedSystemMessage;
251911
252069
  await bash.exec(`echo "${b64}" | base64 -d > ${filename}`);
251912
252070
  summary.content = `${summary.content}
251913
252071
 
251914
- [\u26A0\uFE0F FULL RESULT TOO LARGE \u2014 stored at: ${filename}. Use working_memory tool with "cat ${filename}" to read the complete content on demand.]`;
252072
+ [\u26A0\uFE0F FULL RESULT TOO LARGE \u2014 full data stored at ${filename}. Read it with the working_memory tool, but do NOT \`cat\` the whole file (it re-truncates). Project just the part you need, e.g. \`grep -n PATTERN ${filename}\` or \`sed -n '100,200p' ${filename}\` \u2014 do NOT re-call the original tool to recover the omitted part.]`;
251915
252073
  } catch {
251916
252074
  }
251917
252075
  }
@@ -269922,6 +270080,15 @@ Edit it, then /verify-extension ${arg.trim()} to check it loads, /trust (project
269922
270080
  push({ kind: "system", text: plan, tone: "info" });
269923
270081
  ui2.requestRender();
269924
270082
  }
270083
+ } else if (e14.type === "compaction") {
270084
+ const data2 = e14.data;
270085
+ finalize();
270086
+ const head = `\u{1F9F9} Compacted ${data2?.droppedCount ?? 0} older messages into a checkpoint`;
270087
+ const body = data2?.summary ? `${head}:
270088
+
270089
+ ${data2.summary}` : `${head}.`;
270090
+ push({ kind: "system", text: body, tone: "info" });
270091
+ ui2.requestRender();
269925
270092
  }
269926
270093
  };
269927
270094
  try {
@@ -270781,7 +270948,7 @@ init_tui_select();
270781
270948
  init_model_registry();
270782
270949
 
270783
270950
  // package.json
270784
- var version2 = "1.152.0";
270951
+ var version2 = "1.152.1";
270785
270952
 
270786
270953
  // src/adapters/cli/model-catalog.ts
270787
270954
  init_codex_auth();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usabledev/usable-chat",
3
- "version": "1.152.0",
3
+ "version": "1.152.1",
4
4
  "description": "usable-chat — terminal harness for usable-chat (headless + TUI)",
5
5
  "type": "module",
6
6
  "bin": {