Skip to content

Commit 106f776

Browse files
sbhooleycursoragent
andcommitted
fix(windows): SessionStart timeout, fast MCP check, terminal banner mirror
Raise SessionStart hook timeout to 60s. Verify MCP in-process when the hook already runs in .venv. Mirror full banner to stderr on Windows and write logs/sessionstart_last.json for diagnosis. Co-authored-by: Cursor <[email protected]>
1 parent d798995 commit 106f776

4 files changed

Lines changed: 95 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## 0.4.0 — 2026-05-18
44

5+
### Windows SessionStart visibility + timeout
6+
7+
- **SessionStart timeout** raised to **60s** (was 20s; MCP subprocess preflight could exceed it on Windows ARM).
8+
- **In-process MCP verify** on SessionStart (avoids extra 30s subprocess when the hook already runs in `.venv`).
9+
- **Windows terminal mirror** — full `[AINL Cortex]` banner printed to stderr as `SessionStart:startup says:` (native Claude often hides hook UI).
10+
- **`logs/sessionstart_last.json`** — written every SessionStart for offline diagnosis.
11+
512
### Windows hook fix + cross-OS self-heal
613

714
- **`run_hook.cmd`** — fixed plugin root resolution (`scripts\.` → real root). Restores SessionStart `[AINL Cortex]` banner and stops `scripts\.\scripts\bootstrap_no_python.ps1` PostToolUse errors.

docs/INSTALL_WINDOWS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ Usually **`git pull` + `.\setup.cmd -PythonOnly`** is enough. Re-clone only if t
170170
| Symptom | Fix |
171171
|---------|-----|
172172
| PostToolUse hook: `scripts\.\scripts\bootstrap_no_python.ps1` does not exist | Fixed in `run_hook.cmd` (use `git pull` or re-clone). Old batch logic set plugin root to `...\scripts\.` |
173-
| No `[AINL Cortex]` banner on SessionStart (macOS shows it, Windows does not) | Same bug — SessionStart never ran. After `git pull` (commit **f739b2d+**): `.\setup.cmd -PythonOnly`, quit Claude, `/reload-plugins`. Test: `.\scripts\verify_sessionstart.cmd` — stdout JSON should contain `[AINL Cortex]` |
173+
| No `[AINL Cortex]` banner on SessionStart (macOS shows it, Windows does not) | **(1)** Use a **new** session — `claude --resume` does **not** re-run SessionStart. Run `claude` fresh or `/clear`. **(2)** Hook may have timed out (fixed: 60s timeout + faster in-process MCP check). **(3)** After `git pull`: `.\setup.cmd -PythonOnly`, quit Claude, `/reload-plugins`. Check `logs\sessionstart_last.json` in the plugin folder. Test: `.\scripts\verify_sessionstart.cmd` |
174174
| `hooks/hooks.json` still says `python ... run_hook.py` | Run `.\setup.cmd -PythonOnly` — Windows should use `"${CLAUDE_PLUGIN_ROOT}/scripts/run_hook.cmd" startup` |
175175
| `Remove-Item`: directory in use | Quit Claude Code; stop `python`/`py` under `ainl-cortex`; retry |
176176
| `python` not found | Reinstall Python with PATH enabled; reopen terminal |

hooks/startup.py

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,53 @@ def _plugin_root() -> Path:
5454
return Path(__file__).resolve().parent.parent
5555

5656

57+
def _write_sessionstart_probe(
58+
root: Path,
59+
ok: bool,
60+
system_message: str,
61+
*,
62+
error: Optional[str] = None,
63+
) -> None:
64+
"""Persist last SessionStart result so operators can verify hooks on Windows."""
65+
try:
66+
log_dir = root / "logs"
67+
log_dir.mkdir(parents=True, exist_ok=True)
68+
payload = {
69+
"ok": ok,
70+
"ts": time.time(),
71+
"platform": sys.platform,
72+
"plugin_root": str(root),
73+
"preview": (system_message or "")[:1200],
74+
"error": error,
75+
}
76+
(log_dir / "sessionstart_last.json").write_text(
77+
json.dumps(payload, indent=2),
78+
encoding="utf-8",
79+
)
80+
except OSError:
81+
pass
82+
83+
84+
def _mirror_sessionstart_banner(system_message: str) -> None:
85+
"""
86+
macOS Claude Code prints hook systemMessage as ``SessionStart:startup says:``.
87+
Windows native builds often omit that UI — mirror the banner to stderr.
88+
"""
89+
if not (system_message or "").strip():
90+
return
91+
try:
92+
from mcp_server.platform_paths import is_windows as _is_windows
93+
except Exception:
94+
_win = sys.platform == "win32"
95+
else:
96+
_win = _is_windows()
97+
if not _win:
98+
return
99+
first = system_message.strip().split("\n", 1)[0]
100+
print(f"SessionStart:startup says: {first}", file=sys.stderr, flush=True)
101+
print(system_message, file=sys.stderr, flush=True)
102+
103+
57104
def _hook_cwd() -> Path:
58105
try:
59106
from shared.stdin import read_stdin_json
@@ -123,6 +170,31 @@ def _env_for_mcp_test(plugin_root: Path) -> dict:
123170
return env
124171

125172

173+
def verify_mcp_imports_inprocess(plugin_root: Path) -> Tuple[bool, str]:
174+
"""Fast path when SessionStart already runs inside the plugin venv."""
175+
try:
176+
if str(plugin_root) not in sys.path:
177+
sys.path.insert(0, str(plugin_root))
178+
from mcp_server.runtime_bootstrap import bootstrap_runtime
179+
from mcp_server.import_compat import (
180+
verify_bare_graph_store_import,
181+
verify_bare_node_types_import,
182+
verify_bare_retrieval_import,
183+
)
184+
185+
bootstrap_runtime(plugin_root, heal_deps=True)
186+
ok = (
187+
verify_bare_node_types_import()
188+
and verify_bare_graph_store_import()
189+
and verify_bare_retrieval_import()
190+
)
191+
if ok:
192+
return True, "in-process venv imports OK"
193+
return False, "in-process import verify returned false"
194+
except Exception as exc:
195+
return False, f"in-process: {exc}"[:200]
196+
197+
126198
def verify_mcp_imports(plugin_root: Path) -> Tuple[bool, str]:
127199
"""
128200
Resolves a Python that can import mcp (venv binary preferred; else system
@@ -503,7 +575,9 @@ def main():
503575
except OSError as e:
504576
db_s = f"error: {e}"
505577

506-
mcp_ok, mcp_detail = verify_mcp_imports(root)
578+
mcp_ok, mcp_detail = verify_mcp_imports_inprocess(root)
579+
if not mcp_ok:
580+
mcp_ok, mcp_detail = verify_mcp_imports(root)
507581
_agent_install_banner = ""
508582
try:
509583
sys.path.insert(0, str(root))
@@ -518,7 +592,9 @@ def main():
518592
_iok, _imsg = maybe_auto_install_at_session_start(root)
519593
_auto_note = _imsg
520594
if _iok:
521-
mcp_ok, mcp_detail = verify_mcp_imports(root)
595+
mcp_ok, mcp_detail = verify_mcp_imports_inprocess(root)
596+
if not mcp_ok:
597+
mcp_ok, mcp_detail = verify_mcp_imports(root)
522598
_agent_install_banner = build_agent_install_banner(
523599
root,
524600
mcp_ok=mcp_ok,
@@ -919,6 +995,9 @@ def main():
919995
except Exception:
920996
pass
921997

998+
_write_sessionstart_probe(root, True, system_message)
999+
_mirror_sessionstart_banner(system_message)
1000+
9221001
# Transcript: JSON stdout is what Claude documents; also mirror to stderr in raw terminals
9231002
j = json.dumps(out)
9241003
print(j, file=sys.stdout, flush=True)
@@ -931,10 +1010,13 @@ def main():
9311010

9321011
except Exception as e:
9331012
logger.error("SessionStart error: %s", e)
1013+
_write_sessionstart_probe(root, False, "", error=str(e))
1014+
err_msg = f"[AINL Graph Memory] SessionStart error (non-fatal): {e}"
1015+
_mirror_sessionstart_banner(err_msg)
9341016
out = {
9351017
"continue": True,
9361018
"suppressOutput": False,
937-
"systemMessage": f"[AINL Graph Memory] SessionStart error (non-fatal): {e}",
1019+
"systemMessage": err_msg,
9381020
"hookSpecificOutput": {
9391021
"hookEventName": "SessionStart",
9401022
"additionalContext": str(e),

scripts/setup_install.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ def write_hooks_json(root: Path) -> None:
140140
"Stop": ["stop"],
141141
}
142142
timeouts = {
143-
"startup": 20,
143+
# SessionStart runs MCP preflight + banner; 20s was too low on Windows ARM.
144+
"startup": 60,
144145
"user_prompt_submit": 5,
145146
"ainl_detection": 3,
146147
"user_prompt_expansion": 3,

0 commit comments

Comments
 (0)