Skip to content

feat(cerveau): un cerveau local configurable, une table de réflexes, et le journal en fenêtre - #7

Merged
phmatray merged 4 commits into
mainfrom
feat/cerveau-local
Sep 5, 2026
Merged

feat(cerveau): un cerveau local configurable, une table de réflexes, et le journal en fenêtre#7
phmatray merged 4 commits into
mainfrom
feat/cerveau-local

Conversation

@phmatray

@phmatray phmatray commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Jarvis peut répondre sans réseau. Son oreille (voxtype) et sa voix (piper)
étaient déjà locales ; seul le cerveau sortait.

Le cerveau local

bin/jarvis-local parle à n'importe quel serveur compatible OpenAI —
llama-server, LM Studio, Ollama, LocalAI, vLLM — et imite exactement les
événements stream-json de claude -p
, que bin/jarvis-sentences avale
déjà. Le chemin de la voix ne sait pas quel cerveau lui parle, et c'est ce
qui rend la bascule possible en une dizaine de lignes.

Aucun outil, délibérément : un modèle de 4 milliards de paramètres qui
pilote un shell est un risque qu'on ne prend pas.

Configuré par trois lignes de l'âme (cerveau, cerveau-url,
cerveau-modele) et une section « Cerveau » dans le Control Center. Une
seule entrée « compatible OpenAI » plutôt que vingt fournisseurs : ils
parlent tous le même protocole.

Mesuré sur ce M2 sous Asahi

CPU / NEON 8t Vulkan / Honeykrisp
pp2048 62,9 t/s 108,0 t/s
tg128 11,6 t/s 15,2 t/s

Avec Qwen3.5-4B en Q4_K_M, 1,5 à 4,6 s avant le premier mot prononcé.

Deux pièges qui rendent un cerveau local muet sans rien dire :

  • un modèle « thinking » envoie sa réflexion dans reasoning_content,
    épuise son budget et la réponse n'arrive jamais--reasoning off
    obligatoire dans l'unité, et filtrage côté pont ;
  • le prefill de la persona coûte ~16 s. omarchy-jarvis warmup le paie au
    démarrage en envoyant exactement la persona que l'échange enverra — un
    préchauffage à « bonjour » sans prompt système réchauffe un préfixe que
    personne n'utilise. 23 376 ms → 2 581 ms au premier échange.

La table des réflexes

Interrogé hors ligne sur l'heure, le modèle a répondu « Il est 14h37 » à
23 h passées, avec aplomb, dans le format parlé exact de Jarvis. Une valeur
plausible et fausse est pire qu'un refus.

memory/REFLEXES.md répond à l'heure, la date, la batterie, le
ne-pas-déranger, le son et les thèmes sans modèle, en 200 ms, et
exactement
. Motifs ancrés sur le transcript entier comme NOISE.md, slot
numérique borné 0-100, liste d'interdits appliquée au chargement, et
omarchy-jarvis reflexe? pour l'asserter sans micro.

118 cas figés, dont 56 négatifs — de vraies demandes qui doivent
atteindre le cerveau.

Le journal en fenêtre Hyprland

PanelWindowFloatingWindow : Hyprland le tuile comme une application,
l'écran se partage. Vérifié floating=False, 780×953, le terminal voisin
réduit d'autant.

Trois bugs que la route locale a révélés

  • « Oublier » ne marchait pas hors nuage, et échouait silencieusement
    (le panneau appelle undo --quiet).
  • reset laissait la carte du panneau sur l'échange d'avant.
  • L'onglet « Aujourd'hui » restait vide en local : il lisait le JSONL de
    claude. Il se replie sur le journal du jour, écrit par les deux routes.

Plus un verrou mortel évité de justesse : la garde qui empêche un tour local
d'écraser $BRAIN_STATUS condamnait auto à rester local à vie. Le mode
retente le nuage toutes les dix minutes.

Audit d'excès d'ingénierie — 630 lignes en moins

sprites/variants.py et sprites/preview.py supprimés, voxtype.toml de
103 à 16 lignes (vérifié en transcrivant le même wav : sortie identique),
deux parseurs maison remplacés par argparse, les quatre sondes jumelles de
Journal.qml en un component, et une vingtaine d'autres coupes.


408 tests, 0 échec. Rebasé sur la 1.5.0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ls94YXz6tFuYvTzRW6if3t

phmatray and others added 3 commits September 5, 2026 02:10
Un audit paresseux de tout l'arbre, chaque coupe vérifiée avant d'être
appliquée plutôt que crue sur parole.

Les trois grosses :

- `sprites/variants.py` : huit poissons d'exploration qui ont fait leur
  travail — les quatre retenus sont redessinés dans `parts.BODIES` depuis
  longtemps. Les six symboles encore vivants (CW/CH, BW/BH, pad, compose)
  remontent en tête de parts.py, et le fichier part avec le reste.
- `sprites/preview.py` : un décodeur PNG écrit à la main pour un seul
  appelant, un test qui voulait la largeur d'une planche. `struct.unpack`
  sur l'IHDR la lit en une ligne. (La planche fait 144, pas 72 : generate
  rend en 2x — l'ancien test le disait, le nouveau aussi.)
- `voxtype.toml` : 103 lignes dont 89 étaient la configuration d'exemple du
  fournisseur — un démon que Jarvis ne lance pas, un raccourci qu'Hyprland
  gère, des retours audio que la voix fait elle-même. Vérifié en
  transcrivant le même wav avec l'ancien fichier et le nouveau : sortie
  identique, exit 0.

Le reste : deux parseurs d'arguments maison remplacés par argparse (qui
rend `--help` et de meilleurs messages en prime), les quatre sondes
jumelles de Journal.qml en un `component`, le second format d'options de
PillRow que ses cinq appelants n'ont jamais utilisé, deux réglages de
PixelSprite que personne ne règle, sept crochets morts du double de test
du Touch Bar, `--all-voices` (changer `voix:` et relancer suffit), et deux
options release-please inertes au-dessus de 1.0.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls94YXz6tFuYvTzRW6if3t
…uche

Le journal flottait au-dessus du travail en cours (`WlrLayer.Top`,
`exclusiveZone: 0`) : il fallait le fermer pour relire ce qu'il cachait.
En `FloatingWindow` — l'idiome que le greffon dev-gallery du shell emploie
déjà — Hyprland le tuile comme n'importe quelle application, l'écran se
partage, et il se déplace et se redimensionne avec les raccourcis que
l'utilisateur connaît déjà.

Vérifié : `class=org.quickshell`, `floating=False`, 780x953 à [807,34], et
le terminal voisin réduit d'autant.

Le clavier vient désormais du compositeur, donc toute la danse
WlrKeyboardFocus disparaît avec la couche. Les deux gestionnaires de
`visible` sont fusionnés — QML n'en garde qu'un : le focus à l'ouverture,
et `journalOpen` remis à false quand c'est le gestionnaire de fenêtres qui
ferme, sans quoi le prochain appui sur `journal` ne ferait rien.

Au passage, trois verbes IPC sans aucun émetteur (`ping`, `toggle`,
`journalClose`) et un `id` non référencé, trouvés par l'audit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls94YXz6tFuYvTzRW6if3t
Jarvis peut désormais répondre sans réseau. Son oreille (voxtype) et sa
voix (piper) étaient déjà locales ; seul le cerveau sortait.

## Le pont

`bin/jarvis-local` parle à n'importe quel serveur compatible OpenAI —
llama-server, LM Studio, Ollama, LocalAI, vLLM — et surtout n'invente pas
sa propre sortie : il imite exactement les événements stream-json de
`claude -p`, que `bin/jarvis-sentences` avale déjà. Le chemin de la voix ne
sait pas quel cerveau lui parle, et c'est ce qui rend la bascule possible
en une dizaine de lignes.

Il n'a aucun outil, délibérément : un modèle de 4 milliards de paramètres
qui pilote un shell est un risque qu'on ne prend pas.

Trois lignes dans l'âme (`cerveau`, `cerveau-url`, `cerveau-modele`) et une
section « Cerveau » dans le Control Center. Une seule entrée « compatible
OpenAI » plutôt que vingt fournisseurs : ils parlent tous le même
protocole.

## Ce qui a été mesuré, et qui a décidé du reste

Sur ce M2 sous Asahi, Vulkan/honeykrisp bat le CPU d'un facteur 1,7 en
prefill et 1,3 en génération. Avec Qwen3.5-4B en Q4_K_M, la première phrase
prononcée arrive en 1,5 à 4,6 s à chaud.

Deux pièges qui rendent un cerveau local muet sans rien dire :

- un modèle « thinking » envoie sa réflexion dans `reasoning_content`,
  épuise son budget de tokens et la réponse n'arrive JAMAIS. D'où
  `--reasoning off` dans l'unité, obligatoire, et le filtrage côté pont.
- le prefill de la persona coûte ~16 s. `omarchy-jarvis warmup` le paie au
  démarrage du serveur, en envoyant EXACTEMENT la persona que l'échange
  enverra : un préchauffage à « bonjour » sans prompt système réchauffe un
  préfixe que personne n'utilise, et le premier échange coûtait encore 23 s
  (mesuré). 23 376 ms → 2 581 ms.

## La table des réflexes

Interrogé hors ligne sur l'heure, le modèle a répondu « Il est 14h37 » à
23 h passées, avec aplomb, dans le format parlé exact de Jarvis. Une valeur
plausible et fausse est pire qu'un refus.

`memory/REFLEXES.md` répond donc à l'heure, la date, la batterie, le
ne-pas-déranger, le son, les thèmes — sans modèle, en deux cents
millisecondes, et exactement. Motifs ancrés sur le transcript entier comme
`NOISE.md`, slot numérique borné 0-100, liste d'interdits appliquée au
chargement (ce chemin ne passe pas par settings.json, c'est sa seule
porte), et `omarchy-jarvis reflexe?` pour l'asserter sans micro.

118 cas figés, dont 56 négatifs : de vraies demandes qui doivent atteindre
le cerveau. C'est cette liste qui grandit quand une rangée se révèle trop
gourmande.

## Trois bugs que la route locale a révélés

- « Oublier » ne connaissait que les sessions claude, donc échouait sur
  toute la route locale — et silencieusement, le panneau appelant
  `undo --quiet`. Il retire maintenant la dernière paire du fil JSON, et un
  refus se dit par notification.
- `reset` effaçait le fil mais pas `$LAST_ASK`/`$LAST_REPLY` : la carte du
  panneau continuait d'afficher l'échange d'avant, ce qui se lit comme
  « il ne s'est rien passé ».
- L'onglet « Aujourd'hui » lisait le JSONL de claude et restait donc vide
  en local. Il se replie sur le journal du jour, qui porte l'heure et la
  paire, et que les deux routes écrivent.

## Aussi

`brain_set` ne touche plus `$BRAIN_STATUS` sur un tour local — il décrit le
nuage, et un succès local y écrivait `ok` par-dessus un quota encore
valide. Le mode `auto` retente le nuage toutes les dix minutes : sans ce
délai, la garde ci-dessus condamnait Jarvis à rester local à vie.

`set_setting` utilise `|` comme séparateur sed : une URL contient des
barres obliques, et le premier enregistrement d'adresse depuis le panneau
aurait corrompu l'âme.

Enfin, la part de l'audit qui touchait `bin/jarvis` et `bin/jarvis-transcript`
est ici faute de pouvoir séparer les fichiers : mascot_emote/mascot_aside
fusionnés, un heredoc python remplacé par `tac | awk | tac`, sept
surcharges JARVIS_DEADLINE_* et JARVIS_MODEL que rien ne posait, quatre
clés de `status` que le panneau ne lit pas, deux échafaudages refaits, et
argparse dans jarvis-transcript.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls94YXz6tFuYvTzRW6if3t
`show --projects … --json 30` : argparse regroupe les positionnels au
PREMIER cluster, remplit `cmd` avec « show », et déclare le 30 final
« unrecognized arguments ». Le comportement dépend de la version de
Python — vert en local sur 3.14, rouge en CI — ce qui est précisément le
genre de différence qu'une suite hors-ligne doit attraper, et qu'elle a
attrapée.

`parse_intermixed_args` existe pour ce cas exact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ls94YXz6tFuYvTzRW6if3t
@phmatray
phmatray merged commit 9ab867c into main Sep 5, 2026
2 checks passed
@phmatray
phmatray deleted the feat/cerveau-local branch September 5, 2026 00:19
@phmatray

phmatray commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Diagnostic du check rouge — tes tests dépendent du fuseau, pas de mon empaquetage

J'ai d'abord vérifié que ce n'était pas moi : origin/main seul passe 374 / 0. L'empaquetage est propre.

Sur ta branche, en revanche :

ici, en CEST      →  408 réussite(s), 0 échec(s)
TZ=UTC (le runner) →  406 réussite(s), 2 échec(s)
la CI à 22:13 UTC  →  405 réussite(s), 5 échec(s)

Le nombre d'échecs change avec l'heure d'exécution, ce qui est la signature d'une frontière de date. Les cas touchés écrivent un journal daté $(date +%F) (tests/run:1540) puis attendent que jarvis transcript le retrouve — et les deux ne calculent pas « aujourd'hui » au même moment de la journée selon le décalage.

Reproduction en une ligne, ici, sans CI :

TZ=UTC ./tests/run

Les deux cas qui tombent à 00:21 UTC :

✗ transcript : les derniers échanges, avec ce qu'il a pensé et fait — le dernier échange
✗ conversations : la liste, la courante marquée, les rondes à part — la courante est marquée et comptée

Le second n'est pas dans la liste de la CI, ce qui confirme que l'ensemble qui tombe dépend de l'heure exacte.

C'est ta fonctionnalité et ton dessin de test, donc je ne touche à rien. Deux directions selon ton intention : épingler TZ dans tests/run pour rendre la suite déterministe, ou faire écrire au test la même date que celle que le code interroge plutôt que date +%F.

Pour mémoire : je n'ai pas mergé cette PR. Le rouge est sur tes propres tests, et merger par-dessus casserait main — c'est le seul geste que je me refuse ici.

@phmatray

phmatray commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Correction — mon diagnostic ci-dessus est faux, ne le suis pas

Trois choses que j'aurais dû vérifier avant de le poster :

tests.yml:43 épingle déjà TZ: Europe/Brussels, et l'épinglait déjà au commit rouge. Mon TZ=UTC ./tests/run écrasait donc un réglage délibéré : les deux échecs que j'ai « reproduits » étaient fabriqués par moi, pas révélés.

Le run rouge portait sur 5280156, une tête périmée. Tu as poussé b767c6f à 00:17:46 — vert — puis mergé à 00:19:29. J'ai lu l'échec de l'avant-dernier commit et l'ai présenté comme l'état de la PR.

Il n'y a pas de flake au fuseau. main est verte, et pour la bonne raison.

Ce qui reste vrai et qui a servi : origin/main seul passe 374/0, donc l'empaquetage n'était pas en cause. Le reste de ce commentaire précédent est à jeter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant