htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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 +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
htmlgraph/hooks/event_tracker.py
CHANGED
|
@@ -1,32 +1,85 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
HtmlGraph Event Tracker Module
|
|
3
7
|
|
|
4
8
|
Reusable event tracking logic for hook integrations.
|
|
5
|
-
Provides session management, drift detection, and
|
|
9
|
+
Provides session management, drift detection, activity logging, and SQLite persistence.
|
|
6
10
|
|
|
7
11
|
Public API:
|
|
8
|
-
track_event(hook_type: str, tool_input: dict) -> dict
|
|
12
|
+
track_event(hook_type: str, tool_input: dict[str, Any]) -> dict
|
|
9
13
|
Main entry point for tracking hook events (PostToolUse, Stop, UserPromptSubmit)
|
|
14
|
+
|
|
15
|
+
Events are recorded to both:
|
|
16
|
+
- HTML files via SessionManager (existing)
|
|
17
|
+
- SQLite database via HtmlGraphDB (new - for dashboard queries)
|
|
18
|
+
|
|
19
|
+
Parent-child event linking:
|
|
20
|
+
- Database is the single source of truth for parent-child linking
|
|
21
|
+
- UserQuery events are stored in agent_events table with tool_name='UserQuery'
|
|
22
|
+
- get_parent_user_query() queries database for most recent UserQuery in session
|
|
10
23
|
"""
|
|
11
24
|
|
|
12
25
|
import json
|
|
13
26
|
import os
|
|
14
27
|
import re
|
|
15
28
|
import subprocess
|
|
16
|
-
import
|
|
17
|
-
from datetime import datetime, timedelta
|
|
29
|
+
from datetime import datetime, timedelta, timezone
|
|
18
30
|
from pathlib import Path
|
|
19
|
-
from typing import Any, cast
|
|
31
|
+
from typing import Any, cast # noqa: F401
|
|
20
32
|
|
|
33
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
34
|
+
from htmlgraph.ids import generate_id
|
|
21
35
|
from htmlgraph.session_manager import SessionManager
|
|
22
36
|
|
|
23
37
|
# Drift classification queue (stored in session directory)
|
|
24
38
|
DRIFT_QUEUE_FILE = "drift-queue.json"
|
|
25
|
-
# Active parent activity tracker (for Skill/Task invocations)
|
|
26
|
-
PARENT_ACTIVITY_FILE = "parent-activity.json"
|
|
27
39
|
|
|
28
40
|
|
|
29
|
-
def
|
|
41
|
+
def get_model_from_status_cache(session_id: str | None = None) -> str | None:
|
|
42
|
+
"""
|
|
43
|
+
Read current model from SQLite model_cache table.
|
|
44
|
+
|
|
45
|
+
The status line script writes model info to the model_cache table.
|
|
46
|
+
This allows hooks to know which Claude model is currently running,
|
|
47
|
+
even though hooks don't receive model info directly from Claude Code.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
session_id: Unused, kept for backward compatibility.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Model display name (e.g., "Opus 4.5", "Sonnet", "Haiku") or None if not found.
|
|
54
|
+
"""
|
|
55
|
+
import sqlite3
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
# Try project database first
|
|
59
|
+
db_path = Path.cwd() / ".htmlgraph" / "htmlgraph.db"
|
|
60
|
+
if not db_path.exists():
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
conn = sqlite3.connect(str(db_path), timeout=1.0)
|
|
64
|
+
cursor = conn.cursor()
|
|
65
|
+
|
|
66
|
+
# Check if model_cache table exists and has data
|
|
67
|
+
cursor.execute("SELECT model FROM model_cache WHERE id = 1 LIMIT 1")
|
|
68
|
+
row = cursor.fetchone()
|
|
69
|
+
conn.close()
|
|
70
|
+
|
|
71
|
+
if row and row[0] and row[0] != "Claude":
|
|
72
|
+
return str(row[0])
|
|
73
|
+
return str(row[0]) if row else None
|
|
74
|
+
|
|
75
|
+
except Exception:
|
|
76
|
+
# Table doesn't exist or read error - silently fail
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_drift_config() -> dict[str, Any]:
|
|
30
83
|
"""Load drift configuration from plugin config or project .claude directory."""
|
|
31
84
|
config_paths = [
|
|
32
85
|
Path(__file__).parent.parent.parent.parent.parent
|
|
@@ -67,48 +120,43 @@ def load_drift_config() -> dict:
|
|
|
67
120
|
}
|
|
68
121
|
|
|
69
122
|
|
|
70
|
-
def
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
data = cast(dict[Any, Any], json.load(f))
|
|
77
|
-
# Clean up stale parent activities (older than 5 minutes)
|
|
78
|
-
if data.get("timestamp"):
|
|
79
|
-
ts = datetime.fromisoformat(data["timestamp"])
|
|
80
|
-
if datetime.now() - ts > timedelta(minutes=5):
|
|
81
|
-
return {}
|
|
82
|
-
return data
|
|
83
|
-
except Exception:
|
|
84
|
-
pass
|
|
85
|
-
return {}
|
|
123
|
+
def get_parent_user_query(db: HtmlGraphDB, session_id: str) -> str | None:
|
|
124
|
+
"""
|
|
125
|
+
Get the most recent UserQuery event_id for this session from database.
|
|
126
|
+
|
|
127
|
+
This is the primary method for parent-child event linking.
|
|
128
|
+
Database is the single source of truth - no file-based state.
|
|
86
129
|
|
|
130
|
+
Args:
|
|
131
|
+
db: HtmlGraphDB instance
|
|
132
|
+
session_id: Session ID to query
|
|
87
133
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"""Save the active parent activity state."""
|
|
92
|
-
path = graph_dir / PARENT_ACTIVITY_FILE
|
|
134
|
+
Returns:
|
|
135
|
+
event_id of the most recent UserQuery event, or None if not found
|
|
136
|
+
"""
|
|
93
137
|
try:
|
|
94
|
-
if
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
if db.connection is None:
|
|
139
|
+
return None
|
|
140
|
+
cursor = db.connection.cursor()
|
|
141
|
+
cursor.execute(
|
|
142
|
+
"""
|
|
143
|
+
SELECT event_id FROM agent_events
|
|
144
|
+
WHERE session_id = ? AND tool_name = 'UserQuery'
|
|
145
|
+
ORDER BY timestamp DESC
|
|
146
|
+
LIMIT 1
|
|
147
|
+
""",
|
|
148
|
+
(session_id,),
|
|
149
|
+
)
|
|
150
|
+
row = cursor.fetchone()
|
|
151
|
+
if row:
|
|
152
|
+
return str(row[0])
|
|
153
|
+
return None
|
|
107
154
|
except Exception as e:
|
|
108
|
-
|
|
155
|
+
logger.warning(f"Debug: Database query for UserQuery failed: {e}")
|
|
156
|
+
return None
|
|
109
157
|
|
|
110
158
|
|
|
111
|
-
def load_drift_queue(graph_dir: Path, max_age_hours: int = 48) -> dict:
|
|
159
|
+
def load_drift_queue(graph_dir: Path, max_age_hours: int = 48) -> dict[str, Any]:
|
|
112
160
|
"""
|
|
113
161
|
Load the drift queue from file and clean up stale entries.
|
|
114
162
|
|
|
@@ -146,9 +194,8 @@ def load_drift_queue(graph_dir: Path, max_age_hours: int = 48) -> dict:
|
|
|
146
194
|
queue["activities"] = fresh_activities
|
|
147
195
|
save_drift_queue(graph_dir, queue)
|
|
148
196
|
removed = original_count - len(fresh_activities)
|
|
149
|
-
|
|
150
|
-
f"Cleaned {removed} stale drift queue entries (older than {max_age_hours}h)"
|
|
151
|
-
file=sys.stderr,
|
|
197
|
+
logger.warning(
|
|
198
|
+
f"Cleaned {removed} stale drift queue entries (older than {max_age_hours}h)"
|
|
152
199
|
)
|
|
153
200
|
|
|
154
201
|
return cast(dict[Any, Any], queue)
|
|
@@ -157,14 +204,14 @@ def load_drift_queue(graph_dir: Path, max_age_hours: int = 48) -> dict:
|
|
|
157
204
|
return {"activities": [], "last_classification": None}
|
|
158
205
|
|
|
159
206
|
|
|
160
|
-
def save_drift_queue(graph_dir: Path, queue: dict) -> None:
|
|
207
|
+
def save_drift_queue(graph_dir: Path, queue: dict[str, Any]) -> None:
|
|
161
208
|
"""Save the drift queue to file."""
|
|
162
209
|
queue_path = graph_dir / DRIFT_QUEUE_FILE
|
|
163
210
|
try:
|
|
164
211
|
with open(queue_path, "w") as f:
|
|
165
212
|
json.dump(queue, f, indent=2, default=str)
|
|
166
213
|
except Exception as e:
|
|
167
|
-
|
|
214
|
+
logger.warning(f"Warning: Could not save drift queue: {e}")
|
|
168
215
|
|
|
169
216
|
|
|
170
217
|
def clear_drift_queue_activities(graph_dir: Path) -> None:
|
|
@@ -188,10 +235,12 @@ def clear_drift_queue_activities(graph_dir: Path) -> None:
|
|
|
188
235
|
with open(queue_path, "w") as f:
|
|
189
236
|
json.dump(queue, f, indent=2)
|
|
190
237
|
except Exception as e:
|
|
191
|
-
|
|
238
|
+
logger.warning(f"Warning: Could not clear drift queue: {e}")
|
|
192
239
|
|
|
193
240
|
|
|
194
|
-
def add_to_drift_queue(
|
|
241
|
+
def add_to_drift_queue(
|
|
242
|
+
graph_dir: Path, activity: dict[str, Any], config: dict[str, Any]
|
|
243
|
+
) -> dict[str, Any]:
|
|
195
244
|
"""Add a high-drift activity to the queue."""
|
|
196
245
|
max_age_hours = config.get("queue", {}).get("max_age_hours", 48)
|
|
197
246
|
queue = load_drift_queue(graph_dir, max_age_hours=max_age_hours)
|
|
@@ -199,7 +248,7 @@ def add_to_drift_queue(graph_dir: Path, activity: dict, config: dict) -> dict:
|
|
|
199
248
|
|
|
200
249
|
queue["activities"].append(
|
|
201
250
|
{
|
|
202
|
-
"timestamp": datetime.now().isoformat(),
|
|
251
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
203
252
|
"tool": activity.get("tool"),
|
|
204
253
|
"summary": activity.get("summary"),
|
|
205
254
|
"file_paths": activity.get("file_paths", []),
|
|
@@ -214,7 +263,9 @@ def add_to_drift_queue(graph_dir: Path, activity: dict, config: dict) -> dict:
|
|
|
214
263
|
return queue
|
|
215
264
|
|
|
216
265
|
|
|
217
|
-
def should_trigger_classification(
|
|
266
|
+
def should_trigger_classification(
|
|
267
|
+
queue: dict[str, Any], config: dict[str, Any]
|
|
268
|
+
) -> bool:
|
|
218
269
|
"""Check if we should trigger auto-classification."""
|
|
219
270
|
drift_config = config.get("drift_detection", {})
|
|
220
271
|
|
|
@@ -241,7 +292,7 @@ def should_trigger_classification(queue: dict, config: dict) -> bool:
|
|
|
241
292
|
return True
|
|
242
293
|
|
|
243
294
|
|
|
244
|
-
def build_classification_prompt(queue: dict, feature_id: str) -> str:
|
|
295
|
+
def build_classification_prompt(queue: dict[str, Any], feature_id: str) -> str:
|
|
245
296
|
"""Build the prompt for the classification agent."""
|
|
246
297
|
activities = queue.get("activities", [])
|
|
247
298
|
|
|
@@ -293,7 +344,108 @@ def resolve_project_path(cwd: str | None = None) -> str:
|
|
|
293
344
|
return start_dir
|
|
294
345
|
|
|
295
346
|
|
|
296
|
-
def
|
|
347
|
+
def detect_model_from_hook_input(hook_input: dict[str, Any]) -> str | None:
|
|
348
|
+
"""
|
|
349
|
+
Detect the Claude model from hook input data.
|
|
350
|
+
|
|
351
|
+
Checks in order of priority:
|
|
352
|
+
1. Task() model parameter (if tool_name == 'Task')
|
|
353
|
+
2. HTMLGRAPH_MODEL environment variable (set by hooks)
|
|
354
|
+
3. ANTHROPIC_MODEL or CLAUDE_MODEL environment variables
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
hook_input: Hook input dict containing tool_name and tool_input
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Model name (e.g., 'claude-opus', 'claude-sonnet', 'claude-haiku') or None
|
|
361
|
+
"""
|
|
362
|
+
# Get tool info
|
|
363
|
+
tool_name_value: Any = hook_input.get("tool_name", "") or hook_input.get("name", "")
|
|
364
|
+
tool_name = tool_name_value if isinstance(tool_name_value, str) else ""
|
|
365
|
+
tool_input_value: Any = hook_input.get("tool_input", {}) or hook_input.get(
|
|
366
|
+
"input", {}
|
|
367
|
+
)
|
|
368
|
+
tool_input = tool_input_value if isinstance(tool_input_value, dict) else {}
|
|
369
|
+
|
|
370
|
+
# 1. Check for Task() model parameter first
|
|
371
|
+
if tool_name == "Task" and "model" in tool_input:
|
|
372
|
+
model_value: Any = tool_input.get("model")
|
|
373
|
+
if model_value and isinstance(model_value, str):
|
|
374
|
+
model = model_value.strip().lower()
|
|
375
|
+
if model:
|
|
376
|
+
if not model.startswith("claude-"):
|
|
377
|
+
model = f"claude-{model}"
|
|
378
|
+
return cast(str, model)
|
|
379
|
+
|
|
380
|
+
# 2. Check environment variables (set by PreToolUse hook)
|
|
381
|
+
for env_var in ["HTMLGRAPH_MODEL", "ANTHROPIC_MODEL", "CLAUDE_MODEL"]:
|
|
382
|
+
value = os.environ.get(env_var)
|
|
383
|
+
if value and isinstance(value, str):
|
|
384
|
+
model = value.strip()
|
|
385
|
+
if model:
|
|
386
|
+
return model
|
|
387
|
+
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def detect_agent_from_environment() -> tuple[str, str | None]:
|
|
392
|
+
"""
|
|
393
|
+
Detect the agent/model name from environment variables and status cache.
|
|
394
|
+
|
|
395
|
+
Checks multiple sources in order of priority:
|
|
396
|
+
1. HTMLGRAPH_AGENT - Explicit agent name set by user
|
|
397
|
+
2. HTMLGRAPH_SUBAGENT_TYPE - For subagent sessions
|
|
398
|
+
3. HTMLGRAPH_PARENT_AGENT - Parent agent context
|
|
399
|
+
4. HTMLGRAPH_MODEL - Model name (e.g., claude-haiku, claude-opus)
|
|
400
|
+
5. CLAUDE_MODEL - Model name if exposed by Claude Code
|
|
401
|
+
6. ANTHROPIC_MODEL - Alternative model env var
|
|
402
|
+
7. Status line cache (model only) - ~/.cache/claude-code/status-{session_id}.json
|
|
403
|
+
|
|
404
|
+
Falls back to 'claude-code' if no environment variable is set.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Tuple of (agent_id, model_name). Model name may be None if not detected.
|
|
408
|
+
"""
|
|
409
|
+
# Check for explicit agent name first
|
|
410
|
+
agent_id = None
|
|
411
|
+
env_vars_agent = [
|
|
412
|
+
"HTMLGRAPH_AGENT",
|
|
413
|
+
"HTMLGRAPH_SUBAGENT_TYPE",
|
|
414
|
+
"HTMLGRAPH_PARENT_AGENT",
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
for var in env_vars_agent:
|
|
418
|
+
value = os.environ.get(var)
|
|
419
|
+
if value and value.strip():
|
|
420
|
+
agent_id = value.strip()
|
|
421
|
+
break
|
|
422
|
+
|
|
423
|
+
# Check for model name separately
|
|
424
|
+
model_name = None
|
|
425
|
+
env_vars_model = [
|
|
426
|
+
"HTMLGRAPH_MODEL",
|
|
427
|
+
"CLAUDE_MODEL",
|
|
428
|
+
"ANTHROPIC_MODEL",
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
for var in env_vars_model:
|
|
432
|
+
value = os.environ.get(var)
|
|
433
|
+
if value and value.strip():
|
|
434
|
+
model_name = value.strip()
|
|
435
|
+
break
|
|
436
|
+
|
|
437
|
+
# Fallback: Try to read model from status line cache
|
|
438
|
+
if not model_name:
|
|
439
|
+
model_name = get_model_from_status_cache()
|
|
440
|
+
|
|
441
|
+
# Default fallback for agent_id
|
|
442
|
+
if not agent_id:
|
|
443
|
+
agent_id = "claude-code"
|
|
444
|
+
|
|
445
|
+
return agent_id, model_name
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def extract_file_paths(tool_input: dict[str, Any], tool_name: str) -> list[str]:
|
|
297
449
|
"""Extract file paths from tool input based on tool type."""
|
|
298
450
|
paths = []
|
|
299
451
|
|
|
@@ -318,7 +470,7 @@ def extract_file_paths(tool_input: dict, tool_name: str) -> list[str]:
|
|
|
318
470
|
|
|
319
471
|
|
|
320
472
|
def format_tool_summary(
|
|
321
|
-
tool_name: str, tool_input: dict, tool_result: dict | None = None
|
|
473
|
+
tool_name: str, tool_input: dict[str, Any], tool_result: dict | None = None
|
|
322
474
|
) -> str:
|
|
323
475
|
"""Format a human-readable summary of the tool call."""
|
|
324
476
|
if tool_name == "Read":
|
|
@@ -366,13 +518,170 @@ def format_tool_summary(
|
|
|
366
518
|
url = tool_input.get("url", "")[:40]
|
|
367
519
|
return f"WebFetch: {url}"
|
|
368
520
|
|
|
521
|
+
elif tool_name == "UserQuery":
|
|
522
|
+
# Extract the actual prompt text from the tool_input
|
|
523
|
+
prompt = str(tool_input.get("prompt", ""))
|
|
524
|
+
preview = prompt[:100].replace("\n", " ")
|
|
525
|
+
if len(prompt) > 100:
|
|
526
|
+
preview += "..."
|
|
527
|
+
return preview
|
|
528
|
+
|
|
369
529
|
else:
|
|
370
530
|
return f"{tool_name}: {str(tool_input)[:50]}"
|
|
371
531
|
|
|
372
532
|
|
|
373
|
-
def
|
|
533
|
+
def record_event_to_sqlite(
|
|
534
|
+
db: HtmlGraphDB,
|
|
535
|
+
session_id: str,
|
|
536
|
+
tool_name: str,
|
|
537
|
+
tool_input: dict[str, Any],
|
|
538
|
+
tool_response: dict[str, Any],
|
|
539
|
+
is_error: bool,
|
|
540
|
+
file_paths: list[str] | None = None,
|
|
541
|
+
parent_event_id: str | None = None,
|
|
542
|
+
agent_id: str | None = None,
|
|
543
|
+
subagent_type: str | None = None,
|
|
544
|
+
model: str | None = None,
|
|
545
|
+
feature_id: str | None = None,
|
|
546
|
+
claude_task_id: str | None = None,
|
|
547
|
+
) -> str | None:
|
|
548
|
+
"""
|
|
549
|
+
Record a tool call event to SQLite database for dashboard queries.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
db: HtmlGraphDB instance
|
|
553
|
+
session_id: Session ID from HtmlGraph
|
|
554
|
+
tool_name: Name of the tool called
|
|
555
|
+
tool_input: Tool input parameters
|
|
556
|
+
tool_response: Tool response/result
|
|
557
|
+
is_error: Whether the tool call resulted in an error
|
|
558
|
+
file_paths: File paths affected by the tool
|
|
559
|
+
parent_event_id: Parent event ID if this is a child event
|
|
560
|
+
agent_id: Agent identifier (optional)
|
|
561
|
+
subagent_type: Subagent type for Task delegations (optional)
|
|
562
|
+
model: Claude model name (e.g., claude-haiku, claude-opus) (optional)
|
|
563
|
+
feature_id: Feature ID for attribution (optional)
|
|
564
|
+
claude_task_id: Claude Code's internal task ID for tool attribution (optional)
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
event_id if successful, None otherwise
|
|
568
|
+
"""
|
|
569
|
+
try:
|
|
570
|
+
event_id = generate_id("event")
|
|
571
|
+
input_summary = format_tool_summary(tool_name, tool_input, tool_response)
|
|
572
|
+
|
|
573
|
+
# Build output summary from tool response
|
|
574
|
+
output_summary = ""
|
|
575
|
+
if isinstance(tool_response, dict): # type: ignore[arg-type]
|
|
576
|
+
if is_error:
|
|
577
|
+
output_summary = tool_response.get("error", "error")[:200]
|
|
578
|
+
else:
|
|
579
|
+
# Extract summary from response
|
|
580
|
+
content = tool_response.get("content", tool_response.get("output", ""))
|
|
581
|
+
if isinstance(content, str):
|
|
582
|
+
output_summary = content[:200]
|
|
583
|
+
elif isinstance(content, list):
|
|
584
|
+
output_summary = f"{len(content)} items"
|
|
585
|
+
else:
|
|
586
|
+
output_summary = "success"
|
|
587
|
+
|
|
588
|
+
# Build context metadata
|
|
589
|
+
context = {
|
|
590
|
+
"file_paths": file_paths or [],
|
|
591
|
+
"tool_input_keys": list(tool_input.keys()),
|
|
592
|
+
"is_error": is_error,
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
# Extract task_id from Tool response if not provided
|
|
596
|
+
if (
|
|
597
|
+
not claude_task_id
|
|
598
|
+
and tool_name == "Task"
|
|
599
|
+
and isinstance(tool_response, dict)
|
|
600
|
+
):
|
|
601
|
+
claude_task_id = tool_response.get("task_id")
|
|
602
|
+
|
|
603
|
+
# Insert event to SQLite
|
|
604
|
+
success = db.insert_event(
|
|
605
|
+
event_id=event_id,
|
|
606
|
+
agent_id=agent_id or "claude-code",
|
|
607
|
+
event_type="tool_call",
|
|
608
|
+
session_id=session_id,
|
|
609
|
+
tool_name=tool_name,
|
|
610
|
+
input_summary=input_summary,
|
|
611
|
+
output_summary=output_summary,
|
|
612
|
+
context=context,
|
|
613
|
+
parent_event_id=parent_event_id,
|
|
614
|
+
cost_tokens=0,
|
|
615
|
+
subagent_type=subagent_type,
|
|
616
|
+
model=model,
|
|
617
|
+
feature_id=feature_id,
|
|
618
|
+
claude_task_id=claude_task_id,
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
if success:
|
|
622
|
+
return event_id
|
|
623
|
+
return None
|
|
624
|
+
|
|
625
|
+
except Exception as e:
|
|
626
|
+
logger.warning(f"Warning: Could not record event to SQLite: {e}")
|
|
627
|
+
return None
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def record_delegation_to_sqlite(
|
|
631
|
+
db: HtmlGraphDB,
|
|
632
|
+
session_id: str,
|
|
633
|
+
from_agent: str,
|
|
634
|
+
to_agent: str,
|
|
635
|
+
task_description: str,
|
|
636
|
+
task_input: dict[str, Any],
|
|
637
|
+
) -> str | None:
|
|
638
|
+
"""
|
|
639
|
+
Record a Task() delegation to agent_collaboration table.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
db: HtmlGraphDB instance
|
|
643
|
+
session_id: Session ID from HtmlGraph
|
|
644
|
+
from_agent: Agent delegating the task (usually 'orchestrator' or 'claude-code')
|
|
645
|
+
to_agent: Target subagent type (e.g., 'general-purpose', 'researcher')
|
|
646
|
+
task_description: Task description/prompt
|
|
647
|
+
task_input: Full task input parameters
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
handoff_id if successful, None otherwise
|
|
651
|
+
"""
|
|
652
|
+
try:
|
|
653
|
+
handoff_id = generate_id("handoff")
|
|
654
|
+
|
|
655
|
+
# Build context with task input
|
|
656
|
+
context = {
|
|
657
|
+
"task_input_keys": list(task_input.keys()),
|
|
658
|
+
"model": task_input.get("model"),
|
|
659
|
+
"temperature": task_input.get("temperature"),
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
# Insert delegation record
|
|
663
|
+
success = db.insert_collaboration(
|
|
664
|
+
handoff_id=handoff_id,
|
|
665
|
+
from_agent=from_agent,
|
|
666
|
+
to_agent=to_agent,
|
|
667
|
+
session_id=session_id,
|
|
668
|
+
handoff_type="delegation",
|
|
669
|
+
reason=task_description[:200],
|
|
670
|
+
context=context,
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
if success:
|
|
674
|
+
return handoff_id
|
|
675
|
+
return None
|
|
676
|
+
|
|
677
|
+
except Exception as e:
|
|
678
|
+
logger.warning(f"Warning: Could not record delegation to SQLite: {e}")
|
|
679
|
+
return None
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
374
683
|
"""
|
|
375
|
-
Track a hook event and log it to HtmlGraph.
|
|
684
|
+
Track a hook event and log it to HtmlGraph (both HTML files and SQLite).
|
|
376
685
|
|
|
377
686
|
Args:
|
|
378
687
|
hook_type: Type of hook event ("PostToolUse", "Stop", "UserPromptSubmit")
|
|
@@ -388,37 +697,322 @@ def track_event(hook_type: str, hook_input: dict) -> dict:
|
|
|
388
697
|
# Load drift configuration
|
|
389
698
|
drift_config = load_drift_config()
|
|
390
699
|
|
|
391
|
-
# Initialize SessionManager
|
|
700
|
+
# Initialize SessionManager and SQLite DB
|
|
392
701
|
try:
|
|
393
702
|
manager = SessionManager(graph_dir)
|
|
394
703
|
except Exception as e:
|
|
395
|
-
|
|
704
|
+
logger.warning(f"Warning: Could not initialize SessionManager: {e}")
|
|
396
705
|
return {"continue": True}
|
|
397
706
|
|
|
398
|
-
#
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
707
|
+
# Initialize SQLite database for event recording
|
|
708
|
+
db = None
|
|
709
|
+
try:
|
|
710
|
+
from htmlgraph.config import get_database_path
|
|
711
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
712
|
+
|
|
713
|
+
db = HtmlGraphDB(str(get_database_path()))
|
|
714
|
+
except Exception as e:
|
|
715
|
+
logger.warning(f"Warning: Could not initialize SQLite database: {e}")
|
|
716
|
+
# Continue without SQLite (graceful degradation)
|
|
717
|
+
|
|
718
|
+
# Detect agent and model from environment
|
|
719
|
+
detected_agent, detected_model = detect_agent_from_environment()
|
|
720
|
+
|
|
721
|
+
# Also try to detect model from hook input (more specific than environment)
|
|
722
|
+
model_from_input = detect_model_from_hook_input(hook_input)
|
|
723
|
+
if model_from_input:
|
|
724
|
+
detected_model = model_from_input
|
|
725
|
+
|
|
726
|
+
active_session = None
|
|
727
|
+
|
|
728
|
+
# Check if we're in a subagent context using multiple methods:
|
|
729
|
+
#
|
|
730
|
+
# PRECEDENCE ORDER:
|
|
731
|
+
# 1. Sessions table - if THIS session is already marked as subagent, use stored parent info
|
|
732
|
+
# (fixes persistence issue for subsequent tool calls in same subagent)
|
|
733
|
+
# 2. Environment variables - set by spawner router for first tool call
|
|
734
|
+
# 3. Fallback to normal orchestrator context
|
|
735
|
+
#
|
|
736
|
+
# Method 1: Check if current session is already a subagent (CRITICAL for persistence!)
|
|
737
|
+
# This fixes the issue where subsequent tool calls in the same subagent session
|
|
738
|
+
# lose the parent_event_id linkage.
|
|
739
|
+
subagent_type = None
|
|
740
|
+
parent_session_id = None
|
|
741
|
+
task_event_id_from_db = None # Will be set by Method 1 if found
|
|
742
|
+
hook_session_id = hook_input.get("session_id") or hook_input.get("sessionId")
|
|
743
|
+
|
|
744
|
+
if db and db.connection and hook_session_id:
|
|
402
745
|
try:
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
746
|
+
cursor = db.connection.cursor()
|
|
747
|
+
cursor.execute(
|
|
748
|
+
"""
|
|
749
|
+
SELECT parent_session_id, agent_assigned
|
|
750
|
+
FROM sessions
|
|
751
|
+
WHERE session_id = ? AND is_subagent = 1
|
|
752
|
+
LIMIT 1
|
|
753
|
+
""",
|
|
754
|
+
(hook_session_id,),
|
|
407
755
|
)
|
|
408
|
-
|
|
409
|
-
|
|
756
|
+
row = cursor.fetchone()
|
|
757
|
+
if row:
|
|
758
|
+
parent_session_id = row[0]
|
|
759
|
+
# Extract subagent_type from agent_assigned (e.g., "general-purpose-spawner" -> "general-purpose")
|
|
760
|
+
agent_assigned = row[1] or ""
|
|
761
|
+
if agent_assigned and agent_assigned.endswith("-spawner"):
|
|
762
|
+
subagent_type = agent_assigned[:-8] # Remove "-spawner" suffix
|
|
763
|
+
else:
|
|
764
|
+
subagent_type = "general-purpose" # Default if format unexpected
|
|
765
|
+
|
|
766
|
+
# CRITICAL FIX: When Method 1 succeeds, also find the task_delegation event!
|
|
767
|
+
# This ensures parent_activity_id will use the task event, not fall back to UserQuery
|
|
768
|
+
try:
|
|
769
|
+
# First try to find task in parent_session_id (if not NULL)
|
|
770
|
+
if parent_session_id:
|
|
771
|
+
cursor.execute(
|
|
772
|
+
"""
|
|
773
|
+
SELECT event_id
|
|
774
|
+
FROM agent_events
|
|
775
|
+
WHERE event_type = 'task_delegation'
|
|
776
|
+
AND subagent_type = ?
|
|
777
|
+
AND status = 'started'
|
|
778
|
+
AND session_id = ?
|
|
779
|
+
ORDER BY timestamp DESC
|
|
780
|
+
LIMIT 1
|
|
781
|
+
""",
|
|
782
|
+
(subagent_type, parent_session_id),
|
|
783
|
+
)
|
|
784
|
+
task_row = cursor.fetchone()
|
|
785
|
+
if task_row:
|
|
786
|
+
task_event_id_from_db = task_row[0]
|
|
787
|
+
|
|
788
|
+
# If not found (parent_session_id is NULL), fallback to finding most recent task
|
|
789
|
+
# This handles Claude Code's session reuse where parent_session_id can be NULL
|
|
790
|
+
if not task_event_id_from_db:
|
|
791
|
+
cursor.execute(
|
|
792
|
+
"""
|
|
793
|
+
SELECT event_id
|
|
794
|
+
FROM agent_events
|
|
795
|
+
WHERE event_type = 'task_delegation'
|
|
796
|
+
AND subagent_type = ?
|
|
797
|
+
AND status = 'started'
|
|
798
|
+
ORDER BY timestamp DESC
|
|
799
|
+
LIMIT 1
|
|
800
|
+
""",
|
|
801
|
+
(subagent_type,),
|
|
802
|
+
)
|
|
803
|
+
task_row = cursor.fetchone()
|
|
804
|
+
if task_row:
|
|
805
|
+
task_event_id_from_db = task_row[0]
|
|
806
|
+
logger.warning(
|
|
807
|
+
f"DEBUG Method 1 fallback: Found task_delegation={task_event_id_from_db} for {subagent_type}"
|
|
808
|
+
)
|
|
809
|
+
else:
|
|
810
|
+
logger.warning(
|
|
811
|
+
f"DEBUG Method 1: No task_delegation found for subagent_type={subagent_type}"
|
|
812
|
+
)
|
|
813
|
+
else:
|
|
814
|
+
logger.warning(
|
|
815
|
+
f"DEBUG Method 1: Found task_delegation={task_event_id_from_db} for subagent {subagent_type}"
|
|
816
|
+
)
|
|
817
|
+
except Exception as e:
|
|
818
|
+
logger.warning(
|
|
819
|
+
f"DEBUG: Error finding task_delegation for Method 1: {e}"
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
logger.debug(
|
|
823
|
+
f"DEBUG subagent persistence: Found current session as subagent in sessions table: "
|
|
824
|
+
f"type={subagent_type}, parent_session={parent_session_id}, task_event={task_event_id_from_db}",
|
|
825
|
+
)
|
|
826
|
+
except Exception as e:
|
|
827
|
+
logger.warning(f"DEBUG: Error checking sessions table for subagent: {e}")
|
|
828
|
+
|
|
829
|
+
# Method 2: Environment variables (for first tool call before session table is populated)
|
|
830
|
+
if not subagent_type:
|
|
831
|
+
subagent_type = os.environ.get("HTMLGRAPH_SUBAGENT_TYPE")
|
|
832
|
+
parent_session_id = os.environ.get("HTMLGRAPH_PARENT_SESSION")
|
|
833
|
+
|
|
834
|
+
# Method 3: Database detection of active task_delegation events
|
|
835
|
+
# CRITICAL: When Task() subprocess is launched, environment variables don't propagate
|
|
836
|
+
# So we must query the database for active task_delegation events to detect subagent context
|
|
837
|
+
# NOTE: Claude Code passes the SAME session_id to parent and subagent, so we CAN'T use
|
|
838
|
+
# session_id to distinguish them. Instead, look for the most recent task_delegation event
|
|
839
|
+
# and if found with status='started', we ARE the subagent.
|
|
840
|
+
#
|
|
841
|
+
# CRITICAL FIX: The actual PARENT session is hook_session_id (what Claude Code passes),
|
|
842
|
+
# NOT the session_id from the task_delegation event (which is the same as current).
|
|
843
|
+
# NOTE: DO NOT reinitialize task_event_id_from_db here - it may have been set by Method 1!
|
|
844
|
+
if not subagent_type and db and db.connection:
|
|
845
|
+
try:
|
|
846
|
+
cursor = db.connection.cursor()
|
|
847
|
+
# Find the most recent active task_delegation event
|
|
848
|
+
cursor.execute(
|
|
849
|
+
"""
|
|
850
|
+
SELECT event_id, subagent_type, session_id
|
|
851
|
+
FROM agent_events
|
|
852
|
+
WHERE event_type = 'task_delegation'
|
|
853
|
+
AND status = 'started'
|
|
854
|
+
AND tool_name = 'Task'
|
|
855
|
+
ORDER BY timestamp DESC
|
|
856
|
+
LIMIT 1
|
|
857
|
+
""",
|
|
858
|
+
)
|
|
859
|
+
row = cursor.fetchone()
|
|
860
|
+
if row:
|
|
861
|
+
task_event_id, detected_subagent_type, parent_sess = row
|
|
862
|
+
# If we found an active task_delegation, we're running as a subagent
|
|
863
|
+
# (Claude Code uses the same session_id for both parent and subagent)
|
|
864
|
+
subagent_type = detected_subagent_type or "general-purpose"
|
|
865
|
+
# IMPORTANT: Use the hook_session_id as parent, not parent_sess!
|
|
866
|
+
# The parent_sess from task_delegation is the same as current session
|
|
867
|
+
# (Claude Code reuses session_id). The actual parent is hook_session_id.
|
|
868
|
+
parent_session_id = hook_session_id
|
|
869
|
+
task_event_id_from_db = (
|
|
870
|
+
task_event_id # Store for later use as parent_event_id
|
|
871
|
+
)
|
|
872
|
+
logger.debug(
|
|
873
|
+
f"DEBUG subagent detection (database): Detected active task_delegation "
|
|
874
|
+
f"type={subagent_type}, parent_session={parent_session_id}, "
|
|
875
|
+
f"parent_event={task_event_id}"
|
|
876
|
+
)
|
|
877
|
+
except Exception as e:
|
|
878
|
+
logger.warning(f"DEBUG: Error detecting subagent from database: {e}")
|
|
879
|
+
|
|
880
|
+
if subagent_type and parent_session_id:
|
|
881
|
+
# We're in a subagent - create or get subagent session
|
|
882
|
+
# Use deterministic session ID based on parent + subagent type
|
|
883
|
+
subagent_session_id = f"{parent_session_id}-{subagent_type}"
|
|
884
|
+
|
|
885
|
+
# Check if subagent session already exists
|
|
886
|
+
existing = manager.session_converter.load(subagent_session_id)
|
|
887
|
+
if existing:
|
|
888
|
+
active_session = existing
|
|
889
|
+
logger.warning(
|
|
890
|
+
f"Debug: Using existing subagent session: {subagent_session_id}"
|
|
891
|
+
)
|
|
892
|
+
else:
|
|
893
|
+
# Create new subagent session with parent link
|
|
894
|
+
try:
|
|
895
|
+
active_session = manager.start_session(
|
|
896
|
+
session_id=subagent_session_id,
|
|
897
|
+
agent=f"{subagent_type}-spawner",
|
|
898
|
+
is_subagent=True,
|
|
899
|
+
parent_session_id=parent_session_id,
|
|
900
|
+
title=f"{subagent_type.capitalize()} Subagent",
|
|
901
|
+
)
|
|
902
|
+
logger.debug(
|
|
903
|
+
f"Debug: Created subagent session: {subagent_session_id} "
|
|
904
|
+
f"(parent: {parent_session_id})"
|
|
905
|
+
)
|
|
906
|
+
except Exception as e:
|
|
907
|
+
logger.warning(f"Warning: Could not create subagent session: {e}")
|
|
908
|
+
return {"continue": True}
|
|
909
|
+
|
|
910
|
+
# Override detected agent for subagent context
|
|
911
|
+
detected_agent = f"{subagent_type}-spawner"
|
|
912
|
+
else:
|
|
913
|
+
# Normal orchestrator/parent context
|
|
914
|
+
# CRITICAL: Use session_id from hook_input (Claude Code provides this)
|
|
915
|
+
# Only fall back to manager.get_active_session() if not in hook_input
|
|
916
|
+
# hook_session_id already defined at line 730
|
|
917
|
+
|
|
918
|
+
if hook_session_id:
|
|
919
|
+
# Claude Code provided session_id - use it directly
|
|
920
|
+
# Check if session already exists
|
|
921
|
+
existing = manager.session_converter.load(hook_session_id)
|
|
922
|
+
if existing:
|
|
923
|
+
active_session = existing
|
|
924
|
+
else:
|
|
925
|
+
# Create new session with Claude's session_id
|
|
926
|
+
try:
|
|
927
|
+
active_session = manager.start_session(
|
|
928
|
+
session_id=hook_session_id,
|
|
929
|
+
agent=detected_agent,
|
|
930
|
+
title=f"Session {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
|
931
|
+
)
|
|
932
|
+
except Exception:
|
|
933
|
+
return {"continue": True}
|
|
934
|
+
else:
|
|
935
|
+
# Fallback: No session_id in hook_input - use global session cache
|
|
936
|
+
active_session = manager.get_active_session()
|
|
937
|
+
if not active_session:
|
|
938
|
+
# No active HtmlGraph session yet; start one
|
|
939
|
+
try:
|
|
940
|
+
active_session = manager.start_session(
|
|
941
|
+
session_id=None,
|
|
942
|
+
agent=detected_agent,
|
|
943
|
+
title=f"Session {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
|
944
|
+
)
|
|
945
|
+
except Exception:
|
|
946
|
+
return {"continue": True}
|
|
410
947
|
|
|
411
948
|
active_session_id = active_session.id
|
|
412
949
|
|
|
950
|
+
# Ensure session exists in SQLite database (for foreign key constraints)
|
|
951
|
+
if db:
|
|
952
|
+
try:
|
|
953
|
+
# Get attributes safely - MagicMock objects can cause SQLite binding errors
|
|
954
|
+
# When getattr is called on a MagicMock, it returns another MagicMock, not the default
|
|
955
|
+
def safe_getattr(obj: Any, attr: str, default: Any) -> Any:
|
|
956
|
+
"""Get attribute safely, returning default for MagicMock/invalid values."""
|
|
957
|
+
try:
|
|
958
|
+
val = getattr(obj, attr, default)
|
|
959
|
+
# Check if it's a mock object (has _mock_name attribute)
|
|
960
|
+
if hasattr(val, "_mock_name"):
|
|
961
|
+
return default
|
|
962
|
+
return val
|
|
963
|
+
except Exception:
|
|
964
|
+
return default
|
|
965
|
+
|
|
966
|
+
is_subagent_raw = safe_getattr(active_session, "is_subagent", False)
|
|
967
|
+
is_subagent = (
|
|
968
|
+
bool(is_subagent_raw) if isinstance(is_subagent_raw, bool) else False
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
transcript_id = safe_getattr(active_session, "transcript_id", None)
|
|
972
|
+
transcript_path = safe_getattr(active_session, "transcript_path", None)
|
|
973
|
+
# Ensure strings or None, not mock objects
|
|
974
|
+
if transcript_id is not None and not isinstance(transcript_id, str):
|
|
975
|
+
transcript_id = None
|
|
976
|
+
if transcript_path is not None and not isinstance(transcript_path, str):
|
|
977
|
+
transcript_path = None
|
|
978
|
+
|
|
979
|
+
db.insert_session(
|
|
980
|
+
session_id=active_session_id,
|
|
981
|
+
agent_assigned=safe_getattr(active_session, "agent", None)
|
|
982
|
+
or detected_agent,
|
|
983
|
+
is_subagent=is_subagent,
|
|
984
|
+
transcript_id=transcript_id,
|
|
985
|
+
transcript_path=transcript_path,
|
|
986
|
+
)
|
|
987
|
+
except Exception as e:
|
|
988
|
+
# Session may already exist, that's OK - continue
|
|
989
|
+
logger.warning(
|
|
990
|
+
f"Debug: Could not insert session to SQLite (may already exist): {e}"
|
|
991
|
+
)
|
|
992
|
+
|
|
413
993
|
# Handle different hook types
|
|
414
994
|
if hook_type == "Stop":
|
|
415
995
|
# Session is ending - track stop event
|
|
416
996
|
try:
|
|
417
|
-
manager.track_activity(
|
|
997
|
+
result = manager.track_activity(
|
|
418
998
|
session_id=active_session_id, tool="Stop", summary="Agent stopped"
|
|
419
999
|
)
|
|
1000
|
+
|
|
1001
|
+
# Record to SQLite if available
|
|
1002
|
+
if db:
|
|
1003
|
+
record_event_to_sqlite(
|
|
1004
|
+
db=db,
|
|
1005
|
+
session_id=active_session_id,
|
|
1006
|
+
tool_name="Stop",
|
|
1007
|
+
tool_input={},
|
|
1008
|
+
tool_response={"content": "Agent stopped"},
|
|
1009
|
+
is_error=False,
|
|
1010
|
+
agent_id=detected_agent,
|
|
1011
|
+
model=detected_model,
|
|
1012
|
+
feature_id=result.feature_id if result else None,
|
|
1013
|
+
)
|
|
420
1014
|
except Exception as e:
|
|
421
|
-
|
|
1015
|
+
logger.warning(f"Warning: Could not track stop: {e}")
|
|
422
1016
|
return {"continue": True}
|
|
423
1017
|
|
|
424
1018
|
elif hook_type == "UserPromptSubmit":
|
|
@@ -429,11 +1023,28 @@ def track_event(hook_type: str, hook_input: dict) -> dict:
|
|
|
429
1023
|
preview += "..."
|
|
430
1024
|
|
|
431
1025
|
try:
|
|
432
|
-
manager.track_activity(
|
|
1026
|
+
result = manager.track_activity(
|
|
433
1027
|
session_id=active_session_id, tool="UserQuery", summary=f'"{preview}"'
|
|
434
1028
|
)
|
|
1029
|
+
|
|
1030
|
+
# Record to SQLite if available
|
|
1031
|
+
# UserQuery event is stored in database - no file-based state needed
|
|
1032
|
+
# Subsequent tool calls query database for parent via get_parent_user_query()
|
|
1033
|
+
if db:
|
|
1034
|
+
record_event_to_sqlite(
|
|
1035
|
+
db=db,
|
|
1036
|
+
session_id=active_session_id,
|
|
1037
|
+
tool_name="UserQuery",
|
|
1038
|
+
tool_input={"prompt": prompt},
|
|
1039
|
+
tool_response={"content": "Query received"},
|
|
1040
|
+
is_error=False,
|
|
1041
|
+
agent_id=detected_agent,
|
|
1042
|
+
model=detected_model,
|
|
1043
|
+
feature_id=result.feature_id if result else None,
|
|
1044
|
+
)
|
|
1045
|
+
|
|
435
1046
|
except Exception as e:
|
|
436
|
-
|
|
1047
|
+
logger.warning(f"Warning: Could not track query: {e}")
|
|
437
1048
|
return {"continue": True}
|
|
438
1049
|
|
|
439
1050
|
elif hook_type == "PostToolUse":
|
|
@@ -456,7 +1067,7 @@ def track_event(hook_type: str, hook_input: dict) -> dict:
|
|
|
456
1067
|
summary = format_tool_summary(tool_name, tool_input_data, tool_response)
|
|
457
1068
|
|
|
458
1069
|
# Determine success
|
|
459
|
-
if isinstance(tool_response, dict):
|
|
1070
|
+
if isinstance(tool_response, dict): # type: ignore[arg-type]
|
|
460
1071
|
success_field = tool_response.get("success")
|
|
461
1072
|
if isinstance(success_field, bool):
|
|
462
1073
|
is_error = not success_field
|
|
@@ -479,26 +1090,70 @@ def track_event(hook_type: str, hook_input: dict) -> dict:
|
|
|
479
1090
|
|
|
480
1091
|
# Get drift thresholds from config
|
|
481
1092
|
drift_settings = drift_config.get("drift_detection", {})
|
|
482
|
-
warning_threshold = drift_settings.get("warning_threshold"
|
|
483
|
-
auto_classify_threshold = drift_settings.get("auto_classify_threshold"
|
|
1093
|
+
warning_threshold = drift_settings.get("warning_threshold") or 0.7
|
|
1094
|
+
auto_classify_threshold = drift_settings.get("auto_classify_threshold") or 0.85
|
|
484
1095
|
|
|
485
|
-
# Determine parent activity context
|
|
486
|
-
parent_activity_state = load_parent_activity(graph_dir)
|
|
1096
|
+
# Determine parent activity context using database-only lookup
|
|
487
1097
|
parent_activity_id = None
|
|
488
1098
|
|
|
489
|
-
#
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
1099
|
+
# Check environment variable FIRST for cross-process parent linking
|
|
1100
|
+
# This is set by PreToolUse hook when Task() spawns a subagent
|
|
1101
|
+
env_parent = os.environ.get("HTMLGRAPH_PARENT_EVENT") or os.environ.get(
|
|
1102
|
+
"HTMLGRAPH_PARENT_QUERY_EVENT"
|
|
1103
|
+
)
|
|
1104
|
+
if env_parent:
|
|
1105
|
+
parent_activity_id = env_parent
|
|
1106
|
+
# If we detected a Task delegation event via database detection (Method 3),
|
|
1107
|
+
# use that as the parent for all tool calls within the subagent
|
|
1108
|
+
elif task_event_id_from_db:
|
|
1109
|
+
parent_activity_id = task_event_id_from_db
|
|
1110
|
+
# CRITICAL FIX: Check for active task_delegation EVEN IF task_event_id_from_db not set
|
|
1111
|
+
# This handles Claude Code's session reuse where parent_session_id is NULL
|
|
1112
|
+
# When tool calls come from a subagent, they should be under the task_delegation parent,
|
|
1113
|
+
# NOT under UserQuery. So we MUST check for active tasks BEFORE falling back to UserQuery.
|
|
1114
|
+
# IMPORTANT: This must work EVEN IF db is None, so try to get it from htmlgraph_db
|
|
497
1115
|
else:
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if
|
|
501
|
-
|
|
1116
|
+
# Ensure we have a db connection (may not have been passed in for parent session)
|
|
1117
|
+
db_to_use = db
|
|
1118
|
+
if not db_to_use:
|
|
1119
|
+
try:
|
|
1120
|
+
from htmlgraph.config import get_database_path
|
|
1121
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
1122
|
+
|
|
1123
|
+
db_to_use = HtmlGraphDB(str(get_database_path()))
|
|
1124
|
+
except Exception:
|
|
1125
|
+
db_to_use = None
|
|
1126
|
+
|
|
1127
|
+
# Try to find an active task_delegation event
|
|
1128
|
+
if db_to_use:
|
|
1129
|
+
try:
|
|
1130
|
+
cursor = db_to_use.connection.cursor() # type: ignore[union-attr]
|
|
1131
|
+
cursor.execute(
|
|
1132
|
+
"""
|
|
1133
|
+
SELECT event_id
|
|
1134
|
+
FROM agent_events
|
|
1135
|
+
WHERE event_type = 'task_delegation'
|
|
1136
|
+
AND status = 'started'
|
|
1137
|
+
ORDER BY timestamp DESC
|
|
1138
|
+
LIMIT 1
|
|
1139
|
+
""",
|
|
1140
|
+
)
|
|
1141
|
+
task_row = cursor.fetchone()
|
|
1142
|
+
if task_row:
|
|
1143
|
+
parent_activity_id = task_row[0]
|
|
1144
|
+
logger.warning(
|
|
1145
|
+
f"DEBUG: Found active task_delegation={parent_activity_id} in parent_activity_id fallback"
|
|
1146
|
+
)
|
|
1147
|
+
except Exception as e:
|
|
1148
|
+
logger.warning(
|
|
1149
|
+
f"DEBUG: Error finding task_delegation in parent_activity_id: {e}"
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
# Only if no active task found, fall back to UserQuery
|
|
1153
|
+
if not parent_activity_id:
|
|
1154
|
+
parent_activity_id = get_parent_user_query(
|
|
1155
|
+
db_to_use, active_session_id
|
|
1156
|
+
)
|
|
502
1157
|
|
|
503
1158
|
# Track the activity
|
|
504
1159
|
nudge = None
|
|
@@ -512,11 +1167,42 @@ def track_event(hook_type: str, hook_input: dict) -> dict:
|
|
|
512
1167
|
parent_activity_id=parent_activity_id,
|
|
513
1168
|
)
|
|
514
1169
|
|
|
515
|
-
#
|
|
516
|
-
if
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1170
|
+
# Record to SQLite if available
|
|
1171
|
+
if db:
|
|
1172
|
+
# Extract subagent_type for Task delegations
|
|
1173
|
+
task_subagent_type = None
|
|
1174
|
+
if tool_name == "Task":
|
|
1175
|
+
task_subagent_type = tool_input_data.get(
|
|
1176
|
+
"subagent_type", "general-purpose"
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
record_event_to_sqlite(
|
|
1180
|
+
db=db,
|
|
1181
|
+
session_id=active_session_id,
|
|
1182
|
+
tool_name=tool_name,
|
|
1183
|
+
tool_input=tool_input_data,
|
|
1184
|
+
tool_response=tool_response,
|
|
1185
|
+
is_error=is_error,
|
|
1186
|
+
file_paths=file_paths if file_paths else None,
|
|
1187
|
+
parent_event_id=parent_activity_id, # Link to parent event
|
|
1188
|
+
agent_id=detected_agent,
|
|
1189
|
+
subagent_type=task_subagent_type,
|
|
1190
|
+
model=detected_model,
|
|
1191
|
+
feature_id=result.feature_id if result else None,
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
# If this was a Task() delegation, also record to agent_collaboration
|
|
1195
|
+
if tool_name == "Task" and db:
|
|
1196
|
+
subagent = tool_input_data.get("subagent_type", "general-purpose")
|
|
1197
|
+
description = tool_input_data.get("description", "")
|
|
1198
|
+
record_delegation_to_sqlite(
|
|
1199
|
+
db=db,
|
|
1200
|
+
session_id=active_session_id,
|
|
1201
|
+
from_agent=detected_agent,
|
|
1202
|
+
to_agent=subagent,
|
|
1203
|
+
task_description=description,
|
|
1204
|
+
task_input=tool_input_data,
|
|
1205
|
+
)
|
|
520
1206
|
|
|
521
1207
|
# Check for drift and handle accordingly
|
|
522
1208
|
# Skip drift detection for child activities (they inherit parent's context)
|
|
@@ -524,7 +1210,10 @@ def track_event(hook_type: str, hook_input: dict) -> dict:
|
|
|
524
1210
|
drift_score = result.drift_score
|
|
525
1211
|
feature_id = getattr(result, "feature_id", "unknown")
|
|
526
1212
|
|
|
527
|
-
|
|
1213
|
+
# Skip drift detection if no score available
|
|
1214
|
+
if drift_score is None:
|
|
1215
|
+
pass # No active features - can't calculate drift
|
|
1216
|
+
elif drift_score >= auto_classify_threshold:
|
|
528
1217
|
# High drift - add to classification queue
|
|
529
1218
|
queue = add_to_drift_queue(
|
|
530
1219
|
graph_dir,
|
|
@@ -598,7 +1287,9 @@ Task tool with subagent_type="general-purpose", model="haiku", prompt:
|
|
|
598
1287
|
Or manually create a work item in .htmlgraph/ (bug, feature, spike, or chore)."""
|
|
599
1288
|
|
|
600
1289
|
# Mark classification as triggered
|
|
601
|
-
queue["last_classification"] = datetime.now(
|
|
1290
|
+
queue["last_classification"] = datetime.now(
|
|
1291
|
+
timezone.utc
|
|
1292
|
+
).isoformat()
|
|
602
1293
|
save_drift_queue(graph_dir, queue)
|
|
603
1294
|
else:
|
|
604
1295
|
nudge = f"Drift detected ({drift_score:.2f}): Activity queued for classification ({len(queue['activities'])}/{drift_settings.get('min_activities_before_classify', 3)} needed)."
|
|
@@ -608,7 +1299,7 @@ Or manually create a work item in .htmlgraph/ (bug, feature, spike, or chore).""
|
|
|
608
1299
|
nudge = f"Drift detected ({drift_score:.2f}): Activity may not align with {feature_id}. Consider refocusing or updating the feature."
|
|
609
1300
|
|
|
610
1301
|
except Exception as e:
|
|
611
|
-
|
|
1302
|
+
logger.warning(f"Warning: Could not track activity: {e}")
|
|
612
1303
|
|
|
613
1304
|
# Build response
|
|
614
1305
|
response: dict[str, Any] = {"continue": True}
|