feat(mem): auto-curadoria do cérebro via assinatura Claude do usuário#154
Merged
Conversation
… (porta mecanismo do Odysseus) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… lock cross-process, isolamento por conta, PII em claro, expiry ms/s + expires_in, bare-token TTL) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fresh) Porta o mecanismo do Odysseus para o engine (D1 do spec 2026-06-19-curadoria-assinatura-claude): módulo PURO src/claude-subscription.ts, sem DB e sem rede real, usando `fetch` nativo (não o SDK Anthropic, que usa x-api-key). Constantes fixas e centralizadas (CLIENT_ID, TOKEN_URL/MESSAGES_URL, anthropic-version, oauth-beta, TTL default, spoof de Claude Code). - parsePastedCredential: 3 formatos (bare sk-ant-oat01, JSON claudeAiOauth, flat), heurística epoch ms-vs-s (>1e12), TTL default sem expiry, lixo -> erro claro. - oauthHeaders: Authorization Bearer + anthropic-beta oauth-2025-04-20, NUNCA x-api-key. - buildSubscriptionSystemBlocks: identidade + guidance SEMPRE antes do system do usuário (sem isso, 429 da Anthropic em Opus/Sonnet). - needsRefresh: skew 300s; sem refresh_token (bare token) nunca tenta. - supportsEffort: Opus >=4.5, Sonnet >=4.6, Fable/Mythos (boundary octopus). - refreshSubscription: POST grant_type=refresh_token + client_id; novo expiry de expires_in (delta s); preserva refresh_token antigo quando a resposta nao traz. AC 1-4 verdes (29 testes). Token nunca logado. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + callSubscription)
D2/D3 do spec 2026-06-19-curadoria-assinatura-claude:
src/claude-subscription-store.ts — persiste a credencial no vault existente
(account_secrets kind claude_oauth, JSON {access_token,refresh_token,
expires_at ISO,auth_mode} criptografado) e expõe a chamada central.
- loadCredential/saveCredential/deleteCredential via set/get/deleteAccountSecret.
- Erros tipados: SubscriptionNotConnected, SubscriptionReauthRequired,
SubscriptionRateLimited.
- resolveAccessToken(accountId, deps): accountId EXPLÍCITO (nunca getAccountId,
que cairia no fallback 'bruno'). Refresh sob lock CROSS-PROCESS
(pg_advisory_xact_lock(hashtext('claude_refresh:'||accountId)) numa transação),
atrás do seam injetável deps.withAccountLock pra testar a serialização sem
Postgres real. Re-lê + re-checa sob o lock, refresca preservando o refresh_token
antigo quando ausente, grava. Sem credencial -> NotConnected; 401 -> Reauth.
- callSubscription: resolve -> headers OAuth (Bearer, nunca x-api-key) + payload
com spoof de Claude Code + max_tokens; effort/thinking só em supportsEffort e aí
OMITE temperature; URL FIXA /v1/messages; 401->Reauth, 429->RateLimited; token
NUNCA logado (catch loga só status/mensagem).
AC 4b/5/9 verdes. Build (tsc) limpo; suite completa 1391/1391, zero regressão.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r conta
Adiciona POST /portal/claude/connect, POST /portal/claude/disconnect e
GET /portal/claude/status em src/portal/routes.ts (spec D7/AC9). Account
sempre de res.locals.accountId (sessão, nunca body/query). connect valida
string não-vazia + teto de 16 KB antes de parsear, chama parsePastedCredential
→ saveParsedCredential e devolve só {ok, expires_at}; parse inválido → 400
invalid_credential. status devolve {connected, expires_at?, needs_reauth?} sem
nunca expor access/refresh token. disconnect chama deleteCredential da conta da
sessão. Testes de rota com pool em memória (sem Postgres).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inatura/cursor Plumbing account-scoped para o extrator de memória (spec 2026-06-19-curadoria-assinatura-claude-design, D4/D6): - storage.ts: listConversationMemoriesSince(accountId, since, limit) — SELECT read-only de brain_chunks source_type='conversation' NEWER que o cursor, oldest-first, account_id como único filtro de tenant. NUNCA muta brain_chunks nem o índice de busca (fica fora do eval gate). - claude-subscription-store.ts: listAccountsWithSubscription() (DISTINCT account_id em account_secrets kind='claude_oauth' — quem tem assinatura, NÃO quem tem fatos) + load/saveMemoryExtractCursor() (cursor por conta em account_secrets kind='memory_extract_cursor'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… isolado)
Agente de auto-curadoria powered pela assinatura da própria pessoa (spec
2026-06-19-curadoria-assinatura-claude-design, D4/D5/D6; AC 6/7/8):
- memory-extractor.ts: runMemoryExtraction(deps) processa CADA conta com
assinatura DENTRO de requestContext.run({authType:'bearer',scopes:[],accountId,
isOperator:false}) — accountId SEMPRE explícito, nunca getAccountId(). Lê
memórias de conversa desde o cursor (orçamento 50/conta), chama callSubscription
(opus-4-8, maxTokens 1024), parseia array JSON (tolera ```json). Cada fato:
looksLikeSecret descarta, stripSecrets redige → upsertProfileFact status=signal
source=llm + insertMemoryAudit trigger=llm com evidence_ref=PONTEIRO (source_id,
nunca conteúdo/token). Cursor avança; erros por conta (parse/401/429) pulam a
conta sem derrubar as outras. A conta A só lê a própria credencial e só escreve
fatos com o próprio account_id.
- memory-extraction-tick.ts: tickMemoryExtraction(label, run?) atrás do gate
MEMORY_EXTRACT_ENABLED (kill-switch DENTRO do tick), lazy-import das deps reais,
recordRun(source='memory-extraction'). Espelha memory-curation-tick.
- index-classifier.ts: cron noturno (default 04:45, após a curadoria 04:15).
- Testes (deps injetadas, sem rede/DB): sinais, audit com ponteiro, isolamento
A/B (AC7), guarda de segredo, cursor, gating off (AC8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adiciona ao Perfil logado (Next.js) um card onde a pessoa conecta a
assinatura Claude dela colando a credencial do `claude setup-token`, vê
o status (conectado / desconectado / reconectar) com o expiry, e
desconecta com confirmação inline. Consome o contrato já no backend:
GET /portal/claude/status, POST /portal/claude/{connect,disconnect}.
- web/hooks/claude.ts: useClaudeStatus + connect/disconnect (invalidam
no sucesso), no padrão de hooks/perfil.ts.
- web/components/profile/ClaudeConnectionCard.tsx: estados
loading/erro/desconectado/conectado/needs_reauth; trata 400
invalid_credential com mensagem clara; nunca exibe a credencial depois
de enviada (limpa no sucesso).
- contracts: ClaudeStatusResponse / ClaudeConnect{Request,Response}.
- plugado no Perfil após o BudgetMeter.
- 5 testes de componente (vitest); typecheck + build + build basePath /app verdes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
O quê
O cérebro se mantém sozinho: para cada conta que conecta a própria assinatura Claude, um agente server-side (cron) usa a conexão dela para extrair fatos das memórias de conversa → vira sinal na escada de confiança do perfil curado. Custo na assinatura da pessoa, não numa chave central. Porta o mecanismo OAuth-de-assinatura do Odysseus. Spec (revisado adversarialmente):
docs/superpowers/specs/2026-06-19-curadoria-assinatura-claude-design.md.Componentes
claude-subscription.tspuro +claude-subscription-store.ts): parse da credencial colada (3 formatos + heurística ms/s), headers OAuth (Bearer+oauth-2025-04-20, nunca x-api-key), spoof de Claude Code (system blocks), refresh com lock cross-process (pg_advisory_xact_lock),callSubscription(accountId,...)(accountId explícito, nunca getAccountId; effort gated por modelo; token nunca logado). Vaultaccount_secretskindclaude_oauth./portal/claude/{connect,disconnect,status}: account deres.locals(nunca input), teto de 16 KB, token nunca exposto.memory-extractor.ts+ tick gatedMEMORY_EXTRACT_ENABLED): loop por conta com assinatura,requestContext.runpor conta (isolado), só LÊ brain_chunks (fora do eval gate), guarda de segredo (looksLikeSecret/stripSecrets),memory_audittriggerllmcomevidence_ref= ponteiro, cursor por conta.web/app/(app)/perfil/): card "Conexão Claude" (connect via paste doclaude setup-token, status com expiry, disconnect).Segurança
NEXT_PUBLIC_BASE_PATH=/app bun run buildOK.Deploy (após merge)
Engine (rotina) + build do Next na VPS. Enable:
MEMORY_EXTRACT_ENABLED=true(kill-switch global; cada conta entra ao conectar a assinatura).🤖 Generated with Claude Code