htmlgraph 0.26.8__py3-none-any.whl → 0.26.10__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.
- htmlgraph/__init__.py +1 -1
- htmlgraph/hooks/event_tracker.py +103 -348
- htmlgraph/hooks/subagent_stop.py +12 -71
- {htmlgraph-0.26.8.dist-info → htmlgraph-0.26.10.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.8.dist-info → htmlgraph-0.26.10.dist-info}/RECORD +12 -12
- {htmlgraph-0.26.8.data → htmlgraph-0.26.10.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.8.data → htmlgraph-0.26.10.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.8.data → htmlgraph-0.26.10.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.8.data → htmlgraph-0.26.10.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.8.data → htmlgraph-0.26.10.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.8.dist-info → htmlgraph-0.26.10.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.8.dist-info → htmlgraph-0.26.10.dist-info}/entry_points.txt +0 -0
htmlgraph/__init__.py
CHANGED
htmlgraph/hooks/event_tracker.py
CHANGED
|
@@ -353,7 +353,6 @@ def detect_model_from_hook_input(hook_input: dict[str, Any]) -> str | None:
|
|
|
353
353
|
1. Task() model parameter (if tool_name == 'Task')
|
|
354
354
|
2. HTMLGRAPH_MODEL environment variable (set by hooks)
|
|
355
355
|
3. ANTHROPIC_MODEL or CLAUDE_MODEL environment variables
|
|
356
|
-
4. Status line cache (for orchestrator tool calls)
|
|
357
356
|
|
|
358
357
|
Args:
|
|
359
358
|
hook_input: Hook input dict containing tool_name and tool_input
|
|
@@ -387,14 +386,6 @@ def detect_model_from_hook_input(hook_input: dict[str, Any]) -> str | None:
|
|
|
387
386
|
if model:
|
|
388
387
|
return model
|
|
389
388
|
|
|
390
|
-
# 3. Fallback to status line cache for orchestrator's own tool calls
|
|
391
|
-
# This gives regular tool calls (Bash, Read, etc.) the model of the orchestrator
|
|
392
|
-
session_id = hook_input.get("session_id") or hook_input.get("sessionId")
|
|
393
|
-
if session_id:
|
|
394
|
-
model_from_cache = get_model_from_status_cache(session_id)
|
|
395
|
-
if model_from_cache:
|
|
396
|
-
return model_from_cache
|
|
397
|
-
|
|
398
389
|
return None
|
|
399
390
|
|
|
400
391
|
|
|
@@ -455,179 +446,6 @@ def detect_agent_from_environment() -> tuple[str, str | None]:
|
|
|
455
446
|
return agent_id, model_name
|
|
456
447
|
|
|
457
448
|
|
|
458
|
-
def detect_subagent_context_from_database(
|
|
459
|
-
db: HtmlGraphDB,
|
|
460
|
-
current_session_id: str,
|
|
461
|
-
parent_event_id_hint: str | None = None,
|
|
462
|
-
current_tool_name: str | None = None,
|
|
463
|
-
) -> tuple[str | None, str | None, str | None]:
|
|
464
|
-
"""
|
|
465
|
-
Detect if we're in a subagent context by checking for active task_delegation events.
|
|
466
|
-
|
|
467
|
-
This is the DATABASE-BASED approach to subagent detection, which is necessary because
|
|
468
|
-
environment variables set by PreToolUse hooks in the parent process do NOT propagate
|
|
469
|
-
to subagent processes spawned by Claude Code's Task() tool.
|
|
470
|
-
|
|
471
|
-
IMPORTANT CONTEXT:
|
|
472
|
-
- Claude Code passes the SAME session_id to both parent and subagent hooks
|
|
473
|
-
- Environment variables set in hooks don't persist (each hook is a new subprocess)
|
|
474
|
-
- The only way to detect subagent context is through database state
|
|
475
|
-
|
|
476
|
-
DETECTION STRATEGY:
|
|
477
|
-
- If there's an active task_delegation (status='started') within the time window,
|
|
478
|
-
AND the current tool is NOT the Task tool itself (to avoid self-detection),
|
|
479
|
-
then we're likely in a subagent context.
|
|
480
|
-
- The Task tool check is critical: when PostToolUse fires for the Task tool itself,
|
|
481
|
-
we should NOT consider ourselves in a subagent context - we're the orchestrator
|
|
482
|
-
that just finished delegating.
|
|
483
|
-
|
|
484
|
-
Strategy (in order of precedence):
|
|
485
|
-
1. If parent_event_id_hint is provided, look up that specific event directly
|
|
486
|
-
2. Otherwise, query for task_delegation events with status='started' (fallback)
|
|
487
|
-
3. If found within the last 5 minutes AND current tool is not Task, we're in subagent context
|
|
488
|
-
4. Return the subagent_type and parent session info
|
|
489
|
-
|
|
490
|
-
Args:
|
|
491
|
-
db: HtmlGraphDB instance
|
|
492
|
-
current_session_id: The session_id from hook_input (Claude Code's session ID)
|
|
493
|
-
parent_event_id_hint: Optional event_id from environment variable for direct lookup.
|
|
494
|
-
This is the preferred method when available, as it correctly
|
|
495
|
-
handles parallel Task() calls.
|
|
496
|
-
current_tool_name: The tool being executed. If "Task", we skip subagent detection
|
|
497
|
-
to avoid the orchestrator thinking it's a subagent.
|
|
498
|
-
|
|
499
|
-
Returns:
|
|
500
|
-
Tuple of (subagent_type, parent_session_id, parent_event_id)
|
|
501
|
-
All None if not in subagent context
|
|
502
|
-
"""
|
|
503
|
-
# Skip detection if the current tool is Task - we're the orchestrator, not a subagent
|
|
504
|
-
if current_tool_name == "Task":
|
|
505
|
-
print(
|
|
506
|
-
"DEBUG detect_subagent_context_from_database: "
|
|
507
|
-
"Skipping detection for Task tool (we're the orchestrator)",
|
|
508
|
-
file=sys.stderr,
|
|
509
|
-
)
|
|
510
|
-
return None, None, None
|
|
511
|
-
try:
|
|
512
|
-
if db.connection is None:
|
|
513
|
-
return None, None, None
|
|
514
|
-
|
|
515
|
-
cursor = db.connection.cursor()
|
|
516
|
-
|
|
517
|
-
# Priority 1: Direct lookup using parent_event_id_hint (handles parallel tasks correctly)
|
|
518
|
-
if parent_event_id_hint:
|
|
519
|
-
cursor.execute(
|
|
520
|
-
"""
|
|
521
|
-
SELECT event_id, session_id, subagent_type, timestamp
|
|
522
|
-
FROM agent_events
|
|
523
|
-
WHERE event_id = ?
|
|
524
|
-
AND event_type = 'task_delegation'
|
|
525
|
-
AND status = 'started'
|
|
526
|
-
""",
|
|
527
|
-
(parent_event_id_hint,),
|
|
528
|
-
)
|
|
529
|
-
row = cursor.fetchone()
|
|
530
|
-
|
|
531
|
-
if row:
|
|
532
|
-
parent_event_id = row[0]
|
|
533
|
-
parent_session_id = row[1]
|
|
534
|
-
subagent_type = row[2] or "general-purpose"
|
|
535
|
-
|
|
536
|
-
print(
|
|
537
|
-
f"Debug: Detected subagent context via hint: "
|
|
538
|
-
f"type={subagent_type}, parent_session={parent_session_id}, "
|
|
539
|
-
f"parent_event={parent_event_id}",
|
|
540
|
-
file=sys.stderr,
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
return subagent_type, parent_session_id, parent_event_id
|
|
544
|
-
|
|
545
|
-
# Hint provided but event not found or not in 'started' status
|
|
546
|
-
# This can happen if the event was already completed
|
|
547
|
-
print(
|
|
548
|
-
f"Debug: Parent event hint '{parent_event_id_hint}' not found or not active",
|
|
549
|
-
file=sys.stderr,
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
# Priority 2: Fallback to most recent active task_delegation
|
|
553
|
-
# WARNING: This can pick the wrong parent when multiple Task() calls run in parallel!
|
|
554
|
-
# This is kept as a fallback for cases where environment variables don't propagate.
|
|
555
|
-
#
|
|
556
|
-
# IMPORTANT: We previously had `AND session_id != ?` to exclude the current session,
|
|
557
|
-
# but this was WRONG. Claude Code passes the SAME session_id to subagent hooks as
|
|
558
|
-
# to the parent session. So we need to find task_delegation events from the SAME
|
|
559
|
-
# session that are in 'started' status (not yet completed).
|
|
560
|
-
#
|
|
561
|
-
# The key insight: when a subagent runs, the task_delegation event from the parent
|
|
562
|
-
# is ALREADY in the database with status='started'. We just need to find it.
|
|
563
|
-
print(
|
|
564
|
-
f"DEBUG detect_subagent_context_from_database: "
|
|
565
|
-
f"Querying for task_delegation events in session {current_session_id}",
|
|
566
|
-
file=sys.stderr,
|
|
567
|
-
)
|
|
568
|
-
|
|
569
|
-
# First, let's see what task_delegation events exist at all (debug)
|
|
570
|
-
cursor.execute(
|
|
571
|
-
"""
|
|
572
|
-
SELECT event_id, session_id, subagent_type, status, timestamp
|
|
573
|
-
FROM agent_events
|
|
574
|
-
WHERE event_type = 'task_delegation'
|
|
575
|
-
ORDER BY timestamp DESC
|
|
576
|
-
LIMIT 5
|
|
577
|
-
"""
|
|
578
|
-
)
|
|
579
|
-
debug_rows = cursor.fetchall()
|
|
580
|
-
print(
|
|
581
|
-
f"DEBUG detect_subagent_context_from_database: "
|
|
582
|
-
f"Recent task_delegation events: {debug_rows}",
|
|
583
|
-
file=sys.stderr,
|
|
584
|
-
)
|
|
585
|
-
|
|
586
|
-
# Query for active task_delegation in the SAME session (or any session within time window)
|
|
587
|
-
# The subagent may have the same session_id as parent, so we look for ANY active
|
|
588
|
-
# task_delegation within the time window. The most recent one is likely our parent.
|
|
589
|
-
cursor.execute(
|
|
590
|
-
"""
|
|
591
|
-
SELECT event_id, session_id, subagent_type, timestamp
|
|
592
|
-
FROM agent_events
|
|
593
|
-
WHERE event_type = 'task_delegation'
|
|
594
|
-
AND status = 'started'
|
|
595
|
-
AND timestamp >= datetime('now', '-5 minutes')
|
|
596
|
-
ORDER BY timestamp DESC
|
|
597
|
-
LIMIT 1
|
|
598
|
-
"""
|
|
599
|
-
)
|
|
600
|
-
row = cursor.fetchone()
|
|
601
|
-
print(
|
|
602
|
-
f"DEBUG detect_subagent_context_from_database: "
|
|
603
|
-
f"Fallback query result: {row}",
|
|
604
|
-
file=sys.stderr,
|
|
605
|
-
)
|
|
606
|
-
|
|
607
|
-
if row:
|
|
608
|
-
parent_event_id = row[0]
|
|
609
|
-
parent_session_id = row[1]
|
|
610
|
-
subagent_type = row[2] or "general-purpose"
|
|
611
|
-
|
|
612
|
-
print(
|
|
613
|
-
f"Debug: Detected subagent context from database (fallback): "
|
|
614
|
-
f"type={subagent_type}, parent_session={parent_session_id}, "
|
|
615
|
-
f"parent_event={parent_event_id}",
|
|
616
|
-
file=sys.stderr,
|
|
617
|
-
)
|
|
618
|
-
|
|
619
|
-
return subagent_type, parent_session_id, parent_event_id
|
|
620
|
-
|
|
621
|
-
return None, None, None
|
|
622
|
-
|
|
623
|
-
except Exception as e:
|
|
624
|
-
print(
|
|
625
|
-
f"Debug: Error detecting subagent context from database: {e}",
|
|
626
|
-
file=sys.stderr,
|
|
627
|
-
)
|
|
628
|
-
return None, None, None
|
|
629
|
-
|
|
630
|
-
|
|
631
449
|
def extract_file_paths(tool_input: dict[str, Any], tool_name: str) -> list[str]:
|
|
632
450
|
"""Extract file paths from tool input based on tool type."""
|
|
633
451
|
paths = []
|
|
@@ -862,24 +680,6 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
862
680
|
Returns:
|
|
863
681
|
Response dict with {"continue": True} and optional hookSpecificOutput
|
|
864
682
|
"""
|
|
865
|
-
# Check for debug mode (set HTMLGRAPH_DEBUG=1 to enable verbose logging)
|
|
866
|
-
debug_mode = os.environ.get("HTMLGRAPH_DEBUG") == "1"
|
|
867
|
-
|
|
868
|
-
if debug_mode:
|
|
869
|
-
print(
|
|
870
|
-
f"DEBUG track_event: hook_type={hook_type}, "
|
|
871
|
-
f"session_id={hook_input.get('session_id')}, "
|
|
872
|
-
f"tool_name={hook_input.get('tool_name', hook_input.get('name', 'unknown'))}",
|
|
873
|
-
file=sys.stderr,
|
|
874
|
-
)
|
|
875
|
-
print(
|
|
876
|
-
f"DEBUG track_event: ENV HTMLGRAPH_PARENT_EVENT="
|
|
877
|
-
f"{os.environ.get('HTMLGRAPH_PARENT_EVENT')}, "
|
|
878
|
-
f"HTMLGRAPH_SUBAGENT_TYPE={os.environ.get('HTMLGRAPH_SUBAGENT_TYPE')}, "
|
|
879
|
-
f"HTMLGRAPH_PARENT_SESSION={os.environ.get('HTMLGRAPH_PARENT_SESSION')}",
|
|
880
|
-
file=sys.stderr,
|
|
881
|
-
)
|
|
882
|
-
|
|
883
683
|
cwd = hook_input.get("cwd")
|
|
884
684
|
project_dir = resolve_project_path(cwd if cwd else None)
|
|
885
685
|
graph_dir = Path(project_dir) / ".htmlgraph"
|
|
@@ -913,115 +713,106 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
913
713
|
detected_model = model_from_input
|
|
914
714
|
|
|
915
715
|
active_session = None
|
|
916
|
-
is_subagent_session = False
|
|
917
|
-
parent_event_id_for_session = None
|
|
918
|
-
|
|
919
|
-
# Get session_id from hook_input first (Claude Code provides this)
|
|
920
|
-
hook_session_id = hook_input.get("session_id") or hook_input.get("sessionId")
|
|
921
716
|
|
|
922
717
|
# Check if we're in a subagent context using multiple methods:
|
|
923
718
|
#
|
|
924
|
-
# PRECEDENCE ORDER
|
|
925
|
-
# 1.
|
|
926
|
-
#
|
|
927
|
-
# 2.
|
|
928
|
-
# 3.
|
|
719
|
+
# PRECEDENCE ORDER:
|
|
720
|
+
# 1. Sessions table - if THIS session is already marked as subagent, use stored parent info
|
|
721
|
+
# (fixes persistence issue for subsequent tool calls in same subagent)
|
|
722
|
+
# 2. Environment variables - set by spawner router for first tool call
|
|
723
|
+
# 3. Fallback to normal orchestrator context
|
|
929
724
|
#
|
|
930
|
-
# Method 1:
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
print(
|
|
937
|
-
f"DEBUG subagent detection: "
|
|
938
|
-
f"env subagent_type={subagent_type}, "
|
|
939
|
-
f"env parent_session_id={parent_session_id}, "
|
|
940
|
-
f"env parent_event_id={env_parent_event_id}",
|
|
941
|
-
file=sys.stderr,
|
|
942
|
-
)
|
|
943
|
-
|
|
944
|
-
# If we have parent event from environment, use it directly
|
|
945
|
-
if env_parent_event_id:
|
|
946
|
-
parent_event_id_for_session = env_parent_event_id
|
|
947
|
-
print(
|
|
948
|
-
f"Debug: Using environment variable for parent event: {env_parent_event_id}",
|
|
949
|
-
file=sys.stderr,
|
|
950
|
-
)
|
|
725
|
+
# Method 1: Check if current session is already a subagent (CRITICAL for persistence!)
|
|
726
|
+
# This fixes the issue where subsequent tool calls in the same subagent session
|
|
727
|
+
# lose the parent_event_id linkage.
|
|
728
|
+
subagent_type = None
|
|
729
|
+
parent_session_id = None
|
|
730
|
+
hook_session_id = hook_input.get("session_id") or hook_input.get("sessionId")
|
|
951
731
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
file=sys.stderr,
|
|
964
|
-
)
|
|
965
|
-
if not subagent_type and db and hook_session_id:
|
|
966
|
-
print(
|
|
967
|
-
f"DEBUG db detection: calling detect_subagent_context_from_database "
|
|
968
|
-
f"with current_session_id={hook_session_id}, hint={env_parent_event_id}, "
|
|
969
|
-
f"current_tool_name={current_tool_name}",
|
|
970
|
-
file=sys.stderr,
|
|
971
|
-
)
|
|
972
|
-
db_subagent_type, db_parent_session_id, db_parent_event_id = (
|
|
973
|
-
detect_subagent_context_from_database(
|
|
974
|
-
db,
|
|
975
|
-
hook_session_id,
|
|
976
|
-
parent_event_id_hint=env_parent_event_id,
|
|
977
|
-
current_tool_name=current_tool_name,
|
|
732
|
+
if db and db.connection and hook_session_id:
|
|
733
|
+
try:
|
|
734
|
+
cursor = db.connection.cursor()
|
|
735
|
+
cursor.execute(
|
|
736
|
+
"""
|
|
737
|
+
SELECT parent_session_id, agent_assigned
|
|
738
|
+
FROM sessions
|
|
739
|
+
WHERE session_id = ? AND is_subagent = 1
|
|
740
|
+
LIMIT 1
|
|
741
|
+
""",
|
|
742
|
+
(hook_session_id,),
|
|
978
743
|
)
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
744
|
+
row = cursor.fetchone()
|
|
745
|
+
if row:
|
|
746
|
+
parent_session_id = row[0]
|
|
747
|
+
# Extract subagent_type from agent_assigned (e.g., "general-purpose-spawner" -> "general-purpose")
|
|
748
|
+
agent_assigned = row[1] or ""
|
|
749
|
+
if agent_assigned and agent_assigned.endswith("-spawner"):
|
|
750
|
+
subagent_type = agent_assigned[:-8] # Remove "-spawner" suffix
|
|
751
|
+
else:
|
|
752
|
+
subagent_type = "general-purpose" # Default if format unexpected
|
|
753
|
+
|
|
754
|
+
print(
|
|
755
|
+
f"DEBUG subagent persistence: Found current session as subagent in sessions table: "
|
|
756
|
+
f"type={subagent_type}, parent_session={parent_session_id}",
|
|
757
|
+
file=sys.stderr,
|
|
758
|
+
)
|
|
759
|
+
except Exception as e:
|
|
993
760
|
print(
|
|
994
|
-
f"
|
|
995
|
-
f"type={subagent_type}, parent={parent_session_id}, "
|
|
996
|
-
f"parent_event={parent_event_id_for_session}",
|
|
761
|
+
f"DEBUG: Error checking sessions table for subagent: {e}",
|
|
997
762
|
file=sys.stderr,
|
|
998
763
|
)
|
|
999
764
|
|
|
1000
|
-
#
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
)
|
|
765
|
+
# Method 2: Environment variables (for first tool call before session table is populated)
|
|
766
|
+
if not subagent_type:
|
|
767
|
+
subagent_type = os.environ.get("HTMLGRAPH_SUBAGENT_TYPE")
|
|
768
|
+
parent_session_id = os.environ.get("HTMLGRAPH_PARENT_SESSION")
|
|
769
|
+
|
|
770
|
+
# Method 3: Database detection of active task_delegation events
|
|
771
|
+
# CRITICAL: When Task() subprocess is launched, environment variables don't propagate
|
|
772
|
+
# So we must query the database for active task_delegation events to detect subagent context
|
|
773
|
+
# NOTE: Claude Code passes the SAME session_id to parent and subagent, so we CAN'T use
|
|
774
|
+
# session_id to distinguish them. Instead, look for the most recent task_delegation event
|
|
775
|
+
# and if found with status='started', we ARE the subagent.
|
|
776
|
+
if not subagent_type and db and db.connection:
|
|
777
|
+
try:
|
|
778
|
+
cursor = db.connection.cursor()
|
|
779
|
+
# Find the most recent active task_delegation event
|
|
780
|
+
cursor.execute(
|
|
781
|
+
"""
|
|
782
|
+
SELECT event_id, subagent_type, session_id
|
|
783
|
+
FROM agent_events
|
|
784
|
+
WHERE event_type = 'task_delegation'
|
|
785
|
+
AND status = 'started'
|
|
786
|
+
AND tool_name = 'Task'
|
|
787
|
+
ORDER BY timestamp DESC
|
|
788
|
+
LIMIT 1
|
|
789
|
+
""",
|
|
790
|
+
)
|
|
791
|
+
row = cursor.fetchone()
|
|
792
|
+
if row:
|
|
793
|
+
task_event_id, detected_subagent_type, parent_sess = row
|
|
794
|
+
# If we found an active task_delegation, we're running as a subagent
|
|
795
|
+
# (Claude Code uses the same session_id for both parent and subagent)
|
|
796
|
+
subagent_type = detected_subagent_type or "general-purpose"
|
|
797
|
+
parent_session_id = parent_sess
|
|
798
|
+
print(
|
|
799
|
+
f"DEBUG subagent detection (database): Detected active task_delegation "
|
|
800
|
+
f"type={subagent_type}, parent_session={parent_session_id}, "
|
|
801
|
+
f"parent_event={task_event_id}",
|
|
802
|
+
file=sys.stderr,
|
|
803
|
+
)
|
|
804
|
+
except Exception as e:
|
|
805
|
+
print(
|
|
806
|
+
f"DEBUG: Error detecting subagent from database: {e}",
|
|
807
|
+
file=sys.stderr,
|
|
808
|
+
)
|
|
1007
809
|
|
|
1008
810
|
if subagent_type and parent_session_id:
|
|
1009
|
-
# We're in a subagent
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
"DEBUG: SUBAGENT CONTEXT DETECTED! Creating subagent session...",
|
|
1013
|
-
file=sys.stderr,
|
|
1014
|
-
)
|
|
811
|
+
# We're in a subagent - create or get subagent session
|
|
812
|
+
# Use deterministic session ID based on parent + subagent type
|
|
813
|
+
subagent_session_id = f"{parent_session_id}-{subagent_type}"
|
|
1015
814
|
|
|
1016
|
-
#
|
|
1017
|
-
# This ensures events are properly tracked to this session
|
|
1018
|
-
subagent_session_id = hook_session_id or f"{parent_session_id}-{subagent_type}"
|
|
1019
|
-
print(
|
|
1020
|
-
f"DEBUG: subagent_session_id={subagent_session_id}",
|
|
1021
|
-
file=sys.stderr,
|
|
1022
|
-
)
|
|
1023
|
-
|
|
1024
|
-
# Check if session already exists in our system
|
|
815
|
+
# Check if subagent session already exists
|
|
1025
816
|
existing = manager.session_converter.load(subagent_session_id)
|
|
1026
817
|
if existing:
|
|
1027
818
|
active_session = existing
|
|
@@ -1032,11 +823,6 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1032
823
|
else:
|
|
1033
824
|
# Create new subagent session with parent link
|
|
1034
825
|
try:
|
|
1035
|
-
print(
|
|
1036
|
-
f"DEBUG: Creating NEW subagent session with is_subagent=True, "
|
|
1037
|
-
f"parent_session_id={parent_session_id}",
|
|
1038
|
-
file=sys.stderr,
|
|
1039
|
-
)
|
|
1040
826
|
active_session = manager.start_session(
|
|
1041
827
|
session_id=subagent_session_id,
|
|
1042
828
|
agent=f"{subagent_type}-spawner",
|
|
@@ -1046,7 +832,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1046
832
|
)
|
|
1047
833
|
print(
|
|
1048
834
|
f"Debug: Created subagent session: {subagent_session_id} "
|
|
1049
|
-
f"(parent: {parent_session_id}
|
|
835
|
+
f"(parent: {parent_session_id})",
|
|
1050
836
|
file=sys.stderr,
|
|
1051
837
|
)
|
|
1052
838
|
except Exception as e:
|
|
@@ -1062,6 +848,8 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1062
848
|
# Normal orchestrator/parent context
|
|
1063
849
|
# CRITICAL: Use session_id from hook_input (Claude Code provides this)
|
|
1064
850
|
# Only fall back to manager.get_active_session() if not in hook_input
|
|
851
|
+
# hook_session_id already defined at line 730
|
|
852
|
+
|
|
1065
853
|
if hook_session_id:
|
|
1066
854
|
# Claude Code provided session_id - use it directly
|
|
1067
855
|
# Check if session already exists
|
|
@@ -1110,13 +898,10 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1110
898
|
except Exception:
|
|
1111
899
|
return default
|
|
1112
900
|
|
|
1113
|
-
# Use is_subagent_session flag from our detection, not just from session object
|
|
1114
901
|
is_subagent_raw = safe_getattr(active_session, "is_subagent", False)
|
|
1115
|
-
|
|
902
|
+
is_subagent = (
|
|
1116
903
|
bool(is_subagent_raw) if isinstance(is_subagent_raw, bool) else False
|
|
1117
904
|
)
|
|
1118
|
-
# Prefer our detection (is_subagent_session) over object attribute
|
|
1119
|
-
final_is_subagent = is_subagent_session or is_subagent_from_obj
|
|
1120
905
|
|
|
1121
906
|
transcript_id = safe_getattr(active_session, "transcript_id", None)
|
|
1122
907
|
transcript_path = safe_getattr(active_session, "transcript_path", None)
|
|
@@ -1126,35 +911,14 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1126
911
|
if transcript_path is not None and not isinstance(transcript_path, str):
|
|
1127
912
|
transcript_path = None
|
|
1128
913
|
|
|
1129
|
-
# Get parent_session_id from our detection or from session object
|
|
1130
|
-
final_parent_session_id = parent_session_id or safe_getattr(
|
|
1131
|
-
active_session, "parent_session_id", None
|
|
1132
|
-
)
|
|
1133
|
-
if final_parent_session_id is not None and not isinstance(
|
|
1134
|
-
final_parent_session_id, str
|
|
1135
|
-
):
|
|
1136
|
-
final_parent_session_id = None
|
|
1137
|
-
|
|
1138
914
|
db.insert_session(
|
|
1139
915
|
session_id=active_session_id,
|
|
1140
916
|
agent_assigned=safe_getattr(active_session, "agent", None)
|
|
1141
917
|
or detected_agent,
|
|
1142
|
-
|
|
1143
|
-
parent_event_id=parent_event_id_for_session,
|
|
1144
|
-
is_subagent=final_is_subagent,
|
|
918
|
+
is_subagent=is_subagent,
|
|
1145
919
|
transcript_id=transcript_id,
|
|
1146
920
|
transcript_path=transcript_path,
|
|
1147
921
|
)
|
|
1148
|
-
|
|
1149
|
-
# Log subagent session creation for debugging
|
|
1150
|
-
if final_is_subagent:
|
|
1151
|
-
print(
|
|
1152
|
-
f"Debug: Inserted subagent session to SQLite: "
|
|
1153
|
-
f"session_id={active_session_id}, is_subagent=True, "
|
|
1154
|
-
f"parent_session={final_parent_session_id}, "
|
|
1155
|
-
f"parent_event={parent_event_id_for_session}",
|
|
1156
|
-
file=sys.stderr,
|
|
1157
|
-
)
|
|
1158
922
|
except Exception as e:
|
|
1159
923
|
# Session may already exist, that's OK - continue
|
|
1160
924
|
print(
|
|
@@ -1265,29 +1029,20 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1265
1029
|
warning_threshold = drift_settings.get("warning_threshold") or 0.7
|
|
1266
1030
|
auto_classify_threshold = drift_settings.get("auto_classify_threshold") or 0.85
|
|
1267
1031
|
|
|
1268
|
-
# Determine parent activity context using
|
|
1032
|
+
# Determine parent activity context using database-only lookup
|
|
1269
1033
|
parent_activity_id = None
|
|
1270
1034
|
|
|
1271
|
-
#
|
|
1272
|
-
# the task_delegation that spawned us (detected from database)
|
|
1273
|
-
if is_subagent_session and parent_event_id_for_session:
|
|
1274
|
-
parent_activity_id = parent_event_id_for_session
|
|
1275
|
-
print(
|
|
1276
|
-
f"Debug: Using parent_event from subagent detection: {parent_activity_id}",
|
|
1277
|
-
file=sys.stderr,
|
|
1278
|
-
)
|
|
1279
|
-
# Priority 2: Check environment variable for cross-process parent linking
|
|
1035
|
+
# Check environment variable FIRST for cross-process parent linking
|
|
1280
1036
|
# This is set by PreToolUse hook when Task() spawns a subagent
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
parent_activity_id = get_parent_user_query(db, active_session_id)
|
|
1037
|
+
env_parent = os.environ.get("HTMLGRAPH_PARENT_EVENT") or os.environ.get(
|
|
1038
|
+
"HTMLGRAPH_PARENT_QUERY_EVENT"
|
|
1039
|
+
)
|
|
1040
|
+
if env_parent:
|
|
1041
|
+
parent_activity_id = env_parent
|
|
1042
|
+
# Query database for most recent UserQuery event as parent
|
|
1043
|
+
# Database is the single source of truth for parent-child linking
|
|
1044
|
+
elif db:
|
|
1045
|
+
parent_activity_id = get_parent_user_query(db, active_session_id)
|
|
1291
1046
|
|
|
1292
1047
|
# Track the activity
|
|
1293
1048
|
nudge = None
|
htmlgraph/hooks/subagent_stop.py
CHANGED
|
@@ -35,61 +35,12 @@ def get_parent_event_id() -> str | None:
|
|
|
35
35
|
|
|
36
36
|
Set by PreToolUse hook when Task() is detected.
|
|
37
37
|
|
|
38
|
-
NOTE: This relies on environment variables which DON'T persist between
|
|
39
|
-
hook invocations (each hook is a new subprocess). This function is kept
|
|
40
|
-
for backward compatibility but will almost always return None.
|
|
41
|
-
Use get_parent_event_id_from_database() instead.
|
|
42
|
-
|
|
43
38
|
Returns:
|
|
44
39
|
Parent event ID (evt-XXXXX) or None if not found
|
|
45
40
|
"""
|
|
46
41
|
return os.environ.get("HTMLGRAPH_PARENT_EVENT")
|
|
47
42
|
|
|
48
43
|
|
|
49
|
-
def get_parent_event_id_from_database(db_path: str) -> tuple[str | None, str | None]:
|
|
50
|
-
"""
|
|
51
|
-
Get the parent event ID by querying the database for active task_delegation events.
|
|
52
|
-
|
|
53
|
-
This is the reliable method for finding parent events since environment variables
|
|
54
|
-
don't persist between hook invocations.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
db_path: Path to SQLite database
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
Tuple of (parent_event_id, parent_start_time) or (None, None) if not found
|
|
61
|
-
"""
|
|
62
|
-
try:
|
|
63
|
-
conn = sqlite3.connect(db_path)
|
|
64
|
-
cursor = conn.cursor()
|
|
65
|
-
|
|
66
|
-
# Find the most recent task_delegation event that's still 'started'
|
|
67
|
-
cursor.execute(
|
|
68
|
-
"""
|
|
69
|
-
SELECT event_id, timestamp
|
|
70
|
-
FROM agent_events
|
|
71
|
-
WHERE event_type = 'task_delegation'
|
|
72
|
-
AND status = 'started'
|
|
73
|
-
AND timestamp >= datetime('now', '-10 minutes')
|
|
74
|
-
ORDER BY timestamp DESC
|
|
75
|
-
LIMIT 1
|
|
76
|
-
"""
|
|
77
|
-
)
|
|
78
|
-
row = cursor.fetchone()
|
|
79
|
-
conn.close()
|
|
80
|
-
|
|
81
|
-
if row:
|
|
82
|
-
logger.debug(f"Found active task_delegation from database: {row[0]}")
|
|
83
|
-
return row[0], row[1]
|
|
84
|
-
|
|
85
|
-
logger.debug("No active task_delegation found in database")
|
|
86
|
-
return None, None
|
|
87
|
-
|
|
88
|
-
except Exception as e:
|
|
89
|
-
logger.warning(f"Error querying database for parent event: {e}")
|
|
90
|
-
return None, None
|
|
91
|
-
|
|
92
|
-
|
|
93
44
|
def get_session_id() -> str | None:
|
|
94
45
|
"""
|
|
95
46
|
Get the current session ID from environment.
|
|
@@ -271,7 +222,14 @@ def handle_subagent_stop(hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
271
222
|
Returns:
|
|
272
223
|
Response: {"continue": True} with optional context
|
|
273
224
|
"""
|
|
274
|
-
# Get
|
|
225
|
+
# Get parent event ID from environment
|
|
226
|
+
parent_event_id = get_parent_event_id()
|
|
227
|
+
|
|
228
|
+
if not parent_event_id:
|
|
229
|
+
logger.debug("No parent event ID found, skipping subagent stop tracking")
|
|
230
|
+
return {"continue": True}
|
|
231
|
+
|
|
232
|
+
# Get project directory and database path
|
|
275
233
|
try:
|
|
276
234
|
from htmlgraph.config import get_database_path
|
|
277
235
|
|
|
@@ -286,28 +244,11 @@ def handle_subagent_stop(hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
286
244
|
logger.warning(f"Error resolving database path: {e}")
|
|
287
245
|
return {"continue": True}
|
|
288
246
|
|
|
289
|
-
#
|
|
290
|
-
|
|
291
|
-
parent_start_time = None
|
|
292
|
-
|
|
293
|
-
# Fall back to database-based detection (the reliable method)
|
|
294
|
-
if not parent_event_id:
|
|
295
|
-
parent_event_id, parent_start_time = get_parent_event_id_from_database(db_path)
|
|
296
|
-
|
|
297
|
-
if not parent_event_id:
|
|
298
|
-
logger.debug(
|
|
299
|
-
"No parent event ID found (env or database), skipping subagent stop tracking"
|
|
300
|
-
)
|
|
301
|
-
return {"continue": True}
|
|
302
|
-
|
|
303
|
-
logger.info(f"SubagentStop: Found parent event {parent_event_id}")
|
|
304
|
-
|
|
305
|
-
# Get parent event start time if not already retrieved from database
|
|
247
|
+
# Get parent event start time
|
|
248
|
+
parent_start_time = get_parent_event_start_time(db_path, parent_event_id)
|
|
306
249
|
if not parent_start_time:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
logger.warning(f"Could not find parent event start time: {parent_event_id}")
|
|
310
|
-
return {"continue": True}
|
|
250
|
+
logger.warning(f"Could not find parent event: {parent_event_id}")
|
|
251
|
+
return {"continue": True}
|
|
311
252
|
|
|
312
253
|
# Count child spikes
|
|
313
254
|
child_spike_count = count_child_spikes(db_path, parent_event_id, parent_start_time)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: htmlgraph
|
|
3
|
-
Version: 0.26.
|
|
3
|
+
Version: 0.26.10
|
|
4
4
|
Summary: HTML is All You Need - Graph database on web standards
|
|
5
5
|
Project-URL: Homepage, https://github.com/Shakes-tzd/htmlgraph
|
|
6
6
|
Project-URL: Documentation, https://github.com/Shakes-tzd/htmlgraph#readme
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
htmlgraph/__init__.py,sha256=
|
|
1
|
+
htmlgraph/__init__.py,sha256=MDmhPlkaw8qf7dHbBJgy5oLU7eQntoSC5abH-qy6Dbo,5718
|
|
2
2
|
htmlgraph/agent_detection.py,sha256=wEmrDv4hssPX2OkEnJZBHPbalxcaloiJF_hOOow_5WE,3511
|
|
3
3
|
htmlgraph/agent_registry.py,sha256=Usa_35by7p5gtpvHO7K3AcGimnorw-FzgPVa3cWTQ58,9448
|
|
4
4
|
htmlgraph/agents.py,sha256=Yvu6x1nOfrW2WhRTAHiCuSpvqoVJXx1Mkzd59kwEczw,33466
|
|
@@ -189,7 +189,7 @@ htmlgraph/hooks/cigs_pretool_enforcer.py,sha256=Lyp4DDaw_sVHEcW-kzdegldyfXjvVD25
|
|
|
189
189
|
htmlgraph/hooks/concurrent_sessions.py,sha256=qOiwDfynphVG0-2pVBakEzOwMORU8ebN1gMjcN4S0z0,6476
|
|
190
190
|
htmlgraph/hooks/context.py,sha256=tJ4dIL8uTFHyqyuuMc-ETDuOikeD5cN3Mdjmfg6W0HE,13108
|
|
191
191
|
htmlgraph/hooks/drift_handler.py,sha256=QckL5U5ooku51kI6mppGLsXzaKVt1Yx5uNu-iXZrgSk,17602
|
|
192
|
-
htmlgraph/hooks/event_tracker.py,sha256=
|
|
192
|
+
htmlgraph/hooks/event_tracker.py,sha256=CcIwUNYJraSGIZttnQ5kJ9qg08FvuITBELSU2jS4XiA,45453
|
|
193
193
|
htmlgraph/hooks/git_commands.py,sha256=NPzthfzGJ_bkDi7soehHOxI9FLL-6BL8Tie9Byb_zf4,4803
|
|
194
194
|
htmlgraph/hooks/hooks-config.example.json,sha256=tXpk-U-FZzGOoNJK2uiDMbIHCYEHA794J-El0fBwkqg,197
|
|
195
195
|
htmlgraph/hooks/installer.py,sha256=nOctCFDEV7BEh7ZzxNY-apu1KZG0SHPMq74UPIOChqY,11756
|
|
@@ -209,7 +209,7 @@ htmlgraph/hooks/session_handler.py,sha256=Kft-rpSOnAYXPUzvujhWhy8TeWzzjTw0Rz_O-y
|
|
|
209
209
|
htmlgraph/hooks/session_summary.py,sha256=t-7VZP4BcQQS3213xZBO9ZZvuMcT3K64ZUymZTx1FCY,13055
|
|
210
210
|
htmlgraph/hooks/state_manager.py,sha256=ni86zHChSqzlr2CnDoTDwJ9a_zbNqRlPIdQkhLIUEKQ,17645
|
|
211
211
|
htmlgraph/hooks/subagent_detection.py,sha256=SQBksApkGniCnsf8_l3TNoH9l9Kvc50ruKGc3y3wtN0,5737
|
|
212
|
-
htmlgraph/hooks/subagent_stop.py,sha256
|
|
212
|
+
htmlgraph/hooks/subagent_stop.py,sha256=-YgIs1fUyuuzarweukk51XfDUZO-vZfdkfReExUfJaU,9085
|
|
213
213
|
htmlgraph/hooks/task_enforcer.py,sha256=eXaQaNibUu1ty78t9M46h0Hhw2M1Fi1lnqCpUdsBZ9Y,7927
|
|
214
214
|
htmlgraph/hooks/task_validator.py,sha256=GwEoqL2lptPWQqckkfl0N-Auc7TtHiyRlOf6p7HcoIo,5438
|
|
215
215
|
htmlgraph/hooks/validator.py,sha256=QO1NLdiFB7Uli6XZP_CRYHDGR1r4ujbr282b1d_cDk0,21437
|
|
@@ -249,12 +249,12 @@ htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4be
|
|
|
249
249
|
htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
|
|
250
250
|
htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
|
|
251
251
|
htmlgraph/templates/orchestration-view.html,sha256=DlS7LlcjH0oO_KYILjuF1X42t8QhKLH4F85rkO54alY,10472
|
|
252
|
-
htmlgraph-0.26.
|
|
253
|
-
htmlgraph-0.26.
|
|
254
|
-
htmlgraph-0.26.
|
|
255
|
-
htmlgraph-0.26.
|
|
256
|
-
htmlgraph-0.26.
|
|
257
|
-
htmlgraph-0.26.
|
|
258
|
-
htmlgraph-0.26.
|
|
259
|
-
htmlgraph-0.26.
|
|
260
|
-
htmlgraph-0.26.
|
|
252
|
+
htmlgraph-0.26.10.data/data/htmlgraph/dashboard.html,sha256=MUT6SaYnazoyDcvHz5hN1omYswyIoUfeoZLf2M_iblo,251268
|
|
253
|
+
htmlgraph-0.26.10.data/data/htmlgraph/styles.css,sha256=oDUSC8jG-V-hKojOBO9J88hxAeY2wJrBYTq0uCwX_Y4,7135
|
|
254
|
+
htmlgraph-0.26.10.data/data/htmlgraph/templates/AGENTS.md.template,sha256=f96h7V6ygwj-v-fanVI48eYMxR6t_se4bet1H4ZsDpI,7642
|
|
255
|
+
htmlgraph-0.26.10.data/data/htmlgraph/templates/CLAUDE.md.template,sha256=h1kG2hTX2XYig2KszsHBfzrwa_4Cfcq2Pj4SwqzeDlM,1984
|
|
256
|
+
htmlgraph-0.26.10.data/data/htmlgraph/templates/GEMINI.md.template,sha256=gAGzE53Avki87BM_otqy5HdcYCoLsHgqaKjVzNzPMX8,1622
|
|
257
|
+
htmlgraph-0.26.10.dist-info/METADATA,sha256=0ZY1pSBhJOIj7PNKPPFfFX8KAPoULKNSg4JruJJYvRQ,10237
|
|
258
|
+
htmlgraph-0.26.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
259
|
+
htmlgraph-0.26.10.dist-info/entry_points.txt,sha256=Wmdo5cx8pt6NoMsssVE2mZH1CZLSUsrg_3iSWatiyn0,103
|
|
260
|
+
htmlgraph-0.26.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{htmlgraph-0.26.8.data → htmlgraph-0.26.10.data}/data/htmlgraph/templates/AGENTS.md.template
RENAMED
|
File without changes
|
{htmlgraph-0.26.8.data → htmlgraph-0.26.10.data}/data/htmlgraph/templates/CLAUDE.md.template
RENAMED
|
File without changes
|
{htmlgraph-0.26.8.data → htmlgraph-0.26.10.data}/data/htmlgraph/templates/GEMINI.md.template
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|