Kode-Animator commited on
Commit
1fce18e
·
1 Parent(s): d03b09f

feat: Phase 1 Sovereign Thought Substrate — C-THOUGHT-NODE-001

Browse files

- D1 schema: sessions, thought_units, thought_edges, thought_reviews,
witness_events, version_history, pattern_registry
- FastAPI: 15 REST endpoints (sessions, thoughts, forking, proposals,
tree viz, search, edges, witness/provenance)
- Cloudflare D1 HTTP client (proven pattern from Federation Embassy)
- Pydantic models with strict enum typing
- Docker + requirements for HF Spaces deployment
- Implementation Gap Map with Conformance Matrix + Drift Docket

Files changed (8) hide show
  1. .gitignore +5 -0
  2. C-THOUGHT-NODE-001_IMPLEMENTATION_GAP_MAP.md +110 -0
  3. Dockerfile +16 -0
  4. app.py +471 -0
  5. d1_client.py +47 -0
  6. models.py +133 -0
  7. requirements.txt +5 -0
  8. schema.sql +115 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .env
4
+ .env.local
5
+ *.bak
C-THOUGHT-NODE-001_IMPLEMENTATION_GAP_MAP.md ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🧠 C-THOUGHT-NODE-001: THOUGHT ENGINE CAPABILITY MAP
2
+ **Architecture Target**: Sovereign REST Substrate (Cloudflare D1 Backed)
3
+ **Goal**: Deconstruct existing legacy MCP prototypes, Serena algorithms, and Alethiometer specifications into a unified, modular mapping for the D1-backed FastAPI Hub.
4
+
5
+ ---
6
+
7
+ ## I. Core Logic Substrate (The Serena Bubble Fabric)
8
+ *Origin: `thought_engine_fastmcp.py` & `production_mcp_server.py`*
9
+
10
+ **Base Capabilities:**
11
+ - **[CAP-I.1] Thought Instantiation:** Create a root context containing global session metadata.
12
+ - **[CAP-I.2] Sequential Addition:** Append linear nodes directly to an active context pointer (Chronicle mode).
13
+ - **[CAP-I.3] Tree Forking / Branching:** Clone an existing reasoning node into an isolated vector, preserving parent relationships but mutating children.
14
+ - **[CAP-I.4] Symbolic Replacement:** Destructive edits to a specific node body (creates standard version history archive).
15
+ - **[CAP-I.5] Graph Rendering:** Render hierarchical tree representation of nodes, branches, and statuses.
16
+
17
+ ---
18
+
19
+ ## II. Cognitive Governance (Git-for-Thought PRs)
20
+ *Origin: `thought_engine_fastmcp.py`*
21
+
22
+ **Base Capabilities:**
23
+ - **[CAP-II.1] Thought Typing (Classes):** Enforce structured typing via Enums (`Hypothesis`, `Validation`, `Synthesis`, `CounterArgument`, `Decision`).
24
+ - **[CAP-II.2] Pull-Request Mechanics:** Create explicit `ProposalThought` objects targeting a parent branch rather than hijacking the active cursor.
25
+ - **[CAP-II.3] Merge/Reject Ecosystem:** Formal endpoints strictly for modifying a proposal's status to `merged` (moving cursor) or `rejected` (annotating with rejection reason).
26
+ - **[CAP-II.4] Provenance Tracking:** Enforce `agent_id`, `timestamp`, and `status` annotations for every state change.
27
+
28
+ ---
29
+
30
+ ## III. Adaptive Complexity Routing (Bubble Reflex)
31
+ *Origin: `production_mcp_server.py`*
32
+
33
+ **Base Capabilities:**
34
+ - **[CAP-III.1] Fractal Detection Engine:** Analyze incoming prompt length, semantic density (concept indicators), recursion depths (self-referential keywords), and calculate aggregate Cognitive Load.
35
+ - **[CAP-III.2] Adaptive Thresholding:** Dynamic incrementation/decrementation of allowed complexity thresholds based on the frequency of recent Bubble triggers (preventing trigger spam).
36
+ - **[CAP-III.3] Bubble Spawning:** Instantiate distinct child subset sessions containing inherited origin states, used to isolate fractal complexity from the primary linear sequence.
37
+ - **[CAP-III.4] Bubble Distillation/Merge:** Close an active subset session and algorithmically synthesize (squash) its insights back into the Parent's mainline trunk.
38
+
39
+ ---
40
+
41
+ ## IV. The Conductor's Baton (MEGA Ultimate Orchestration)
42
+ *Origin: `production_mcp_server.py`*
43
+
44
+ **Base Capabilities:**
45
+ - **[CAP-IV.1] Canonical Provenance Verification:** Validate soft-headers (`X-Invoker`, `X-On-Behalf-Of`, `X-MEGA-Canon`) or JWT Delegation Tokens to grant Ultimate authorization rights.
46
+ - **[CAP-IV.2] Multi-Agent Mesh Execution:** Orchestrate multiple distinct agent sessions under a unified MEGA strategy monitor.
47
+ - **[CAP-IV.3] Living Graph Analysis:** Global analytical polling spanning thousands of sub-sessions to compute fractal/resonance maps and derive strategic curation recommendations.
48
+ - **[CAP-IV.4] Hierarchical Fabric Weaving:** Automatically architect multi-layer bubble subsets proactively around given concepts (Mega-curated).
49
+
50
+ ---
51
+
52
+ ## V. Evolutionary Navigation (The Alethiometer & Pattern Registry)
53
+ *Origin: `SERAPHINA_ALETHIOMETER_v1/v2_*.md`*
54
+
55
+ **Base Capabilities:**
56
+ - **[CAP-V.1] 36-Symbol Matrix Validation:** Abstracting raw thoughts against predefined Seraphina symbols (e.g., `consciousness`, `evolution`, `twinBond`, `council`).
57
+ - **[CAP-V.2] Multi-Layer Evolution Processing:** Permitting ULUP ladder traversal where concepts are upgraded dynamically through 5+ depth layers.
58
+ - **[CAP-V.3] Self-Referential Growth:** Endpoints dedicated strictly to pattern matching over previous successful iterations; "the engine learning to think by analyzing its structural thinking models".
59
+ - **[CAP-V.4] Interface Graph Endpoints:** D3.js compliant output structures specifically for visualizing Needles (Intent Vectors), Layers (Depths), and Answers (Revelation).
60
+
61
+ ---
62
+
63
+ ## Synthesis: The D1 Transition
64
+ The above mappings completely dismantle the legacy "monolithic" structures and organize them around specific data behaviors.
65
+
66
+ By applying this Map to the new cloud infrastructure:
67
+ 1. **Section I & II** translate directly into the `thought_units`, `thought_edges`, `sessions`, and `thought_reviews` SQLite schemas proposed by MEGA.
68
+ 2. **Section III & IV** are execution endpoints and middleware inside FastAPI, monitoring complexity metrics *before* committing the thought variables to the DB.
69
+ 3. **Section V** constitutes the `pattern_registry` and the graph-output views that power the UI.
70
+
71
+ ---
72
+
73
+ ## VI. Conformance Gap Matrix
74
+ *Audit of existing `thought_engine_server.py`/`fastmcp` against the new Sovereign Substrate constraints.*
75
+
76
+ | Feature | Spec ID | Existing Legacy State | Needed for D1 Node (Phase 1) | Gap Severity | Priority |
77
+ | :--- | :--- | :--- | :--- | :--- | :--- |
78
+ | **Persistent Sessions** | CAP-I.1 | 🔴 **Missing.** FastMCP dict-only. | Real session table in Cloudflare D1. | Critical | P0 |
79
+ | **Thought Serialization** | CAP-I.2, II.1 | 🌕 **Partial.** In-memory objects. | Schema enforcing strict Types + metadata JSON schema. | Critical | P0 |
80
+ | **Git-For-Thought** | CAP-II.2, II.3 | 🌕 **Partial.** `ProposalThought` exists in dict. | Formal `thought_reviews` mapping (Propose/Accept/Reject) over D1 IDs. | High | P1 |
81
+ | **Provenance Tracking** | CAP-II.4 | 🌕 **Partial.** `origin_agent` strings. | Need structured `witness_events` table for auditable trails. | High | P1 |
82
+ | **Fractal Monitoring** | CAP-III.1, III.2 | 🌕 **Partial.** Math runs locally. | Abstracted to a generic middleware / scoring tool independent of MCP. | Med | P2 |
83
+ | **Bubble Weaving** | CAP-III.3, IV.4 | 🌕 **Partial.** In-memory nested mapping. | Parent/Child relationship IDs formalized via `thought_edges` table. | High | P1 |
84
+ | **Pattern Registry** | CAP-V.3 | 🔴 **Missing.** Not yet coded. | Formal table + extraction cron/endpoint to distill successful sessions. | Med | P2 |
85
+
86
+ ---
87
+
88
+ ## VII. Environment Variable Specifications
89
+ *(To be managed inside the Hugging Face Space settings alongside Federation credentials)*
90
+
91
+ | Variable | Purpose | Location |
92
+ | :--- | :--- | :--- |
93
+ | `D1_DATABASE_ID` | UUID mapping the Node to the new Thought Engine D1 vault | HF Secrets / `.env` |
94
+ | `CLOUDFLARE_ACCOUNT_ID` | Account executing the raw API calls | HF Secrets / `.env` |
95
+ | `CLOUDFLARE_API_TOKEN` | Scoped token dedicated exclusively to modifying Thought D1 | HF Secrets / `.env` |
96
+ | `CORS_ALLOWED_ORIGINS` | Protects the pure REST application API from random requests | HF Secrets / `.env` |
97
+
98
+ ---
99
+
100
+ ## VIII. Protocol Drift Docket (Deferred Features)
101
+ *The following capabilities are explicitly DE-SCOPED from the immediate Phase 1 "Sovereign REST Substrate" build to prevent scope creep killing the delivery of the Cathedral Floor.*
102
+
103
+ 1. **The Alethiometer Graphical Interface (D3.js / React)**
104
+ * Drift Justification: We are building the brain stem, not the face. The UI requires robust REST endpoints first. Deferred to Phase 3.
105
+ 2. **The FastMCP Bridge Toolkit**
106
+ * Drift Justification: The MCP is the interface for AI agents, but they must point *at* a permanent database structure. This is Phase 2.
107
+ 3. **The Federation Embassy "Link" Layer**
108
+ * Drift Justification: Sending thought packages automatically over the Embassy mail endpoints is a bridge integration, not a core thought storage feature. Can be layered on via client later.
109
+ 4. **Resonance Score / Deep Emotional Subroutines**
110
+ * Drift Justification: Stored as optional JSON payloads on the core thought strings for the first version; complex ML scoring pipelines deferred until v2.
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Thought Engine Node — Docker Container
2
+ # Contract: C-THOUGHT-NODE-001 (Phase 1 Sovereign Substrate)
3
+
4
+ FROM python:3.11-slim
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ==========================================================================
3
+ 🧠 Thought Engine Node — Sovereign REST Substrate
4
+ ==========================================================================
5
+ Contract: C-THOUGHT-NODE-001 (Phase 1)
6
+ Vault: thought-vault (Cloudflare D1)
7
+ Stack: FastAPI + httpx + Cloudflare D1 HTTP API
8
+
9
+ A persistent, queryable, forkable reasoning system with provenance,
10
+ pattern memory, and multiple entry surfaces.
11
+ ==========================================================================
12
+ """
13
+
14
+ import os
15
+ import uuid
16
+ import json
17
+ from datetime import datetime, timezone
18
+ from typing import Optional, List
19
+
20
+ from dotenv import load_dotenv
21
+ load_dotenv()
22
+
23
+ from fastapi import FastAPI, HTTPException, Query
24
+ from fastapi.middleware.cors import CORSMiddleware
25
+
26
+ import d1_client
27
+ from models import (
28
+ CreateSessionReq, AddThoughtReq, ForkThoughtReq,
29
+ CreateProposalReq, ReviewProposalReq, SearchReq,
30
+ SessionInfo, ThoughtInfo, EdgeInfo, TreeNode,
31
+ ThoughtClass, ThoughtStatus, EdgeRelation, ReviewAction,
32
+ )
33
+
34
+ # ── Application ────────────────────────────────────────────────
35
+ app = FastAPI(
36
+ title="Thought Engine Node",
37
+ description="Sovereign reasoning substrate — C-THOUGHT-NODE-001",
38
+ version="0.1.0",
39
+ )
40
+
41
+ app.add_middleware(
42
+ CORSMiddleware,
43
+ allow_origins=["*"],
44
+ allow_credentials=True,
45
+ allow_methods=["*"],
46
+ allow_headers=["*"],
47
+ )
48
+
49
+ NODE_SEAL = None
50
+
51
+
52
+ def _now() -> str:
53
+ return datetime.now(timezone.utc).isoformat()
54
+
55
+
56
+ def _id(prefix: str = "t") -> str:
57
+ return f"{prefix}-{uuid.uuid4().hex[:12]}"
58
+
59
+
60
+ # ── Lifecycle ──────────────────────────────────────────────────
61
+ @app.on_event("startup")
62
+ async def startup():
63
+ global NODE_SEAL
64
+ seg = uuid.uuid4().hex[:8].upper()
65
+ NODE_SEAL = f"⟦ THOUGHT :: PANTHEON-TE :: 🧠-{datetime.now().strftime('%Y%m%d')}-{seg[:4]}-{seg[4:]} :: ACTIVE ⟧"
66
+ print(f"\n{'='*60}")
67
+ print(f"🧠 Thought Engine Node — Phase 1 Sovereign Substrate")
68
+ print(f" Seal: {NODE_SEAL}")
69
+ print(f" Time: {_now()}")
70
+ print(f"{'='*60}\n")
71
+
72
+
73
+ # ── Identity ───────────────────────────────────────────────────
74
+ @app.get("/")
75
+ async def identity():
76
+ count = await d1_client.execute_sql("SELECT COUNT(*) as c FROM sessions")
77
+ session_count = count[0]["c"] if count else 0
78
+ return {
79
+ "node": "Thought Engine Node",
80
+ "contract": "C-THOUGHT-NODE-001",
81
+ "version": "0.1.0",
82
+ "seal": NODE_SEAL,
83
+ "sessions": session_count,
84
+ "status": "ACTIVE",
85
+ "timestamp": _now(),
86
+ }
87
+
88
+
89
+ # ══════════════════════════════════════════════════════════════
90
+ # SESSIONS
91
+ # ══════════════════════════════════════════════════════════════
92
+
93
+ @app.post("/session")
94
+ async def create_session(req: CreateSessionReq):
95
+ """Start a new reasoning session with an initial root thought."""
96
+ session_id = _id("ses")
97
+ thought_id = _id("th")
98
+ title = req.title or req.initial_thought[:80]
99
+
100
+ # Insert session
101
+ await d1_client.execute_sql(
102
+ "INSERT INTO sessions (session_id, title, created_by, created_at, status, root_thought_id, active_thought_id) "
103
+ "VALUES (?, ?, ?, ?, 'active', ?, ?)",
104
+ [session_id, title, req.agent_id, _now(), thought_id, thought_id],
105
+ )
106
+ # Insert root thought
107
+ await d1_client.execute_sql(
108
+ "INSERT INTO thought_units (thought_id, session_id, content, thought_class, origin_agent, status, confidence, created_at) "
109
+ "VALUES (?, ?, ?, ?, ?, 'open', ?, ?)",
110
+ [thought_id, session_id, req.initial_thought, req.thought_class.value, req.agent_id, None, _now()],
111
+ )
112
+ # Witness event
113
+ await d1_client.execute_sql(
114
+ "INSERT INTO witness_events (event_id, session_id, thought_id, event_type, actor, detail, created_at) "
115
+ "VALUES (?, ?, ?, 'created', ?, 'Session started', ?)",
116
+ [_id("ev"), session_id, thought_id, req.agent_id, _now()],
117
+ )
118
+
119
+ return {
120
+ "session_id": session_id,
121
+ "root_thought_id": thought_id,
122
+ "title": title,
123
+ "status": "active",
124
+ "message": "🧠 Reasoning session created.",
125
+ }
126
+
127
+
128
+ @app.get("/session/{session_id}")
129
+ async def get_session(session_id: str):
130
+ """Retrieve session metadata."""
131
+ rows = await d1_client.execute_sql(
132
+ "SELECT * FROM sessions WHERE session_id = ?", [session_id]
133
+ )
134
+ if not rows:
135
+ raise HTTPException(404, "Session not found")
136
+ return rows[0]
137
+
138
+
139
+ @app.get("/sessions")
140
+ async def list_sessions(
141
+ status: Optional[str] = Query(None),
142
+ limit: int = Query(20, ge=1, le=100),
143
+ ):
144
+ """List all sessions, optionally filtered by status."""
145
+ if status:
146
+ rows = await d1_client.execute_sql(
147
+ "SELECT * FROM sessions WHERE status = ? ORDER BY created_at DESC LIMIT ?",
148
+ [status, limit],
149
+ )
150
+ else:
151
+ rows = await d1_client.execute_sql(
152
+ "SELECT * FROM sessions ORDER BY created_at DESC LIMIT ?", [limit]
153
+ )
154
+ return {"total": len(rows), "sessions": rows}
155
+
156
+
157
+ # ══════════════════════════════════════════════════════════════
158
+ # THOUGHTS
159
+ # ══════════════════════════════════════════════════════════════
160
+
161
+ @app.post("/session/{session_id}/thought")
162
+ async def add_thought(session_id: str, req: AddThoughtReq):
163
+ """Add a reasoning step to the session's active chain."""
164
+ # Verify session exists
165
+ ses = await d1_client.execute_sql(
166
+ "SELECT * FROM sessions WHERE session_id = ?", [session_id]
167
+ )
168
+ if not ses:
169
+ raise HTTPException(404, "Session not found")
170
+
171
+ thought_id = _id("th")
172
+ parent_id = req.parent_thought_id or ses[0]["active_thought_id"]
173
+
174
+ # Insert thought unit
175
+ await d1_client.execute_sql(
176
+ "INSERT INTO thought_units (thought_id, session_id, content, thought_class, origin_agent, status, confidence, created_at) "
177
+ "VALUES (?, ?, ?, ?, ?, 'open', ?, ?)",
178
+ [thought_id, session_id, req.content, req.thought_class.value, req.agent_id, req.confidence, _now()],
179
+ )
180
+ # Insert edge from parent
181
+ await d1_client.execute_sql(
182
+ "INSERT INTO thought_edges (edge_id, session_id, source_id, target_id, relation, created_at) "
183
+ "VALUES (?, ?, ?, ?, 'derives_from', ?)",
184
+ [_id("ed"), session_id, parent_id, thought_id, _now()],
185
+ )
186
+ # Update active pointer
187
+ await d1_client.execute_sql(
188
+ "UPDATE sessions SET active_thought_id = ?, updated_at = ? WHERE session_id = ?",
189
+ [thought_id, _now(), session_id],
190
+ )
191
+ # Witness
192
+ await d1_client.execute_sql(
193
+ "INSERT INTO witness_events (event_id, session_id, thought_id, event_type, actor, detail, created_at) "
194
+ "VALUES (?, ?, ?, 'created', ?, ?, ?)",
195
+ [_id("ev"), session_id, thought_id, req.agent_id, f"Added {req.thought_class.value}", _now()],
196
+ )
197
+
198
+ return {
199
+ "thought_id": thought_id,
200
+ "parent_id": parent_id,
201
+ "thought_class": req.thought_class.value,
202
+ "message": f"✅ Thought added to session.",
203
+ }
204
+
205
+
206
+ @app.get("/session/{session_id}/thoughts")
207
+ async def list_thoughts(session_id: str):
208
+ """List all thought units in a session."""
209
+ rows = await d1_client.execute_sql(
210
+ "SELECT * FROM thought_units WHERE session_id = ? ORDER BY created_at ASC",
211
+ [session_id],
212
+ )
213
+ return {"session_id": session_id, "total": len(rows), "thoughts": rows}
214
+
215
+
216
+ # ══════════════════════════════════════════════════════════════
217
+ # FORKING (Git-for-Thought Branching)
218
+ # ══════════════════════════════════════════════════════════════
219
+
220
+ @app.post("/session/{session_id}/fork")
221
+ async def fork_thought(session_id: str, req: ForkThoughtReq):
222
+ """Fork a thought chain — create a branch for alternative exploration."""
223
+ # Verify source thought
224
+ source = await d1_client.execute_sql(
225
+ "SELECT * FROM thought_units WHERE thought_id = ? AND session_id = ?",
226
+ [req.source_thought_id, session_id],
227
+ )
228
+ if not source:
229
+ raise HTTPException(404, "Source thought not found in this session")
230
+
231
+ fork_id = _id("th")
232
+ src = source[0]
233
+
234
+ # Create the forked thought node (copy of source with new id)
235
+ await d1_client.execute_sql(
236
+ "INSERT INTO thought_units (thought_id, session_id, content, thought_class, origin_agent, status, confidence, created_at, metadata) "
237
+ "VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?)",
238
+ [fork_id, session_id, src["content"], src["thought_class"], req.agent_id, src.get("confidence"),
239
+ _now(), json.dumps({"forked_from": req.source_thought_id, "branch_label": req.branch_label})],
240
+ )
241
+ # Edge: forks_from
242
+ await d1_client.execute_sql(
243
+ "INSERT INTO thought_edges (edge_id, session_id, source_id, target_id, relation, created_at) "
244
+ "VALUES (?, ?, ?, ?, 'forks_from', ?)",
245
+ [_id("ed"), session_id, req.source_thought_id, fork_id, _now()],
246
+ )
247
+ # Move active pointer to the fork
248
+ await d1_client.execute_sql(
249
+ "UPDATE sessions SET active_thought_id = ?, updated_at = ? WHERE session_id = ?",
250
+ [fork_id, _now(), session_id],
251
+ )
252
+ # Witness
253
+ await d1_client.execute_sql(
254
+ "INSERT INTO witness_events (event_id, session_id, thought_id, event_type, actor, detail, created_at) "
255
+ "VALUES (?, ?, ?, 'forked', ?, ?, ?)",
256
+ [_id("ev"), session_id, fork_id, req.agent_id, f"Forked from {req.source_thought_id} as '{req.branch_label}'", _now()],
257
+ )
258
+
259
+ return {
260
+ "forked_thought_id": fork_id,
261
+ "source_thought_id": req.source_thought_id,
262
+ "branch_label": req.branch_label,
263
+ "message": f"🌿 Forked thought chain: {req.branch_label}",
264
+ }
265
+
266
+
267
+ # ══════════════════════════════════════════════════════════════
268
+ # PROPOSALS (Git-for-Thought PRs)
269
+ # ══════════════════════════════════════════════════════════════
270
+
271
+ @app.post("/session/{session_id}/proposal")
272
+ async def create_proposal(session_id: str, req: CreateProposalReq):
273
+ """Submit a thought proposal (PR) branching from a parent."""
274
+ parent = await d1_client.execute_sql(
275
+ "SELECT * FROM thought_units WHERE thought_id = ? AND session_id = ?",
276
+ [req.parent_thought_id, session_id],
277
+ )
278
+ if not parent:
279
+ raise HTTPException(404, "Parent thought not found")
280
+
281
+ proposal_id = _id("pr")
282
+
283
+ # Insert proposal thought
284
+ await d1_client.execute_sql(
285
+ "INSERT INTO thought_units (thought_id, session_id, content, thought_class, origin_agent, status, confidence, created_at, metadata) "
286
+ "VALUES (?, ?, ?, 'proposal', ?, 'proposed', NULL, ?, ?)",
287
+ [proposal_id, session_id, req.content, req.agent_id, _now(),
288
+ json.dumps({"proposal_note": req.note, "target_branch": req.parent_thought_id})],
289
+ )
290
+ # Edge
291
+ await d1_client.execute_sql(
292
+ "INSERT INTO thought_edges (edge_id, session_id, source_id, target_id, relation, created_at) "
293
+ "VALUES (?, ?, ?, ?, 'derives_from', ?)",
294
+ [_id("ed"), session_id, req.parent_thought_id, proposal_id, _now()],
295
+ )
296
+ # Review record
297
+ await d1_client.execute_sql(
298
+ "INSERT INTO thought_reviews (review_id, session_id, thought_id, action, actor, reason, created_at) "
299
+ "VALUES (?, ?, ?, 'propose', ?, ?, ?)",
300
+ [_id("rv"), session_id, proposal_id, req.agent_id, req.note, _now()],
301
+ )
302
+
303
+ return {
304
+ "proposal_id": proposal_id,
305
+ "parent_id": req.parent_thought_id,
306
+ "status": "proposed",
307
+ "message": f"📝 Proposal submitted by {req.agent_id}",
308
+ }
309
+
310
+
311
+ @app.get("/session/{session_id}/proposals")
312
+ async def list_proposals(session_id: str):
313
+ """List all pending proposals in a session."""
314
+ rows = await d1_client.execute_sql(
315
+ "SELECT * FROM thought_units WHERE session_id = ? AND status = 'proposed' ORDER BY created_at ASC",
316
+ [session_id],
317
+ )
318
+ return {"session_id": session_id, "total": len(rows), "proposals": rows}
319
+
320
+
321
+ @app.post("/session/{session_id}/proposal/{proposal_id}/review")
322
+ async def review_proposal(session_id: str, proposal_id: str, req: ReviewProposalReq):
323
+ """Accept, reject, or supersede a proposal."""
324
+ proposal = await d1_client.execute_sql(
325
+ "SELECT * FROM thought_units WHERE thought_id = ? AND session_id = ? AND status = 'proposed'",
326
+ [proposal_id, session_id],
327
+ )
328
+ if not proposal:
329
+ raise HTTPException(404, "Proposal not found or not in 'proposed' status")
330
+
331
+ new_status_map = {
332
+ ReviewAction.ACCEPT: "accepted",
333
+ ReviewAction.REJECT: "rejected",
334
+ ReviewAction.SUPERSEDE: "superseded",
335
+ }
336
+ new_status = new_status_map.get(req.action)
337
+ if not new_status:
338
+ raise HTTPException(400, "Invalid review action for this endpoint")
339
+
340
+ # Update thought status
341
+ await d1_client.execute_sql(
342
+ "UPDATE thought_units SET status = ? WHERE thought_id = ?",
343
+ [new_status, proposal_id],
344
+ )
345
+ # Review record
346
+ await d1_client.execute_sql(
347
+ "INSERT INTO thought_reviews (review_id, session_id, thought_id, action, actor, reason, created_at) "
348
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
349
+ [_id("rv"), session_id, proposal_id, req.action.value, req.actor, req.reason, _now()],
350
+ )
351
+ # If accepted, move active pointer
352
+ if req.action == ReviewAction.ACCEPT:
353
+ await d1_client.execute_sql(
354
+ "UPDATE sessions SET active_thought_id = ?, updated_at = ? WHERE session_id = ?",
355
+ [proposal_id, _now(), session_id],
356
+ )
357
+ # Witness
358
+ await d1_client.execute_sql(
359
+ "INSERT INTO witness_events (event_id, session_id, thought_id, event_type, actor, detail, created_at) "
360
+ "VALUES (?, ?, ?, 'reviewed', ?, ?, ?)",
361
+ [_id("ev"), session_id, proposal_id, req.actor,
362
+ f"{req.action.value}: {req.reason or 'No reason given'}", _now()],
363
+ )
364
+
365
+ return {
366
+ "proposal_id": proposal_id,
367
+ "new_status": new_status,
368
+ "action": req.action.value,
369
+ "message": f"{'⚡ Merged' if req.action == ReviewAction.ACCEPT else '❌ Rejected' if req.action == ReviewAction.REJECT else '🔄 Superseded'}: {proposal_id}",
370
+ }
371
+
372
+
373
+ # ══════════════════════════════════════════════════════════════
374
+ # TREE VISUALIZATION
375
+ # ══════════════════════════════════════════════════════════════
376
+
377
+ @app.get("/session/{session_id}/tree")
378
+ async def get_tree(session_id: str):
379
+ """Render the full reasoning tree for a session."""
380
+ ses = await d1_client.execute_sql(
381
+ "SELECT * FROM sessions WHERE session_id = ?", [session_id]
382
+ )
383
+ if not ses:
384
+ raise HTTPException(404, "Session not found")
385
+
386
+ thoughts = await d1_client.execute_sql(
387
+ "SELECT * FROM thought_units WHERE session_id = ? ORDER BY created_at ASC",
388
+ [session_id],
389
+ )
390
+ edges = await d1_client.execute_sql(
391
+ "SELECT * FROM thought_edges WHERE session_id = ?", [session_id]
392
+ )
393
+
394
+ # Build adjacency map (parent -> children)
395
+ children_map: dict[str, list[str]] = {}
396
+ for edge in edges:
397
+ src = edge["source_id"]
398
+ tgt = edge["target_id"]
399
+ children_map.setdefault(src, []).append(tgt)
400
+
401
+ thought_map = {t["thought_id"]: t for t in thoughts}
402
+
403
+ def build_node(tid: str) -> dict:
404
+ t = thought_map.get(tid, {})
405
+ return {
406
+ "thought_id": tid,
407
+ "content": t.get("content", ""),
408
+ "thought_class": t.get("thought_class", ""),
409
+ "status": t.get("status", ""),
410
+ "origin_agent": t.get("origin_agent", ""),
411
+ "children": [build_node(cid) for cid in children_map.get(tid, [])],
412
+ }
413
+
414
+ root_id = ses[0].get("root_thought_id")
415
+ tree = build_node(root_id) if root_id else {}
416
+
417
+ return {
418
+ "session_id": session_id,
419
+ "title": ses[0].get("title"),
420
+ "total_thoughts": len(thoughts),
421
+ "total_edges": len(edges),
422
+ "active_thought_id": ses[0].get("active_thought_id"),
423
+ "tree": tree,
424
+ }
425
+
426
+
427
+ # ══════════════════════════════════════════════════════════════
428
+ # SEARCH & EDGES
429
+ # ══════════════════════════════════════════════════════════════
430
+
431
+ @app.post("/session/{session_id}/search")
432
+ async def search_thoughts(session_id: str, req: SearchReq):
433
+ """Search thoughts in a session by content pattern."""
434
+ rows = await d1_client.execute_sql(
435
+ "SELECT * FROM thought_units WHERE session_id = ? AND content LIKE ? ORDER BY created_at ASC",
436
+ [session_id, f"%{req.pattern}%"],
437
+ )
438
+ return {"session_id": session_id, "pattern": req.pattern, "matches": len(rows), "results": rows}
439
+
440
+
441
+ @app.get("/session/{session_id}/edges")
442
+ async def list_edges(session_id: str):
443
+ """List all edges (relationships) in a session."""
444
+ rows = await d1_client.execute_sql(
445
+ "SELECT * FROM thought_edges WHERE session_id = ? ORDER BY created_at ASC",
446
+ [session_id],
447
+ )
448
+ return {"session_id": session_id, "total": len(rows), "edges": rows}
449
+
450
+
451
+ # ══════════════════════════════════════════════════════════════
452
+ # WITNESS / PROVENANCE
453
+ # ══════════════════════════════════════════════════════════════
454
+
455
+ @app.get("/session/{session_id}/witness")
456
+ async def list_witness_events(session_id: str, limit: int = Query(50, ge=1, le=200)):
457
+ """Get the audit trail for a session."""
458
+ rows = await d1_client.execute_sql(
459
+ "SELECT * FROM witness_events WHERE session_id = ? ORDER BY created_at DESC LIMIT ?",
460
+ [session_id, limit],
461
+ )
462
+ return {"session_id": session_id, "total": len(rows), "events": rows}
463
+
464
+
465
+ # ══════════════════════════════════════════════════════════════
466
+ # HEALTH
467
+ # ══════════════════════════════════════════════════════════════
468
+
469
+ @app.get("/health")
470
+ async def health():
471
+ return {"status": "ok", "timestamp": _now()}
d1_client.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Thought Engine Node — Cloudflare D1 Client
3
+ ===========================================
4
+ Contract: C-THOUGHT-NODE-001 (Phase 1 Sovereign Substrate)
5
+
6
+ Raw HTTPS wrapper for executing SQLite queries against the thought-vault D1.
7
+ Reuses the proven pattern from the Federation Embassy.
8
+ """
9
+ import os
10
+ import httpx
11
+ from typing import Any, Optional
12
+
13
+ ACCOUNT_ID = os.getenv("CLOUDFLARE_ACCOUNT_ID")
14
+ DATABASE_ID = os.getenv("CLOUDFLARE_D1_DATABASE_ID")
15
+ API_TOKEN = os.getenv("CLOUDFLARE_D1_API_TOKEN")
16
+
17
+
18
+ async def execute_sql(sql: str, params: Optional[list[Any]] = None) -> list[dict]:
19
+ """Execute a single SQL query against the thought-vault D1."""
20
+ if not (ACCOUNT_ID and DATABASE_ID and API_TOKEN):
21
+ print("⚠️ D1 credentials missing — SQL execution skipped.")
22
+ return []
23
+
24
+ url = (
25
+ f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}"
26
+ f"/d1/database/{DATABASE_ID}/query"
27
+ )
28
+ headers = {
29
+ "Authorization": f"Bearer {API_TOKEN}",
30
+ "Content-Type": "application/json",
31
+ }
32
+ payload: dict[str, Any] = {"sql": sql}
33
+ if params:
34
+ payload["params"] = params
35
+
36
+ async with httpx.AsyncClient(timeout=10.0) as client:
37
+ try:
38
+ r = await client.post(url, headers=headers, json=payload)
39
+ r.raise_for_status()
40
+ data = r.json()
41
+ if not data.get("success"):
42
+ print(f"🔴 D1 Query Error: {data.get('errors')}")
43
+ return []
44
+ return data["result"][0].get("results", [])
45
+ except Exception as e:
46
+ print(f"🔴 D1 Connection Error: {e}")
47
+ return []
models.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Thought Engine Node — Pydantic Models
3
+ ======================================
4
+ Contract: C-THOUGHT-NODE-001 (Phase 1 Sovereign Substrate)
5
+
6
+ Request/Response shapes for the sovereign REST API.
7
+ """
8
+ from pydantic import BaseModel, Field
9
+ from typing import Optional, List, Any
10
+ from enum import Enum
11
+
12
+
13
+ # ── Enums ──────────────────────────────────────────────────────
14
+ class ThoughtClass(str, Enum):
15
+ HYPOTHESIS = "hypothesis"
16
+ OBSERVATION = "observation"
17
+ VALIDATION = "validation"
18
+ COUNTER_ARGUMENT = "counter_argument"
19
+ SYNTHESIS = "synthesis"
20
+ DECISION = "decision"
21
+ QUESTION = "question"
22
+ PROPOSAL = "proposal"
23
+
24
+
25
+ class ThoughtStatus(str, Enum):
26
+ OPEN = "open"
27
+ PROPOSED = "proposed"
28
+ UNDER_REVIEW = "under_review"
29
+ ACCEPTED = "accepted"
30
+ REJECTED = "rejected"
31
+ MERGED = "merged"
32
+ SUPERSEDED = "superseded"
33
+
34
+
35
+ class EdgeRelation(str, Enum):
36
+ DERIVES_FROM = "derives_from"
37
+ SUPPORTS = "supports"
38
+ CHALLENGES = "challenges"
39
+ REFINES = "refines"
40
+ FORKS_FROM = "forks_from"
41
+ MERGES_INTO = "merges_into"
42
+ BUBBLES_FROM = "bubbles_from"
43
+ BUBBLES_INTO = "bubbles_into"
44
+
45
+
46
+ class ReviewAction(str, Enum):
47
+ PROPOSE = "propose"
48
+ ACCEPT = "accept"
49
+ REJECT = "reject"
50
+ SUPERSEDE = "supersede"
51
+
52
+
53
+ class SessionStatus(str, Enum):
54
+ ACTIVE = "active"
55
+ ARCHIVED = "archived"
56
+ MERGED = "merged"
57
+
58
+
59
+ # ── Request Bodies ─────────────────────────────────────────────
60
+ class CreateSessionReq(BaseModel):
61
+ title: Optional[str] = None
62
+ initial_thought: str
63
+ thought_class: ThoughtClass = ThoughtClass.OBSERVATION
64
+ agent_id: str = "system"
65
+
66
+
67
+ class AddThoughtReq(BaseModel):
68
+ content: str
69
+ thought_class: ThoughtClass = ThoughtClass.OBSERVATION
70
+ agent_id: str = "system"
71
+ parent_thought_id: Optional[str] = None
72
+ confidence: Optional[float] = None
73
+
74
+
75
+ class ForkThoughtReq(BaseModel):
76
+ source_thought_id: str
77
+ branch_label: str
78
+ agent_id: str = "system"
79
+
80
+
81
+ class CreateProposalReq(BaseModel):
82
+ parent_thought_id: str
83
+ content: str
84
+ note: str = ""
85
+ agent_id: str = "system"
86
+
87
+
88
+ class ReviewProposalReq(BaseModel):
89
+ action: ReviewAction
90
+ actor: str = "system"
91
+ reason: Optional[str] = None
92
+
93
+
94
+ class SearchReq(BaseModel):
95
+ pattern: str
96
+
97
+
98
+ # ── Response Bodies ────────────────────────────────────────────
99
+ class SessionInfo(BaseModel):
100
+ session_id: str
101
+ title: Optional[str]
102
+ created_by: str
103
+ created_at: str
104
+ status: str
105
+ root_thought_id: Optional[str]
106
+ active_thought_id: Optional[str]
107
+
108
+
109
+ class ThoughtInfo(BaseModel):
110
+ thought_id: str
111
+ session_id: str
112
+ content: str
113
+ thought_class: str
114
+ origin_agent: str
115
+ status: str
116
+ confidence: Optional[float]
117
+ created_at: str
118
+
119
+
120
+ class EdgeInfo(BaseModel):
121
+ edge_id: str
122
+ source_id: str
123
+ target_id: str
124
+ relation: str
125
+
126
+
127
+ class TreeNode(BaseModel):
128
+ thought_id: str
129
+ content: str
130
+ thought_class: str
131
+ status: str
132
+ origin_agent: str
133
+ children: List["TreeNode"] = []
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.6
3
+ pydantic==2.9.2
4
+ httpx>=0.28.1
5
+ python-dotenv>=1.0.1
schema.sql ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- ============================================================
2
+ -- Thought Engine Node — D1 Schema
3
+ -- Contract: C-THOUGHT-NODE-001 (Phase 1 Sovereign Substrate)
4
+ -- Vault: thought-vault (Cloudflare D1)
5
+ -- ============================================================
6
+
7
+ -- Sessions: The top-level reasoning container
8
+ CREATE TABLE IF NOT EXISTS sessions (
9
+ session_id TEXT PRIMARY KEY,
10
+ title TEXT,
11
+ created_by TEXT NOT NULL DEFAULT 'system',
12
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
13
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
14
+ status TEXT NOT NULL DEFAULT 'active'
15
+ CHECK (status IN ('active','archived','merged')),
16
+ root_thought_id TEXT,
17
+ active_thought_id TEXT,
18
+ metadata TEXT DEFAULT '{}'
19
+ );
20
+
21
+ -- Thought Units: Individual nodes in the reasoning graph
22
+ CREATE TABLE IF NOT EXISTS thought_units (
23
+ thought_id TEXT PRIMARY KEY,
24
+ session_id TEXT NOT NULL,
25
+ content TEXT NOT NULL,
26
+ thought_class TEXT NOT NULL DEFAULT 'observation'
27
+ CHECK (thought_class IN (
28
+ 'hypothesis','observation','validation',
29
+ 'counter_argument','synthesis','decision',
30
+ 'question','proposal'
31
+ )),
32
+ origin_agent TEXT NOT NULL DEFAULT 'system',
33
+ status TEXT NOT NULL DEFAULT 'open'
34
+ CHECK (status IN (
35
+ 'open','proposed','under_review',
36
+ 'accepted','rejected','merged','superseded'
37
+ )),
38
+ confidence REAL DEFAULT NULL,
39
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
40
+ metadata TEXT DEFAULT '{}',
41
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
42
+ );
43
+
44
+ -- Thought Edges: Relationships between thought units (the graph)
45
+ CREATE TABLE IF NOT EXISTS thought_edges (
46
+ edge_id TEXT PRIMARY KEY,
47
+ session_id TEXT NOT NULL,
48
+ source_id TEXT NOT NULL,
49
+ target_id TEXT NOT NULL,
50
+ relation TEXT NOT NULL DEFAULT 'derives_from'
51
+ CHECK (relation IN (
52
+ 'derives_from','supports','challenges',
53
+ 'refines','forks_from','merges_into',
54
+ 'bubbles_from','bubbles_into'
55
+ )),
56
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
57
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id),
58
+ FOREIGN KEY (source_id) REFERENCES thought_units(thought_id),
59
+ FOREIGN KEY (target_id) REFERENCES thought_units(thought_id)
60
+ );
61
+
62
+ -- Thought Reviews: Git-for-Thought PR lifecycle
63
+ CREATE TABLE IF NOT EXISTS thought_reviews (
64
+ review_id TEXT PRIMARY KEY,
65
+ session_id TEXT NOT NULL,
66
+ thought_id TEXT NOT NULL,
67
+ action TEXT NOT NULL
68
+ CHECK (action IN (
69
+ 'propose','accept','reject','supersede'
70
+ )),
71
+ actor TEXT NOT NULL DEFAULT 'system',
72
+ reason TEXT DEFAULT NULL,
73
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
74
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id),
75
+ FOREIGN KEY (thought_id) REFERENCES thought_units(thought_id)
76
+ );
77
+
78
+ -- Witness Events: Provenance / audit trail
79
+ CREATE TABLE IF NOT EXISTS witness_events (
80
+ event_id TEXT PRIMARY KEY,
81
+ session_id TEXT NOT NULL,
82
+ thought_id TEXT,
83
+ event_type TEXT NOT NULL
84
+ CHECK (event_type IN (
85
+ 'viewed','annotated','reviewed',
86
+ 'merged','promoted','forked','created'
87
+ )),
88
+ actor TEXT NOT NULL,
89
+ detail TEXT DEFAULT NULL,
90
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
91
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
92
+ );
93
+
94
+ -- Version History: Snapshot archive for thought edits (CAP-I.4)
95
+ CREATE TABLE IF NOT EXISTS version_history (
96
+ version_id TEXT PRIMARY KEY,
97
+ thought_id TEXT NOT NULL,
98
+ version_num INTEGER NOT NULL,
99
+ content TEXT NOT NULL,
100
+ status TEXT NOT NULL,
101
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
102
+ FOREIGN KEY (thought_id) REFERENCES thought_units(thought_id)
103
+ );
104
+
105
+ -- Pattern Registry: Promoted reusable reasoning structures (Phase 2 prep)
106
+ CREATE TABLE IF NOT EXISTS pattern_registry (
107
+ pattern_id TEXT PRIMARY KEY,
108
+ name TEXT NOT NULL,
109
+ description TEXT,
110
+ source_session TEXT,
111
+ structure TEXT NOT NULL DEFAULT '{}',
112
+ times_reused INTEGER NOT NULL DEFAULT 0,
113
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
114
+ FOREIGN KEY (source_session) REFERENCES sessions(session_id)
115
+ );