---
tags: [archive, nodered, brouillon]
status: archived
---

Voici la synthèse. Comme vous n'avez plus de SSE, la table inverse `session → room` disparaît : il ne reste qu'une table de contexte persistante.

## 1\. Flux principal (4 subflows)

```mermaid
flowchart TD
  IN["Matrix in"] --> FIL["Function: Filtrer
  MXID autorisé, pas le bot"]
  FIL --> CMD{"Switch: commande ?"}

  CMD -- "/new" --> NEW["Function: oublier la session"]
  CMD -- "/stop" --> STOP["HTTP POST interrupt"]
  CMD -- "/status" --> STAT["HTTP GET active"]
  CMD -- "message normal" --> SF1

  SF1[["SUBFLOW 1
  OC Session"]] --> BUSY{"Function: Garde busy"}
  BUSY -- "déjà occupée" --> MSGBUSY["Texte: déjà en cours"]
  BUSY -- "libre" --> ACK["Matrix Typing on
  + message Reçu"]
  ACK --> PREP["Function: Prép. prompt"]
  PREP --> POST["HTTP POST prompt"]
  POST --> SF2[["SUBFLOW 2
  OC Attendre fin"]]

  SF2 -- "sortie 1: terminé" --> SF3[["SUBFLOW 3
  OC Lire réponse"]]
  SF2 -- "sortie 2: timeout" --> MSGTO["Texte: toujours en cours"]
  SF3 --> SF4[["SUBFLOW 4
  Matrix Envoyer"]]
  MSGTO --> SF4
  MSGBUSY --> SF4
  NEW --> SF4
  STOP --> SF4
  STAT --> SF4
  SF4 --> FREE["Function: Libérer busy
  + Typing off"]

  CATCH["Catch: tout le flux"] --> ERR["Function: message d'erreur"]
  ERR --> SF4
  ERR --> FREE

  SESS[("GLOBAL persistant
  oc_sessions")]
  BUSYCTX[("FLOW mémoire
  busy")]
  SF1 -.-> SESS
  NEW -.-> SESS
  BUSY -.-> BUSYCTX
  FREE -.-> BUSYCTX

  classDef sub fill:#dbeafe,stroke:#2563eb,color:#000
  classDef ctx fill:#fef3c7,stroke:#d97706,color:#000
  class SF1,SF2,SF3,SF4 sub
  class SESS,BUSYCTX ctx
```

## 2\. Intérieur du subflow 2 « Attendre la fin »

```mermaid
flowchart TD
  I["Entrée"] --> INIT["Function: init
  attempts=0, absent=0"]
  INIT --> DLY["Delay fixe: POLL_MS"]
  DLY --> TYP["Matrix Typing"]
  TYP --> GET["HTTP GET /api/session/active"]
  GET --> DEC{"Function: Décision"}
  DEC -- "encore actif" --> DLY
  DEC -- "absent ABSENT_REQUIRED fois" --> O1["Sortie 1: terminé"]
  DEC -- "attempts > MAX_ATTEMPTS" --> O2["Sortie 2: timeout"]
```

## 2\. Subflow 1 : OC Session

```mermaid
flowchart TD
  I["Entrée"] --> SW{"Switch: sessionId vide ?"}
  SW -- "non" --> O["Sortie"]
  SW -- "oui" --> CP["Change: corps de création
  title = roomId"]
  CP --> HC["HTTP POST /api/session"]
  HC --> CS["Change:
  sessionId = payload.data.id
  table oc_sessions roomId = sessionId"]
  CS --> O
```

## 3\. Subflow 2 : OC Attendre fin

```mermaid
flowchart TD
  I["Entrée"] --> DLY["Delay fixe: POLL_MS"]
  DLY --> TYP["Matrix Typing"]
  TYP --> GET["HTTP GET /api/session/active"]
  GET --> CH["Change: compteurs JSONata
  running, attempts+1, absent"]
  CH --> SW{"Switch: décision
  premier match seulement"}
  SW -- "attempts > MAX_ATTEMPTS" --> O2["Sortie 2: timeout"]
  SW -- "absent >= ABSENT_REQUIRED" --> O1["Sortie 1: terminé"]
  SW -- "sinon" --> DLY
```

## 4\. Subflows 3 et 4

```mermaid
flowchart LR
  subgraph SF3["SUBFLOW 3 : OC Read Response"]
    direction TB
    A1["Change: url du GET message"] --> A2["HTTP GET message
    type=assistant, order=desc, limit=1"]
    A2 --> A3["Change: extraction du texte
    ⚠️ à écrire après le debug"]
  end
  subgraph SF4["SUBFLOW 4 : Matrix Send"]
    direction TB
    B1["Split: chaîne par longueur
    8000 caractères"] --> B2["Delay: limitation 1 msg/s"]
    B2 --> B3["Matrix out"]
  end
  SF3 --> SF4
```




Les compteurs `attempts` et `absent` vivent dans le `msg`, pas dans le contexte, pour que plusieurs conversations puissent tourner en parallèle.

## 3\. Contenu des subflows

| Subflow | Entrée | Sortie | Contenu |
| --- | --- | --- | --- |
| 1\. OC Session | `msg.roomId` | `msg.sessionId` | Lit `oc_sessions[roomId]`. Si absent, `POST /api/session` puis enregistre. Un `404` ultérieur doit supprimer l'entrée et recréer. |
| 2\. OC Attendre fin | `msg.sessionId` | 2 sorties : terminé / timeout | Boucle ci-dessus. |
| 3\. OC Lire réponse | `msg.sessionId` | `msg.payload` = texte | `GET .../message?type=assistant&order=desc&limit=1`, puis extraction du texte (à écrire une fois le `debug` vu). |
| 4\. Matrix Envoyer | `msg.roomId`, `msg.payload` | aucune | Découpage à 8 000 caractères, Delay en mode « rate limit » 1/s, Matrix out. |

Le nœud `catch` doit rester au niveau du flux, car il ne capte pas les erreurs depuis l'intérieur d'un subflow de la même façon.

## 4\. Variables

**Contexte (données partagées)**

| Nom | Portée | Magasin | Contenu |
| --- | --- | --- | --- |
| `oc_sessions` | global | **fichier** (persistant) | `{ roomId: sessionId }` |
| `busy` | flow | mémoire | `{ roomId: true }` |

Dans `settings.js`, déclarez deux magasins : `memory` par défaut et `file` (`localfilesystem`). Lecture : `global.get("oc_sessions","file")` et `flow.get("busy")`.

**Variables d'environnement** (propriétés du flux ou du subflow, lues par `env.get("NOM")` ou `${NOM}` dans les champs)

| Nom | Où  | Valeur |
| --- | --- | --- |
| `OC_BASE_URL` | flux | `http://127.0.0.1:4096` |
| `OC_AUTH` | flux | en-tête d'authentification, si vous en configurez un |
| `ALLOWED_MXID` | flux | votre identifiant Matrix |
| `BOT_MXID` | flux | l'identifiant du bot |
| `POLL_MS` | subflow 2 | `5000` |
| `MAX_ATTEMPTS` | subflow 2 | `360` (30 min) |
| `ABSENT_REQUIRED` | subflow 2 | `2` |
| `MAX_CHUNK` | subflow 4 | `8000` |

Le nœud `delay` ne lit pas toujours les variables d'environnement dans sa durée selon la version. Si c'est votre cas, mettez 5 s en dur dans le nœud.

**Propriétés du `msg`** (propres à chaque conversation)

`roomId`, `sessionId`, `userText`, `attempts`, `absent`.

Si vous voulez, je peux aussi publier ces schémas en page rendue pour les consulter plus facilement sur le téléphone.



---
## 1\. Le schéma complet

```
        ┌────────────── FLUX UNIQUE (une « bille » msg par message Matrix) ──────────────┐

Matrix in
  → Function « Filtrer »        (mon MXID, pas le bot ; sinon on arrête)
  → Function « Session »        (roomId → sessionId, création si absente)
  → Function « Garde busy »     (room déjà en cours ? → message d'info, FIN)
  → Matrix Typing (on) + message « 👀 Reçu »
  → Function « Prép. prompt »
  → HTTP POST /api/session/{id}/prompt        ← répond tout de suite (message admis)
  │
  ▼
  ┌─► Delay 5 s  (mode « délai fixe », pas « limitation »)
  │     → Matrix Typing (relance l'indicateur)
  │     → Function « Prép. active »
  │     → HTTP GET /api/session/active
  │     → Function « Décision » ──┬─ sortie 1 : encore en cours ──► (retour au Delay)
  └───────────────────────────────┤
                                  ├─ sortie 2 : terminé
                                  │     → Function « Prép. lecture »
                                  │     → HTTP GET /api/session/{id}/message?type=assistant&order=desc&limit=1
                                  │     → Function « Extraire texte »
                                  │     → Function « Découper » → Delay (limitation 1/s) → Matrix out
                                  │     → Function « Libérer busy »
                                  │
                                  └─ sortie 3 : trop long (30 min)
                                        → message « ⏱️ toujours en cours, /status ou /stop »

catch (tout le flux) → Function « Libérer busy » → Matrix out (message d'erreur)
```

## 2\. Comment lire la réponse

Quand la session disparaît de `/api/session/active`, l'agent a fini. Vous demandez alors à OpenCode son dernier message de type assistant, avec l'URL :

`http://127.0.0.1:4096/api/session/{sessionId}/message?type=assistant&order=desc&limit=1`

Je ne connais pas encore la structure exacte de la réponse, car je n'ai pas lu le schéma `SessionMessagesResponse`. Faites donc une première fois un nœud `debug` sur cette réponse et collez-la-moi, je vous écrirai alors le nœud « Extraire texte ». En attendant, voici le squelette à adapter :

```js
// Function « Extraire texte » : à ajuster selon le debug
const items = (msg.payload && msg.payload.data) || [];
// TODO : localiser le texte dans items[0] (parties de type "text" ?)
msg.payload = "(structure à adapter)";
return msg;
```

Si une même demande produit plusieurs messages assistant (agent qui enchaîne outils puis réponse), il faudra prendre ceux qui suivent le dernier message utilisateur. Le debug le montrera.

## 3\. Le code des nœuds clés

**« Prép. prompt »** (avant le POST) :

```js
msg.attempts = 0;
msg.absent = 0;
msg.method = "POST";
msg.url = `http://127.0.0.1:4096/api/session/${msg.sessionId}/prompt`;
msg.headers = { "Content-Type": "application/json" };  // + auth si configurée
msg.payload = { text: msg.userText };
return msg;
```

**« Prép. active »** :

```js
msg.method = "GET";
msg.url = "http://127.0.0.1:4096/api/session/active";
msg.headers = {};          // + auth si configurée
delete msg.payload;
return msg;
```

**« Décision »** (3 sorties) :

```js
const active = (msg.payload && msg.payload.data) || {};
const running = Object.prototype.hasOwnProperty.call(active, msg.sessionId);

msg.attempts += 1;
msg.absent = running ? 0 : msg.absent + 1;

if (msg.attempts > 360) {                       // 360 × 5 s = 30 min
  msg.payload = "⏱️ Toujours en cours. /status pour l'état, /stop pour interrompre.";
  return [null, null, msg];
}
if (msg.absent >= 2) return [null, msg, null];  // absente 2 fois de suite : terminé
return [msg, null, null];                       // encore en cours : on reboucle
```

L'exigence de deux absences consécutives évite un faux « terminé » juste après l'envoi : le serveur admet le message puis réveille l'exécution, et un court décalage est possible. C'est une précaution de ma part, pas un comportement documenté. Le délai minimal avant réponse est donc d'environ 10 s.

**« Garde busy »** (au début) et **« Libérer busy »** (à la fin et dans le `catch`) :

```js
// Garde
const busy = flow.get("busy") || {};
if (busy[msg.roomId]) { 
  msg.payload = "⏳ Déjà en cours. /stop pour interrompre."; 
  return [null, msg];
}
busy[msg.roomId] = true; 
flow.set("busy", busy);
return [msg, null];

// Libérer
const b = flow.get("busy") || {};
delete b[msg.roomId]; flow.set("busy", b);
return msg;
```

Si vous avez configuré un context store persistant par défaut, utilisez `flow.get("busy", "memory")` pour que ce drapeau ne survive pas à un redémarrage. La table roomId → sessionId, elle, doit rester persistante.

## 4\. Pourquoi ça ne bloque pas les autres sessions

*   **Chaque message Matrix est un objet `msg` indépendant.** Node-RED en fait circuler plusieurs en même temps dans le même flux. Pendant que le message A attend dans le `Delay`, le message B traverse tout le flux sans être gêné.
*   **Le `Delay` doit être en mode « Delay each message by a fixed amount of time ».** En mode « Rate limit », il met les messages en file et traite un message à la fois, ce qui bloquerait tout. Gardez ce mode seulement pour la limitation avant Matrix out.
*   **Côté OpenCode**, l'exécution est sérialisée par session, mais des sessions différentes tournent en parallèle. Un appel `/api/session/active` ne bloque personne : il renvoie juste la liste des sessions en cours.

Règle importante : tout ce qui est propre à une conversation en cours (`sessionId`, `roomId`, compteurs, `userText`) doit vivre **dans le `msg`**, pas dans le contexte `flow` ou `global`. Sinon deux conversations simultanées s'écraseraient mutuellement. Le contexte ne sert que pour la table roomId → sessionId et le drapeau `busy`.

## 5\. Pièges de configuration

*   **Le nœud `http request` écrase `msg.headers`** avec les en-têtes de la réponse. C'est pourquoi chaque fonction « Prép. » refait les siens juste avant l'appel.
*   Réglez le nœud HTTP sur **« a parsed JSON object »** pour lire `msg.payload.data` directement.
*   Dans le nœud `http request`, laissez le timeout par défaut : chaque appel est maintenant court.
*   Je ne connais pas l'interface exacte de votre nœud `Matrix Typing` (durée, activation/désactivation). Si l'indicateur disparaît entre deux itérations, c'est que sa durée est inférieure à 5 s ; augmentez-la. N'oubliez pas de l'éteindre avant l'envoi final si le nœud le permet.

Dites-moi ce que renvoie le `GET .../message` dans le `debug` et je complète le nœud « Extraire texte ».