From 8e42ba17696bfb78bdb64d2f42ae37a33f3d66b1 Mon Sep 17 00:00:00 2001 From: Serge Alex Date: Sat, 7 Nov 2026 08:58:32 +0300 Subject: [PATCH] Memory: write triggers in protocol, stats read v2 entries, backfill 5 entries --- content/content.js | 12 ++++- content/panel.js | 2 +- memory/state.template.json | 128 ++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 133 insertions(+), 9 deletions(-) diff --git a/content/content.js b/content/content.js index 42e2e36..9af3441 100644 --- a/content/content.js +++ b/content/content.js @@ -372,5 +372,15 @@ if (parsed.calls.length === 0) { if (hasMarker) { try { var __dh = hashText(text __refreshStats(); }); }); -function __refreshStats() { try { var SB = globalThis.STORAGE_BRIDGE; if (!SB || !UI || !UI.setStats) return; SB.getState().then(function (st) { var mN = st && st.memories ? Object.keys(st.memories).length : 0; var sN = st && st.skills && st.skills.length ? st.skills.length : 0; return SB.authHeaders().then(function (h) { return fetch(SB.BU + '/health', { headers: h }); }).then(function (r) { return r.json(); }).then(function (j) { UI.setStats((j && j.workspace) || '?', mN, sN); }); }).catch(function () { UI.setStats('?', 0, 0); }); } catch (e) { console.warn('[dsh-memory] stats ' + String(e)); } } +function __refreshStats() { try { var SB = globalThis.STORAGE_BRIDGE; if (!SB || !UI || !UI.setStats) return; SB.authHeaders().then(function (h) { + return fetch(SB.BU + '/memory/state', { headers: h }); + }).then(function (r) { return r.json(); }).then(function (j) { + var st = (j && j.ok && j.data) ? j.data : {}; + var entries = st.entries || []; + var total = entries.length; + var alwaysN = entries.filter(function (e) { return e.importance === 'always'; }).length; + return SB.authHeaders().then(function (h) { return fetch(SB.BU + '/health', { headers: h }); }).then(function (r) { return r.json(); }).then(function (hj) { + UI.setStats((hj && hj.workspace) || '?', total, alwaysN); + }); + }).catch(function (e) { console.warn('[dsh-memory] stats ' + String(e)); UI.setStats('?', 0, 0); }); } catch (e) { console.warn('[dsh-memory] stats ' + String(e)); } } })(); diff --git a/content/panel.js b/content/panel.js index 7e64048..8b22091 100644 --- a/content/panel.js +++ b/content/panel.js @@ -147,7 +147,7 @@ } }, setConnected: function (b) { state.connected = !!b; API._applyDot(); }, setDot: function (m) { state.dotMode = m || null; API._applyDot(); }, _applyDot: function () { if (!el.dot) return; var cls = 'dshmem-dot'; if (!state.connected) { cls += ' err'; } else { var m = state.dotMode || 'off'; if (m === 'busy' || m === 'ok-busy') cls += ' busy'; else if (m === 'error') cls += ' error'; else if (m === 'stopped') cls += ' stopped'; else if (m === 'ok') cls += ' ok'; else cls += ' off'; } el.dot.className = cls; }, - setStartEnabled: function (b) { if (el.toggle) el.toggle.disabled = !b; }, setStats: function (ws, memN, skillN) { if (!el.stats) return; el.stats.textContent = 'WS: ' + (ws || '?') + ' mem: ' + (memN || 0) + ' skills: ' + (skillN || 0); }, setMetrics: function (m) { if (!el.metrics) return; if (!m) { el.metrics.textContent = 'metrics: —'; return; } state.lastMetrics = m; var __pct = 0; if (state.accLimit > 0) __pct = Math.round((m.accum || 0) * 100 / state.accLimit); var __cls = 'dshmem-acc-ok'; if (__pct > 100) __cls = 'dshmem-acc-crit'; else if (__pct >= 80) __cls = 'dshmem-acc-err'; else if (__pct >= 40) __cls = 'dshmem-acc-warn'; el.metrics.className = 'dshmem-stats ' + __cls; el.metrics.textContent = 'acc: ' + m.accum + ' · turn: ' + m.delta + ' · msg: ' + m.chars + 'ch (~' + m.est_tok + 't) ctx ' + (m.ctx_tok || 0) + 't ' + (m.ctx_pct || 0) + 'pct'; }, + setStartEnabled: function (b) { if (el.toggle) el.toggle.disabled = !b; }, setStats: function (ws, totalN, alwaysN) { if (!el.stats) return; el.stats.textContent = 'WS: ' + (ws || '?') + ' entries: ' + (totalN || 0) + ' (always ' + (alwaysN || 0) + ')'; }, setMetrics: function (m) { if (!el.metrics) return; if (!m) { el.metrics.textContent = 'metrics: —'; return; } state.lastMetrics = m; var __pct = 0; if (state.accLimit > 0) __pct = Math.round((m.accum || 0) * 100 / state.accLimit); var __cls = 'dshmem-acc-ok'; if (__pct > 100) __cls = 'dshmem-acc-crit'; else if (__pct >= 80) __cls = 'dshmem-acc-err'; else if (__pct >= 40) __cls = 'dshmem-acc-warn'; el.metrics.className = 'dshmem-stats ' + __cls; el.metrics.textContent = 'acc: ' + m.accum + ' · turn: ' + m.delta + ' · msg: ' + m.chars + 'ch (~' + m.est_tok + 't) ctx ' + (m.ctx_tok || 0) + 't ' + (m.ctx_pct || 0) + 'pct'; }, toggleForceInject: function () { state.forceInject = !state.forceInject; try { chrome.storage.local.set({ 'dshmem-force-inject': state.forceInject }) } catch (e) { } diff --git a/memory/state.template.json b/memory/state.template.json index 0a96913..96a436c 100644 --- a/memory/state.template.json +++ b/memory/state.template.json @@ -1,6 +1,6 @@ { "version": 2, - "updatedAt": "2026-11-03T13:09:21.539Z", + "updatedAt": "2026-11-07T05:58:15.566Z", "entries": [ { "id": "0f90dcf0-5d43-4ca4-bf86-ad8b3ee20382", @@ -84,18 +84,19 @@ "scope": "user-global", "workspaceKey": "", "key": "protocol_memory_system", - "text": "МЕХАНИЗМ ПАМЯТИ dsh-memory v2\n\nСлои. Protocol (track agent, importance always) инжектится в первый ход. Session (directive scope workspace) инжектится в первый ход текущего чата. Reference (called) и Archive (state archive) только по запросу.\n\nЧто инжектится всегда: все track agent + importance always, плюс directive_ scope workspace state active для текущего chatId с проверкой workspaceKey. Fingerprint: если тот же набор уже в истории чата — пропуск всего блока.\n\nЗапросы через dsh_memory (op):\nlist track scope importance state since limit — items с preview и bytes, totalBytes, matched\nget ids — items с content, totalBytes\nrecent since — items\nunread chatId — новее lastInjectAt\nsearch query track — plain substring, totalBytes\ndirectives chatId state active|done|archive|all — директивы\nbudgets — used по track и scope, alwaysCount, лимиты\nadd entry — новая запись, entry включает track scope workspaceKey key text importance tags\nreplace id text tags importance state chatId — правка\nremove id — удаление\n\nСервер ВСЕГДА отдаёт totalBytes в meta ДО контента. Модель видит объём и решает: взять всё, взять по ids, взять preview, отложить.\n\nТаксономия ключей: user_ факты о пользователе, project_ проекты, directive_ правила и шаги, pref_ привычки, tech_ стек, interest_ интересы, pitfall_ грабли и обходы, decision_ архитектурные решения. Префикс задаёт группу, программа не решает релевантность.\n\nЛимиты: always записей 10, tags 5 на запись по 32 символа, директив на чат 5, preview 200. Калибровать по обратной связи.\n\nОбновление факта: replace по тому же id, не дубликат. AUTHENTIC EXTRACTION ONLY: только явно сказанное пользователем. NEVER cite memory tags в ответе.", + "text": "МЕХАНИЗМ ПАМЯТИ dsh-memory v2\n\nСлои. Protocol (track agent, importance always) инжектится в первый ход. Session (directive scope workspace) инжектится в первый ход текущего чата. Reference (called) и Archive (state archive) только по запросу.\n\nЧто инжектится всегда: все track agent + importance always, плюс directive_ scope workspace state active для текущего chatId с проверкой workspaceKey. Fingerprint: если тот же набор уже в истории чата — пропуск всего блока.\n\nЗапросы через dsh_memory (op):\nlist track scope importance state since limit — items с preview и bytes, totalBytes, matched\nget ids — items с content, totalBytes\nrecent since — items\nunread chatId — новее lastInjectAt\nsearch query track — plain substring, totalBytes\ndirectives chatId state active|done|archive|all — директивы\nbudgets — used по track и scope, alwaysCount, лимиты\nadd entry — новая запись, entry включает track scope workspaceKey key text importance tags\nreplace id text tags importance state chatId — правка\nremove id — удаление\n\nСервер ВСЕГДА отдаёт totalBytes в meta ДО контента. Модель видит объём и решает: взять всё, взять по ids, взять preview, отложить.\n\nТаксономия ключей: user_ факты о пользователе, project_ проекты, directive_ правила и шаги, pref_ привычки, tech_ стек, interest_ интересы, pitfall_ грабли и обходы, decision_ архитектурные решения. Префикс задаёт группу, программа не решает релевантность.\n\nКОГДА ПИСАТЬ (триггеры, важнее чем формат):\n1. Завершён этап работы (коммит, фича, интеграция) -> project_<тема> или decision_<тема>, importance called. Что сделано, где код, какие грабли.\n2. Наступил на грабли второй раз -> pitfall_<тема>, called. Симптом + обход.\n3. Изменилось правило поведения (новая политика, новый лимит) -> replace существующего protocol_*, всегда. Не дубликат.\n4. Появился новый инструмент/команда/приложение -> protocol_*, always. Чтобы модель в новом чате знала.\n5. Пользователь сказал факт о себе или предпочтение -> user_/pref_, called.\n6. Проект/сервис добавлен или изменён -> project_*, called.\n\nПравило записи: после каждого завершённого этапа (2+ коммита или значимая правка) — записать хотя бы одну project_ или decision_ запись. Если этап был без грабель и без новых правил — всё равно project_: что сделано, где смотреть. Иначе через неделю память не отражает реальность.\n\nЛимиты: always записей 10, tags 5 на запись по 32 символа, директив на чат 5, preview 200. Калибровать по обратной связи.\n\nОбновление факта: replace по тому же id, не дубликат. AUTHENTIC EXTRACTION ONLY: только явно сказанное пользователем. NEVER cite memory tags в ответе.", "importance": "always", "tags": [ "protocol", - "memory" + "memory", + "triggers" ], - "version": 4, + "version": 5, "createdAt": "2026-11-02T23:35:16.258Z", - "updatedAt": "2026-11-03T03:41:45.792Z", + "updatedAt": "2026-11-07T05:57:12.533Z", "lastRecalled": null, "recallCount": 0, - "bytes": 2327 + "bytes": 3807 }, { "id": "2853ce0e-de50-455c-bd5a-ebbf9a3fe337", @@ -155,7 +156,120 @@ "lastRecalled": null, "recallCount": 0, "bytes": 2117 + }, + { + "id": "3e51562d-a56b-4919-9720-cf36597553b6", + "track": "agent", + "scope": "user-global", + "workspaceKey": "", + "key": "project_mcp_integration", + "text": "MCP-интеграция ArtCraft в dsh-memory. Сделано 06.11.2026, коммиты 45222a0, 93504ed, fe9c4eb, f03dd76, 45ace2c, 4090ba0.\n\nЧТО СДЕЛАНО. Мост научился управлять приложениями ArtCraft через MCP (stdio JSON-RPC). 17 приложений: vectorcraft, photocraft, figcraft, designcraft, deckcraft, cadcraft, craftanything, effectcraft, filmcraft, gridcraft, lightcraft, materialcraft, pdfcraft, solidcraft, soundcraft, texturecraft, wordcraft. Порты 7979-7995, у каждого свой. Клиент поднимается лениво при первом вызове. Инструменты _mcp_tools и _mcp_call.\n\nФАЙЛЫ. bridge/mcp-client.js (JSON-RPC по stdio), bridge/mcp-registry.js (discovery, порты, флаги коннекта), bridge/mcp-tools.js (менеджер, ленивый старт, launchGui, stop), bridge/server.js (маршруты /mcp/apps, /mcp/launch, /mcp/stop, /mcp/rescan, /mcp/root).\n\nLIVE РЕЖИМ. vectorcraft.exe --control 7979, CLI коннектится через --connect 127.0.0.1:7979. Флаги у приложений разные: --connect ADDR (vectorcraft, designcraft, gridcraft, wordcraft, cadcraft, solidcraft, soundcraft, deckcraft), --bridge (photocraft, effectcraft, filmcraft, materialcraft, texturecraft), --attach (figcraft). pdfcraft и craftanything live не умеют.\n\nГРАБЛИ. Мост кэширует CLI-клиент: если окно подняли после старта клиента, он не переподключится. launchGui это лечит (закрывает старый клиент). Мост выбрасывает inline-картинки из MCP-ответов (mcp-client.js:81) — всегда просить path. ui_screenshot не принимает path, файл пишет screenshot {window:true, path}. draw_shape поле shape не kind, add_text color не fill — схемы через _mcp_tools {filter}.", + "importance": "called", + "tags": [ + "project", + "mcp", + "artcraft" + ], + "state": null, + "chatId": "", + "version": 1, + "createdAt": "2026-11-07T05:57:38.065Z", + "updatedAt": "2026-11-07T05:57:38.065Z", + "lastRecalled": null, + "recallCount": 0, + "bytes": 2024 + }, + { + "id": "4bcc17ab-cc89-4877-8695-63042b0aeaab", + "track": "agent", + "scope": "user-global", + "workspaceKey": "", + "key": "decision_bash_policy", + "text": "Bash-политика dsh-memory: safe-list вместо блоков. Решение принято 07.11.2026, коммиты c2cf419, 69602ec, a298e6d.\n\nПОЧЕМУ. Раньше метасимволы (| & > < ; обратный апостроф $) и blacklist-паттерны блокировали команду намертво. Это создавало видимость защиты: обход через node script.mjs был тривиален, не логировался, а модель обходила блоки через .mjs-файлы. Пользователь сформулировал: программа не должна запрещать, она должна спрашивать.\n\nКАК СЕЙЧАС. Жёстко блокируются только пустая команда и >2000 символов. Safe-list (без подтверждения): pwd, dir, ls, tree, where, which, whoami, hostname, uname, ver, cd, echo, date, time, git status/diff/log/show/branch/rev-parse/ls-files/describe/remote/config (с пропуском -C, -c, --no-pager), npm/pnpm/yarn/bun test/run/build/lint/typecheck/check/ls/list/why/outdated/info/view (с пропуском --prefix, --workspace, -w), npx tsc/eslint/prettier/vitest/jest/mocha, node/npm/npx --version, tsc, eslint. Не включены сознательно: cat, head, tail, type, more, less — читают файлы в обход validatePath.\n\nPOLICY-ФЛАГИ (confirm): shell (метасимволы), multiline, destructive (rm, del, format, mv, cp, git push/commit/reset/clean, taskkill, reg, sc, schtasks), network (curl, wget, ssh, scp, ftp, powershell, iwr), absolute-path, parent-path. Любой флаг отменяет allow.\n\nДВА СЛОЯ. Pre-flight: модель объявляет в тексте до отправки, ждёт да (protocol_policy). Панельный confirm: Allow/Deny с ожиданием клика (content.js executeOne), fail-closed при недоступной панели (E_CONFIRM_UNAVAILABLE).\n\nГРАБЛИ. git -C path status раньше уходил в confirm (skipGlobalFlags лечит). Двойной E_NEEDS_CONFIRM в логе (server.js: при confirmToken сразу call(true)).", + "importance": "called", + "tags": [ + "decision", + "bash", + "policy", + "security" + ], + "state": null, + "chatId": "", + "version": 1, + "createdAt": "2026-11-07T05:57:41.076Z", + "updatedAt": "2026-11-07T05:57:41.076Z", + "lastRecalled": null, + "recallCount": 0, + "bytes": 2252 + }, + { + "id": "917a3647-9bb5-4283-8eff-f59dfa1c3648", + "track": "agent", + "scope": "user-global", + "workspaceKey": "", + "key": "pitfall_eol_normalize", + "text": "dsh_edit не находит oldText в файлах со смешанными CRLF/LF. Наступал многократно за 06-07.11.2026 на server.js, tools.js, panel.js, overlay.css, background.js.\n\nСИМПТОМ. E_ARGS_INVALID oldText not found (0 matches), хотя текст визуально совпадает. Причина: файл в CRLF, а oldText в LF (или наоборот).\n\nОБХОД. Перед правкой проверить EOL скриптом (посчитать CRLF vs LF), нормализовать файл к LF: fs.readFileSync -> replace(CRLF, LF) -> writeFileSync. Потом dsh_edit. После правки node -c для проверки синтаксиса. Для больших блоков (десятки строк) надёжнее не dsh_edit, а отдельный скрипт замены по маркерам: найти индексы start/end, собрать merged = before + section + after.", + "importance": "called", + "tags": [ + "pitfall", + "eol", + "crlf", + "dsh_edit" + ], + "state": null, + "chatId": "", + "version": 1, + "createdAt": "2026-11-07T05:57:44.088Z", + "updatedAt": "2026-11-07T05:57:44.088Z", + "lastRecalled": null, + "recallCount": 0, + "bytes": 972 + }, + { + "id": "1c3a7d4c-fef0-4575-ab7c-d6cf1978e9d0", + "track": "agent", + "scope": "user-global", + "workspaceKey": "", + "key": "pitfall_dsh_edit_large_oldtext", + "text": "dsh_edit с очень большим oldText (десятки строк) может применить замену криво: вставить новый блок, но не удалить старый, или задвоить содержимое. Произошло 07.11.2026 с README.md: файл раздулся с 690 до 1396 строк (дубли всех разделов).\n\nПРИЧИНА. Не подтверждена достоверно, но воспроизводилось.\n\nОБХОД. Для блоков больше 15-20 строк не использовать dsh_edit. Писать новый блок в отдельный .md/.txt файл, затем скрипт: прочитать целевой файл, найти indexOf(startMark) и indexOf(endMark), собрать merged = lines.slice(0,start) + sectionLines + lines.slice(end), записать. Это детерминированно. После — проверить размер (bytes, lines) и посчитать вхождения ключевых заголовков (должно быть по 1). При сомнениях git checkout -- file и повторить.", + "importance": "called", + "tags": [ + "pitfall", + "dsh_edit", + "large-block" + ], + "state": null, + "chatId": "", + "version": 1, + "createdAt": "2026-11-07T05:57:47.096Z", + "updatedAt": "2026-11-07T05:57:47.096Z", + "lastRecalled": null, + "recallCount": 0, + "bytes": 1176 + }, + { + "id": "06442ed5-f07c-4beb-bbaf-6084b5b2dddc", + "track": "agent", + "scope": "workspace", + "workspaceKey": "V:/Site", + "key": "project_warp_cz", + "text": "WARP на чешском VPS (cvetochny, 85.137.166.251): скрыть IP VPS от клиентов российского туннеля. Спроектировано 06.11.2026, скрипты написаны, на сервер не выкатывалось.\n\nЗАЧЕМ. Клиенты site-to-site туннеля Россия-Чехия (RU 188.127.239.17, CZ 85.137.166.251) выходят в интернет с чешского IP. WARP завернёт только клиентов (10.8.1.0/24) в Cloudflare, сервисы на самом VPS (3proxy :3128 для Telegram-бота starvery, nginx :8443) останутся на чешском IP.\n\nФАЙЛЫ. backup-20261104/warp-cz/: warp-on.sh (wg-quick up wgcf, таблица warp id 201, правило from 10.8.1.0/24 priority 50, NAT на wgcf, TCPMSS clamp, проверка trace), warp-off.sh, warp-status.sh, wgcf.conf.example, README.md. Раздел 18 в backup-20261104/RECOVERY-AWG-SITE-TO-SITE.md.\n\nРЕЖИМ. Fail-open: старый NAT -s 10.8.1.0/24 -o ens3 MASQUERADE не удаляется. При падении WARP клиенты получают чешский IP, не теряют связь. Геолокация не важна.\n\nГРАБЛИ. Table = off в wgcf.conf обязателен (иначе wg-quick перезапишет default route, SSH умрёт). DNS = закомментировать (иначе /etc/resolv.conf перепишется, 3proxy/nginx/apt начнут резолвить через 1.1.1.1). WARP-адреса общие, Google/банки дают капчу. Геолокация не привязана к Чехии (Cloudflare выходит через ближайший DC). Хостинг может блокировать WARP — проверять curl --interface wgcf https://www.cloudflare.com/cdn-cgi/trace до включения.", + "importance": "called", + "tags": [ + "project", + "vpn", + "warp", + "czech" + ], + "state": null, + "chatId": "", + "version": 1, + "createdAt": "2026-11-07T05:57:50.110Z", + "updatedAt": "2026-11-07T05:57:50.110Z", + "lastRecalled": null, + "recallCount": 0, + "bytes": 1880 } ], "chats": {} -} \ No newline at end of file +}