@inceptionstack/roundhouse 0.5.25 → 0.5.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inceptionstack/roundhouse",
3
- "version": "0.5.25",
3
+ "version": "0.5.26",
4
4
  "type": "module",
5
5
  "description": "Multi-platform chat gateway that routes messages through a configured AI agent",
6
6
  "license": "MIT",
@@ -439,6 +439,26 @@ export class Gateway {
439
439
 
440
440
  stopTyping = startTypingLoop(thread);
441
441
 
442
+ // Pre-turn recovery: if a prior turn failed to compact (state has
443
+ // pendingCompact === "emergency"), the live session is almost certainly
444
+ // still over the model's context limit, so calling agent.prompt() now
445
+ // will throw with "prompt is too long" before our post-turn pressure
446
+ // handler ever runs — perpetuating the loop. Run the pressure handler
447
+ // BEFORE the agent call to recover. Best-effort: if it fails, fall
448
+ // through to the normal turn (which will then post the error and let
449
+ // the user see something is wrong).
450
+ if (memoryPrepared?.pendingCompact === "emergency") {
451
+ console.log(`[roundhouse] pre-turn recovery: pendingCompact=emergency, compacting before agent.prompt for thread=${agentThreadId}`);
452
+ try {
453
+ await this.handleContextPressure(thread, agentThreadId, agent, memoryRoot, "emergency");
454
+ // Clear the pending flag in our prepared snapshot so the post-turn
455
+ // pressure logic doesn't double-up on a redundant emergency pass.
456
+ memoryPrepared.pendingCompact = undefined;
457
+ } catch (err) {
458
+ console.error(`[roundhouse] pre-turn emergency compact failed:`, (err as Error).message);
459
+ }
460
+ }
461
+
442
462
  let deferredSoftFlush: { thread: any; agentThreadId: string; agent: AgentAdapter; memoryRoot: string } | undefined;
443
463
  try {
444
464
  let turnUsedTools = false;
@@ -239,7 +239,15 @@ export async function flushMemoryThenCompact(
239
239
  // NOT require the live session to fit under the limit — so skipping flush
240
240
  // lets us recover. Facts-to-MEMORY.md (the whole point of flush) is a
241
241
  // best-effort nicety that the next soft/hard flush can catch up on.
242
- const skipFlush = effectiveLevel === "emergency";
242
+ //
243
+ // We also skip flush when state already has pendingCompact === "emergency":
244
+ // a prior turn detected emergency pressure and could not complete (e.g. the
245
+ // current call was triggered by /compact while stuck). Even at "hard" or
246
+ // "manual" level, attempting the flush in that condition will hit the same
247
+ // 200k rejection. Deferring flush to a later (successful) turn is the safe
248
+ // recovery path.
249
+ const stuckInEmergency = (await loadThreadMemoryState(threadId)).pendingCompact === "emergency";
250
+ const skipFlush = effectiveLevel === "emergency" || stuckInEmergency;
243
251
 
244
252
  try {
245
253
  let flushMs = 0;
@@ -255,10 +263,12 @@ export async function flushMemoryThenCompact(
255
263
  }
256
264
 
257
265
  // Step 2: compact (use flush model if compactWithModel is available)
258
- const flushNote = skipFlush ? "flush skipped (emergency)" : `flush took ${flushMs}ms`;
266
+ const flushNote = skipFlush
267
+ ? (effectiveLevel === "emergency" ? "flush skipped (emergency)" : "flush skipped (recovery from prior emergency)")
268
+ : `flush took ${flushMs}ms`;
259
269
  console.log(`[memory] compacting ${threadId} (${flushNote})`);
260
270
  const progressNote = skipFlush
261
- ? "✂️ Compacting context... (emergency — skipping flush)"
271
+ ? `✂️ Compacting context... (${effectiveLevel === "emergency" ? "emergency " : "recovery — "}skipping flush)`
262
272
  : `✂️ Compacting context... (flush took ${(flushMs / 1000).toFixed(1)}s)`;
263
273
  await onProgress?.(progressNote);
264
274
  const t1 = Date.now();