O histórico de trabalho do usuário já existe em disco — sessões do Claude Code, rollouts do Codex, o export do ChatGPT — mas em três formatos incompatíveis. O @alembic/sessions os normaliza num único SessionRecord, e o A4b (alembic import) transforma cada turno em registro de memória de transcript com id determinístico e tempo lido da fonte: reimportar é sempre no-op. Nada de Date.now(); nada de fabricação.
O escopo do pacote cabe numa palavra do próprio header: "SCOPE: READERS" (packages/sessions/src/index.ts:4–16). Três funções, cada uma dona de um formato on-disk real: readCodexSession (o rollout-*.jsonl de ~/.codex/sessions/YYYY/MM/DD/), readClaudeSession (o <sessionId>.jsonl de ~/.claude/projects/<cwd-codificado>/) e readChatGptExport (o conversations.json do data export — que devolve MUITOS records de uma vez). Não há escritor nem analytics aqui; as únicas deps são @alembic/contracts e zod, e o filesystem é injetado via deps.readText — o núcleo é puro e testável em memória (index.ts:16–23).
Pense como… um arquivista que recebe fitas de três gravadores diferentes — cada um com seu rótulo, sua velocidade, seu formato de bobina — e transcreve todas para a MESMA ficha de ata: quem falou, o que disse, quando a reunião começou. Onde quebra: o arquivista humano interpreta; estes leitores não — linha que não casa com o schema é pulada ou vira err, nunca "corrigida".
startedAt é lido DOS DADOS — o timestamp da primeira linha (Claude), o timestamp do session_meta (Codex), o create_time em segundos-epoch convertido para ISO (ChatGPT). Nunca Date.now()/new Date() como leitura de relógio, que a plan-VM proíbe (index.ts:18–22). Uma sessão importada hoje ou daqui a um ano carimba o MESMO tempo: o dela.O detalhe de robustez é a assimetria deliberada entre err e skip: um transcript sem sessionId ou sem timestamp algum é err — não dá para ancorar identidade nem tempo (claude-reader.ts:127–134); mas uma linha individual malformada, um tipo que não é turno (ai-title, summary, reasoning, function_call…) é apenas pulada (claude-reader.ts:24–26; codex-reader.ts:11–17). No ChatGPT, a árvore mapping é achatada a partir da raiz seguindo children, com um guard de visited contra ciclos (chatgpt-reader.ts:184–189), e o texto de cada nó é o join dos parts — nó de sistema vazio é descartado (chatgpt-reader.ts:14–17).
Quem decide qual leitor usar é a CLI, e a regra é legível de trás para frente (apps/cli/src/commands.ts:6246–6263): --format explícito SEMPRE vence; senão, o path fala — nome começando com rollout- ou um segmento /.codex/ ⇒ codex; um segmento /.claude/ ⇒ claude; conversations.json ou um .json que não é .jsonl ⇒ chatgpt. Path ambíguo? Fail closed: a mensagem manda passar --format claude|codex|chatgpt, em vez de adivinhar e importar lixo com cara de memória.
if (explicit !== undefined) return ok(explicit); // --format vence const lower = path.toLowerCase(); const base = basename(lower); if (base.startsWith('rollout-') || lower.includes('/.codex/')) return ok('codex'); if (lower.includes('/.claude/')) return ok('claude'); if (base === 'conversations.json' || (lower.endsWith('.json') && !lower.endsWith('.jsonl'))) { return ok('chatgpt'); }
Ler é metade; a outra metade é o mapper do hermes, sessionToTranscriptRecords (packages/hermes/src/memory/transcript-import.ts:51–86), que transforma cada turno NÃO-VAZIO num TranscriptRecord completo e já validado. As decisões anti-fabricação estão todas no envelope:
import-<agent>-<sessionId>-<turnIndex> — derivado, nunca aleatório (transcript-import.ts:30, 74). O agente está DENTRO do id de propósito: o log de transcript é um arquivo compartilhado particionado pelo campo agent, então dois agentes importando a mesma fonte precisam de ids distintos — "otherwise the agent-blind dedupe would skip the second agent's records entirely" (transcript-import.ts:70–74).at = Date.parse(session.startedAt) — o começo da sessão, nunca o relógio de agora (transcript-import.ts:55–56). Um startedAt imparseável devolve []: melhor nada do que um registro com tempo inventado (transcript-import.ts:22–24).transcript-import.ts:62–68).transcript-import.ts:89–91).A escrita idempotente é appendNewTranscriptRecords (transcript-import.ts:122–142): recebe o conjunto de ids já em disco, pula qualquer registro cujo id já exista (ou duplicado dentro do próprio lote) e falha o lote FECHADO na primeira porta que der err — "the caller never reports a partial success it cannot prove". O destino é <memory-dir>/transcript.jsonl (commands.ts:6021) — exatamente o arquivo que alembic memory transcript add|list usa. E o default do comando é dry-run $0: preview de contagens, nada escrito; --write é quem libera o append (commands.ts:5908–5912).
--distill --online adiciona uma segunda passada A4b: um modelo local destila a sessão em memória semântica + de decisão. Cada registro destilado é MARCADO como model-distilled — proveniência session:<id> + distilled:<modelId> (tags falsificadas pelo modelo são STRIPPED) e confiança ≤ 0.5 — e --write continua sendo o único portão de escrita. Sem --online, --distill falha fechado: sem gasto, sem fabricação.alembic import ~/.claude/projects/x/abc.jsonl --write duas vezes seguidas. O que a segunda run faz?import-<agent>-<sessionId>-<n>, transcript-import.ts:74) — a mesma fonte produz registros byte-idênticos — e appendNewTranscriptRecords pula todo id já presente (transcript-import.ts:122–142). Reimportar é seguro por construção; nem err, nem duplicata.agent faz parte do id do registro importado, e não só import-<sessão>-<n>?agent; se dois agentes importam a mesma sessão com ids iguais, o dedupe-por-id descartaria os registros do segundo — "two agents importing the same source must get DISTINCT ids" (transcript-import.ts:70–74). Ordenação não usa o id, e reimportação repetida continua no-op de propósito.Default é dry-run $0 (nada escrito, nada de rede):
# preview: formato inferido do path (.claude/ ⇒ claude) alembic import ~/.claude/projects/<dir>/<sessionId>.jsonl --agent alex --json # escrever de verdade (idempotente; rode 2× e compare appended/skipped) alembic import ~/.claude/projects/<dir>/<sessionId>.jsonl --agent alex --write # export do ChatGPT (MUITAS sessões de uma vez) alembic import ~/Downloads/conversations.json --agent alex --json # conferir o destino — o MESMO arquivo do multi-store alembic memory transcript list --agent alex --limit 5 # a regra de inferência e o mapper, na fonte sed -n '6246,6263p' apps/cli/src/commands.ts sed -n '51,86p' packages/hermes/src/memory/transcript-import.ts
Formato ambíguo (ex.: um .jsonl fora de .claude//.codex/) falha fechado — passe --format.
SessionRecord com startedAt lido DA FONTE (index.ts:18–22).claude-reader.ts:24–26).mapping é árvore — achatada com guard de ciclo; 1 export ⇒ MUITOS records (chatgpt-reader.ts:184–189).--format vence; senão o path decide; ambíguo ⇒ fail-closed (commands.ts:6246–6263).import-<agent>-<sess>-<n> + dedupe-por-id ⇒ reimportação no-op (transcript-import.ts:74, 122–142).--write gate a escrita; --distill marca interpretação como model-distilled (confiança ≤ 0.5).@alembic/ingestion caminha as famílias wiki dir-per-package e emite a ponte package.jsonl que alimenta o funil.