htmlgraph 0.26.1__py3-none-any.whl → 0.26.2__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/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/main.py +66 -9
- htmlgraph/api/templates/partials/activity-feed.html +59 -0
- htmlgraph/cli.py +1 -1
- htmlgraph/config.py +173 -96
- htmlgraph/dashboard.html +631 -7277
- htmlgraph/db/schema.py +4 -5
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +1 -1
- htmlgraph/hooks/context.py +39 -8
- htmlgraph/hooks/event_tracker.py +60 -12
- htmlgraph/hooks/pretooluse.py +60 -30
- htmlgraph/hooks/subagent_stop.py +3 -2
- htmlgraph/operations/fastapi_server.py +2 -2
- htmlgraph/orchestration/headless_spawner.py +167 -1
- htmlgraph/server.py +100 -203
- htmlgraph-0.26.2.data/data/htmlgraph/dashboard.html +812 -0
- {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.2.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.2.dist-info}/RECORD +27 -24
- htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +0 -7458
- {htmlgraph-0.26.1.data → htmlgraph-0.26.2.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.1.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.1.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.1.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.2.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.1.dist-info → htmlgraph-0.26.2.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
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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:
|
htmlgraph/hooks/context.py
CHANGED
|
@@ -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.
|
|
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,39 @@ class HookContext:
|
|
|
115
121
|
or os.environ.get("CLAUDE_SESSION_ID")
|
|
116
122
|
)
|
|
117
123
|
|
|
118
|
-
# Fallback
|
|
124
|
+
# Fallback: Query database for most recent active session
|
|
125
|
+
# This solves the issue where PostToolUse hooks don't receive session_id
|
|
126
|
+
# in hook_input, but SessionStart hook already created a session in the database.
|
|
119
127
|
if not session_id:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
db_path = graph_dir / "htmlgraph.db"
|
|
129
|
+
if db_path.exists():
|
|
130
|
+
try:
|
|
131
|
+
import sqlite3
|
|
132
|
+
|
|
133
|
+
conn = sqlite3.connect(str(db_path), timeout=1.0)
|
|
134
|
+
cursor = conn.cursor()
|
|
135
|
+
cursor.execute("""
|
|
136
|
+
SELECT session_id FROM sessions
|
|
137
|
+
WHERE status = 'active'
|
|
138
|
+
ORDER BY created_at DESC
|
|
139
|
+
LIMIT 1
|
|
140
|
+
""")
|
|
141
|
+
row = cursor.fetchone()
|
|
142
|
+
conn.close()
|
|
143
|
+
if row:
|
|
144
|
+
session_id = row[0]
|
|
145
|
+
logger.info(f"Resolved session_id from database: {session_id}")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.warning(f"Failed to query active session from database: {e}")
|
|
148
|
+
|
|
149
|
+
# Final fallback to "unknown" if database query fails
|
|
150
|
+
if not session_id:
|
|
151
|
+
session_id = "unknown"
|
|
152
|
+
logger.warning(
|
|
153
|
+
"Could not resolve session_id from hook_input, environment, or database. "
|
|
154
|
+
"Events will not be linked to parent UserQuery. "
|
|
155
|
+
"For multi-window support, set HTMLGRAPH_SESSION_ID env var."
|
|
156
|
+
)
|
|
126
157
|
|
|
127
158
|
# Detect agent ID (priority order)
|
|
128
159
|
# 1. Explicit agent_id in hook input
|
htmlgraph/hooks/event_tracker.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
if
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
721
|
-
|
|
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
|
|
htmlgraph/hooks/pretooluse.py
CHANGED
|
@@ -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/
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
376
|
+
task_parent_event_id = None
|
|
369
377
|
if tool_name == "Task":
|
|
370
|
-
|
|
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
|
|
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
|
-
#
|
|
458
|
-
|
|
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
|
-
|
|
463
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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 {}
|
htmlgraph/hooks/subagent_stop.py
CHANGED
|
@@ -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
|
-
|
|
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" / "
|
|
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" / "
|
|
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",
|