@gajae-code/agent-core 0.13.2 → 0.13.3

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.13.3] - 2026-08-15
6
+
7
+ ### Fixed
8
+ - Emergency compaction now considers managed transcript file size so sessions compact before the managed per-file limit (#4411).
9
+ - Managed runs discard assistant turns whose tool calls carried `\uXXXX`-escaped arguments and report them through the typed `escaped_arguments_discarded` outcome instead of executing unverifiable text; unmanaged runs reject such calls per-call with an actionable error (#4515).
10
+
5
11
  ## [0.13.2] - 2026-08-13
6
12
 
7
13
  ## [0.13.1] - 2026-08-11
@@ -80,7 +80,7 @@ export declare function effectiveReserveTokens(contextWindow: number, settings:
80
80
  */
81
81
  export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings, maxOutputTokens?: number): boolean;
82
82
  /** Reason a compaction was triggered. `token` is the normal user-configurable path; the rest are emergency floors. */
83
- export type CompactionTriggerReason = "token" | "heap" | "retainedMemory" | "providerBytes" | "messageCount" | "imageBytes";
83
+ export type CompactionTriggerReason = "token" | "heap" | "retainedMemory" | "transcriptFile" | "providerBytes" | "messageCount" | "imageBytes";
84
84
  /** A point-in-time resource sample. Supplied by an injectable sampler so tests never read real RSS. */
85
85
  export interface EmergencyCompactionSample {
86
86
  /** Resident heap bytes (e.g. process.memoryUsage().heapUsed). */
@@ -99,6 +99,8 @@ export interface EmergencyCompactionSample {
99
99
  tuiChatChildren?: number;
100
100
  /** Bytes retained by TUI render caches. */
101
101
  tuiCachedRenderBytes?: number;
102
+ /** On-disk JSONL transcript file size in bytes; 0/undefined when unknown. */
103
+ transcriptFileBytes?: number;
102
104
  }
103
105
  export interface EmergencyCompactionLimits {
104
106
  heapUsedBytes: number;
@@ -109,6 +111,7 @@ export interface EmergencyCompactionLimits {
109
111
  retainedMemoryDiagnosticBytes?: number;
110
112
  tuiChatChildren?: number;
111
113
  tuiChatChildrenDiagnostic?: number;
114
+ transcriptFileBytes?: number;
112
115
  }
113
116
  export declare function resetEmergencyRetainedMemoryDiagnosticsForTests(): void;
114
117
  export declare function resolveEmergencyCompactionLimits(totalMemoryBytes?: number): EmergencyCompactionLimits;
@@ -119,7 +122,7 @@ export declare function resolveEmergencyCompactionLimits(totalMemoryBytes?: numb
119
122
  */
120
123
  export declare const DEFAULT_EMERGENCY_COMPACTION_LIMITS: EmergencyCompactionLimits;
121
124
  /**
122
- * Returns the first emergency limit exceeded (heap > retainedMemory > providerBytes > imageBytes > messageCount),
125
+ * Returns the first emergency limit exceeded (heap > retainedMemory > transcriptFile > providerBytes > imageBytes > messageCount),
123
126
  * or null when none is. Pure apart from retained-memory diagnostics; the caller routes the result through the
124
127
  * normal pair-safe `compact()` cut logic so a tool_use/tool_result pair is never split.
125
128
  */
@@ -162,6 +162,11 @@ export type ManagedAttemptOutcome = {
162
162
  transportFailure?: TransportFailureFacts;
163
163
  };
164
164
  scope?: AttemptScope;
165
+ } | {
166
+ type: "escaped_arguments_discarded";
167
+ /** The defective assistant turn; already removed from usable history by the loop. */
168
+ message: AssistantMessage;
169
+ scope?: AttemptScope;
165
170
  } | {
166
171
  type: "context_overflow_discarded";
167
172
  message: AssistantMessage;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/agent-core",
4
- "version": "0.13.2",
4
+ "version": "0.13.3",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -32,9 +32,9 @@
32
32
  "fmt": "biome format --write ."
33
33
  },
34
34
  "dependencies": {
35
- "@gajae-code/ai": "0.13.2",
36
- "@gajae-code/natives": "0.13.2",
37
- "@gajae-code/utils": "0.13.2",
35
+ "@gajae-code/ai": "0.13.3",
36
+ "@gajae-code/natives": "0.13.3",
37
+ "@gajae-code/utils": "0.13.3",
38
38
  "@opentelemetry/api": "^1.9.0"
39
39
  },
40
40
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -263,6 +263,15 @@ function managedContextOverflowOutcome(message: AssistantMessage, scope?: Attemp
263
263
  return { type: "context_overflow_discarded", message, scope };
264
264
  }
265
265
 
266
+ /** Whether any tool call in the turn carried `\uXXXX`-escaped arguments. */
267
+ function hasEscapedNonAsciiToolCall(message: AssistantMessage): boolean {
268
+ return message.content.some(block => block.type === "toolCall" && block.escapedNonAsciiArguments === true);
269
+ }
270
+
271
+ function managedEscapedArgumentsOutcome(message: AssistantMessage, scope?: AttemptScope): ManagedAttemptOutcome {
272
+ return { type: "escaped_arguments_discarded", message, scope };
273
+ }
274
+
266
275
  function managedFailureMessage(error: unknown, config: AgentLoopConfig): AssistantMessage {
267
276
  const errorMessage = managedProperty(error, "message");
268
277
  const transportFailure = managedTransportFailure(error);
@@ -809,6 +818,7 @@ function managedAssistantContent(value: unknown): AssistantMessage["content"][nu
809
818
  const intent = managedProperty(value, "intent");
810
819
  const customWireName = managedProperty(value, "customWireName");
811
820
  const incompleteArguments = managedProperty(value, "incompleteArguments");
821
+ const escapedNonAsciiArguments = managedProperty(value, "escapedNonAsciiArguments");
812
822
  return {
813
823
  type,
814
824
  id,
@@ -818,6 +828,7 @@ function managedAssistantContent(value: unknown): AssistantMessage["content"][nu
818
828
  ...(typeof intent === "string" ? { intent } : {}),
819
829
  ...(typeof customWireName === "string" ? { customWireName } : {}),
820
830
  ...(typeof incompleteArguments === "boolean" ? { incompleteArguments } : {}),
831
+ ...(typeof escapedNonAsciiArguments === "boolean" ? { escapedNonAsciiArguments } : {}),
821
832
  };
822
833
  }
823
834
 
@@ -1773,6 +1784,35 @@ async function runLoopBody(
1773
1784
  }
1774
1785
  }
1775
1786
 
1787
+ // Escaped-non-ASCII tool arguments: managed bounded turn resample.
1788
+ //
1789
+ // Arguments that spell a printable non-ASCII character as `\uXXXX`
1790
+ // instead of literal UTF-8 are a wire-format defect, not a decision the
1791
+ // model needs to be told about. The payload parses cleanly, but one
1792
+ // mistyped nibble decodes to a different, equally valid character, so it
1793
+ // can never be verified or repaired after the fact. Reporting it as a
1794
+ // tool error spends the whole turn and writes the literal escape syntax
1795
+ // back into the context the model samples from next. A managed
1796
+ // invocation instead drops the defective turn and reports it through the
1797
+ // typed `escaped_arguments_discarded` outcome so the session policy owns
1798
+ // a bounded same-model retry; the defect is never treated as provider
1799
+ // evidence, so the fallback chain never advances on it. Unmanaged runs
1800
+ // keep the per-call rejection in `executeToolCalls` as the terminal
1801
+ // answer.
1802
+ if (
1803
+ config.fallbackManaged &&
1804
+ message.stopReason !== "error" &&
1805
+ message.stopReason !== "aborted" &&
1806
+ hasEscapedNonAsciiToolCall(message)
1807
+ ) {
1808
+ transaction?.discard();
1809
+ currentContext.messages.splice(contextMessageCount);
1810
+ newMessages.splice(newMessageCount);
1811
+ await config.onManagedAttemptOutcome?.(managedEscapedArgumentsOutcome(message, transaction?.scope));
1812
+ stream.end(newMessages);
1813
+ return;
1814
+ }
1815
+
1776
1816
  const overflow = managedContextOverflow(message, config);
1777
1817
  if (config.fallbackManaged && overflow) {
1778
1818
  transaction?.discard();
@@ -2743,6 +2783,20 @@ async function executeToolCalls(
2743
2783
  `Re-issue the call with complete arguments, splitting the work into smaller steps if needed.`,
2744
2784
  );
2745
2785
  }
2786
+ if (toolCall.escapedNonAsciiArguments) {
2787
+ record.argumentValidationFailed = true;
2788
+ // The arguments decoded cleanly, but they were spelled as `\uXXXX`
2789
+ // escapes rather than literal UTF-8. Hand-written hex is where models
2790
+ // mistype digits, and every mistyped nibble decodes to a different but
2791
+ // equally valid character — the payload is unverifiable and cannot be
2792
+ // repaired after parsing, so it is rejected rather than executed on
2793
+ // silently corrupted text.
2794
+ throw new Error(
2795
+ `Tool call "${toolCall.name}" spelled non-ASCII text as \\uXXXX escapes instead of literal UTF-8. ` +
2796
+ `Escaped text cannot be verified — a single wrong hex digit silently becomes a different character — ` +
2797
+ `so the call was not executed. Re-issue it writing every non-ASCII character literally.`,
2798
+ );
2799
+ }
2746
2800
  if (!tool) {
2747
2801
  // A discoverable tool that hasn't been activated yet resolves to
2748
2802
  // undefined here. The model often "remembers" such a tool (e.g.
@@ -253,6 +253,7 @@ export type CompactionTriggerReason =
253
253
  | "token"
254
254
  | "heap"
255
255
  | "retainedMemory"
256
+ | "transcriptFile"
256
257
  | "providerBytes"
257
258
  | "messageCount"
258
259
  | "imageBytes";
@@ -275,6 +276,8 @@ export interface EmergencyCompactionSample {
275
276
  tuiChatChildren?: number;
276
277
  /** Bytes retained by TUI render caches. */
277
278
  tuiCachedRenderBytes?: number;
279
+ /** On-disk JSONL transcript file size in bytes; 0/undefined when unknown. */
280
+ transcriptFileBytes?: number;
278
281
  }
279
282
 
280
283
  export interface EmergencyCompactionLimits {
@@ -286,6 +289,7 @@ export interface EmergencyCompactionLimits {
286
289
  retainedMemoryDiagnosticBytes?: number;
287
290
  tuiChatChildren?: number;
288
291
  tuiChatChildrenDiagnostic?: number;
292
+ transcriptFileBytes?: number;
289
293
  }
290
294
 
291
295
  const MAX_EMERGENCY_HEAP_FLOOR_BYTES = 1_536 * 1024 * 1024; // 1.5 GiB resident heap
@@ -293,6 +297,7 @@ const EMERGENCY_RETAINED_MEMORY_BYTES = 128 * 1024 * 1024;
293
297
  const DIAGNOSTIC_RETAINED_MEMORY_BYTES = 64 * 1024 * 1024;
294
298
  const EMERGENCY_TUI_CHAT_CHILDREN = 1000;
295
299
  const DIAGNOSTIC_TUI_CHAT_CHILDREN = 700;
300
+ const EMERGENCY_TRANSCRIPT_FILE_BYTES = 48 * 1024 * 1024; // 48 MiB (75% of the 64 MiB managed cap)
296
301
  let retainedMemoryDiagnosticActive = false;
297
302
  let tuiChatChildrenDiagnosticActive = false;
298
303
 
@@ -315,6 +320,7 @@ export function resolveEmergencyCompactionLimits(totalMemoryBytes: number = os.t
315
320
  retainedMemoryDiagnosticBytes: DIAGNOSTIC_RETAINED_MEMORY_BYTES,
316
321
  tuiChatChildren: EMERGENCY_TUI_CHAT_CHILDREN,
317
322
  tuiChatChildrenDiagnostic: DIAGNOSTIC_TUI_CHAT_CHILDREN,
323
+ transcriptFileBytes: EMERGENCY_TRANSCRIPT_FILE_BYTES,
318
324
  };
319
325
  }
320
326
 
@@ -326,7 +332,7 @@ export function resolveEmergencyCompactionLimits(totalMemoryBytes: number = os.t
326
332
  export const DEFAULT_EMERGENCY_COMPACTION_LIMITS: EmergencyCompactionLimits = resolveEmergencyCompactionLimits();
327
333
 
328
334
  /**
329
- * Returns the first emergency limit exceeded (heap > retainedMemory > providerBytes > imageBytes > messageCount),
335
+ * Returns the first emergency limit exceeded (heap > retainedMemory > transcriptFile > providerBytes > imageBytes > messageCount),
330
336
  * or null when none is. Pure apart from retained-memory diagnostics; the caller routes the result through the
331
337
  * normal pair-safe `compact()` cut logic so a tool_use/tool_result pair is never split.
332
338
  */
@@ -360,6 +366,11 @@ export function emergencyCompactionReason(
360
366
  tuiChatChildren >= (limits.tuiChatChildren ?? EMERGENCY_TUI_CHAT_CHILDREN)
361
367
  )
362
368
  return "retainedMemory";
369
+ if (
370
+ sample.transcriptFileBytes &&
371
+ sample.transcriptFileBytes > (limits.transcriptFileBytes ?? EMERGENCY_TRANSCRIPT_FILE_BYTES)
372
+ )
373
+ return "transcriptFile";
363
374
  if (sample.providerBytes > limits.providerBytes) return "providerBytes";
364
375
  if (sample.imageBytes > limits.imageBytes) return "imageBytes";
365
376
  if (sample.messageCount > limits.messageCount) return "messageCount";
package/src/types.ts CHANGED
@@ -182,6 +182,12 @@ export type ManagedAttemptOutcome =
182
182
  };
183
183
  scope?: AttemptScope;
184
184
  }
185
+ | {
186
+ type: "escaped_arguments_discarded";
187
+ /** The defective assistant turn; already removed from usable history by the loop. */
188
+ message: AssistantMessage;
189
+ scope?: AttemptScope;
190
+ }
185
191
  | { type: "context_overflow_discarded"; message: AssistantMessage; scope?: AttemptScope }
186
192
  | { type: "run_terminal"; reason: "cancelled" | "error" | "exhausted"; scope?: AttemptScope };
187
193