@opengeni/runtime 0.6.1 → 0.7.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 (45) hide show
  1. package/dist/{chunk-DYBF7ZEB.js → chunk-RYDCVTMW.js} +1958 -207
  2. package/dist/chunk-RYDCVTMW.js.map +1 -0
  3. package/dist/{index-BTo_bZvN.d.ts → index-DIiiwQY2.d.ts} +689 -52
  4. package/dist/index.d.ts +293 -123
  5. package/dist/index.js +1819 -790
  6. package/dist/index.js.map +1 -1
  7. package/dist/sandbox/index.d.ts +1 -1
  8. package/dist/sandbox/index.js +79 -3
  9. package/package.json +21 -20
  10. package/src/codex-tool-search.ts +185 -31
  11. package/src/context-compaction.ts +78 -243
  12. package/src/history-sanitizer.ts +172 -20
  13. package/src/index.ts +2573 -898
  14. package/src/metrics.ts +28 -2
  15. package/src/sandbox/capabilities.ts +12 -3
  16. package/src/sandbox/channel-a.ts +420 -72
  17. package/src/sandbox/display-stack.ts +18 -4
  18. package/src/sandbox/errors.ts +13 -0
  19. package/src/sandbox/index.ts +398 -112
  20. package/src/sandbox/op-correlation.ts +70 -0
  21. package/src/sandbox/providers/index.ts +9 -3
  22. package/src/sandbox/providers/modal.ts +84 -14
  23. package/src/sandbox/providers/selfhosted.ts +1 -4
  24. package/src/sandbox/recording.ts +146 -90
  25. package/src/sandbox/routing/backend-resolver.ts +144 -10
  26. package/src/sandbox/routing/routing-session.ts +129 -25
  27. package/src/sandbox/select.ts +60 -8
  28. package/src/sandbox/selfhosted/capabilities.ts +59 -25
  29. package/src/sandbox/selfhosted/control-rpc.ts +160 -28
  30. package/src/sandbox/selfhosted/fault-rendering.ts +226 -0
  31. package/src/sandbox/selfhosted/op-observer.ts +54 -0
  32. package/src/sandbox/selfhosted/op-stream.ts +801 -0
  33. package/src/sandbox/selfhosted/op-testing.ts +468 -0
  34. package/src/sandbox/selfhosted/op-transport.ts +151 -0
  35. package/src/sandbox/selfhosted/retry-policy.ts +177 -0
  36. package/src/sandbox/selfhosted/session.ts +498 -55
  37. package/src/sandbox/selfhosted/testing.ts +60 -19
  38. package/src/sandbox/stream-port.ts +17 -5
  39. package/src/sandbox/stream-token.ts +8 -2
  40. package/src/sandbox/terminal-server.ts +18 -4
  41. package/src/sandbox-computer.ts +332 -70
  42. package/src/usage-telemetry.ts +73 -0
  43. package/dist/chunk-DYBF7ZEB.js.map +0 -1
  44. package/src/image-history.ts +0 -149
  45. package/src/screenshot-omitted-card.ts +0 -15
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Client-side conversation context compaction (the Azure/client path).
2
+ * Portable conversation context compaction, following Codex CLI's local path.
3
3
  *
4
- * This mirrors Codex CLI's compaction model: the checkpoint model sees the
5
- * current active history plus one fixed checkpoint prompt, then the active
6
- * history is rebuilt as all real user messages plus one summary message.
4
+ * The checkpoint model sees the current active history plus one fixed
5
+ * checkpoint prompt, then the active history is rebuilt from the newest real
6
+ * user messages within one cumulative 20k-token budget plus one summary.
7
7
  * Assistant messages, tool calls/results, reasoning, and images are removed
8
8
  * from the active model-facing history; the database audit rows remain.
9
9
  */
@@ -19,11 +19,15 @@ export type CompactionItem = Record<string, unknown>;
19
19
  export const COMPACTION_SUMMARY_MARKER = "opengeni_context_summary";
20
20
 
21
21
  export const SUMMARY_BUFFER_TOKENS = 20_000;
22
+ // A single cumulative budget for all retained real user messages, matching
23
+ // Codex core's build_compacted_history_with_limit (not a per-message allowance).
22
24
  export const COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000;
23
- export const DEFAULT_COMPACTION_THRESHOLD_RATIO = 0.6;
25
+ // 0.9: compact as LATE as possible — retained context is worth more than early
26
+ // headroom now that declared per-model windows are honest. Model-catalog
27
+ // explicit limits take precedence; the ratio is used for models without one.
28
+ export const DEFAULT_COMPACTION_THRESHOLD_RATIO = 0.9;
24
29
  export const MIN_COMPACTION_THRESHOLD_RATIO = 0.3;
25
30
  export const MAX_COMPACTION_THRESHOLD_RATIO = 0.9;
26
- export const COMPACTION_FALLBACK_TARGET_RATIO = 0.5;
27
31
 
28
32
  // Verbatim from Codex CLI:
29
33
  // codex-rs/prompts/templates/compact/prompt.md
@@ -74,14 +78,18 @@ export function isUserMessage(item: unknown): boolean {
74
78
  /** True for our synthetic compaction summary item. */
75
79
  export function isCompactionSummary(item: unknown): boolean {
76
80
  return (
77
- isUserMessage(item)
78
- && (item as Record<string, unknown>)[COMPACTION_SUMMARY_MARKER] === true
81
+ isUserMessage(item) && (item as Record<string, unknown>)[COMPACTION_SUMMARY_MARKER] === true
79
82
  );
80
83
  }
81
84
 
85
+ /** Platform-authored system context exists for one inference and is never persisted. */
86
+ export function isEphemeralInternalContext(item: unknown): boolean {
87
+ return itemType(item) === "message" && itemRole(item) === "system";
88
+ }
89
+
82
90
  /**
83
91
  * Rough token estimate for an item: char/4 over its serialized text. Used for
84
- * the pre-first-call fallback, per-user-message cap, and read-path airbag.
92
+ * the pre-first-call signal and the retained user-message budget.
85
93
  */
86
94
  export function estimateItemTokens(item: CompactionItem): number {
87
95
  let text: string;
@@ -102,36 +110,51 @@ export function estimateTokens(items: readonly CompactionItem[]): number {
102
110
  }
103
111
 
104
112
  export function clampCompactionThresholdRatio(value: number | undefined | null): number {
105
- const numeric = typeof value === "number" && Number.isFinite(value)
106
- ? value
107
- : DEFAULT_COMPACTION_THRESHOLD_RATIO;
108
- return Math.min(MAX_COMPACTION_THRESHOLD_RATIO, Math.max(MIN_COMPACTION_THRESHOLD_RATIO, numeric));
113
+ const numeric =
114
+ typeof value === "number" && Number.isFinite(value)
115
+ ? value
116
+ : DEFAULT_COMPACTION_THRESHOLD_RATIO;
117
+ return Math.min(
118
+ MAX_COMPACTION_THRESHOLD_RATIO,
119
+ Math.max(MIN_COMPACTION_THRESHOLD_RATIO, numeric),
120
+ );
109
121
  }
110
122
 
111
- export function clientCompactionThresholdTokens(input: {
123
+ export function compactionThresholdTokens(input: {
112
124
  contextWindowTokens: number;
113
125
  contextReservedOutputTokens: number;
114
- contextCompactionThresholdRatio?: number | null;
126
+ contextAutoCompactThresholdTokens?: number | null | undefined;
127
+ contextCompactionThresholdRatio?: number | null | undefined;
115
128
  }): number {
116
- return Math.floor(Math.max(0, input.contextWindowTokens) * clampCompactionThresholdRatio(input.contextCompactionThresholdRatio));
129
+ const window = Math.max(0, input.contextWindowTokens);
130
+ const codexMaximum = Math.floor(window * MAX_COMPACTION_THRESHOLD_RATIO);
131
+ if (
132
+ typeof input.contextAutoCompactThresholdTokens === "number" &&
133
+ Number.isFinite(input.contextAutoCompactThresholdTokens) &&
134
+ input.contextAutoCompactThresholdTokens > 0
135
+ ) {
136
+ return Math.min(Math.floor(input.contextAutoCompactThresholdTokens), codexMaximum);
137
+ }
138
+ return Math.floor(window * clampCompactionThresholdRatio(input.contextCompactionThresholdRatio));
117
139
  }
118
140
 
119
- export type ClientCompactionDecision = {
141
+ export type CompactionDecision = {
120
142
  shouldCompact: boolean;
121
143
  reason: "force" | "above_threshold" | "below_threshold" | "no_history";
122
144
  signalTokens: number;
123
145
  thresholdTokens: number;
124
146
  };
125
147
 
126
- export function decideClientCompaction(input: {
148
+ export function decideCompaction(input: {
127
149
  items: readonly CompactionItem[];
128
150
  lastInputTokens?: number | null;
129
151
  contextWindowTokens: number;
130
152
  contextReservedOutputTokens: number;
131
- contextCompactionThresholdRatio?: number | null;
153
+ contextAutoCompactThresholdTokens?: number | null | undefined;
154
+ contextCompactionThresholdRatio?: number | null | undefined;
132
155
  force?: boolean;
133
- }): ClientCompactionDecision {
134
- const thresholdTokens = clientCompactionThresholdTokens(input);
156
+ }): CompactionDecision {
157
+ const thresholdTokens = compactionThresholdTokens(input);
135
158
  const recorded =
136
159
  typeof input.lastInputTokens === "number" && input.lastInputTokens > 0
137
160
  ? input.lastInputTokens
@@ -143,7 +166,7 @@ export function decideClientCompaction(input: {
143
166
  if (input.force) {
144
167
  return { shouldCompact: true, reason: "force", signalTokens, thresholdTokens };
145
168
  }
146
- if (signalTokens > thresholdTokens) {
169
+ if (signalTokens >= thresholdTokens) {
147
170
  return { shouldCompact: true, reason: "above_threshold", signalTokens, thresholdTokens };
148
171
  }
149
172
  return { shouldCompact: false, reason: "below_threshold", signalTokens, thresholdTokens };
@@ -153,23 +176,32 @@ export class CompactionNeededError extends Error {
153
176
  readonly signalTokens: number;
154
177
  readonly thresholdTokens: number;
155
178
  readonly signalSource: "provider" | "estimate";
179
+ readonly trigger: "threshold" | "operator";
156
180
 
157
181
  constructor(input: {
158
182
  signalTokens: number;
159
183
  thresholdTokens: number;
160
184
  signalSource: "provider" | "estimate";
185
+ trigger?: "threshold" | "operator";
161
186
  }) {
187
+ const trigger = input.trigger ?? "threshold";
162
188
  super(
163
- `Context compaction needed: signal ${input.signalTokens} tokens exceeded threshold ${input.thresholdTokens}`,
189
+ trigger === "operator"
190
+ ? "Context compaction requested by the operator"
191
+ : `Context compaction needed: signal ${input.signalTokens} tokens exceeded threshold ${input.thresholdTokens}`,
164
192
  );
165
193
  this.name = "CompactionNeededError";
166
194
  this.signalTokens = input.signalTokens;
167
195
  this.thresholdTokens = input.thresholdTokens;
168
196
  this.signalSource = input.signalSource;
197
+ this.trigger = trigger;
169
198
  }
170
199
  }
171
200
 
172
- export function findCompactionNeededError(error: unknown, seen = new WeakSet<object>()): CompactionNeededError | null {
201
+ export function findCompactionNeededError(
202
+ error: unknown,
203
+ seen = new WeakSet<object>(),
204
+ ): CompactionNeededError | null {
173
205
  if (error instanceof CompactionNeededError) {
174
206
  return error;
175
207
  }
@@ -182,66 +214,10 @@ export function findCompactionNeededError(error: unknown, seen = new WeakSet<obj
182
214
  seen.add(error);
183
215
  const record = error as Record<string, unknown>;
184
216
  return (
185
- findCompactionNeededError(record.cause, seen)
186
- ?? findCompactionNeededError(record.error, seen)
217
+ findCompactionNeededError(record.cause, seen) ?? findCompactionNeededError(record.error, seen)
187
218
  );
188
219
  }
189
220
 
190
- /**
191
- * Walk backwards from the end of `items` keeping whole turns until the kept
192
- * tail would exceed `keepRecentTokens`, and return the index of the first kept
193
- * item. Retained for the read-path budget guard only; the client compaction
194
- * rebuild no longer uses a keep-recent tail.
195
- */
196
- export function findKeepBoundary(items: readonly CompactionItem[], keepRecentTokens: number): number {
197
- const boundaries: number[] = [];
198
- for (let i = 0; i < items.length; i += 1) {
199
- if (isUserMessage(items[i])) {
200
- boundaries.push(i);
201
- }
202
- }
203
- if (boundaries.length === 0) {
204
- return 0;
205
- }
206
- for (let b = 0; b < boundaries.length; b += 1) {
207
- const boundary = boundaries[b]!;
208
- if (estimateTokens(items.slice(boundary)) <= keepRecentTokens) {
209
- return boundary;
210
- }
211
- }
212
- return boundaries[boundaries.length - 1]!;
213
- }
214
-
215
- /**
216
- * READ-PATH BUDGET GUARD (last-resort backstop).
217
- *
218
- * Drops the oldest history at a clean user-message boundary until an assembled
219
- * input fits the request budget. This remains a request-local safety rail; it
220
- * is not the compaction strategy.
221
- */
222
- export function enforceInputBudget<T extends CompactionItem>(
223
- items: readonly T[],
224
- maxTokens: number,
225
- trailingTokens = 0,
226
- ): { items: T[]; trimmed: boolean; droppedCount: number; estimatedTokens: number } {
227
- const total = estimateTokens(items) + Math.max(0, trailingTokens);
228
- if (items.length === 0 || total <= maxTokens) {
229
- return { items: items.slice(), trimmed: false, droppedCount: 0, estimatedTokens: total };
230
- }
231
- const historyBudget = Math.max(0, maxTokens - Math.max(0, trailingTokens));
232
- const boundary = findKeepBoundary(items, historyBudget);
233
- if (boundary <= 0) {
234
- return { items: items.slice(), trimmed: false, droppedCount: 0, estimatedTokens: total };
235
- }
236
- const kept = items.slice(boundary);
237
- return {
238
- items: kept,
239
- trimmed: true,
240
- droppedCount: boundary,
241
- estimatedTokens: estimateTokens(kept) + Math.max(0, trailingTokens),
242
- };
243
- }
244
-
245
221
  /**
246
222
  * The exact checkpoint input shape: current active history followed by Codex's
247
223
  * checkpoint prompt as a synthesized user message.
@@ -259,86 +235,31 @@ export function buildCompactionPromptInput(items: readonly CompactionItem[]): Co
259
235
 
260
236
  /**
261
237
  * Build the active history after compaction:
262
- * all real user messages (prior summaries excluded, images removed, each
263
- * message capped) plus one marked summary item.
238
+ * the newest real user messages that fit one cumulative 20k-token budget
239
+ * (prior summaries excluded, images removed) plus one marked summary item.
264
240
  */
265
241
  export function buildCompactionReplacementHistory(
266
242
  items: readonly CompactionItem[],
267
243
  summaryBody: string,
268
244
  ): CompactionItem[] {
269
- const history: CompactionItem[] = [];
270
- for (const item of items) {
245
+ const retainedReversed: CompactionItem[] = [];
246
+ let remaining = COMPACT_USER_MESSAGE_MAX_TOKENS;
247
+ for (let index = items.length - 1; index >= 0 && remaining > 0; index -= 1) {
248
+ const item = items[index]!;
271
249
  if (!isUserMessage(item) || isCompactionSummary(item)) {
272
250
  continue;
273
251
  }
274
- history.push(compactUserMessage(item));
275
- }
276
- history.push(buildSummaryItem(summaryBody));
277
- return history;
278
- }
279
-
280
- export type DeterministicFallbackCompactionInput = {
281
- items: readonly CompactionItem[];
282
- cause?: string;
283
- targetTokens?: number;
284
- };
285
-
286
- export function buildDeterministicFallbackCompactionHistory(input: DeterministicFallbackCompactionInput): CompactionItem[] {
287
- const sourceTokens = Math.max(1, estimateTokens(input.items));
288
- const requestedTarget = typeof input.targetTokens === "number" && input.targetTokens > 0
289
- ? Math.floor(input.targetTokens)
290
- : Math.floor(sourceTokens * COMPACTION_FALLBACK_TARGET_RATIO);
291
- const targetTokens = Math.max(1, Math.min(requestedTarget, Math.max(1, sourceTokens - 1)));
292
- const summaryItem = buildSummaryItem([
293
- "Non-LLM context compaction fallback.",
294
- input.cause ? `Summarizer failure: ${input.cause}` : "Summarizer failure: unavailable.",
295
- "Older assistant, tool, reasoning, and image history was dropped deterministically.",
296
- "Retained context above is limited to initial instructions and the most recent user messages that fit the fallback budget; oversized retained messages may be middle-truncated.",
297
- ].join("\n"));
298
- const summaryTokens = estimateItemTokens(summaryItem);
299
- let remaining = Math.max(0, targetTokens - summaryTokens);
300
- const retainedInstructions: CompactionItem[] = [];
301
-
302
- for (const item of input.items) {
303
- if (!isInitialInstructionMessage(item)) {
304
- if (isUserMessage(item)) {
305
- break;
306
- }
307
- continue;
308
- }
309
- if (remaining <= 0) {
252
+ const textTokens = estimatedTextTokens(messageText(item));
253
+ retainedReversed.push(compactMessageToTokenBudget(item, remaining));
254
+ if (textTokens > remaining) {
255
+ remaining = 0;
310
256
  break;
311
257
  }
312
- const capped = compactMessageToEstimatedItemBudget(item, Math.min(2_000, remaining));
313
- if (!capped) {
314
- continue;
315
- }
316
- const tokens = estimateItemTokens(capped);
317
- if (tokens <= remaining) {
318
- retainedInstructions.push(capped);
319
- remaining -= tokens;
320
- }
258
+ remaining -= textTokens;
321
259
  }
322
-
323
- const retainedUsers: CompactionItem[] = [];
324
- for (let index = input.items.length - 1; index >= 0 && remaining > 0; index -= 1) {
325
- const item = input.items[index]!;
326
- if (!isUserMessage(item) || isCompactionSummary(item)) {
327
- continue;
328
- }
329
- const capped = compactMessageToEstimatedItemBudget(item, Math.min(COMPACT_USER_MESSAGE_MAX_TOKENS, remaining));
330
- if (!capped) {
331
- continue;
332
- }
333
- const tokens = estimateItemTokens(capped);
334
- if (tokens <= remaining) {
335
- retainedUsers.push(capped);
336
- remaining -= tokens;
337
- }
338
- }
339
- retainedUsers.reverse();
340
-
341
- return [...retainedInstructions, ...retainedUsers, summaryItem];
260
+ const history = retainedReversed.reverse();
261
+ history.push(buildSummaryItem(summaryBody));
262
+ return history;
342
263
  }
343
264
 
344
265
  /**
@@ -346,7 +267,7 @@ export function buildDeterministicFallbackCompactionHistory(input: Deterministic
346
267
  * rebuilt active history.
347
268
  */
348
269
  export function buildSummaryItem(summaryBody: string): CompactionItem {
349
- const trimmed = summaryBody.trim();
270
+ const trimmed = summaryBody.trim() || "(no summary available)";
350
271
  return {
351
272
  type: "message",
352
273
  role: "user",
@@ -355,10 +276,6 @@ export function buildSummaryItem(summaryBody: string): CompactionItem {
355
276
  };
356
277
  }
357
278
 
358
- function compactUserMessage(item: CompactionItem): CompactionItem {
359
- return compactMessageToTokenBudget(item, COMPACT_USER_MESSAGE_MAX_TOKENS);
360
- }
361
-
362
279
  function compactMessageToTokenBudget(item: CompactionItem, maxTokens: number): CompactionItem {
363
280
  const text = messageText(item);
364
281
  const next = { ...item };
@@ -370,27 +287,6 @@ function compactMessageToTokenBudget(item: CompactionItem, maxTokens: number): C
370
287
  return next;
371
288
  }
372
289
 
373
- function compactMessageToEstimatedItemBudget(item: CompactionItem, maxItemTokens: number): CompactionItem | null {
374
- if (maxItemTokens <= 0) {
375
- return null;
376
- }
377
- const overheadTokens = estimateItemTokens({ ...item, content: "" });
378
- let contentBudget = Math.max(0, maxItemTokens - overheadTokens);
379
- for (let attempt = 0; attempt < 6; attempt += 1) {
380
- const capped = compactMessageToTokenBudget(item, contentBudget);
381
- const estimated = estimateItemTokens(capped);
382
- if (estimated <= maxItemTokens) {
383
- return capped;
384
- }
385
- const overage = estimated - maxItemTokens;
386
- if (contentBudget <= 0) {
387
- break;
388
- }
389
- contentBudget = Math.max(0, contentBudget - Math.max(1, overage));
390
- }
391
- return null;
392
- }
393
-
394
290
  function estimatedTextTokens(text: string): number {
395
291
  return Math.ceil(text.length / 4);
396
292
  }
@@ -447,60 +343,26 @@ function messageText(item: CompactionItem): string {
447
343
  return "";
448
344
  }
449
345
 
450
- function isInitialInstructionMessage(item: unknown): boolean {
451
- const type = itemType(item);
452
- const role = itemRole(item);
453
- return type === "message" && (role === "system" || role === "developer");
454
- }
455
-
456
- export type RenderCompactionTranscriptOptions = {
457
- /**
458
- * Hard cap for a rendered transcript. When set, oldest rendered items are
459
- * dropped first while preserving the final checkpoint prompt; if the retained
460
- * item itself is too large, it is middle-truncated instead of looping.
461
- */
462
- maxEstimatedTokens?: number;
463
- };
464
-
465
- export function renderCompactionPromptInputForChat(
466
- input: readonly CompactionItem[],
467
- options: RenderCompactionTranscriptOptions = {},
468
- ): string {
469
- const rendered = input.map(renderItem);
470
- if (typeof options.maxEstimatedTokens !== "number" || options.maxEstimatedTokens <= 0) {
471
- return rendered.join("\n");
472
- }
473
- const maxChars = Math.max(1, Math.floor(options.maxEstimatedTokens * 4));
474
- const last = rendered.at(-1);
475
- const prefix = rendered.slice(0, -1);
476
- let kept = prefix.slice();
477
- while (kept.length > 0 && transcriptLength([...kept, last].filter((line): line is string => line !== undefined)) > maxChars) {
478
- kept.shift();
479
- }
480
- const lines = [...kept, last].filter((line): line is string => line !== undefined);
481
- const joined = lines.join("\n");
482
- if (joined.length <= maxChars) {
483
- return joined;
484
- }
485
- return truncateMiddleByChars(joined, maxChars);
346
+ export function renderCompactionPromptInputForChat(input: readonly CompactionItem[]): string {
347
+ return input.map(renderItem).join("\n");
486
348
  }
487
349
 
488
350
  function renderItem(item: CompactionItem): string {
489
351
  const type = itemType(item) ?? "unknown";
490
352
  if (type === "message") {
491
353
  const role = itemRole(item) ?? "assistant";
492
- return `[${role}] ${truncateForTranscript(messageText(item), 4000)}`;
354
+ return `[${role}] ${messageText(item)}`;
493
355
  }
494
356
  if (type === "reasoning") {
495
357
  return "[reasoning] (omitted)";
496
358
  }
497
359
  if (RESULT_TYPES.has(type)) {
498
- return `[tool_result] ${truncateForTranscript(resultText(item), 2000)}`;
360
+ return `[tool_result] ${resultText(item)}`;
499
361
  }
500
362
  if (RESULT_TYPE_BY_CALL_TYPE[type]) {
501
- return `[tool_call ${type}] ${truncateForTranscript(callText(item), 1000)}`;
363
+ return `[tool_call ${type}] ${callText(item)}`;
502
364
  }
503
- return `[${type}] ${truncateForTranscript(safeStringify(item), 1000)}`;
365
+ return `[${type}] ${safeStringify(item)}`;
504
366
  }
505
367
 
506
368
  function resultText(item: CompactionItem): string {
@@ -526,30 +388,3 @@ function safeStringify(value: unknown): string {
526
388
  return String(value);
527
389
  }
528
390
  }
529
-
530
- function truncateForTranscript(text: string, max: number): string {
531
- if (text.length <= max) {
532
- return text;
533
- }
534
- return `${text.slice(0, max)}... (${text.length - max} more chars)`;
535
- }
536
-
537
- function transcriptLength(lines: readonly string[]): number {
538
- if (lines.length === 0) {
539
- return 0;
540
- }
541
- return lines.reduce((sum, line) => sum + line.length, 0) + Math.max(0, lines.length - 1);
542
- }
543
-
544
- function truncateMiddleByChars(text: string, maxChars: number): string {
545
- if (text.length <= maxChars) {
546
- return text;
547
- }
548
- if (maxChars <= USER_MESSAGE_TRUNCATION_MARKER.length) {
549
- return USER_MESSAGE_TRUNCATION_MARKER.slice(0, maxChars);
550
- }
551
- const keepChars = maxChars - USER_MESSAGE_TRUNCATION_MARKER.length;
552
- const headChars = Math.ceil(keepChars / 2);
553
- const tailChars = Math.floor(keepChars / 2);
554
- return `${text.slice(0, headChars)}${USER_MESSAGE_TRUNCATION_MARKER}${text.slice(text.length - tailChars)}`;
555
- }