Curso / Lição 9  ·  EN
Lição 09 · Mergulho profundo · subsistema 3 de 7

UsageStore + runCurator — a metade da disposição

O ciclo fechado tem duas metades: o SkillStore autora memória procedural, e o curador dispõe do que fica sem uso. UsageStore é um sidecar de telemetria que conta use/view/patch de skills; runCurator é um passo determinístico que move skills de agente há muito ociosas active → stale → archived. Dois invariantes o definem: nunca deletar (archive é o estado terminal), e TEMPO é um Clock injetado — nunca Date.now(). Um CLONE do skill_usage.py + curator.py do Hermes.

O ciclo de vida

active criado aqui stale ocioso ≥ 30d archived ocioso ≥ 90d · terminal corte de stale corte de archive usado de novo ⇒ reativa velho o bastante ⇒ active pula direto para archived (pula stale)

O motor de transições — a ordem dos ramos é estrutural

A decisão é uma função pura sobre uma âncora de inatividade e dois cortes. A ordem dos ramos — archive primeiro, depois stale, depois reativar — espelha o apply_automatic_transitions exatamente:

// packages/hermes/src/curator/curator.ts:123-136 — nextState
const nextState = (record, staleCutoff, archiveCutoff): SkillState => {
  const anchor = record.lastActivityAt > 0
    ? record.lastActivityAt
    : Number.POSITIVE_INFINITY;          // nunca-ativo ⇒ nunca velho
  const current = record.state;
  if (anchor <= archiveCutoff && current !== 'archived') return 'archived';
  if (anchor <= staleCutoff   && current === 'active')   return 'stale';
  if (anchor >  staleCutoff   && current === 'stale')    return 'active'; // reativa
  return current;
};
A âncora de nunca-ativo — um detalhe preciso

Uma skill recém-criada tem lastActivityAt = 0 (criação não é atividade). Se 0 fosse usado literalmente como âncora, seria ≤ todo corte e a skill arquivaria no primeiro passo. Então o código ancora um registro nunca-ativo a Number.POSITIVE_INFINITY — mais novo que qualquer corte — garantindo que ele não fique nem stale nem archived até ter sido de fato usado e ficado ocioso. O teste "never archives a brand-new, never-active skill (lastActivityAt = 0)" prova. (O comentário do cabeçalho fraseia isto como "tratado como agora"; a implementação usa +Infinity, que tem o mesmo efeito e é independente da ordem.)

Por que archive-primeiro? Uma skill active ociosa além do corte de archive deve cair em archived diretamente — pulando stale — então o ramo de archive precisa vencer antes do ramo de stale. O teste "jumps active → archived directly when old enough (skips stale)" protege essa ordem.

Os dois portões: proveniência e pin

Antes de qualquer transição, runCurator aplica dois opt-outs ortogonais. Só skills autoradas pelo agente são geridas pelo curador, e uma skill pinned é isenta em todo caminho:

// packages/hermes/src/curator/curator.ts:86-108 (condensado)
for (const name of names) {
  const record = sidecar[name];
  if (record.createdBy !== 'agent') { skipped.push({ name, reason: 'not-agent-created' }); continue; }
  if (record.pinned)             { skipped.push({ name, reason: 'pinned' });           continue; }
  const to = nextState(record, staleCutoff, archiveCutoff);
  if (to === record.state)       { skipped.push({ name, reason: 'no-change' });        continue; }
  const persisted = await deps.usage.put(name, { ...record, state: to });
  if (!persisted.ok) return persisted;     // fail-closed: não reporta trabalho não salvo
  transitioned.push({ name, from: record.state, to });
}

Skips são reportados, não escondidos — o CuratorReport carrega tanto transitioned[] quanto skipped[] (cada um com razão tipada: pinned/not-agent-created/no-change), ambos em ordem estável de nome. E a persistência é por-transição: se um put falha, o passo aborta com aquele err em vez de reportar uma mudança de estado que não aconteceu duravelmente.

O sidecar de telemetria — leituras best-effort, escritas atômicas

UsageStore registra eventos incrementando um contador e carimbando lastActivityAt = clock(). Carrega a mesma assimetria leitura/escrita do memory store — um sidecar corrompido lê como vazio (nunca quebra um caminho quente), mas uma escrita que falha emerge como err:

// packages/hermes/src/curator/usage-store.ts:153-166 — load best-effort
private async load(): Promise<UsageSidecar> {
  const stat = await tryCatchAsync(() => this.fs.stat(this.sidecarPath));
  if (!stat.ok || !stat.value) return {};        // ausente ⇒ vazio
  const read = await tryCatchAsync(() => this.fs.readText(this.sidecarPath));
  if (!read.ok) return {};                         // erro de IO ⇒ vazio
  const parsed = tryParseJson(read.value);
  if (!parsed.ok) return {};                       // JSON ruim ⇒ vazio
  const valid = usageSidecarSchema.safeParse(parsed.value);
  if (!valid.success) return {};                   // forma errada ⇒ vazio
  return valid.data;
}
Um clock, dois leitores. O mesmo Clock é injetado no UsageStore e no runCurator (curator.ts:53-59 diz isto explicitamente). Então "um evento registrado agora" e "uma transição decidida agora" concordam — não há skew entre quando a atividade foi carimbada e quando a obsolescência é julgada. O sidecar é serializado com chaves ordenadas + indent de 2 espaços (espelhando o json.dump(..., sort_keys=True, indent=2) do Python) para que as escritas sejam byte-estáveis e amigáveis a diff.
1. Uma skill tem createdBy: 'user' e não foi tocada em um ano. O que runCurator faz?
Correto: c. O portão de proveniência toca só skills autoradas pelo agente; skills user/bundled/hub são reportadas como skipped com not-agent-created. (E nada é jamais deletado — archive é o estado terminal.)
2. Uma skill de agente recém-criada tem lastActivityAt = 0. No primeiro passo do curador ela é…
Correto: b. Criação não é atividade, então lastActivityAt fica 0; nextState ancora isso a Number.POSITIVE_INFINITY para que a skill não fique nem stale nem archived até ter sido genuinamente usada e depois ficado ociosa.
3. Por que a ordem dos ramos em nextState (archive → stale → reativar) é estrutural?
Correto: d. Se stale fosse checado primeiro, uma skill active velha só daria um passo para stale neste passo em vez de pular para archived. O teste "jumps active → archived directly" protege a ordem, espelhando apply_automatic_transitions.

Confusões comuns

"Archived significa deletado." Não — archive é recuperável e é o estado terminal por invariante ("max action = archive"). O core portável nunca remove nada; a movimentação para o diretório .archive/ é uma preocupação de transporte fora de escopo. Um caminho de reativação até traz uma skill stale de volta a active se ela for usada de novo antes do corte de archive.
"Um sidecar corrompido vai quebrar as chamadas de skill." Não — leituras são best-effort: um sidecar ausente, ilegível, com JSON malformado, ou de forma errada todos colapsam para um mapa vazio. O ponto inteiro é que um arquivo de telemetria quebrado nunca quebre a chamada de skill do host. Só uma falha explícita de escrita emerge como err.