htmlgraph 0.24.1__py3-none-any.whl → 0.25.0__py3-none-any.whl

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.
Files changed (103) hide show
  1. htmlgraph/__init__.py +20 -1
  2. htmlgraph/agent_detection.py +26 -10
  3. htmlgraph/analytics/cross_session.py +4 -3
  4. htmlgraph/analytics/work_type.py +52 -16
  5. htmlgraph/analytics_index.py +51 -19
  6. htmlgraph/api/__init__.py +3 -0
  7. htmlgraph/api/main.py +2115 -0
  8. htmlgraph/api/static/htmx.min.js +1 -0
  9. htmlgraph/api/static/style-redesign.css +1344 -0
  10. htmlgraph/api/static/style.css +1079 -0
  11. htmlgraph/api/templates/dashboard-redesign.html +812 -0
  12. htmlgraph/api/templates/dashboard.html +783 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +570 -0
  15. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  16. htmlgraph/api/templates/partials/agents.html +317 -0
  17. htmlgraph/api/templates/partials/event-traces.html +373 -0
  18. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  19. htmlgraph/api/templates/partials/features.html +509 -0
  20. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  21. htmlgraph/api/templates/partials/metrics.html +346 -0
  22. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  23. htmlgraph/api/templates/partials/orchestration.html +163 -0
  24. htmlgraph/api/templates/partials/spawners.html +375 -0
  25. htmlgraph/atomic_ops.py +560 -0
  26. htmlgraph/builders/base.py +55 -1
  27. htmlgraph/builders/bug.py +17 -2
  28. htmlgraph/builders/chore.py +17 -2
  29. htmlgraph/builders/epic.py +17 -2
  30. htmlgraph/builders/feature.py +25 -2
  31. htmlgraph/builders/phase.py +17 -2
  32. htmlgraph/builders/spike.py +27 -2
  33. htmlgraph/builders/track.py +14 -0
  34. htmlgraph/cigs/__init__.py +4 -0
  35. htmlgraph/cigs/reporter.py +818 -0
  36. htmlgraph/cli.py +1427 -401
  37. htmlgraph/cli_commands/__init__.py +1 -0
  38. htmlgraph/cli_commands/feature.py +195 -0
  39. htmlgraph/cli_framework.py +115 -0
  40. htmlgraph/collections/__init__.py +2 -0
  41. htmlgraph/collections/base.py +21 -0
  42. htmlgraph/collections/session.py +189 -0
  43. htmlgraph/collections/spike.py +7 -1
  44. htmlgraph/collections/task_delegation.py +236 -0
  45. htmlgraph/collections/traces.py +482 -0
  46. htmlgraph/config.py +113 -0
  47. htmlgraph/converter.py +41 -0
  48. htmlgraph/cost_analysis/__init__.py +5 -0
  49. htmlgraph/cost_analysis/analyzer.py +438 -0
  50. htmlgraph/dashboard.html +3315 -492
  51. htmlgraph-0.24.1.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
  52. htmlgraph/dashboard.html.bak +7181 -0
  53. htmlgraph/dashboard.html.bak2 +7231 -0
  54. htmlgraph/dashboard.html.bak3 +7232 -0
  55. htmlgraph/db/__init__.py +38 -0
  56. htmlgraph/db/queries.py +790 -0
  57. htmlgraph/db/schema.py +1334 -0
  58. htmlgraph/deploy.py +26 -27
  59. htmlgraph/docs/API_REFERENCE.md +841 -0
  60. htmlgraph/docs/HTTP_API.md +750 -0
  61. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  62. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
  63. htmlgraph/docs/README.md +533 -0
  64. htmlgraph/docs/version_check.py +3 -1
  65. htmlgraph/error_handler.py +544 -0
  66. htmlgraph/event_log.py +2 -0
  67. htmlgraph/hooks/__init__.py +8 -0
  68. htmlgraph/hooks/bootstrap.py +169 -0
  69. htmlgraph/hooks/context.py +271 -0
  70. htmlgraph/hooks/drift_handler.py +521 -0
  71. htmlgraph/hooks/event_tracker.py +405 -15
  72. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  73. htmlgraph/hooks/pretooluse.py +476 -6
  74. htmlgraph/hooks/prompt_analyzer.py +648 -0
  75. htmlgraph/hooks/session_handler.py +583 -0
  76. htmlgraph/hooks/state_manager.py +501 -0
  77. htmlgraph/hooks/subagent_stop.py +309 -0
  78. htmlgraph/hooks/task_enforcer.py +39 -0
  79. htmlgraph/models.py +111 -15
  80. htmlgraph/operations/fastapi_server.py +230 -0
  81. htmlgraph/orchestration/headless_spawner.py +22 -14
  82. htmlgraph/pydantic_models.py +476 -0
  83. htmlgraph/quality_gates.py +350 -0
  84. htmlgraph/repo_hash.py +511 -0
  85. htmlgraph/sdk.py +348 -10
  86. htmlgraph/server.py +194 -0
  87. htmlgraph/session_hooks.py +300 -0
  88. htmlgraph/session_manager.py +131 -1
  89. htmlgraph/session_registry.py +587 -0
  90. htmlgraph/session_state.py +436 -0
  91. htmlgraph/system_prompts.py +449 -0
  92. htmlgraph/templates/orchestration-view.html +350 -0
  93. htmlgraph/track_builder.py +19 -0
  94. htmlgraph/validation.py +115 -0
  95. htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +7417 -0
  96. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/METADATA +91 -64
  97. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/RECORD +103 -42
  98. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/styles.css +0 -0
  99. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  100. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  101. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  102. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/WHEEL +0 -0
  103. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,583 @@
1
+ """
2
+ HtmlGraph Session Handler Module
3
+
4
+ Centralizes session lifecycle and tracking logic for hooks.
5
+ Provides unified functions for session initialization, tracking, and cleanup.
6
+
7
+ This module extracts common patterns from session-start.py and session-end.py
8
+ hooks to provide reusable session management operations.
9
+
10
+ Public API:
11
+ init_or_get_session(context: HookContext) -> Session | None
12
+ Get or create session from SessionManager
13
+
14
+ handle_session_start(context: HookContext, session: Session | None) -> dict
15
+ Initialize HtmlGraph tracking and build feature context
16
+
17
+ handle_session_end(context: HookContext) -> dict
18
+ Close session gracefully and record final metrics
19
+
20
+ record_user_query_event(context: HookContext, prompt: str) -> str | None
21
+ Create UserQuery event in database
22
+
23
+ check_version_status() -> dict | None
24
+ Check if HtmlGraph has updates available
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import logging
31
+ import os
32
+ import subprocess
33
+ from datetime import datetime, timezone
34
+ from pathlib import Path
35
+ from typing import TYPE_CHECKING, Any
36
+
37
+ if TYPE_CHECKING:
38
+ from htmlgraph.hooks.context import HookContext
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ def init_or_get_session(context: HookContext) -> Any | None:
44
+ """
45
+ Get or create session from SessionManager.
46
+
47
+ Attempts to get an active session for the current agent.
48
+ If none exists, creates a new one with automatic initialization.
49
+
50
+ Args:
51
+ context: HookContext with project and graph directory information
52
+
53
+ Returns:
54
+ Session object if successful, None if SessionManager unavailable or error occurs
55
+
56
+ Note:
57
+ - Handles graceful degradation if SessionManager cannot be imported
58
+ - Caches session in context for reuse
59
+ - Logs session ID for debugging
60
+ """
61
+ try:
62
+ manager = context.session_manager
63
+ agent = context.agent_id
64
+
65
+ # Try to get existing session for this agent
66
+ active = manager.get_active_session_for_agent(agent=agent)
67
+ if not active:
68
+ # Create new session with commit info
69
+ try:
70
+ head_commit = _get_head_commit(context.project_dir)
71
+ except Exception:
72
+ head_commit = None
73
+
74
+ active = manager.start_session(
75
+ session_id=None,
76
+ agent=agent,
77
+ start_commit=head_commit,
78
+ title=f"Session {datetime.now().strftime('%Y-%m-%d %H:%M')}",
79
+ )
80
+
81
+ context.log("info", f"Session initialized: {active.id if active else 'None'}")
82
+ return active
83
+
84
+ except ImportError as e:
85
+ context.log("error", f"SessionManager not available: {e}")
86
+ return None
87
+ except Exception as e:
88
+ context.log("error", f"Failed to initialize session: {e}")
89
+ return None
90
+
91
+
92
+ def handle_session_start(context: HookContext, session: Any | None) -> dict:
93
+ """
94
+ Initialize HtmlGraph tracking for the session.
95
+
96
+ Performs session startup operations:
97
+ - Initializes database entry if needed
98
+ - Loads active features and spikes from project
99
+ - Builds feature context string
100
+ - Records session start event
101
+ - Creates conversation-init spike if new conversation
102
+
103
+ Args:
104
+ context: HookContext with project and graph directory information
105
+ session: Session object from SessionManager (optional)
106
+
107
+ Returns:
108
+ dict with:
109
+ {
110
+ "continue": True,
111
+ "hookSpecificOutput": {
112
+ "sessionFeatureContext": str with feature context,
113
+ "versionInfo": optional version check result
114
+ }
115
+ }
116
+ """
117
+ output: dict[str, Any] = {
118
+ "continue": True,
119
+ "hookSpecificOutput": {
120
+ "sessionFeatureContext": "",
121
+ "versionInfo": None,
122
+ },
123
+ }
124
+
125
+ if not session:
126
+ return output
127
+
128
+ # Ensure session exists in database
129
+ try:
130
+ db = context.database
131
+ cursor = db.connection.cursor()
132
+ cursor.execute(
133
+ "SELECT COUNT(*) FROM sessions WHERE session_id = ?",
134
+ (session.id,),
135
+ )
136
+ session_exists = cursor.fetchone()[0] > 0
137
+
138
+ if not session_exists:
139
+ cursor.execute(
140
+ """
141
+ INSERT INTO sessions (session_id, agent_assigned, created_at, status)
142
+ VALUES (?, ?, ?, 'active')
143
+ """,
144
+ (
145
+ session.id,
146
+ context.agent_id,
147
+ datetime.now(timezone.utc).isoformat(),
148
+ ),
149
+ )
150
+ db.connection.commit()
151
+ context.log("info", f"Created database session: {session.id}")
152
+ except ImportError:
153
+ context.log("debug", "Database not available, skipping session entry")
154
+ except Exception as e:
155
+ context.log("warning", f"Could not create database session: {e}")
156
+
157
+ # Track session start activity
158
+ try:
159
+ external_session_id = context.hook_input.get("session_id", "unknown")
160
+ context.session_manager.track_activity(
161
+ session_id=session.id,
162
+ tool="SessionStart",
163
+ summary=f"Session started: {external_session_id}",
164
+ payload={
165
+ "agent": context.agent_id,
166
+ "external_session_id": external_session_id,
167
+ },
168
+ )
169
+ except Exception as e:
170
+ context.log("warning", f"Could not track session start activity: {e}")
171
+
172
+ # Load features and build context
173
+ try:
174
+ features = _load_features(context.graph_dir)
175
+ active_features = [f for f in features if f.get("status") == "in-progress"]
176
+
177
+ if active_features:
178
+ feature_list = "\n".join(
179
+ [f"- **{f['id']}**: {f['title']}" for f in active_features[:3]]
180
+ )
181
+ context_str = f"""## Active Features
182
+
183
+ {feature_list}
184
+
185
+ Activity will be attributed to these features based on file patterns and keywords."""
186
+ output["hookSpecificOutput"]["sessionFeatureContext"] = context_str
187
+ context.log("info", f"Loaded {len(active_features)} active features")
188
+
189
+ except Exception as e:
190
+ context.log("warning", f"Could not load features: {e}")
191
+
192
+ # Check version status
193
+ try:
194
+ version_info = check_version_status()
195
+ if version_info and version_info.get("is_outdated"):
196
+ output["hookSpecificOutput"]["versionInfo"] = version_info
197
+ context.log(
198
+ "info",
199
+ f"Update available: {version_info.get('installed')} → {version_info.get('latest')}",
200
+ )
201
+ except Exception as e:
202
+ context.log("debug", f"Could not check version: {e}")
203
+
204
+ return output
205
+
206
+
207
+ def handle_session_end(context: HookContext) -> dict:
208
+ """
209
+ Close session gracefully and record final metrics.
210
+
211
+ Performs session end operations:
212
+ - Captures handoff notes if provided
213
+ - Links transcript if available
214
+ - Records session end event
215
+ - Cleans up temporary state files
216
+
217
+ Args:
218
+ context: HookContext with project and graph directory information
219
+
220
+ Returns:
221
+ dict with:
222
+ {
223
+ "continue": True,
224
+ "status": "success" | "partial" | "error"
225
+ }
226
+ """
227
+ output: dict[str, Any] = {
228
+ "continue": True,
229
+ "status": "success",
230
+ }
231
+
232
+ try:
233
+ session = context.session_manager.get_active_session()
234
+ if not session:
235
+ context.log("debug", "No active session to close")
236
+ return output
237
+
238
+ # Capture handoff context if provided
239
+ handoff_notes = context.hook_input.get("handoff_notes") or os.environ.get(
240
+ "HTMLGRAPH_HANDOFF_NOTES"
241
+ )
242
+ recommended_next = context.hook_input.get("recommended_next") or os.environ.get(
243
+ "HTMLGRAPH_HANDOFF_RECOMMEND"
244
+ )
245
+ blockers_raw = context.hook_input.get("blockers") or os.environ.get(
246
+ "HTMLGRAPH_HANDOFF_BLOCKERS"
247
+ )
248
+
249
+ blockers = None
250
+ if isinstance(blockers_raw, str):
251
+ blockers = [b.strip() for b in blockers_raw.split(",") if b.strip()]
252
+ elif isinstance(blockers_raw, list):
253
+ blockers = [str(b).strip() for b in blockers_raw if str(b).strip()]
254
+
255
+ if handoff_notes or recommended_next or blockers:
256
+ try:
257
+ context.session_manager.set_session_handoff(
258
+ session_id=session.id,
259
+ handoff_notes=handoff_notes,
260
+ recommended_next=recommended_next,
261
+ blockers=blockers,
262
+ )
263
+ context.log("info", "Session handoff recorded")
264
+ except Exception as e:
265
+ context.log("warning", f"Could not set handoff: {e}")
266
+ output["status"] = "partial"
267
+
268
+ # Link transcript if external session ID provided
269
+ external_session_id = context.hook_input.get("session_id") or os.environ.get(
270
+ "CLAUDE_SESSION_ID"
271
+ )
272
+ if external_session_id:
273
+ try:
274
+ from htmlgraph.transcript import TranscriptReader
275
+
276
+ reader = TranscriptReader()
277
+ transcript = reader.read_session(external_session_id)
278
+ if transcript:
279
+ context.session_manager.link_transcript(
280
+ session_id=session.id,
281
+ transcript_id=external_session_id,
282
+ transcript_path=str(transcript.path),
283
+ git_branch=transcript.git_branch
284
+ if hasattr(transcript, "git_branch")
285
+ else None,
286
+ )
287
+ context.log("info", "Transcript linked to session")
288
+ except ImportError:
289
+ context.log("debug", "Transcript reader not available")
290
+ except Exception as e:
291
+ context.log("warning", f"Could not link transcript: {e}")
292
+ output["status"] = "partial"
293
+
294
+ # Record session end activity
295
+ try:
296
+ context.session_manager.track_activity(
297
+ session_id=session.id,
298
+ tool="SessionEnd",
299
+ summary="Session ended",
300
+ )
301
+ except Exception as e:
302
+ context.log("warning", f"Could not track session end: {e}")
303
+ output["status"] = "partial"
304
+
305
+ context.log("info", f"Session closed: {session.id}")
306
+
307
+ except ImportError:
308
+ context.log("error", "SessionManager not available")
309
+ output["status"] = "error"
310
+ except Exception as e:
311
+ context.log("error", f"Failed to close session: {e}")
312
+ output["status"] = "error"
313
+
314
+ # Always cleanup temp files
315
+ try:
316
+ _cleanup_temp_files(context.graph_dir)
317
+ except Exception as e:
318
+ context.log("warning", f"Could not cleanup temp files: {e}")
319
+
320
+ return output
321
+
322
+
323
+ def record_user_query_event(context: HookContext, prompt: str) -> str | None:
324
+ """
325
+ Create UserQuery event in database.
326
+
327
+ Records a user query prompt as an event in the database for later
328
+ reference by tool calls in the same conversation turn.
329
+
330
+ Args:
331
+ context: HookContext with project and graph directory information
332
+ prompt: The user query prompt text
333
+
334
+ Returns:
335
+ event_id if successful, None otherwise
336
+
337
+ Note:
338
+ - Event ID is stored for parent-child linking of subsequent tool calls
339
+ - Events expire after 10 minutes (conversation turn boundary)
340
+ - Safe to call even if database unavailable (graceful degradation)
341
+ """
342
+ try:
343
+ from htmlgraph.ids import generate_id
344
+
345
+ db = context.database
346
+ event_id = generate_id("event")
347
+
348
+ # Preview for logging
349
+ preview = prompt[:100].replace("\n", " ")
350
+ if len(prompt) > 100:
351
+ preview += "..."
352
+
353
+ # Insert UserQuery event
354
+ success = db.insert_event(
355
+ event_id=event_id,
356
+ agent_id=context.agent_id,
357
+ event_type="user_query",
358
+ session_id=context.session_id,
359
+ tool_name="UserQuery",
360
+ input_summary=preview,
361
+ output_summary="Query recorded",
362
+ context={"full_prompt_length": len(prompt)},
363
+ )
364
+
365
+ if success:
366
+ context.log("info", f"Recorded UserQuery event: {event_id}")
367
+ return event_id
368
+ else:
369
+ context.log("warning", "Failed to insert UserQuery event")
370
+ return None
371
+
372
+ except ImportError:
373
+ context.log("debug", "Database not available for UserQuery event")
374
+ return None
375
+ except Exception as e:
376
+ context.log("error", f"Failed to record UserQuery event: {e}")
377
+ return None
378
+
379
+
380
+ def check_version_status() -> dict | None:
381
+ """
382
+ Check if HtmlGraph has updates available.
383
+
384
+ Compares installed version with latest version on PyPI.
385
+ Attempts multiple methods to get version information:
386
+ 1. import htmlgraph and check __version__
387
+ 2. pip show htmlgraph
388
+ 3. PyPI JSON API (requires network)
389
+
390
+ Returns:
391
+ dict with version info if outdated:
392
+ {
393
+ "installed": "0.9.0",
394
+ "latest": "0.9.1",
395
+ "is_outdated": True
396
+ }
397
+ None if versions match or cannot be determined
398
+
399
+ Note:
400
+ - Never blocks on network errors (5 second timeout)
401
+ - Gracefully degrades if methods unavailable
402
+ - Safe for use in hooks (catches all exceptions)
403
+ """
404
+ try:
405
+ installed_version = _get_installed_version()
406
+ latest_version = _get_latest_pypi_version()
407
+
408
+ if not (installed_version and latest_version):
409
+ return None
410
+
411
+ if installed_version == latest_version:
412
+ return None
413
+
414
+ # Compare versions
415
+ is_outdated = _compare_versions(installed_version, latest_version)
416
+ if is_outdated:
417
+ return {
418
+ "installed": installed_version,
419
+ "latest": latest_version,
420
+ "is_outdated": True,
421
+ }
422
+
423
+ return None
424
+
425
+ except Exception:
426
+ return None
427
+
428
+
429
+ # ============================================================================
430
+ # Private Helper Functions
431
+ # ============================================================================
432
+
433
+
434
+ def _get_head_commit(project_dir: str) -> str | None:
435
+ """Get current HEAD commit hash (short form)."""
436
+ try:
437
+ result = subprocess.run(
438
+ ["git", "rev-parse", "--short", "HEAD"],
439
+ capture_output=True,
440
+ text=True,
441
+ cwd=project_dir,
442
+ timeout=5,
443
+ )
444
+ if result.returncode == 0:
445
+ return result.stdout.strip()
446
+ except Exception:
447
+ pass
448
+ return None
449
+
450
+
451
+ def _load_features(graph_dir: Path) -> list[dict]:
452
+ """Load all features as dicts."""
453
+ try:
454
+ from htmlgraph.converter import node_to_dict # type: ignore[import]
455
+ from htmlgraph.graph import HtmlGraph
456
+
457
+ features_dir = graph_dir / "features"
458
+ if not features_dir.exists():
459
+ return []
460
+
461
+ graph = HtmlGraph(features_dir, auto_load=True)
462
+ return [node_to_dict(node) for node in graph.nodes.values()]
463
+
464
+ except Exception:
465
+ return []
466
+
467
+
468
+ def _get_installed_version() -> str | None:
469
+ """Get installed htmlgraph version."""
470
+ # Method 1: Import and check __version__
471
+ try:
472
+ import htmlgraph
473
+
474
+ return htmlgraph.__version__
475
+ except Exception:
476
+ pass
477
+
478
+ # Method 2: pip show
479
+ try:
480
+ result = subprocess.run(
481
+ ["pip", "show", "htmlgraph"],
482
+ capture_output=True,
483
+ text=True,
484
+ timeout=10,
485
+ )
486
+ if result.returncode == 0:
487
+ for line in result.stdout.splitlines():
488
+ if line.startswith("Version:"):
489
+ return line.split(":", 1)[1].strip()
490
+ except Exception:
491
+ pass
492
+
493
+ return None
494
+
495
+
496
+ def _get_latest_pypi_version() -> str | None:
497
+ """Get latest htmlgraph version from PyPI."""
498
+ try:
499
+ import urllib.request
500
+
501
+ req = urllib.request.Request(
502
+ "https://pypi.org/pypi/htmlgraph/json",
503
+ headers={
504
+ "Accept": "application/json",
505
+ "User-Agent": "htmlgraph-version-check",
506
+ },
507
+ )
508
+ with urllib.request.urlopen(req, timeout=5) as response:
509
+ data = json.loads(response.read().decode())
510
+ return data.get("info", {}).get("version")
511
+ except Exception:
512
+ return None
513
+
514
+
515
+ def _compare_versions(installed: str, latest: str) -> bool:
516
+ """
517
+ Check if installed version is older than latest.
518
+
519
+ Args:
520
+ installed: Installed version string
521
+ latest: Latest version string
522
+
523
+ Returns:
524
+ True if installed < latest, False otherwise
525
+
526
+ Note:
527
+ - Uses semantic versioning comparison
528
+ - Falls back to string comparison for non-semver versions
529
+ """
530
+ try:
531
+ # Try semantic version comparison
532
+ installed_parts = [int(x) for x in installed.split(".")]
533
+ latest_parts = [int(x) for x in latest.split(".")]
534
+ return installed_parts < latest_parts
535
+ except (ValueError, IndexError):
536
+ # Fallback to string comparison
537
+ return installed != latest
538
+
539
+
540
+ def _cleanup_temp_files(graph_dir: Path) -> None:
541
+ """
542
+ Clean up temporary state files after session end.
543
+
544
+ Removes session-scoped temporary files that are no longer needed.
545
+ Safe to call even if files don't exist (idempotent).
546
+
547
+ Args:
548
+ graph_dir: Path to .htmlgraph directory
549
+ """
550
+ temp_patterns = [
551
+ "parent-activity.json",
552
+ "user-query-event-*.json",
553
+ ]
554
+
555
+ for pattern in temp_patterns:
556
+ if "*" in pattern:
557
+ # Handle glob patterns
558
+ import glob
559
+
560
+ for path in glob.glob(str(graph_dir / pattern)):
561
+ try:
562
+ Path(path).unlink()
563
+ logger.debug(f"Cleaned up: {path}")
564
+ except Exception as e:
565
+ logger.debug(f"Could not clean up {path}: {e}")
566
+ else:
567
+ # Handle single file
568
+ path = graph_dir / pattern
569
+ try:
570
+ if path.exists():
571
+ path.unlink()
572
+ logger.debug(f"Cleaned up: {path}")
573
+ except Exception as e:
574
+ logger.debug(f"Could not clean up {path}: {e}")
575
+
576
+
577
+ __all__ = [
578
+ "init_or_get_session",
579
+ "handle_session_start",
580
+ "handle_session_end",
581
+ "record_user_query_event",
582
+ "check_version_status",
583
+ ]