@usabledev/usable-chat 1.152.0 → 1.153.0

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 +330 -128
  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
 
@@ -175858,6 +175976,33 @@ data: ${JSON.stringify(envelope)}
175858
175976
  });
175859
175977
 
175860
175978
  // src/core/tools/spawn-subagent.ts
175979
+ async function maybeStartBackgroundRelay(redis, context, conversationId, taskId, success2) {
175980
+ if (!redis || !context.startBackgroundSubagentRelay) return false;
175981
+ try {
175982
+ const record2 = await spawnedTaskStore.get(redis, taskId);
175983
+ if (record2?.notifiedTurnAt) {
175984
+ return true;
175985
+ }
175986
+ const lockKey = `subagents:background-relay-lock:${conversationId}`;
175987
+ const acquired = await redis.set(lockKey, taskId, "NX", "EX", 180).catch(() => null);
175988
+ if (!acquired) {
175989
+ return true;
175990
+ }
175991
+ const started = await context.startBackgroundSubagentRelay({
175992
+ conversationId,
175993
+ taskId,
175994
+ reason: success2 ? "completed" : "failed"
175995
+ });
175996
+ return started;
175997
+ } catch (err) {
175998
+ logger.debug("SpawnSubagent", "Failed to start background relay job", {
175999
+ conversationId,
176000
+ taskId,
176001
+ error: err instanceof Error ? err.message : String(err)
176002
+ });
176003
+ return false;
176004
+ }
176005
+ }
175861
176006
  async function maybeEmitPingPrompt(redis, conversationId, taskId) {
175862
176007
  if (!redis) return;
175863
176008
  try {
@@ -176158,7 +176303,10 @@ HOW TO USE THIS:
176158
176303
  tokenUsage: response.tokenUsage
176159
176304
  }
176160
176305
  });
176161
- await maybeEmitPingPrompt(redis, conversationId, taskId);
176306
+ const relayed = await maybeStartBackgroundRelay(redis, context, conversationId, taskId, success2);
176307
+ if (!relayed) {
176308
+ await maybeEmitPingPrompt(redis, conversationId, taskId);
176309
+ }
176162
176310
  } catch (err) {
176163
176311
  const errorMessage = err instanceof Error ? err.message : String(err);
176164
176312
  const status = abort.signal.aborted ? "cancelled" : "failed";
@@ -176173,7 +176321,10 @@ HOW TO USE THIS:
176173
176321
  taskId,
176174
176322
  data: { error: errorMessage }
176175
176323
  });
176176
- await maybeEmitPingPrompt(redis, conversationId, taskId);
176324
+ const relayed = await maybeStartBackgroundRelay(redis, context, conversationId, taskId, false);
176325
+ if (!relayed) {
176326
+ await maybeEmitPingPrompt(redis, conversationId, taskId);
176327
+ }
176177
176328
  } finally {
176178
176329
  spawnedTaskStore.unregisterLocalAbort(taskId);
176179
176330
  }
@@ -248154,6 +248305,65 @@ function pruneOversizedToolResultBodies(messages4, maxCharsPerResult) {
248154
248305
  });
248155
248306
  return { messages: pruned, prunedCount, bytesReclaimed };
248156
248307
  }
248308
+ async function buildCollectionContextParts(opts) {
248309
+ const { usableApiUrl, accessToken, contextId, workspaceId } = opts;
248310
+ const parts = [];
248311
+ const fallback = () => {
248312
+ parts.length = 0;
248313
+ parts.push(`<collection id="${contextId}" name="${opts.metadataName || "Unnamed"}">`);
248314
+ if (opts.metadataSummary) parts.push(`<description>${opts.metadataSummary}</description>`);
248315
+ parts.push("</collection>");
248316
+ return parts;
248317
+ };
248318
+ if (!workspaceId) return fallback();
248319
+ const headers = {
248320
+ Authorization: `Bearer ${accessToken}`,
248321
+ "Content-Type": "application/json"
248322
+ };
248323
+ const collectionResponse = await fetch(
248324
+ `${usableApiUrl}/workspaces/${workspaceId}/collections/${contextId}`,
248325
+ { headers }
248326
+ );
248327
+ if (!collectionResponse.ok) return fallback();
248328
+ const collectionData = await collectionResponse.json();
248329
+ const collection = collectionData.collection ?? collectionData;
248330
+ let fragments = [];
248331
+ let totalCount = 0;
248332
+ try {
248333
+ const fragmentsResponse = await fetch(
248334
+ `${usableApiUrl}/workspaces/${workspaceId}/collections/${contextId}/fragments?limit=50`,
248335
+ { headers }
248336
+ );
248337
+ if (fragmentsResponse.ok) {
248338
+ const fragmentsData = await fragmentsResponse.json();
248339
+ fragments = fragmentsData.fragments || fragmentsData.data || [];
248340
+ totalCount = fragmentsData.total ?? fragments.length;
248341
+ }
248342
+ } catch {
248343
+ }
248344
+ parts.push(
248345
+ `<collection id="${contextId}" name="${collection.name || opts.metadataName || "Unnamed"}" workspace-id="${collection.workspaceId || workspaceId}">`
248346
+ );
248347
+ if (collection.description) parts.push(`<description>${collection.description}</description>`);
248348
+ parts.push(`<fragments count="${totalCount}">`);
248349
+ for (const frag of fragments.slice(0, 50)) {
248350
+ const fragId = frag.id || frag.fragmentId;
248351
+ const fragTitle = frag.title || "Untitled";
248352
+ const fragSummary = frag.summary || "";
248353
+ parts.push(`<fragment-ref id="${fragId}" title="${fragTitle}" summary="${fragSummary}" />`);
248354
+ }
248355
+ parts.push("</fragments>");
248356
+ if (totalCount > 50) {
248357
+ parts.push(
248358
+ `<note>Collection has ${totalCount} fragments total. Only showing first 50. Use list-memory-fragments with collectionId to see more.</note>`
248359
+ );
248360
+ }
248361
+ parts.push(
248362
+ "<instructions>Use get-memory-fragment-content to read full content of specific fragments. Don't add all fragments to context.</instructions>"
248363
+ );
248364
+ parts.push("</collection>");
248365
+ return parts;
248366
+ }
248157
248367
  function enforceContextLimits(messages4, systemMessage, contextTokens, toCountableMessage, modelId, toolDefinitionTokens = 0) {
248158
248368
  const model = getModelById(modelId);
248159
248369
  const contextLength = model?.capabilities.contextLength || 128e3;
@@ -249145,7 +249355,8 @@ async function orchestrate(request) {
249145
249355
  chatMode,
249146
249356
  imageGenModel: context.metadata?.imageGenModel,
249147
249357
  imageGenThinking: context.metadata?.imageGenThinking,
249148
- registeredParentToolSchemas: context.registeredParentToolSchemas
249358
+ registeredParentToolSchemas: context.registeredParentToolSchemas,
249359
+ startBackgroundSubagentRelay: context.startBackgroundSubagentRelay
249149
249360
  });
249150
249361
  if (!config3.localFilesystem) {
249151
249362
  aiTools["spawn_subagent"] = {
@@ -249613,54 +249824,16 @@ Folder behavior:
249613
249824
  }
249614
249825
  contextParts.push("</tool-result>");
249615
249826
  } 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
- }
249827
+ contextParts.push(
249828
+ ...await buildCollectionContextParts({
249829
+ usableApiUrl: config4.mcp.url.replace("/mcp", ""),
249830
+ accessToken: context.session.user?.accessToken,
249831
+ contextId: item.contextId,
249832
+ workspaceId: item.metadata?.workspaceId,
249833
+ metadataName: item.metadata?.name,
249834
+ metadataSummary: item.metadata?.summary
249835
+ })
249625
249836
  );
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
249837
  }
249665
249838
  } catch (error41) {
249666
249839
  orchestrationLogger.warn("Failed to fetch context item content", {
@@ -249942,54 +250115,16 @@ Folder behavior:
249942
250115
  }
249943
250116
  contextParts.push("</tool-result>");
249944
250117
  } 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
- }
250118
+ contextParts.push(
250119
+ ...await buildCollectionContextParts({
250120
+ usableApiUrl: config4.mcp.url.replace("/mcp", ""),
250121
+ accessToken: context.session.user?.accessToken,
250122
+ contextId: item.contextId,
250123
+ workspaceId: item.metadata?.workspaceId,
250124
+ metadataName: item.metadata?.name,
250125
+ metadataSummary: item.metadata?.summary
250126
+ })
249954
250127
  );
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
250128
  }
249994
250129
  } catch (error41) {
249995
250130
  orchestrationLogger.warn("Failed to fetch context item content", {
@@ -251287,44 +251422,101 @@ ${combinedSystemMessage}` : combinedSystemMessage;
251287
251422
  const ctxLen = getModelById(selectedModelId)?.capabilities.contextLength || 128e3;
251288
251423
  const envRatio = Number(process.env.USABLE_HARNESS_COMPACT_RATIO);
251289
251424
  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);
251425
+ const tokensOf = (m33) => countMessageTokens(toCountableMessage(m33));
251426
+ const compactToolDefTokens = estimateToolDefinitionTokens(rawTools);
251427
+ const systemTokens = systemMessage ? countTokens(systemMessage) : 0;
251428
+ const baseTokens = systemTokens + contextTokens + compactToolDefTokens;
251429
+ const messageTokens = conversationMessages.reduce((s17, m33) => s17 + tokensOf(m33), 0);
251430
+ const estTokens = baseTokens + messageTokens;
251431
+ const threshold = Math.floor(ctxLen * compactRatio);
251292
251432
  const alreadyCompacted = isCompactionCheckpoint(conversationMessages[0]);
251293
- if (!alreadyCompacted && estTokens > ctxLen * compactRatio) {
251433
+ orchestrationLogger.debug("\u{1F9F9} compaction check", {
251434
+ estTokens,
251435
+ baseTokens,
251436
+ systemTokens,
251437
+ contextTokens,
251438
+ toolDefTokens: compactToolDefTokens,
251439
+ messageTokens,
251440
+ threshold,
251441
+ ctxLen,
251442
+ compactRatio,
251443
+ messages: conversationMessages.length,
251444
+ alreadyCompacted,
251445
+ path: config3.localFilesystem ? "cli" : "web"
251446
+ });
251447
+ if (!alreadyCompacted && estTokens > threshold) {
251294
251448
  const beforeLen = conversationMessages.length;
251295
251449
  const envKeep = Number(process.env.USABLE_HARNESS_KEEP_TOKENS);
251296
- const keepRecentTokens = envKeep > 0 ? envKeep : Math.floor(ctxLen * HARNESS_KEEP_RECENT_RATIO);
251450
+ const desiredKeep = envKeep > 0 ? envKeep : Math.floor(ctxLen * HARNESS_KEEP_RECENT_RATIO);
251451
+ const roomForMessages = Math.floor(ctxLen * 0.85) - baseTokens;
251452
+ const keepRecentTokens = Math.max(2e3, Math.min(desiredKeep, roomForMessages));
251297
251453
  const cut = findCutIndex(conversationMessages, keepRecentTokens, tokensOf);
251298
251454
  if (cut >= 3) {
251455
+ const compactionStartEvent = emitter.emit("compaction-needed", {
251456
+ contextUsagePercent: Math.round(estTokens / ctxLen * 100),
251457
+ inputTokens: estTokens,
251458
+ contextLength: ctxLen,
251459
+ model: selectedModelId,
251460
+ serverCompacted: true
251461
+ });
251462
+ multiplexer.send(compactionStartEvent);
251463
+ const compactingPlan = emitter.emit("plan", {
251464
+ plan: "\u{1F9F9} Summarizing older conversation to free context\u2026",
251465
+ steps: config3.maxSteps
251466
+ });
251467
+ multiplexer.send(compactingPlan);
251299
251468
  const dropped = conversationMessages.slice(0, cut);
251300
251469
  const tail2 = conversationMessages.slice(cut);
251301
251470
  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);
251471
+ const compactionConversationId = context.metadata?.conversationId;
251472
+ const { summary, cacheHit } = await generateCompactionSummaryCached(
251473
+ compactionConversationId,
251474
+ dropped,
251475
+ {
251476
+ provider: providerRouting?.provider,
251477
+ model: selectedModelId,
251478
+ openRouterApiKey: apiKey,
251479
+ anthropicApiKey,
251480
+ baseUrl: request.providerBaseUrl,
251481
+ authToken: request.providerAuthToken,
251482
+ codexAuth: request.codexAuth,
251483
+ abortSignal: context.abortSignal
251484
+ }
251485
+ );
251486
+ let checkpoint = summary ? buildSummaryCheckpoint(summary, dropped.length, fileOps) : buildCompactionMarker(goalText(conversationMessages), dropped.length, fileOps);
251487
+ const spillPaths = extractSpillPointers(dropped);
251488
+ if (spillPaths.length > 0) {
251489
+ checkpoint = {
251490
+ role: "user",
251491
+ content: `${checkpoint.content}
251492
+
251493
+ Large tool results from the compacted work are PRESERVED at:
251494
+ ` + spillPaths.map((p28) => `- ${p28}`).join("\n") + `
251495
+ 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.`
251496
+ };
251497
+ }
251313
251498
  conversationMessages = ensureToolCallIntegrity([checkpoint, ...tail2]);
251314
251499
  orchestrationLogger.warn("\u{1F9F9} Proactive conversation compaction", {
251315
251500
  beforeLen,
251316
251501
  afterLen: conversationMessages.length,
251317
251502
  droppedCount: dropped.length,
251318
251503
  estTokens,
251504
+ baseTokens,
251505
+ messageTokens,
251506
+ keepRecentTokens,
251507
+ threshold,
251319
251508
  contextLength: ctxLen,
251320
251509
  usedLlmSummary: !!summary,
251510
+ cacheHit,
251321
251511
  path: config3.localFilesystem ? "cli" : "web"
251322
251512
  });
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
251513
+ const compactionDoneEvent = emitter.emit("compaction", {
251514
+ summary: summary ?? (typeof checkpoint.content === "string" ? checkpoint.content : ""),
251515
+ droppedCount: dropped.length,
251516
+ usedLlmSummary: !!summary,
251517
+ cacheHit
251326
251518
  });
251327
- multiplexer.send(compactPlan);
251519
+ multiplexer.send(compactionDoneEvent);
251328
251520
  if (request.onContextCompacted) {
251329
251521
  try {
251330
251522
  await request.onContextCompacted(conversationMessages);
@@ -251911,7 +252103,7 @@ ${combinedSystemMessage}` : combinedSystemMessage;
251911
252103
  await bash.exec(`echo "${b64}" | base64 -d > ${filename}`);
251912
252104
  summary.content = `${summary.content}
251913
252105
 
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.]`;
252106
+ [\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
252107
  } catch {
251916
252108
  }
251917
252109
  }
@@ -252242,6 +252434,7 @@ function createOrchestratorRequest(messages4, context, config3, persona, apiKey,
252242
252434
  metadata: context.metadata,
252243
252435
  allowedWorkspaceIds: context.allowedWorkspaceIds,
252244
252436
  registeredParentToolSchemas: context.registeredParentToolSchemas,
252437
+ startBackgroundSubagentRelay: context.startBackgroundSubagentRelay,
252245
252438
  extensions: context.extensions
252246
252439
  // Plugin hook seam (CLI only; undefined on web)
252247
252440
  },
@@ -269922,6 +270115,15 @@ Edit it, then /verify-extension ${arg.trim()} to check it loads, /trust (project
269922
270115
  push({ kind: "system", text: plan, tone: "info" });
269923
270116
  ui2.requestRender();
269924
270117
  }
270118
+ } else if (e14.type === "compaction") {
270119
+ const data2 = e14.data;
270120
+ finalize();
270121
+ const head = `\u{1F9F9} Compacted ${data2?.droppedCount ?? 0} older messages into a checkpoint`;
270122
+ const body = data2?.summary ? `${head}:
270123
+
270124
+ ${data2.summary}` : `${head}.`;
270125
+ push({ kind: "system", text: body, tone: "info" });
270126
+ ui2.requestRender();
269925
270127
  }
269926
270128
  };
269927
270129
  try {
@@ -270781,7 +270983,7 @@ init_tui_select();
270781
270983
  init_model_registry();
270782
270984
 
270783
270985
  // package.json
270784
- var version2 = "1.152.0";
270986
+ var version2 = "1.153.0";
270785
270987
 
270786
270988
  // src/adapters/cli/model-catalog.ts
270787
270989
  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.153.0",
4
4
  "description": "usable-chat — terminal harness for usable-chat (headless + TUI)",
5
5
  "type": "module",
6
6
  "bin": {