mixdog 0.9.0 → 0.9.2
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 +10 -3
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -4
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +146 -9
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -647
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +154 -20
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// responds with OK_PARTIAL plus a hex-encoded failures payload that
|
|
28
28
|
// the JS side surfaces per-entry.
|
|
29
29
|
|
|
30
|
-
import { existsSync, readFileSync, realpathSync, statSync, mkdirSync } from 'node:fs';
|
|
30
|
+
import { existsSync, readFileSync, realpathSync, statSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
31
31
|
import { unlink } from 'node:fs/promises';
|
|
32
32
|
import { spawn } from 'node:child_process';
|
|
33
33
|
import { createInterface } from 'node:readline';
|
|
@@ -581,6 +581,41 @@ function isPatchErrorText(text) {
|
|
|
581
581
|
return /^Error:/i.test(String(text ?? '').trimStart());
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
// ── UI-only unified-diff side-channel ───────────────────────────────────────
|
|
585
|
+
// apply_patch's MODEL-VISIBLE result stays the compact summary ("Applied 1
|
|
586
|
+
// File (Native)\n OK Modify path — +1 Line · -1 Line"). The standard unified
|
|
587
|
+
// diff it builds internally (nativePatchStr) is stashed here, keyed by the
|
|
588
|
+
// tool_use id, so the agent loop can attach it to the TUI tool-result message
|
|
589
|
+
// as a UI-only field (never sent to the model). The TUI's expanded (ctrl+o)
|
|
590
|
+
// raw view then colorizes it via the existing formatExpandedResult/diff path.
|
|
591
|
+
// Size-capped per entry and FIFO-bounded so a huge patch (or a leaked entry
|
|
592
|
+
// whose take() never ran) cannot grow the transcript item or this Map.
|
|
593
|
+
const APPLY_PATCH_UI_DIFF_MAX_CHARS = 64 * 1024;
|
|
594
|
+
const APPLY_PATCH_UI_DIFF_REGISTRY_MAX = 64;
|
|
595
|
+
const _applyPatchUiDiffByCallId = new Map();
|
|
596
|
+
|
|
597
|
+
function registerApplyPatchUiDiff(callId, diff) {
|
|
598
|
+
if (!callId || typeof diff !== 'string' || !diff.trim()) return;
|
|
599
|
+
let text = diff;
|
|
600
|
+
if (text.length > APPLY_PATCH_UI_DIFF_MAX_CHARS) {
|
|
601
|
+
text = `${text.slice(0, APPLY_PATCH_UI_DIFF_MAX_CHARS)}\n… [diff truncated for display]`;
|
|
602
|
+
}
|
|
603
|
+
if (_applyPatchUiDiffByCallId.size >= APPLY_PATCH_UI_DIFF_REGISTRY_MAX) {
|
|
604
|
+
const oldest = _applyPatchUiDiffByCallId.keys().next().value;
|
|
605
|
+
if (oldest !== undefined) _applyPatchUiDiffByCallId.delete(oldest);
|
|
606
|
+
}
|
|
607
|
+
_applyPatchUiDiffByCallId.set(callId, text);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Consume (read + remove) the UI-only unified diff for a tool_use id. Returns
|
|
611
|
+
// null when none was registered (error/rename-only/partial-fail/dry-skip).
|
|
612
|
+
export function takeApplyPatchUiDiff(callId) {
|
|
613
|
+
if (!callId) return null;
|
|
614
|
+
const value = _applyPatchUiDiffByCallId.get(callId) || null;
|
|
615
|
+
if (value != null) _applyPatchUiDiffByCallId.delete(callId);
|
|
616
|
+
return value;
|
|
617
|
+
}
|
|
618
|
+
|
|
584
619
|
// Count how many source lines a hunk consumes vs produces so we can
|
|
585
620
|
// surface a concise `lines_changed` figure without re-diffing.
|
|
586
621
|
function countHunkChanges(hunks) {
|
|
@@ -2806,6 +2841,14 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
2806
2841
|
if (renameLines.length > 0 && !isPatchErrorText(nativeResult)) {
|
|
2807
2842
|
combined = `${renameLines.join('\n')}\n${nativeResult}`;
|
|
2808
2843
|
}
|
|
2844
|
+
// UI-only: stash the standard unified diff (nativePatchStr) keyed by the
|
|
2845
|
+
// tool_use id so the agent loop can attach it to the TUI tool-result for the
|
|
2846
|
+
// expanded (ctrl+o) colored-diff view. NEVER added to the model-visible
|
|
2847
|
+
// result string. Only on clean success — partial/failed cases surface as an
|
|
2848
|
+
// Error card with the failure text instead.
|
|
2849
|
+
if (!isPatchErrorText(combined) && options?.toolCallId) {
|
|
2850
|
+
registerApplyPatchUiDiff(options.toolCallId, nativePatchStr);
|
|
2851
|
+
}
|
|
2809
2852
|
if (!isPatchErrorText(combined) && rejectedV4AHunks.length > 0) {
|
|
2810
2853
|
const tail = [
|
|
2811
2854
|
'',
|
|
@@ -2824,6 +2867,74 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
2824
2867
|
export const __patchTestHooks = { findFirstFailingUnifiedHunk, computeUnifiedChangeBand, collectUnifiedOps, unifiedOldLinesMatchAt, splitBufferLinesForPatch };
|
|
2825
2868
|
|
|
2826
2869
|
export async function executePatchTool(name, args, cwd, options = {}) {
|
|
2870
|
+
// --- Opt-in failure replay capture -------------------------------------
|
|
2871
|
+
// When MIXDOG_PATCH_REPLAY_CAPTURE=1, a FAILED apply_patch (throw or
|
|
2872
|
+
// "Error:" body) is frozen to a replay file with the ORIGINAL patch args,
|
|
2873
|
+
// cwd, error, and a full snapshot of every target file's current bytes, so
|
|
2874
|
+
// scripts/patch-replay.mjs can re-run the exact failing call against updated
|
|
2875
|
+
// code. Local dev only; the patch body is stored verbatim (no redaction).
|
|
2876
|
+
return _executePatchTool(name, args, cwd, options);
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
function patchReplayDir() {
|
|
2880
|
+
const base = process.env.MIXDOG_PATCH_REPLAY_DIR
|
|
2881
|
+
|| pathJoin(getPluginDataDir(), 'history', 'patch-replays');
|
|
2882
|
+
return base;
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
function getPluginDataDir() {
|
|
2886
|
+
try { return getPluginData(); } catch { /* fall through */ }
|
|
2887
|
+
return process.env.MIXDOG_DATA_DIR || pathJoin(process.env.USERPROFILE || process.env.HOME || '.', '.mixdog', 'data');
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
function patchTargetPaths(patchStr, basePath) {
|
|
2891
|
+
const out = [];
|
|
2892
|
+
const re = /^\*\*\* (?:Update|Add|Delete) File:\s*(.+)$/gm;
|
|
2893
|
+
let m;
|
|
2894
|
+
while ((m = re.exec(String(patchStr || '')))) {
|
|
2895
|
+
const rel = m[1].trim();
|
|
2896
|
+
if (rel) out.push(rel);
|
|
2897
|
+
}
|
|
2898
|
+
// unified headers
|
|
2899
|
+
const ure = /^\+\+\+ (?:b\/)?(.+)$/gm;
|
|
2900
|
+
while ((m = ure.exec(String(patchStr || '')))) {
|
|
2901
|
+
const rel = m[1].trim();
|
|
2902
|
+
if (rel && rel !== '/dev/null') out.push(rel);
|
|
2903
|
+
}
|
|
2904
|
+
return [...new Set(out)];
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
function maybeCapturePatchReplay(args, cwd, errorText) {
|
|
2908
|
+
if (process.env.MIXDOG_PATCH_REPLAY_CAPTURE !== '1') return;
|
|
2909
|
+
try {
|
|
2910
|
+
const patchStr = typeof args?.patch === 'string' ? args.patch : '';
|
|
2911
|
+
const basePath = pathResolve(String(args?.base_path || cwd || process.cwd()));
|
|
2912
|
+
const dir = patchReplayDir();
|
|
2913
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
2914
|
+
const rels = patchTargetPaths(patchStr, basePath);
|
|
2915
|
+
const files = {};
|
|
2916
|
+
for (const rel of rels) {
|
|
2917
|
+
try {
|
|
2918
|
+
const abs = isAbsolute(rel) ? rel : pathResolve(basePath, rel);
|
|
2919
|
+
files[rel] = existsSync(abs) ? readFileSync(abs, 'utf8') : null;
|
|
2920
|
+
} catch { files[rel] = null; }
|
|
2921
|
+
}
|
|
2922
|
+
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2923
|
+
const record = {
|
|
2924
|
+
id,
|
|
2925
|
+
ts: Date.now(),
|
|
2926
|
+
tool: 'apply_patch',
|
|
2927
|
+
args: { patch: patchStr, base_path: args?.base_path ?? null, format: args?.format ?? null, dry_run: args?.dry_run ?? null, fuzzy: args?.fuzzy ?? null, reject_partial: args?.reject_partial ?? null },
|
|
2928
|
+
cwd: basePath,
|
|
2929
|
+
error_first_line: String(errorText || '').split('\n')[0].slice(0, 400),
|
|
2930
|
+
targets: rels,
|
|
2931
|
+
file_snapshots: files,
|
|
2932
|
+
};
|
|
2933
|
+
writeFileSync(pathJoin(dir, `${id}.json`), JSON.stringify(record, null, 2), { mode: 0o600 });
|
|
2934
|
+
} catch { /* capture is best-effort; never affect the tool result */ }
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
async function _executePatchTool(name, args, cwd, options = {}) {
|
|
2827
2938
|
const effectiveCwd = cwd || process.cwd();
|
|
2828
2939
|
switch (name) {
|
|
2829
2940
|
case 'apply_patch': {
|
|
@@ -2831,8 +2942,11 @@ export async function executePatchTool(name, args, cwd, options = {}) {
|
|
|
2831
2942
|
try {
|
|
2832
2943
|
result = await apply_patch(args || {}, effectiveCwd, options);
|
|
2833
2944
|
} catch (err) {
|
|
2834
|
-
|
|
2945
|
+
const errText = `Error: ${err?.message || String(err)}`;
|
|
2946
|
+
maybeCapturePatchReplay(args, effectiveCwd, errText);
|
|
2947
|
+
return errText;
|
|
2835
2948
|
}
|
|
2949
|
+
if (isPatchErrorText(String(result))) maybeCapturePatchReplay(args, effectiveCwd, String(result));
|
|
2836
2950
|
// ② completion progress (claude "Found N" parity). Best-effort, no-op
|
|
2837
2951
|
// when onProgress is absent (no progressToken). Never throws — only
|
|
2838
2952
|
// emits on success (an "Error:" body is left to the tool result alone).
|
|
@@ -16,10 +16,11 @@ import {
|
|
|
16
16
|
realpathSync
|
|
17
17
|
} from "fs";
|
|
18
18
|
import { join, sep } from "path";
|
|
19
|
-
import {
|
|
19
|
+
import { createHash } from "crypto";
|
|
20
|
+
import { chunk, formatForDiscord, MAX_DISCORD_MESSAGE } from "../lib/format.mjs";
|
|
20
21
|
import { withConfigLock } from "../lib/config-lock.mjs";
|
|
21
22
|
import { readSection, updateSection } from "../../shared/config.mjs";
|
|
22
|
-
const MAX_CHUNK_LIMIT =
|
|
23
|
+
const MAX_CHUNK_LIMIT = MAX_DISCORD_MESSAGE;
|
|
23
24
|
const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
24
25
|
const RECENT_SENT_CAP = 200;
|
|
25
26
|
function defaultAccess() {
|
|
@@ -52,6 +53,7 @@ function safeAttName(att) {
|
|
|
52
53
|
}
|
|
53
54
|
class DiscordBackend {
|
|
54
55
|
name = "discord";
|
|
56
|
+
MAX_MESSAGE_LENGTH = MAX_DISCORD_MESSAGE;
|
|
55
57
|
onMessage = null;
|
|
56
58
|
onInteraction = null;
|
|
57
59
|
onModalRequest = null;
|
|
@@ -77,6 +79,9 @@ class DiscordBackend {
|
|
|
77
79
|
this.initialAccess = normalizeAccess(config.access);
|
|
78
80
|
this.client = null;
|
|
79
81
|
}
|
|
82
|
+
formatOutgoing(text) {
|
|
83
|
+
return formatForDiscord(text);
|
|
84
|
+
}
|
|
80
85
|
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
81
86
|
async connect() {
|
|
82
87
|
// Re-entry guard: if a connect() is already in-flight or completed, return
|
|
@@ -335,33 +340,118 @@ class DiscordBackend {
|
|
|
335
340
|
}
|
|
336
341
|
}
|
|
337
342
|
if (files.length > 10) throw new Error("max 10 attachments per message");
|
|
338
|
-
|
|
343
|
+
// Resume-token support (opaque to callers). A partial-send failure hands the
|
|
344
|
+
// caller a token { hash, nextChunkIdx, sentIds, prefixed }; a requeued retry
|
|
345
|
+
// passes it back so we resume at the failed chunk instead of re-sending
|
|
346
|
+
// chunks that already landed.
|
|
347
|
+
//
|
|
348
|
+
// The "\u3164\n" prefix is applied from this.sendCount, which advances when
|
|
349
|
+
// ANY backend send fully succeeds. Between the original partial failure and
|
|
350
|
+
// this retry, an unrelated send (scheduler/lifecycle/permission) can flip
|
|
351
|
+
// sendCount from 0 to >0, which would change the prefix — and therefore the
|
|
352
|
+
// chunked text and its hash — breaking the token match and forcing a
|
|
353
|
+
// duplicate full-resend. To keep the retry byte-identical to the original
|
|
354
|
+
// attempt, freeze the prefix decision in the token and reuse it here instead
|
|
355
|
+
// of recomputing from the (possibly changed) current sendCount.
|
|
356
|
+
const resumeToken = opts?.resumeToken;
|
|
357
|
+
const applyPrefix = (resumeToken && typeof resumeToken.prefixed === "boolean")
|
|
358
|
+
? resumeToken.prefixed
|
|
359
|
+
: (text && this.sendCount > 0 ? true : false);
|
|
360
|
+
if (text && applyPrefix) {
|
|
339
361
|
text = "\u3164\n" + text;
|
|
340
362
|
}
|
|
341
363
|
const access = this.loadAccess();
|
|
342
364
|
const limit = Math.max(1, Math.min(access.textChunkLimit ?? MAX_CHUNK_LIMIT, MAX_CHUNK_LIMIT));
|
|
343
365
|
const replyMode = access.replyToMode ?? "off";
|
|
344
366
|
const chunks = chunk(text, limit);
|
|
367
|
+
// Hash the FINAL prefixed text so the token pins to the exact bytes sent.
|
|
368
|
+
const contentHash = createHash("md5").update(text).digest("hex");
|
|
369
|
+
let startIdx = 0;
|
|
345
370
|
const sentIds = [];
|
|
371
|
+
// Resume only when BOTH the text hash AND the effective chunk limit match.
|
|
372
|
+
// The token's nextChunkIdx is an index into the chunk array, which is only
|
|
373
|
+
// meaningful for the same `limit`; if access.textChunkLimit changed during
|
|
374
|
+
// the retry window the same text can split differently, so a stale index
|
|
375
|
+
// could skip or duplicate a range. On mismatch fall through to startIdx=0
|
|
376
|
+
// (full, safe resend).
|
|
377
|
+
if (resumeToken && resumeToken.hash === contentHash && resumeToken.limit === limit) {
|
|
378
|
+
startIdx = Math.max(0, Math.min(resumeToken.nextChunkIdx ?? 0, chunks.length));
|
|
379
|
+
// Seed sentIds with the already-delivered ids so the returned list stays
|
|
380
|
+
// complete across the resume boundary.
|
|
381
|
+
if (Array.isArray(resumeToken.sentIds)) sentIds.push(...resumeToken.sentIds);
|
|
382
|
+
}
|
|
346
383
|
try {
|
|
347
|
-
for (let i =
|
|
384
|
+
for (let i = startIdx; i < chunks.length; i++) {
|
|
348
385
|
const shouldReplyTo = replyTo != null && replyMode !== "off" && (replyMode === "all" || i === 0);
|
|
349
386
|
const embeds = i === 0 ? opts?.embeds ?? [] : [];
|
|
350
387
|
const components = i === 0 ? opts?.components ?? [] : [];
|
|
351
|
-
const
|
|
388
|
+
const payload = {
|
|
352
389
|
content: chunks[i],
|
|
353
390
|
...embeds.length > 0 ? { embeds } : {},
|
|
354
391
|
...components.length > 0 ? { components } : {},
|
|
355
392
|
...i === 0 && files.length > 0 ? { files } : {},
|
|
356
393
|
...shouldReplyTo ? { reply: { messageReference: replyTo, failIfNotExists: false } } : {}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
394
|
+
};
|
|
395
|
+
// Per-chunk send with retry. On 429 honour Retry-After; on other
|
|
396
|
+
// transient errors do a short backoff. Cap at 3 attempts per chunk;
|
|
397
|
+
// after the cap throw an error carrying a resume token so the caller
|
|
398
|
+
// (output-forwarder) requeues the WHOLE item and the next sendMessage
|
|
399
|
+
// resumes at this chunk index instead of re-sending 0..i-1.
|
|
400
|
+
let attempt = 0;
|
|
401
|
+
for (;;) {
|
|
402
|
+
try {
|
|
403
|
+
const sent = await ch.send(payload);
|
|
404
|
+
this.noteSent(sent.id);
|
|
405
|
+
sentIds.push(sent.id);
|
|
406
|
+
break;
|
|
407
|
+
} catch (err) {
|
|
408
|
+
attempt++;
|
|
409
|
+
const status = err?.status ?? err?.code ?? err?.httpStatus;
|
|
410
|
+
// Classify: PERMANENT = a 4xx client error (unknown channel/message,
|
|
411
|
+
// missing access/permissions, 404 …) that will never succeed on
|
|
412
|
+
// retry. TRANSIENT = 429, any 5xx, or network error (no status). A
|
|
413
|
+
// permanent error must NOT retry-loop and must NOT carry a resume
|
|
414
|
+
// token — throw it immediately flagged so the forwarder drops the
|
|
415
|
+
// item instead of requeuing forever.
|
|
416
|
+
const isPermanent = typeof status === "number" && status >= 400 && status < 500 && status !== 429;
|
|
417
|
+
if (isPermanent) {
|
|
418
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
419
|
+
e.permanent = true;
|
|
420
|
+
throw e;
|
|
421
|
+
}
|
|
422
|
+
if (attempt >= 3) {
|
|
423
|
+
// Attach an opaque resume token pointing at the chunk that failed
|
|
424
|
+
// (i). sentIds holds every chunk already delivered (including any
|
|
425
|
+
// seeded from a prior token).
|
|
426
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
427
|
+
e.resumeToken = { hash: contentHash, nextChunkIdx: i, sentIds: [...sentIds], prefixed: applyPrefix, limit };
|
|
428
|
+
throw e;
|
|
429
|
+
}
|
|
430
|
+
if (status === 429) {
|
|
431
|
+
// @discordjs/rest RateLimitError.retryAfter is ALWAYS in ms
|
|
432
|
+
// (no unit guess). Clamp to [0, 60000]; fall back to 1s when the
|
|
433
|
+
// field is absent/invalid. (discord.js also auto-sleeps rate
|
|
434
|
+
// limits internally, so we rarely even reach this branch.)
|
|
435
|
+
const retryAfterMs = Number(err?.retryAfter);
|
|
436
|
+
const ms = Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? Math.min(retryAfterMs, 60_000) : 1000;
|
|
437
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
438
|
+
} else {
|
|
439
|
+
// Other transient error (5xx / network): short backoff.
|
|
440
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
360
444
|
}
|
|
361
445
|
this.sendCount += sentIds.length;
|
|
362
446
|
} catch (err) {
|
|
363
447
|
const msg = err instanceof Error ? err.message : String(err);
|
|
364
|
-
|
|
448
|
+
const wrapped = new Error(`send failed after ${sentIds.length}/${chunks.length} chunk(s): ${msg}`);
|
|
449
|
+
// Preserve the opaque resume token across the rewrap so the caller can
|
|
450
|
+
// persist it on the queue item and resume on retry.
|
|
451
|
+
if (err?.resumeToken) wrapped.resumeToken = err.resumeToken;
|
|
452
|
+
// Propagate the permanent flag so the forwarder drops (not requeues) it.
|
|
453
|
+
if (err?.permanent) wrapped.permanent = true;
|
|
454
|
+
throw wrapped;
|
|
365
455
|
}
|
|
366
456
|
return { sentIds };
|
|
367
457
|
}
|