mixdog 0.8.1 → 0.9.1
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 +34 -28
- 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 +205 -33
- 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 +1222 -90
- 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 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- 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 +108 -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 +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -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 +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- 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 +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- 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 +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- 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 +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- 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.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 +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- 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 +519 -118
- 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/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- 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 +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -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 +104 -473
- 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 +44 -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 +139 -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 -95
- 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 +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- 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 +428 -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 +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- 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 +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -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 +756 -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 +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- 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/ink.js +54 -8
- 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/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/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/channels/lib/hook-pipe-server.mjs +0 -671
- 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,39 +329,61 @@ 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 };
|
|
348
344
|
}
|
|
345
|
+
return params;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function publishChannelNotify(method, params) {
|
|
349
349
|
try {
|
|
350
|
-
|
|
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');
|
|
351
366
|
} catch (err) {
|
|
352
|
-
try { process.stderr.write(`mixdog channels: notify
|
|
367
|
+
try { process.stderr.write(`mixdog channels: notify bus write failed: ${err && err.message || err}\n`); } catch {}
|
|
353
368
|
}
|
|
354
369
|
}
|
|
355
370
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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;
|
|
382
|
+
}
|
|
359
383
|
try {
|
|
360
|
-
process.send({ type: '
|
|
384
|
+
process.send({ type: 'notify', method, params: outParams });
|
|
361
385
|
} catch (err) {
|
|
362
|
-
try { process.stderr.write(`mixdog channels:
|
|
386
|
+
try { process.stderr.write(`mixdog channels: notify IPC send failed: ${err && err.message || err}\n`); } catch {}
|
|
363
387
|
}
|
|
364
388
|
}
|
|
365
389
|
|
|
@@ -612,6 +636,98 @@ const ACTIVE_OWNER_STALE_MS = 1e4;
|
|
|
612
636
|
// never blocks process exit. Single JSON atomic write, no measurable load.
|
|
613
637
|
const OWNER_HEARTBEAT_INTERVAL_MS = 5e3;
|
|
614
638
|
let ownerHeartbeatTimer = null;
|
|
639
|
+
let channelDaemonIdleTimer = null;
|
|
640
|
+
let channelDaemonLastClientAt = Date.now();
|
|
641
|
+
let channelDaemonBackgroundLogAt = 0;
|
|
642
|
+
const CHANNEL_CLIENT_DIR = path.join(RUNTIME_ROOT, 'channel-clients');
|
|
643
|
+
|
|
644
|
+
function pidAlive(pid) {
|
|
645
|
+
const n = Number(pid);
|
|
646
|
+
if (!Number.isInteger(n) || n <= 0) return false;
|
|
647
|
+
try {
|
|
648
|
+
process.kill(n, 0);
|
|
649
|
+
return true;
|
|
650
|
+
} catch (e) {
|
|
651
|
+
return e?.code === 'EPERM';
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function countLiveChannelClients() {
|
|
656
|
+
let live = 0;
|
|
657
|
+
const now = Date.now();
|
|
658
|
+
try {
|
|
659
|
+
fs.mkdirSync(CHANNEL_CLIENT_DIR, { recursive: true });
|
|
660
|
+
for (const file of fs.readdirSync(CHANNEL_CLIENT_DIR)) {
|
|
661
|
+
if (!file.endsWith('.json')) continue;
|
|
662
|
+
const full = path.join(CHANNEL_CLIENT_DIR, file);
|
|
663
|
+
let item = null;
|
|
664
|
+
let st = null;
|
|
665
|
+
try {
|
|
666
|
+
st = fs.statSync(full);
|
|
667
|
+
item = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
668
|
+
} catch {
|
|
669
|
+
try { fs.unlinkSync(full); } catch {}
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
const pid = Number(item?.pid ?? file.replace(/\.json$/, ''));
|
|
673
|
+
const fresh = now - Math.max(Number(item?.updatedAt) || 0, st.mtimeMs) < 20_000;
|
|
674
|
+
if (pidAlive(pid) && fresh) {
|
|
675
|
+
live += 1;
|
|
676
|
+
} else {
|
|
677
|
+
try { fs.unlinkSync(full); } catch {}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} catch {}
|
|
681
|
+
return live;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function hasChannelBackgroundWork() {
|
|
685
|
+
if (!channelBridgeActive || !bridgeRuntimeConnected) return false;
|
|
686
|
+
if (config.webhook?.enabled === true) return true;
|
|
687
|
+
if (Array.isArray(config.events?.rules) && config.events.rules.length > 0) return true;
|
|
688
|
+
if (Array.isArray(config.nonInteractive) && config.nonInteractive.length > 0) return true;
|
|
689
|
+
if (Array.isArray(config.interactive) && config.interactive.length > 0) return true;
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function checkChannelDaemonIdle() {
|
|
694
|
+
if (!_isChannelDaemonMode || CHANNEL_DAEMON_IDLE_TTL_MS <= 0) return;
|
|
695
|
+
const liveClients = countLiveChannelClients();
|
|
696
|
+
if (liveClients > 0) {
|
|
697
|
+
channelDaemonLastClientAt = Date.now();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (hasChannelBackgroundWork()) {
|
|
701
|
+
const now = Date.now();
|
|
702
|
+
if (now - channelDaemonBackgroundLogAt > 60_000) {
|
|
703
|
+
channelDaemonBackgroundLogAt = now;
|
|
704
|
+
try { process.stderr.write('[channels-worker] daemon idle: keeping alive for configured background work\n'); } catch {}
|
|
705
|
+
}
|
|
706
|
+
channelDaemonLastClientAt = Date.now();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (Date.now() - channelDaemonLastClientAt < CHANNEL_DAEMON_IDLE_TTL_MS) return;
|
|
710
|
+
try { process.stderr.write(`[channels-worker] daemon idle TTL elapsed (${CHANNEL_DAEMON_IDLE_TTL_MS}ms) — shutting down\n`); } catch {}
|
|
711
|
+
stop()
|
|
712
|
+
.then(() => process.exit(0))
|
|
713
|
+
.catch((e) => {
|
|
714
|
+
try { process.stderr.write(`[channels-worker] daemon idle shutdown failed: ${e?.message || e}\n`); } catch {}
|
|
715
|
+
process.exit(1);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function startChannelDaemonIdleMonitor() {
|
|
720
|
+
if (!_isChannelDaemonMode || CHANNEL_DAEMON_IDLE_TTL_MS <= 0 || channelDaemonIdleTimer) return;
|
|
721
|
+
channelDaemonLastClientAt = Date.now();
|
|
722
|
+
channelDaemonIdleTimer = setInterval(checkChannelDaemonIdle, 5000);
|
|
723
|
+
channelDaemonIdleTimer.unref?.();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function stopChannelDaemonIdleMonitor() {
|
|
727
|
+
if (!channelDaemonIdleTimer) return;
|
|
728
|
+
clearInterval(channelDaemonIdleTimer);
|
|
729
|
+
channelDaemonIdleTimer = null;
|
|
730
|
+
}
|
|
615
731
|
// Owner gating here is multi-process runtime coordination: only the active
|
|
616
732
|
// bindingReady gates all send paths until the boot-time refreshBridgeOwnership
|
|
617
733
|
// ({ restoreBinding: true }) call completes. Without this, scheduler/webhook
|
|
@@ -870,7 +986,7 @@ const OWNER_ROUTES = {
|
|
|
870
986
|
},
|
|
871
987
|
"/trigger-schedule": async (req, res, body) => {
|
|
872
988
|
// Native fallback for `mcp__trigger_schedule` so out-of-band
|
|
873
|
-
// verification works when the MCP stdio bridge is down (
|
|
989
|
+
// verification works when the MCP stdio bridge is down (host agent
|
|
874
990
|
// disconnected, supervisor restart pending, etc.). Same authz as
|
|
875
991
|
// /inject — x-owner-token must equal INSTANCE_ID.
|
|
876
992
|
if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
|
|
@@ -1015,26 +1131,6 @@ const OWNER_ROUTES = {
|
|
|
1015
1131
|
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
1016
1132
|
}
|
|
1017
1133
|
},
|
|
1018
|
-
"/recap/reset": async (req, res /*, body*/) => {
|
|
1019
|
-
if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ error: "POST required" })); return; }
|
|
1020
|
-
if (!requireOwnerToken(req, res)) return;
|
|
1021
|
-
// Called by hooks/session-start.cjs on `/clear` (matcher startup|clear).
|
|
1022
|
-
// The session-start hook runs in a separate cjs process with no IPC
|
|
1023
|
-
// handle to this forked channels child, so it can't drop recap
|
|
1024
|
-
// status directly. Reset to an `empty` baseline so the statusline
|
|
1025
|
-
// doesn't carry the prior session's `injected`/`error` recap badge
|
|
1026
|
-
// into the cleared session.
|
|
1027
|
-
const now = Date.now();
|
|
1028
|
-
recapState.state = 'empty';
|
|
1029
|
-
recapState.running = false;
|
|
1030
|
-
recapState.startedAt = null;
|
|
1031
|
-
recapState.lastCompletedAt = now;
|
|
1032
|
-
recapState.updatedAt = now;
|
|
1033
|
-
recapState.errorMessage = null;
|
|
1034
|
-
sendRecapStateToParent();
|
|
1035
|
-
res.writeHead(200);
|
|
1036
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1037
|
-
},
|
|
1038
1134
|
"/cycle1": async (req, res, body) => {
|
|
1039
1135
|
if (req.method !== "POST") { res.writeHead(405); res.end(JSON.stringify({ ok: false, reason: "method-not-allowed", error: "POST required" })); return; }
|
|
1040
1136
|
if (!requireOwnerToken(req, res)) return;
|
|
@@ -1241,7 +1337,7 @@ function getBridgeOwnershipSnapshot() {
|
|
|
1241
1337
|
// MIXDOG_PIN_OWNER=1 in the owning process writes `pinned:true` into
|
|
1242
1338
|
// active-instance.json. Pinned owners ignore the 10 s stale window — they
|
|
1243
1339
|
// only relinquish ownership when their OS process actually dies. Set per
|
|
1244
|
-
// session (env var on the
|
|
1340
|
+
// session (env var on the host agent shell) to lock that Lead as the
|
|
1245
1341
|
// schedule/webhook receiver across multi-session use.
|
|
1246
1342
|
function canStealOwnership(active) {
|
|
1247
1343
|
if (!active) return true;
|
|
@@ -1730,14 +1826,14 @@ function injectAndRecord(channelId, name, content, options) {
|
|
|
1730
1826
|
// outbound body. Markers are
|
|
1731
1827
|
// intentionally prepended onto tool RESULTS upstream (tool-loop-guard.mjs
|
|
1732
1828
|
// build*Warn) so the model
|
|
1733
|
-
// self-corrects, but
|
|
1829
|
+
// self-corrects, but agent roles commonly echo them and we don't want them
|
|
1734
1830
|
// surfacing in Discord / Lead channel push.
|
|
1735
1831
|
if (typeof content === 'string') content = stripSoftWarns(content);
|
|
1736
|
-
// Skip-protocol guard:
|
|
1832
|
+
// Skip-protocol guard: agents (webhook-handler / scheduler-task)
|
|
1737
1833
|
// prefix `[meta:silent]` on the first line to opt out
|
|
1738
1834
|
// of Lead inject for genuine no-op results (label-only events, dedup,
|
|
1739
1835
|
// "nothing to report"). The body still goes to Discord for audit; only
|
|
1740
|
-
// the Lead-context inject is suppressed. See rules/
|
|
1836
|
+
// the Lead-context inject is suppressed. See rules/agent/20-skip-protocol.md.
|
|
1741
1837
|
if (typeof content === 'string') {
|
|
1742
1838
|
const m = content.match(/^\s*\[meta:silent\][^\n]*\n?([\s\S]*)$/);
|
|
1743
1839
|
if (m) {
|
|
@@ -1829,7 +1925,7 @@ function wireWebhookHandlers() {
|
|
|
1829
1925
|
+ ". Relay the finding to the user naturally — summarize clearly, call out any issues, and note what needs a decision.";
|
|
1830
1926
|
const notifyFn = (text, meta = {}) => {
|
|
1831
1927
|
if (!text) return;
|
|
1832
|
-
// Webhook skip protocol: when the
|
|
1928
|
+
// Webhook skip protocol: when the agent worker emits a `[meta:silent]`
|
|
1833
1929
|
// marker (optionally behind model/role tag prefixes), the event is a
|
|
1834
1930
|
// no-op (label-only, dedup, "nothing to report"). Drop the message
|
|
1835
1931
|
// entirely — neither Lead inject nor Discord forward — instead of the
|
|
@@ -1979,7 +2075,7 @@ try {
|
|
|
1979
2075
|
}
|
|
1980
2076
|
}
|
|
1981
2077
|
// No matching pending request — leave the signal on disk so a
|
|
1982
|
-
//
|
|
2078
|
+
// agent role hook (or other consumer) gets a chance to claim it.
|
|
1983
2079
|
if (!oldestKey || !oldestEntry) return;
|
|
1984
2080
|
if (oldestEntry.channelId && oldestEntry.messageId) {
|
|
1985
2081
|
try {
|
|
@@ -2022,7 +2118,7 @@ try {
|
|
|
2022
2118
|
backend.onInteraction = (interaction) => {
|
|
2023
2119
|
// Channel-route permission reply. Custom_id format: perm-ch-{request_id}-{allow|session|deny}.
|
|
2024
2120
|
// request_id is the 5-letter short ID CC generates via shortRequestId().
|
|
2025
|
-
// Emit notifications/claude/channel/permission back to
|
|
2121
|
+
// Emit notifications/claude/channel/permission back to the MCP host; the race
|
|
2026
2122
|
// logic in interactiveHandler.ts resolves the pending request and dismisses
|
|
2027
2123
|
// every other racer (local dialog, bridge, hook, classifier).
|
|
2028
2124
|
if (interaction.customId?.startsWith("perm-ch-")) {
|
|
@@ -2989,6 +3085,7 @@ async function init(_sharedMcp) {
|
|
|
2989
3085
|
});
|
|
2990
3086
|
}
|
|
2991
3087
|
async function start() {
|
|
3088
|
+
startChannelDaemonIdleMonitor();
|
|
2992
3089
|
channelBridgeActive = true;
|
|
2993
3090
|
writeBridgeState(true);
|
|
2994
3091
|
if (_isCliOwnedMode) {
|
|
@@ -3012,6 +3109,7 @@ async function start() {
|
|
|
3012
3109
|
})();
|
|
3013
3110
|
}
|
|
3014
3111
|
async function stop() {
|
|
3112
|
+
stopChannelDaemonIdleMonitor();
|
|
3015
3113
|
try { await stopVoiceWhisperServer(); } catch {}
|
|
3016
3114
|
await stopOwnedRuntime("unified server stop");
|
|
3017
3115
|
cleanupInstanceRuntimeFiles(INSTANCE_ID);
|
|
@@ -3182,16 +3280,16 @@ if (_isWorkerMode && process.send) {
|
|
|
3182
3280
|
} catch { /* best-effort */ }
|
|
3183
3281
|
return;
|
|
3184
3282
|
}
|
|
3185
|
-
//
|
|
3283
|
+
// Host permission request → Discord Allow/Deny prompt.
|
|
3186
3284
|
// Parent (server.mjs) receives notifications/claude/channel/permission_request
|
|
3187
|
-
// from
|
|
3285
|
+
// from the MCP host and forwards the params here. We post a buttoned message;
|
|
3188
3286
|
// button clicks are handled in backend.onInteraction and sent back to CC as
|
|
3189
3287
|
// notifications/claude/channel/permission via sendNotifyToParent.
|
|
3190
3288
|
if (msg && msg.type === 'permission_request_inbound') {
|
|
3191
3289
|
try {
|
|
3192
3290
|
const { request_id, tool_name, description, input_preview } = msg.params || {};
|
|
3193
3291
|
// tool_input arrives via the passthrough() schema in server.mjs when
|
|
3194
|
-
//
|
|
3292
|
+
// The host includes it in the permission_request notification.
|
|
3195
3293
|
// Used to bind the pendingPermRequest to a specific file so two
|
|
3196
3294
|
// concurrent Edit/Write requests cannot cross-approve via the
|
|
3197
3295
|
// terminal signal.
|