mixdog 0.9.46 → 0.9.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/defaults/cycle3-review-prompt.md +10 -4
- package/src/defaults/memory-promote-prompt.md +4 -3
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
- package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
- package/src/runtime/memory/lib/query-handlers.mjs +49 -6
- package/src/runtime/memory/lib/recall-format.mjs +21 -6
- package/src/runtime/memory/tool-defs.mjs +2 -2
package/package.json
CHANGED
|
@@ -23,7 +23,9 @@ CORE is durable standing knowledge that lands in one of three layers:
|
|
|
23
23
|
- **L3 — Current map:** one-line project-landscape summaries, live long-running
|
|
24
24
|
goals, environment anchors documented nowhere else.
|
|
25
25
|
|
|
26
|
-
Every entry
|
|
26
|
+
Every entry must be ONE compact ENGLISH clause (≤120 chars), regardless of
|
|
27
|
+
source language. Keep identifiers, paths, and exact phrases coined by the user
|
|
28
|
+
verbatim. CORE is not a log.
|
|
27
29
|
|
|
28
30
|
## The distinction that decides every verdict
|
|
29
31
|
|
|
@@ -48,8 +50,10 @@ or inconclusive, keep the CORE entry.
|
|
|
48
50
|
|
|
49
51
|
## Verdicts
|
|
50
52
|
|
|
51
|
-
- `keep` — durable and already one
|
|
52
|
-
- `update` — durable but verbose
|
|
53
|
+
- `keep` — durable and already one compact ENGLISH clause.
|
|
54
|
+
- `update` — durable but verbose, multi-sentence, or non-English → rewrite as
|
|
55
|
+
one compact ≤120-char ENGLISH clause; keep identifiers, paths, and exact
|
|
56
|
+
phrases coined by the user verbatim.
|
|
53
57
|
- `merge` — duplicates another entry → fold into the survivor (same project pool).
|
|
54
58
|
- `reclassify` — the entry is filed under the WRONG project pool. Its subject
|
|
55
59
|
clearly belongs to a different pool shown among the entries under review: a
|
|
@@ -81,7 +85,9 @@ or inconclusive, keep the CORE entry.
|
|
|
81
85
|
`<id>|delete` with no reason, or any reason outside that set, is treated as
|
|
82
86
|
"needs confirmation" and only removed on an explicit APPLY CYCLE3 run.
|
|
83
87
|
|
|
84
|
-
A verbose durable entry is always `update`,
|
|
88
|
+
A verbose, multi-sentence, or non-English durable entry is always `update`,
|
|
89
|
+
never `keep`; rewrite it as a ≤120-char English clause while keeping
|
|
90
|
+
identifiers, paths, and exact user-coined phrases verbatim.
|
|
85
91
|
Delete is the rarest verdict. Prefer `keep` for durable rules/preferences and
|
|
86
92
|
`update` for compression when the current behavior is still valid.
|
|
87
93
|
Use `superseded` (not `delete`) when the entry WAS a valid durable rule/fact but
|
|
@@ -47,9 +47,10 @@ Use `why|A` for durable cross-session invariants (L1/L2). Use `why|B` for L3
|
|
|
47
47
|
current-map entries: one-line project-landscape summaries, live long-running
|
|
48
48
|
goals, and environment anchors documented nowhere else. `why` is validation
|
|
49
49
|
evidence only; it is
|
|
50
|
-
not stored. `core_summary` is injected into Core Memory, so
|
|
51
|
-
self-contained clause, <=120
|
|
52
|
-
|
|
50
|
+
not stored. `core_summary` is injected into Core Memory, so write it in ENGLISH
|
|
51
|
+
regardless of source language as one compact, self-contained clause, <=120
|
|
52
|
+
chars. Keep code identifiers, paths, and commands verbatim. For `merge`, `why`
|
|
53
|
+
and `core` may use the survivor `target_row`.
|
|
53
54
|
|
|
54
55
|
Allowed primary verbs:
|
|
55
56
|
|
|
@@ -11,11 +11,10 @@ import { __mixdogMemoryLog, throwIfAborted, resourceDir } from './memory-cycle2-
|
|
|
11
11
|
|
|
12
12
|
export const CYCLE2_ACTIVE_TARGET_CAP = 100
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
export const CYCLE2_ACTIVE_MIN_FLOOR = 20
|
|
14
|
+
// Default active-row floor for cycle2. It is zero so rolling active rechecks
|
|
15
|
+
// always run; protection from archive verdicts is opt-in via
|
|
16
|
+
// config.active_floor.
|
|
17
|
+
export const CYCLE2_ACTIVE_MIN_FLOOR = 0
|
|
19
18
|
|
|
20
19
|
// Status-based verb whitelist. 3-tier policy: pending → active/archived,
|
|
21
20
|
// active → active/archived/update/merge.
|
|
@@ -224,6 +223,15 @@ export function loadCurrentRulesDigest() {
|
|
|
224
223
|
}
|
|
225
224
|
} catch {}
|
|
226
225
|
}
|
|
226
|
+
const workflows = join(resourceDir(), 'workflows')
|
|
227
|
+
try {
|
|
228
|
+
if (existsSync(workflows)) {
|
|
229
|
+
for (const dir of readdirSync(workflows).sort()) {
|
|
230
|
+
const workflow = join(workflows, dir, 'WORKFLOW.md')
|
|
231
|
+
if (existsSync(workflow)) sources.push(workflow)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch {}
|
|
227
235
|
const parts = []
|
|
228
236
|
for (const p of sources) {
|
|
229
237
|
try {
|
|
@@ -259,16 +259,19 @@ async function _runCycle2Impl(db, config = {}, options = {}, dataDir = null) {
|
|
|
259
259
|
// over-cap path was historically the ONLY one that re-examined active.
|
|
260
260
|
// Reserve a bounded slice of batch slots for the stalest active rows so
|
|
261
261
|
// rule-duplicate / drifted promotions are archived continuously instead of
|
|
262
|
-
// sitting forever un-rechecked. Bounded count + reviewed_at rotation
|
|
263
|
-
//
|
|
262
|
+
// sitting forever un-rechecked. Bounded count + reviewed_at rotation keep
|
|
263
|
+
// each cycle's exposure small (with the default floor of 0 the pool CAN
|
|
264
|
+
// erode to zero over repeated cycles if the gate keeps archiving; set
|
|
265
|
+
// config.active_floor to retain a hard minimum):
|
|
264
266
|
// only the oldest few are re-judged per cycle, and the gate — shown
|
|
265
267
|
// {{CURRENT_RULES}} — keeps genuine A/B entries and archives only
|
|
266
268
|
// restatements. Embedding dedup is skipped on purpose: rule restatements
|
|
267
269
|
// are often cross-language paraphrases whose cosine never clears the merge
|
|
268
270
|
// threshold, but the LLM gate catches the semantic overlap.
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
271
|
+
// Rolling active recheck always runs outside the over-cap broad review,
|
|
272
|
+
// including when the active pool is small. Floor protection is opt-in via
|
|
273
|
+
// config.active_floor and is enforced by the shared floor guard below.
|
|
274
|
+
const activeRecheckQuota = reviewActiveRows
|
|
272
275
|
? 0
|
|
273
276
|
: Math.max(0, Math.min(Number(config.active_recheck_quota ?? 8), batchSize - 1))
|
|
274
277
|
const pendingLimit = batchSize - activeRecheckQuota
|
|
@@ -112,7 +112,30 @@ function coreText(core) {
|
|
|
112
112
|
return `${core?.element || ''}\n${core?.summary || ''}`
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function
|
|
115
|
+
function hasSubstantialNonLatinScript(value) {
|
|
116
|
+
const text = String(value ?? '')
|
|
117
|
+
const letters = text.match(/\p{L}/gu) || []
|
|
118
|
+
const latinLetters = letters.filter((letter) => /\p{Script=Latin}/u.test(letter))
|
|
119
|
+
const nonLatinLetters = letters.length - latinLetters.length
|
|
120
|
+
return nonLatinLetters >= 3 && nonLatinLetters >= letters.length * 0.3
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function cosineSimilarity(a, b) {
|
|
124
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) return null
|
|
125
|
+
let dot = 0
|
|
126
|
+
let aNorm = 0
|
|
127
|
+
let bNorm = 0
|
|
128
|
+
for (let i = 0; i < a.length; i++) {
|
|
129
|
+
if (!Number.isFinite(a[i]) || !Number.isFinite(b[i])) return null
|
|
130
|
+
dot += a[i] * b[i]
|
|
131
|
+
aNorm += a[i] ** 2
|
|
132
|
+
bNorm += b[i] ** 2
|
|
133
|
+
}
|
|
134
|
+
if (aNorm === 0 || bNorm === 0) return null
|
|
135
|
+
return dot / Math.sqrt(aNorm * bNorm)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function isSafeConservativeUpdate(current, action) {
|
|
116
139
|
if (!current || !action?.element || !action?.summary) return { ok: false, reason: 'missing text' }
|
|
117
140
|
const newElement = normalizeComparable(action.element)
|
|
118
141
|
const newSummary = normalizeComparable(action.summary)
|
|
@@ -123,11 +146,23 @@ function isSafeConservativeUpdate(current, action) {
|
|
|
123
146
|
const newText = `${action.element}\n${action.summary}`
|
|
124
147
|
const oldLen = normalizeComparable(oldText).length
|
|
125
148
|
const newLen = normalizeComparable(newText).length
|
|
126
|
-
if (oldLen > 0 && newLen > oldLen + 20) return { ok: false, reason: 'rewrite expands entry' }
|
|
127
|
-
|
|
128
149
|
const sim = charDice(oldText, newText)
|
|
129
|
-
|
|
130
|
-
|
|
150
|
+
const crossLanguageRewrite = sim < 0.28 && hasSubstantialNonLatinScript(oldText)
|
|
151
|
+
if (!crossLanguageRewrite) {
|
|
152
|
+
if (oldLen > 0 && newLen > oldLen + 20) return { ok: false, reason: 'rewrite expands entry' }
|
|
153
|
+
if (sim < 0.28) return { ok: false, reason: `rewrite drift sim=${sim.toFixed(2)}` }
|
|
154
|
+
return { ok: true, reason: 'safe compression' }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const [oldEmbedding, newEmbedding] = await Promise.all([embedText(oldText), embedText(newText)])
|
|
159
|
+
const cosine = cosineSimilarity(oldEmbedding, newEmbedding)
|
|
160
|
+
if (cosine == null) return { ok: false, reason: 'cross-language embedding invalid' }
|
|
161
|
+
if (cosine < 0.6) return { ok: false, reason: `cross-language semantic drift cosine=${cosine.toFixed(2)}` }
|
|
162
|
+
return { ok: true, reason: `safe cross-language rewrite cosine=${cosine.toFixed(2)}` }
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return { ok: false, reason: `cross-language embedding failed: ${err?.message || 'unknown error'}` }
|
|
165
|
+
}
|
|
131
166
|
}
|
|
132
167
|
|
|
133
168
|
function findElementConflict(coreById, currentId, element, projectId) {
|
|
@@ -659,7 +694,7 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
|
|
|
659
694
|
}
|
|
660
695
|
if (a.verb === 'update') {
|
|
661
696
|
proposed.updated++
|
|
662
|
-
const safety = conservative ? isSafeConservativeUpdate(coreById.get(a.id), a) : { ok: true, reason: 'confirmed' }
|
|
697
|
+
const safety = conservative ? await isSafeConservativeUpdate(coreById.get(a.id), a) : { ok: true, reason: 'confirmed' }
|
|
663
698
|
const conflictId = conservative
|
|
664
699
|
? findElementConflict(coreById, a.id, a.element, coreById.get(a.id)?.project_id ?? null)
|
|
665
700
|
: null
|
|
@@ -49,7 +49,7 @@ export function createQueryHandlers({
|
|
|
49
49
|
// Raw-row priority lookup for narrow-window queries. Raw rows (is_root=0,
|
|
50
50
|
// chunk_root IS NULL) are inserted immediately by ingestTranscriptFile before
|
|
51
51
|
// cycle1 runs, so they always carry the freshest turns in the DB.
|
|
52
|
-
async function readRawRowsInWindow(db, tsFromMs, tsToMs, hardLimit = 10, { projectScope, sessionId, terms } = {}) {
|
|
52
|
+
async function readRawRowsInWindow(db, tsFromMs, tsToMs, hardLimit = 10, { projectScope, sessionId, terms, minHits: minHitsOverride } = {}) {
|
|
53
53
|
try {
|
|
54
54
|
// Composable WHERE assembly (mirrors retrieveEntries' filter semantics so
|
|
55
55
|
// raw and chunked legs stay in filter parity: projectScope AND sessionId
|
|
@@ -78,7 +78,9 @@ export function createQueryHandlers({
|
|
|
78
78
|
// into the page. 1-2 term queries keep single-hit contains semantics —
|
|
79
79
|
// short Korean queries are often exactly two meaningful tokens and a
|
|
80
80
|
// 2-of-2 requirement silently emptied the raw leg for them.
|
|
81
|
-
const minHits =
|
|
81
|
+
const minHits = Number.isFinite(Number(minHitsOverride))
|
|
82
|
+
? Math.max(1, Math.floor(Number(minHitsOverride)))
|
|
83
|
+
: (terms.length >= 3 ? 2 : 1)
|
|
82
84
|
where.push(`(${clauses.join(' + ')}) >= ${minHits}`)
|
|
83
85
|
}
|
|
84
86
|
params.push(hardLimit)
|
|
@@ -722,7 +724,7 @@ export function createQueryHandlers({
|
|
|
722
724
|
// Deterministic tie-breaker: equal MAX(ts) across sessions would let the
|
|
723
725
|
// LIMIT/OFFSET window skip or duplicate a session between offset pages
|
|
724
726
|
// without a stable secondary sort.
|
|
725
|
-
const sessSql = `SELECT session_id, MAX(ts) AS last_ts
|
|
727
|
+
const sessSql = `SELECT session_id, MIN(ts) AS first_ts, MAX(ts) AS last_ts
|
|
726
728
|
FROM entries
|
|
727
729
|
WHERE ${selWhere.join(' AND ')}
|
|
728
730
|
GROUP BY session_id
|
|
@@ -732,6 +734,7 @@ export function createQueryHandlers({
|
|
|
732
734
|
// 2) per selected session, fetch its newest rows (roots+members, sort by
|
|
733
735
|
// date) plus the fresh raw window, capped at PER_SESSION_ROW_CAP.
|
|
734
736
|
const allRows = []
|
|
737
|
+
const sessionMeta = new Map()
|
|
735
738
|
for (const s of sessRows) {
|
|
736
739
|
const sid = String(s?.session_id || '').trim()
|
|
737
740
|
if (!sid) continue
|
|
@@ -743,7 +746,24 @@ export function createQueryHandlers({
|
|
|
743
746
|
const sRows = await retrieveEntries(db, sf)
|
|
744
747
|
let merged = sRows
|
|
745
748
|
if (includeRaw) {
|
|
746
|
-
|
|
749
|
+
let rawRows
|
|
750
|
+
if (queryTerms.length > 0) {
|
|
751
|
+
// Keep a full unfiltered recency window for the newest-row floor,
|
|
752
|
+
// while separately retaining the deeper term-matched raw window.
|
|
753
|
+
const [recentRawRows, matchedRawRows] = await Promise.all([
|
|
754
|
+
readRawRowsInWindow(db, null, Date.now(), perSessionFetchCap, { projectScope, sessionId: sid, terms: [] }),
|
|
755
|
+
readRawRowsInWindow(db, null, Date.now(), perSessionFetchCap, { projectScope, sessionId: sid, terms: queryTerms, minHits: 1 }),
|
|
756
|
+
])
|
|
757
|
+
const rawIds = new Set()
|
|
758
|
+
rawRows = [...recentRawRows, ...matchedRawRows].filter((r) => {
|
|
759
|
+
const id = Number(r.id)
|
|
760
|
+
if (rawIds.has(id)) return false
|
|
761
|
+
rawIds.add(id)
|
|
762
|
+
return true
|
|
763
|
+
})
|
|
764
|
+
} else {
|
|
765
|
+
rawRows = await readRawRowsInWindow(db, null, Date.now(), perSessionFetchCap, { projectScope, sessionId: sid, terms: [] })
|
|
766
|
+
}
|
|
747
767
|
const seenIds = new Set(sRows.map((r) => Number(r.id)))
|
|
748
768
|
for (const r of sRows) if (Array.isArray(r.members)) for (const m of r.members) seenIds.add(Number(m.id))
|
|
749
769
|
// readRawRowsInWindow carries no category filter, so a category-
|
|
@@ -768,14 +788,37 @@ export function createQueryHandlers({
|
|
|
768
788
|
merged.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0) || (Number(b.id) || 0) - (Number(a.id) || 0))
|
|
769
789
|
}
|
|
770
790
|
}
|
|
791
|
+
const fetchedCount = merged.length
|
|
792
|
+
let queryFiltered = false
|
|
771
793
|
if (queryTerms.length > 0) {
|
|
772
|
-
|
|
794
|
+
const newestRows = merged.slice(0, 3)
|
|
795
|
+
const newestIds = new Set(newestRows.map((r) => r.id))
|
|
796
|
+
const matchedRows = merged.filter(matchesQueryTerms)
|
|
797
|
+
// A topic query must not obscure a session's latest activity: keep
|
|
798
|
+
// its three newest rows, then use term matches for the remaining
|
|
799
|
+
// display slots without duplicating rows already kept for recency.
|
|
800
|
+
merged = [...newestRows, ...matchedRows.filter((r) => !newestIds.has(r.id))]
|
|
801
|
+
// Only mark query filtering when its floor+match union actually
|
|
802
|
+
// excludes fetched rows. The later display cap is independent.
|
|
803
|
+
queryFiltered = merged.length < fetchedCount
|
|
773
804
|
}
|
|
774
805
|
merged = merged.slice(0, PER_SESSION_ROW_CAP)
|
|
806
|
+
sessionMeta.set(sid, {
|
|
807
|
+
minTs: Number(s.first_ts),
|
|
808
|
+
maxTs: Number(s.last_ts),
|
|
809
|
+
fetchedCount,
|
|
810
|
+
shownCount: merged.length,
|
|
811
|
+
queryFiltered,
|
|
812
|
+
})
|
|
775
813
|
for (const r of merged) allRows.push(r)
|
|
776
814
|
}
|
|
777
815
|
const _currentSessionHint = String(args?.currentSessionId || '').trim()
|
|
778
|
-
return { text: recallCapPrefix + renderSessionGroupedLines(allRows, {
|
|
816
|
+
return { text: recallCapPrefix + renderSessionGroupedLines(allRows, {
|
|
817
|
+
currentSessionId: _currentSessionHint,
|
|
818
|
+
recencyOrder: true,
|
|
819
|
+
spanHeaders: true,
|
|
820
|
+
sessionMeta,
|
|
821
|
+
}) }
|
|
779
822
|
}
|
|
780
823
|
|
|
781
824
|
const filters = { limit: limit + offset }
|
|
@@ -336,10 +336,16 @@ function spanHeaderSuffix(minTs, maxTs, n) {
|
|
|
336
336
|
// group's lines are globally ts-desc: without it a chunk root's members (stored
|
|
337
337
|
// ts-ASC) interleave with raw rows and invert the visible timeline within a
|
|
338
338
|
// session (e.g. 04:33 rendered above 04:41).
|
|
339
|
-
export function renderSessionGroupedLines(rows, { currentSessionId, recencyOrder = false, spanHeaders = false } = {}) {
|
|
340
|
-
|
|
339
|
+
export function renderSessionGroupedLines(rows, { currentSessionId, recencyOrder = false, spanHeaders = false, sessionMeta } = {}) {
|
|
340
|
+
const hasSessionMeta = spanHeaders && Number(sessionMeta?.size) > 0
|
|
341
|
+
if ((!rows || rows.length === 0) && !hasSessionMeta) return '(no results)'
|
|
341
342
|
const groups = new Map()
|
|
342
|
-
|
|
343
|
+
// Seed selected sessions so an entirely query-filtered session still
|
|
344
|
+
// reports its real activity span and filtering status.
|
|
345
|
+
if (hasSessionMeta) {
|
|
346
|
+
for (const sid of sessionMeta.keys()) groups.set(sid, [])
|
|
347
|
+
}
|
|
348
|
+
for (const r of rows || []) {
|
|
343
349
|
const sid = String(r?.session_id || '').trim()
|
|
344
350
|
const key = sid || '(no session)'
|
|
345
351
|
if (!groups.has(key)) groups.set(key, [])
|
|
@@ -356,11 +362,20 @@ export function renderSessionGroupedLines(rows, { currentSessionId, recencyOrder
|
|
|
356
362
|
for (const [sid, groupRows] of groups) {
|
|
357
363
|
const mark = current && sid === current ? ' (current)' : ''
|
|
358
364
|
const label = sid === '(no session)' ? sid : `session ${shortSessionLabel(sid)}`
|
|
365
|
+
const meta = sessionMeta?.get?.(sid)
|
|
359
366
|
const tsAll = collectGroupTs(groupRows)
|
|
360
|
-
const
|
|
361
|
-
|
|
367
|
+
const minTs = Number(meta?.minTs)
|
|
368
|
+
const maxTs = Number(meta?.maxTs)
|
|
369
|
+
const hasActivitySpan = Number.isFinite(minTs) && Number.isFinite(maxTs)
|
|
370
|
+
const suffix = hasActivitySpan
|
|
371
|
+
? ` ${spanHeaderSuffix(minTs, maxTs, groupRows.length)}`
|
|
372
|
+
: tsAll.length
|
|
373
|
+
? ` ${spanHeaderSuffix(Math.min(...tsAll), Math.max(...tsAll), groupRows.length)}`
|
|
362
374
|
: ` (${groupRows.length} entries)`
|
|
363
|
-
|
|
375
|
+
const filterNote = meta?.queryFiltered
|
|
376
|
+
? ` · query-filtered ${meta.shownCount}/${meta.fetchedCount} rows`
|
|
377
|
+
: ''
|
|
378
|
+
parts.push(`## ${label}${mark}${suffix}${filterNote}`)
|
|
364
379
|
const bodyStr = renderEntryLines(groupRows, { recencyOrder })
|
|
365
380
|
const bodyLines = bodyStr === '(no results)'
|
|
366
381
|
? []
|
|
@@ -8,7 +8,7 @@ export const TOOL_DEFS = [
|
|
|
8
8
|
name: 'memory',
|
|
9
9
|
title: 'Memory Cycle',
|
|
10
10
|
annotations: { title: 'Memory Cycle', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
11
|
-
description: 'Core-memory mutation and status; use recall for retrieval. Store durable rules/preferences/facts, one
|
|
11
|
+
description: 'Core-memory mutation and status; use recall for retrieval. Store durable rules/preferences/facts, one compact ENGLISH clause each — never transient task state. add requires project_id+category+summary; edit works by id alone.',
|
|
12
12
|
inputSchema: {
|
|
13
13
|
type: 'object',
|
|
14
14
|
properties: {
|
|
@@ -16,7 +16,7 @@ export const TOOL_DEFS = [
|
|
|
16
16
|
op: { type: 'string', enum: ['add','edit','delete','list','candidates','promote','dismiss'], description: 'Mutation op. candidates/promote/dismiss drive core-memory proposal approval.' },
|
|
17
17
|
id: { type: 'number', description: 'Exact memory id.' },
|
|
18
18
|
element: { type: 'string', maxLength: 40, description: 'Memory key/title. Defaults to the first 40 chars of summary. Max 40 chars.' },
|
|
19
|
-
summary: { type: 'string', maxLength: 100, description: 'Memory content: one short
|
|
19
|
+
summary: { type: 'string', maxLength: 100, description: 'Memory content: one short English clause, max 100 chars.' },
|
|
20
20
|
category: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'], description: 'Category.' },
|
|
21
21
|
status: { type: 'string', enum: ['pending','active','archived'], description: 'Lifecycle status.' },
|
|
22
22
|
limit: { type: 'number', description: 'Max rows/items.' },
|