mixdog 0.8.0 → 0.9.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.
- package/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,15 +1,44 @@
|
|
|
1
|
+
export const APPLY_PATCH_LARK_GRAMMAR = `start: begin_patch hunk+ end_patch
|
|
2
|
+
begin_patch: "*** Begin Patch" LF
|
|
3
|
+
end_patch: "*** End Patch" LF?
|
|
4
|
+
|
|
5
|
+
hunk: add_hunk | delete_hunk | update_hunk
|
|
6
|
+
add_hunk: "*** Add File: " filename LF add_line+
|
|
7
|
+
delete_hunk: "*** Delete File: " filename LF
|
|
8
|
+
update_hunk: "*** Update File: " filename LF change_move? change?
|
|
9
|
+
|
|
10
|
+
filename: /(.+)/
|
|
11
|
+
add_line: "+" /(.*)/ LF -> line
|
|
12
|
+
|
|
13
|
+
change_move: "*** Move to: " filename LF
|
|
14
|
+
change: (change_context | change_line)+ eof_line?
|
|
15
|
+
change_context: ("@@" | "@@ " /(.+)/) LF
|
|
16
|
+
change_line: ("+" | "-" | " ") /(.*)/ LF
|
|
17
|
+
eof_line: "*** End of File" LF
|
|
18
|
+
|
|
19
|
+
%import common.LF
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
export const APPLY_PATCH_FREEFORM_DESCRIPTION = 'Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.';
|
|
23
|
+
|
|
1
24
|
export const PATCH_TOOL_DEFS = [
|
|
2
25
|
{
|
|
3
26
|
name: 'apply_patch',
|
|
4
27
|
title: 'Mixdog Apply Patch',
|
|
5
28
|
annotations: { title: 'Mixdog Apply Patch', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, compressible: false, compressibleLossless: true },
|
|
6
|
-
description: 'Apply file patches. Prefer V4A with
|
|
29
|
+
description: 'Apply file patches. Prefer V4A envelope with exact current context.',
|
|
30
|
+
freeformDescription: APPLY_PATCH_FREEFORM_DESCRIPTION,
|
|
31
|
+
freeform: {
|
|
32
|
+
type: 'grammar',
|
|
33
|
+
syntax: 'lark',
|
|
34
|
+
definition: APPLY_PATCH_LARK_GRAMMAR,
|
|
35
|
+
},
|
|
7
36
|
inputSchema: {
|
|
8
37
|
type: 'object',
|
|
9
38
|
properties: {
|
|
10
|
-
patch: { type: 'string', description: 'Patch text
|
|
11
|
-
format: { type: 'string', enum: ['unified', 'v4a'], description: 'Auto-detected.
|
|
12
|
-
base_path: { type: 'string', description: 'Repo root
|
|
39
|
+
patch: { type: 'string', description: 'Patch text. V4A preferred: `*** Begin Patch`; one file block per target file; exact current context.' },
|
|
40
|
+
format: { type: 'string', enum: ['unified', 'v4a'], description: 'Auto-detected.' },
|
|
41
|
+
base_path: { type: 'string', description: 'Repo root.' },
|
|
13
42
|
dry_run: { type: 'boolean', description: 'Default false. true = validate only, no write.' },
|
|
14
43
|
},
|
|
15
44
|
required: ['patch'],
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
withBuiltinPathLocks,
|
|
48
48
|
} from './builtin.mjs';
|
|
49
49
|
import { withAdvisoryLocks } from './builtin/advisory-lock.mjs';
|
|
50
|
-
import { markCodeGraphDirtyPaths } from './code-graph.mjs';
|
|
50
|
+
import { markCodeGraphDirtyPaths } from './code-graph-state.mjs';
|
|
51
51
|
import { wrapMutationRouteOutput } from './mutation-planner.mjs';
|
|
52
52
|
import { getPluginData } from '../config.mjs';
|
|
53
53
|
import { ensurePatchBinary, findCachedPatchBinary } from './patch-binary-fetcher.mjs';
|
|
@@ -527,6 +527,56 @@ function classifyEntry(entry) {
|
|
|
527
527
|
return 'modify';
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
function parsedEntryTargetKey(entry, basePath) {
|
|
531
|
+
if (classifyEntry(entry) !== 'modify') return '';
|
|
532
|
+
const headerName = entry.oldFileName || entry.newFileName;
|
|
533
|
+
if (!headerName || DEV_NULL.test(headerName)) return '';
|
|
534
|
+
const fullPath = resolveEntryPath(basePath, headerName);
|
|
535
|
+
return process.platform === 'win32' ? fullPath.toLowerCase() : fullPath;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function mergeDuplicateParsedModifyEntries(parsed, basePath) {
|
|
539
|
+
const out = [];
|
|
540
|
+
const byTarget = new Map();
|
|
541
|
+
let changed = false;
|
|
542
|
+
for (const entry of parsed || []) {
|
|
543
|
+
const key = parsedEntryTargetKey(entry, basePath);
|
|
544
|
+
if (!key) {
|
|
545
|
+
out.push(entry);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const existing = byTarget.get(key);
|
|
549
|
+
if (!existing) {
|
|
550
|
+
byTarget.set(key, entry);
|
|
551
|
+
out.push(entry);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
existing.hunks.push(...(entry.hunks || []));
|
|
555
|
+
changed = true;
|
|
556
|
+
}
|
|
557
|
+
return { parsed: out, changed };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function unifiedRange(start, lines) {
|
|
561
|
+
const s = Math.max(0, Number(start) || 0);
|
|
562
|
+
const n = Math.max(0, Number(lines) || 0);
|
|
563
|
+
return `${s},${n}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function renderParsedUnifiedPatch(parsed) {
|
|
567
|
+
const out = [];
|
|
568
|
+
for (const entry of parsed || []) {
|
|
569
|
+
out.push(`--- ${entry.oldFileName || '/dev/null'}`);
|
|
570
|
+
out.push(`+++ ${entry.newFileName || '/dev/null'}`);
|
|
571
|
+
for (const hunk of entry.hunks || []) {
|
|
572
|
+
const section = hunk.section ? ` ${hunk.section}` : '';
|
|
573
|
+
out.push(`@@ -${unifiedRange(hunk.oldStart, hunk.oldLines)} +${unifiedRange(hunk.newStart, hunk.newLines)} @@${section}`);
|
|
574
|
+
for (const line of hunk.lines || []) out.push(line);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return `${out.join('\n')}\n`;
|
|
578
|
+
}
|
|
579
|
+
|
|
530
580
|
function isPatchErrorText(text) {
|
|
531
581
|
return /^Error:/i.test(String(text ?? '').trimStart());
|
|
532
582
|
}
|
|
@@ -637,7 +687,27 @@ async function preValidateNativeBatch(parsed, basePath) {
|
|
|
637
687
|
const kind = classifyEntry(entry);
|
|
638
688
|
const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
|
|
639
689
|
if (!nativeHeaderSupported(entry, basePath)) {
|
|
640
|
-
|
|
690
|
+
// Two distinct failure modes share nativeHeaderSupported()===false:
|
|
691
|
+
// (a) no usable target path could be parsed from the section header
|
|
692
|
+
// (oldFileName/newFileName empty or /dev/null) — a SHAPE problem,
|
|
693
|
+
// (b) a parsed path that escapes base_path (`..` segment / out-of-base
|
|
694
|
+
// absolute) — a SECURITY refusal.
|
|
695
|
+
// Collapsing both into the "path escapes base_path or contains `..`"
|
|
696
|
+
// message mislabels (a) as (b): the model sees a nonsensical reason for a
|
|
697
|
+
// patch that uses no `..`, cannot self-correct, and re-submits the same
|
|
698
|
+
// patch until the iteration cap. Emit a shape-specific, actionable hint
|
|
699
|
+
// for (a) so the loop breaks on the first failure.
|
|
700
|
+
const hasUsableHeader = headerName && !DEV_NULL.test(headerName);
|
|
701
|
+
if (!hasUsableHeader) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
'apply_patch: a file section header could not be parsed (no target path). '
|
|
704
|
+
+ 'Each section must start with a valid header: `*** Update File: <path>` / '
|
|
705
|
+
+ '`*** Add File: <path>` / `*** Delete File: <path>` (V4A), or a '
|
|
706
|
+
+ '`--- a/<path>` + `+++ b/<path>` pair (unified). Wrap multi-hunk V4A '
|
|
707
|
+
+ 'edits in a `*** Begin Patch` / `*** End Patch` envelope and pass format:"v4a".',
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
const display = normalizeOutputPath(stripDiffPrefix(headerName));
|
|
641
711
|
throw new Error(`apply_patch: header ${display} is unsupported (path escapes base_path or contains \`..\`).`);
|
|
642
712
|
}
|
|
643
713
|
if (kind !== 'delete' && !(entry.hunks?.length > 0)) {
|
|
@@ -1240,8 +1310,15 @@ async function dispatchNativePatch({ entries, basePath, nativePatchStr, fuzz, re
|
|
|
1240
1310
|
: `${verbLabel} ${countLabel(writtenEntries.length, 'File')} (Native)${dryRun ? ' Dry Run' : ''}`;
|
|
1241
1311
|
const lines = [summary];
|
|
1242
1312
|
for (const entry of writtenEntries) {
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1313
|
+
const added = entry.added || 0;
|
|
1314
|
+
const removed = entry.removed || 0;
|
|
1315
|
+
const parts = [];
|
|
1316
|
+
if (added > 0) parts.push(`+${countLabel(added, 'Line')}`);
|
|
1317
|
+
if (removed > 0) parts.push(`-${countLabel(removed, 'Line')}`);
|
|
1318
|
+
const detail = parts.join(' · ');
|
|
1319
|
+
lines.push(detail
|
|
1320
|
+
? ` OK ${kindLabel(entry.kind)} ${entry.displayPath} — ${detail}`
|
|
1321
|
+
: ` OK ${kindLabel(entry.kind)} ${entry.displayPath}`);
|
|
1245
1322
|
}
|
|
1246
1323
|
for (const f of stats.failures || []) {
|
|
1247
1324
|
lines.push(` SKIP ${f.path || '(unknown)'} — ${f.reason}${formatNativeFailureContext(parsed, basePath, f.path, { fuzz })}`);
|
|
@@ -1711,7 +1788,7 @@ function longestCommonSubstringLen(a, b, cap = 4000) {
|
|
|
1711
1788
|
}
|
|
1712
1789
|
|
|
1713
1790
|
// Normalize common typographic code-points to their ASCII equivalents, then
|
|
1714
|
-
// trim. Mirrors the Rust mixdog-patch normalize_typographic() and
|
|
1791
|
+
// trim. Mirrors the Rust mixdog-patch normalize_typographic() and V4A
|
|
1715
1792
|
// apply_patch's normalise() so V4A->unified anchor resolution stays consistent
|
|
1716
1793
|
// across engines: an ASCII-authored patch can still anchor on source that
|
|
1717
1794
|
// carries curly quotes, em/en dashes, NBSP and other exotic spaces.
|
|
@@ -2643,7 +2720,7 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
2643
2720
|
parsed = parsePatch(normalizedPatchStr);
|
|
2644
2721
|
} catch (err) {
|
|
2645
2722
|
if (!canFallbackCountedUnified(patchStr, requestedFormat, err)) {
|
|
2646
|
-
throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; prefer
|
|
2723
|
+
throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; prefer V4A envelope for multi-hunk edits (no @@ line counts)`);
|
|
2647
2724
|
}
|
|
2648
2725
|
try {
|
|
2649
2726
|
inputPatchStr = await convertUnifiedCountedToUnifiedPatchViaV4A(patchStr, basePath, v4aConvertOpts);
|
|
@@ -2661,6 +2738,13 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
2661
2738
|
if (!v4aRenameOnly && (!Array.isArray(parsed) || parsed.length === 0)) {
|
|
2662
2739
|
return 'Error: patch contained no file sections';
|
|
2663
2740
|
}
|
|
2741
|
+
if (!v4aRenameOnly) {
|
|
2742
|
+
const merged = mergeDuplicateParsedModifyEntries(parsed, basePath);
|
|
2743
|
+
if (merged.changed) {
|
|
2744
|
+
parsed = merged.parsed;
|
|
2745
|
+
normalizedPatchStr = renderParsedUnifiedPatch(parsed);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2664
2748
|
|
|
2665
2749
|
// Pre-validate paths / duplicates / symlink escapes — throws on any
|
|
2666
2750
|
// unsupported entry. Throws bubble out to the tool dispatcher as clean
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Per-tool live-status message builder (ported from
|
|
1
|
+
// Per-tool live-status message builder (ported from a reference agent's progress
|
|
2
2
|
// generalization / getActivityDescription). Dependency-light and
|
|
3
3
|
// side-effect-free: given a tool name + its raw args, return a short
|
|
4
4
|
// human-readable "what's happening now" string. Used by the central dispatch
|
|
5
5
|
// path to emit a single start-of-tool progress notification. Every tool gets a
|
|
6
6
|
// meaningful verb; `running ${name}` is only the last-resort fallback.
|
|
7
7
|
//
|
|
8
|
-
// Verbs are kept symmetric with
|
|
8
|
+
// Verbs are kept symmetric with common agent wording: Read→"reading",
|
|
9
9
|
// Edit→"editing", Write→"writing", Grep→"searching for", Glob→"finding",
|
|
10
10
|
// web search→"searching web for", web fetch→"fetching".
|
|
11
11
|
|
|
@@ -45,6 +45,8 @@ export function formatToolStartProgress(name, args = {}) {
|
|
|
45
45
|
return Array.isArray(a.pattern) ? `searching for ${_plural(a.pattern.length, 'pattern')}` : `searching for ${_t(a.pattern)}`;
|
|
46
46
|
case 'glob':
|
|
47
47
|
return Array.isArray(a.pattern) ? `finding ${_plural(a.pattern.length, 'glob')}` : `finding ${_t(a.pattern)}`;
|
|
48
|
+
case 'find':
|
|
49
|
+
return `finding ${_t(a.query || 'files')}`;
|
|
48
50
|
case 'list':
|
|
49
51
|
return a.mode === 'find' ? 'finding files' : `listing ${_t(a.path || 'cwd')}`;
|
|
50
52
|
case 'code_graph':
|
|
@@ -57,12 +59,12 @@ export function formatToolStartProgress(name, args = {}) {
|
|
|
57
59
|
case 'web_fetch':
|
|
58
60
|
return Array.isArray(a.url) ? `fetching ${_plural(a.url.length, 'URL')}` : `fetching ${_t(a.url)}`;
|
|
59
61
|
|
|
60
|
-
// ── agent module: explore /
|
|
62
|
+
// ── agent module: explore / agent / models ───────────────────────
|
|
61
63
|
case 'explore': {
|
|
62
64
|
const n = Array.isArray(a.query) ? a.query.length : (a.query ? 1 : 0);
|
|
63
65
|
return `exploring ${_plural(n, 'query', 'queries')}`;
|
|
64
66
|
}
|
|
65
|
-
case '
|
|
67
|
+
case 'agent': {
|
|
66
68
|
const route = [a.preset, [a.provider, a.model].filter(Boolean).join('/')].filter(Boolean).join(' ');
|
|
67
69
|
const suffix = route ? ` (${_t(route, 32)})` : '';
|
|
68
70
|
if (a.role) return `dispatching ${_t(a.role)}${suffix}`;
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import * as nodeUtil from 'node:util';
|
|
36
|
-
import {
|
|
36
|
+
import { traceAgentCompress, traceAgentBatch } from '../agent-trace.mjs';
|
|
37
37
|
import { BUILTIN_TOOLS } from './builtin.mjs';
|
|
38
38
|
import { PATCH_TOOL_DEFS } from './patch-tool-defs.mjs';
|
|
39
39
|
import { CODE_GRAPH_TOOL_DEFS } from './code-graph-tool-defs.mjs';
|
|
@@ -137,7 +137,7 @@ function normalizeTrailingNewlines(text) {
|
|
|
137
137
|
return text.replace(/\n+$/, '\n');
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// Retained as named exports for non-tool-result consumers (
|
|
140
|
+
// Retained as named exports for non-tool-result consumers (agent
|
|
141
141
|
// aggregated worker bodies in ai-wrapped-dispatch). These functions are
|
|
142
142
|
// NO LONGER part of the tool-result compression chain — that chain is
|
|
143
143
|
// lossless-only.
|
|
@@ -192,7 +192,7 @@ export function compressToolResult(toolName, args, result, ctx) {
|
|
|
192
192
|
out = normalizeTrailingNewlines(out);
|
|
193
193
|
if (out.length >= before) return result;
|
|
194
194
|
if (ctx?.sessionId) {
|
|
195
|
-
try {
|
|
195
|
+
try { traceAgentCompress({ sessionId: ctx.sessionId, toolName, before, after: out.length }); } catch { /* trace best-effort */ }
|
|
196
196
|
}
|
|
197
197
|
return out;
|
|
198
198
|
}
|
|
@@ -205,7 +205,7 @@ export function compressToolResult(toolName, args, result, ctx) {
|
|
|
205
205
|
export function recordToolBatch(sessionId, toolCallCount) {
|
|
206
206
|
const n = Number(toolCallCount);
|
|
207
207
|
if (!sessionId || !Number.isFinite(n) || n <= 0) return;
|
|
208
|
-
try {
|
|
208
|
+
try { traceAgentBatch({ sessionId, toolCallCount: n }); } catch { /* trace best-effort */ }
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
export const _internals = {
|
|
@@ -53,7 +53,7 @@ const SHELL_OUTPUT_DISK_CAP = 100 * 1024 * 1024;
|
|
|
53
53
|
|
|
54
54
|
// Background-task disk watchdog cadence. The size guard polls the spilled
|
|
55
55
|
// stdout/stderr files every interval and SIGKILLs the child once the
|
|
56
|
-
// combined size exceeds SHELL_OUTPUT_DISK_CAP. 5 s matches
|
|
56
|
+
// combined size exceeds SHELL_OUTPUT_DISK_CAP. 5 s matches reference agent
|
|
57
57
|
// upstream cadence — short enough that a runaway loop is caught within a
|
|
58
58
|
// few seconds, long enough that the stat overhead is negligible.
|
|
59
59
|
const SIZE_WATCHDOG_INTERVAL_MS = 1_000;
|
|
@@ -585,6 +585,13 @@ export function execShellCommand({
|
|
|
585
585
|
cwd,
|
|
586
586
|
windowsHide: true,
|
|
587
587
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
588
|
+
// NOTE (child-spawn-gate): intentionally NOT routed through
|
|
589
|
+
// src/runtime/shared/child-spawn-gate.mjs. bash/pwsh commands can run
|
|
590
|
+
// for minutes (or auto-background), so holding a finite gate slot for
|
|
591
|
+
// the whole lifetime would let a few long shells starve rg/code_graph —
|
|
592
|
+
// the opposite of the gate's intent. TODO: if shell saturation becomes
|
|
593
|
+
// a problem, gate only the brief spawn burst (release on first output /
|
|
594
|
+
// adoption), not the full run.
|
|
588
595
|
// POSIX: detached gives the child its own process group so treeKill can
|
|
589
596
|
// signal the whole group. The child is still CLI-owned because we do
|
|
590
597
|
// not unref it after adoption. Windows detached has different console
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// pyenv / mise / asdf / direnv setup the user gets in their interactive
|
|
9
9
|
// terminal — without paying a fresh login-shell startup on every call.
|
|
10
10
|
//
|
|
11
|
-
// Mirrors
|
|
11
|
+
// Mirrors upstream shell snapshot pattern (reference ShellSnapshot.ts:413,
|
|
12
12
|
// createAndSaveSnapshot). Simpler scope: bash and zsh only, no embedded
|
|
13
13
|
// search-tool injection (mixdog ships its own grep/glob helpers).
|
|
14
14
|
|
|
@@ -34,7 +34,7 @@ const _cache = new Map();
|
|
|
34
34
|
// command. Cleared on process exit (process-scoped Set).
|
|
35
35
|
const _failedShells = new Set();
|
|
36
36
|
|
|
37
|
-
// Mirrors
|
|
37
|
+
// Mirrors reference cleanupRegistry pattern (utils/
|
|
38
38
|
// cleanupRegistry.ts + ShellSnapshot.ts:534-545). Snapshot files are
|
|
39
39
|
// session-scoped and must be unlinked on graceful shutdown — otherwise
|
|
40
40
|
// they pile up forever (each rc-file mtime change creates a new file).
|
|
@@ -155,7 +155,7 @@ function _runSnapshot(shellPath, snapshotPath, configFileExists) {
|
|
|
155
155
|
return new Promise((resolve) => {
|
|
156
156
|
const script = getSnapshotScript(shellPath, snapshotPath, configFileExists);
|
|
157
157
|
let stderrBuf = '';
|
|
158
|
-
// Mirror
|
|
158
|
+
// Mirror reference implementation (bash/ShellSnapshot.ts:458):
|
|
159
159
|
// `-c -l` (login non-interactive). Earlier `-ic` (interactive command)
|
|
160
160
|
// forced bash-completion to load — Git's completion loader spawns `find`
|
|
161
161
|
// in subshells (__git_find_on_cmdline etc.) which detach on Windows when
|
|
@@ -163,7 +163,7 @@ function _runSnapshot(shellPath, snapshotPath, configFileExists) {
|
|
|
163
163
|
// mode runs .bash_profile / .profile (which typically sources .bashrc)
|
|
164
164
|
// without triggering completion init. The script also explicitly sources
|
|
165
165
|
// the rc file, so the interactive-guard `[[ $- == *i* ]] && return` is
|
|
166
|
-
// accepted as a known tradeoff (matches
|
|
166
|
+
// accepted as a known tradeoff (matches the reference choice).
|
|
167
167
|
const child = spawn(shellPath, ['-c', '-l', script], {
|
|
168
168
|
// P3 fix: blank prompts so an interactive sourcing in -ic does not
|
|
169
169
|
// print PS1 / PS2 / RPROMPT / PROMPT noise to stderr (which our
|
|
@@ -170,6 +170,8 @@ if (process.env.MIXDOG_CHANNELS_NO_CONNECT) {
|
|
|
170
170
|
}
|
|
171
171
|
const _isWorkerMode = process.env.MIXDOG_WORKER_MODE === '1'
|
|
172
172
|
const _isCliOwnedMode = process.env.MIXDOG_CLI_OWNED === '1'
|
|
173
|
+
const _isChannelDaemonMode = process.env.MIXDOG_CHANNEL_DAEMON === '1'
|
|
174
|
+
const CHANNEL_DAEMON_IDLE_TTL_MS = Math.max(0, Number(process.env.MIXDOG_CHANNEL_IDLE_TTL_MS) || 60_000)
|
|
173
175
|
const _bootLogEarly = path.join(
|
|
174
176
|
DATA_DIR || path.join(os.tmpdir(), "mixdog"),
|
|
175
177
|
"boot.log"
|
|
@@ -225,7 +227,7 @@ try {
|
|
|
225
227
|
// GC stale ephemeral session files. closeSession plants a closed=true
|
|
226
228
|
// tombstone, but bench / smoke / probe drivers historically created sessions
|
|
227
229
|
// without ever calling closeSession, leaving 175-byte placeholders behind.
|
|
228
|
-
// 7-day TTL is safe because live
|
|
230
|
+
// 7-day TTL is safe because live agent sessions touch their JSON file on
|
|
229
231
|
// every ask iteration, so any file older than 7 days is provably abandoned.
|
|
230
232
|
const _SESSIONS_DIR = path.join(DATA_DIR, 'sessions');
|
|
231
233
|
const _STALE_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -327,24 +329,56 @@ const INSTRUCTIONS = "";
|
|
|
327
329
|
// never `connect()`ed to any transport, so `.notification()` silently
|
|
328
330
|
// threw 'Not connected' inside the SDK and every call was dropped by an
|
|
329
331
|
// outer `.catch(() => {})`. That regression is what this path replaces.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
// CC channel schema requires meta: Record<string,string> (channelNotification.ts).
|
|
336
|
-
// Coerce every meta value to string so a non-string (e.g. a Discord
|
|
337
|
-
// interaction.type number) can't fail zod and silently drop the notify.
|
|
338
|
-
// silent_to_agent stays boolean — an internal routing flag the daemon
|
|
339
|
-
// router / agentNotify consume (=== true) before the CC zod boundary.
|
|
340
|
-
let outParams = params;
|
|
332
|
+
let _channelNotifyBusSeq = 0;
|
|
333
|
+
const CHANNEL_NOTIFY_BUS_FILE = path.join(RUNTIME_ROOT, 'channel-notifications.jsonl');
|
|
334
|
+
const CHANNEL_NOTIFY_BUS_MAX_BYTES = 5 * 1024 * 1024;
|
|
335
|
+
|
|
336
|
+
function normalizeChannelNotifyParams(method, params) {
|
|
341
337
|
if (method === 'notifications/claude/channel' && params && params.meta) {
|
|
342
338
|
const m = {};
|
|
343
339
|
for (const [k, v] of Object.entries(params.meta)) {
|
|
344
340
|
if (v === undefined || v === null) continue;
|
|
345
341
|
m[k] = k === 'silent_to_agent' ? (v === true || v === 'true') : String(v);
|
|
346
342
|
}
|
|
347
|
-
|
|
343
|
+
return { ...params, meta: m };
|
|
344
|
+
}
|
|
345
|
+
return params;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function publishChannelNotify(method, params) {
|
|
349
|
+
try {
|
|
350
|
+
fs.mkdirSync(RUNTIME_ROOT, { recursive: true });
|
|
351
|
+
try {
|
|
352
|
+
const st = fs.statSync(CHANNEL_NOTIFY_BUS_FILE);
|
|
353
|
+
if (st.size > CHANNEL_NOTIFY_BUS_MAX_BYTES) {
|
|
354
|
+
try { fs.unlinkSync(CHANNEL_NOTIFY_BUS_FILE + '.1'); } catch {}
|
|
355
|
+
try { fs.renameSync(CHANNEL_NOTIFY_BUS_FILE, CHANNEL_NOTIFY_BUS_FILE + '.1'); } catch {}
|
|
356
|
+
}
|
|
357
|
+
} catch {}
|
|
358
|
+
const item = {
|
|
359
|
+
id: `${Date.now()}-${process.pid}-${++_channelNotifyBusSeq}`,
|
|
360
|
+
ts: Date.now(),
|
|
361
|
+
pid: process.pid,
|
|
362
|
+
method,
|
|
363
|
+
params,
|
|
364
|
+
};
|
|
365
|
+
fs.appendFileSync(CHANNEL_NOTIFY_BUS_FILE, JSON.stringify(item) + '\n');
|
|
366
|
+
} catch (err) {
|
|
367
|
+
try { process.stderr.write(`mixdog channels: notify bus write failed: ${err && err.message || err}\n`); } catch {}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function sendNotifyToParent(method, params) {
|
|
372
|
+
// CC channel schema requires meta: Record<string,string> (channelNotification.ts).
|
|
373
|
+
// Coerce every meta value to string so a non-string (e.g. a Discord
|
|
374
|
+
// interaction.type number) can't fail zod and silently drop the notify.
|
|
375
|
+
// silent_to_agent stays boolean — an internal routing flag the daemon
|
|
376
|
+
// router / agentNotify consume (=== true) before the CC zod boundary.
|
|
377
|
+
const outParams = normalizeChannelNotifyParams(method, params);
|
|
378
|
+
publishChannelNotify(method, outParams);
|
|
379
|
+
if (!process.send) {
|
|
380
|
+
try { process.stderr.write(`mixdog channels: notify queued on bus (no IPC): ${method}\n`); } catch {}
|
|
381
|
+
return;
|
|
348
382
|
}
|
|
349
383
|
try {
|
|
350
384
|
process.send({ type: 'notify', method, params: outParams });
|
|
@@ -612,6 +646,98 @@ const ACTIVE_OWNER_STALE_MS = 1e4;
|
|
|
612
646
|
// never blocks process exit. Single JSON atomic write, no measurable load.
|
|
613
647
|
const OWNER_HEARTBEAT_INTERVAL_MS = 5e3;
|
|
614
648
|
let ownerHeartbeatTimer = null;
|
|
649
|
+
let channelDaemonIdleTimer = null;
|
|
650
|
+
let channelDaemonLastClientAt = Date.now();
|
|
651
|
+
let channelDaemonBackgroundLogAt = 0;
|
|
652
|
+
const CHANNEL_CLIENT_DIR = path.join(RUNTIME_ROOT, 'channel-clients');
|
|
653
|
+
|
|
654
|
+
function pidAlive(pid) {
|
|
655
|
+
const n = Number(pid);
|
|
656
|
+
if (!Number.isInteger(n) || n <= 0) return false;
|
|
657
|
+
try {
|
|
658
|
+
process.kill(n, 0);
|
|
659
|
+
return true;
|
|
660
|
+
} catch (e) {
|
|
661
|
+
return e?.code === 'EPERM';
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function countLiveChannelClients() {
|
|
666
|
+
let live = 0;
|
|
667
|
+
const now = Date.now();
|
|
668
|
+
try {
|
|
669
|
+
fs.mkdirSync(CHANNEL_CLIENT_DIR, { recursive: true });
|
|
670
|
+
for (const file of fs.readdirSync(CHANNEL_CLIENT_DIR)) {
|
|
671
|
+
if (!file.endsWith('.json')) continue;
|
|
672
|
+
const full = path.join(CHANNEL_CLIENT_DIR, file);
|
|
673
|
+
let item = null;
|
|
674
|
+
let st = null;
|
|
675
|
+
try {
|
|
676
|
+
st = fs.statSync(full);
|
|
677
|
+
item = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
678
|
+
} catch {
|
|
679
|
+
try { fs.unlinkSync(full); } catch {}
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const pid = Number(item?.pid ?? file.replace(/\.json$/, ''));
|
|
683
|
+
const fresh = now - Math.max(Number(item?.updatedAt) || 0, st.mtimeMs) < 20_000;
|
|
684
|
+
if (pidAlive(pid) && fresh) {
|
|
685
|
+
live += 1;
|
|
686
|
+
} else {
|
|
687
|
+
try { fs.unlinkSync(full); } catch {}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch {}
|
|
691
|
+
return live;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function hasChannelBackgroundWork() {
|
|
695
|
+
if (!channelBridgeActive || !bridgeRuntimeConnected) return false;
|
|
696
|
+
if (config.webhook?.enabled === true) return true;
|
|
697
|
+
if (Array.isArray(config.events?.rules) && config.events.rules.length > 0) return true;
|
|
698
|
+
if (Array.isArray(config.nonInteractive) && config.nonInteractive.length > 0) return true;
|
|
699
|
+
if (Array.isArray(config.interactive) && config.interactive.length > 0) return true;
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function checkChannelDaemonIdle() {
|
|
704
|
+
if (!_isChannelDaemonMode || CHANNEL_DAEMON_IDLE_TTL_MS <= 0) return;
|
|
705
|
+
const liveClients = countLiveChannelClients();
|
|
706
|
+
if (liveClients > 0) {
|
|
707
|
+
channelDaemonLastClientAt = Date.now();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (hasChannelBackgroundWork()) {
|
|
711
|
+
const now = Date.now();
|
|
712
|
+
if (now - channelDaemonBackgroundLogAt > 60_000) {
|
|
713
|
+
channelDaemonBackgroundLogAt = now;
|
|
714
|
+
try { process.stderr.write('[channels-worker] daemon idle: keeping alive for configured background work\n'); } catch {}
|
|
715
|
+
}
|
|
716
|
+
channelDaemonLastClientAt = Date.now();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (Date.now() - channelDaemonLastClientAt < CHANNEL_DAEMON_IDLE_TTL_MS) return;
|
|
720
|
+
try { process.stderr.write(`[channels-worker] daemon idle TTL elapsed (${CHANNEL_DAEMON_IDLE_TTL_MS}ms) — shutting down\n`); } catch {}
|
|
721
|
+
stop()
|
|
722
|
+
.then(() => process.exit(0))
|
|
723
|
+
.catch((e) => {
|
|
724
|
+
try { process.stderr.write(`[channels-worker] daemon idle shutdown failed: ${e?.message || e}\n`); } catch {}
|
|
725
|
+
process.exit(1);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function startChannelDaemonIdleMonitor() {
|
|
730
|
+
if (!_isChannelDaemonMode || CHANNEL_DAEMON_IDLE_TTL_MS <= 0 || channelDaemonIdleTimer) return;
|
|
731
|
+
channelDaemonLastClientAt = Date.now();
|
|
732
|
+
channelDaemonIdleTimer = setInterval(checkChannelDaemonIdle, 5000);
|
|
733
|
+
channelDaemonIdleTimer.unref?.();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function stopChannelDaemonIdleMonitor() {
|
|
737
|
+
if (!channelDaemonIdleTimer) return;
|
|
738
|
+
clearInterval(channelDaemonIdleTimer);
|
|
739
|
+
channelDaemonIdleTimer = null;
|
|
740
|
+
}
|
|
615
741
|
// Owner gating here is multi-process runtime coordination: only the active
|
|
616
742
|
// bindingReady gates all send paths until the boot-time refreshBridgeOwnership
|
|
617
743
|
// ({ restoreBinding: true }) call completes. Without this, scheduler/webhook
|
|
@@ -870,7 +996,7 @@ const OWNER_ROUTES = {
|
|
|
870
996
|
},
|
|
871
997
|
"/trigger-schedule": async (req, res, body) => {
|
|
872
998
|
// Native fallback for `mcp__trigger_schedule` so out-of-band
|
|
873
|
-
// verification works when the MCP stdio bridge is down (
|
|
999
|
+
// verification works when the MCP stdio bridge is down (host agent
|
|
874
1000
|
// disconnected, supervisor restart pending, etc.). Same authz as
|
|
875
1001
|
// /inject — x-owner-token must equal INSTANCE_ID.
|
|
876
1002
|
if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
|
|
@@ -1241,7 +1367,7 @@ function getBridgeOwnershipSnapshot() {
|
|
|
1241
1367
|
// MIXDOG_PIN_OWNER=1 in the owning process writes `pinned:true` into
|
|
1242
1368
|
// active-instance.json. Pinned owners ignore the 10 s stale window — they
|
|
1243
1369
|
// only relinquish ownership when their OS process actually dies. Set per
|
|
1244
|
-
// session (env var on the
|
|
1370
|
+
// session (env var on the host agent shell) to lock that Lead as the
|
|
1245
1371
|
// schedule/webhook receiver across multi-session use.
|
|
1246
1372
|
function canStealOwnership(active) {
|
|
1247
1373
|
if (!active) return true;
|
|
@@ -1730,14 +1856,14 @@ function injectAndRecord(channelId, name, content, options) {
|
|
|
1730
1856
|
// outbound body. Markers are
|
|
1731
1857
|
// intentionally prepended onto tool RESULTS upstream (tool-loop-guard.mjs
|
|
1732
1858
|
// build*Warn) so the model
|
|
1733
|
-
// self-corrects, but
|
|
1859
|
+
// self-corrects, but agent roles commonly echo them and we don't want them
|
|
1734
1860
|
// surfacing in Discord / Lead channel push.
|
|
1735
1861
|
if (typeof content === 'string') content = stripSoftWarns(content);
|
|
1736
|
-
// Skip-protocol guard:
|
|
1862
|
+
// Skip-protocol guard: agents (webhook-handler / scheduler-task)
|
|
1737
1863
|
// prefix `[meta:silent]` on the first line to opt out
|
|
1738
1864
|
// of Lead inject for genuine no-op results (label-only events, dedup,
|
|
1739
1865
|
// "nothing to report"). The body still goes to Discord for audit; only
|
|
1740
|
-
// the Lead-context inject is suppressed. See rules/
|
|
1866
|
+
// the Lead-context inject is suppressed. See rules/agent/20-skip-protocol.md.
|
|
1741
1867
|
if (typeof content === 'string') {
|
|
1742
1868
|
const m = content.match(/^\s*\[meta:silent\][^\n]*\n?([\s\S]*)$/);
|
|
1743
1869
|
if (m) {
|
|
@@ -1829,7 +1955,7 @@ function wireWebhookHandlers() {
|
|
|
1829
1955
|
+ ". Relay the finding to the user naturally — summarize clearly, call out any issues, and note what needs a decision.";
|
|
1830
1956
|
const notifyFn = (text, meta = {}) => {
|
|
1831
1957
|
if (!text) return;
|
|
1832
|
-
// Webhook skip protocol: when the
|
|
1958
|
+
// Webhook skip protocol: when the agent worker emits a `[meta:silent]`
|
|
1833
1959
|
// marker (optionally behind model/role tag prefixes), the event is a
|
|
1834
1960
|
// no-op (label-only, dedup, "nothing to report"). Drop the message
|
|
1835
1961
|
// entirely — neither Lead inject nor Discord forward — instead of the
|
|
@@ -1979,7 +2105,7 @@ try {
|
|
|
1979
2105
|
}
|
|
1980
2106
|
}
|
|
1981
2107
|
// No matching pending request — leave the signal on disk so a
|
|
1982
|
-
//
|
|
2108
|
+
// agent role hook (or other consumer) gets a chance to claim it.
|
|
1983
2109
|
if (!oldestKey || !oldestEntry) return;
|
|
1984
2110
|
if (oldestEntry.channelId && oldestEntry.messageId) {
|
|
1985
2111
|
try {
|
|
@@ -2022,7 +2148,7 @@ try {
|
|
|
2022
2148
|
backend.onInteraction = (interaction) => {
|
|
2023
2149
|
// Channel-route permission reply. Custom_id format: perm-ch-{request_id}-{allow|session|deny}.
|
|
2024
2150
|
// request_id is the 5-letter short ID CC generates via shortRequestId().
|
|
2025
|
-
// Emit notifications/claude/channel/permission back to
|
|
2151
|
+
// Emit notifications/claude/channel/permission back to the MCP host; the race
|
|
2026
2152
|
// logic in interactiveHandler.ts resolves the pending request and dismisses
|
|
2027
2153
|
// every other racer (local dialog, bridge, hook, classifier).
|
|
2028
2154
|
if (interaction.customId?.startsWith("perm-ch-")) {
|
|
@@ -2989,6 +3115,7 @@ async function init(_sharedMcp) {
|
|
|
2989
3115
|
});
|
|
2990
3116
|
}
|
|
2991
3117
|
async function start() {
|
|
3118
|
+
startChannelDaemonIdleMonitor();
|
|
2992
3119
|
channelBridgeActive = true;
|
|
2993
3120
|
writeBridgeState(true);
|
|
2994
3121
|
if (_isCliOwnedMode) {
|
|
@@ -3012,6 +3139,7 @@ async function start() {
|
|
|
3012
3139
|
})();
|
|
3013
3140
|
}
|
|
3014
3141
|
async function stop() {
|
|
3142
|
+
stopChannelDaemonIdleMonitor();
|
|
3015
3143
|
try { await stopVoiceWhisperServer(); } catch {}
|
|
3016
3144
|
await stopOwnedRuntime("unified server stop");
|
|
3017
3145
|
cleanupInstanceRuntimeFiles(INSTANCE_ID);
|
|
@@ -3182,16 +3310,16 @@ if (_isWorkerMode && process.send) {
|
|
|
3182
3310
|
} catch { /* best-effort */ }
|
|
3183
3311
|
return;
|
|
3184
3312
|
}
|
|
3185
|
-
//
|
|
3313
|
+
// Host permission request → Discord Allow/Deny prompt.
|
|
3186
3314
|
// Parent (server.mjs) receives notifications/claude/channel/permission_request
|
|
3187
|
-
// from
|
|
3315
|
+
// from the MCP host and forwards the params here. We post a buttoned message;
|
|
3188
3316
|
// button clicks are handled in backend.onInteraction and sent back to CC as
|
|
3189
3317
|
// notifications/claude/channel/permission via sendNotifyToParent.
|
|
3190
3318
|
if (msg && msg.type === 'permission_request_inbound') {
|
|
3191
3319
|
try {
|
|
3192
3320
|
const { request_id, tool_name, description, input_preview } = msg.params || {};
|
|
3193
3321
|
// tool_input arrives via the passthrough() schema in server.mjs when
|
|
3194
|
-
//
|
|
3322
|
+
// The host includes it in the permission_request notification.
|
|
3195
3323
|
// Used to bind the pendingPermRequest to a specific file so two
|
|
3196
3324
|
// concurrent Edit/Write requests cannot cross-approve via the
|
|
3197
3325
|
// terminal signal.
|