@ddtcorex/dsh-maestro-memory 1.1.0 → 1.2.0

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.
@@ -373,6 +373,22 @@ export class MaestroMemoryStore {
373
373
  const t = normalizeTarget(target)
374
374
  let content = String(entry ?? '').trim()
375
375
  if (!content) return { ok: false, error: 'empty content' }
376
+ // Guard against idle-loop spam: reject trivial placeholder entries that carry
377
+ // no new information (e.g. "Idle", "Idle — no change"). The snapshot
378
+ // discipline now says to skip when idle, but keep a code-level guard as
379
+ // defense-in-depth so a misbehaving model cannot flood daily/project.
380
+ const trivialStripped = content.replace(/^\[.*?\]\s*/g, '').trim().toLowerCase()
381
+ if (
382
+ trivialStripped.startsWith('idle') ||
383
+ trivialStripped.startsWith('no change') ||
384
+ trivialStripped.startsWith('awaiting') ||
385
+ trivialStripped.startsWith('waiting')
386
+ ) {
387
+ // Idle/placeholder entries carry no new information; treat as duplicate/no-op
388
+ // rather than error so the caller can proceed without retrying. Cap at 150
389
+ // chars so legitimate long entries that merely mention "idle" are not blocked.
390
+ if (trivialStripped.length < 150) return { ok: true, duplicate: true }
391
+ }
376
392
  // Desensitize by default (opt-out via {desensitize:false} for tests/internal)
377
393
  const doDesensitize = opts.desensitize !== false
378
394
  if (doDesensitize) {
@@ -99,12 +99,15 @@ export function renderSnapshot(
99
99
 
100
100
  // Recent daily slot (512B) — last 2 days' newest entries
101
101
  // Keeps recent context without exceeding cap; full logs remain query-only.
102
+ // Use local calendar (matching store.todayStamp) to avoid UTC/local drift near midnight.
102
103
  try {
103
104
  const recentDaily: string[] = []
104
105
  for (let i = 0; i < 2; i++) {
105
106
  const d = new Date()
106
107
  d.setDate(d.getDate() - i)
107
- const ds = d.toISOString().slice(0, 10)
108
+ const mm = String(d.getMonth() + 1).padStart(2, '0')
109
+ const dd = String(d.getDate()).padStart(2, '0')
110
+ const ds = `${d.getFullYear()}-${mm}-${dd}`
108
111
  try {
109
112
  const list: string[] = store.list('daily', undefined, { date: ds } as any)
110
113
  if (list.length) recentDaily.push(list[list.length - 1])
@@ -117,8 +120,9 @@ export function renderSnapshot(
117
120
  } catch {}
118
121
 
119
122
  // End-of-turn discipline note — verbatim contract (hardened: exactly once, always last)
123
+ // Conditional to avoid idle loops: only when turn produced meaningful progress.
120
124
  const discipline =
121
- `---\nEnd of every turn you must: 1. Write daily+project via memory entries (daily+project in one call) 2. Check dtodo list (bounded, max 8)\nFor important project decisions (convention, incident, infra) use memory_suggest target=key with reason, not memory add.`
125
+ `---\nEnd of every turn if this turn produced meaningful progress (code, decisions, learnings, or next steps): 1. Write daily+project via memory entries (daily+project in one call, skip if idle/waiting or no new information — never write entries containing only 'Idle' or placeholders) 2. Check dtodo list only if relevant (bounded, max 8)\nFor important project decisions (convention, incident, infra) use memory_suggest target=key with reason, not memory add.`
122
126
  // Defensive: strip any pre-existing discipline entry (should never occur — parts is fresh per call)
123
127
  // then append exactly once so the note is guaranteed last even for empty stores or repeated calls.
124
128
  const deduped = parts.filter((p) => p !== discipline)