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
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Session Handoff and Continuity - Phase 2 Feature 3
|
|
5
|
+
|
|
6
|
+
Provides cross-session continuity features:
|
|
7
|
+
- HandoffBuilder: Fluent API for creating handoffs with context
|
|
8
|
+
- SessionResume: Load and resume from previous session
|
|
9
|
+
- HandoffTracker: Track handoff effectiveness metrics
|
|
10
|
+
- ContextRecommender: Suggest files to keep context for next session
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
# End session with handoff
|
|
14
|
+
sdk.sessions.end(
|
|
15
|
+
summary="Completed OAuth integration",
|
|
16
|
+
next_focus="Implement JWT token refresh",
|
|
17
|
+
blockers=["Waiting for security review"],
|
|
18
|
+
keep_context=["src/auth/", "docs/security"]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Resume next session
|
|
22
|
+
resumed = sdk.sessions.continue_from_last()
|
|
23
|
+
if resumed:
|
|
24
|
+
logger.info("%s", resumed.summary)
|
|
25
|
+
logger.info("%s", resumed.recommended_files)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import logging
|
|
31
|
+
import subprocess
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from datetime import datetime, timedelta, timezone
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import TYPE_CHECKING, Any
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from htmlgraph.models import Session
|
|
39
|
+
from htmlgraph.sdk import SDK
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SessionResumeInfo:
|
|
46
|
+
"""Information loaded from previous session for resumption."""
|
|
47
|
+
|
|
48
|
+
session_id: str
|
|
49
|
+
agent: str
|
|
50
|
+
ended_at: datetime | None
|
|
51
|
+
summary: str | None # handoff_notes
|
|
52
|
+
next_focus: str | None # recommended_next
|
|
53
|
+
blockers: list[str]
|
|
54
|
+
recommended_files: list[str]
|
|
55
|
+
worked_on_features: list[str]
|
|
56
|
+
recent_commits: list[dict[str, str]]
|
|
57
|
+
time_since_last: timedelta | None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class HandoffMetrics:
|
|
62
|
+
"""Metrics for a session handoff."""
|
|
63
|
+
|
|
64
|
+
handoff_id: str
|
|
65
|
+
from_session_id: str
|
|
66
|
+
to_session_id: str | None
|
|
67
|
+
items_in_context: int
|
|
68
|
+
items_accessed: int
|
|
69
|
+
time_to_resume_seconds: int
|
|
70
|
+
user_rating: int | None
|
|
71
|
+
created_at: datetime
|
|
72
|
+
resumed_at: datetime | None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ContextRecommender:
|
|
76
|
+
"""
|
|
77
|
+
Recommends files to keep context for next session.
|
|
78
|
+
|
|
79
|
+
Uses git history to identify recently edited files and
|
|
80
|
+
combines with feature context.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, repo_root: Path | None = None):
|
|
84
|
+
"""
|
|
85
|
+
Initialize ContextRecommender.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
repo_root: Root of git repository (auto-detected if None)
|
|
89
|
+
"""
|
|
90
|
+
self.repo_root = repo_root or self._find_repo_root()
|
|
91
|
+
|
|
92
|
+
def _find_repo_root(self) -> Path | None:
|
|
93
|
+
"""Find git repository root."""
|
|
94
|
+
try:
|
|
95
|
+
result = subprocess.run(
|
|
96
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
check=True,
|
|
100
|
+
timeout=5,
|
|
101
|
+
)
|
|
102
|
+
return Path(result.stdout.strip())
|
|
103
|
+
except (
|
|
104
|
+
subprocess.CalledProcessError,
|
|
105
|
+
FileNotFoundError,
|
|
106
|
+
subprocess.TimeoutExpired,
|
|
107
|
+
):
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
def get_recent_files(
|
|
111
|
+
self,
|
|
112
|
+
since_minutes: int = 60,
|
|
113
|
+
max_files: int = 10,
|
|
114
|
+
exclude_patterns: list[str] | None = None,
|
|
115
|
+
) -> list[str]:
|
|
116
|
+
"""
|
|
117
|
+
Get recently edited files from git.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
since_minutes: Time window to check
|
|
121
|
+
max_files: Maximum files to return
|
|
122
|
+
exclude_patterns: Patterns to exclude (e.g., ["*.md", "tests/*"])
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of file paths (relative to repo root)
|
|
126
|
+
"""
|
|
127
|
+
if not self.repo_root:
|
|
128
|
+
return []
|
|
129
|
+
|
|
130
|
+
exclude_patterns = exclude_patterns or []
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# Get files changed in last N minutes
|
|
134
|
+
result = subprocess.run(
|
|
135
|
+
[
|
|
136
|
+
"git",
|
|
137
|
+
"log",
|
|
138
|
+
f"--since={since_minutes} minutes ago",
|
|
139
|
+
"--name-only",
|
|
140
|
+
"--pretty=format:",
|
|
141
|
+
"--diff-filter=AMR", # Added, Modified, Renamed
|
|
142
|
+
],
|
|
143
|
+
cwd=str(self.repo_root),
|
|
144
|
+
capture_output=True,
|
|
145
|
+
text=True,
|
|
146
|
+
check=True,
|
|
147
|
+
timeout=10,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Parse files and deduplicate
|
|
151
|
+
files = []
|
|
152
|
+
seen = set()
|
|
153
|
+
for line in result.stdout.strip().split("\n"):
|
|
154
|
+
line = line.strip()
|
|
155
|
+
if not line or line in seen:
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
# Check exclusion patterns
|
|
159
|
+
excluded = False
|
|
160
|
+
for pattern in exclude_patterns:
|
|
161
|
+
if self._matches_pattern(line, pattern):
|
|
162
|
+
excluded = True
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
if not excluded:
|
|
166
|
+
files.append(line)
|
|
167
|
+
seen.add(line)
|
|
168
|
+
|
|
169
|
+
if len(files) >= max_files:
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
return files
|
|
173
|
+
|
|
174
|
+
except (
|
|
175
|
+
subprocess.CalledProcessError,
|
|
176
|
+
subprocess.TimeoutExpired,
|
|
177
|
+
FileNotFoundError,
|
|
178
|
+
):
|
|
179
|
+
logger.debug("Could not get recent files from git")
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
def _matches_pattern(self, path: str, pattern: str) -> bool:
|
|
183
|
+
"""Check if path matches glob pattern."""
|
|
184
|
+
import fnmatch
|
|
185
|
+
|
|
186
|
+
return fnmatch.fnmatch(path, pattern)
|
|
187
|
+
|
|
188
|
+
def recommend_for_session(
|
|
189
|
+
self,
|
|
190
|
+
session: Session,
|
|
191
|
+
max_files: int = 10,
|
|
192
|
+
) -> list[str]:
|
|
193
|
+
"""
|
|
194
|
+
Recommend files to keep context for next session.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
session: Session ending with handoff
|
|
198
|
+
max_files: Maximum files to recommend
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List of recommended file paths
|
|
202
|
+
"""
|
|
203
|
+
# Get recently edited files
|
|
204
|
+
recent_files = self.get_recent_files(
|
|
205
|
+
since_minutes=120, # 2 hours
|
|
206
|
+
max_files=max_files,
|
|
207
|
+
exclude_patterns=["*.md", "*.txt", "*.json", "__pycache__/*"],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# TODO: Could enhance this by:
|
|
211
|
+
# - Checking which files were Read/Edit in session activity log
|
|
212
|
+
# - Prioritizing files related to features worked on
|
|
213
|
+
# - Using file change frequency
|
|
214
|
+
|
|
215
|
+
return recent_files[:max_files]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class HandoffBuilder:
|
|
219
|
+
"""
|
|
220
|
+
Fluent builder for creating session handoffs.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
handoff = HandoffBuilder(session)
|
|
224
|
+
.add_summary("Completed OAuth integration")
|
|
225
|
+
.add_next_focus("Implement JWT token refresh")
|
|
226
|
+
.add_blockers(["Waiting for security review"])
|
|
227
|
+
.add_context_files(["src/auth/oauth.py", "docs/security.md"])
|
|
228
|
+
.build()
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def __init__(self, session: Session):
|
|
232
|
+
"""
|
|
233
|
+
Initialize HandoffBuilder.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
session: Session to add handoff to
|
|
237
|
+
"""
|
|
238
|
+
self.session = session
|
|
239
|
+
self._summary: str | None = None
|
|
240
|
+
self._next_focus: str | None = None
|
|
241
|
+
self._blockers: list[str] = []
|
|
242
|
+
self._context_files: list[str] = []
|
|
243
|
+
|
|
244
|
+
def add_summary(self, summary: str) -> HandoffBuilder:
|
|
245
|
+
"""
|
|
246
|
+
Add handoff summary (what was accomplished).
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
summary: Summary of what was done
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Self for chaining
|
|
253
|
+
"""
|
|
254
|
+
self._summary = summary
|
|
255
|
+
return self
|
|
256
|
+
|
|
257
|
+
def add_next_focus(self, next_focus: str) -> HandoffBuilder:
|
|
258
|
+
"""
|
|
259
|
+
Add recommended next focus.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
next_focus: What should be done next
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Self for chaining
|
|
266
|
+
"""
|
|
267
|
+
self._next_focus = next_focus
|
|
268
|
+
return self
|
|
269
|
+
|
|
270
|
+
def add_blocker(self, blocker: str) -> HandoffBuilder:
|
|
271
|
+
"""
|
|
272
|
+
Add a single blocker.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
blocker: Description of blocker
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Self for chaining
|
|
279
|
+
"""
|
|
280
|
+
self._blockers.append(blocker)
|
|
281
|
+
return self
|
|
282
|
+
|
|
283
|
+
def add_blockers(self, blockers: list[str]) -> HandoffBuilder:
|
|
284
|
+
"""
|
|
285
|
+
Add multiple blockers.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
blockers: List of blocker descriptions
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Self for chaining
|
|
292
|
+
"""
|
|
293
|
+
self._blockers.extend(blockers)
|
|
294
|
+
return self
|
|
295
|
+
|
|
296
|
+
def add_context_file(self, file_path: str) -> HandoffBuilder:
|
|
297
|
+
"""
|
|
298
|
+
Add a file to keep context for.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
file_path: Path to file
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Self for chaining
|
|
305
|
+
"""
|
|
306
|
+
self._context_files.append(file_path)
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
def add_context_files(self, file_paths: list[str]) -> HandoffBuilder:
|
|
310
|
+
"""
|
|
311
|
+
Add multiple files to keep context for.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
file_paths: List of file paths
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Self for chaining
|
|
318
|
+
"""
|
|
319
|
+
self._context_files.extend(file_paths)
|
|
320
|
+
return self
|
|
321
|
+
|
|
322
|
+
def auto_recommend_context(
|
|
323
|
+
self,
|
|
324
|
+
recommender: ContextRecommender | None = None,
|
|
325
|
+
max_files: int = 10,
|
|
326
|
+
) -> HandoffBuilder:
|
|
327
|
+
"""
|
|
328
|
+
Automatically recommend context files.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
recommender: ContextRecommender instance (creates new if None)
|
|
332
|
+
max_files: Maximum files to recommend
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Self for chaining
|
|
336
|
+
"""
|
|
337
|
+
if recommender is None:
|
|
338
|
+
recommender = ContextRecommender()
|
|
339
|
+
|
|
340
|
+
recommended = recommender.recommend_for_session(
|
|
341
|
+
self.session, max_files=max_files
|
|
342
|
+
)
|
|
343
|
+
self._context_files.extend(recommended)
|
|
344
|
+
return self
|
|
345
|
+
|
|
346
|
+
def build(self) -> dict[str, Any]:
|
|
347
|
+
"""
|
|
348
|
+
Build handoff data dictionary.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Dictionary with handoff data
|
|
352
|
+
"""
|
|
353
|
+
return {
|
|
354
|
+
"handoff_notes": self._summary,
|
|
355
|
+
"recommended_next": self._next_focus,
|
|
356
|
+
"blockers": self._blockers,
|
|
357
|
+
"recommended_context": self._context_files,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class SessionResume:
|
|
362
|
+
"""
|
|
363
|
+
Loads and presents context from previous session for resumption.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
def __init__(self, sdk: SDK):
|
|
367
|
+
"""
|
|
368
|
+
Initialize SessionResume.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
sdk: SDK instance
|
|
372
|
+
"""
|
|
373
|
+
self.sdk = sdk
|
|
374
|
+
self.graph_dir = sdk._directory
|
|
375
|
+
|
|
376
|
+
def get_last_session(self, agent: str | None = None) -> Session | None:
|
|
377
|
+
"""
|
|
378
|
+
Get the most recent completed session.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
agent: Filter by agent (None = any agent)
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
Most recent session or None
|
|
385
|
+
"""
|
|
386
|
+
from htmlgraph.converter import SessionConverter
|
|
387
|
+
|
|
388
|
+
converter = SessionConverter(self.graph_dir / "sessions")
|
|
389
|
+
sessions = converter.load_all()
|
|
390
|
+
|
|
391
|
+
# Filter by ended sessions
|
|
392
|
+
ended = [s for s in sessions if s.status == "ended"]
|
|
393
|
+
|
|
394
|
+
# Filter by agent if specified
|
|
395
|
+
if agent:
|
|
396
|
+
ended = [s for s in ended if s.agent == agent]
|
|
397
|
+
|
|
398
|
+
if not ended:
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
# Sort by ended_at (most recent first)
|
|
402
|
+
ended.sort(key=lambda s: s.ended_at or datetime.min, reverse=True)
|
|
403
|
+
return ended[0]
|
|
404
|
+
|
|
405
|
+
def build_resume_info(self, session: Session) -> SessionResumeInfo:
|
|
406
|
+
"""
|
|
407
|
+
Build resumption information from a session.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
session: Previous session
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
SessionResumeInfo with context for resumption
|
|
414
|
+
"""
|
|
415
|
+
# Calculate time since last session
|
|
416
|
+
time_since = None
|
|
417
|
+
if session.ended_at:
|
|
418
|
+
time_since = datetime.now(timezone.utc) - session.ended_at
|
|
419
|
+
|
|
420
|
+
# Get recent commits
|
|
421
|
+
recent_commits = self._get_recent_commits(since_commit=session.start_commit)
|
|
422
|
+
|
|
423
|
+
return SessionResumeInfo(
|
|
424
|
+
session_id=session.id,
|
|
425
|
+
agent=session.agent,
|
|
426
|
+
ended_at=session.ended_at,
|
|
427
|
+
summary=session.handoff_notes,
|
|
428
|
+
next_focus=session.recommended_next,
|
|
429
|
+
blockers=session.blockers,
|
|
430
|
+
recommended_files=self._parse_json_list(session, "recommended_context"),
|
|
431
|
+
worked_on_features=session.worked_on,
|
|
432
|
+
recent_commits=recent_commits,
|
|
433
|
+
time_since_last=time_since,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def _parse_json_list(self, session: Session, field_name: str) -> list[str]:
|
|
437
|
+
"""Parse JSON list field from session."""
|
|
438
|
+
# Session model stores these as Python lists already
|
|
439
|
+
value = getattr(session, field_name, None)
|
|
440
|
+
if isinstance(value, list):
|
|
441
|
+
return [str(item) for item in value] # Ensure list[str]
|
|
442
|
+
if isinstance(value, str):
|
|
443
|
+
try:
|
|
444
|
+
result = json.loads(value)
|
|
445
|
+
return (
|
|
446
|
+
[str(item) for item in result] if isinstance(result, list) else []
|
|
447
|
+
)
|
|
448
|
+
except json.JSONDecodeError:
|
|
449
|
+
return []
|
|
450
|
+
return []
|
|
451
|
+
|
|
452
|
+
def _get_recent_commits(
|
|
453
|
+
self, since_commit: str | None = None, limit: int = 5
|
|
454
|
+
) -> list[dict[str, str]]:
|
|
455
|
+
"""
|
|
456
|
+
Get recent git commits.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
since_commit: Get commits since this one
|
|
460
|
+
limit: Maximum commits to return
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
List of commit dictionaries with hash, message, author, date
|
|
464
|
+
"""
|
|
465
|
+
try:
|
|
466
|
+
args = ["git", "log", f"-{limit}", "--oneline", "--no-merges"]
|
|
467
|
+
if since_commit:
|
|
468
|
+
args.append(f"{since_commit}..HEAD")
|
|
469
|
+
|
|
470
|
+
result = subprocess.run(
|
|
471
|
+
args,
|
|
472
|
+
capture_output=True,
|
|
473
|
+
text=True,
|
|
474
|
+
check=True,
|
|
475
|
+
timeout=5,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
commits = []
|
|
479
|
+
for line in result.stdout.strip().split("\n"):
|
|
480
|
+
if not line:
|
|
481
|
+
continue
|
|
482
|
+
parts = line.split(" ", 1)
|
|
483
|
+
if len(parts) == 2:
|
|
484
|
+
commits.append({"hash": parts[0], "message": parts[1]})
|
|
485
|
+
|
|
486
|
+
return commits
|
|
487
|
+
|
|
488
|
+
except (
|
|
489
|
+
subprocess.CalledProcessError,
|
|
490
|
+
subprocess.TimeoutExpired,
|
|
491
|
+
FileNotFoundError,
|
|
492
|
+
):
|
|
493
|
+
logger.debug("Could not get recent commits")
|
|
494
|
+
return []
|
|
495
|
+
|
|
496
|
+
def format_resume_prompt(self, info: SessionResumeInfo) -> str:
|
|
497
|
+
"""
|
|
498
|
+
Format a user-friendly resumption prompt.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
info: Session resumption information
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Formatted multi-line string for display
|
|
505
|
+
"""
|
|
506
|
+
lines = [
|
|
507
|
+
"═" * 70,
|
|
508
|
+
"CONTINUE FROM LAST SESSION",
|
|
509
|
+
"═" * 70,
|
|
510
|
+
]
|
|
511
|
+
|
|
512
|
+
# Session info
|
|
513
|
+
if info.ended_at:
|
|
514
|
+
lines.append(
|
|
515
|
+
f'Last: {info.ended_at.strftime("%A %I:%M %p")} - "{info.summary or "No summary"}"'
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
lines.append(f"Last: {info.session_id}")
|
|
519
|
+
|
|
520
|
+
# Time gap
|
|
521
|
+
if info.time_since_last:
|
|
522
|
+
hours = info.time_since_last.total_seconds() / 3600
|
|
523
|
+
if hours < 1:
|
|
524
|
+
time_str = (
|
|
525
|
+
f"{int(info.time_since_last.total_seconds() / 60)} minutes ago"
|
|
526
|
+
)
|
|
527
|
+
elif hours < 24:
|
|
528
|
+
time_str = f"{int(hours)} hours ago"
|
|
529
|
+
else:
|
|
530
|
+
time_str = f"{int(hours / 24)} days ago"
|
|
531
|
+
lines.append(f"Gap: {time_str}")
|
|
532
|
+
|
|
533
|
+
lines.append("")
|
|
534
|
+
|
|
535
|
+
# Next focus
|
|
536
|
+
if info.next_focus:
|
|
537
|
+
lines.append("Next Focus:")
|
|
538
|
+
lines.append(f" {info.next_focus}")
|
|
539
|
+
lines.append("")
|
|
540
|
+
|
|
541
|
+
# Blockers
|
|
542
|
+
if info.blockers:
|
|
543
|
+
lines.append("Blockers:")
|
|
544
|
+
for blocker in info.blockers:
|
|
545
|
+
lines.append(f" ⚠️ {blocker}")
|
|
546
|
+
lines.append("")
|
|
547
|
+
|
|
548
|
+
# Context files
|
|
549
|
+
if info.recommended_files:
|
|
550
|
+
lines.append("Context to Load:")
|
|
551
|
+
for i, file_path in enumerate(info.recommended_files[:5], 1):
|
|
552
|
+
lines.append(f" {i}. {file_path}")
|
|
553
|
+
if len(info.recommended_files) > 5:
|
|
554
|
+
lines.append(f" ... and {len(info.recommended_files) - 5} more")
|
|
555
|
+
lines.append("")
|
|
556
|
+
|
|
557
|
+
# Features worked on
|
|
558
|
+
if info.worked_on_features:
|
|
559
|
+
lines.append("Features in Progress:")
|
|
560
|
+
for feature_id in info.worked_on_features[:3]:
|
|
561
|
+
lines.append(f" - {feature_id}")
|
|
562
|
+
if len(info.worked_on_features) > 3:
|
|
563
|
+
lines.append(f" ... and {len(info.worked_on_features) - 3} more")
|
|
564
|
+
lines.append("")
|
|
565
|
+
|
|
566
|
+
# Recent commits
|
|
567
|
+
if info.recent_commits:
|
|
568
|
+
lines.append("Recent Commits:")
|
|
569
|
+
for commit in info.recent_commits[:3]:
|
|
570
|
+
lines.append(f" {commit['hash']} {commit['message']}")
|
|
571
|
+
lines.append("")
|
|
572
|
+
|
|
573
|
+
lines.append(
|
|
574
|
+
"[L]oad context files [O]pen in editor [S]how summary [C]ontinue"
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
return "\n".join(lines)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class HandoffTracker:
|
|
581
|
+
"""
|
|
582
|
+
Tracks handoff effectiveness metrics.
|
|
583
|
+
|
|
584
|
+
Records how helpful handoffs are and enables optimization.
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
def __init__(self, sdk: SDK):
|
|
588
|
+
"""
|
|
589
|
+
Initialize HandoffTracker.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
sdk: SDK instance
|
|
593
|
+
"""
|
|
594
|
+
self.sdk = sdk
|
|
595
|
+
self.db = getattr(sdk, "_db", None)
|
|
596
|
+
|
|
597
|
+
def create_handoff(
|
|
598
|
+
self,
|
|
599
|
+
from_session_id: str,
|
|
600
|
+
items_in_context: int = 0,
|
|
601
|
+
) -> str:
|
|
602
|
+
"""
|
|
603
|
+
Create a handoff tracking record.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
from_session_id: Session ending with handoff
|
|
607
|
+
items_in_context: Number of context items provided
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
Handoff ID
|
|
611
|
+
"""
|
|
612
|
+
from htmlgraph.ids import generate_id
|
|
613
|
+
|
|
614
|
+
handoff_id = generate_id("hand")
|
|
615
|
+
|
|
616
|
+
if self.db and self.db.connection:
|
|
617
|
+
# Ensure session exists in database (handles FK constraint)
|
|
618
|
+
self.db._ensure_session_exists(from_session_id)
|
|
619
|
+
|
|
620
|
+
cursor = self.db.connection.cursor()
|
|
621
|
+
cursor.execute(
|
|
622
|
+
"""
|
|
623
|
+
INSERT INTO handoff_tracking
|
|
624
|
+
(handoff_id, from_session_id, items_in_context)
|
|
625
|
+
VALUES (?, ?, ?)
|
|
626
|
+
""",
|
|
627
|
+
(handoff_id, from_session_id, items_in_context),
|
|
628
|
+
)
|
|
629
|
+
self.db.connection.commit()
|
|
630
|
+
|
|
631
|
+
return handoff_id
|
|
632
|
+
|
|
633
|
+
def resume_handoff(
|
|
634
|
+
self,
|
|
635
|
+
handoff_id: str,
|
|
636
|
+
to_session_id: str,
|
|
637
|
+
items_accessed: int = 0,
|
|
638
|
+
time_to_resume_seconds: int = 0,
|
|
639
|
+
) -> bool:
|
|
640
|
+
"""
|
|
641
|
+
Update handoff with resumption data.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
handoff_id: Handoff ID
|
|
645
|
+
to_session_id: New session ID
|
|
646
|
+
items_accessed: Number of context items accessed
|
|
647
|
+
time_to_resume_seconds: Time to resume work (seconds)
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
True if successful
|
|
651
|
+
"""
|
|
652
|
+
if not self.db or not self.db.connection:
|
|
653
|
+
return False
|
|
654
|
+
|
|
655
|
+
try:
|
|
656
|
+
# Ensure to_session exists in database (handles FK constraint)
|
|
657
|
+
self.db._ensure_session_exists(to_session_id)
|
|
658
|
+
|
|
659
|
+
cursor = self.db.connection.cursor()
|
|
660
|
+
cursor.execute(
|
|
661
|
+
"""
|
|
662
|
+
UPDATE handoff_tracking
|
|
663
|
+
SET to_session_id = ?,
|
|
664
|
+
items_accessed = ?,
|
|
665
|
+
time_to_resume_seconds = ?,
|
|
666
|
+
resumed_at = CURRENT_TIMESTAMP
|
|
667
|
+
WHERE handoff_id = ?
|
|
668
|
+
""",
|
|
669
|
+
(to_session_id, items_accessed, time_to_resume_seconds, handoff_id),
|
|
670
|
+
)
|
|
671
|
+
self.db.connection.commit()
|
|
672
|
+
return True
|
|
673
|
+
except Exception as e:
|
|
674
|
+
logger.error(f"Error updating handoff: {e}")
|
|
675
|
+
return False
|
|
676
|
+
|
|
677
|
+
def rate_handoff(self, handoff_id: str, rating: int) -> bool:
|
|
678
|
+
"""
|
|
679
|
+
Rate handoff effectiveness (1-5 scale).
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
handoff_id: Handoff ID
|
|
683
|
+
rating: Rating (1-5)
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
True if successful
|
|
687
|
+
"""
|
|
688
|
+
if not 1 <= rating <= 5:
|
|
689
|
+
raise ValueError("Rating must be between 1 and 5")
|
|
690
|
+
|
|
691
|
+
if not self.db or not self.db.connection:
|
|
692
|
+
return False
|
|
693
|
+
|
|
694
|
+
try:
|
|
695
|
+
cursor = self.db.connection.cursor()
|
|
696
|
+
cursor.execute(
|
|
697
|
+
"""
|
|
698
|
+
UPDATE handoff_tracking
|
|
699
|
+
SET user_rating = ?
|
|
700
|
+
WHERE handoff_id = ?
|
|
701
|
+
""",
|
|
702
|
+
(rating, handoff_id),
|
|
703
|
+
)
|
|
704
|
+
self.db.connection.commit()
|
|
705
|
+
return True
|
|
706
|
+
except Exception as e:
|
|
707
|
+
logger.error(f"Error rating handoff: {e}")
|
|
708
|
+
return False
|
|
709
|
+
|
|
710
|
+
def get_handoff_metrics(self, limit: int = 10) -> list[HandoffMetrics]:
|
|
711
|
+
"""
|
|
712
|
+
Get recent handoff metrics.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
limit: Maximum records to return
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
List of HandoffMetrics
|
|
719
|
+
"""
|
|
720
|
+
if not self.db or not self.db.connection:
|
|
721
|
+
return []
|
|
722
|
+
|
|
723
|
+
try:
|
|
724
|
+
cursor = self.db.connection.cursor()
|
|
725
|
+
cursor.execute(
|
|
726
|
+
"""
|
|
727
|
+
SELECT handoff_id, from_session_id, to_session_id,
|
|
728
|
+
items_in_context, items_accessed, time_to_resume_seconds,
|
|
729
|
+
user_rating, created_at, resumed_at
|
|
730
|
+
FROM handoff_tracking
|
|
731
|
+
ORDER BY created_at DESC
|
|
732
|
+
LIMIT ?
|
|
733
|
+
""",
|
|
734
|
+
(limit,),
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
metrics = []
|
|
738
|
+
for row in cursor.fetchall():
|
|
739
|
+
metrics.append(
|
|
740
|
+
HandoffMetrics(
|
|
741
|
+
handoff_id=row[0],
|
|
742
|
+
from_session_id=row[1],
|
|
743
|
+
to_session_id=row[2],
|
|
744
|
+
items_in_context=row[3],
|
|
745
|
+
items_accessed=row[4],
|
|
746
|
+
time_to_resume_seconds=row[5],
|
|
747
|
+
user_rating=row[6],
|
|
748
|
+
created_at=datetime.fromisoformat(row[7]),
|
|
749
|
+
resumed_at=(datetime.fromisoformat(row[8]) if row[8] else None),
|
|
750
|
+
)
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
return metrics
|
|
754
|
+
except Exception as e:
|
|
755
|
+
logger.error(f"Error getting handoff metrics: {e}")
|
|
756
|
+
return []
|