sessionlog 0.1.3__tar.gz

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.
@@ -0,0 +1,19 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ .venv/
9
+ .env
10
+ *.sqlite
11
+ *.sqlite-shm
12
+ *.sqlite-wal
13
+ .DS_Store
14
+ .pytest_cache/
15
+ .coverage
16
+ htmlcov/
17
+ .mypy_cache/
18
+ .ruff_cache/
19
+ package-lock.json
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: sessionlog
3
+ Version: 0.1.3
4
+ Summary: Real-time ingestion daemon for AI coding agent sessions
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: click>=8.0
7
+ Requires-Dist: watchdog>=4.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest; extra == 'dev'
10
+ Requires-Dist: pytest-cov; extra == 'dev'
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "sessionlog"
3
+ version = "0.1.3"
4
+ description = "Real-time ingestion daemon for AI coding agent sessions"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "watchdog>=4.0",
8
+ "click>=8.0",
9
+ ]
10
+
11
+ [project.scripts]
12
+ sessionlog = "sessionlog.__main__:cli"
13
+
14
+ [project.optional-dependencies]
15
+ dev = ["pytest", "pytest-cov"]
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.pytest.ini_options]
22
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """sessionlog — real-time ingestion for AI coding agent sessions."""
2
+
3
+ __version__ = "0.1.3"
@@ -0,0 +1,60 @@
1
+ """CLI entry point: sessionlog start / status / stop."""
2
+
3
+ import click
4
+
5
+
6
+ @click.group()
7
+ def cli():
8
+ """sessionlog — real-time ingestion for AI coding agent sessions."""
9
+
10
+
11
+ @cli.command()
12
+ @click.option("--db", default="~/.sessionlog/data.sqlite", show_default=True, help="SQLite database path")
13
+ @click.option("--sources-dir", default="~/.claude/projects", show_default=True, help="Directory to watch for session files")
14
+ def start(db: str, sources_dir: str):
15
+ """Start the ingestion daemon."""
16
+ from sessionlog.watcher import IngestionWorker
17
+
18
+ click.echo(f"Watching {sources_dir} → {db}")
19
+ worker = IngestionWorker(run_immediately=True)
20
+ worker.start()
21
+ try:
22
+ worker.join()
23
+ except KeyboardInterrupt:
24
+ worker.stop()
25
+
26
+
27
+ @cli.command()
28
+ def status():
29
+ """Show ingestion daemon status."""
30
+ click.echo("Not implemented yet")
31
+
32
+
33
+ @cli.command()
34
+ @click.option("--force", is_flag=True, default=False, help="Re-ingest all files, ignoring the ingestion log.")
35
+ def ingest(force: bool):
36
+ """Run a one-shot incremental ingestion of all JSONL files."""
37
+ from sessionlog.db import get_writer
38
+ from sessionlog.ingest import run_ingest
39
+
40
+ if force:
41
+ conn = get_writer()
42
+ conn.execute("DELETE FROM ingestion_log")
43
+ conn.commit()
44
+ click.echo("Cleared ingestion log — will re-ingest all files.")
45
+
46
+ stats = run_ingest()
47
+ click.echo(
48
+ f"Done. "
49
+ f"{stats['ingested_files']}/{stats['total_files']} files ingested, "
50
+ f"{stats['total_entries']} raw entries, "
51
+ f"{stats['total_progress_entries']} progress entries "
52
+ f"({stats['skipped_files']} skipped, {stats['failed_files']} failed). "
53
+ f"DB totals: {stats['total_entries_in_db']} entries, "
54
+ f"{stats['total_sessions_found']} sessions, "
55
+ f"{stats['total_projects']} projects."
56
+ )
57
+
58
+
59
+ if __name__ == "__main__":
60
+ cli()
@@ -0,0 +1,10 @@
1
+ """Configuration: paths and file constants for ingestion."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ # Paths
7
+ CLAUDE_PROJECTS_DIR = Path.home() / ".claude" / "projects"
8
+ DB_PATH = Path(
9
+ os.environ.get("CLAUDE_RETRO_DB", Path.home() / ".claude" / "retro.sqlite")
10
+ )
@@ -0,0 +1,448 @@
1
+ """SQLite database with WAL mode for proper concurrency.
2
+
3
+ Replaces DuckDB which had constant lock contention issues.
4
+ SQLite with WAL mode supports:
5
+ - Multiple concurrent readers
6
+ - Single writer (but writers don't block readers)
7
+ - No lock timeout errors
8
+ """
9
+
10
+ import sqlite3
11
+ import threading
12
+ from pathlib import Path
13
+
14
+ from sessionlog.config import DB_PATH
15
+
16
+ # Thread-local storage for reader connections
17
+ _local = threading.local()
18
+
19
+ # Single writer connection (protected by lock)
20
+ _writer_lock = threading.Lock()
21
+ _writer_conn = None
22
+
23
+
24
+ def get_writer() -> sqlite3.Connection:
25
+ """Get the serialized writer connection.
26
+
27
+ Use this for INSERT, UPDATE, DELETE, or DDL statements.
28
+ """
29
+ global _writer_conn
30
+ if _writer_conn is None:
31
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
32
+ _writer_conn = _connect()
33
+ _init_schema(_writer_conn)
34
+ return _writer_conn
35
+
36
+
37
+ def get_reader() -> sqlite3.Connection:
38
+ """Get a reader connection for this thread.
39
+
40
+ Each thread gets its own reader. Uses autocommit so each query sees
41
+ the latest committed WAL data without holding a stale snapshot.
42
+ """
43
+ if not hasattr(_local, 'reader'):
44
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
45
+ _local.reader = _connect(autocommit=True)
46
+ return _local.reader
47
+
48
+
49
+ def get_conn() -> sqlite3.Connection:
50
+ """Legacy API: returns reader by default."""
51
+ return get_reader()
52
+
53
+
54
+ def _connect(autocommit: bool = False) -> sqlite3.Connection:
55
+ """Create a SQLite connection with optimal settings.
56
+
57
+ autocommit=True uses isolation_level=None so readers always see the
58
+ latest committed WAL data without holding a stale snapshot.
59
+ """
60
+ conn = sqlite3.connect(
61
+ str(DB_PATH),
62
+ check_same_thread=False, # Allow use across threads
63
+ timeout=30.0, # 30s timeout (rarely hit with WAL)
64
+ isolation_level=None if autocommit else "",
65
+ )
66
+
67
+ # Enable WAL mode for concurrent access
68
+ conn.execute("PRAGMA journal_mode=WAL")
69
+
70
+ # Other optimizations
71
+ conn.execute("PRAGMA synchronous=NORMAL") # Faster, still safe with WAL
72
+ conn.execute("PRAGMA busy_timeout=30000") # 30s busy timeout
73
+ conn.execute("PRAGMA cache_size=-64000") # 64MB cache
74
+ conn.execute("PRAGMA foreign_keys=ON")
75
+ conn.execute("PRAGMA temp_store=MEMORY")
76
+
77
+ return conn
78
+
79
+
80
+ def _migrate_add_columns(conn: sqlite3.Connection, table: str, columns: list):
81
+ """Add columns to table if they don't exist (safe migration)."""
82
+ existing = {row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
83
+ for col_name, col_type in columns:
84
+ if col_name not in existing:
85
+ conn.execute(f"ALTER TABLE {table} ADD COLUMN {col_name} {col_type}")
86
+
87
+
88
+ def _init_schema(conn: sqlite3.Connection):
89
+ """Initialize database schema."""
90
+
91
+ # Main tables
92
+ conn.execute("""
93
+ CREATE TABLE IF NOT EXISTS raw_entries (
94
+ entry_id TEXT PRIMARY KEY,
95
+ session_id TEXT,
96
+ project_name TEXT,
97
+ entry_type TEXT,
98
+ timestamp_utc TIMESTAMP,
99
+ parent_uuid TEXT,
100
+ is_sidechain INTEGER DEFAULT 0,
101
+ user_text TEXT,
102
+ user_text_length INTEGER DEFAULT 0,
103
+ is_tool_result INTEGER DEFAULT 0,
104
+ tool_result_error INTEGER DEFAULT 0,
105
+ tool_result_error_type TEXT, -- classified error type (command_failed, user_rejected, etc.)
106
+ model TEXT,
107
+ content_types TEXT, -- JSON array as text
108
+ tool_names TEXT, -- JSON array as text
109
+ tool_file_paths TEXT, -- JSON array of file paths from file-touching tool_use blocks
110
+ text_content TEXT,
111
+ text_length INTEGER DEFAULT 0,
112
+ input_tokens INTEGER DEFAULT 0,
113
+ output_tokens INTEGER DEFAULT 0,
114
+ system_subtype TEXT,
115
+ duration_ms INTEGER DEFAULT 0,
116
+ git_branch TEXT,
117
+ cwd TEXT
118
+ )
119
+ """)
120
+
121
+ conn.execute("""
122
+ CREATE TABLE IF NOT EXISTS sessions (
123
+ session_id TEXT PRIMARY KEY,
124
+ project_name TEXT,
125
+ started_at TIMESTAMP,
126
+ ended_at TIMESTAMP,
127
+ duration_seconds INTEGER DEFAULT 0,
128
+ user_prompt_count INTEGER DEFAULT 0,
129
+ assistant_msg_count INTEGER DEFAULT 0,
130
+ tool_use_count INTEGER DEFAULT 0,
131
+ tool_error_count INTEGER DEFAULT 0,
132
+ turn_count INTEGER DEFAULT 0,
133
+ first_prompt TEXT,
134
+ intent TEXT DEFAULT 'unknown',
135
+ trajectory TEXT DEFAULT 'unknown',
136
+ convergence_score REAL DEFAULT 0.0,
137
+ drift_score REAL DEFAULT 0.0,
138
+ thrash_score REAL DEFAULT 0.0
139
+ )
140
+ """)
141
+
142
+ conn.execute("""
143
+ CREATE TABLE IF NOT EXISTS session_features (
144
+ session_id TEXT PRIMARY KEY,
145
+ avg_prompt_length REAL DEFAULT 0,
146
+ prompt_length_trend REAL DEFAULT 0,
147
+ max_prompt_length INTEGER DEFAULT 0,
148
+ avg_response_length REAL DEFAULT 0,
149
+ response_length_trend REAL DEFAULT 0,
150
+ response_length_cv REAL DEFAULT 0,
151
+ total_input_tokens INTEGER DEFAULT 0,
152
+ total_output_tokens INTEGER DEFAULT 0,
153
+ edit_write_ratio REAL DEFAULT 0,
154
+ read_grep_ratio REAL DEFAULT 0,
155
+ bash_ratio REAL DEFAULT 0,
156
+ task_ratio REAL DEFAULT 0,
157
+ web_ratio REAL DEFAULT 0,
158
+ unique_tools_used INTEGER DEFAULT 0,
159
+ avg_turn_duration_ms REAL DEFAULT 0,
160
+ hour_of_day INTEGER DEFAULT 0,
161
+ day_of_week INTEGER DEFAULT 0,
162
+ correction_count INTEGER DEFAULT 0,
163
+ correction_rate REAL DEFAULT 0,
164
+ rephrasing_count INTEGER DEFAULT 0,
165
+ decision_marker_count INTEGER DEFAULT 0,
166
+ topic_keyword_entropy REAL DEFAULT 0,
167
+ sidechain_count INTEGER DEFAULT 0,
168
+ sidechain_ratio REAL DEFAULT 0,
169
+ abandoned INTEGER DEFAULT 0,
170
+ has_pr_link INTEGER DEFAULT 0,
171
+ branch_switch_count INTEGER DEFAULT 0,
172
+ prompt_length_oscillation REAL DEFAULT 0,
173
+ api_error_count INTEGER DEFAULT 0
174
+ )
175
+ """)
176
+
177
+ conn.execute("""
178
+ CREATE TABLE IF NOT EXISTS session_tool_usage (
179
+ session_id TEXT,
180
+ tool_name TEXT,
181
+ use_count INTEGER DEFAULT 0,
182
+ error_count INTEGER DEFAULT 0,
183
+ PRIMARY KEY (session_id, tool_name)
184
+ )
185
+ """)
186
+
187
+ conn.execute("""
188
+ CREATE TABLE IF NOT EXISTS session_languages (
189
+ session_id TEXT,
190
+ extension TEXT,
191
+ file_count INTEGER DEFAULT 0,
192
+ PRIMARY KEY (session_id, extension)
193
+ )
194
+ """)
195
+
196
+ conn.execute("""
197
+ CREATE TABLE IF NOT EXISTS progress_entries (
198
+ entry_id TEXT PRIMARY KEY,
199
+ session_id TEXT,
200
+ progress_type TEXT, -- 'agent_progress' | 'bash_progress'
201
+ parent_tool_id TEXT, -- toolUseId of parent Task/Bash call
202
+ tool_name TEXT, -- sub-agent tool name (agent_progress only)
203
+ has_result INTEGER DEFAULT 0, -- 1 if tool_result was included inline
204
+ result_error INTEGER DEFAULT 0, -- 1 if tool_result had is_error=true
205
+ timestamp_utc TIMESTAMP
206
+ )
207
+ """)
208
+
209
+ conn.execute("""
210
+ CREATE TABLE IF NOT EXISTS baselines (
211
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
212
+ window_size INTEGER,
213
+ computed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
214
+ avg_convergence REAL,
215
+ avg_drift REAL,
216
+ avg_thrash REAL,
217
+ avg_duration REAL,
218
+ avg_turns REAL,
219
+ avg_tool_errors REAL,
220
+ avg_correction_rate REAL,
221
+ session_count INTEGER
222
+ )
223
+ """)
224
+
225
+ conn.execute("""
226
+ CREATE TABLE IF NOT EXISTS prescriptions (
227
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
228
+ category TEXT,
229
+ title TEXT,
230
+ description TEXT,
231
+ evidence TEXT,
232
+ confidence REAL,
233
+ dismissed INTEGER DEFAULT 0,
234
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
235
+ )
236
+ """)
237
+
238
+ conn.execute("""
239
+ CREATE TABLE IF NOT EXISTS session_judgments (
240
+ session_id TEXT PRIMARY KEY,
241
+ outcome TEXT,
242
+ outcome_confidence REAL DEFAULT 0.0,
243
+ outcome_reasoning TEXT,
244
+ prompt_clarity REAL DEFAULT 0.0,
245
+ prompt_completeness REAL DEFAULT 0.0,
246
+ prompt_missing TEXT,
247
+ prompt_summary TEXT,
248
+ trajectory_summary TEXT,
249
+ underspecified_parts TEXT,
250
+ misalignment_count INTEGER DEFAULT 0,
251
+ misalignments TEXT,
252
+ correction_count INTEGER DEFAULT 0,
253
+ corrections TEXT,
254
+ productive_turns INTEGER DEFAULT 0,
255
+ waste_turns INTEGER DEFAULT 0,
256
+ productivity_ratio REAL DEFAULT 0.0,
257
+ waste_breakdown TEXT,
258
+ narrative TEXT,
259
+ what_worked TEXT,
260
+ what_failed TEXT,
261
+ user_quote TEXT,
262
+ claude_md_suggestion TEXT,
263
+ claude_md_rationale TEXT,
264
+ raw_analysis_1 TEXT,
265
+ raw_analysis_2 TEXT,
266
+ judged_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
267
+ )
268
+ """)
269
+
270
+ conn.execute("""
271
+ CREATE TABLE IF NOT EXISTS ingestion_log (
272
+ file_path TEXT PRIMARY KEY,
273
+ mtime REAL,
274
+ entry_count INTEGER DEFAULT 0,
275
+ ingested_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
276
+ )
277
+ """)
278
+
279
+ conn.execute("""
280
+ CREATE TABLE IF NOT EXISTS skip_cache (
281
+ file_path TEXT PRIMARY KEY,
282
+ mtime REAL,
283
+ error_type TEXT,
284
+ error_message TEXT,
285
+ skip_until TIMESTAMP,
286
+ cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
287
+ )
288
+ """)
289
+
290
+ conn.execute("""
291
+ CREATE TABLE IF NOT EXISTS session_skills (
292
+ session_id TEXT PRIMARY KEY,
293
+ d1_level INTEGER DEFAULT 0,
294
+ d1_opportunity INTEGER DEFAULT 0,
295
+ d2_level INTEGER DEFAULT 0,
296
+ d2_opportunity INTEGER DEFAULT 0,
297
+ d3_level INTEGER DEFAULT 0,
298
+ d3_opportunity INTEGER DEFAULT 0,
299
+ d4_level INTEGER DEFAULT 0,
300
+ d4_opportunity INTEGER DEFAULT 0,
301
+ d5_level INTEGER DEFAULT 0,
302
+ d5_opportunity INTEGER DEFAULT 0,
303
+ d6_level INTEGER DEFAULT 0,
304
+ d6_opportunity INTEGER DEFAULT 0,
305
+ d7_level INTEGER DEFAULT 0,
306
+ d7_opportunity INTEGER DEFAULT 0,
307
+ d8_level INTEGER DEFAULT 0,
308
+ d8_opportunity INTEGER DEFAULT 0,
309
+ d9_level INTEGER DEFAULT 0,
310
+ d9_opportunity INTEGER DEFAULT 0,
311
+ d10_level INTEGER DEFAULT 0,
312
+ d10_opportunity INTEGER DEFAULT 0,
313
+ detection_confidence REAL DEFAULT 0.0,
314
+ assessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
315
+ )
316
+ """)
317
+
318
+ conn.execute("""
319
+ CREATE TABLE IF NOT EXISTS skill_profile (
320
+ id INTEGER PRIMARY KEY DEFAULT 1,
321
+ d1_score REAL DEFAULT 0.0,
322
+ d2_score REAL DEFAULT 0.0,
323
+ d3_score REAL DEFAULT 0.0,
324
+ d4_score REAL DEFAULT 0.0,
325
+ d5_score REAL DEFAULT 0.0,
326
+ d6_score REAL DEFAULT 0.0,
327
+ d7_score REAL DEFAULT 0.0,
328
+ d8_score REAL DEFAULT 0.0,
329
+ d9_score REAL DEFAULT 0.0,
330
+ d10_score REAL DEFAULT 0.0,
331
+ gap_1 TEXT,
332
+ gap_2 TEXT,
333
+ gap_3 TEXT,
334
+ session_count INTEGER DEFAULT 0,
335
+ computed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
336
+ )
337
+ """)
338
+
339
+ conn.execute("""
340
+ CREATE TABLE IF NOT EXISTS skill_nudges (
341
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
342
+ dimension TEXT,
343
+ current_level INTEGER DEFAULT 0,
344
+ target_level INTEGER DEFAULT 0,
345
+ nudge_text TEXT,
346
+ evidence TEXT,
347
+ frequency INTEGER DEFAULT 1,
348
+ dismissed INTEGER DEFAULT 0,
349
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
350
+ )
351
+ """)
352
+
353
+ conn.execute("""
354
+ CREATE TABLE IF NOT EXISTS synthesis (
355
+ id INTEGER PRIMARY KEY DEFAULT 1,
356
+ at_a_glance TEXT,
357
+ usage_narrative TEXT,
358
+ top_wins TEXT,
359
+ top_friction TEXT,
360
+ claude_md_additions TEXT,
361
+ fun_headline TEXT,
362
+ generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
363
+ )
364
+ """)
365
+
366
+ # Migrate: add new columns to raw_entries if missing
367
+ _migrate_add_columns(conn, "raw_entries", [
368
+ ("tool_result_error_type", "TEXT"),
369
+ ("tool_file_paths", "TEXT"),
370
+ # Primary tool's key input: Bash command, Task prompt snippet, etc.
371
+ # Lets the live monitor show "what it's doing" beyond just the tool name.
372
+ ("tool_input_preview", "TEXT"),
373
+ ])
374
+
375
+ # Migrate: add new columns to session_judgments if missing
376
+ _migrate_add_columns(conn, "session_judgments", [
377
+ ("narrative", "TEXT"),
378
+ ("what_worked", "TEXT"),
379
+ ("what_failed", "TEXT"),
380
+ ("user_quote", "TEXT"),
381
+ ("claude_md_suggestion", "TEXT"),
382
+ ("claude_md_rationale", "TEXT"),
383
+ ])
384
+
385
+ # FTS5 virtual table for full-text search across messages
386
+ conn.execute("""
387
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
388
+ content,
389
+ session_id UNINDEXED,
390
+ entry_type UNINDEXED,
391
+ tokenize='porter unicode61'
392
+ )
393
+ """)
394
+
395
+ # Migrate: add subagent feature columns to session_features if missing
396
+ _migrate_add_columns(conn, "session_features", [
397
+ ("subagent_spawn_count", "INTEGER DEFAULT 0"),
398
+ ("subagent_tool_diversity", "INTEGER DEFAULT 0"),
399
+ ("subagent_error_rate", "REAL DEFAULT 0"),
400
+ ("bash_heartbeat_count", "INTEGER DEFAULT 0"),
401
+ ])
402
+
403
+ # Create indexes for common queries
404
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_raw_entries_session ON raw_entries(session_id)")
405
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_raw_entries_timestamp ON raw_entries(timestamp_utc)")
406
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_name)")
407
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)")
408
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_progress_session ON progress_entries(session_id)")
409
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_progress_type ON progress_entries(progress_type)")
410
+
411
+ conn.commit()
412
+
413
+
414
+ def execute_write(sql: str, params=None):
415
+ """Execute a write query with proper locking."""
416
+ with _writer_lock:
417
+ writer = get_writer()
418
+ if params:
419
+ result = writer.execute(sql, params)
420
+ else:
421
+ result = writer.execute(sql)
422
+ writer.commit()
423
+ return result
424
+
425
+
426
+ def execute_read(sql: str, params=None):
427
+ """Execute a read query using a reader connection."""
428
+ reader = get_reader()
429
+ if params:
430
+ return reader.execute(sql, params)
431
+ return reader.execute(sql)
432
+
433
+
434
+ def rebuild_fts_index():
435
+ """Rebuild the FTS5 index from raw_entries."""
436
+ writer = get_writer()
437
+ writer.execute("DELETE FROM messages_fts")
438
+ writer.execute("""
439
+ INSERT INTO messages_fts(content, session_id, entry_type)
440
+ SELECT
441
+ COALESCE(user_text, '') || ' ' || COALESCE(text_content, ''),
442
+ session_id,
443
+ entry_type
444
+ FROM raw_entries
445
+ WHERE (user_text IS NOT NULL AND user_text != '')
446
+ OR (text_content IS NOT NULL AND text_content != '')
447
+ """)
448
+ writer.commit()