mixdog 0.7.11 → 0.7.12

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.
Files changed (41) hide show
  1. package/.claude-plugin/marketplace.json +5 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +193 -249
  4. package/bin/statusline-launcher.mjs +5 -1
  5. package/bin/statusline-lib.mjs +14 -6
  6. package/bin/statusline.mjs +14 -6
  7. package/hooks/lib/settings-loader.cjs +4 -3
  8. package/hooks/pre-tool-subagent.cjs +7 -2
  9. package/hooks/session-start.cjs +52 -24
  10. package/lib/mixdog-debug.cjs +163 -0
  11. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  12. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  13. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  14. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  15. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  16. package/package.json +1 -1
  17. package/scripts/builtin-utils-smoke.mjs +14 -8
  18. package/scripts/bump.mjs +80 -0
  19. package/scripts/doctor.mjs +8 -3
  20. package/scripts/mutation-io-smoke.mjs +17 -1
  21. package/scripts/permission-eval-smoke.mjs +18 -1
  22. package/scripts/statusline-launcher-smoke.mjs +2 -2
  23. package/scripts/webhook-selfheal-smoke.mjs +1 -3
  24. package/server-main.mjs +57 -3
  25. package/setup/install.mjs +574 -574
  26. package/setup/setup-server.mjs +10 -2
  27. package/setup/setup.html +43 -8
  28. package/src/agent/orchestrator/providers/openai-oauth.mjs +9 -2
  29. package/src/agent/orchestrator/providers/openai-ws.mjs +23 -0
  30. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +29 -8
  31. package/src/agent/orchestrator/tools/graph-manifest.json +11 -11
  32. package/src/agent/orchestrator/tools/patch-manifest.json +11 -11
  33. package/src/channels/index.mjs +27 -8
  34. package/src/channels/lib/event-queue.mjs +24 -1
  35. package/src/channels/lib/hook-pipe-server.mjs +21 -8
  36. package/src/channels/lib/webhook.mjs +142 -20
  37. package/src/memory/lib/memory-cycle1.mjs +7 -3
  38. package/src/memory/lib/memory-recall-store.mjs +27 -10
  39. package/src/search/lib/backends/openai-oauth.mjs +6 -2
  40. package/src/search/lib/cache.mjs +55 -7
  41. package/scripts/test-config-rmw-restore.mjs +0 -122
@@ -599,9 +599,17 @@ function _modelFromConfiguredId(id, provider) {
599
599
  }
600
600
 
601
601
  function _familyFromModelId(id) {
602
- const claude = String(id || '').match(/^claude-(opus|sonnet|haiku)/i);
602
+ const s = String(id || '').toLowerCase();
603
+ const claude = s.match(/^claude-(opus|sonnet|haiku)/i);
603
604
  if (claude) return claude[1].toLowerCase();
604
- const gpt = String(id || '').match(/^(gpt-\d+)/i);
605
+ if (s.includes('nano')) return 'gpt-nano';
606
+ if (s.includes('mini')) return 'gpt-mini';
607
+ if (s.includes('codex')) return 'gpt-codex';
608
+ if (s.startsWith('gpt-5.5')) return 'gpt-5.5';
609
+ if (s.startsWith('gpt-5.4')) return 'gpt-5.4';
610
+ if (s.startsWith('gpt-5.2')) return 'gpt-5.2';
611
+ if (s.startsWith('gpt-5')) return 'gpt-5';
612
+ const gpt = s.match(/^(gpt-\d+(?:\.\d+)?)/i);
605
613
  if (gpt) return gpt[1].toLowerCase();
606
614
  return undefined;
607
615
  }
package/setup/setup.html CHANGED
@@ -2640,9 +2640,48 @@ const AG_EFFORT_LABEL = { none: 'None', low: 'Low', medium: 'Medium', high: 'Hig
2640
2640
  // Families that don't support fast mode even when the provider does.
2641
2641
  const AG_FAMILY_NO_FAST = new Set(['haiku', 'gpt-nano', 'gpt-codex']);
2642
2642
  const AG_FAST_PROVIDERS = new Set(['anthropic', 'anthropic-oauth', 'openai', 'openai-oauth']);
2643
+ const AG_OPENAI_DIRECT_FAST_MODEL_RE = [
2644
+ /^gpt-5\.5(?:-\d{4}|$)/,
2645
+ /^gpt-5\.4(?:-\d{4}|$)/,
2646
+ /^gpt-5\.4-mini(?:-\d{4}|$)/,
2647
+ ];
2643
2648
  let agModelList = [];
2644
2649
  const AG_ACCESS_LABELS = { full: 'Read & Write', readonly: 'Read Only', mcp: 'None' };
2645
2650
 
2651
+ function agNormalizeFastProvider(provider) {
2652
+ return provider === 'openai-api' ? 'openai' : provider;
2653
+ }
2654
+
2655
+ function agOpenAIDirectSupportsFast(modelId) {
2656
+ const id = String(modelId || '').trim();
2657
+ return AG_OPENAI_DIRECT_FAST_MODEL_RE.some(re => re.test(id));
2658
+ }
2659
+
2660
+ function agExplicitFastSupport(model) {
2661
+ const hasServiceTiers = Array.isArray(model?.serviceTiers);
2662
+ const hasSpeedTiers = Array.isArray(model?.additionalSpeedTiers);
2663
+ if (!hasServiceTiers && !hasSpeedTiers) return null;
2664
+ const serviceTiers = hasServiceTiers ? model.serviceTiers : [];
2665
+ const speedTiers = hasSpeedTiers ? model.additionalSpeedTiers : [];
2666
+ const serviceFast = serviceTiers.some(t => {
2667
+ const id = typeof t === 'string' ? t : t?.id;
2668
+ return id === 'priority' || id === 'fast';
2669
+ });
2670
+ const speedFast = speedTiers.some(t => t === 'fast' || t === 'priority');
2671
+ return serviceFast || speedFast;
2672
+ }
2673
+
2674
+ function agModelSupportsFast(provider, model) {
2675
+ const normalizedProvider = agNormalizeFastProvider(provider);
2676
+ if (!model) return false;
2677
+ const explicit = agExplicitFastSupport(model);
2678
+ if (explicit !== null) return explicit;
2679
+ if (normalizedProvider === 'openai') return agOpenAIDirectSupportsFast(model.id);
2680
+ const providerFast = AG_FAST_PROVIDERS.has(normalizedProvider);
2681
+ const familyNoFast = model?.family && AG_FAMILY_NO_FAST.has(model.family);
2682
+ return providerFast && !familyNoFast;
2683
+ }
2684
+
2646
2685
  async function loadAgentData() {
2647
2686
  const r = await fetch('/agent/config').then(r => r.json()).catch(() => ({}));
2648
2687
  agConfig = r.config || {};
@@ -2974,9 +3013,7 @@ function agUpdateEffortAndFast(provider) {
2974
3013
  effortSel.innerHTML = allowed.map(v => `<option value="${v}">${AG_EFFORT_LABEL[v] || v}</option>`).join('');
2975
3014
  }
2976
3015
 
2977
- const providerFast = AG_FAST_PROVIDERS.has(provider);
2978
- const familyNoFast = model?.family && AG_FAMILY_NO_FAST.has(model.family);
2979
- const fastAllowed = modelResolved && providerFast && !familyNoFast;
3016
+ const fastAllowed = modelResolved && agModelSupportsFast(provider, model);
2980
3017
  fastRow.style.display = fastAllowed ? 'flex' : 'none';
2981
3018
  if (!fastAllowed) document.getElementById('ag-pf-fast').classList.remove('on');
2982
3019
  }
@@ -3479,8 +3516,8 @@ async function srRenderModelPresets() {
3479
3516
  }
3480
3517
 
3481
3518
  // Reveal/hide effort + fast based on the currently selected openai model.
3482
- // Mirrors agUpdateEffortAndFast() so the rules are identical: model family
3483
- // drives effort options, AG_FAST_PROVIDERS / AG_FAMILY_NO_FAST drive fast.
3519
+ // Mirrors agUpdateEffortAndFast() so the rules are identical: provider/model
3520
+ // metadata drives effort options and Fast Mode availability.
3484
3521
  function srUpdateOpenAIEffortFast() {
3485
3522
  const body = document.getElementById('sr-model-presets-body');
3486
3523
  if (!body) return;
@@ -3505,9 +3542,7 @@ function srUpdateOpenAIEffortFast() {
3505
3542
  effortSel.style.display = '';
3506
3543
  effortSel.innerHTML = allowed.map(v => '<option value="' + v + '"' + (v === storedEffort ? ' selected' : '') + '>' + (AG_EFFORT_LABEL[v] || v) + '</option>').join('');
3507
3544
  }
3508
- const providerFast = AG_FAST_PROVIDERS.has(normalizedProvider);
3509
- const familyNoFast = model?.family && AG_FAMILY_NO_FAST.has(model.family);
3510
- const fastAllowed = !!model && providerFast && !familyNoFast;
3545
+ const fastAllowed = !!model && agModelSupportsFast(normalizedProvider, model);
3511
3546
  const fastLabel = body.querySelector('[data-fam-fast-label="openai"]');
3512
3547
  fastEl.style.display = fastAllowed ? '' : 'none';
3513
3548
  if (fastLabel) fastLabel.style.display = fastAllowed ? '' : 'none';
@@ -133,7 +133,14 @@ function _codexServiceTiers(modelInfo) {
133
133
  return Array.isArray(modelInfo?.serviceTiers) ? modelInfo.serviceTiers : [];
134
134
  }
135
135
 
136
- function _codexModelSupportsServiceTier(id, serviceTier) {
136
+ function _codexModelBlocksServiceTier(id, serviceTier) {
137
+ if (serviceTier !== 'priority') return false;
138
+ const family = _codexFamily(id);
139
+ return family === 'gpt-mini' || family === 'gpt-nano' || family === 'gpt-codex';
140
+ }
141
+
142
+ export function codexModelSupportsServiceTier(id, serviceTier) {
143
+ if (_codexModelBlocksServiceTier(id, serviceTier)) return false;
137
144
  const info = _findCachedCodexModel(id);
138
145
  if (!info) return true;
139
146
  const tiers = _codexServiceTiers(info);
@@ -530,7 +537,7 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
530
537
  // accepts on the wire: 'fast' is hard-rejected ("Unsupported
531
538
  // service_tier: fast", probed 2026-06-11). Match official Codex:
532
539
  // only send the request value when the model catalog advertises it.
533
- if (_codexModelSupportsServiceTier(model, 'priority')) {
540
+ if (codexModelSupportsServiceTier(model, 'priority')) {
534
541
  body.service_tier = 'priority';
535
542
  }
536
543
  }
@@ -16,6 +16,24 @@ import { sendViaWebSocket } from './openai-oauth-ws.mjs';
16
16
  import { buildRequestBody } from './openai-oauth.mjs';
17
17
  import { resolveProviderCacheKey } from '../smart-bridge/cache-strategy.mjs';
18
18
 
19
+ const OPENAI_DIRECT_PRIORITY_MODEL_PATTERNS = Object.freeze([
20
+ /^gpt-5\.5(?:-\d{4}|$)/,
21
+ /^gpt-5\.4(?:-\d{4}|$)/,
22
+ /^gpt-5\.4-mini(?:-\d{4}|$)/,
23
+ ]);
24
+
25
+ export function openAiDirectSupportsPriority(model) {
26
+ const id = String(model || '').trim();
27
+ return OPENAI_DIRECT_PRIORITY_MODEL_PATTERNS.some(re => re.test(id));
28
+ }
29
+
30
+ export function applyOpenAIDirectFastTier(body, model, opts) {
31
+ if (opts?.fast === true && openAiDirectSupportsPriority(model)) {
32
+ body.service_tier = 'priority';
33
+ }
34
+ return body;
35
+ }
36
+
19
37
  export class OpenAIDirectProvider {
20
38
  // input_tokens INCLUDES cached tokens (OpenAI convention). See registry.mjs.
21
39
  static inputExcludesCache = false;
@@ -38,6 +56,11 @@ export class OpenAIDirectProvider {
38
56
  const apiKey = this._ensureKey();
39
57
  const useModel = model || 'gpt-5.5';
40
58
  const body = buildRequestBody(messages, useModel, tools, sendOpts);
59
+ // Public OpenAI API priority support is documented separately from the
60
+ // Codex OAuth catalog. Keep this provider's service-tier decision local
61
+ // so gpt-5.4-mini can opt into Priority even when the Codex catalog does
62
+ // not advertise a Fast tier for its OAuth endpoint.
63
+ applyOpenAIDirectFastTier(body, useModel, opts);
41
64
  // Public Responses API supports prompt_cache_retention='24h' at no
42
65
  // extra cost (same cached_input_tokens billing as the default 5–10
43
66
  // min in-memory cache). Codex/oauth rejects the parameter, so it's
@@ -19,21 +19,35 @@ export function nativeEditMode() {
19
19
  return String(process.env.MIXDOG_EDIT_NATIVE || 'auto').toLowerCase();
20
20
  }
21
21
 
22
- export function nativeEditBinPath() {
22
+ function nativeEditBinCandidate() {
23
23
  const override = process.env.MIXDOG_EDIT_NATIVE_BIN || process.env.MIXDOG_PATCH_NATIVE_BIN;
24
- if (override) return override;
25
- if (existsSync(NATIVE_EDIT_DEFAULT_BIN)) return NATIVE_EDIT_DEFAULT_BIN;
26
- return findCachedPatchBinary(getPluginData()) || NATIVE_EDIT_DEFAULT_BIN;
24
+ if (override) return { path: override, kind: 'override' };
25
+ if (existsSync(NATIVE_EDIT_DEFAULT_BIN)) return { path: NATIVE_EDIT_DEFAULT_BIN, kind: 'local' };
26
+ const cached = findCachedPatchBinary(getPluginData());
27
+ if (cached) return { path: cached, kind: 'cached' };
28
+ return { path: NATIVE_EDIT_DEFAULT_BIN, kind: 'missing' };
29
+ }
30
+
31
+ export function nativeEditBinPath() {
32
+ return nativeEditBinCandidate().path;
27
33
  }
28
34
 
29
35
  export function nativeEditShouldAttempt({ editSnapshot, oldStr, newStr, preloadedContent, preloadedRawBuf }) {
30
36
  const mode = nativeEditMode();
31
37
  if (/^(0|false|no|off|js|legacy)$/i.test(mode)) return false;
32
- if (!existsSync(nativeEditBinPath())) return false;
38
+ const forcedNative = /^(1|true|yes|on|native)$/i.test(mode);
39
+ const candidate = nativeEditBinCandidate();
40
+ if (!existsSync(candidate.path)) return false;
41
+ // Cached release prebuilds are guaranteed valid for apply_patch, but older
42
+ // manifests (currently v0.6.5 in clean CI) predate the EDIT server protocol.
43
+ // In auto mode, native edit is only an acceleration, so require either a
44
+ // local cargo build or an explicit override. If a user forces native mode,
45
+ // still try the cached binary and surface any protocol failure.
46
+ if (candidate.kind === 'cached' && !forcedNative) return false;
33
47
  if (!snapshotCoversFullFile(editSnapshot)) return false;
34
48
  if (preloadedContent !== null || preloadedRawBuf !== null) return false;
35
49
  if (typeof oldStr !== 'string' || oldStr.length === 0 || typeof newStr !== 'string') return false;
36
- if (/^(1|true|yes|on|native)$/i.test(mode)) return true;
50
+ if (forcedNative) return true;
37
51
  // auto: the persistent server removed per-call spawn cost, so route edits to
38
52
  // native edit2 by default (B3). Same-size edits keep the JS in-place partial
39
53
  // write, which rewrites bytes in place instead of the whole file.
@@ -44,6 +58,7 @@ export function nativeEditShouldAttempt({ editSnapshot, oldStr, newStr, preloade
44
58
  }
45
59
 
46
60
  export async function runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll, signal = null }) {
61
+ const forcedNative = /^(1|true|yes|on|native)$/i.test(nativeEditMode());
47
62
  if (signal?.aborted) {
48
63
  return { ok: false, fallback: false, error: signal.reason?.message || signal.reason || 'native edit aborted' };
49
64
  }
@@ -82,8 +97,14 @@ export async function runNativeExactEdit({ fullPath, oldStr, newStr, replaceAll,
82
97
  }
83
98
  const msg = String(err?.message || err);
84
99
  // Tier misses and not-found map to a JS fallback; transport/spawn errors
85
- // also fall back so a server hiccup never blocks an edit.
86
- const fallback = /old_string (?:not found|found \d+ times)|not valid UTF-8|no exact match|not found|server/i.test(msg);
100
+ // also fall back so a server hiccup never blocks an edit. Older cached
101
+ // mixdog-patch binaries (for example the v0.6.5 release prebuilds used
102
+ // by clean CI before a local cargo build exists) support APPLY but not
103
+ // the EDIT server protocol, and answer EDIT with the APPLY parser's
104
+ // "bad header" error. In auto mode that means "native edit unavailable",
105
+ // not "the edit is invalid", so fall through to the JS editor. When the
106
+ // user explicitly forces native mode, keep surfacing the native failure.
107
+ const fallback = !forcedNative && /old_string (?:not found|found \d+ times)|not valid UTF-8|no exact match|not found|server|bad header|bad edit header/i.test(msg);
87
108
  return { ok: false, fallback, error: msg };
88
109
  }
89
110
  }
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.6.5",
2
+ "version": "0.7.12",
3
3
  "_comment": "Rewritten by .github/workflows/graph-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-graph binary on the GitHub release. A local cargo build under native/mixdog-graph/target/release always takes precedence at runtime. (v0.5.236 entries were filled manually after CI's commit step hit detached HEAD; the workflow now checks out ref: main so future releases self-update.)",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-graph-darwin-arm64",
7
- "sha256": "7016c273a07d19ca9e2f56e8fa7f273fdd40fc41bdc7fef206bf23e31a21a736"
6
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-graph-darwin-arm64",
7
+ "sha256": "75bfdd200b2f8553b72dc877ec2637208f581800083d1ee5f9caf33f87792bf7"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-graph-darwin-x64",
11
- "sha256": "d076e97da4420f49a6c726bc088a3321e2e7f6a9bfb32d39162c8c53045cfcdb"
10
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-graph-darwin-x64",
11
+ "sha256": "04742fbb4cbe09bb76943f312ee129c05814543e7bc9d37e1241fb4e65b97137"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-graph-linux-arm64",
15
- "sha256": "74754562b3c080868738c032c5b6e0e13bc53d7a5277002176b036f8d6681f39"
14
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-graph-linux-arm64",
15
+ "sha256": "4b3edcd7be1ffec7184c48fe6bc7d6bce42f2ea67d4709f44d4402e6b48564f2"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-graph-linux-x64",
19
- "sha256": "0d8e8bbdd49b18746ed3f972fc3719731a1143ee03ac9e6d86586788b0b431f8"
18
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-graph-linux-x64",
19
+ "sha256": "4394bb7884a8706dd6a4eea55f8755c76ba584cd02248863802d94acc3e1413c"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-graph-win32-x64.exe",
23
- "sha256": "1a671558e5a5f13c7429ff9987a46ad72a71e52241e200d2da820a13d7cbdae7"
22
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-graph-win32-x64.exe",
23
+ "sha256": "cbfe189d690085aee1dfd70f5c0b9c26c260d0a080914cbeb504c84510ec3a5a"
24
24
  }
25
25
  }
26
26
  }
@@ -1,26 +1,26 @@
1
1
  {
2
- "version": "0.6.5",
2
+ "version": "0.7.12",
3
3
  "_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
4
4
  "assets": {
5
5
  "darwin-arm64": {
6
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-patch-darwin-arm64",
7
- "sha256": "d37afb583cd597a9599ea9feac76a853d215ffaadc2bb2b54cf71ef28848f7ae"
6
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-patch-darwin-arm64",
7
+ "sha256": "836a0b60a443b0a6a8c1bbe24d15a79ed70ee92c2f0fbc05374c4e9ed2536415"
8
8
  },
9
9
  "darwin-x64": {
10
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-patch-darwin-x64",
11
- "sha256": "bc4dad6a7fdc2fcdfceb850bcaae43757ae304740ca503be955eb57cf8cd07e3"
10
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-patch-darwin-x64",
11
+ "sha256": "cab10c4e1e8b72d3958241dffdff764712ed74f4861d105bafa8258961215c98"
12
12
  },
13
13
  "linux-arm64": {
14
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-patch-linux-arm64",
15
- "sha256": "ebe3fd45aaed0f383f7b7940733fd141dd0b93c4c44e08fbffab10a3e43b788c"
14
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-patch-linux-arm64",
15
+ "sha256": "a90c32ce3417a7d853f2723f82f3613cf2cd030fe885cf710cfc9f8e4b193264"
16
16
  },
17
17
  "linux-x64": {
18
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-patch-linux-x64",
19
- "sha256": "627289a3b5c0156bc299d4ff7563e4e4536a5a5bf6329e7c9151e811c795928f"
18
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-patch-linux-x64",
19
+ "sha256": "0fea40ab98acd35bfb47515756024d1882a2abbaddce8a0b51642d20ac405577"
20
20
  },
21
21
  "win32-x64": {
22
- "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.6.5/mixdog-patch-win32-x64.exe",
23
- "sha256": "4282546719a8c149597d3afe21eaf31aab3079a371876bf7f91ff0bccbe316b2"
22
+ "url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.12/mixdog-patch-win32-x64.exe",
23
+ "sha256": "f8a74fb9bb7bf333fa441b800b76b28d83a2a7b4795e4a769122faf47a59d1f1"
24
24
  }
25
25
  }
26
26
  }
@@ -183,10 +183,18 @@ const _bootLogEarly = path.join(
183
183
  process.env.CLAUDE_PLUGIN_DATA || path.join(os.tmpdir(), "mixdog"),
184
184
  "boot.log"
185
185
  );
186
+ const {
187
+ isMixdogDebugEnabled: isMixdogDebug,
188
+ pruneStalePluginDataLogSiblings,
189
+ appendSessionStartCriticalLog,
190
+ DEFAULT_STALE_LOG_SIBLING_MAX,
191
+ } = _require("../../lib/mixdog-debug.cjs");
186
192
  // One-shot log rotation at worker boot (10 MB threshold, .1 suffix overwrite).
187
- try { if (fs.statSync(_bootLogEarly).size > 10 * 1024 * 1024) fs.renameSync(_bootLogEarly, _bootLogEarly + '.1') } catch {}
188
- fs.appendFileSync(_bootLogEarly, `[${localTimestamp()}] bootstrap start pid=${process.pid}
193
+ if (isMixdogDebug()) {
194
+ try { if (fs.statSync(_bootLogEarly).size > 10 * 1024 * 1024) fs.renameSync(_bootLogEarly, _bootLogEarly + '.1') } catch {}
195
+ fs.appendFileSync(_bootLogEarly, `[${localTimestamp()}] bootstrap start pid=${process.pid}
189
196
  `);
197
+ }
190
198
  const _bootLog = path.join(DATA_DIR, "boot.log");
191
199
  let config = loadConfig();
192
200
  let backend = createBackend(config);
@@ -238,6 +246,11 @@ try {
238
246
  try { if (_now - fs.statSync(_p).mtimeMs > _STALE_SESSION_TTL_MS) fs.unlinkSync(_p); } catch {}
239
247
  }
240
248
  } catch {}
249
+ // Count-based cap: drop oldest *.log siblings when plugin-data accumulates
250
+ // hundreds of per-process files (doctor warns above 300).
251
+ try {
252
+ pruneStalePluginDataLogSiblings(DATA_DIR, DEFAULT_STALE_LOG_SIBLING_MAX);
253
+ } catch {}
241
254
 
242
255
  // ── Buffered drop-trace writer (channels/index) ──────────────────────────────
243
256
  // Flushes every 1 s OR when buffer reaches 64 KB — whichever fires first.
@@ -2030,8 +2043,12 @@ backend.onInteraction = (interaction) => {
2030
2043
  const [, uuid, action] = match;
2031
2044
  const access = config.access;
2032
2045
  if (!access) {
2033
- fs.appendFileSync(_bootLog, `[${localTimestamp()}] perm interaction dropped: no access config
2034
- `);
2046
+ const _permDropLine = `[${localTimestamp()}] perm interaction dropped: no access config\n`;
2047
+ if (isMixdogDebug()) {
2048
+ fs.appendFileSync(_bootLog, _permDropLine);
2049
+ } else {
2050
+ appendSessionStartCriticalLog(DATA_DIR, `[channels] ${_permDropLine}`);
2051
+ }
2035
2052
  return;
2036
2053
  }
2037
2054
  if (access.allowFrom?.length > 0 && !access.allowFrom.includes(interaction.userId)) {
@@ -3016,12 +3033,14 @@ async function stop() {
3016
3033
  return false;
3017
3034
  };
3018
3035
  _channelFlagDetected = detectChannelFlag();
3019
- fs.appendFileSync(_bootLog, `[${localTimestamp()}] channelFlag: ${_channelFlagDetected}
3020
- `);
3036
+ if (isMixdogDebug()) {
3037
+ fs.appendFileSync(_bootLog, `[${localTimestamp()}] channelFlag: ${_channelFlagDetected}\n`);
3038
+ if (_channelFlagDetected) {
3039
+ fs.appendFileSync(_bootLog, `[${localTimestamp()}] channel mode detected — bridge auto-activated\n`);
3040
+ }
3041
+ }
3021
3042
  if (_channelFlagDetected) {
3022
3043
  channelBridgeActive = true;
3023
- fs.appendFileSync(_bootLog, `[${localTimestamp()}] channel mode detected \u2014 bridge auto-activated
3024
- `);
3025
3044
  }
3026
3045
  writeBridgeState(channelBridgeActive);
3027
3046
  const previousOwner = readActiveInstance();
@@ -1,4 +1,4 @@
1
- import { readdirSync, readFileSync, existsSync as fsExistsSync } from "fs";
1
+ import { readdirSync, readFileSync, existsSync as fsExistsSync, statSync, unlinkSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { DATA_DIR } from "./config.mjs";
4
4
  import { ensureDir } from "./state-file.mjs";
@@ -7,6 +7,27 @@ import { renameWithRetrySync, writeJsonAtomicSync } from "../../shared/atomic-fi
7
7
  const QUEUE_DIR = join(DATA_DIR, "events", "queue");
8
8
  const IN_PROGRESS_DIR = join(DATA_DIR, "events", "in-progress");
9
9
  const PROCESSED_DIR = join(DATA_DIR, "events", "processed");
10
+ const PROCESSED_DIR_MAX_ENTRIES = 200;
11
+ function pruneProcessedDir() {
12
+ try {
13
+ if (!fsExistsSync(PROCESSED_DIR)) return;
14
+ const names = readdirSync(PROCESSED_DIR);
15
+ if (names.length <= PROCESSED_DIR_MAX_ENTRIES) return;
16
+ const ranked = [];
17
+ for (const name of names) {
18
+ try {
19
+ const st = statSync(join(PROCESSED_DIR, name));
20
+ ranked.push({ name, mtime: st.mtimeMs });
21
+ } catch {}
22
+ }
23
+ ranked.sort((a, b) => b.mtime - a.mtime);
24
+ for (let i = PROCESSED_DIR_MAX_ENTRIES; i < ranked.length; i++) {
25
+ try {
26
+ unlinkSync(join(PROCESSED_DIR, ranked[i].name));
27
+ } catch {}
28
+ }
29
+ } catch {}
30
+ }
10
31
  function finiteInt(value, { min, max, def }) {
11
32
  const n = Number(value);
12
33
  if (!Number.isFinite(n)) return def;
@@ -290,6 +311,7 @@ ${p.item.prompt}`).join("\n\n")}`;
290
311
  const fromQueue = join(QUEUE_DIR, file);
291
312
  const src = this.existsSync(fromInProgress) ? fromInProgress : fromQueue;
292
313
  renameWithRetrySync(src, join(PROCESSED_DIR, `${status}-${file}`));
314
+ pruneProcessedDir();
293
315
  } catch {
294
316
  }
295
317
  }
@@ -297,6 +319,7 @@ ${p.item.prompt}`).join("\n\n")}`;
297
319
  try {
298
320
  ensureDir(PROCESSED_DIR);
299
321
  renameWithRetrySync(join(IN_PROGRESS_DIR, file), join(PROCESSED_DIR, `${status}-${file}`));
322
+ pruneProcessedDir();
300
323
  } catch {
301
324
  }
302
325
  }
@@ -24,6 +24,11 @@ import { request as httpsRequest } from 'node:https'
24
24
  import { createRequire } from 'node:module'
25
25
 
26
26
  const moduleRequire = createRequire(import.meta.url)
27
+ const {
28
+ isMixdogDebugEnabled,
29
+ pruneStalePluginDataLogSiblings,
30
+ DEFAULT_STALE_LOG_SIBLING_MAX,
31
+ } = moduleRequire('../../../lib/mixdog-debug.cjs')
27
32
 
28
33
  // IPC transport path. Windows uses a named pipe (`\\.\pipe\…`); Unix uses a
29
34
  // Unix domain socket under XDG_RUNTIME_DIR (or /tmp as fallback). Node's
@@ -57,9 +62,13 @@ const POLL_INTERVAL_MS = 2000
57
62
  const SUBAGENT_TIMEOUT_MS = 120_000
58
63
  const DEFAULT_DISPATCH_TIMEOUT_MS = 15_000
59
64
  const SESSION_START_MEMORY_DISPATCH_TIMEOUT_MS = 125_000
60
- const SESSION_START_TRACE_ENABLED =
61
- process.env.MIXDOG_DEBUG_SESSION_START === '1' ||
62
- process.env.MIXDOG_DEBUG_SESSION_START === 'true'
65
+ const MIXDOG_DEBUG_ENABLED = isMixdogDebugEnabled()
66
+ let _hookPipeLogsPruned = false
67
+
68
+ function hookPipeDebugStderr(line) {
69
+ if (!MIXDOG_DEBUG_ENABLED) return
70
+ try { process.stderr.write(line) } catch {}
71
+ }
63
72
 
64
73
  let _started = false
65
74
  let _server = null
@@ -82,13 +91,17 @@ function formatError(err) {
82
91
  }
83
92
 
84
93
  function traceSessionStart(message) {
85
- if (!SESSION_START_TRACE_ENABLED) return
94
+ if (!MIXDOG_DEBUG_ENABLED) return
86
95
  const line = `[${new Date().toISOString()}] [hook-pipe][session-start] ${message}\n`
87
96
  try { process.stderr.write(line) } catch {}
88
97
  try {
89
98
  const dataDir = process.env.CLAUDE_PLUGIN_DATA ||
90
99
  join(homedir(), '.claude', 'plugins', 'data', 'mixdog-trib-plugin')
91
100
  mkdirSync(dataDir, { recursive: true })
101
+ if (!_hookPipeLogsPruned) {
102
+ _hookPipeLogsPruned = true
103
+ pruneStalePluginDataLogSiblings(dataDir, DEFAULT_STALE_LOG_SIBLING_MAX)
104
+ }
92
105
  appendFileSync(join(dataDir, 'session-start.log'), line)
93
106
  } catch {}
94
107
  }
@@ -320,10 +333,10 @@ async function handlePreToolSubagent(payload) {
320
333
  }
321
334
  const route = routeMod.shouldRoutePermissionToDiscord()
322
335
  if (route.route !== 'discord') {
323
- process.stderr.write(`[hook-pipe] pre-tool-subagent discord-route=off agent=${agentIdRaw || 'unknown'} tool=${toolName} reason=${route.reason || 'inactive'}\n`)
336
+ hookPipeDebugStderr(`[hook-pipe] pre-tool-subagent discord-route=off agent=${agentIdRaw || 'unknown'} tool=${toolName} reason=${route.reason || 'inactive'}\n`)
324
337
  return null
325
338
  }
326
- process.stderr.write(`[hook-pipe] pre-tool-subagent discord-route=on agent=${agentIdRaw || 'unknown'} tool=${toolName}\n`)
339
+ hookPipeDebugStderr(`[hook-pipe] pre-tool-subagent discord-route=on agent=${agentIdRaw || 'unknown'} tool=${toolName}\n`)
327
340
 
328
341
  let getDiscordToken
329
342
  try {
@@ -715,7 +728,7 @@ export function startHookPipeServer() {
715
728
  _server.on('error', (err) => {
716
729
  const msg = String(err?.message || err || '')
717
730
  if (err?.code === 'EADDRINUSE' || msg.includes('EADDRINUSE') || msg.includes('Failed to listen')) {
718
- process.stderr.write(`[hook-pipe] ${PIPE_PATH} already owned by a peer daemon; standby for hook IPC\n`)
731
+ hookPipeDebugStderr(`[hook-pipe] ${PIPE_PATH} already owned by a peer daemon; standby for hook IPC\n`)
719
732
  _server = null
720
733
  _started = false
721
734
  return
@@ -727,7 +740,7 @@ export function startHookPipeServer() {
727
740
  try {
728
741
  _server.listen(PIPE_PATH, () => {
729
742
  _started = true
730
- process.stderr.write(`[hook-pipe] listening on ${PIPE_PATH}\n`)
743
+ hookPipeDebugStderr(`[hook-pipe] listening on ${PIPE_PATH}\n`)
731
744
  })
732
745
  } catch (err) {
733
746
  process.stderr.write(`[hook-pipe] listen failed: ${err?.message || err}\n`)