shellbrain 0.1.0__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.
Files changed (165) hide show
  1. app/__init__.py +1 -0
  2. app/__main__.py +7 -0
  3. app/boot/__init__.py +1 -0
  4. app/boot/admin_db.py +88 -0
  5. app/boot/config.py +14 -0
  6. app/boot/create_policy.py +52 -0
  7. app/boot/db.py +70 -0
  8. app/boot/embeddings.py +55 -0
  9. app/boot/home.py +45 -0
  10. app/boot/migrations.py +61 -0
  11. app/boot/read_policy.py +179 -0
  12. app/boot/repos.py +15 -0
  13. app/boot/retrieval.py +3 -0
  14. app/boot/thresholds.py +19 -0
  15. app/boot/update_policy.py +34 -0
  16. app/boot/use_cases.py +22 -0
  17. app/config/__init__.py +1 -0
  18. app/config/defaults/create_policy.yaml +7 -0
  19. app/config/defaults/read_policy.yaml +25 -0
  20. app/config/defaults/runtime.yaml +10 -0
  21. app/config/defaults/thresholds.yaml +3 -0
  22. app/config/defaults/update_policy.yaml +5 -0
  23. app/config/loader.py +58 -0
  24. app/core/__init__.py +1 -0
  25. app/core/contracts/__init__.py +1 -0
  26. app/core/contracts/errors.py +29 -0
  27. app/core/contracts/requests.py +211 -0
  28. app/core/contracts/responses.py +15 -0
  29. app/core/entities/__init__.py +1 -0
  30. app/core/entities/associations.py +58 -0
  31. app/core/entities/episodes.py +66 -0
  32. app/core/entities/evidence.py +29 -0
  33. app/core/entities/facts.py +30 -0
  34. app/core/entities/guidance.py +47 -0
  35. app/core/entities/identity.py +48 -0
  36. app/core/entities/memory.py +34 -0
  37. app/core/entities/runtime_context.py +19 -0
  38. app/core/entities/session_state.py +31 -0
  39. app/core/entities/telemetry.py +152 -0
  40. app/core/entities/utility.py +14 -0
  41. app/core/interfaces/__init__.py +1 -0
  42. app/core/interfaces/clock.py +12 -0
  43. app/core/interfaces/config.py +28 -0
  44. app/core/interfaces/embeddings.py +12 -0
  45. app/core/interfaces/idgen.py +11 -0
  46. app/core/interfaces/repos.py +279 -0
  47. app/core/interfaces/retrieval.py +20 -0
  48. app/core/interfaces/session_state_store.py +33 -0
  49. app/core/interfaces/unit_of_work.py +50 -0
  50. app/core/policies/__init__.py +1 -0
  51. app/core/policies/_shared/__init__.py +1 -0
  52. app/core/policies/_shared/executor.py +132 -0
  53. app/core/policies/_shared/side_effects.py +9 -0
  54. app/core/policies/create_policy/__init__.py +1 -0
  55. app/core/policies/create_policy/pipeline.py +96 -0
  56. app/core/policies/read_policy/__init__.py +1 -0
  57. app/core/policies/read_policy/bm25.py +114 -0
  58. app/core/policies/read_policy/context_pack_builder.py +140 -0
  59. app/core/policies/read_policy/expansion.py +132 -0
  60. app/core/policies/read_policy/fusion_rrf.py +34 -0
  61. app/core/policies/read_policy/lexical_query.py +101 -0
  62. app/core/policies/read_policy/pipeline.py +93 -0
  63. app/core/policies/read_policy/scenario_lift.py +11 -0
  64. app/core/policies/read_policy/scoring.py +61 -0
  65. app/core/policies/read_policy/seed_retrieval.py +54 -0
  66. app/core/policies/read_policy/utility_prior.py +11 -0
  67. app/core/policies/update_policy/__init__.py +1 -0
  68. app/core/policies/update_policy/pipeline.py +80 -0
  69. app/core/use_cases/__init__.py +1 -0
  70. app/core/use_cases/build_guidance.py +85 -0
  71. app/core/use_cases/create_memory.py +26 -0
  72. app/core/use_cases/manage_session_state.py +159 -0
  73. app/core/use_cases/read_memory.py +21 -0
  74. app/core/use_cases/record_episode_sync_telemetry.py +19 -0
  75. app/core/use_cases/record_operation_telemetry.py +32 -0
  76. app/core/use_cases/sync_episode.py +162 -0
  77. app/core/use_cases/update_memory.py +40 -0
  78. app/migrations/__init__.py +1 -0
  79. app/migrations/env.py +65 -0
  80. app/migrations/versions/20260226_0001_initial_schema.py +232 -0
  81. app/migrations/versions/20260312_0002_add_hard_invariants.py +60 -0
  82. app/migrations/versions/20260312_0003_drop_create_confidence.py +40 -0
  83. app/migrations/versions/20260313_0004_episode_sync_hardening.py +71 -0
  84. app/migrations/versions/20260313_0005_evidence_episode_event_refs.py +45 -0
  85. app/migrations/versions/20260318_0006_usage_telemetry_schema.py +175 -0
  86. app/migrations/versions/20260319_0007_identity_session_guidance.py +49 -0
  87. app/migrations/versions/20260320_0008_instance_metadata_and_backup_safety.py +31 -0
  88. app/migrations/versions/__init__.py +1 -0
  89. app/periphery/__init__.py +1 -0
  90. app/periphery/admin/__init__.py +1 -0
  91. app/periphery/admin/backup.py +360 -0
  92. app/periphery/admin/destructive_guard.py +32 -0
  93. app/periphery/admin/doctor.py +192 -0
  94. app/periphery/admin/init.py +996 -0
  95. app/periphery/admin/instance_guard.py +211 -0
  96. app/periphery/admin/machine_state.py +354 -0
  97. app/periphery/admin/privileges.py +42 -0
  98. app/periphery/admin/repo_state.py +266 -0
  99. app/periphery/admin/restore.py +30 -0
  100. app/periphery/cli/__init__.py +1 -0
  101. app/periphery/cli/handlers.py +830 -0
  102. app/periphery/cli/hydration.py +119 -0
  103. app/periphery/cli/main.py +710 -0
  104. app/periphery/cli/presenter_json.py +10 -0
  105. app/periphery/cli/schema_validation.py +201 -0
  106. app/periphery/db/__init__.py +1 -0
  107. app/periphery/db/engine.py +10 -0
  108. app/periphery/db/models/__init__.py +1 -0
  109. app/periphery/db/models/associations.py +55 -0
  110. app/periphery/db/models/episodes.py +55 -0
  111. app/periphery/db/models/evidence.py +19 -0
  112. app/periphery/db/models/experiences.py +33 -0
  113. app/periphery/db/models/instance_metadata.py +17 -0
  114. app/periphery/db/models/memories.py +39 -0
  115. app/periphery/db/models/metadata.py +6 -0
  116. app/periphery/db/models/registry.py +18 -0
  117. app/periphery/db/models/telemetry.py +174 -0
  118. app/periphery/db/models/utility.py +19 -0
  119. app/periphery/db/models/views.py +154 -0
  120. app/periphery/db/repos/__init__.py +1 -0
  121. app/periphery/db/repos/relational/__init__.py +1 -0
  122. app/periphery/db/repos/relational/associations_repo.py +117 -0
  123. app/periphery/db/repos/relational/episodes_repo.py +188 -0
  124. app/periphery/db/repos/relational/evidence_repo.py +82 -0
  125. app/periphery/db/repos/relational/experiences_repo.py +41 -0
  126. app/periphery/db/repos/relational/memories_repo.py +99 -0
  127. app/periphery/db/repos/relational/read_policy_repo.py +202 -0
  128. app/periphery/db/repos/relational/telemetry_repo.py +161 -0
  129. app/periphery/db/repos/relational/utility_repo.py +30 -0
  130. app/periphery/db/repos/semantic/__init__.py +1 -0
  131. app/periphery/db/repos/semantic/keyword_retrieval_repo.py +63 -0
  132. app/periphery/db/repos/semantic/semantic_retrieval_repo.py +111 -0
  133. app/periphery/db/session.py +10 -0
  134. app/periphery/db/uow.py +75 -0
  135. app/periphery/embeddings/__init__.py +1 -0
  136. app/periphery/embeddings/local_provider.py +35 -0
  137. app/periphery/embeddings/query_vector_search.py +18 -0
  138. app/periphery/episodes/__init__.py +1 -0
  139. app/periphery/episodes/claude_code.py +387 -0
  140. app/periphery/episodes/codex.py +423 -0
  141. app/periphery/episodes/launcher.py +66 -0
  142. app/periphery/episodes/normalization.py +31 -0
  143. app/periphery/episodes/poller.py +299 -0
  144. app/periphery/episodes/source_discovery.py +66 -0
  145. app/periphery/episodes/tool_filter.py +165 -0
  146. app/periphery/identity/__init__.py +1 -0
  147. app/periphery/identity/claude_hook_install.py +67 -0
  148. app/periphery/identity/claude_runtime.py +83 -0
  149. app/periphery/identity/codex_runtime.py +32 -0
  150. app/periphery/identity/compatibility.py +38 -0
  151. app/periphery/identity/resolver.py +163 -0
  152. app/periphery/session_state/__init__.py +1 -0
  153. app/periphery/session_state/file_store.py +100 -0
  154. app/periphery/telemetry/__init__.py +33 -0
  155. app/periphery/telemetry/operation_summary.py +299 -0
  156. app/periphery/telemetry/session_selection.py +156 -0
  157. app/periphery/telemetry/sync_summary.py +65 -0
  158. app/periphery/validation/__init__.py +1 -0
  159. app/periphery/validation/integrity_validation.py +253 -0
  160. app/periphery/validation/semantic_validation.py +94 -0
  161. shellbrain-0.1.0.dist-info/METADATA +130 -0
  162. shellbrain-0.1.0.dist-info/RECORD +165 -0
  163. shellbrain-0.1.0.dist-info/WHEEL +5 -0
  164. shellbrain-0.1.0.dist-info/entry_points.txt +2 -0
  165. shellbrain-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,19 @@
1
+ """This module defines SQLAlchemy Core tables for utility observation records."""
2
+
3
+ from sqlalchemy import CheckConstraint, Column, Float, ForeignKey, String, Table
4
+ from sqlalchemy.dialects.postgresql import TIMESTAMP
5
+
6
+ from app.periphery.db.models.metadata import metadata
7
+
8
+
9
+ utility_observations = Table(
10
+ "utility_observations",
11
+ metadata,
12
+ Column("id", String, primary_key=True),
13
+ Column("memory_id", String, ForeignKey("memories.id", ondelete="CASCADE"), nullable=False),
14
+ Column("problem_id", String, ForeignKey("memories.id", ondelete="CASCADE"), nullable=False),
15
+ Column("vote", Float, nullable=False),
16
+ Column("rationale", String),
17
+ Column("created_at", TIMESTAMP(timezone=True), nullable=False),
18
+ CheckConstraint("vote >= -1 AND vote <= 1", name="ck_utility_observations_vote_range"),
19
+ )
@@ -0,0 +1,154 @@
1
+ """This module defines view SQL strings used to build derived read-model views."""
2
+
3
+
4
+ CURRENT_FACT_SNAPSHOT_SQL = """
5
+ CREATE OR REPLACE VIEW current_fact_snapshot AS
6
+ SELECT m.*
7
+ FROM memories m
8
+ WHERE m.kind = 'fact'
9
+ AND m.archived = FALSE
10
+ AND NOT EXISTS (
11
+ SELECT 1 FROM fact_updates fu WHERE fu.old_fact_id = m.id
12
+ );
13
+ """
14
+
15
+
16
+ GLOBAL_UTILITY_SQL = """
17
+ CREATE OR REPLACE VIEW global_utility AS
18
+ SELECT memory_id, AVG(vote) AS utility_mean, COUNT(*) AS observations
19
+ FROM utility_observations
20
+ GROUP BY memory_id;
21
+ """
22
+
23
+
24
+ USAGE_COMMAND_DAILY_SQL = """
25
+ CREATE OR REPLACE VIEW usage_command_daily AS
26
+ SELECT
27
+ repo_id,
28
+ date_trunc('day', created_at AT TIME ZONE 'UTC') AS day_utc,
29
+ command,
30
+ outcome,
31
+ COUNT(*)::INTEGER AS invocation_count,
32
+ AVG(total_latency_ms)::DOUBLE PRECISION AS avg_latency_ms
33
+ FROM operation_invocations
34
+ GROUP BY repo_id, date_trunc('day', created_at AT TIME ZONE 'UTC'), command, outcome;
35
+ """
36
+
37
+
38
+ USAGE_MEMORY_RETRIEVAL_SQL = """
39
+ CREATE OR REPLACE VIEW usage_memory_retrieval AS
40
+ SELECT
41
+ oi.repo_id,
42
+ rri.memory_id,
43
+ rri.kind,
44
+ rri.section,
45
+ COUNT(*)::INTEGER AS retrieval_count,
46
+ MAX(oi.created_at) AS last_seen_at
47
+ FROM read_result_items rri
48
+ JOIN operation_invocations oi ON oi.id = rri.invocation_id
49
+ GROUP BY oi.repo_id, rri.memory_id, rri.kind, rri.section;
50
+ """
51
+
52
+
53
+ USAGE_WRITE_EFFECTS_SQL = """
54
+ CREATE OR REPLACE VIEW usage_write_effects AS
55
+ SELECT
56
+ wei.repo_id,
57
+ date_trunc('day', oi.created_at AT TIME ZONE 'UTC') AS day_utc,
58
+ wei.effect_type,
59
+ COUNT(*)::INTEGER AS effect_count
60
+ FROM write_effect_items wei
61
+ JOIN operation_invocations oi ON oi.id = wei.invocation_id
62
+ GROUP BY wei.repo_id, date_trunc('day', oi.created_at AT TIME ZONE 'UTC'), wei.effect_type;
63
+ """
64
+
65
+
66
+ USAGE_SYNC_HEALTH_SQL = """
67
+ CREATE OR REPLACE VIEW usage_sync_health AS
68
+ WITH sync_grouped AS (
69
+ SELECT
70
+ repo_id,
71
+ host_app,
72
+ date_trunc('day', created_at AT TIME ZONE 'UTC') AS day_utc,
73
+ COUNT(*)::INTEGER AS sync_run_count,
74
+ COUNT(*) FILTER (WHERE outcome = 'error')::INTEGER AS failed_sync_count,
75
+ COALESCE(SUM(imported_event_count), 0)::INTEGER AS imported_event_count
76
+ FROM episode_sync_runs
77
+ GROUP BY repo_id, host_app, date_trunc('day', created_at AT TIME ZONE 'UTC')
78
+ ),
79
+ tool_grouped AS (
80
+ SELECT
81
+ esr.repo_id,
82
+ esr.host_app,
83
+ date_trunc('day', esr.created_at AT TIME ZONE 'UTC') AS day_utc,
84
+ estt.tool_type,
85
+ SUM(estt.event_count)::INTEGER AS event_count
86
+ FROM episode_sync_tool_types estt
87
+ JOIN episode_sync_runs esr ON esr.id = estt.sync_run_id
88
+ GROUP BY esr.repo_id, esr.host_app, date_trunc('day', esr.created_at AT TIME ZONE 'UTC'), estt.tool_type
89
+ ),
90
+ tool_objects AS (
91
+ SELECT
92
+ repo_id,
93
+ host_app,
94
+ day_utc,
95
+ jsonb_object_agg(tool_type, event_count ORDER BY tool_type) AS tool_type_counts
96
+ FROM tool_grouped
97
+ GROUP BY repo_id, host_app, day_utc
98
+ )
99
+ SELECT
100
+ sg.repo_id,
101
+ sg.host_app,
102
+ sg.day_utc,
103
+ sg.sync_run_count,
104
+ sg.failed_sync_count,
105
+ sg.imported_event_count,
106
+ COALESCE(toj.tool_type_counts, '{}'::jsonb) AS tool_type_counts
107
+ FROM sync_grouped sg
108
+ LEFT JOIN tool_objects toj
109
+ ON toj.repo_id = sg.repo_id
110
+ AND toj.host_app = sg.host_app
111
+ AND toj.day_utc = sg.day_utc;
112
+ """
113
+
114
+
115
+ USAGE_SESSION_PROTOCOL_SQL = """
116
+ CREATE OR REPLACE VIEW usage_session_protocol AS
117
+ SELECT
118
+ oi.repo_id,
119
+ oi.selected_thread_id,
120
+ COUNT(*) FILTER (WHERE oi.command = 'read')::INTEGER AS read_count,
121
+ COUNT(*) FILTER (WHERE oi.command = 'events')::INTEGER AS events_count,
122
+ COUNT(*) FILTER (WHERE oi.command IN ('create', 'update'))::INTEGER AS write_count,
123
+ COUNT(*) FILTER (
124
+ WHERE oi.command = 'read'
125
+ AND COALESCE(ris.zero_results, FALSE)
126
+ )::INTEGER AS zero_result_read_count,
127
+ COUNT(*) FILTER (WHERE oi.selection_ambiguous)::INTEGER AS ambiguous_selection_count,
128
+ COUNT(*) FILTER (
129
+ WHERE oi.command IN ('create', 'update')
130
+ AND EXISTS (
131
+ SELECT 1
132
+ FROM operation_invocations prior_events
133
+ WHERE prior_events.repo_id = oi.repo_id
134
+ AND prior_events.selected_thread_id = oi.selected_thread_id
135
+ AND prior_events.command = 'events'
136
+ AND prior_events.created_at < oi.created_at
137
+ )
138
+ )::INTEGER AS writes_preceded_by_events_count,
139
+ COUNT(*) FILTER (
140
+ WHERE oi.command = 'events'
141
+ AND NOT EXISTS (
142
+ SELECT 1
143
+ FROM operation_invocations later_writes
144
+ WHERE later_writes.repo_id = oi.repo_id
145
+ AND later_writes.selected_thread_id = oi.selected_thread_id
146
+ AND later_writes.command IN ('create', 'update')
147
+ AND later_writes.created_at > oi.created_at
148
+ )
149
+ )::INTEGER AS events_without_following_write_count
150
+ FROM operation_invocations oi
151
+ LEFT JOIN read_invocation_summaries ris ON ris.invocation_id = oi.id
152
+ WHERE oi.selected_thread_id IS NOT NULL
153
+ GROUP BY oi.repo_id, oi.selected_thread_id;
154
+ """
@@ -0,0 +1 @@
1
+ """This package groups relational and semantic repository implementations."""
@@ -0,0 +1 @@
1
+ """This package contains aggregate-oriented relational repository classes."""
@@ -0,0 +1,117 @@
1
+ """This module defines relational repository operations for association structures."""
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ from sqlalchemy import case, select, update
6
+
7
+ from app.core.entities.associations import AssociationEdge, AssociationObservation, AssociationSourceMode, AssociationState
8
+ from app.core.interfaces.repos import IAssociationsRepo
9
+ from app.periphery.db.models.associations import association_edges, association_observations
10
+
11
+
12
+ class AssociationsRepo(IAssociationsRepo):
13
+ """This class provides persistence operations for association edges and observations."""
14
+
15
+ def __init__(self, session) -> None:
16
+ """This method stores the active DB session for repository operations."""
17
+
18
+ self._session = session
19
+
20
+ def upsert_edge(self, edge: AssociationEdge) -> AssociationEdge:
21
+ """This method inserts or updates an association edge and returns the stored edge."""
22
+
23
+ existing = (
24
+ self._session.execute(
25
+ select(association_edges).where(
26
+ association_edges.c.repo_id == edge.repo_id,
27
+ association_edges.c.from_memory_id == edge.from_memory_id,
28
+ association_edges.c.to_memory_id == edge.to_memory_id,
29
+ association_edges.c.relation_type == edge.relation_type.value,
30
+ )
31
+ )
32
+ .mappings()
33
+ .first()
34
+ )
35
+
36
+ now = datetime.now(timezone.utc)
37
+ if existing is None:
38
+ self._session.execute(
39
+ association_edges.insert().values(
40
+ id=edge.id,
41
+ repo_id=edge.repo_id,
42
+ from_memory_id=edge.from_memory_id,
43
+ to_memory_id=edge.to_memory_id,
44
+ relation_type=edge.relation_type.value,
45
+ source_mode=edge.source_mode.value,
46
+ state=edge.state.value,
47
+ strength=edge.strength,
48
+ obs_count=0,
49
+ positive_obs=0,
50
+ negative_obs=0,
51
+ salience_sum=0.0,
52
+ created_at=now,
53
+ updated_at=now,
54
+ )
55
+ )
56
+ return edge
57
+
58
+ source_mode = existing["source_mode"]
59
+ if source_mode != edge.source_mode.value and source_mode != "mixed":
60
+ source_mode = "mixed"
61
+ strength = max(float(existing["strength"]), edge.strength)
62
+ self._session.execute(
63
+ update(association_edges)
64
+ .where(association_edges.c.id == existing["id"])
65
+ .values(
66
+ source_mode=source_mode,
67
+ state=edge.state.value,
68
+ strength=strength,
69
+ updated_at=now,
70
+ )
71
+ )
72
+ return AssociationEdge(
73
+ id=existing["id"],
74
+ repo_id=edge.repo_id,
75
+ from_memory_id=edge.from_memory_id,
76
+ to_memory_id=edge.to_memory_id,
77
+ relation_type=edge.relation_type,
78
+ source_mode=AssociationSourceMode(source_mode),
79
+ state=AssociationState(edge.state.value),
80
+ strength=strength,
81
+ )
82
+
83
+ def append_observation(self, observation: AssociationObservation) -> None:
84
+ """This method appends an immutable association observation row."""
85
+
86
+ now = datetime.now(timezone.utc)
87
+ self._session.execute(
88
+ association_observations.insert().values(
89
+ id=observation.id,
90
+ repo_id=observation.repo_id,
91
+ edge_id=observation.edge_id,
92
+ from_memory_id=observation.from_memory_id,
93
+ to_memory_id=observation.to_memory_id,
94
+ relation_type=observation.relation_type.value,
95
+ source=observation.source,
96
+ problem_id=observation.problem_id,
97
+ episode_id=observation.episode_id,
98
+ valence=observation.valence,
99
+ salience=observation.salience,
100
+ created_at=now,
101
+ )
102
+ )
103
+ if observation.edge_id:
104
+ self._session.execute(
105
+ update(association_edges)
106
+ .where(association_edges.c.id == observation.edge_id)
107
+ .values(
108
+ obs_count=association_edges.c.obs_count + 1,
109
+ positive_obs=association_edges.c.positive_obs
110
+ + case((observation.valence > 0, 1), else_=0),
111
+ negative_obs=association_edges.c.negative_obs
112
+ + case((observation.valence < 0, 1), else_=0),
113
+ salience_sum=association_edges.c.salience_sum + observation.salience,
114
+ last_reinforced_at=now,
115
+ updated_at=now,
116
+ )
117
+ )
@@ -0,0 +1,188 @@
1
+ """This module defines relational repository operations for episodic provenance tables."""
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Sequence
5
+
6
+ from sqlalchemy import func, select, update
7
+
8
+ from app.core.entities.episodes import Episode, EpisodeEvent, EpisodeEventSource, EpisodeStatus, SessionTransfer
9
+ from app.core.interfaces.repos import IEpisodesRepo
10
+ from app.periphery.db.models.episodes import episode_events, episodes, session_transfers
11
+
12
+
13
+ class EpisodesRepo(IEpisodesRepo):
14
+ """This class provides persistence operations for episodes, events, and transfers."""
15
+
16
+ def __init__(self, session) -> None:
17
+ """This method stores the active DB session for repository operations."""
18
+
19
+ self._session = session
20
+
21
+ def create_episode(self, episode: Episode) -> None:
22
+ """This method persists an episode row."""
23
+
24
+ self._session.execute(
25
+ episodes.insert().values(
26
+ id=episode.id,
27
+ repo_id=episode.repo_id,
28
+ host_app=episode.host_app,
29
+ thread_id=episode.thread_id,
30
+ title=episode.title,
31
+ objective=episode.objective,
32
+ status=episode.status.value,
33
+ started_at=episode.started_at or datetime.now(timezone.utc),
34
+ ended_at=episode.ended_at,
35
+ created_at=episode.created_at or datetime.now(timezone.utc),
36
+ )
37
+ )
38
+
39
+ def get_episode_by_thread(
40
+ self,
41
+ *,
42
+ repo_id: str,
43
+ thread_id: str,
44
+ ) -> Episode | None:
45
+ """This method fetches one episode by canonical host session key."""
46
+
47
+ row = (
48
+ self._session.execute(
49
+ select(episodes).where(
50
+ episodes.c.repo_id == repo_id,
51
+ episodes.c.thread_id == thread_id,
52
+ )
53
+ )
54
+ .mappings()
55
+ .first()
56
+ )
57
+ if row is None:
58
+ return None
59
+ return Episode(
60
+ id=row["id"],
61
+ repo_id=row["repo_id"],
62
+ host_app=row["host_app"],
63
+ thread_id=row["thread_id"],
64
+ title=row["title"],
65
+ objective=row["objective"],
66
+ status=EpisodeStatus(row["status"]),
67
+ started_at=row["started_at"],
68
+ ended_at=row["ended_at"],
69
+ created_at=row["created_at"],
70
+ )
71
+
72
+ def list_event_keys(self, *, episode_id: str) -> list[str]:
73
+ """This method returns already-imported upstream event keys for one episode."""
74
+
75
+ rows = self._session.execute(
76
+ select(episode_events.c.host_event_key).where(episode_events.c.episode_id == episode_id)
77
+ ).scalars()
78
+ return [str(value) for value in rows]
79
+
80
+ def next_event_seq(self, *, episode_id: str) -> int:
81
+ """This method returns the next append sequence number for one episode."""
82
+
83
+ max_seq = self._session.execute(
84
+ select(func.max(episode_events.c.seq)).where(episode_events.c.episode_id == episode_id)
85
+ ).scalar_one()
86
+ return 1 if max_seq is None else int(max_seq) + 1
87
+
88
+ def append_event(self, event: EpisodeEvent) -> None:
89
+ """This method appends an episode event row."""
90
+
91
+ self._session.execute(
92
+ episode_events.insert().values(
93
+ id=event.id,
94
+ episode_id=event.episode_id,
95
+ seq=event.seq,
96
+ host_event_key=event.host_event_key,
97
+ source=event.source.value,
98
+ content=event.content,
99
+ created_at=event.created_at or datetime.now(timezone.utc),
100
+ )
101
+ )
102
+
103
+ def close_episode(self, *, episode_id: str, ended_at: datetime) -> None:
104
+ """This method marks an active episode closed."""
105
+
106
+ self._session.execute(
107
+ update(episodes)
108
+ .where(episodes.c.id == episode_id)
109
+ .values(status="closed", ended_at=ended_at)
110
+ )
111
+
112
+ def append_transfer(self, transfer: SessionTransfer) -> None:
113
+ """This method appends a session transfer row."""
114
+
115
+ self._session.execute(
116
+ session_transfers.insert().values(
117
+ id=transfer.id,
118
+ repo_id=transfer.repo_id,
119
+ from_episode_id=transfer.from_episode_id,
120
+ to_episode_id=transfer.to_episode_id,
121
+ event_id=transfer.event_id,
122
+ transfer_kind=transfer.transfer_kind,
123
+ rationale=transfer.rationale,
124
+ transferred_by=transfer.transferred_by,
125
+ created_at=transfer.created_at or datetime.now(timezone.utc),
126
+ )
127
+ )
128
+
129
+ def list_existing_event_ids(self, *, event_ids: Sequence[str]) -> list[str]:
130
+ """This method returns stored event ids regardless of repo visibility."""
131
+
132
+ if not event_ids:
133
+ return []
134
+ rows = self._session.execute(
135
+ select(episode_events.c.id).where(episode_events.c.id.in_(event_ids))
136
+ ).scalars()
137
+ return [str(value) for value in rows]
138
+
139
+ def list_visible_event_ids(self, *, repo_id: str, event_ids: Sequence[str]) -> list[str]:
140
+ """This method returns stored event ids visible to one repo."""
141
+
142
+ if not event_ids:
143
+ return []
144
+ rows = self._session.execute(
145
+ select(episode_events.c.id)
146
+ .select_from(episode_events.join(episodes, episode_events.c.episode_id == episodes.c.id))
147
+ .where(
148
+ episodes.c.repo_id == repo_id,
149
+ episode_events.c.id.in_(event_ids),
150
+ )
151
+ ).scalars()
152
+ return [str(value) for value in rows]
153
+
154
+ def list_recent_events(
155
+ self,
156
+ *,
157
+ repo_id: str,
158
+ episode_id: str,
159
+ limit: int,
160
+ ) -> list[EpisodeEvent]:
161
+ """This method returns recent events for one repo-visible episode ordered newest first."""
162
+
163
+ rows = (
164
+ self._session.execute(
165
+ select(episode_events)
166
+ .select_from(episode_events.join(episodes, episode_events.c.episode_id == episodes.c.id))
167
+ .where(
168
+ episodes.c.repo_id == repo_id,
169
+ episode_events.c.episode_id == episode_id,
170
+ )
171
+ .order_by(episode_events.c.seq.desc())
172
+ .limit(limit)
173
+ )
174
+ .mappings()
175
+ .all()
176
+ )
177
+ return [
178
+ EpisodeEvent(
179
+ id=row["id"],
180
+ episode_id=row["episode_id"],
181
+ seq=row["seq"],
182
+ host_event_key=row["host_event_key"],
183
+ source=EpisodeEventSource(row["source"]),
184
+ content=row["content"],
185
+ created_at=row["created_at"],
186
+ )
187
+ for row in rows
188
+ ]
@@ -0,0 +1,82 @@
1
+ """This module defines relational repository operations for evidence references and links."""
2
+
3
+ from datetime import datetime, timezone
4
+ from uuid import uuid4
5
+
6
+ from sqlalchemy import select, update
7
+ from sqlalchemy.dialects.postgresql import insert
8
+
9
+ from app.core.entities.evidence import EvidenceRef
10
+ from app.core.interfaces.repos import IEvidenceRepo
11
+ from app.periphery.db.models.associations import association_edge_evidence
12
+ from app.periphery.db.models.evidence import evidence_refs
13
+ from app.periphery.db.models.memories import memory_evidence
14
+
15
+
16
+ class EvidenceRepo(IEvidenceRepo):
17
+ """This class provides persistence operations for evidence references and links."""
18
+
19
+ def __init__(self, session) -> None:
20
+ """This method stores the active DB session for repository operations."""
21
+
22
+ self._session = session
23
+
24
+ def upsert_ref(self, repo_id: str, ref: str) -> EvidenceRef:
25
+ """This method inserts or returns a canonical evidence reference row."""
26
+
27
+ existing = (
28
+ self._session.execute(
29
+ select(evidence_refs).where(
30
+ evidence_refs.c.repo_id == repo_id,
31
+ (evidence_refs.c.episode_event_id == ref) | (evidence_refs.c.ref == ref),
32
+ )
33
+ )
34
+ .mappings()
35
+ .first()
36
+ )
37
+ if existing:
38
+ if existing["episode_event_id"] is None:
39
+ self._session.execute(
40
+ update(evidence_refs)
41
+ .where(evidence_refs.c.id == existing["id"])
42
+ .values(episode_event_id=ref, ref=ref)
43
+ )
44
+ existing = dict(existing)
45
+ existing["episode_event_id"] = ref
46
+ existing["ref"] = ref
47
+ return EvidenceRef(
48
+ id=existing["id"],
49
+ repo_id=existing["repo_id"],
50
+ ref=existing["ref"],
51
+ episode_event_id=existing["episode_event_id"],
52
+ )
53
+
54
+ evidence_id = str(uuid4())
55
+ self._session.execute(
56
+ evidence_refs.insert().values(
57
+ id=evidence_id,
58
+ repo_id=repo_id,
59
+ ref=ref,
60
+ episode_event_id=ref,
61
+ created_at=datetime.now(timezone.utc),
62
+ )
63
+ )
64
+ return EvidenceRef(id=evidence_id, repo_id=repo_id, ref=ref, episode_event_id=ref)
65
+
66
+ def link_memory_evidence(self, memory_id: str, evidence_id: str) -> None:
67
+ """This method creates shellbrain-to-evidence link rows."""
68
+
69
+ self._session.execute(
70
+ insert(memory_evidence)
71
+ .values(memory_id=memory_id, evidence_id=evidence_id)
72
+ .on_conflict_do_nothing(index_elements=["memory_id", "evidence_id"])
73
+ )
74
+
75
+ def link_association_edge_evidence(self, edge_id: str, evidence_id: str) -> None:
76
+ """This method creates association-edge-to-evidence link rows."""
77
+
78
+ self._session.execute(
79
+ insert(association_edge_evidence)
80
+ .values(edge_id=edge_id, evidence_id=evidence_id)
81
+ .on_conflict_do_nothing(index_elements=["edge_id", "evidence_id"])
82
+ )
@@ -0,0 +1,41 @@
1
+ """This module defines relational repository operations for experiential links."""
2
+
3
+ from datetime import datetime, timezone
4
+
5
+ from app.core.entities.facts import FactUpdate, ProblemAttempt
6
+ from app.core.interfaces.repos import IExperiencesRepo
7
+ from app.periphery.db.models.experiences import fact_updates, problem_attempts
8
+
9
+
10
+ class ExperiencesRepo(IExperiencesRepo):
11
+ """This class provides persistence operations for problem attempts and fact updates."""
12
+
13
+ def __init__(self, session) -> None:
14
+ """This method stores the active DB session for repository operations."""
15
+
16
+ self._session = session
17
+
18
+ def create_problem_attempt(self, attempt: ProblemAttempt) -> None:
19
+ """This method persists a problem-attempt link row."""
20
+
21
+ self._session.execute(
22
+ problem_attempts.insert().values(
23
+ problem_id=attempt.problem_id,
24
+ attempt_id=attempt.attempt_id,
25
+ role=attempt.role.value,
26
+ created_at=datetime.now(timezone.utc),
27
+ )
28
+ )
29
+
30
+ def create_fact_update(self, fact_update: FactUpdate) -> None:
31
+ """This method persists a fact-update chain row."""
32
+
33
+ self._session.execute(
34
+ fact_updates.insert().values(
35
+ id=fact_update.id,
36
+ old_fact_id=fact_update.old_fact_id,
37
+ change_id=fact_update.change_id,
38
+ new_fact_id=fact_update.new_fact_id,
39
+ created_at=datetime.now(timezone.utc),
40
+ )
41
+ )