Skip to content

fix(windows): eliminate console window flashes using PseudoConsole#22

Merged
cpfiffer merged 2 commits intoletta-ai:mainfrom
scrossle:fix/windows-pseudoconsole
Feb 24, 2026
Merged

fix(windows): eliminate console window flashes using PseudoConsole#22
cpfiffer merged 2 commits intoletta-ai:mainfrom
scrossle:fix/windows-pseudoconsole

Conversation

@scrossle
Copy link
Contributor

Summary

Fixes #20. On Windows 11 / Windows Terminal, every hook execution caused a visible console window flash (popup). This was because npx spawned through shell: true allocated a new console window.

This PR replaces the simple npx wrapper (silent-npx.js) with a three-part solution:

  • PseudoConsole (ConPTY) + CREATE_NO_WINDOW: A C# launcher (SilentLauncher.cs) compiled as a winexe creates a headless PseudoConsole session. Scripts run inside this invisible console — no popup.
  • Single-process tsx execution: Uses --import tsx/esm as a Node.js ESM loader instead of the tsx CLI (which spawns a child process). Combined with --require stdio-preload.cjs, stdin/stdout flow through temp files so the PseudoConsole doesn't need STARTF_USESTDHANDLES (which re-triggers the flash on Win11 26300).
  • Background worker survival: Workers (for async Letta API calls) spawn through silent-launcher.exe with detached: true. Since the exe is a winexe, detached doesn't create a console flash, and workers get their own PseudoConsole so they survive the parent's closure.

Additional fixes included

  • Letta API message ordering: The /conversations/{id}/messages endpoint does not guarantee newest-first ordering. Added explicit date sorting so lastSeenMessageId tracking works correctly.
  • API limit: Bumped message fetch limit from 50 to 300 — Letta returns multiple entries per logical message (hidden_reasoning + assistant_message pairs), so 50 was insufficient for active conversations.

Files changed

File Description
hooks/SilentLauncher.cs New — C# source for the PseudoConsole launcher
hooks/silent-launcher.exe New — Compiled binary (AnyCPU winexe, works on x64 and ARM64)
hooks/stdio-preload.cjs New — Node.js --require script for temp file stdin/stdout
hooks/silent-npx.cjs New — Cross-platform shim: delegates to silent-launcher.exe on Windows, runs tsx directly elsewhere
hooks/silent-npx.js Deleted — Replaced by .cjs version (required since package.json has "type": "module")
hooks/hooks.json Modified — .js.cjs extension
scripts/sync_letta_memory.ts Modified — Date sorting, limit bump, Windows worker spawn
scripts/send_messages_to_letta.ts Modified — Windows worker spawn

Non-Windows impact

No behavior change on Linux/macOS. The new silent-npx.cjs falls back to running tsx directly (same as before, just without the npx indirection).

Test plan

  • Verified no console popups across 6+ hook firing cycles on Windows 11 ARM64 (build 26300)
  • Verified bidirectional Letta message sync (messages TO and FROM Subconscious)
  • Verified background workers survive main script exit
  • Verified lastSeenMessageId advances correctly (no duplicate messages)
  • Needs testing on x64 Windows
  • Needs testing on Linux/macOS (should be no behavior change)

Supersedes #21.

🤖 Generated with Claude Code

On Windows 11 / Windows Terminal, hook execution caused visible console
window popups. This replaces the simple npx wrapper (silent-npx.js) with
a PseudoConsole (ConPTY) + CREATE_NO_WINDOW approach that runs scripts
in a headless console session.

New files:
- SilentLauncher.cs: C# launcher creating a PseudoConsole with
  CREATE_NO_WINDOW, stdin/stdout via temp files, and --import tsx/esm
  for single-process execution
- silent-launcher.exe: Compiled as AnyCPU winexe (works on x64 and ARM64)
- stdio-preload.cjs: Node.js --require script for temp file I/O
- silent-npx.cjs: Cross-platform shim that delegates to
  silent-launcher.exe on Windows, runs tsx directly elsewhere

Also fixes:
- Letta API message sync: bumped limit from 50 to 300 and added date
  sorting (API does not guarantee newest-first ordering)
- Background workers on Windows: spawn through silent-launcher.exe with
  detached:true so workers survive PseudoConsole closure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Abeansits
Copy link

@cpfiffer rdy to merge this? 🙏

- Add dual P/Invoke for UpdateProcThreadAttribute (Win11 26300+) vs
  UpdateProcThreadAttributeList (Win10/older) with EntryPointNotFoundException
  fallback in SilentLauncher.cs
- Extract shared spawnSilentWorker() utility into conversation_utils.ts,
  replacing ~50 duplicated lines in send_messages_to_letta.ts and
  sync_letta_memory.ts
- Add build.ps1 for reproducible silent-launcher.exe builds
- Rebuild silent-launcher.exe with updated source

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cpfiffer cpfiffer merged commit 1411947 into letta-ai:main Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

silent-npx.js fails with 'require is not defined' due to ESM/CJS conflict

3 participants