Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions docs.local/tasks/REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# D4-T3 ingestion iterate report

Status: iteration implemented and project-UUID regression repaired on
`feat/d4-t3-thread-ingest`; production entrypoint executed; no push or PR.

## Review blockers resolved

- `brainlayer ingest-t3` is now a first-class CLI entrypoint. The dedicated
`scripts/launchd/com.brainlayer.t3-ingest.plist` runs it at load and daily at
03:45; `scripts/launchd/install.sh all` installs it.
- Provider runtime linkage now reaches every emitted chunk as
`t3_provider_name`, `t3_provider_session_id`, and `t3_mirrored` metadata.
- The fatal schema contract is limited to columns actually consumed:
`projection_threads(thread_id, project_id, title, created_at)`,
`projection_thread_messages(message_id, thread_id, role, text, created_at)`,
`projection_projects(project_id, title)`,
and `provider_session_runtime(thread_id, provider_name, resume_cursor_json)`.
`projection_thread_sessions` and all `updated_at` columns are no longer
required.
- The dead `read_t3_threads` export was removed.
- `health-check` accepts `--t3-health-path`, includes the JSON snapshot in its
result, and raises a critical `t3_ingest_unhealthy` issue when `alerting` is
true.
- SQLite/DB sources no longer pass a 704 MB binary file through the JSONL
timestamp sniffer; T3 chunk metadata supplies its timestamps.

Relevant implementation locations are `src/brainlayer/cli/__init__.py:3681-3715`,
`scripts/launchd/com.brainlayer.t3-ingest.plist`,
`src/brainlayer/ingest/t3.py:28-33,156-235,295-317`, and
`src/brainlayer/health_check.py:779-795,961-964` (iteration changes; final
branch commit is recorded below). The original reader’s read-only and alarm
primitives remain at `src/brainlayer/ingest/t3.py:156-180`.

## Re-verified live source facts

The source is opened with SQLite URI `mode=ro&immutable=0`, autocommit,
`PRAGMA query_only=ON`, and a 1-second busy timeout. The production source
remained read-only during the live ingest.

| Fact | Observed |
|---|---:|
| `~/.t3/userdata/state.sqlite` size | 704,430,080 bytes |
| `projection_threads` | 45 |
| `projection_thread_messages` | 2,349 |
| `projection_thread_sessions` | 44 (not required by the reader) |
| `projection_thread_activities` | 38,381 |
| `provider_session_runtime` | 34 |

## Schema map and provider linkage

| Table/column | Meaning and relationship |
|---|---|
| `projection_threads.thread_id` | Thread identity and primary join key. |
| `projection_threads.project_id`, `title`, `created_at` | Thread identity/title/timestamp fields consumed by the reader. |
| `projection_projects.project_id`, `title` | Human project-name lookup keyed by `projection_threads.project_id`. |
| `projection_thread_messages.message_id` | Message identity. |
| `projection_thread_messages.thread_id` | Message-to-thread link to `projection_threads.thread_id`. |
| `projection_thread_messages.role`, `text`, `created_at` | Message payload and ordering fields consumed. |
| `provider_session_runtime.thread_id` | Runtime-mirror-to-thread link; a matching row marks the thread mirrored. |
| `provider_session_runtime.provider_name` | Provider name persisted to chunk metadata. |
| `provider_session_runtime.resume_cursor_json.threadId` | Provider session ID persisted to `t3_provider_session_id` when present. |

The reader validates this consumed contract before snapshot queries. Missing
tables/columns raise the existing `t3_schema_drift` alarm and write an alerting
health snapshot before raising. The 34 runtime rows are accepted as duplicate
content by design; no mirror exclusion or source-specific dedup heuristic was
added.

## Project-name derivation and live repair

The initial live ingest exposed a regression: the prior reader copied the raw
T3 UUID into `chunks.project` (`src/brainlayer/ingest/t3.py:301-308 @ 74baae28`).
The fix joins `projection_projects` on `project_id` and chooses `title` because
it is T3’s curated human-facing project label; it avoids inferring a name from
filesystem path components. A missing mapping yields `None`, never the UUID.
The raw source database remains read-only.

Before the repair, the canonical DB contained 2,505 `source='t3'` rows with
UUID projects: 2,338 still had `provenance_class='t3-thread'`, and 167 had
subsequently been reclassified by enrichment (`AGENT-INFERENCE` 147,
`AGENT-PARAPHRASE` 4, `RAW-ETAN-DIRECT` 16). The repair intentionally covered
all 2,505 T3 rows so the project invariant applies regardless of later
provenance classification.

| Repair metric | Count |
|---|---:|
| T3 rows examined | 2,505 |
| Rows re-derived | 2,505 |
| Rows left alone | 0 |
| Missing project mappings | 0 |
| Batches | 1 × 5,000-row batch |
| UUID projects remaining after repair | 0 |

The rollback artifact was captured before the write at
`/Users/etanheyman/.local/share/brainlayer/t3-project-uuid-rollback-74baae28.jsonl`:
2,505 `(id, project)` records, SHA-256
`c7c5eee753cc9c8ab5174c4770ec5a5fc16c89e9b3d3999e0e673cc0e99ab928`.
Enrichment, watcher, drain, index, and hotlane writers were paused; WAL
checkpoint before the repair returned `0|2863|2863`, and the post-repair
checkpoint returned `0|2273|2273`. All paused services were restored afterward.

## The 11 unmirrored threads

“Unmirrored” means no matching `provider_session_runtime` row. It is not an
exclusion rule.

| Thread ID | Created | Messages |
|---|---|---:|
| `a7b35b2a-50f2-4e8c-83a5-747e7a29757c` | 2026-03-06T17:45:31.449Z | 1 |
| `2b5cad3c-eefb-4766-949b-01ffcdfcfbf5` | 2026-03-07T18:21:10.185Z | 1 |
| `e9cf5dd4-a039-4dec-bfe8-d717dd2e9c23` | 2026-03-07T18:23:16.780Z | 1 |
| `44dd2387-fbf5-4b92-b778-b1f5487a15f0` | 2026-03-07T18:25:01.591Z | 1 |
| `263650ba-2190-43f7-a32a-2a87975a7d5e` | 2026-03-07T18:28:00.738Z | 1 |
| `7eb8d353-7eb2-4420-b7ad-e31926128aef` | 2026-03-07T18:28:53.755Z | 0 |
| `de99ab14-0595-42da-b3f1-cf863e8d5835` | 2026-03-07T18:29:26.869Z | 1 |
| `da804581-3b59-49e3-bef1-96e9f851a1f1` | 2026-03-07T19:08:49.225Z | 1 |
| `1babbcbf-0e37-40a4-bb14-14b1b336d542` | 2026-03-07T19:13:08.735Z | 1 |
| `d762ec6e-cb56-41a2-aea6-46dd547f2e75` | 2026-03-07T19:16:05.780Z | 1 |
| `95eee65c-59b3-49c7-9f3f-5051ead264ca` | 2026-03-07T19:17:18.220Z | 1 |

## Real production invocation

The installed console entrypoint was run with the checked-out source:

```text
PYTHONPATH=src brainlayer ingest-t3 \
--state-db /Users/etanheyman/.t3/userdata/state.sqlite \
--db /Users/etanheyman/.local/share/brainlayer/brainlayer.db \
--health-path /Users/etanheyman/.local/share/brainlayer/t3-health.json
```

| Metric | Count |
|---|---:|
| Threads seen / ingested | 45 / 45 |
| Messages seen / ingested | 2,349 / 2,349 |
| Chunks planned | 2,506 |
| Chunks indexed | 2,505 |
| Mirrored threads accepted as duplicates | 34 |
| T3 messages skipped by role/empty-text policy | 0 |
| Chunks filtered by shared system-prompt guard | 1 |

The one filtered chunk is message
`c65c95ed-2d60-4f6d-92a6-8a2ff6ad0b40` in thread
`f2a72bc0-1abc-450d-ae50-ab6b3e9812dd`; its user text begins with agent
instruction scaffolding and matched the existing global guard. This is why the
indexed count is one below the planned count; it was not a T3 reader failure.

Read-only post-ingest verification of the canonical BrainLayer DB found:

- `2,505` T3 source rows; enrichment later reclassified 167, leaving `2,338`
with `provenance_class = 't3-thread'`;
- `44` nonempty T3 conversations;
- `2,495` mirrored rows and `2,487` rows carrying a non-null provider session ID;
- `t3-health.json` with `alerting=false`, `threads_seen=45`,
`messages_seen=2349`, `chunks_planned=2506`, and `chunks_indexed=2505`.
- After the project repair: 2,505 T3 source rows, 15 distinct human project
titles, 0 UUID projects, and 0 NULL projects.

## Verification

Passed:

- Focused and cross-cutting suite: **82 passed**.
- Isolated baseline phase-2 queue file: **11 passed**.
- Live production invocation and read-only destination/source verification.

The full `pytest -q` run reached the repository’s existing phase-2 queue
failure at 68%, then cascaded into file-descriptor errors (`OSError: [Errno
24] Too many open files`) during temporary-directory cleanup. The isolated
phase-2 file passes, and no phase-2 queue file changed in this iteration.

## Commit

The implementation and report are committed on the branch; no push or PR was
performed.

TASK_DONE
53 changes: 53 additions & 0 deletions scripts/launchd/com.brainlayer.t3-ingest.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.brainlayer.t3-ingest</string>
<key>ProgramArguments</key>
<array>
<string>__BRAINLAYER_ENV_RUN__</string>
<string>__BRAINLAYER_BIN__</string>
<string>ingest-t3</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>45</integer>
Comment on lines +15 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize T3 ingestion with the nightly index

When the nightly index is delayed or configured beyond its 1,800-second default, this 03:45 job overlaps the index agent scheduled at 03:15; the checked writer wrapper uses a nonblocking pidfile lock and raises WriterInUseError rather than waiting, so the sole daily T3 run can be skipped until the following day. Schedule it with a safe gap or add writer-aware retry/serialization instead of starting at the index deadline boundary.

AGENTS.md reference: AGENTS.md:L28-L31

Useful? React with 👍 / 👎.

</dict>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>__HOME__/Library/Logs/brainlayer/t3-ingest.out.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/Library/Logs/brainlayer/t3-ingest.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:__HOME__/.local/bin</string>
<key>PYTHONUNBUFFERED</key>
<string>1</string>
<key>BRAINLAYER_REPO_ROOT</key>
<string>__BRAINLAYER_DIR__</string>
<key>BRAINLAYER_ENV_FILE</key>
<string>__BRAINLAYER_ENV_FILE__</string>
<key>BRAINLAYER_LAUNCHD_SERVICE</key>
<string>t3-ingest</string>
</dict>
<key>Nice</key>
<integer>10</integer>
<key>ProcessType</key>
<string>Background</string>
<key>ExitTimeOut</key>
<integer>1800</integer>
<key>LowPriorityIO</key>
<true/>
<key>SoftResourceLimits</key>
<dict>
<key>NumberOfFiles</key>
<integer>4096</integer>
</dict>
</dict>
</plist>
9 changes: 7 additions & 2 deletions scripts/launchd/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
# ./scripts/launchd/install.sh throughput-watchdog # Install watcher throughput watchdog only
# ./scripts/launchd/install.sh hotlane # Install BrainBar hotlane embed/enrich daemon only
# ./scripts/launchd/install.sh p0-counter # Install daily P0 longitudinal counter only
# ./scripts/launchd/install.sh t3-ingest # Install T3 thread ingestion only
# ./scripts/launchd/install.sh remove # Unload and remove all
set -euo pipefail

Expand Down Expand Up @@ -661,13 +662,16 @@ case "${1:-all}" in
p0-counter)
install_plist p0-counter
;;
t3-ingest)
install_plist t3-ingest
;;
all)
install_env_runner
verify_config_file
verify_gemini_env_file
failures=0
enrichment_ok=0
if ! install_many index drain watch hotlane-brainbar; then
if ! install_many index t3-ingest drain watch hotlane-brainbar; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make T3 launchd installation opt-in when its DB is absent

On hosts that use BrainLayer without T3 Code, install.sh all now unconditionally installs this RunAtLoad service even though ~/.t3/userdata/state.sqlite does not exist. The reader treats that absence as a fatal failure and writes an alerting snapshot consumed as a critical global health issue, so an otherwise valid installation becomes permanently unhealthy; condition this target on source availability or an explicit enable flag.

AGENTS.md reference: AGENTS.md:L5-L8

Useful? React with 👍 / 👎.

failures=1
fi
if install_plist enrichment; then
Expand Down Expand Up @@ -707,6 +711,7 @@ case "${1:-all}" in
;;
remove)
remove_plist index
remove_plist t3-ingest 2>/dev/null || true
remove_plist enrich 2>/dev/null || true
remove_plist enrichment 2>/dev/null || true
remove_plist watch 2>/dev/null || true
Expand All @@ -730,7 +735,7 @@ case "${1:-all}" in
rm -f "$HOTLANE_BRAINBAR_DST"
;;
*)
echo "Usage: $0 [index|watch|enrich|enrichment|decay|drain|hotlane|repair-fts|load [name]|unload [name]|checkpoint|backup|jsonl-backup|maintenance|maintenance-nightly|maintenance-weekly|health-check|tier0-watchdog|throughput-watchdog|p0-counter|all|remove]"
echo "Usage: $0 [index|t3-ingest|watch|enrich|enrichment|decay|drain|hotlane|repair-fts|load [name]|unload [name]|checkpoint|backup|jsonl-backup|maintenance|maintenance-nightly|maintenance-weekly|health-check|tier0-watchdog|throughput-watchdog|p0-counter|all|remove]"
exit 1
;;
esac
Expand Down
44 changes: 44 additions & 0 deletions src/brainlayer/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def _default_pause_labels() -> list[str]:
return [
"com.brainlayer.watch",
"com.brainlayer.drain",
"com.brainlayer.t3-ingest",
"com.brainlayer.hotlane-brainbar",
"com.brainlayer.enrichment",
"com.brainlayer.health-check",
Expand Down Expand Up @@ -845,6 +846,11 @@ def health_check_command(
"--drain-health-path",
help="Drain health JSON path.",
),
t3_health_path: Path = typer.Option(
Path("~/.local/share/brainlayer/t3-health.json"),
"--t3-health-path",
help="T3 ingestion health JSON path.",
),
queue_dir: Path = typer.Option(Path("~/.brainlayer/queue"), "--queue-dir", help="Durable queue directory."),
offsets_path: Path = typer.Option(
Path("~/.local/share/brainlayer/offsets.json"),
Expand Down Expand Up @@ -880,6 +886,7 @@ def health_check_command(
else HealthCheckConfig().source_jsonl_globs,
pause_sentinel_path=pause_sentinel_path.expanduser(),
drain_health_path=drain_health_path.expanduser(),
t3_health_path=t3_health_path.expanduser(),
queue_dir=queue_dir.expanduser(),
offsets_path=offsets_path.expanduser(),
watcher_health_path=watcher_health_path.expanduser(),
Expand Down Expand Up @@ -3671,6 +3678,43 @@ def progress_callback(embedded_count, total_embed):
raise typer.Exit(1)


@app.command("ingest-t3")
def ingest_t3_command(
state_db: Path = typer.Option(
Path("~/.t3/userdata/state.sqlite"),
"--state-db",
help="Read-only T3 state SQLite database.",
),
db: Optional[Path] = typer.Option(None, "--db", help="BrainLayer destination database."),
health_path: Path = typer.Option(
Path("~/.local/share/brainlayer/t3-health.json"),
"--health-path",
help="T3 ingestion health JSON path.",
),
dry_run: bool = typer.Option(False, "--dry-run", help="Read and plan chunks without writing BrainLayer."),
) -> None:
"""Ingest the live T3 thread projection into BrainLayer."""
from ..ingest.t3 import ingest_t3
from ..paths import get_db_path

result = ingest_t3(
state_db.expanduser(),
db_path=(db.expanduser() if db is not None else get_db_path()),
health_path=health_path.expanduser(),
dry_run=dry_run,
)
mode = "dry-run " if dry_run else ""
rprint(
f"{mode}threads_seen={result.threads_seen} "
f"threads_ingested={result.threads_ingested} "
f"messages_seen={result.messages_seen} "
f"messages_ingested={result.messages_ingested} "
f"chunks_planned={result.chunks_planned} "
f"chunks_indexed={result.chunks_indexed} "
f"duplicates_accepted={result.duplicates_accepted}"
)


@app.command("ingest-codex")
def ingest_codex(
path: Optional[Path] = typer.Argument(
Expand Down
Loading
Loading