From 04d086b8190c823d14486fdd7edeb13cb8858afd Mon Sep 17 00:00:00 2001 From: Serge Alex Date: Tue, 3 Nov 2026 02:58:47 +0300 Subject: [PATCH] directives-lifecycle-active-done-archive --- bridge/memory.js | 25 ++++++++++++++++++++++++- bridge/tools.js | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/bridge/memory.js b/bridge/memory.js index a500858..3cfba08 100644 --- a/bridge/memory.js +++ b/bridge/memory.js @@ -162,6 +162,7 @@ function validateEntry(e) { if (!e.key || !/^[a-z0-9_]{1,64}$/.test(String(e.key))) throw new Error('key snake_case max 64'); if (typeof e.text !== 'string' || !e.text) throw new Error('text required'); if (e.importance !== 'always' && e.importance !== 'called') throw new Error('importance always or called'); + if (e.state !== undefined && e.state !== 'active' && e.state !== 'done' && e.state !== 'archive') throw new Error('state active|done|archive'); if (Array.isArray(e.tags)) { if (e.tags.length > TAGS_LIMIT) throw new Error('tags max ' + TAGS_LIMIT); for (const t of e.tags) if (String(t).length > TAG_LEN_LIMIT) throw new Error('tag too long'); @@ -178,6 +179,7 @@ function opWrite(state, args) { const entry = { id: newId(), track: e.track, scope: e.scope, workspaceKey: String(e.workspaceKey || ''), key: e.key, text: e.text, importance: e.importance, tags: Array.isArray(e.tags) ? e.tags.slice(0, TAGS_LIMIT) : [], + state: e.state || null, chatId: e.chatId || '', version: 1, createdAt: nowIso(), updatedAt: nowIso(), lastRecalled: null, recallCount: 0, bytes: byteLen(e.text) }; @@ -191,6 +193,8 @@ function opWrite(state, args) { if (typeof args.text === 'string' && args.text) { e.text = args.text; e.bytes = byteLen(args.text); } if (Array.isArray(args.tags)) e.tags = args.tags.slice(0, TAGS_LIMIT); if (args.importance === 'always' || args.importance === 'called') e.importance = args.importance; + if (args.state === 'active' || args.state === 'done' || args.state === 'archive') e.state = args.state; + if (typeof args.chatId === 'string') e.chatId = args.chatId; e.version = (e.version || 1) + 1; e.updatedAt = nowIso(); return { ok: true, op: 'replace', id: e.id, version: e.version }; @@ -205,10 +209,26 @@ function opWrite(state, args) { throw new Error('op add|replace|remove'); } +function opDirectives(state, args) { + const chatId = String(args.chatId || ''); + const stateFilter = args.state ? String(args.state) : 'active'; + const pv = Math.min(Math.max(1, Number(args.preview) || 200), 800); + const hits = state.entries.filter(function (e) { + if (String(e.key).indexOf('directive_') !== 0) return false; + if (stateFilter !== 'all' && e.state !== stateFilter) return false; + if (chatId && e.chatId && e.chatId !== chatId) return false; + return true; + }).sort(rank); + let totalBytes = 0; + for (const e of hits) totalBytes += e.bytes || 0; + return { items: hits.map(function (e) { return { id: e.id, key: e.key, state: e.state, chatId: e.chatId || '', importance: e.importance, bytes: e.bytes, updatedAt: e.updatedAt, preview: preview(e.text, pv) }; }), totalBytes: totalBytes, matched: hits.length, stateFilter: stateFilter }; +} + function query(state, args) { const op = String(args.op || 'list'); if (op === 'list') return opList(state, args); if (op === 'add' || op === 'replace' || op === 'remove') return opWrite(state, Object.assign({}, args, { op: op })); + if (op === 'directives') return opDirectives(state, args); if (op === 'get') return opGet(state, args); if (op === 'recent') return opRecent(state, args); if (op === 'unread') return opUnread(state, args); @@ -222,9 +242,12 @@ function query(state, args) { function getInjection(state, chatId) { const entries = []; for (const e of state.entries) { + if (e.key && e.key.indexOf('directive_') === 0 && e.scope === 'workspace' && e.state === 'active') { + if (!chatId || !e.chatId || e.chatId === chatId) { entries.push(e); } + continue; + } if (e.importance !== 'always') continue; if (e.track === 'agent' && e.scope === 'user-global') { entries.push(e); continue; } - if (e.key && e.key.indexOf('directive_') === 0 && e.scope === 'workspace') { entries.push(e); continue; } } const texts = entries.map(function (e) { return String(e.text); }); const fpBase = entries.map(function (e) { return e.id + ':' + (e.version || 1); }).sort().join('|'); diff --git a/bridge/tools.js b/bridge/tools.js index 9d16829..8758b56 100644 --- a/bridge/tools.js +++ b/bridge/tools.js @@ -52,7 +52,7 @@ const TOOL_DEFS = [ { name: 'dsh_git_diff', description: 'git diff (read-only, dumps to file if too large)', params: { path: 'string?', staged: 'bool?', maxBytes: 'int? default 48000' } }, { name: 'dsh_handoff', description: '[Recommended for multi-step tasks] Local agent (dsh headless) runs the full plan autonomously: reads/searches/edits/runs commands with no step-by-step driving, then returns only the final result. planText must be a self-contained full plan (with acceptance criteria). After calling, end your reply immediately; do not output further tool calls', params: { planText: 'string full plan text', workspace: 'string?', taskNote: 'string? task title (used for log names)', timeoutMs: 'int? default 300000, max 1800000' } }, { name: 'dsh_log', description: 'Read tail of execution log (.dsh-pro/logs/execution-log.jsonl, one entry per tool call)', params: { lines: 'int? default 100, max 1000' } }, - { name: 'dsh_memory', description: 'Persistent memory. op: list|get|recent|unread|search|budgets|write. list/get/recent/unread/search return items with bytes and totalBytes in meta; always check totalBytes before get. write op: add|replace|remove.', params: { op: 'string', track: 'string?', scope: 'string?', importance: 'string?', since: 'string?', limit: 'int?', ids: 'string[]?', id: 'string?', query: 'string?', entry: 'object?', text: 'string?', tags: 'string[]?' } }, + { name: 'dsh_memory', description: 'Persistent memory. op: list|get|recent|unread|search|budgets|directives|add|replace|remove. list/get/recent/unread/search return items with bytes and totalBytes in meta; always check totalBytes before get. write op: add|replace|remove.', params: { op: 'string', track: 'string?', scope: 'string?', importance: 'string?', since: 'string?', limit: 'int?', ids: 'string[]?', id: 'string?', query: 'string?', entry: 'object?', text: 'string?', tags: 'string[]?', state: 'string?', chatId: 'string?' } }, ]; // ---------------------------------------------------------------------------