htmlgraph 0.26.1__py3-none-any.whl → 0.26.3__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 (28) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +1 -1
  5. htmlgraph/api/main.py +66 -9
  6. htmlgraph/api/templates/partials/activity-feed.html +59 -0
  7. htmlgraph/cli.py +1 -1
  8. htmlgraph/config.py +173 -96
  9. htmlgraph/dashboard.html +631 -7277
  10. htmlgraph/db/schema.py +4 -5
  11. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +1 -1
  12. htmlgraph/hooks/context.py +40 -8
  13. htmlgraph/hooks/event_tracker.py +60 -12
  14. htmlgraph/hooks/pretooluse.py +60 -30
  15. htmlgraph/hooks/subagent_stop.py +3 -2
  16. htmlgraph/operations/fastapi_server.py +2 -2
  17. htmlgraph/orchestration/headless_spawner.py +167 -1
  18. htmlgraph/server.py +100 -203
  19. htmlgraph-0.26.3.data/data/htmlgraph/dashboard.html +812 -0
  20. {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.3.dist-info}/METADATA +1 -1
  21. {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.3.dist-info}/RECORD +27 -24
  22. htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +0 -7458
  23. {htmlgraph-0.26.1.data → htmlgraph-0.26.3.data}/data/htmlgraph/styles.css +0 -0
  24. {htmlgraph-0.26.1.data → htmlgraph-0.26.3.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  25. {htmlgraph-0.26.1.data → htmlgraph-0.26.3.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  26. {htmlgraph-0.26.1.data → htmlgraph-0.26.3.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  27. {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.3.dist-info}/WHEEL +0 -0
  28. {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.3.dist-info}/entry_points.txt +0 -0
htmlgraph/db/schema.py CHANGED
@@ -547,9 +547,9 @@ class HtmlGraphDB:
547
547
  try:
548
548
  cursor = self.connection.cursor() # type: ignore[union-attr]
549
549
  # Temporarily disable foreign key constraints to allow inserting
550
- # parent_event_id references that may not exist yet (will be created later)
551
- if parent_event_id:
552
- cursor.execute("PRAGMA foreign_keys=OFF")
550
+ # events even if parent_event_id or session_id don't exist yet
551
+ # (useful for cross-process event tracking where sessions are created asynchronously)
552
+ cursor.execute("PRAGMA foreign_keys=OFF")
553
553
  cursor.execute(
554
554
  """
555
555
  INSERT INTO agent_events
@@ -576,8 +576,7 @@ class HtmlGraphDB:
576
576
  ),
577
577
  )
578
578
  # Re-enable foreign key constraints
579
- if parent_event_id:
580
- cursor.execute("PRAGMA foreign_keys=ON")
579
+ cursor.execute("PRAGMA foreign_keys=ON")
581
580
  self.connection.commit() # type: ignore[union-attr]
582
581
  return True
583
582
  except sqlite3.IntegrityError as e:
@@ -2,5 +2,5 @@
2
2
  "dismissed_at": null,
3
3
  "dismissed_by": null,
4
4
  "session_id": null,
5
- "show_count": 101
5
+ "show_count": 121
6
6
  }
@@ -101,13 +101,19 @@ class HookContext:
101
101
  # 2. hook_input["sessionId"] (camelCase variant)
102
102
  # 3. HTMLGRAPH_SESSION_ID environment variable
103
103
  # 4. CLAUDE_SESSION_ID environment variable
104
- # 5. "unknown" as last resort
104
+ # 5. Most recent active session from database (NEW)
105
+ # 6. "unknown" as last resort
105
106
  #
106
107
  # NOTE: We intentionally do NOT use SessionManager.get_active_session()
107
108
  # as a fallback because the "active session" is stored in a global file
108
109
  # (.htmlgraph/session.json) that's shared across all Claude windows.
109
110
  # Using it would cause cross-window event contamination where tool calls
110
111
  # from Window B get linked to UserQuery events from Window A.
112
+ #
113
+ # However, we DO query the database by status='active' and created_at,
114
+ # which is different because it retrieves the most recent session that
115
+ # was explicitly marked as active (e.g., by SessionStart hook), without
116
+ # relying on a shared global agent state file.
111
117
  session_id = (
112
118
  hook_input.get("session_id")
113
119
  or hook_input.get("sessionId")
@@ -115,14 +121,40 @@ class HookContext:
115
121
  or os.environ.get("CLAUDE_SESSION_ID")
116
122
  )
117
123
 
118
- # Fallback to "unknown" - better than cross-window contamination
124
+ # Fallback: Query database for session with most recent UserQuery event
125
+ # This solves the issue where PostToolUse hooks don't receive session_id
126
+ # in hook_input. UserPromptSubmit hooks DO receive it and create UserQuery
127
+ # events with the correct session_id, so we use that as the source of truth.
119
128
  if not session_id:
120
- session_id = "unknown"
121
- logger.warning(
122
- "Could not resolve session_id from hook_input or environment. "
123
- "Events will not be linked to parent UserQuery. "
124
- "For multi-window support, set HTMLGRAPH_SESSION_ID env var."
125
- )
129
+ db_path = graph_dir / "htmlgraph.db"
130
+ if db_path.exists():
131
+ try:
132
+ import sqlite3
133
+
134
+ conn = sqlite3.connect(str(db_path), timeout=1.0)
135
+ cursor = conn.cursor()
136
+ cursor.execute("""
137
+ SELECT session_id FROM agent_events
138
+ WHERE tool_name = 'UserQuery'
139
+ ORDER BY timestamp DESC
140
+ LIMIT 1
141
+ """)
142
+ row = cursor.fetchone()
143
+ conn.close()
144
+ if row:
145
+ session_id = row[0]
146
+ logger.info(f"Resolved session_id from database: {session_id}")
147
+ except Exception as e:
148
+ logger.warning(f"Failed to query active session from database: {e}")
149
+
150
+ # Final fallback to "unknown" if database query fails
151
+ if not session_id:
152
+ session_id = "unknown"
153
+ logger.warning(
154
+ "Could not resolve session_id from hook_input, environment, or database. "
155
+ "Events will not be linked to parent UserQuery. "
156
+ "For multi-window support, set HTMLGRAPH_SESSION_ID env var."
157
+ )
126
158
 
127
159
  # Detect agent ID (priority order)
128
160
  # 1. Explicit agent_id in hook input
@@ -694,7 +694,9 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
694
694
  # Initialize SQLite database for event recording
695
695
  db = None
696
696
  try:
697
- db = HtmlGraphDB(str(graph_dir / "index.sqlite"))
697
+ from htmlgraph.config import get_database_path
698
+
699
+ db = HtmlGraphDB(str(get_database_path()))
698
700
  except Exception as e:
699
701
  print(f"Warning: Could not initialize SQLite database: {e}", file=sys.stderr)
700
702
  # Continue without SQLite (graceful degradation)
@@ -707,18 +709,64 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
707
709
  if model_from_input:
708
710
  detected_model = model_from_input
709
711
 
710
- # Get active session ID
711
- active_session = manager.get_active_session()
712
- if not active_session:
713
- # No active HtmlGraph session yet; start one (stable internal id).
714
- try:
715
- active_session = manager.start_session(
716
- session_id=None,
717
- agent=detected_agent,
718
- title=f"Session {datetime.now().strftime('%Y-%m-%d %H:%M')}",
712
+ active_session = None
713
+
714
+ # Check if we're in a subagent context (environment variables set by spawner router)
715
+ # This MUST be checked BEFORE using get_active_session() to avoid attributing
716
+ # subagent events to the parent orchestrator session
717
+ subagent_type = os.environ.get("HTMLGRAPH_SUBAGENT_TYPE")
718
+ parent_session_id = os.environ.get("HTMLGRAPH_PARENT_SESSION")
719
+
720
+ if subagent_type and parent_session_id:
721
+ # We're in a subagent - create or get subagent session
722
+ # Use deterministic session ID based on parent + subagent type
723
+ subagent_session_id = f"{parent_session_id}-{subagent_type}"
724
+
725
+ # Check if subagent session already exists
726
+ existing = manager.session_converter.load(subagent_session_id)
727
+ if existing:
728
+ active_session = existing
729
+ print(
730
+ f"Debug: Using existing subagent session: {subagent_session_id}",
731
+ file=sys.stderr,
719
732
  )
720
- except Exception:
721
- return {"continue": True}
733
+ else:
734
+ # Create new subagent session with parent link
735
+ try:
736
+ active_session = manager.start_session(
737
+ session_id=subagent_session_id,
738
+ agent=f"{subagent_type}-spawner",
739
+ is_subagent=True,
740
+ parent_session_id=parent_session_id,
741
+ title=f"{subagent_type.capitalize()} Subagent",
742
+ )
743
+ print(
744
+ f"Debug: Created subagent session: {subagent_session_id} "
745
+ f"(parent: {parent_session_id})",
746
+ file=sys.stderr,
747
+ )
748
+ except Exception as e:
749
+ print(
750
+ f"Warning: Could not create subagent session: {e}",
751
+ file=sys.stderr,
752
+ )
753
+ return {"continue": True}
754
+
755
+ # Override detected agent for subagent context
756
+ detected_agent = f"{subagent_type}-spawner"
757
+ else:
758
+ # Normal orchestrator/parent context - use global session cache
759
+ active_session = manager.get_active_session()
760
+ if not active_session:
761
+ # No active HtmlGraph session yet; start one (stable internal id).
762
+ try:
763
+ active_session = manager.start_session(
764
+ session_id=None,
765
+ agent=detected_agent,
766
+ title=f"Session {datetime.now().strftime('%Y-%m-%d %H:%M')}",
767
+ )
768
+ except Exception:
769
+ return {"continue": True}
722
770
 
723
771
  active_session_id = active_session.id
724
772
 
@@ -340,8 +340,6 @@ def create_start_event(
340
340
  """
341
341
  tool_use_id = None
342
342
  try:
343
- from pathlib import Path
344
-
345
343
  tool_use_id = generate_tool_use_id()
346
344
  trace_id = os.environ.get("HTMLGRAPH_TRACE_ID", tool_use_id)
347
345
  start_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
@@ -349,9 +347,10 @@ def create_start_event(
349
347
  # Sanitize input before storing
350
348
  sanitized_input = sanitize_tool_input(tool_input)
351
349
 
352
- # Connect to database (use project's .htmlgraph/index.sqlite, not home directory)
353
- graph_dir = Path.cwd() / ".htmlgraph"
354
- db_path = str(graph_dir / "index.sqlite")
350
+ # Connect to database (use project's .htmlgraph/htmlgraph.db, not home directory)
351
+ from htmlgraph.config import get_database_path
352
+
353
+ db_path = str(get_database_path())
355
354
  db = HtmlGraphDB(db_path)
356
355
 
357
356
  # Ensure session exists (create placeholder if needed)
@@ -364,10 +363,19 @@ def create_start_event(
364
363
 
365
364
  cursor = db.connection.cursor() # type: ignore[union-attr]
366
365
 
366
+ # Get UserQuery event ID for ALL tool calls (links conversation turns)
367
+ user_query_event_id = None
368
+ try:
369
+ from htmlgraph.hooks.event_tracker import get_parent_user_query
370
+
371
+ user_query_event_id = get_parent_user_query(db, session_id)
372
+ except Exception:
373
+ pass
374
+
367
375
  # Check if this is a Task() call for parent event creation
368
- parent_event_id = None
376
+ task_parent_event_id = None
369
377
  if tool_name == "Task":
370
- parent_event_id = create_task_parent_event(
378
+ task_parent_event_id = create_task_parent_event(
371
379
  db, tool_input, session_id, start_time
372
380
  )
373
381
 
@@ -376,6 +384,11 @@ def create_start_event(
376
384
 
377
385
  event_id = f"evt-{str(uuid.uuid4())[:8]}"
378
386
 
387
+ # Determine parent: Task() uses task_parent_event, others use UserQuery
388
+ parent_event_id = (
389
+ task_parent_event_id if tool_name == "Task" else user_query_event_id
390
+ )
391
+
379
392
  cursor.execute(
380
393
  """
381
394
  INSERT INTO agent_events
@@ -392,10 +405,17 @@ def create_start_event(
392
405
  json.dumps(sanitized_input)[:500], # Truncate for summary
393
406
  session_id,
394
407
  "recorded",
395
- parent_event_id, # Link to parent if this is Task()
408
+ parent_event_id, # Link to UserQuery or Task parent
396
409
  ),
397
410
  )
398
411
 
412
+ # Export Bash event as parent for child processes (e.g., spawner executables)
413
+ if tool_name == "Bash":
414
+ os.environ["HTMLGRAPH_PARENT_EVENT"] = event_id
415
+ logger.debug(
416
+ f"Exported HTMLGRAPH_PARENT_EVENT={event_id} for Bash tool call"
417
+ )
418
+
399
419
  # Also insert into tool_traces for correlation (if table exists)
400
420
  try:
401
421
  cursor.execute(
@@ -450,36 +470,46 @@ async def run_event_tracing(
450
470
  Event tracing response: {"hookSpecificOutput": {"tool_use_id": "...", ...}}
451
471
  """
452
472
  try:
473
+ from htmlgraph.hooks.context import HookContext
474
+
453
475
  loop = asyncio.get_event_loop()
454
476
  tool_name = tool_input.get("name", "") or tool_input.get("tool_name", "")
455
- session_id = get_current_session_id()
456
477
 
457
- # Skip if no session ID
458
- if not session_id:
459
- logger.debug("No session ID found, skipping event tracing")
460
- return {}
478
+ # Use HookContext to properly extract session_id (same as UserPromptSubmit)
479
+ context = HookContext.from_input(tool_input)
461
480
 
462
- # Run in thread pool since it involves I/O
463
- tool_use_id = await loop.run_in_executor(
464
- None,
465
- create_start_event,
466
- tool_name,
467
- tool_input,
468
- session_id,
469
- )
481
+ try:
482
+ session_id = context.session_id
470
483
 
471
- if tool_use_id:
472
- # Store in environment for PostToolUse correlation
473
- os.environ["HTMLGRAPH_TOOL_USE_ID"] = tool_use_id
484
+ # Skip if no session ID
485
+ if not session_id or session_id == "unknown":
486
+ logger.debug("No session ID found, skipping event tracing")
487
+ return {}
474
488
 
475
- return {
476
- "hookSpecificOutput": {
477
- "tool_use_id": tool_use_id,
478
- "additionalContext": f"Event tracing started: {tool_use_id}",
489
+ # Run in thread pool since it involves I/O
490
+ tool_use_id = await loop.run_in_executor(
491
+ None,
492
+ create_start_event,
493
+ tool_name,
494
+ tool_input,
495
+ session_id,
496
+ )
497
+
498
+ if tool_use_id:
499
+ # Store in environment for PostToolUse correlation
500
+ os.environ["HTMLGRAPH_TOOL_USE_ID"] = tool_use_id
501
+
502
+ return {
503
+ "hookSpecificOutput": {
504
+ "tool_use_id": tool_use_id,
505
+ "additionalContext": f"Event tracing started: {tool_use_id}",
506
+ }
479
507
  }
480
- }
481
508
 
482
- return {}
509
+ return {}
510
+ finally:
511
+ # Ensure context resources are properly closed
512
+ context.close()
483
513
  except Exception:
484
514
  # Graceful degradation - allow on error
485
515
  return {}
@@ -231,9 +231,10 @@ def handle_subagent_stop(hook_input: dict[str, Any]) -> dict[str, Any]:
231
231
 
232
232
  # Get project directory and database path
233
233
  try:
234
+ from htmlgraph.config import get_database_path
235
+
234
236
  cwd = hook_input.get("cwd", os.getcwd())
235
- graph_dir = Path(cwd) / ".htmlgraph"
236
- db_path = str(graph_dir / "index.sqlite")
237
+ db_path = str(get_database_path(cwd))
237
238
 
238
239
  if not Path(db_path).exists():
239
240
  logger.warning(f"Database not found: {db_path}")
@@ -79,12 +79,12 @@ def start_fastapi_server(
79
79
  if db_path is None:
80
80
  # Check for project-local database first
81
81
  project_dir = _resolve_project_dir()
82
- project_db = Path(project_dir) / ".htmlgraph" / "index.sqlite"
82
+ project_db = Path(project_dir) / ".htmlgraph" / "htmlgraph.db"
83
83
  if project_db.exists():
84
84
  db_path = str(project_db) # Use project-local database
85
85
  else:
86
86
  db_path = str(
87
- Path.home() / ".htmlgraph" / "index.sqlite"
87
+ Path.home() / ".htmlgraph" / "htmlgraph.db"
88
88
  ) # Fall back to home
89
89
 
90
90
  # Ensure database exists
@@ -3,9 +3,10 @@
3
3
  import json
4
4
  import os
5
5
  import subprocess
6
+ import sys
6
7
  import time
7
8
  from dataclasses import dataclass
8
- from typing import TYPE_CHECKING
9
+ from typing import TYPE_CHECKING, Any
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from htmlgraph.orchestration.live_events import LiveEventPublisher
@@ -463,6 +464,8 @@ class HeadlessSpawner:
463
464
  include_directories: list[str] | None = None,
464
465
  track_in_htmlgraph: bool = True,
465
466
  timeout: int = 120,
467
+ tracker: Any = None,
468
+ parent_event_id: str | None = None,
466
469
  ) -> AIResult:
467
470
  """
468
471
  Spawn Gemini in headless mode.
@@ -474,6 +477,8 @@ class HeadlessSpawner:
474
477
  include_directories: Directories to include for context. Default: None
475
478
  track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
476
479
  timeout: Max seconds to wait
480
+ tracker: Optional SpawnerEventTracker for recording subprocess invocation
481
+ parent_event_id: Optional parent event ID for event hierarchy
477
482
 
478
483
  Returns:
479
484
  AIResult with response, error, and tracked events if tracking enabled
@@ -529,6 +534,45 @@ class HeadlessSpawner:
529
534
  details="Running Gemini CLI",
530
535
  )
531
536
 
537
+ # Record subprocess invocation if tracker is available
538
+ subprocess_event_id = None
539
+ print(
540
+ f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
541
+ file=sys.stderr,
542
+ )
543
+ if tracker and parent_event_id:
544
+ print(
545
+ "DEBUG: Recording subprocess invocation for Gemini...",
546
+ file=sys.stderr,
547
+ )
548
+ try:
549
+ subprocess_event = tracker.record_tool_call(
550
+ tool_name="subprocess.gemini",
551
+ tool_input={"cmd": cmd},
552
+ phase_event_id=parent_event_id,
553
+ spawned_agent="gemini-2.0-flash",
554
+ )
555
+ if subprocess_event:
556
+ subprocess_event_id = subprocess_event.get("event_id")
557
+ print(
558
+ f"DEBUG: Subprocess event created for Gemini: {subprocess_event_id}",
559
+ file=sys.stderr,
560
+ )
561
+ else:
562
+ print("DEBUG: subprocess_event was None", file=sys.stderr)
563
+ except Exception as e:
564
+ # Tracking failure should not break execution
565
+ print(
566
+ f"DEBUG: Exception recording Gemini subprocess: {e}",
567
+ file=sys.stderr,
568
+ )
569
+ pass
570
+ else:
571
+ print(
572
+ f"DEBUG: Skipping Gemini subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
573
+ file=sys.stderr,
574
+ )
575
+
532
576
  # Execute with timeout and stderr redirection
533
577
  # Note: Cannot use capture_output with stderr parameter
534
578
  result = subprocess.run(
@@ -539,6 +583,18 @@ class HeadlessSpawner:
539
583
  timeout=timeout,
540
584
  )
541
585
 
586
+ # Complete subprocess invocation tracking
587
+ if tracker and subprocess_event_id:
588
+ try:
589
+ tracker.complete_tool_call(
590
+ event_id=subprocess_event_id,
591
+ output_summary=result.stdout[:500] if result.stdout else "",
592
+ success=result.returncode == 0,
593
+ )
594
+ except Exception:
595
+ # Tracking failure should not break execution
596
+ pass
597
+
542
598
  # Publish live event: processing response
543
599
  self._publish_live_event(
544
600
  "spawner_phase",
@@ -755,6 +811,8 @@ class HeadlessSpawner:
755
811
  bypass_approvals: bool = False,
756
812
  track_in_htmlgraph: bool = True,
757
813
  timeout: int = 120,
814
+ tracker: Any = None,
815
+ parent_event_id: str | None = None,
758
816
  ) -> AIResult:
759
817
  """
760
818
  Spawn Codex in headless mode.
@@ -774,6 +832,8 @@ class HeadlessSpawner:
774
832
  bypass_approvals: Bypass approval checks. Default: False
775
833
  track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
776
834
  timeout: Max seconds to wait
835
+ tracker: Optional SpawnerEventTracker for recording subprocess invocation
836
+ parent_event_id: Optional parent event ID for event hierarchy
777
837
 
778
838
  Returns:
779
839
  AIResult with response, error, and tracked events if tracking enabled
@@ -867,6 +927,45 @@ class HeadlessSpawner:
867
927
  details="Running Codex CLI",
868
928
  )
869
929
 
930
+ # Record subprocess invocation if tracker is available
931
+ subprocess_event_id = None
932
+ print(
933
+ f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
934
+ file=sys.stderr,
935
+ )
936
+ if tracker and parent_event_id:
937
+ print(
938
+ "DEBUG: Recording subprocess invocation for Codex...",
939
+ file=sys.stderr,
940
+ )
941
+ try:
942
+ subprocess_event = tracker.record_tool_call(
943
+ tool_name="subprocess.codex",
944
+ tool_input={"cmd": cmd},
945
+ phase_event_id=parent_event_id,
946
+ spawned_agent="gpt-4",
947
+ )
948
+ if subprocess_event:
949
+ subprocess_event_id = subprocess_event.get("event_id")
950
+ print(
951
+ f"DEBUG: Subprocess event created for Codex: {subprocess_event_id}",
952
+ file=sys.stderr,
953
+ )
954
+ else:
955
+ print("DEBUG: subprocess_event was None", file=sys.stderr)
956
+ except Exception as e:
957
+ # Tracking failure should not break execution
958
+ print(
959
+ f"DEBUG: Exception recording Codex subprocess: {e}",
960
+ file=sys.stderr,
961
+ )
962
+ pass
963
+ else:
964
+ print(
965
+ f"DEBUG: Skipping Codex subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
966
+ file=sys.stderr,
967
+ )
968
+
870
969
  result = subprocess.run(
871
970
  cmd,
872
971
  stdout=subprocess.PIPE,
@@ -875,6 +974,18 @@ class HeadlessSpawner:
875
974
  timeout=timeout,
876
975
  )
877
976
 
977
+ # Complete subprocess invocation tracking
978
+ if tracker and subprocess_event_id:
979
+ try:
980
+ tracker.complete_tool_call(
981
+ event_id=subprocess_event_id,
982
+ output_summary=result.stdout[:500] if result.stdout else "",
983
+ success=result.returncode == 0,
984
+ )
985
+ except Exception:
986
+ # Tracking failure should not break execution
987
+ pass
988
+
878
989
  # Publish live event: processing
879
990
  self._publish_live_event(
880
991
  "spawner_phase",
@@ -1035,6 +1146,8 @@ class HeadlessSpawner:
1035
1146
  deny_tools: list[str] | None = None,
1036
1147
  track_in_htmlgraph: bool = True,
1037
1148
  timeout: int = 120,
1149
+ tracker: Any = None,
1150
+ parent_event_id: str | None = None,
1038
1151
  ) -> AIResult:
1039
1152
  """
1040
1153
  Spawn GitHub Copilot in headless mode.
@@ -1046,6 +1159,8 @@ class HeadlessSpawner:
1046
1159
  deny_tools: Tools to deny (--deny-tool). Default: None
1047
1160
  track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
1048
1161
  timeout: Max seconds to wait
1162
+ tracker: Optional SpawnerEventTracker for recording subprocess invocation
1163
+ parent_event_id: Optional parent event ID for event hierarchy
1049
1164
 
1050
1165
  Returns:
1051
1166
  AIResult with response, error, and tracked events if tracking enabled
@@ -1101,6 +1216,45 @@ class HeadlessSpawner:
1101
1216
  details="Running Copilot CLI",
1102
1217
  )
1103
1218
 
1219
+ # Record subprocess invocation if tracker is available
1220
+ subprocess_event_id = None
1221
+ print(
1222
+ f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
1223
+ file=sys.stderr,
1224
+ )
1225
+ if tracker and parent_event_id:
1226
+ print(
1227
+ "DEBUG: Recording subprocess invocation for Copilot...",
1228
+ file=sys.stderr,
1229
+ )
1230
+ try:
1231
+ subprocess_event = tracker.record_tool_call(
1232
+ tool_name="subprocess.copilot",
1233
+ tool_input={"cmd": cmd},
1234
+ phase_event_id=parent_event_id,
1235
+ spawned_agent="github-copilot",
1236
+ )
1237
+ if subprocess_event:
1238
+ subprocess_event_id = subprocess_event.get("event_id")
1239
+ print(
1240
+ f"DEBUG: Subprocess event created for Copilot: {subprocess_event_id}",
1241
+ file=sys.stderr,
1242
+ )
1243
+ else:
1244
+ print("DEBUG: subprocess_event was None", file=sys.stderr)
1245
+ except Exception as e:
1246
+ # Tracking failure should not break execution
1247
+ print(
1248
+ f"DEBUG: Exception recording Copilot subprocess: {e}",
1249
+ file=sys.stderr,
1250
+ )
1251
+ pass
1252
+ else:
1253
+ print(
1254
+ f"DEBUG: Skipping Copilot subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
1255
+ file=sys.stderr,
1256
+ )
1257
+
1104
1258
  result = subprocess.run(
1105
1259
  cmd,
1106
1260
  capture_output=True,
@@ -1108,6 +1262,18 @@ class HeadlessSpawner:
1108
1262
  timeout=timeout,
1109
1263
  )
1110
1264
 
1265
+ # Complete subprocess invocation tracking
1266
+ if tracker and subprocess_event_id:
1267
+ try:
1268
+ tracker.complete_tool_call(
1269
+ event_id=subprocess_event_id,
1270
+ output_summary=result.stdout[:500] if result.stdout else "",
1271
+ success=result.returncode == 0,
1272
+ )
1273
+ except Exception:
1274
+ # Tracking failure should not break execution
1275
+ pass
1276
+
1111
1277
  # Publish live event: processing
1112
1278
  self._publish_live_event(
1113
1279
  "spawner_phase",