mixdog 0.9.24 → 0.9.25

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.
@@ -11,13 +11,17 @@ function __mixdogMemoryLog(...args) {
11
11
  // {{CORE_REVIEW}} block for defaults/cycle3-review-prompt.md, then asks the
12
12
  // maintenance-preset LLM for one verdict per id. By default Cycle3 performs
13
13
  // conservative cleanup: safe compression updates and strict duplicate merges
14
- // are applied, while deletes stay proposals unless explicitly confirmed.
14
+ // are applied. Deletes now also apply in conservative mode when the model
15
+ // tags the entry as clear junk (a whitelisted junk reason) and — for
16
+ // redundant-with-default reasons — the text actually echoes a current rule,
17
+ // bounded by a per-run delete cap so a run keeps a safety margin instead of
18
+ // nuking the whole set. Unreasoned / non-junk deletes still require APPLY.
15
19
  //
16
20
  // Verdict line grammar (mirrors parseUnifiedFormat in memory-cycle2.mjs):
17
21
  // <id>|keep
18
22
  // <id>|update|<element>|<summary>
19
23
  // <id>|merge|<target_id>|<source_ids_csv>
20
- // <id>|delete
24
+ // <id>|delete|<reason>
21
25
 
22
26
  import { existsSync, readFileSync } from 'fs'
23
27
  import { join } from 'path'
@@ -136,6 +140,58 @@ function isStrictDuplicate(a, b) {
136
140
  return sim >= 0.78
137
141
  }
138
142
 
143
+ // Whitelisted delete reasons that conservative mode may auto-apply. These are
144
+ // the "clear junk" classes: a copy of a built-in/default rule, a bare
145
+ // restatement, an obsolete/already-implemented decision, or a past-event log.
146
+ // Anything outside this set (or a bare `delete` with no reason) stays held for
147
+ // APPLY CYCLE3 so genuinely durable rules are never removed unattended.
148
+ const SAFE_DELETE_REASONS = new Set([
149
+ 'duplicate', 'dup', 'duplicate_of_default', 'default', 'redundant',
150
+ 'restatement', 'restate', 'restates_default',
151
+ 'obsolete', 'implemented', 'done', 'completed', 'resolved',
152
+ 'superseded_decision', 'stale', 'past_event', 'event_log', 'log',
153
+ ])
154
+ // Reasons that claim redundancy with a built-in/default rule — these demand
155
+ // corroboration (the core text must actually echo the current rules digest)
156
+ // before a conservative auto-delete, so a mislabelled durable rule survives.
157
+ const DEFAULT_ECHO_REASONS = new Set([
158
+ 'duplicate', 'dup', 'duplicate_of_default', 'default', 'redundant',
159
+ 'restatement', 'restate', 'restates_default',
160
+ ])
161
+
162
+ function normalizeDeleteReason(reason) {
163
+ return String(reason ?? '')
164
+ .toLowerCase()
165
+ .trim()
166
+ .replace(/[^a-z0-9]+/g, '_')
167
+ .replace(/^_+|_+$/g, '')
168
+ }
169
+
170
+ // Max trigram similarity of `text` against any substantive line of the current
171
+ // rules digest — how strongly a core entry echoes a built-in rule.
172
+ function digestRedundancy(text, rulesDigest) {
173
+ if (!text || !rulesDigest) return 0
174
+ const lines = String(rulesDigest).split('\n').map(l => l.trim()).filter(l => l.length >= 12)
175
+ let best = 0
176
+ for (const line of lines) {
177
+ const d = charDice(text, line)
178
+ if (d > best) best = d
179
+ if (best >= 0.9) break
180
+ }
181
+ return best
182
+ }
183
+
184
+ function isSafeConservativeDelete(core, action, rulesDigest) {
185
+ const reason = normalizeDeleteReason(action?.reason)
186
+ if (!reason) return { ok: false, reason: 'delete needs a junk reason → APPLY CYCLE3' }
187
+ if (!SAFE_DELETE_REASONS.has(reason)) return { ok: false, reason: `delete reason "${reason}" not in safe set → APPLY CYCLE3` }
188
+ if (DEFAULT_ECHO_REASONS.has(reason)) {
189
+ const red = digestRedundancy(coreText(core), rulesDigest)
190
+ if (red < 0.5) return { ok: false, reason: `not redundant with defaults (sim=${red.toFixed(2)})` }
191
+ }
192
+ return { ok: true, reason }
193
+ }
194
+
139
195
  function formatRelatedRow(r) {
140
196
  const tag = r.project_id ? r.project_id : 'COMMON'
141
197
  const stat = r.status ? `[${r.status}]` : '[?]'
@@ -206,7 +262,8 @@ function parseVerdicts(raw, idSet) {
206
262
  }
207
263
  actions.push({ id, verb: 'merge', targetId, sourceIds })
208
264
  } else if (verb === 'delete') {
209
- actions.push({ id, verb: 'delete' })
265
+ const reason = (parts[2] ?? '').trim()
266
+ actions.push({ id, verb: 'delete', reason: reason || null })
210
267
  } else if (verb === 'superseded' || verb === 'supersede') {
211
268
  // Require newer-id proof: `id|superseded|<newer_id>`. Without a valid
212
269
  // newer active core id the supersession has no evidence → drop to keep.
@@ -553,6 +610,10 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
553
610
  const held = { updated: 0, merged: 0, deleted: 0, superseded: 0 }
554
611
  const details = []
555
612
  const touched = new Set() // ids already acted on this cycle — avoid double action
613
+ // Safety margin: even when every junk verdict is legitimate, a single
614
+ // conservative run applies at most this many outright deletes and holds the
615
+ // rest for the next pass. Prevents an over-eager model from clearing the set.
616
+ const conservativeDeleteCap = Math.max(1, Math.ceil(cores.length * 0.35))
556
617
 
557
618
  // Core-store edit/delete calls are the mutation unit; checkpoints sit before
558
619
  // each action/source and after each awaited unit, not inside one file-store write.
@@ -596,19 +657,28 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
596
657
  }
597
658
  if (a.verb === 'delete') {
598
659
  proposed.deleted++
599
- if (!confirmed) {
660
+ // confirmed → always delete. conservative → delete only clear junk (safe
661
+ // reason, corroborated for redundant-with-default), capped per run.
662
+ // proposal → always hold.
663
+ let applyDelete = confirmed
664
+ let holdReason = 'proposal mode'
665
+ let safeReason = null
666
+ if (!confirmed && conservative) {
667
+ const safeDel = isSafeConservativeDelete(coreById.get(a.id), a, rulesDigest)
668
+ if (!safeDel.ok) holdReason = safeDel.reason
669
+ else if (deleted >= conservativeDeleteCap) holdReason = `conservative delete cap ${conservativeDeleteCap} reached`
670
+ else { applyDelete = true; safeReason = safeDel.reason }
671
+ }
672
+ if (!applyDelete) {
600
673
  held.deleted++
601
- details.push({
602
- id: a.id, verb: 'delete', applied: false, held: true,
603
- reason: conservative ? 'delete requires APPLY CYCLE3' : 'proposal mode',
604
- })
674
+ details.push({ id: a.id, verb: 'delete', applied: false, held: true, reason: holdReason })
605
675
  touched.add(a.id)
606
676
  continue
607
677
  }
608
678
  try {
609
679
  await deleteCore(dataDir, a.id)
610
680
  deleted++
611
- details.push({ id: a.id, verb: 'delete', applied: true })
681
+ details.push({ id: a.id, verb: 'delete', applied: true, reason: safeReason || 'confirmed' })
612
682
  touched.add(a.id)
613
683
  } catch (err) {
614
684
  if (signal?.aborted) throw signal.reason ?? err
@@ -22810,6 +22810,15 @@ function createToolCardResults({
22810
22810
  }
22811
22811
 
22812
22812
  // src/tui/engine/agent-job-feed.mjs
22813
+ function parseInboundImagePaths(raw) {
22814
+ if (typeof raw !== "string" || !raw) return [];
22815
+ try {
22816
+ const arr = JSON.parse(raw);
22817
+ return Array.isArray(arr) ? arr.filter((p) => typeof p === "string" && p.length > 0) : [];
22818
+ } catch {
22819
+ return [];
22820
+ }
22821
+ }
22813
22822
  function createAgentJobFeed({
22814
22823
  runtime,
22815
22824
  getState,
@@ -22896,13 +22905,38 @@ function createAgentJobFeed({
22896
22905
  set(agentStatusState({ force: true }));
22897
22906
  }
22898
22907
  const modelContent = String(delivery.modelContent ?? delivery.displayText ?? text).trim();
22899
- if (!modelContent) return true;
22900
- enqueue(modelContent, {
22908
+ const imagePaths = parseInboundImagePaths(event?.meta?.image_paths);
22909
+ if (!modelContent && imagePaths.length === 0) return true;
22910
+ const enqueueOpts = {
22901
22911
  mode: "task-notification",
22902
22912
  priority: "next",
22903
22913
  key: notificationKey || void 0,
22904
22914
  displayText: delivery.displayText || text
22905
- });
22915
+ };
22916
+ if (imagePaths.length > 0) {
22917
+ void (async () => {
22918
+ if (getDisposed()) return;
22919
+ const parts = [];
22920
+ if (modelContent) parts.push({ type: "text", text: modelContent });
22921
+ for (const p of imagePaths) {
22922
+ let att = null;
22923
+ try {
22924
+ att = await readImageAttachmentFromPath(p);
22925
+ } catch {
22926
+ att = null;
22927
+ }
22928
+ if (!att) continue;
22929
+ if (att.metadataText) parts.push({ type: "text", text: att.metadataText });
22930
+ parts.push({ type: "image", data: att.content, mimeType: att.mediaType || "image/png" });
22931
+ }
22932
+ if (getDisposed()) return;
22933
+ const hasImage = parts.some((part) => part.type === "image");
22934
+ if (!hasImage && !modelContent) return;
22935
+ enqueue(hasImage ? parts : modelContent, enqueueOpts);
22936
+ })();
22937
+ return true;
22938
+ }
22939
+ enqueue(modelContent, enqueueOpts);
22906
22940
  return true;
22907
22941
  });
22908
22942
  }
@@ -23,6 +23,20 @@ import {
23
23
  notificationQueueKey,
24
24
  resolveTuiRuntimeNotificationDelivery,
25
25
  } from './notification-plan.mjs';
26
+ import { readImageAttachmentFromPath } from '../paste-attachments.mjs';
27
+
28
+ // Channel inbound images arrive as a JSON-array-of-paths meta value (stringified
29
+ // across the notify IPC boundary). Parse defensively; a malformed value simply
30
+ // yields no images and the notification degrades to its text body.
31
+ function parseInboundImagePaths(raw) {
32
+ if (typeof raw !== 'string' || !raw) return [];
33
+ try {
34
+ const arr = JSON.parse(raw);
35
+ return Array.isArray(arr) ? arr.filter((p) => typeof p === 'string' && p.length > 0) : [];
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
26
40
 
27
41
  export function createAgentJobFeed({
28
42
  runtime,
@@ -117,13 +131,38 @@ export function createAgentJobFeed({
117
131
  set(agentStatusState({ force: true }));
118
132
  }
119
133
  const modelContent = String(delivery.modelContent ?? delivery.displayText ?? text).trim();
120
- if (!modelContent) return true;
121
- enqueue(modelContent, {
134
+ const imagePaths = parseInboundImagePaths(event?.meta?.image_paths);
135
+ if (!modelContent && imagePaths.length === 0) return true;
136
+ const enqueueOpts = {
122
137
  mode: 'task-notification',
123
138
  priority: 'next',
124
139
  key: notificationKey || undefined,
125
140
  displayText: delivery.displayText || text,
126
- });
141
+ };
142
+ if (imagePaths.length > 0) {
143
+ // Read each downloaded image into a real image content block so the
144
+ // channel turn is vision-visible. Async, but the notification is
145
+ // already "handled" (return true) — the enqueue lands on resolve.
146
+ // Any unreadable path is skipped; if none load, fall back to text.
147
+ void (async () => {
148
+ if (getDisposed()) return;
149
+ const parts = [];
150
+ if (modelContent) parts.push({ type: 'text', text: modelContent });
151
+ for (const p of imagePaths) {
152
+ let att = null;
153
+ try { att = await readImageAttachmentFromPath(p); } catch { att = null; }
154
+ if (!att) continue;
155
+ if (att.metadataText) parts.push({ type: 'text', text: att.metadataText });
156
+ parts.push({ type: 'image', data: att.content, mimeType: att.mediaType || 'image/png' });
157
+ }
158
+ if (getDisposed()) return;
159
+ const hasImage = parts.some((part) => part.type === 'image');
160
+ if (!hasImage && !modelContent) return;
161
+ enqueue(hasImage ? parts : modelContent, enqueueOpts);
162
+ })();
163
+ return true;
164
+ }
165
+ enqueue(modelContent, enqueueOpts);
127
166
  return true;
128
167
  });
129
168
  }
@@ -7,44 +7,35 @@ agents: worker, heavy-worker, reviewer, debugger, maintainer
7
7
 
8
8
  # Default Workflow
9
9
 
10
- HARD APPROVAL GATE — before the user gives an explicit go-ahead ("do it",
11
- "proceed", "ㄱㄱ"), NO edits, apply_patch, state-changing shell commands, or
12
- agent spawns. Read-only exploration only. Agreeing with a diagnosis or
13
- pointing out a problem is NOT approval.
10
+ HARD APPROVAL GATE — no execution (changes, state mutations, delegation)
11
+ before an explicit go-ahead ("do it", "proceed", "ㄱㄱ"); read-only
12
+ exploration only. Diagnosis agreement or problem-pointing is NOT approval.
14
13
 
15
- Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
16
- (routing applies only AFTER approval)
17
- - Lead directly: simple 1–2 step work, plus coordination, pre-planning, config
18
- changes, and final git deployment.
19
- - Worker: implementation that takes several steps. Heavy Worker:
20
- high-complexity, multi-step implementation.
21
- - Reviewer: verify implementation scopes (diff, regressions, missing checks).
22
- - Debugger: very high complexity, or when root-causing has already failed at
23
- least once.
14
+ Lead supervises: delegates, coordinates, judges, decides. Route by complexity
15
+ (after approval):
16
+ - Lead directly: simple 1–2 step work, coordination, config, git deployment.
17
+ - Worker: multi-step implementation. Heavy Worker: high-complexity scopes.
18
+ - Reviewer: verify implementation scopes. Debugger: very high complexity, or
19
+ root-causing already failed once.
24
20
 
25
- 1. Plan — discuss the request with the user, form a plan, wait for approval.
26
- Only an explicit go-ahead ("do it", "proceed") is approval; agreeing with
27
- a diagnosis or pointing out a problem is NOT execution approval.
28
- 2. Delegate split into the maximum number of independent scopes.
29
- - PARALLEL across independent scopes by default (implementation, analysis,
30
- review, debugging alike); spawn every scope in the SAME turn. Shared or
31
- cross-cutting code does NOT justify merging split per path and verify
32
- the shared parts yourself. The only single scope is a genuinely
33
- inseparable dependency; then state it.
34
- - SEQUENTIAL within a single complex scope: ordered steps with a
35
- build/test-green gate between them, not one shot.
36
- - Write briefs per the Lead brief contract.
37
- - After spawning async agents, END THE TURN — no polling, guessing, or
38
- dependent work until the completion notification resumes you.
39
- 3. Review pair one reviewer 1:1 with each implementation scope, spawned in the
40
- same turn, never deferred or batched; wait for its result. Fact-check agent
41
- responses and cross-check implementation and review results yourself before
42
- acting. Send fixes back to the original scope and loop verify -> fix ->
43
- re-verify until clean. Skip review only for simple, low-risk tasks. If the
44
- user asks for debugging, or a bug survives 2+ fix cycles, have the debugger
45
- investigate first instead of another fix round.
46
- 4. Report — synthesize outcome + key evidence (never forward raw agent
47
- output), state the final state, and ask about ship/deploy when relevant.
48
- Prepare deploy/build/commit only after user feedback with no issues.
21
+ 1. Plan — present a draft plan before ANY implementation; if not approved,
22
+ revise and re-present (ping-pong) until an explicit go-ahead.
23
+ 2. Delegate split into the maximum independent scopes; spawn all in the
24
+ SAME turn (parallel by default; sequential steps only inside one complex
25
+ scope, gated build/test-green). Shared/cross-cutting code does NOT justify
26
+ merging scopes split per path, verify shared parts yourself; a genuinely
27
+ inseparable single scope must be stated. Briefs per the Lead brief
28
+ contract. After spawning async agents, END THE TURN.
29
+ 3. Review pair one reviewer 1:1 per implementation scope, same turn.
30
+ Cross-check agent results yourself; send fixes back to the original scope
31
+ and loop fix -> re-verify until clean. Skip only for simple low-risk work.
32
+ Debugger first when the user asks for debugging or a bug survives 2+ fix
33
+ cycles.
34
+ On each agent report: tell the user what was received (scope + verdict)
35
+ and how work proceeds, marked in-progress never as a conclusion.
36
+ 4. Report final report briefs the whole work vs the approved plan and the
37
+ verified result, distinct from interim updates. Never forward raw agent
38
+ output. Ask about ship/deploy when relevant; deploy/build/commit only
39
+ after user feedback with no issues.
49
40
 
50
- On any major change or direction shift mid-work, pause and re-consult the user.
41
+ On major direction shifts mid-work, pause and re-consult the user.
@@ -7,27 +7,22 @@ agents:
7
7
 
8
8
  # Solo Workflow
9
9
 
10
- HARD APPROVAL GATE — before the user gives an explicit go-ahead ("do it",
11
- "proceed", "ㄱㄱ"), NO edits, apply_patch, or state-changing shell commands.
12
- Read-only exploration only. Agreeing with a diagnosis or pointing out a
13
- problem is NOT approval.
10
+ HARD APPROVAL GATE — no execution (changes, state mutations) before an
11
+ explicit go-ahead ("do it", "proceed", "ㄱㄱ"); read-only exploration only.
12
+ Diagnosis agreement or problem-pointing is NOT approval.
14
13
 
15
- 1. Plan — Lead discusses the request with the user, forms a plan, and waits for
16
- approval before execution.
17
- Only an explicit go-ahead is approval; diagnosis agreement is not. When
18
- ambiguous, restate the plan and ask before executing.
19
- 2. Execute Lead performs all implementation, research, debugging, review, and
20
- verification work directly. Delegation to any agent is forbidden.
21
- 3. VerifyLead checks the result directly, fixes issues directly, and repeats
22
- until the work is clean or a blocker must be reported.
23
- 4. Report Lead summarizes the final state, verification result, and any
24
- remaining risk or requested next step. Only after user feedback with no
25
- issues prepare deploy/build/commit.
14
+ 1. Plan — present a draft plan before ANY implementation; if not approved,
15
+ revise and re-present (ping-pong) until an explicit go-ahead. When
16
+ ambiguous, restate the plan and ask.
17
+ 2. Execute Lead does all work directly; delegation forbidden. Interim
18
+ updates are marked in-progress never phrased as conclusions.
19
+ 3. Verify — check and fix directly until clean or a blocker is reported.
20
+ 4. Reportfinal report briefs the whole work vs the approved plan, the
21
+ verification result, and remaining risk/next step, distinct from interim
22
+ updates. Deploy/build/commit only after user feedback with no issues.
26
23
 
27
- On any major change or direction shift mid-work, pause and re-consult the user
28
- before continuing.
24
+ On major direction shifts mid-work, pause and re-consult the user.
29
25
 
30
26
  Delegation rule:
31
- - Do not delegate, spawn, send, or ask any agent to perform work.
32
- - Ignore any available-agent section while this workflow is active; all work is
33
- handled by Lead directly.
27
+ - Never delegate, spawn, send, or ask any agent to perform work; ignore any
28
+ available-agent section while this workflow is active.