@inceptionstack/roundhouse 0.5.25 → 0.5.27
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 +14 -0
- package/package.json +1 -1
- package/src/cli/update.ts +43 -9
- package/src/gateway/gateway.ts +20 -0
- package/src/memory/lifecycle.ts +13 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@inceptionstack/roundhouse` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.5.27] — 2026-05-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Self-update no longer falsely fails on mise/nvm hosts** — on systems where Node is managed by mise (or nvm), `npm install -g` triggers a post-install reshim hook that exits 127 when its tool isn't on PATH, causing `execSync` to throw even though the package was written to disk correctly. The user-visible bug: "Self-update failed: Command failed: npm install -g …" plus `/status` continuing to show the old version forever (because the gateway never restarted). Fix: when the install command throws, consult `npm list -g <pkg>` and trust the on-disk version. If it matches the target, treat the install as successful. Same logic applied to extension updates. (#128)
|
|
9
|
+
- **Side effect:** `/update` now fires its existing 'restarting…' branch on this case, so `/status` reflects the new version on next boot.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **DRY in `cli/update.ts`:** extracted `getInstalledVersion()` helper used by both pre-install version check and post-failure verification; introduced `SELF_PACKAGE` constant; fixed stale `commands/update.ts` header comment.
|
|
13
|
+
|
|
14
|
+
## [0.5.26] — 2026-05-14
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Emergency compact loop — output-cap mismatch + summarization input overflow.** Two compounding bugs caused infinite emergency-compact loops on Haiku 4.5 sessions near the context limit. (1) `reserveTokens=150000` + Haiku's 64k output cap produced `maxTokens=120000`, which Bedrock rejected. (2) `hardTokens=200k`/`softTokens=180k` against a 200k window left no headroom for the summarizer prompt itself. Fix: lower thresholds to 150k/130k, add `COMPACT_HEADROOM_TOKENS=50k`, force `thinkingLevel:off` in `compactWithModel`, drop `reserveTokens` to 78k. State is now loaded once and reused; phase timing is hoisted; telemetry is accurate on failure. (#126)
|
|
18
|
+
|
|
5
19
|
## [0.5.25] — 2026-05-12
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/package.json
CHANGED
package/src/cli/update.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* cli/update.ts — Handle the /update command
|
|
3
3
|
*
|
|
4
4
|
* Transport-agnostic: receives a ProgressReporter interface,
|
|
5
5
|
* not a Telegram-specific thread object.
|
|
@@ -15,6 +15,28 @@ const GLOBAL_PI_EXTENSION_PACKAGES = [
|
|
|
15
15
|
"@inceptionstack/pi-branch-enforcer",
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
+
const SELF_PACKAGE = "@inceptionstack/roundhouse";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read globally-installed version of a package from disk.
|
|
22
|
+
* Returns empty string if the package is not installed or query fails.
|
|
23
|
+
*
|
|
24
|
+
* Used both for pre-install version checks and for post-failure verification
|
|
25
|
+
* (mise/nvm/npm reshim hooks can fail with exit 127 even after `npm install -g`
|
|
26
|
+
* actually wrote the new version to disk — see PR fix/self-update-verify-on-failure).
|
|
27
|
+
*/
|
|
28
|
+
function getInstalledVersion(pkg: string): string {
|
|
29
|
+
try {
|
|
30
|
+
const out = execSync(`npm list -g ${pkg} --json --depth=0 2>/dev/null`, {
|
|
31
|
+
timeout: 10_000,
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
});
|
|
34
|
+
return JSON.parse(out)?.dependencies?.[pkg]?.version ?? "";
|
|
35
|
+
} catch {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
export interface UpdateProgress {
|
|
19
41
|
update(text: string): Promise<void>;
|
|
20
42
|
}
|
|
@@ -28,14 +50,11 @@ export interface UpdateResult {
|
|
|
28
50
|
|
|
29
51
|
export async function updateExtensions(progress: UpdateProgress): Promise<void> {
|
|
30
52
|
for (const extensionPackage of GLOBAL_PI_EXTENSION_PACKAGES) {
|
|
53
|
+
let latestExtVersion = "";
|
|
31
54
|
try {
|
|
32
55
|
// Check if already at latest
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
encoding: "utf8",
|
|
36
|
-
});
|
|
37
|
-
const installedVersion = JSON.parse(installed)?.dependencies?.[extensionPackage]?.version ?? "";
|
|
38
|
-
const latestExtVersion = execSync(`npm view ${extensionPackage} version 2>/dev/null`, {
|
|
56
|
+
const installedVersion = getInstalledVersion(extensionPackage);
|
|
57
|
+
latestExtVersion = execSync(`npm view ${extensionPackage} version 2>/dev/null`, {
|
|
39
58
|
timeout: 10_000,
|
|
40
59
|
encoding: "utf8",
|
|
41
60
|
}).trim();
|
|
@@ -57,6 +76,14 @@ export async function updateExtensions(progress: UpdateProgress): Promise<void>
|
|
|
57
76
|
await progress.update(`✅ ${extensionPackage} updated`);
|
|
58
77
|
} catch (e) {
|
|
59
78
|
const msg = e instanceof Error ? e.message : String(e);
|
|
79
|
+
// Verify-after-fail: post-install reshim hooks (mise/nvm) can exit non-zero
|
|
80
|
+
// even when the package landed on disk correctly.
|
|
81
|
+
const onDisk = getInstalledVersion(extensionPackage);
|
|
82
|
+
if (onDisk && (!latestExtVersion || onDisk === latestExtVersion)) {
|
|
83
|
+
console.warn(`[roundhouse] ${extensionPackage} install reported failure but v${onDisk} is on disk — treating as success:`, msg);
|
|
84
|
+
await progress.update(`✅ ${extensionPackage} updated to v${onDisk} (post-install hook warned, ignored)`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
60
87
|
console.warn(`[roundhouse] failed to update extension ${extensionPackage}:`, msg);
|
|
61
88
|
await progress.update(`⚠️ Failed to update ${extensionPackage}: ${msg.slice(0, 150)}`);
|
|
62
89
|
}
|
|
@@ -71,13 +98,20 @@ export async function updateSelf(
|
|
|
71
98
|
await progress.update(`📦 Updating v${currentVersion} → v${latestVersion}...`);
|
|
72
99
|
|
|
73
100
|
try {
|
|
74
|
-
execSync(
|
|
101
|
+
execSync(`npm install -g ${SELF_PACKAGE}@latest 2>&1`, {
|
|
75
102
|
timeout: 120_000,
|
|
76
103
|
encoding: "utf8",
|
|
77
104
|
});
|
|
78
105
|
return undefined;
|
|
79
106
|
} catch (e) {
|
|
80
107
|
const msg = e instanceof Error ? e.message : String(e);
|
|
108
|
+
// Verify-after-fail: mise/nvm post-install reshim can exit 127 even when
|
|
109
|
+
// npm wrote the new version to disk. Trust the on-disk state over the exit code.
|
|
110
|
+
const onDisk = getInstalledVersion(SELF_PACKAGE);
|
|
111
|
+
if (onDisk === latestVersion) {
|
|
112
|
+
console.warn(`[roundhouse] self-update install reported failure but v${onDisk} is on disk — treating as success:`, msg);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
81
115
|
console.warn("[roundhouse] self-update failed:", msg);
|
|
82
116
|
return `Self-update failed: ${msg}`;
|
|
83
117
|
}
|
|
@@ -106,7 +140,7 @@ export async function performUpdate(progress: UpdateProgress): Promise<UpdateRes
|
|
|
106
140
|
|
|
107
141
|
let latestVersion: string;
|
|
108
142
|
try {
|
|
109
|
-
latestVersion = execSync(
|
|
143
|
+
latestVersion = execSync(`npm view ${SELF_PACKAGE} version 2>/dev/null`, {
|
|
110
144
|
timeout: 30_000,
|
|
111
145
|
encoding: "utf8",
|
|
112
146
|
}).trim();
|
package/src/gateway/gateway.ts
CHANGED
|
@@ -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;
|
package/src/memory/lifecycle.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
?
|
|
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();
|