Back to catalog

chat-export-extraction

Extract insights from ChatGPT/Claude chat exports into wiki.

Category πŸ“ Note Taking
wikiextractionchatgptclaudebatch-processing

Chat Export Extraction

When to use

Processing ChatGPT or Claude chat exports to extract durable insights into the wiki. Full pipeline: index β†’ tier β†’ batch extract β†’ promote to wiki.

Trigger signals

  • User provides a ChatGPT or Claude export (JSON files)
  • User says "analyze my chat history" or "extract from chatgpt/claude"
  • User asks to continue a previous chat export analysis

Pipeline

Phase 1: Index

Build a searchable inventory. Outputs: index.csv + index.md.

ChatGPT format: Multiple conversations-NNN.json files with mapping structure (message.author.role, message.content.parts). Claude format: Single conversations.json with {uuid, name, summary, chat_messages: [{sender, text: "..."}]}. The text field is usually a flat string β€” just read msg["text"] directly. Some older exports may use content: [{type: "text", text: "..."}] block format instead; if text is missing or empty, check content and iterate blocks. The sender field uses values like "human" / "assistant" (not role). Per conversation: id, title, date, model, msg counts, total_chars, has_code, has_image, preview, tags, tier, project. Tiers: tier1 (20K+ chars), tier2 (5-20K), tier3 (<5K), skip (routine >50K). Project mapping (ChatGPT): conversation_template_id β†’ Custom GPTs via GPT_PROJECT_MAP.

Phase 2: Batch Extraction

Batches of 15-25 conversations. Per batch: manifest.json + per-conversation .md + summary.md.

Extraction per conversation: Summary, Key Ideas, Decisions Made, Themes, Notable Quotes, Wiki Concepts. Filenames: Use sanitized_title from manifest β€” NOT UUIDs. ⚠️ Extracted files are summaries only, not full transcripts. The markdown files produced in Phase 2 contain AI-generated Themes, Key Ideas, and Decisions β€” they do NOT contain the full back-and-forth conversation text. The full transcripts live only in original/conversations-*.json. If original/ is deleted (e.g., during cleanup), all detailed chat history is permanently lost. File source mapping: The manifest.json does NOT include file_source. To find which conversations-NNN.json file a conversation lives in:
  • With index.csv: Cross-reference manifest id with index.csv id β†’ file_source. Group conversations by file_source to avoid loading the same 3-6MB JSON file multiple times.
  • Without index.csv (fallback): Search through conversations-*.json files directly. Load each file, check if any conversation ID matches your manifest IDs. Group results by file_source. This is slower but works when index.csv wasn't generated or is stale. Use execute_code with a loop over sorted conversation files β€” load each JSON once, check for matching IDs, build the mapping.
ChatGPT mapping.values() iteration (recommended for batch extraction): The simplest and most reliable approach β€” no tree-walking needed:
for node in mapping.values():
    msg = node.get('message')
    if not msg: continue
    role = msg['author']['role']
    if role not in ('user', 'assistant'): continue
    parts = msg['content']['parts']
    text = '\n'.join(p for p in parts if isinstance(p, str))
    if text.strip():
        messages.append({'role': role, 'text': text.strip()})

This visits every node exactly once, doesn't require finding root or building a children dict, and works regardless of tree structure. Messages may not be in chronological order, but that's fine for batch extraction where we need text content for analysis, not conversation flow. Use this as the default; fall back to tree-walking only when chronological order matters (e.g., continuing a conversation).

ChatGPT tree-walking extraction (for chronological order): ChatGPT JSON uses a mapping structure with parent/child node references (not a flat message list). The children field is often None for every node β€” never rely on it. To extract text in order:

1. Build a children dict by iterating all nodes: for each node_id, node in mapping.items(), if node["parent"] exists, append node_id to children[node["parent"]]

2. Find root node (the one with parent=None) β€” there should be exactly one

3. Walk the tree depth-first from root via the children dict, extracting [role] text from each node's message.content.parts

4. Skip nodes with no message (root is often a placeholder with no message)

Heuristic extraction at scale (50+ conversations): When processing large batches (50+ convos, 2M+ chars), do NOT send everything through LLM subagents β€” it's too expensive and slow. Instead:

1. Write a Python script that does regex-based heuristic extraction (Bible verse refs, theme keywords, decision patterns, question detection)

2. Generate baseline markdown files for ALL conversations

3. Then dispatch LLM subagents ONLY for the top 5-10 most important/largest conversations

4. See references/heuristic-extraction-patterns.md for the regex patterns and theme keyword lists

Parallel subagent batching: Dispatch subagents in groups of 3 (concurrency limit). Each subagent reads the raw text file, produces enhanced analysis, and writes the markdown file directly. Process largest/most important conversations first. Small batch single-script pattern (≀20 conversations): When extracting 10-20 conversations, a single Python script is simpler and faster than subagent dispatch. Write one script that: (1) reads the manifest, (2) checks for existing files, (3) greps for each ID across conversations-*.json to find file_source, (4) loads only the needed JSON files, (5) extracts text via tree-walking (ChatGPT) or chat_messages iteration (Claude), (6) generates markdown with theme/key-idea detection, (7) writes files and updates manifest. This avoids subagent overhead, timeout risks, and staging complexity. Use grep -l for fast IDβ†’file_source mapping instead of loading full JSON. For theme/key-idea detection in this pattern, use lightweight title-based regex matching (see references/lightweight-theme-detection.md) instead of LLM analysis β€” it's fast, deterministic, and sufficient for review-grade output. Size-based batching (recommended over count-based): When conversations vary wildly in size (20K–350K chars), batch by SIZE tier, not count. This prevents the largest conversations from timing out while smaller ones finish quickly. Example split for 16 conversations:
  • Batch 1: 4 smallest (20-26K each, ~94K total)
  • Batch 2: 4 medium (21-40K each, ~118K total)
  • Batch 3: 4 large (30-100K each, ~225K total)
  • Batch 4: 3 mega (118-173K each, ~444K total)
  • Batch 5: 1 mega (352K β€” the corpus giant, alone)

Dispatch each batch's subagents, wait for completion, then dispatch the next batch. Within a batch, dispatch 3 parallel subagents (max concurrency), then the 4th.

/tmp/ staging pattern: Before dispatching subagents, pre-extract all raw conversation texts to /tmp/<project>_convos/ as individual .txt files. This avoids:
  • Multiple subagents reading the same multi-MB JSON file
  • Subagents dealing with ChatGPT's tree-walking extraction logic
  • Duplicate I/O across concurrent subagents

Use execute_code with a grouped-by-file_source loop: load each JSON once, extract all matching conversations, write to /tmp/<project>_convos/<sanitized>.txt. Then subagents just read_file the clean text.

Dispatch pattern (3+1): When you have 4 tasks and max concurrency is 3, dispatch the first 3 via delegate_task(tasks=[...]), then dispatch the 4th as a separate delegate_task(tasks=[single]). The 4th will queue if at capacity, or run synchronously if slots are full.

Phase 3: Cross-Cutting Synthesis (after 70%+ Phase 2)

Produces a single synthesis.md in the ChatGPT extracted/ directory (or the primary corpus root). This is the capstone document β€” it should surface patterns the user wouldn't see from reading individual files.

Reading strategy (in order):

1. Read ALL existing analysis files in Claude extracted/ (analysis.md, corpus-analysis.md, memories.md, reflections.md, decisions.md, self-portrait.md, stats.md, index.md) β€” these provide the Claude-side baseline

2. Read Claude deep-thread analyses (extracted/deep-threads/) and project files (extracted/projects/)

3. Extract metadata from ALL ChatGPT files: use terminal with a loop to pull title, date, tags, chars from frontmatter across all project directories. Group by subdirectory to understand volume per domain.

4. Read full content of representative files from EACH ChatGPT project directory β€” at minimum: the summary.md, the first 2-3 files alphabetically, and any file with 50K+ chars

5. Cross-reference: look for the same topics, frameworks, or decisions appearing in both ChatGPT and Claude corpora

Required synthesis sections:

1. Topic Evolution Timelines β€” Trace 4-6 key topics across the full date range. For each topic, identify phases (curiosity β†’ framework construction β†’ crystallization β†’ automation). Reference actual conversation titles and dates. Show how thinking changed, not just what was discussed.

2. Cross-Domain Connections β€” Map ideas from one domain that were applied to another. Examples: faith principles β†’ consulting methodology, homeschooling insights β†’ content creation, technical skills β†’ financial analysis. The WINS framework often serves as a universal connector.

3. Unpublished Ideas β€” 10-15 concepts that could become blog posts, LinkedIn articles, or book chapters. For each: the idea, source conversation(s) with dates, why it's publishable, and potential title/angle. Mine the faith conversations, consulting frameworks, and writing series especially β€” these are richest for standalone publication.

4. Usage Patterns β€” Topic distribution by time period, platform specialization (what went to ChatGPT vs Claude), the shift from Q&A to collaboration to deep thinking, and which conversation types produced the most value. Include a "highest-value conversations" ranked list.

5. Meta-Observations β€” 4-6 cross-cutting insights about the person: identity tensions, operating philosophies, blind spots, and the corpus-as-mirror finding (deepest AI use is often reflective, not technical).

Output format: Markdown with YAML frontmatter (title, created, source, type). Use ## N. for main sections, ### N.M for subsections. Include an Appendix with corpus statistics table. Quality bar: Every observation must reference specific conversations (title + date). No generic statements. The synthesis should feel like it could only have been written by someone who read the entire corpus.

Phase 4: Wiki Migration (Move to WINS Locations)

After extraction and synthesis, move content directly from chatgpt/extracted/ (and claude/extracted/) to their final WINS wiki locations. No intermediate consume/corpus/ staging β€” move straight to the destination.

Mapping (chatgpt/extracted/ β†’ wiki WINS locations):
Source Folder WINS Destination Notes
health/ self/health/ Merge into existing directory
faith/ self/faith/ Create new subdirectory
homeschooling/ self/homeschooling/ Create new subdirectory
daily-logs/ self/daily-logs/ Create new subdirectory
consulting/ wealth/consulting/ Merge into existing directory
finance/ wealth/finance/ Create new subdirectory
writing/ produce/hermes/writing/ Create new subdirectory
app-dev/ produce/hermes/app-dev/ Create new subdirectory
ai-ethics/ consume/concepts/ai-ethics/ Create new subdirectory
common/ produce/common/ Staging area for later triage
What stays in chatgpt/extracted/ after migration:
  • images/ β€” extracted images (review later)
  • index.csv, index.md β€” conversation index metadata
  • synthesis.md β€” cross-corpus synthesis document
  • batch-001-summary.md β€” extraction batch summary
  • task.md β€” migration task tracking
After Claude processing: claude/extracted/ follows the same pattern β€” content moves to WINS locations, only metadata stays. Execution steps:

1. Create target directories that don't exist yet

2. Move files with mv (not cp -n β€” see pitfall)

3. Remove empty source directories (including manifest.json artifacts)

4. Verify source is clean (only metadata files remain)

5. Verify destination file counts match

6. Update task.md with completion status

Pitfalls:
  • cp -n leaves orphan source files. When merging into existing directories, cp -n (copy no-overwrite) copies files but doesn't remove originals β€” leaving duplicate orphans. Use mv everywhere, or cp -n + explicit rm of source files. Always verify source is empty after move.
  • Some files may have been categorized differently in chatgpt vs claude β€” verify destination before moving
  • The common/ folder is a catch-all; after WINS migration, review for re-categorization (many "common" items may actually fit a WINS bucket)
  • Keep original/ directories intact β€” they're the raw source of truth
  • Metadata files (index.csv, index.md, synthesis.md) stay in chatgpt/extracted/ β€” they're reference artifacts, not content

Pilot-Then-Scale

Always: pilot 1 batch β†’ user reviews β†’ tune β†’ overnight run.

Overnight Execution

Progress in task.md (not JSON). 2 batches/night, ~5h apart. Prompt references task.md.

Directory Structure

Each corpus self-contained: corpus/original/, corpus/extracted/, corpus/scripts/, corpus/task.md, corpus/readme.md.

Project-Based Folder Layout (Phase 2+)

After indexing, organize extracted output by project, not by batch. Batch folders are an intermediate format β€” project folders are the durable structure.

chatgpt/extracted/
β”œβ”€β”€ consulting/       ← GPT: consulting (106 convos) = "[client]" in user's mental model
β”œβ”€β”€ health/           ← GPT: health-medical (137 convos)
β”œβ”€β”€ faith/            ← MERGED: wins-faith + bible-study + bible-genesis + sermons-faith + christian-life + reflections (~240 convos)
β”œβ”€β”€ writing/          ← MERGED: business-content + book-club + field-notes + podcast-planning + career-linkedin (~134 convos)
β”œβ”€β”€ homeschooling/    ← GPT: homeschooling (25 convos)
β”œβ”€β”€ finance/          ← GPT: finance-investing (32 convos)
β”œβ”€β”€ daily-logs/       ← GPT: daily-logs (28 convos)
β”œβ”€β”€ app-dev/          ← GPT: app-dev (17 convos)
β”œβ”€β”€ ai-ethics/        ← GPT: ai-ethics (10 convos)
β”œβ”€β”€ common/           ← GPT: general (780 convos) + ALL unmapped g-p-* GPTs
β”œβ”€β”€ index.csv
β”œβ”€β”€ index.md
└── ...
Merge rationale: Some GPTs are too granular to warrant separate folders. Faith-related GPTs share a domain; writing/content GPTs share a domain. User confirmed this structure. Unmapped GPTs: Small GPTs (1-9 convos, g-p-* IDs) go to common/. User may delete thin ones later.

Image and Asset Handling

ChatGPT exports include assets as .dat files in original/ (1,001 files, ~464 MB total). The extraction script (batch_extract.py) skips all asset parts β€” it only processes string text parts.

.dat file breakdown (verified):
Type Count Notes
JPEG/JPG 901 User screenshots, uploads, generated images
PNG ~51 Including ~1 unmapped file
WebP 11
Markdown 3 Mapped to original names like go-before-you-know-1-samuel-16.md
Text 2 Pasted text.txt files
PDF 1 [client] Automotive Service Page.pdf
Unmapped 33 Mostly markdown/text content (titles like "How to...", "Why CTO...")
Mapping: conversation_asset_file_names.json maps 968 .dat filenames β†’ original filenames. 33 .dat files are unmapped (check first bytes to identify type β€” text files start with # or plain text, images start with magic bytes \xff\xd8 for JPEG, \x89PNG for PNG, RIFF for WebP). Strategy: Keep assets centralized in original/. Do NOT copy to project folders. Reference by asset pointer when needed. Conversations with inline image refs: The conversations JSON files have zero inline image_asset_pointer content_type references β€” the mapping tree's content.parts arrays contain only string parts. Images are standalone files, not referenced inline in conversation text. The chat.html export also has minimal image references.

To pull an asset for a specific conversation:

1. Find asset_pointer in conversation JSON (file-service://file-xxx) β€” if present

2. Look up file-xxx.dat in conversation_asset_file_names.json

3. File is at original/file-xxx.dat

Bulk extraction (for review/discard workflows): Rename .dat β†’ correct extension using the JSON mapping. Non-image files (markdown, text, PDF) should be extracted separately β€” they contain user-written content that may be valuable. After extraction, generate an HTML gallery for visual review:
python3 scripts/generate_gallery.py  # β†’ extracted/images/gallery.html

The gallery is a dark-themed grid with click-to-enlarge. Sync to Dropbox via rclone bisync, review on Mac/iPhone, delete unwanted images. See references/asset-review-workflow.md for the full review/discard workflow including bisync verification and non-image file handling.

Example extraction code:

with open('conversation_asset_file_names.json') as f:
    mapping = json.load(f)
IMAGE_EXT = {'.jpeg', '.jpg', '.png', '.webp', '.gif'}
for dat_name, original_name in mapping.items():
    if Path(original_name).suffix.lower() not in IMAGE_EXT:
        continue  # skip non-images; handle separately
    src = Path('original') / dat_name
    dst = Path('staging') / original_name
    if src.exists() and not dst.exists():
        shutil.copy2(src, dst)

Resuming a Partially-Complete Pipeline

When asked to "check the extraction status" or "continue the chat export work":

1. Audit actual state (don't trust task.md alone)

task.md may be stale. Verify by counting files on disk:

# Per-project extraction counts
for dir in extracted/*/:
    count = find(dir -name "*.md" -not -name "index.md" -not -name "manifest.json" | wc -l)

Then compare with manifest.json already_extracted flags. The manifest is the source of truth for what's been processed; disk count confirms files actually exist.

2. Update task.md

Refresh the Phase 2 progress table with actual counts. The "Next:" line should point to the smallest pending project first (quick wins build momentum), then the large remaining chunk.

3. Delegate remaining batches

  • Use manifest.json to identify unextracted conversations (already_extracted: false)
  • Sort by size (smallest first) for faster initial batches
  • Chunk into batches of ~20 (or by size tier if conversations vary wildly)
  • Dispatch to background subagents β€” they process the manifest, read raw JSON, write markdown files
  • Respect concurrency limits (check delegation.max_concurrent_children in config)
  • Kick off the next batch as slots free up (don't wait for all running batches β€” dispatch the next one as soon as any slot opens)
  • Regenerate manifest between batches: After each batch completes, re-run python3 scripts/batch_extract.py --project <name> to refresh already_extracted flags from disk before dispatching the next batch. The subagent may not have updated the manifest reliably, and stale flags cause duplicate work or missed conversations.
  • Parallel batch overlap prevention: When batch N is still running and you're dispatching batch N+1, you must skip items that batch N is processing. The manifest won't reflect batch N's progress yet (subagent hasn't updated it). Solution: after reading the fresh manifest, take the FIRST N unextracted items as batch N's working set, then skip those when building batch N+1. In practice: remaining = [c for c in manifest if not c['already_extracted']], batch N gets remaining[:20], batch N+1 gets remaining[20:40]. This avoids duplicate extraction without waiting for batch N to finish.
  • Subagent dispatch: include "search all JSON files" instruction. The manifest often lacks file_source. Every subagent dispatch prompt must include: "Search ALL conversations-*.json files in original/ to find which file contains each conversation ID. For Claude, check claude/original/conversations.json." Don't assume the subagent knows this β€” it doesn't have your context. Without this instruction, the subagent will guess file paths and fail to find conversations.

4. Phase 3 readiness check

Phase 3 (cross-cutting synthesis) can start when Phase 2 is ~70%+ complete. Count: (named project extractions done + common done) / total tier1 convos. Don't wait for 100% β€” start synthesis on what's available while finishing remaining extraction.

Continuing a Conversation in Hermes

ChatGPT conversations can't be resumed, but you can continue the thread in Hermes:

1. Look up the conversation in index.csv (by title or ID)

2. Pull full text from original/ JSON via batch_extract.py's load_conversation_text()

3. Summarize context: what was discussed, key conclusions, open questions, where it left off

4. Start a new Hermes session with that context loaded

5. User pastes the summary into the new thread to resume

The original conversation text is fully preserved in original/conversations-*.json β€” nothing is lost.

Consulting Extraction (Staged Files)

When extracting from pre-staged markdown files (not raw JSON), use the consulting-specific format documented in references/consulting-extraction-format.md. This covers the Themes/Key Ideas/Decisions/Questions/Novel Concepts template with metadata headers.

Parallel Sub-Agent Extraction Pattern (Phase 2)

For projects with 30+ conversations, use parallel sub-agents:

1. Pre-load text: Write a Python script that reads the batch plan (manifest + index.csv for file_source), loads each conversation's raw text from original/conversations-NNN.json via load_conversation_text(), and saves to temp files (conv_<sanitized>.txt). Group by file_source to avoid re-reading the same multi-MB JSON.

2. Split into batches of 10-12 conversations (NOT 15-17 β€” see timeout pitfall below). Save each batch as /tmp/writing_batch_N.json with the plan entries including text_file paths.

3. Dispatch 3 parallel sub-agents via delegate_task. Each gets:

  • Batch plan path
  • Output directory
  • Exact markdown format template (with Source ID, File Source, Wiki Concepts)
  • Instruction to write summary.md after processing all conversations

4. Monitor progress: Check ls extracted/<project>/*.md | wc -l periodically. If a sub-agent times out, identify remaining conversations and dispatch a follow-up sub-agent.

5. Post-processing: Update manifest.json to mark newly extracted files as already_extracted: true, update task.md, run batch_extract.py --status.

Extraction format for Phase 2 (richer than consulting): Inline bold metadata headers (NOT YAML frontmatter β€” subagents produce more reliably with inline format, and it matches all existing files on disk):
# <Title>
Source ID: <conversation UUID>
File Source: conversations-NNN.json
Date: YYYY-MM-DD
Tags: tag1, tag2

Themes

1. <Theme Title> β€” <Description>

Key Ideas (synthesized)

- <Key idea with explanation>

Questions Explored

- <Question>

Decisions Made

- <Decision>: <Details>

Novel Concepts

- <Concept>: <Description>
Why inline bold, not YAML frontmatter: Subagents produce more consistently with inline Field: syntax. YAML frontmatter requires exact formatting (indentation, colon placement) that subagents frequently botch. All existing extracted files (health, homeschooling, writing) use this inline format. Match the established convention. Exception β€” task-specified format: When the extraction task explicitly specifies YAML frontmatter (e.g., a parent agent's instructions), use YAML. Some extraction pipelines parse frontmatter programmatically and need it. The tradeoff: YAML is machine-parseable but subagents occasionally produce malformed output; inline bold is human-readable and more reliable for subagents but harder to parse automatically. Choose based on whether the output needs programmatic ingestion. Direct Python scripts (not subagents) can produce YAML frontmatter reliably β€” the subagent unreliability warning applies only to LLM-generated output. Post-extraction summary.md format:
  • Cross-cutting themes across ALL conversations
  • Top concepts worth promoting to wiki (with [[concepts/name]] links)
  • Key decisions and patterns
  • Interconnections between conversations
  • Evolution over time (for multi-year corpora)

Dual-Corpus Projects

Some projects (notably common/) merge conversations from both ChatGPT and Claude into a single manifest. The raw data lives in separate directories:

  • chatgpt/original/conversations-NNN.json (ChatGPT format)
  • claude/original/conversations.json (Claude format, single file)

When extracting from a dual-corpus project:

1. Load the manifest to get metadata (title, date, tags, chars)

2. Check each conversation's corpus field to determine which raw directory to search

3. Load from the correct directory β€” ChatGPT files use mapping tree, Claude uses chat_messages array

4. You may need to load both ChatGPT and Claude raw files in the same script

Claude uuid lookup: When processing multiple Claude conversations, load conversations.json once and build a uuid→conversation dict for O(1) lookup: claude_lookup = {c['uuid']: c for c in claude_data if 'uuid' in c}. This avoids scanning the full list for each conversation. Manifest ID mismatch: The manifest IDs may not match raw file IDs (different export versions or UUID regeneration). When ID-based lookup fails, fall back to title-based search: iterate through raw conversations and match by title (ChatGPT) or name (Claude). This is slower but reliable. Dual-corpus corpus field: Each conversation in the manifest has a corpus field ("chatgpt" or "claude"). Use this to determine which raw directory to search:
  • corpus: "chatgpt" β†’ search chatgpt/original/conversations-NNN.json
  • corpus: "claude" β†’ search claude/original/conversations.json

This avoids searching both directories when you know which corpus the conversation belongs to.

ChatGPT current_node traversal (alternative to children dict): Instead of building a children index and walking from root, you can trace from current_node back to root via parent references, then reverse the chain for chronological order. This is simpler for single-path conversations:
chain = []
node_id = conv.get("current_node")
while node_id and node_id in mapping:
    chain.append(mapping[node_id])
    node_id = mapping[node_id].get("parent")
chain.reverse()  # Now chronological

Pitfalls

  • Honor user intent over literal paths. If the user says "writing project" but the quoted instruction references a faith manifest path, follow the intent (writing), not the literal path. The user's explicit project name takes priority over copy-pasted path references. Prompt injection from reply threads can carry stale paths β€” always verify against the user's actual request.
  • Sub-agent timeout at 600s with very large batches. Batches of 25+ conversations (especially with 50K+ char convos) can hit the 600s timeout. Safe batch size is ~20 conversations for mixed-size corpora (20K-60K chars). If a timeout occurs, dispatch a follow-up sub-agent for the remaining items rather than retrying the full batch.
  • Max concurrent children is 3. delegate_task(tasks=[...]) with 4+ tasks will error. Dispatch in groups of 3, then handle the remainder separately. When dispatching a single task while 3 are running, it may run synchronously (blocking) β€” this is fine, just means you get the result immediately.
  • Don't let subagents read raw JSON (for large batches). Pre-extract conversation texts to /tmp/<project>_convos/ before dispatching subagents. This avoids duplicate I/O (multiple subagents reading the same 3-6MB JSON) and eliminates tree-walking logic from subagent prompts. Exception: For small batches (≀10 convos across ≀2 JSON files), letting subagents read directly is fine β€” the /tmp/ staging adds setup overhead that exceeds the I/O savings.
  • ChatGPT conversation_template_id = project key. Map g-p-* to names via GPT_PROJECT_MAP.
  • Claude has no conversation-to-project link. Don't auto-group.
  • Progress in task.md, not JSON. User explicitly corrected.
  • Meaningful filenames, not UUIDs. User corrected 07fcfbfa.md.
  • Pilot before scale. Prevents wasting tokens on bad prompts.
  • Claude uses sender not role. Values: "human"/"assistant".
  • Claude unnamed conversations (~22%). Use summary as fallback.
  • conversation_template_id empty = 'general'. Don't leave raw IDs.
  • Manifest must have sanitized_title. Subagent uses for filenames.
  • Never invent titles for subagent dispatch. Always read sanitized_title directly from manifest.json. Do NOT sort the manifest, pick titles, and pass them to a subagent β€” the subagent will fail to find conversations because your title strings won't match the manifest exactly. Read the manifest, filter for already_extracted: false, and pass the actual id + sanitized_title values.
  • Search raw JSON by ID when index.csv lacks file_source. When the manifest doesn't include file_source and index.csv cross-reference fails, use execute_code to iterate through all conversations-*.json files, load each, check for matching IDs, and build an idβ†’file_source mapping. Group conversations by file_source to avoid loading the same multi-MB JSON file repeatedly. Fast ID search optimization: Instead of parsing full JSON for each file, read the file as a string and check if the target ID appears in the content. This is much faster for large files (3-6MB) when you only need to verify presence: if conv_id in open(f).read(). Build the idβ†’file_source mapping in one pass, then load only the files you actually need.
  • Script paths: corpus/scripts/ outputs to corpus/extracted/. NOT shared folder.
  • Assets are NOT in extraction output. batch_extract.py skips all non-string content parts. 1,001 .dat files (464 MB) stay in original/. 968 are mapped in conversation_asset_file_names.json (962 images + 6 text/md/pdf). 33 unmapped .dat files exist β€” mostly markdown/text content. Pull on-demand via the JSON mapping. See "Image and Asset Handling" section above.
  • Project folders merge multiple GPTs. Faith GPTs β†’ faith/, writing GPTs β†’ writing/. Don't create per-GPT folders for these.
  • Unmapped GPT IDs β†’ common/. Small g-p-* GPTs (1-9 convos) don't warrant their own folders.
  • Manifest lacks file_source. To find which conversations-NNN.json file a conversation lives in, join manifest id with index.csv id β†’ file_source. Group extractions by file_source to avoid loading the same multi-MB JSON file repeatedly.
  • Heuristic before LLM at scale. For 50+ conversations, run regex-based extraction first (Bible refs, themes, decisions), then LLM subagents only for top 5-10. Sending all through LLM is 10x slower and 20x more expensive with diminishing returns.
  • ChatGPT mapping is a tree, not a list. The children field on nodes is often None for ALL nodes β€” do NOT rely on it. Always build a reverse map from node.parent references: children_of[parent_id].append(node_id). Find root (parent=None), walk depth-first via the reverse map. Don't assume messages are flat, and don't assume children arrays exist. The only reliable traversal is parentβ†’children built from node.parent.
  • Retry after failed batch: regenerate manifest, don't guess. If a subagent times out or fails to find conversations, the manifest may be stale or the IDs may be wrong. Re-run python3 scripts/batch_extract.py --project <name> to regenerate the manifest (it updates already_extracted flags from disk), then read the fresh manifest for the next batch. Do not try to reconstruct IDs from partial output.
  • Check for existing bisync before starting one. rclone bisync --resync does a full comparison and can take 10-30 minutes for large wikis. If one is already running, wait for it to complete β€” starting a second bisync on the same paths causes conflicts. Verify with ps aux | grep "rclone bisync" | grep -v grep.
  • Task.md before execution. For multi-phase work (extraction, reorganization), write the plan and task.md first, get user approval, then execute. Don't start building scripts until the plan is documented.
  • enrich_source_metadata.py silently fails for some conversations. The script looks up file_source from index.csv using the conversation UUID. If the UUID isn't in index.csv (e.g., conversations extracted outside the main pipeline, or tier3 conversations not in the index), it defaults to conversations.json (wrong) instead of the correct conversations-XXX.json format. This produces 51+ files with incorrect file_source and unreliable source_id metadata. After enrichment, audit: count files with file_source: conversations.json (wrong) vs file_source: conversations-XXX.json (correct). Files with wrong metadata have source_ids that won't resolve to real ChatGPT URLs. Investigation and correction: see references/source-id-investigation.md for the diagnostic steps (check JSON files, title matching, content matching) and the decision framework for whether to correct or strip the metadata.
  • ChatGPT URL construction from source_id has limitations. The source_id in extracted files maps to the ChatGPT conversation UUID, and the URL format is https://chatgpt.com/c/{uuid}. However: (1) the URL only works if you're logged into the same ChatGPT account OR the conversation was explicitly shared, (2) shared_conversations.json has TWO different ID fields β€” id (internal UUID, same as index.csv) and conversation_id (the shareable UUID used in URLs) β€” these are NOT the same, (3) conversations with wrong source_id metadata (see above) won't load at all. Before constructing URLs, verify the source_id exists in index.csv.
  • Deletion of original/ is irreversible. Phase 4 of the task.md plan says "Delete original/ directory" β€” this removes all raw JSON (full transcripts) and .dat files (images). After deletion, only the extracted summaries remain. Always confirm with the user before executing this step, and consider archiving (compress) instead of deleting.