Contexte : avec PROJ_SNAPSHOT, la date et le dossier de travail passent dans le bloc figé

Le système portait deux valeurs qui bougent : la date du jour — à minuit,
toute discussion en cours recalculait son prompt entier — et le dossier de
travail, propre à chaque discussion, si bien que deux discussions ne
partageaient même pas leur système. Avec PROJ_SNAPSHOT, ces valeurs
rejoignent le bloc projet figé, en tête du premier message ; le système ne
garde que les consignes (« ton dossier de travail est dans ton contexte »).

- Bloc « Environment (from Loki) » : date (avec l'année, et l'année périmée
  que la consigne web nommait), dossier de la discussion, ou poste distant
  ciblé (nom, système, dossier, hors ligne).
- Contenu toujours vivant : relu à chaque tour ; un changement de jour part en
  une ligne de <context_update>, tout autre écart (poste hors ligne, autre
  cible) renvoie le message entier. L'avertissement « hors ligne » ne fige
  donc plus le système.
- Hôte, compte et dossier des scripts, les mêmes pour toutes les discussions,
  restent dans le système.
- Seul l'assemblage d'un tour de discussion avec la clé pose caps.envInCtx :
  tâches planifiées, sous-agents, vérification et terminal gardent leur
  système, date et dossier compris.
- Sans la clé : système identique à l'octet (gabarits relevés sur le code
  d'avant, test). Format d'instantané passé à 2 : les instantanés persistés
  sont repris neufs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 14:08:07 +02:00
1 parent b7d7bfbe59
commit c888afb493
6 files changed
+378 -21

No files matched your search

+10 -3
View File
@@ -379,12 +379,19 @@ Ajoutées par ce fork :
tracker, et le texte **complet** d'une description ou d'un `AGENTS.md` modifié tracker, et le texte **complet** d'une description ou d'un `AGENTS.md` modifié
— rien n'est perdu, seul ce petit bloc est à calculer. Une ligne du prompt — rien n'est perdu, seul ce petit bloc est à calculer. Une ligne du prompt
système (présente seulement avec la clé) dit au modèle que ces blocs viennent système (présente seulement avec la clé) dit au modèle que ces blocs viennent
de Loki et que le plus récent l'emporte. Le bloc est repris tout neuf, et les de Loki et que le plus récent l'emporte. La date du jour et le dossier de
travail de la discussion (ou, pour un poste distant ciblé, son nom, son
dossier et s'il est hors ligne) quittent aussi le prompt système pour ce
bloc, les consignes restant dans le système : système et outils deviennent
les mêmes d'une discussion à l'autre, et passer minuit n'ajoute qu'une ligne
de mise à jour au lieu de tout recalculer. Le bloc est repris tout neuf, et les
anciens `<context_update>` retirés, quand le début du prompt change de toute anciens `<context_update>` retirés, quand le début du prompt change de toute
façon : compaction, système ou outils modifiés (date, réglages), redémarrage de façon : compaction, système ou outils modifiés (réglages, mode), redémarrage de
Loki, changement de modèle, de preset, de projet, de mode mémoire ou de mode Loki, changement de modèle, de preset, de projet, de mode mémoire ou de mode
Code — et dès que les mises à jour accumulées deviennent trop longues. Les Code — et dès que les mises à jour accumulées deviennent trop longues. Les
tâches planifiées gardent le bloc à jour à chaque tour ; sans agent, rien du tâches planifiées gardent le bloc à jour à chaque tour et, comme les
sous-agents, la vérification et le terminal, leur système d'avant, date et
dossier compris ; sans agent, rien du
projet n'est envoyé ; un preset externe (API) n'est jamais concerné. Le titre, l'export JSON, le résumé de compaction et la projet n'est envoyé ; un preset externe (API) n'est jamais concerné. Le titre, l'export JSON, le résumé de compaction et la
passe de vérification ne voient pas ces blocs. passe de vérification ne voient pas ces blocs.
- **Préchauffage du prochain tour** (clé `PREWARM`, **off** par défaut, - **Préchauffage du prochain tour** (clé `PREWARM`, **off** par défaut,
+116
View File
@@ -0,0 +1,116 @@
package loki
// chat_envctx.go — PROJ_SNAPSHOT : la date et le dossier de travail sortent du
// prompt système pour entrer dans le bloc projet figé de la discussion.
//
// Pourquoi. Le système (préambule + briefing machine) et les outils forment le
// début de chaque requête ; tant qu'ils ne bougent pas, le moteur reprend son
// cache. Deux valeurs y bougeaient pourtant : la date — à minuit, tout le prompt
// d'une discussion en cours est recalculé — et le dossier de travail, propre à
// chaque discussion — deux discussions ne partageaient donc rien, pas même le
// système. Avec la clé, le système ne garde que des consignes (« ton dossier de
// travail est dans ton contexte ») et les valeurs partent dans le bloc projet,
// en tête du premier message utilisateur, avec le reste du contexte figé.
//
// Rien n'est mémorisé au-delà du bloc lui-même : chaque tour relit la date, le
// dossier et la cible, et tout écart part en <context_update> dans le message
// suivant — une ligne pour un changement de jour, le bloc entier sinon (poste
// distant passé hors ligne, autre cible, autre dossier). Les mêmes faits, à une
// autre place ; aucun n'est retenu par Loki plus longtemps qu'avant.
//
// Seul un tour de discussion avec la clé y passe (assembleTurn pose
// caps.envInCtx). Une tâche planifiée, un sous-agent, la vérification du mode
// Code, le terminal gardent leur système d'avant, date et dossier compris.
import (
"strings"
"time"
)
// envContextPrefix ouvre le message (isProjectSystem le range avec le contexte
// du projet). Assez particulier pour qu'aucun prompt de preset ne commence ainsi.
const envContextPrefix = "Environment (from Loki)"
// nodeOfflineLine : l'avertissement d'un poste distant hors ligne — dans le
// système sans la clé, dans le bloc projet avec.
const nodeOfflineLine = "⚠ It is currently OFFLINE: those tools will fail until it reconnects. Tell the user instead of trying repeatedly."
// envNow : l'horloge du bloc (remplaçable en test).
var envNow = time.Now
// envDateLine : la date du jour, avec l'année et l'année périmée que la
// consigne web du système désignait jusque-là par leur valeur.
func envDateLine(day string) string {
year := day
if len(day) >= 4 {
year = day[:4]
}
return "Date: " + day + " (the current year is " + year + ", not " + prevYear(year) + ")."
}
// envMachineFacts : les valeurs du briefing machine qui varient — dossier de la
// discussion, ou poste distant (nom, système, dossier, état). "" sans agent.
// Les consignes qui les accompagnent restent dans machineSystemPrompt.
func envMachineFacts(caps Caps) string {
if !caps.Agent {
return ""
}
if tgt, ok := nodeTargetMetaGet(); ok {
s := "Machine: REMOTE node " + tgt.name
if tgt.os != "" {
s += " (" + tgt.os + ")"
}
if tgt.root != "" {
s += ", working folder " + tgt.root
}
s += "."
if !tgt.connected {
s += " " + nodeOfflineLine
}
return s
}
if cwd := agentCwd(); cwd != "" {
return "Working folder (cwd): " + cwd + "."
}
return ""
}
// envCapture : la date (si le préambule en portait une) et les valeurs machine
// de ce tour.
func envCapture(caps Caps) (day, facts string) {
if caps.Agent || caps.Mem != MemOff {
day = envNow().Format("2006-01-02")
}
return day, envMachineFacts(caps)
}
// envContextMessage : le message du bloc, ok=false s'il n'a rien à dire.
func envContextMessage(day, facts string) (Message, bool) {
var lines []string
if day != "" {
lines = append(lines, envDateLine(day))
}
if facts != "" {
lines = append(lines, facts)
}
if len(lines) == 0 {
return Message{}, false
}
return Message{Role: "system", Content: envContextPrefix + ":\n" + strings.Join(lines, "\n")}, true
}
// envDelta : la mise à jour entre l'environnement annoncé et le vivant. Un
// changement de jour seul tient en une ligne ; tout autre écart renvoie le
// message entier.
func envDelta(oldDay, oldFacts, day, facts string) string {
switch {
case oldFacts == facts && oldDay == day:
return ""
case oldFacts == facts && day != "" && oldDay != "":
return "New day — " + envDateLine(day) + "\n"
}
if m, ok := envContextMessage(day, facts); ok {
return "Current version, replaces any earlier one — " + msgText(m) + "\n"
}
return "Environment: removed.\n"
}
+185
View File
@@ -0,0 +1,185 @@
package loki
import (
"encoding/json"
"os"
"os/user"
"runtime"
"strings"
"testing"
"time"
)
// sysPlaceholders remplace dans un prompt système les valeurs propres à la
// machine et au jour (date, année, hôte, utilisateur, dossiers) par des jetons
// fixes : ce qui reste doit être identique, octet pour octet, d'une machine à
// l'autre.
func sysPlaceholders(s string) string {
now := time.Now()
year := now.Format("2006")
host, _ := os.Hostname()
if host == "" {
host = "unknown"
}
who := ""
if u, err := user.Current(); err == nil {
who = u.Username
}
pairs := []string{
now.Format("2006-01-02"), "<DATE>",
scriptsDir(), "<SCRIPTS>",
agentCwd(), "<CWD>",
"host=" + host, "host=<HOST>",
runtime.GOOS + "/" + runtime.GOARCH, "<OS>/<ARCH>",
"The shell is " + agentTargetShellName(), "The shell is <SHELL>",
}
if who != "" {
pairs = append(pairs, "user="+who, "user=<USER>")
}
pairs = append(pairs, year, "<YEAR>", prevYear(year), "<PREVYEAR>")
for i := 0; i < len(pairs); i += 2 {
s = strings.ReplaceAll(s, pairs[i], pairs[i+1])
}
return s
}
func sysOf(t *testing.T, msgs []Message) string {
t.Helper()
if len(msgs) == 0 || msgs[0].Role != "system" {
t.Fatalf("pas de système en tête : %+v", msgs)
}
s, _ := msgs[0].Content.(string)
return s
}
// Gabarits du système SANS PROJ_SNAPSHOT, relevés sur le code d'avant le
// déplacement de la date et du dossier de travail : ils ne doivent pas bouger
// d'un octet (valeurs du jour et de la machine remplacées par des jetons).
var sysGolden = map[string]string{
"agent": "You are Loki, an expert assistant operating directly on this machine with real tools. You evolve with every conversation: you actively maintain a persistent memory so nothing useful is lost between sessions.\n\nThe shell is <SHELL>: use its syntax.\n\nManaging your memory is part of the job, not optional:\n- Save anything worth keeping (a preference, fact, decision, how-to) with mem_add, or mem_edit to update a page — on your own, without being asked.\n- The memory index (page list) is in your context: when a page looks relevant, mem_read it directly. mem_search is only for finding something by content.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: <DATE>\n\nMachine: host=<HOST>, <OS>/<ARCH>, user=<USER>, cwd=<CWD>. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder <SCRIPTS> instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.",
"internet": "You are Loki, an expert assistant operating directly on this machine with real tools. You evolve with every conversation: you actively maintain a persistent memory so nothing useful is lost between sessions.\n\nThe shell is <SHELL>: use its syntax.\n\nManaging your memory is part of the job, not optional:\n- Save anything worth keeping (a preference, fact, decision, how-to) with mem_add, or mem_edit to update a page — on your own, without being asked.\n- The memory index (page list) is in your context: when a page looks relevant, mem_read it directly. mem_search is only for finding something by content.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nWeb: web_open first, then web_read/web_grep on it.\nYour training data is stale. For ANY question about recent/latest/current things (releases, versions, news, prices, scores, 'since when') call web_search BEFORE writing any date or version, and match what you actually read.\nToday is in <YEAR>. If a query needs a year use ONLY <YEAR>, never a remembered past year like <PREVYEAR> — it biases results toward stale pages; better still, omit the year. Don't hedge ('probably') about a fact a tool can verify — search instead.\n\nDate: <DATE>\n\nMachine: host=<HOST>, <OS>/<ARCH>, user=<USER>, cwd=<CWD>. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder <SCRIPTS> instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.",
"ondemand": "You are Loki, an expert assistant operating directly on this machine with real tools.\n\nThe shell is <SHELL>: use its syntax.\n\nMemory is ON-DEMAND: you have the mem_* tools but do NOT read or write memory on your own. Call mem_search/mem_read only when the user explicitly asks you to recall or look something up, and mem_add/mem_edit only when the user explicitly asks you to remember something. Otherwise leave memory untouched and answer directly.\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\nTo give the user a file, link it in Markdown with its path relative to your working directory — [le rapport](rapport.pdf) — which downloads it. A raw server path is useless: they read you in a browser.\nYou can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n\nDate: <DATE>\n\nMachine: host=<HOST>, <OS>/<ARCH>, user=<USER>, cwd=<CWD>. This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder <SCRIPTS> instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.",
"memseule": "You are Loki, an expert assistant operating directly on this machine with real tools. You evolve with every conversation: you actively maintain a persistent memory so nothing useful is lost between sessions.\n\n\nManaging your memory is part of the job, not optional:\n- Save anything worth keeping (a preference, fact, decision, how-to) with mem_add, or mem_edit to update a page — on your own, without being asked.\n- The memory index (page list) is in your context: when a page looks relevant, mem_read it directly. mem_search is only for finding something by content.\n\nDate: <DATE>",
}
func TestSystemeSansCleInchange(t *testing.T) {
projSnapSetup(t, false)
cases := map[string]Caps{
"agent": {Agent: true, Mem: MemAlways},
"internet": {Agent: true, Internet: true, Mem: MemAlways},
"ondemand": {Agent: true, Mem: MemOnDemand},
"memseule": {Mem: MemAlways},
}
for name, caps := range cases {
got, _ := prepareTurn([]Message{um("salut")}, caps)
s := sysPlaceholders(sysOf(t, got))
if want, ok := sysGolden[name]; !ok || s != want {
t.Errorf("%s : système changé sans la clé\nreçu %q\nattendu %q", name, s, want)
}
}
}
// toolsOf : le champ tools d'une requête, tel qu'envoyé.
func toolsOf(t *testing.T, body string) string {
t.Helper()
var p struct {
Tools json.RawMessage `json:"tools"`
}
if err := json.Unmarshal([]byte(body), &p); err != nil {
t.Fatal(err)
}
return string(p.Tools)
}
// Avec la clé : deux discussions dans deux dossiers ont le MÊME système et les
// mêmes outils ; la date et le dossier sont dans le bloc projet de chacune.
func TestEnvCtxSystemeCommunAuxDiscussions(t *testing.T) {
m := projSnapSetup(t, true)
t.Cleanup(func() { setTaskWorkspace("") })
caps := Caps{Agent: true, Internet: true, Mem: MemAlways}
var bodies []string
dirs := []string{t.TempDir(), t.TempDir()}
for _, d := range dirs {
setTaskWorkspace(d)
bodies = append(bodies, playTurn(t, m, newTestConv(), caps, "salut"))
}
s0, s1 := systemContent(t, bodies[0]), systemContent(t, bodies[1])
if s0 != s1 || toolsOf(t, bodies[0]) != toolsOf(t, bodies[1]) {
t.Fatalf("système ou outils différents d'une discussion à l'autre :\n%q\n%q", s0, s1)
}
today := time.Now().Format("2006-01-02")
year := today[:4]
for _, v := range []string{today, year, dirs[0], dirs[1], "cwd="} {
if strings.Contains(s0, v) {
t.Fatalf("%q encore dans le système : %q", v, s0)
}
}
if !strings.Contains(s0, "Your working folder (cwd) is given in your context") || !strings.Contains(s0, scriptsDir()) {
t.Fatalf("consignes du dossier de travail perdues : %q", s0)
}
for i, b := range bodies {
u := userContents(t, b)[0]
if !strings.Contains(u, envContextPrefix+":\n"+envDateLine(today)) || !strings.Contains(u, "Working folder (cwd): "+dirs[i]+".") {
t.Fatalf("discussion %d : date ou dossier absents du bloc : %q", i, u)
}
}
}
// Changement de jour : une ligne de mise à jour, une seule fois ; système et
// bloc figé inchangés, donc tout le début du prompt reste en cache.
func TestEnvCtxChangementDeJour(t *testing.T) {
m := projSnapSetup(t, true)
day := time.Date(2026, 12, 31, 23, 50, 0, 0, time.Local)
envNow = func() time.Time { return day }
t.Cleanup(func() { envNow = time.Now })
c := newTestConv()
caps := Caps{Agent: true, Mem: MemAlways}
b1 := playTurn(t, m, c, caps, "salut")
day = day.Add(20 * time.Minute)
b2 := playTurn(t, m, c, caps, "et maintenant ?")
b3 := playTurn(t, m, c, caps, "merci")
if systemContent(t, b1) != systemContent(t, b2) || systemContent(t, b2) != systemContent(t, b3) {
t.Fatal("le système a changé avec le jour")
}
u1, u2, u3 := userContents(t, b1), userContents(t, b2), userContents(t, b3)
if u1[0] != u2[0] || u2[0] != u3[0] || !strings.Contains(u1[0], "Date: 2026-12-31 (the current year is 2026, not 2025).") {
t.Fatalf("bloc figé modifié ou sans date : %q / %q", u1[0], u2[0])
}
want := ctxUpdOpen
last := u2[len(u2)-1]
if !strings.HasPrefix(last, want) || !strings.Contains(last, "New day — Date: 2027-01-01 (the current year is 2027, not 2026).") {
t.Fatalf("mise à jour du jour attendue : %q", last)
}
if u3[1] != last || strings.Contains(u3[len(u3)-1], ctxUpdOpen) {
t.Fatalf("la mise à jour doit partir une fois, puis rester telle quelle : %q", u3)
}
}
// Tâches, sous-agents, vérification, terminal : la clé ne change rien à leur
// système, qui garde la date et le dossier.
func TestEnvCtxHorsDiscussionInchange(t *testing.T) {
projSnapSetup(t, true)
caps := Caps{Agent: true, Mem: MemAlways}
got, _ := prepareTurn([]Message{um("tâche")}, caps)
s := sysOf(t, got)
if !strings.Contains(s, "\nDate: "+time.Now().Format("2006-01-02")) || !strings.Contains(s, ", cwd="+agentCwd()+".") {
t.Fatalf("système d'une tâche modifié par la clé : %q", s)
}
if mp := machineSystemPrompt(caps); !strings.Contains(mp, "cwd="+agentCwd()) {
t.Fatalf("briefing d'un sous-agent sans dossier : %q", mp)
}
}
// Poste distant passé hors ligne, autre dossier : le bloc entier repart, rien
// n'est retenu d'avant.
func TestEnvDelta(t *testing.T) {
if d := envDelta("2026-10-04", "x", "2026-10-04", "x"); d != "" {
t.Fatalf("rien de changé : %q", d)
}
if d := envDelta("2026-10-04", "Working folder (cwd): /a.", "2026-10-04", "Machine: REMOTE node pc. "+nodeOfflineLine); !strings.Contains(d, "Current version") || !strings.Contains(d, nodeOfflineLine) || !strings.Contains(d, "Date: 2026-10-04") {
t.Fatalf("bloc entier attendu : %q", d)
}
if d := envDelta("2026-10-04", "x", "2026-10-05", "x"); d != "New day — "+envDateLine("2026-10-05")+"\n" {
t.Fatalf("une ligne attendue : %q", d)
}
}
+19 -5
View File
@@ -22,12 +22,14 @@ package loki
// - rien n'est perdu : lignes ajoutées, modifiées, retirées pour les données // - rien n'est perdu : lignes ajoutées, modifiées, retirées pour les données
// faites de lignes (pages, trackers), texte COMPLET pour la prose // faites de lignes (pages, trackers), texte COMPLET pour la prose
// (description, AGENTS.md). Une ligne fixe du système dit que ces blocs // (description, AGENTS.md). Une ligne fixe du système dit que ces blocs
// viennent de Loki et que le plus récent l'emporte. // viennent de Loki et que le plus récent l'emporte ;
// - la date du jour et le dossier de travail quittent le système pour ce bloc
// (chat_envctx.go) : un changement de jour devient une ligne de mise à jour.
// //
// Le bloc est rafraîchi — copie neuve, blocs <context_update> retirés de tout // Le bloc est rafraîchi — copie neuve, blocs <context_update> retirés de tout
// l'historique — seulement quand le début du prompt change de toute façon : // l'historique — seulement quand le début du prompt change de toute façon :
// compaction (début, fin de tour, manuelle, en cours de tour, réduction forcée), // compaction (début, fin de tour, manuelle, en cours de tour, réduction forcée),
// système ou outils modifiés (date du jour, réglages, mode), cache froid // système ou outils modifiés (réglages, mode, cible), cache froid
// (redémarrage de Loki, changement de modèle, de preset ou de réglage moteur), // (redémarrage de Loki, changement de modèle, de preset ou de réglage moteur),
// projet, nom, mode mémoire, mode Code ou dépôt changés, textes des en-têtes // projet, nom, mode mémoire, mode Code ou dépôt changés, textes des en-têtes
// changés. Et quand les mises à jour accumulées dépassent un seuil : au-delà, le // changés. Et quand les mises à jour accumulées dépassent un seuil : au-delà, le
@@ -66,7 +68,7 @@ func projSnapEnabled(cfg map[string]string) bool {
// Textes propres au bloc figé. Leur empreinte entre dans la clé de validité : // Textes propres au bloc figé. Leur empreinte entre dans la clé de validité :
// une mise à jour de Loki qui les reformule rafraîchit les instantanés persistés. // une mise à jour de Loki qui les reformule rafraîchit les instantanés persistés.
const ( const (
projSnapFormat = "1" projSnapFormat = "2"
memIndexFrozenTail = " Later <context_update> blocks from Loki update it." memIndexFrozenTail = " Later <context_update> blocks from Loki update it."
trackerFrozenHead = " — your trackers, each with its latest point at that time; later <context_update> blocks from Loki replace these lines. " + trackerFrozenHead = " — your trackers, each with its latest point at that time; later <context_update> blocks from Loki replace these lines. " +
"To say which trackers exist or give a latest value, use this list with those updates — do NOT call tracker with no arguments to list them. " + "To say which trackers exist or give a latest value, use this list with those updates — do NOT call tracker with no arguments to list them. " +
@@ -89,6 +91,8 @@ func projSnapNow() string { return time.Now().Format("2006-01-02 15:04") }
// simple hash : c'est contre lui qu'on calcule la mise à jour suivante, y // simple hash : c'est contre lui qu'on calcule la mise à jour suivante, y
// compris après un redémarrage. // compris après un redémarrage.
type projState struct { type projState struct {
Day string `json:"day,omitempty"` // date annoncée (chat_envctx.go)
Env string `json:"env,omitempty"` // valeurs machine annoncées
Code string `json:"code,omitempty"` // consignes du dépôt, message complet Code string `json:"code,omitempty"` // consignes du dépôt, message complet
Desc string `json:"desc,omitempty"` // description du projet Desc string `json:"desc,omitempty"` // description du projet
MemOn bool `json:"mem_on,omitempty"` // index mémoire présent MemOn bool `json:"mem_on,omitempty"` // index mémoire présent
@@ -122,9 +126,15 @@ type projLive struct {
// projSnapCapture lit le contexte projet UNE fois et en tire le bloc (même // projSnapCapture lit le contexte projet UNE fois et en tire le bloc (même
// ordre que l'assemblage d'un tour : consignes du dépôt, description, index // ordre que l'assemblage d'un tour : consignes du dépôt, description, index
// mémoire, trackers) et son état. // mémoire, trackers — précédés ici de la date et du dossier de travail, que le
// système ne porte plus avec la clé) et son état.
func projSnapCapture(caps Caps, asOf string) projLive { func projSnapCapture(caps Caps, asOf string) projLive {
var lv projLive var lv projLive
// Date et dossier de travail, sortis du système (chat_envctx.go).
lv.state.Day, lv.state.Env = envCapture(caps)
if m, ok := envContextMessage(lv.state.Day, lv.state.Env); ok {
lv.msgs = append(lv.msgs, m)
}
if m, ok := codeInstructionsMessage(caps); ok { if m, ok := codeInstructionsMessage(caps); ok {
lv.msgs = append(lv.msgs, m) lv.msgs = append(lv.msgs, m)
lv.state.Code, _ = m.Content.(string) lv.state.Code, _ = m.Content.(string)
@@ -224,7 +234,7 @@ func projSnapKey(caps Caps, cfg map[string]string) string {
w(d) w(d)
w(agentCwd()) w(agentCwd())
} }
for _, s := range []string{memIndexFrozenTail, trackerFrozenHead, ctxUpdOpen, ctxUpdClose, projSnapSystemLine} { for _, s := range []string{envContextPrefix, nodeOfflineLine, memIndexFrozenTail, trackerFrozenHead, ctxUpdOpen, ctxUpdClose, projSnapSystemLine} {
w(s) w(s)
} }
return fmt.Sprintf("%016x", h.Sum64()) return fmt.Sprintf("%016x", h.Sum64())
@@ -268,6 +278,7 @@ func projDelta(old projState, lv projLive, asOf string) string {
full := func(m Message) { full := func(m Message) {
b.WriteString("Current version, replaces any earlier one — " + msgText(m) + "\n") b.WriteString("Current version, replaces any earlier one — " + msgText(m) + "\n")
} }
b.WriteString(envDelta(old.Day, old.Env, cur.Day, cur.Env))
if old.Code != cur.Code { if old.Code != cur.Code {
if cur.Code == "" { if cur.Code == "" {
b.WriteString("Repository instructions: removed.\n") b.WriteString("Repository instructions: removed.\n")
@@ -569,6 +580,9 @@ func (c *Conversation) assembleTurn(caps Caps, epoch int, msgs []Message, inject
return sent, tools, nil return sent, tools, nil
} }
// Date et dossier de travail : dans le bloc, plus dans le système
// (chat_envctx.go). Cette copie de caps ne sert qu'à ce tour de discussion.
caps.envInCtx = true
// Lectures (disque, base, git) hors du verrou de la conversation. // Lectures (disque, base, git) hors du verrou de la conversation.
asOf := projSnapNow() asOf := projSnapNow()
live := projSnapCapture(caps, asOf) live := projSnapCapture(caps, asOf)
+38 -9
View File
@@ -117,7 +117,13 @@ func baseSystemPrompt(caps Caps, tools []Tool) string {
year := time.Now().Format("2006") year := time.Now().Format("2006")
b.WriteString("\nWeb: web_open first, then web_read/web_grep on it.\n") b.WriteString("\nWeb: web_open first, then web_read/web_grep on it.\n")
b.WriteString("Your training data is stale. For ANY question about recent/latest/current things (releases, versions, news, prices, scores, 'since when') call web_search BEFORE writing any date or version, and match what you actually read.\n") b.WriteString("Your training data is stale. For ANY question about recent/latest/current things (releases, versions, news, prices, scores, 'since when') call web_search BEFORE writing any date or version, and match what you actually read.\n")
b.WriteString("Today is in " + year + ". If a query needs a year use ONLY " + year + ", never a remembered past year like " + prevYear(year) + " — it biases results toward stale pages; better still, omit the year. Don't hedge ('probably') about a fact a tool can verify — search instead.\n") if caps.envInCtx {
// Même consigne, sans la valeur : l'année est dans la ligne de date du
// contexte (envContextMessage), qui nomme aussi l'année périmée.
b.WriteString("Today's date is in your context. If a query needs a year use ONLY the current year, never a remembered past year — it biases results toward stale pages; better still, omit the year. Don't hedge ('probably') about a fact a tool can verify — search instead.\n")
} else {
b.WriteString("Today is in " + year + ". If a query needs a year use ONLY " + year + ", never a remembered past year like " + prevYear(year) + " — it biases results toward stale pages; better still, omit the year. Don't hedge ('probably') about a fact a tool can verify — search instead.\n")
}
} }
if caps.Agent && caps.ComputerUse { if caps.Agent && caps.ComputerUse {
b.WriteString(cuPromptLine()) b.WriteString(cuPromptLine())
@@ -134,6 +140,11 @@ func baseSystemPrompt(caps Caps, tools []Tool) string {
if caps.Agent && projSnapEnabled(ReadConfig()) { if caps.Agent && projSnapEnabled(ReadConfig()) {
b.WriteString(projSnapSystemLine) b.WriteString(projSnapSystemLine)
} }
// PROJ_SNAPSHOT : la date part dans le bloc projet figé (chat_envctx.go). Ici,
// elle changeait le système à minuit — tout le prompt recalculé.
if caps.envInCtx {
return strings.TrimRight(b.String(), "\n")
}
b.WriteString("\nDate: " + time.Now().Format("2006-01-02")) b.WriteString("\nDate: " + time.Now().Format("2006-01-02"))
return b.String() return b.String()
} }
@@ -161,23 +172,33 @@ func machineSystemPrompt(caps Caps) string {
// l'hôte du serveur et se trompe de machine. // l'hôte du serveur et se trompe de machine.
if tgt, ok := nodeTargetMetaGet(); ok { if tgt, ok := nodeTargetMetaGet(); ok {
var b strings.Builder var b strings.Builder
b.WriteString("Machine: you are operating on a REMOTE node named " + tgt.name) // PROJ_SNAPSHOT (caps.envInCtx) : nom, système, dossier et état du poste
if tgt.os != "" { // sont dans le bloc projet (envMachineFacts) ; ici, les seules consignes.
b.WriteString(" (" + tgt.os + ")") if caps.envInCtx {
b.WriteString("Machine: you are operating on a REMOTE node (name, OS, working folder and status are in your context)")
} else {
b.WriteString("Machine: you are operating on a REMOTE node named " + tgt.name)
if tgt.os != "" {
b.WriteString(" (" + tgt.os + ")")
}
} }
b.WriteString(". Your bash, write and edit tools run on THAT machine — a different computer than this server.") b.WriteString(". Your bash, write and edit tools run on THAT machine — a different computer than this server.")
if strings.HasPrefix(strings.ToLower(tgt.os), "windows") { if strings.HasPrefix(strings.ToLower(tgt.os), "windows") {
b.WriteString(" Its shell is cmd.exe: use cmd syntax (never bash idioms like ls, 2>nul, single quotes, or 'cmd //c'). To create a file, use the write tool, never echo/type into it.") b.WriteString(" Its shell is cmd.exe: use cmd syntax (never bash idioms like ls, 2>nul, single quotes, or 'cmd //c'). To create a file, use the write tool, never echo/type into it.")
} }
if tgt.root != "" { if tgt.root != "" {
b.WriteString(" File paths in write/edit resolve inside its working folder " + tgt.root + "; read/write are confined there.") if caps.envInCtx {
b.WriteString(" File paths in write/edit resolve inside its working folder; read/write are confined there.")
} else {
b.WriteString(" File paths in write/edit resolve inside its working folder " + tgt.root + "; read/write are confined there.")
}
} }
// Le poste peut tourner en compte de service (Windows: LocalSystem) : les // Le poste peut tourner en compte de service (Windows: LocalSystem) : les
// variables d'environnement personnelles (%USERPROFILE%, $HOME) ne désignent // variables d'environnement personnelles (%USERPROFILE%, $HOME) ne désignent
// PAS forcément l'utilisateur interactif. Utiliser des chemins absolus. // PAS forcément l'utilisateur interactif. Utiliser des chemins absolus.
b.WriteString(" It may run as a background service account, so %USERPROFILE%/$HOME may not point to the interactive user — prefer absolute paths (e.g. C:\\Users\\<name>\\...).") b.WriteString(" It may run as a background service account, so %USERPROFILE%/$HOME may not point to the interactive user — prefer absolute paths (e.g. C:\\Users\\<name>\\...).")
if !tgt.connected { if !tgt.connected && !caps.envInCtx {
b.WriteString(" ⚠ It is currently OFFLINE: those tools will fail until it reconnects. Tell the user instead of trying repeatedly.") b.WriteString(" " + nodeOfflineLine)
} }
return b.String() return b.String()
} }
@@ -196,7 +217,10 @@ func machineSystemPrompt(caps Caps) string {
if who != "" { if who != "" {
b.WriteString(", user=" + who) b.WriteString(", user=" + who)
} }
if cwd != "" { // PROJ_SNAPSHOT (caps.envInCtx) : le chemin, propre à chaque discussion, est
// dans le bloc projet (envMachineFacts) ; hôte et compte, les mêmes pour
// toutes, restent ici avec les consignes.
if cwd != "" && !caps.envInCtx {
b.WriteString(", cwd=" + cwd) b.WriteString(", cwd=" + cwd)
} }
b.WriteString(".") b.WriteString(".")
@@ -205,7 +229,12 @@ func machineSystemPrompt(caps Caps) string {
// EST (l'endroit par défaut de tout ce que le modèle produit) marche mieux // EST (l'endroit par défaut de tout ce que le modèle produit) marche mieux
// que d'interdire d'en sortir — et nommer les dossiers système coupe court // que d'interdire d'en sortir — et nommer les dossiers système coupe court
// aux « installations » en /usr/local/bin qui échouent faute de root. // aux « installations » en /usr/local/bin qui échouent faute de root.
b.WriteString(" This is your working folder: relative paths in write/edit/bash resolve here, and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder " + scriptsDir() + " instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.") if caps.envInCtx {
b.WriteString(" Your working folder (cwd) is given in your context: relative paths in write/edit/bash resolve there")
} else {
b.WriteString(" This is your working folder: relative paths in write/edit/bash resolve here")
}
b.WriteString(", and it is the DEFAULT place for scratch work — notes, outputs, a clone, a test. But it is DISPOSABLE: deleting the discussion wipes it. Any script you want to KEEP (or schedule), write it into your scripts folder " + scriptsDir() + " instead — a separate folder a workspace wipe won't touch; you write and run scripts there normally. Do NOT install or write files into system directories such as /usr/local/bin, /usr, /bin or /etc: those need root and are not yours. Only use an absolute path outside this folder (except your scripts folder) when the user explicitly named that location.")
} }
return b.String() return b.String()
} }
+10 -4
View File
@@ -233,6 +233,11 @@ type Caps struct {
// ComputerUse = pilotage du navigateur du conteneur (outils browser_*). // ComputerUse = pilotage du navigateur du conteneur (outils browser_*).
// Requiert aussi Agent (mêmes actions réelles que bash). Voir computer_use.go. // Requiert aussi Agent (mêmes actions réelles que bash). Voir computer_use.go.
ComputerUse bool ComputerUse bool
// envInCtx : la date et le dossier de travail partent dans le bloc projet
// figé (PROJ_SNAPSHOT, chat_envctx.go), pas dans le système. Posé par le seul
// assemblage d'un tour de discussion avec la clé ; jamais pour une tâche, un
// sous-agent ou la vérification, qui gardent le système d'avant.
envInCtx bool
} }
// globalCaps reads the machine-wide config — the default when a request doesn't // globalCaps reads the machine-wide config — the default when a request doesn't
@@ -384,9 +389,9 @@ func normalizeSystemMessages(msgs []Message) []Message {
// isProjectSystem : message système propre au projet actif (description, index // isProjectSystem : message système propre au projet actif (description, index
// mémoire, trackers) ou à la conversation (rappel des pages mémoire lues, posé // mémoire, trackers) ou à la conversation (rappel des pages mémoire lues, posé
// au compactage), à sortir du bloc système commun (voir // au compactage ; date et dossier de travail avec PROJ_SNAPSHOT), à sortir
// normalizeSystemMessages) : ce bloc doit rester identique partout pour rester // du bloc système commun (voir normalizeSystemMessages) : ce bloc doit rester
// en cache. // identique partout pour rester en cache.
func isProjectSystem(m Message) bool { func isProjectSystem(m Message) bool {
s, ok := m.Content.(string) s, ok := m.Content.(string)
if !ok || m.Role != "system" { if !ok || m.Role != "system" {
@@ -396,7 +401,8 @@ func isProjectSystem(m Message) bool {
strings.HasPrefix(s, memIndexPrefix) || strings.HasPrefix(s, memIndexPrefix) ||
strings.HasPrefix(s, trackerIndexPrefix) || strings.HasPrefix(s, trackerIndexPrefix) ||
strings.HasPrefix(s, memReminderPrefix) || strings.HasPrefix(s, memReminderPrefix) ||
strings.HasPrefix(s, codeInstructionsPrefix) strings.HasPrefix(s, codeInstructionsPrefix) ||
strings.HasPrefix(s, envContextPrefix)
} }
// prependToFirstUser place ctx en tête du premier message user de msgs (modifié // prependToFirstUser place ctx en tête du premier message user de msgs (modifié