htmlgraph 0.26.23__py3-none-any.whl → 0.26.24__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/db/schema.py CHANGED
@@ -163,6 +163,12 @@ class HtmlGraphDB:
163
163
  ("completed_at", "DATETIME"),
164
164
  ("last_user_query_at", "DATETIME"),
165
165
  ("last_user_query", "TEXT"),
166
+ # Phase 2 Feature 3: Cross-Session Continuity handoff fields
167
+ ("handoff_notes", "TEXT"),
168
+ ("recommended_next", "TEXT"),
169
+ ("blockers", "TEXT"), # JSON array of blocker strings
170
+ ("recommended_context", "TEXT"), # JSON array of file paths
171
+ ("continued_from", "TEXT"), # Previous session ID
166
172
  ]
167
173
 
168
174
  # Refresh columns after potential rename
@@ -291,8 +297,14 @@ class HtmlGraphDB:
291
297
  metadata JSON,
292
298
  last_user_query_at DATETIME,
293
299
  last_user_query TEXT,
300
+ handoff_notes TEXT,
301
+ recommended_next TEXT,
302
+ blockers JSON,
303
+ recommended_context JSON,
304
+ continued_from TEXT,
294
305
  FOREIGN KEY (parent_session_id) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE,
295
- FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE
306
+ FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE,
307
+ FOREIGN KEY (continued_from) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE
296
308
  )
297
309
  """)
298
310
 
@@ -407,6 +419,23 @@ class HtmlGraphDB:
407
419
  )
408
420
  """)
409
421
 
422
+ # 10. HANDOFF_TRACKING TABLE - Phase 2 Feature 3: Track handoff effectiveness
423
+ cursor.execute("""
424
+ CREATE TABLE IF NOT EXISTS handoff_tracking (
425
+ handoff_id TEXT PRIMARY KEY,
426
+ from_session_id TEXT NOT NULL,
427
+ to_session_id TEXT,
428
+ items_in_context INTEGER DEFAULT 0,
429
+ items_accessed INTEGER DEFAULT 0,
430
+ time_to_resume_seconds INTEGER DEFAULT 0,
431
+ user_rating INTEGER CHECK(user_rating BETWEEN 1 AND 5),
432
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
433
+ resumed_at DATETIME,
434
+ FOREIGN KEY (from_session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
435
+ FOREIGN KEY (to_session_id) REFERENCES sessions(session_id) ON DELETE SET NULL
436
+ )
437
+ """)
438
+
410
439
  # 9. Create indexes for performance
411
440
  self._create_indexes(cursor)
412
441
 
@@ -496,6 +525,10 @@ class HtmlGraphDB:
496
525
  # live_events indexes - optimized for real-time WebSocket streaming
497
526
  "CREATE INDEX IF NOT EXISTS idx_live_events_pending ON live_events(broadcast_at) WHERE broadcast_at IS NULL",
498
527
  "CREATE INDEX IF NOT EXISTS idx_live_events_created ON live_events(created_at DESC)",
528
+ # handoff_tracking indexes - optimized for handoff effectiveness queries
529
+ "CREATE INDEX IF NOT EXISTS idx_handoff_from_session ON handoff_tracking(from_session_id, created_at DESC)",
530
+ "CREATE INDEX IF NOT EXISTS idx_handoff_to_session ON handoff_tracking(to_session_id, resumed_at DESC)",
531
+ "CREATE INDEX IF NOT EXISTS idx_handoff_rating ON handoff_tracking(user_rating, created_at DESC)",
499
532
  ]
500
533
 
501
534
  for index_sql in indexes:
htmlgraph/models.py CHANGED
@@ -975,10 +975,13 @@ class Session(BaseModel):
975
975
  parent_activity: str | None = None # Parent activity ID
976
976
  nesting_depth: int = 0 # Depth of nesting (0 = top-level)
977
977
 
978
- # Handoff context
978
+ # Handoff context (Phase 2 Feature 3: Cross-Session Continuity)
979
979
  handoff_notes: str | None = None
980
980
  recommended_next: str | None = None
981
981
  blockers: list[str] = Field(default_factory=list)
982
+ recommended_context: list[str] = Field(
983
+ default_factory=list
984
+ ) # File paths to keep context for
982
985
 
983
986
  # High-frequency activity log
984
987
  activity_log: list[ActivityEntry] = Field(default_factory=list)
htmlgraph/sdk.py CHANGED
@@ -333,6 +333,11 @@ class SDK:
333
333
  # Context analytics interface (Context usage tracking)
334
334
  self.context = ContextAnalytics(self)
335
335
 
336
+ # Pattern learning interface (Phase 2: Behavior Pattern Learning)
337
+ from htmlgraph.analytics.pattern_learning import PatternLearner
338
+
339
+ self.pattern_learning = PatternLearner(self._directory)
340
+
336
341
  # Lazy-loaded orchestrator for subagent management
337
342
  self._orchestrator: Any = None
338
343
 
@@ -776,6 +781,92 @@ class SDK:
776
781
  blockers=blockers,
777
782
  )
778
783
 
784
+ def continue_from_last(
785
+ self,
786
+ agent: str | None = None,
787
+ auto_create_session: bool = True,
788
+ ) -> tuple[Any, Any]:
789
+ """
790
+ Continue work from the last completed session.
791
+
792
+ Loads context from previous session including handoff notes,
793
+ recommended files, blockers, and recent commits.
794
+
795
+ Args:
796
+ agent: Filter by agent (None = current SDK agent)
797
+ auto_create_session: Create new session if True
798
+
799
+ Returns:
800
+ Tuple of (new_session, resume_info) or (None, None)
801
+
802
+ Example:
803
+ >>> sdk = SDK(agent="claude")
804
+ >>> session, resume = sdk.continue_from_last()
805
+ >>> if resume:
806
+ ... print(resume.summary)
807
+ ... print(resume.next_focus)
808
+ ... for file in resume.recommended_files:
809
+ ... print(f" - {file}")
810
+ """
811
+ if not agent:
812
+ agent = self._agent_id
813
+
814
+ return self.session_manager.continue_from_last(
815
+ agent=agent,
816
+ auto_create_session=auto_create_session,
817
+ )
818
+
819
+ def end_session_with_handoff(
820
+ self,
821
+ session_id: str | None = None,
822
+ summary: str | None = None,
823
+ next_focus: str | None = None,
824
+ blockers: list[str] | None = None,
825
+ keep_context: list[str] | None = None,
826
+ auto_recommend_context: bool = True,
827
+ ) -> Any:
828
+ """
829
+ End session with handoff information for next session.
830
+
831
+ Args:
832
+ session_id: Session to end (None = active session)
833
+ summary: What was accomplished
834
+ next_focus: What should be done next
835
+ blockers: List of blockers
836
+ keep_context: List of files to keep context for
837
+ auto_recommend_context: Auto-recommend files from git
838
+
839
+ Returns:
840
+ Updated Session or None
841
+
842
+ Example:
843
+ >>> sdk.end_session_with_handoff(
844
+ ... summary="Completed OAuth integration",
845
+ ... next_focus="Implement JWT token refresh",
846
+ ... blockers=["Waiting for security review"],
847
+ ... keep_context=["src/auth/oauth.py"]
848
+ ... )
849
+ """
850
+ if not session_id:
851
+ if self._agent_id:
852
+ active = self.session_manager.get_active_session_for_agent(
853
+ self._agent_id
854
+ )
855
+ else:
856
+ active = self.session_manager.get_active_session()
857
+ if not active:
858
+ return None
859
+ session_id = active.id
860
+
861
+ return self.session_manager.end_session_with_handoff(
862
+ session_id=session_id,
863
+ summary=summary,
864
+ next_focus=next_focus,
865
+ blockers=blockers,
866
+ keep_context=keep_context,
867
+ auto_recommend_context=auto_recommend_context,
868
+ )
869
+
779
870
  def start_session(
780
871
  self,
781
872
  session_id: str | None = None,
@@ -186,7 +186,7 @@ class SessionManager:
186
186
  """Mark a session as stale (kept for history but not considered active)."""
187
187
  if session.status != "active":
188
188
  return
189
- now = datetime.now()
189
+ now = datetime.now(timezone.utc)
190
190
  session.status = "stale"
191
191
  session.ended_at = now
192
192
  session.last_activity = now
@@ -742,7 +742,7 @@ class SessionManager:
742
742
  ActivityEntry(
743
743
  tool="SessionEnd",
744
744
  summary="Session ended",
745
- timestamp=datetime.now(),
745
+ timestamp=datetime.now(timezone.utc),
746
746
  )
747
747
  )
748
748
 
@@ -792,6 +792,164 @@ class SessionManager:
792
792
 
793
793
  return session
794
794
 
795
+ def continue_from_last(
796
+ self,
797
+ agent: str | None = None,
798
+ auto_create_session: bool = True,
799
+ ) -> tuple[Session | None, Any]: # Returns (new_session, resume_info)
800
+ """
801
+ Continue work from the last completed session.
802
+
803
+ Loads context from the previous session including:
804
+ - Handoff notes and next focus
805
+ - Blockers
806
+ - Recommended context files
807
+ - Recent commits
808
+ - Features worked on
809
+
810
+ Args:
811
+ agent: Filter by agent (None = current agent)
812
+ auto_create_session: Create new session if True
813
+
814
+ Returns:
815
+ Tuple of (new_session, resume_info) or (None, None) if no previous session
816
+
817
+ Example:
818
+ >>> manager = SessionManager(".htmlgraph")
819
+ >>> new_session, resume = manager.continue_from_last(agent="claude")
820
+ >>> if resume:
821
+ ... print(resume.summary)
822
+ ... print(resume.recommended_files)
823
+ """
824
+ # Import handoff module
825
+ from typing import Any
826
+
827
+ from htmlgraph.sessions.handoff import SessionResume
828
+
829
+ # Create a minimal SDK-like object with just the directory
830
+ # to avoid circular dependency and database initialization issues
831
+ class MinimalSDK:
832
+ def __init__(self, directory: Path) -> None:
833
+ self._directory = directory
834
+
835
+ sdk: Any = MinimalSDK(self.graph_dir)
836
+ resume = SessionResume(sdk)
837
+
838
+ # Get last session
839
+ last_session = resume.get_last_session(agent=agent)
840
+ if not last_session:
841
+ return None, None
842
+
843
+ # Build resume info
844
+ resume_info = resume.build_resume_info(last_session)
845
+
846
+ # Create new session if requested
847
+ new_session = None
848
+ if auto_create_session:
849
+ from htmlgraph.ids import generate_id
850
+
851
+ session_id = generate_id("sess")
852
+ new_session = self.start_session(
853
+ session_id=session_id,
854
+ agent=agent or last_session.agent,
855
+ title=f"Continuing from {last_session.id}",
856
+ )
857
+
858
+ # Link to previous session
859
+ new_session.continued_from = last_session.id
860
+ self.session_converter.save(new_session)
861
+
862
+ return new_session, resume_info
863
+
864
+ def end_session_with_handoff(
865
+ self,
866
+ session_id: str,
867
+ summary: str | None = None,
868
+ next_focus: str | None = None,
869
+ blockers: list[str] | None = None,
870
+ keep_context: list[str] | None = None,
871
+ auto_recommend_context: bool = True,
872
+ ) -> Session | None:
873
+ """
874
+ End session with handoff information for next session.
875
+
876
+ Args:
877
+ session_id: Session to end
878
+ summary: What was accomplished (handoff notes)
879
+ next_focus: What should be done next
880
+ blockers: List of blockers preventing progress
881
+ keep_context: List of files to keep context for
882
+ auto_recommend_context: Auto-recommend files from git history
883
+
884
+ Returns:
885
+ Updated session or None
886
+
887
+ Example:
888
+ >>> manager.end_session_with_handoff(
889
+ ... session_id="sess-123",
890
+ ... summary="Completed OAuth integration",
891
+ ... next_focus="Implement JWT token refresh",
892
+ ... blockers=["Waiting for security review"],
893
+ ... keep_context=["src/auth/oauth.py"]
894
+ ... )
895
+ """
896
+ from htmlgraph.sessions.handoff import (
897
+ ContextRecommender,
898
+ HandoffBuilder,
899
+ )
900
+
901
+ # Get session
902
+ session = self.get_session(session_id)
903
+ if not session:
904
+ return None
905
+
906
+ # Build handoff using HandoffBuilder
907
+ builder = HandoffBuilder(session)
908
+
909
+ if summary:
910
+ builder.add_summary(summary)
911
+
912
+ if next_focus:
913
+ builder.add_next_focus(next_focus)
914
+
915
+ if blockers:
916
+ builder.add_blockers(blockers)
917
+
918
+ if keep_context:
919
+ builder.add_context_files(keep_context)
920
+
921
+ # Auto-recommend context files
922
+ if auto_recommend_context:
923
+ recommender = ContextRecommender()
924
+ builder.auto_recommend_context(recommender, max_files=10)
925
+
926
+ handoff_data = builder.build()
927
+
928
+ # Update session with handoff data
929
+ session.handoff_notes = handoff_data["handoff_notes"]
930
+ session.recommended_next = handoff_data["recommended_next"]
931
+ session.blockers = handoff_data["blockers"]
932
+
933
+ # Store recommended_context as JSON-serializable list
934
+ # (Session model expects list[str], converter will handle serialization)
935
+ if hasattr(session, "__dict__"):
936
+ session.__dict__["recommended_context"] = handoff_data[
937
+ "recommended_context"
938
+ ]
939
+
940
+ # Persist handoff data to database before ending session
941
+ self.session_converter.save(session)
942
+
943
+ # End the session
944
+ self.end_session(session_id)
945
+
946
+ # Track handoff effectiveness (optional - only if database available)
947
+ # Note: SessionManager doesn't have direct database access,
948
+ # handoff tracking is primarily done through SDK
949
+ # Users should use SDK.end_session_with_handoff() for full tracking
950
+
951
+ return session
952
+
795
953
  def release_session_features(self, session_id: str) -> list[str]:
796
954
  """
797
955
  Release all features claimed by a specific session.
@@ -0,0 +1,23 @@
1
+ """
2
+ Session management and continuity.
3
+
4
+ Provides session lifecycle, handoff, and resumption features.
5
+ """
6
+
7
+ from htmlgraph.sessions.handoff import (
8
+ ContextRecommender,
9
+ HandoffBuilder,
10
+ HandoffMetrics,
11
+ HandoffTracker,
12
+ SessionResume,
13
+ SessionResumeInfo,
14
+ )
15
+
16
+ __all__ = [
17
+ "HandoffBuilder",
18
+ "SessionResume",
19
+ "SessionResumeInfo",
20
+ "HandoffTracker",
21
+ "HandoffMetrics",
22
+ "ContextRecommender",
23
+ ]