mixdog 0.9.95 → 0.9.97
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/README.md +0 -17
- package/package.json +16 -12
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +21 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +34 -34
- package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +76 -248
- package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +9 -6
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +139 -72
- package/src/runtime/shared/config.mjs +120 -47
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +150 -100
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -27
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +47 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +63 -39
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +45 -17
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +96 -24
- package/src/session-runtime/settings-api.mjs +12 -24
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/channel-admin.mjs +53 -64
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/session-client.mjs +1134 -0
- package/src/standalone/session-protocol.mjs +192 -0
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +34 -21
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +3 -4
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +48 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +31 -33
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +10 -3
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +1031 -1676
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +12 -13
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +38 -4
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
- package/src/tui/{engine → session}/session-api.mjs +110 -93
- package/src/tui/{engine → session}/session-flow.mjs +8 -2
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +38 -176
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
- package/src/tui/session.mjs +22 -0
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-client.mjs +0 -894
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
- package/src/standalone/engine-daemon-protocol.mjs +0 -33
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
findLineSequence,
|
|
29
29
|
findLineSequenceEscapeEquiv,
|
|
30
30
|
longestCommonSubstringLen,
|
|
31
|
+
boundedEditDistance,
|
|
32
|
+
EDIT_DISTANCE_ALLOWANCE_PER_LINE,
|
|
31
33
|
splitTextLinesForPatch,
|
|
32
34
|
firstMeaningfulPatchLine,
|
|
33
35
|
nearestPatchLineHint,
|
|
@@ -162,6 +164,18 @@ function noteV4AHunkAmbiguity(displayPath, sourceLines, loc) {
|
|
|
162
164
|
);
|
|
163
165
|
}
|
|
164
166
|
|
|
167
|
+
// `*** End of File` was relaxed to locate this hunk: report where it actually
|
|
168
|
+
// landed so a mid-file hunk that carried the marker by mistake is visible in
|
|
169
|
+
// the same turn instead of silently looking like an end-of-file edit.
|
|
170
|
+
function noteV4AEofSignalIgnored(displayPath, loc) {
|
|
171
|
+
if (_v4aAmbiguityNotices.size >= V4A_AMBIGUITY_NOTICE_CAP) return;
|
|
172
|
+
if (!loc?.eofSignalIgnored) return;
|
|
173
|
+
_v4aAmbiguityNotices.add(
|
|
174
|
+
`${displayPath}: hunk carried *** End of File but its context is at line ${loc.oldStartIdx + 1}, `
|
|
175
|
+
+ 'not the end of file; the marker was ignored. Drop it unless the hunk really ends the file.',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
165
179
|
export function drainV4AAmbiguityNotices() {
|
|
166
180
|
if (_v4aAmbiguityNotices.size === 0) return [];
|
|
167
181
|
const list = [..._v4aAmbiguityNotices];
|
|
@@ -299,14 +313,50 @@ function findContextTolerantWindow(sourceLines, oldLines, oldTags) {
|
|
|
299
313
|
return windows.length === 1 ? { start: windows[0] } : null;
|
|
300
314
|
}
|
|
301
315
|
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
316
|
+
// Bounded edit-distance tier (fuzzy, after the context-tolerance tier).
|
|
317
|
+
// Recovers the block a model retyped from memory with a couple of characters
|
|
318
|
+
// off — a dropped bracket, a mistyped short token — where the tolerance tier
|
|
319
|
+
// refuses because the drift sits on a deletion line. Guards keep it safe:
|
|
320
|
+
// - TOTAL distance across the block stays within ~0.34 characters per line
|
|
321
|
+
// (3 lines buy exactly 1 character), measured on trimmed text;
|
|
322
|
+
// - at least 2 non-blank lines must match byte-exactly as anchors;
|
|
323
|
+
// - the qualifying window must be UNIQUE across the ENTIRE file;
|
|
324
|
+
// - the caller REMAPS every old line to the file's on-disk text, so the
|
|
325
|
+
// emitted hunk stays byte-exact for the applier.
|
|
326
|
+
function findEditDistanceWindow(sourceLines, oldLines) {
|
|
327
|
+
const n = (oldLines || []).length;
|
|
328
|
+
const maxDistance = Math.floor(n * EDIT_DISTANCE_ALLOWANCE_PER_LINE);
|
|
329
|
+
if (n < 3 || maxDistance <= 0) return null;
|
|
330
|
+
const windows = [];
|
|
331
|
+
for (let i = 0; i + n <= sourceLines.length && windows.length < 2; i++) {
|
|
332
|
+
let total = 0;
|
|
333
|
+
let exact = 0;
|
|
334
|
+
let ok = true;
|
|
335
|
+
for (let k = 0; k < n; k++) {
|
|
336
|
+
const pat = String(oldLines[k] ?? '');
|
|
337
|
+
const src = String(sourceLines[i + k] ?? '');
|
|
338
|
+
if (src === pat) {
|
|
339
|
+
if (pat.trim()) exact++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
total += boundedEditDistance(src.trim(), pat.trim(), maxDistance - total);
|
|
343
|
+
if (total > maxDistance) { ok = false; break; }
|
|
344
|
+
}
|
|
345
|
+
if (ok && total > 0 && exact >= 2) windows.push(i);
|
|
346
|
+
}
|
|
347
|
+
return windows.length === 1 ? { start: windows[0] } : null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Recovery for outer context the model got wrong — a stray copied line, or a
|
|
351
|
+
// whole surrounding block retyped from memory — around an edit whose deletion
|
|
352
|
+
// lines are current. Trim only contiguous outer ' ' lines — never '-' or '+'
|
|
353
|
+
// lines — smallest trim first, and accept the shortened old block only when it
|
|
354
|
+
// matches byte-exactly at exactly one place in the ENTIRE file. That
|
|
355
|
+
// uniqueness IS the position proof, so the trim budget runs to all available
|
|
356
|
+
// outer context: a byte-exact, file-unique deletion core cannot land anywhere
|
|
357
|
+
// else, whatever the caller wrote around it. Requiring a deletion keeps this
|
|
358
|
+
// off insertion-only hunks (they carry no payload to anchor on), and any
|
|
359
|
+
// competing trim plan or duplicate occurrence stays a hard context miss.
|
|
310
360
|
function findOuterContextTrimmedWindow(sourceLines, hunk, stats) {
|
|
311
361
|
const body = (hunk?.lines || []).filter(
|
|
312
362
|
(line) => typeof line === 'string'
|
|
@@ -325,7 +375,8 @@ function findOuterContextTrimmedWindow(sourceLines, hunk, stats) {
|
|
|
325
375
|
) trailingAvailable++;
|
|
326
376
|
if (leadingAvailable === 0 && trailingAvailable === 0) return null;
|
|
327
377
|
|
|
328
|
-
|
|
378
|
+
const maxTrimmed = leadingAvailable + trailingAvailable;
|
|
379
|
+
for (let totalTrimmed = 1; totalTrimmed <= maxTrimmed; totalTrimmed++) {
|
|
329
380
|
const plans = [];
|
|
330
381
|
let ambiguous = false;
|
|
331
382
|
for (let leading = 0; leading <= Math.min(totalTrimmed, leadingAvailable); leading++) {
|
|
@@ -424,7 +475,23 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
|
|
|
424
475
|
// of the patch's real character. On match, remap old/context lines to the
|
|
425
476
|
// file's on-disk form so untouched context stays byte-identical and the
|
|
426
477
|
// escape representation survives the edit.
|
|
427
|
-
|
|
478
|
+
// `*** End of File` is a HINT, not a hard constraint. When the EOF-anchored
|
|
479
|
+
// seek misses, retry the pattern as an ordinary in-file search: a mid-file
|
|
480
|
+
// hunk that carried the marker by mistake used to fail with a "context not
|
|
481
|
+
// found" that pointed at its own byte-perfect context, costing a whole retry
|
|
482
|
+
// turn. Once that happens the EOF signal is exhausted, so every recovery
|
|
483
|
+
// tier below treats the hunk as unmarked.
|
|
484
|
+
let eofSignalIgnored = false;
|
|
485
|
+
if (oldStartIdx < 0 && eof && oldLinesPattern.length > 0) {
|
|
486
|
+
const relaxedFrom = Math.max(0, anchorLine - 1);
|
|
487
|
+
const relaxed = findLineSequence(sourceLines, oldLinesPattern, relaxedFrom, relaxedFrom, { fuzzy, eof: false });
|
|
488
|
+
if (relaxed >= 0) {
|
|
489
|
+
oldStartIdx = relaxed;
|
|
490
|
+
eofSignalIgnored = true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (oldStartIdx < 0 && fuzzy && oldLinesPattern.length > 0) {
|
|
494
|
+
if (eof) eofSignalIgnored = true;
|
|
428
495
|
const from = Math.max(0, anchorLine - 1);
|
|
429
496
|
const alt = findLineSequenceEscapeEquiv(sourceLines, oldLinesPattern, from, from);
|
|
430
497
|
if (alt >= 0) {
|
|
@@ -447,7 +514,7 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
|
|
|
447
514
|
// lines are remapped to the file's actual lines on BOTH the old and new
|
|
448
515
|
// side (same shape as the escape-equiv remap above); remapping bails on any
|
|
449
516
|
// string ambiguity so an unrelated occurrence can never be rewritten.
|
|
450
|
-
if (oldStartIdx < 0 && fuzzy &&
|
|
517
|
+
if (oldStartIdx < 0 && fuzzy && oldLinesPattern.length >= 3) {
|
|
451
518
|
const tolTags = trimmedTrailing ? stats.oldTags.slice(0, -1) : stats.oldTags;
|
|
452
519
|
const tol = findContextTolerantWindow(sourceLines, oldLinesPattern, tolTags);
|
|
453
520
|
if (tol) {
|
|
@@ -474,7 +541,7 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
|
|
|
474
541
|
// This fallback is deliberately narrower than the context-tolerance tier:
|
|
475
542
|
// fuzzy mode only, non-EOF, no newline-sentinel interaction, deletion hunks
|
|
476
543
|
// only, byte-exact remaining block, and one unique whole-file location.
|
|
477
|
-
if (oldStartIdx < 0 && fuzzy &&
|
|
544
|
+
if (oldStartIdx < 0 && fuzzy && trimmedTrailing === 0) {
|
|
478
545
|
const trimmed = findOuterContextTrimmedWindow(sourceLines, hunk, stats);
|
|
479
546
|
if (trimmed) {
|
|
480
547
|
oldStartIdx = trimmed.start;
|
|
@@ -484,6 +551,31 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
|
|
|
484
551
|
trimmedTrailingContext = trimmed.trailing;
|
|
485
552
|
}
|
|
486
553
|
}
|
|
554
|
+
// Edit-distance tier: same remap contract as the context-tolerance tier —
|
|
555
|
+
// the window's on-disk lines replace the patch's near-miss lines on BOTH
|
|
556
|
+
// sides, and any string ambiguity abandons the rescue.
|
|
557
|
+
if (oldStartIdx < 0 && fuzzy && oldLinesPattern.length >= 3) {
|
|
558
|
+
const near = findEditDistanceWindow(sourceLines, oldLinesPattern);
|
|
559
|
+
if (near) {
|
|
560
|
+
const remapped = new Map();
|
|
561
|
+
let ambiguous = false;
|
|
562
|
+
for (let k = 0; k < oldLinesPattern.length; k++) {
|
|
563
|
+
const pat = oldLinesPattern[k];
|
|
564
|
+
const src = sourceLines[near.start + k];
|
|
565
|
+
if (src === pat) continue;
|
|
566
|
+
if (
|
|
567
|
+
oldLinesPattern.filter((l) => l === pat).length > 1
|
|
568
|
+
|| (remapped.has(pat) && remapped.get(pat) !== src)
|
|
569
|
+
) { ambiguous = true; break; }
|
|
570
|
+
remapped.set(pat, src);
|
|
571
|
+
}
|
|
572
|
+
if (!ambiguous) {
|
|
573
|
+
newLinesPattern = newLinesPattern.map((l) => remapped.get(l) ?? l);
|
|
574
|
+
oldLinesPattern = oldLinesPattern.map((_, k) => sourceLines[near.start + k]);
|
|
575
|
+
oldStartIdx = near.start;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
487
579
|
if (oldStartIdx < 0) {
|
|
488
580
|
const msg = `V4A hunk context not found: ${formatV4AHunkLocator(hunk)};${formatV4AContextMissHint(sourceLines, stats, anchorLine)}`;
|
|
489
581
|
return { error: msg };
|
|
@@ -506,6 +598,9 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
|
|
|
506
598
|
// hunk carried an @@ anchor (an anchored hunk is never ambiguous).
|
|
507
599
|
pattern: oldLinesPattern,
|
|
508
600
|
anchored: (hunk.anchors || []).filter(Boolean).length > 0,
|
|
601
|
+
// True when the hunk's `*** End of File` marker had to be relaxed to
|
|
602
|
+
// locate it (drives the non-fatal notice on the success output).
|
|
603
|
+
eofSignalIgnored,
|
|
509
604
|
};
|
|
510
605
|
}
|
|
511
606
|
|
|
@@ -1011,6 +1106,7 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
|
|
|
1011
1106
|
],
|
|
1012
1107
|
});
|
|
1013
1108
|
noteV4AHunkAmbiguity(displayPath, sourceLines, loc);
|
|
1109
|
+
noteV4AEofSignalIgnored(displayPath, loc);
|
|
1014
1110
|
nextSearchLine = loc.nextSearchLine;
|
|
1015
1111
|
}
|
|
1016
1112
|
sectionHunkEntries.sort((a, b) => (a.start - b.start) || (a.order - b.order));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const APPLY_PATCH_LARK_GRAMMAR = `start: begin_patch hunk+ end_patch
|
|
1
|
+
const APPLY_PATCH_LARK_GRAMMAR = `start: begin_patch root_line? hunk+ end_patch
|
|
2
2
|
begin_patch: "*** Begin Patch" LF
|
|
3
|
+
root_line: "*** Root: " filename LF
|
|
3
4
|
end_patch: "*** End Patch" LF?
|
|
4
5
|
|
|
5
6
|
hunk: add_hunk | delete_hunk | update_hunk
|
|
@@ -28,7 +29,7 @@ eof_line: "*** End of File" LF
|
|
|
28
29
|
// Batching stays a rules-level policy: every new edit goes in one patch, with
|
|
29
30
|
// one file block per target.
|
|
30
31
|
const APPLY_PATCH_FREEFORM_DESCRIPTION =
|
|
31
|
-
'
|
|
32
|
+
'Edit files with `apply_patch`. FREEFORM input; do not wrap the patch in JSON.';
|
|
32
33
|
|
|
33
34
|
// JSON-schema fallback providers (Anthropic and other non-grammar surfaces)
|
|
34
35
|
// get the full V4A instructions inline: without a grammar the model has
|
|
@@ -41,13 +42,10 @@ const APPLY_PATCH_JSON_DESCRIPTION = [
|
|
|
41
42
|
'*** Begin Patch',
|
|
42
43
|
'[file sections]',
|
|
43
44
|
'*** End Patch',
|
|
44
|
-
'Every section starts with exactly one header:',
|
|
45
|
-
'
|
|
46
|
-
'-
|
|
47
|
-
'-
|
|
48
|
-
'Update hunks open with @@ or @@ <enclosing class/function>; prefix every line with space (context), - (remove), or + (add); a hunk at end of file may close with *** End of File.',
|
|
49
|
-
'Copy 3 context lines above and below verbatim from the newest tool output of the file (after your own patch, use post-patch content; never retype from memory). Do not duplicate overlapping context; if context is not unique, add enclosing @@ headers.',
|
|
50
|
-
'Use project-relative paths. Every added file line needs +. Never submit a compacted-history marker; re-read and create a fresh patch.',
|
|
45
|
+
'Every section starts with exactly one header: *** Add File: <path> (+content lines), *** Delete File: <path> (nothing after), *** Update File: <path> (optional *** Move to: <new path>).',
|
|
46
|
+
'Hunks open with @@ or @@ <symbol>; prefix lines with space, -, or +; an end-of-file hunk may close with *** End of File.',
|
|
47
|
+
'Copy 3 context lines above/below verbatim from the newest tool output — after your own patch use its post-patch body, never memory. No duplicate overlapping context; if still ambiguous, stack @@ headers: @@ class Foo then @@ def bar.',
|
|
48
|
+
'Project-relative paths; every added line needs +. Never submit a compacted-history marker — re-read and send a fresh patch.',
|
|
51
49
|
].join('\n');
|
|
52
50
|
|
|
53
51
|
export const PATCH_TOOL_DEFS = [
|
|
@@ -65,8 +63,11 @@ export const PATCH_TOOL_DEFS = [
|
|
|
65
63
|
inputSchema: {
|
|
66
64
|
type: 'object',
|
|
67
65
|
properties: {
|
|
68
|
-
patch: { type: 'string', description: '
|
|
69
|
-
|
|
66
|
+
patch: { type: 'string', description: 'V4A patch text to apply; rules above.' },
|
|
67
|
+
root: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Write root; required only for targets outside the session directory.',
|
|
70
|
+
},
|
|
70
71
|
},
|
|
71
72
|
required: ['patch'],
|
|
72
73
|
additionalProperties: false,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// ./patch/orchestrator.mjs — apply_patch orchestration + executePatchTool
|
|
21
21
|
// + replay capture + UI-diff side-channel.
|
|
22
22
|
//
|
|
23
|
-
//
|
|
23
|
+
// Executor is unchanged: NATIVE-ONLY dispatch to the mixdog-patch Rust engine,
|
|
24
24
|
// no JS apply fallback. Public exports below match the pre-split surface.
|
|
25
25
|
|
|
26
26
|
// Side-effect import: schedules the native-patch prewarm on module load, as
|
|
@@ -75,8 +75,8 @@ const SHELL_ADMISSION_WAIT_MS = Number.isFinite(_envAdmissionWait) && _envAdmiss
|
|
|
75
75
|
? _envAdmissionWait
|
|
76
76
|
: 30_000;
|
|
77
77
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
78
|
+
// Default: capture child output via file fds (direct mode) instead of
|
|
79
|
+
// parent-side pipes. Opt back into pipe capture with
|
|
80
80
|
// MIXDOG_SHELL_PIPE_CAPTURE=1 (diagnostic escape hatch).
|
|
81
81
|
// win32 EXCEPTION: fd-based stdio entries are UV_INHERIT_FD, which makes
|
|
82
82
|
// libuv DROP CREATE_NO_WINDOW (libuv PR #1659) — the child shell then
|
|
@@ -145,9 +145,30 @@ function _abortableDelay(ms, signal) {
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
// True when an abort was raised because the user sent a NEW message while the
|
|
149
|
+
// command was running (session-api.mjs raises 'interrupt' when steering is
|
|
150
|
+
// pending, 'user-cancel' for a plain ESC). The reason travels as the
|
|
151
|
+
// SessionClosedError's `reason` field; a bare string / message fallback keeps
|
|
152
|
+
// non-session callers working. Anything unrecognized is treated as a real
|
|
153
|
+
// cancellation, so the kill path stays the default.
|
|
154
|
+
export function _abortReasonIsInterrupt(abortSignal) {
|
|
155
|
+
const raw = abortSignal?.reason;
|
|
156
|
+
if (!raw) return false;
|
|
157
|
+
if (typeof raw === 'string') return raw === 'interrupt';
|
|
158
|
+
if (typeof raw === 'object') {
|
|
159
|
+
if (raw.reason === 'interrupt') return true;
|
|
160
|
+
if (typeof raw.message === 'string' && /\breason=interrupt\b/.test(raw.message)) return true;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function acquireShellLeaseBounded(admission, {
|
|
166
|
+
abortSignal, label, dependency = 'scoped', ownerKey = null,
|
|
167
|
+
} = {}) {
|
|
149
168
|
if (!(SHELL_ADMISSION_WAIT_MS > 0)) {
|
|
150
|
-
return admission.acquire('shell', {
|
|
169
|
+
return admission.acquire('shell', {
|
|
170
|
+
signal: abortSignal || null, label, dependency, ownerKey,
|
|
171
|
+
});
|
|
151
172
|
}
|
|
152
173
|
const ctl = new AbortController();
|
|
153
174
|
const onAbort = () => {
|
|
@@ -165,7 +186,9 @@ export async function acquireShellLeaseBounded(admission, { abortSignal, label,
|
|
|
165
186
|
try {
|
|
166
187
|
for (;;) {
|
|
167
188
|
try {
|
|
168
|
-
const lease = await admission.acquire('shell', {
|
|
189
|
+
const lease = await admission.acquire('shell', {
|
|
190
|
+
signal: ctl.signal, label, dependency, ownerKey,
|
|
191
|
+
});
|
|
169
192
|
// Hand governance back to the caller's signal: the internal deadline
|
|
170
193
|
// controller may still fire in a lost race after grant, and a stale
|
|
171
194
|
// aborted signal on the lease would poison later parent-restore paths.
|
|
@@ -235,6 +258,7 @@ export function execShellCommand({
|
|
|
235
258
|
ownerSessionId,
|
|
236
259
|
backgroundOnTimeout,
|
|
237
260
|
promotedTimeoutMs = 0,
|
|
261
|
+
backgroundDeadlineMs = 0,
|
|
238
262
|
admission = resourceAdmission,
|
|
239
263
|
}) {
|
|
240
264
|
return new Promise(async (resolve) => {
|
|
@@ -363,6 +387,7 @@ export function execShellCommand({
|
|
|
363
387
|
resourceLease = await acquireShellLeaseBounded(admission, {
|
|
364
388
|
abortSignal,
|
|
365
389
|
label: String(command || '').slice(0, 120),
|
|
390
|
+
ownerKey: ownerSessionId,
|
|
366
391
|
});
|
|
367
392
|
const _policyErr = await _execPolicyBlockMessage(command);
|
|
368
393
|
if (_policyErr) {
|
|
@@ -443,7 +468,6 @@ export function execShellCommand({
|
|
|
443
468
|
childPid: child.pid,
|
|
444
469
|
childGroupPid: child.pid,
|
|
445
470
|
label: 'shell-command',
|
|
446
|
-
protectHostMemory: true,
|
|
447
471
|
});
|
|
448
472
|
// Windows has no process-group handle. Begin observing while the owned
|
|
449
473
|
// root still exists so descendants can be proven before root exit.
|
|
@@ -768,7 +792,12 @@ export function execShellCommand({
|
|
|
768
792
|
const stdoutPath = taskOutput.spilled ? taskOutput.stdoutPath : null;
|
|
769
793
|
const stderrPath = taskOutput.spilled ? taskOutput.stderrPath : null;
|
|
770
794
|
let job = null;
|
|
771
|
-
const
|
|
795
|
+
const elapsedMs = Math.max(0, Date.now() - _startMs);
|
|
796
|
+
const adoptedTimeoutMs = reason === 'timeout'
|
|
797
|
+
? promotedTimeoutMs
|
|
798
|
+
: (backgroundDeadlineMs > 0
|
|
799
|
+
? Math.max(1, backgroundDeadlineMs - elapsedMs)
|
|
800
|
+
: 0);
|
|
772
801
|
try {
|
|
773
802
|
job = adoptForegroundShellJob({
|
|
774
803
|
command,
|
|
@@ -830,7 +859,12 @@ export function execShellCommand({
|
|
|
830
859
|
// Re-check after the awaited capture reads: cancellation can race after
|
|
831
860
|
// adoption commits. Never report that cancelled process as a successful
|
|
832
861
|
// still-running background task.
|
|
833
|
-
|
|
862
|
+
// EXCEPTION: an interrupt-driven promotion starts FROM an aborted signal
|
|
863
|
+
// by design (the user typed a new message), so this guard must not undo
|
|
864
|
+
// the very transition it was asked to perform. A plain cancellation still
|
|
865
|
+
// reverts adoption and kills.
|
|
866
|
+
if (abortSignal && abortSignal.aborted
|
|
867
|
+
&& !(reason === 'interrupt' && _abortReasonIsInterrupt(abortSignal))) {
|
|
834
868
|
killed = true;
|
|
835
869
|
killCause = 'cancellation';
|
|
836
870
|
try { killShellJob(jobId); } catch {}
|
|
@@ -855,7 +889,7 @@ export function execShellCommand({
|
|
|
855
889
|
}));
|
|
856
890
|
return;
|
|
857
891
|
}
|
|
858
|
-
//
|
|
892
|
+
// Completed-during-promotion race: the child
|
|
859
893
|
// finished while adoption was committing. Report a clean COMPLETED
|
|
860
894
|
// result instead of backgrounded — the caller then never arms the
|
|
861
895
|
// completion watcher, so no redundant task notification fires. Write
|
|
@@ -1037,6 +1071,26 @@ export function execShellCommand({
|
|
|
1037
1071
|
|
|
1038
1072
|
if (abortSignal) {
|
|
1039
1073
|
abortHandler = () => {
|
|
1074
|
+
// Interrupt (the user typed a NEW message while this was running) is a
|
|
1075
|
+
// "also look at this" signal, not "stop that" — CC backgrounds instead
|
|
1076
|
+
// of killing there, and throwing away a long build the user never asked
|
|
1077
|
+
// to stop is the worse outcome. Explicit cancellation (ESC) keeps the
|
|
1078
|
+
// kill. The promotion reuses the timeout path's guards verbatim so a
|
|
1079
|
+
// detached / already-settled / already-exited child can never be
|
|
1080
|
+
// adopted as a job.
|
|
1081
|
+
if (
|
|
1082
|
+
_abortReasonIsInterrupt(abortSignal) &&
|
|
1083
|
+
backgroundOnTimeout &&
|
|
1084
|
+
!_isBackground &&
|
|
1085
|
+
!settled &&
|
|
1086
|
+
!autoBackgrounded &&
|
|
1087
|
+
!killed &&
|
|
1088
|
+
child?.exitCode == null &&
|
|
1089
|
+
child?.signalCode == null
|
|
1090
|
+
) {
|
|
1091
|
+
fireAutoBackground({ reason: 'interrupt' });
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1040
1094
|
_treeKillForceSettle('cancellation');
|
|
1041
1095
|
};
|
|
1042
1096
|
try {
|
|
@@ -209,7 +209,7 @@ export class TaskOutput {
|
|
|
209
209
|
this.stdoutFileSize = 0;
|
|
210
210
|
this.stderrFileSize = 0;
|
|
211
211
|
this.writeError = null;
|
|
212
|
-
//
|
|
212
|
+
// Direct-capture mode hands the spill files to the child as
|
|
213
213
|
// stdio fds (no JS pipes). Sizes are then tracked via stat, not writes.
|
|
214
214
|
this.direct = false;
|
|
215
215
|
// fsync throttle: task wait/read + tail-read polling can call getStdout/
|
|
@@ -292,7 +292,7 @@ export class TaskOutput {
|
|
|
292
292
|
this._ensureFileBacking();
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
//
|
|
295
|
+
// File mode: open the spill files BEFORE spawn and
|
|
296
296
|
// hand their fds to the child as stdio[1]/stdio[2]. The child (and any
|
|
297
297
|
// grandchildren) write straight to disk with no parent-side pipes, so a
|
|
298
298
|
// surviving grandchild can never wedge the caller by holding a pipe handle.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, mkdirSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { DiscordProvider } from "../providers/discord.mjs";
|
|
4
|
+
import { TelegramProvider } from "../providers/telegram.mjs";
|
|
5
5
|
import {
|
|
6
6
|
readSection,
|
|
7
7
|
updateSection,
|
|
@@ -22,11 +22,11 @@ const DEFAULT_ACCESS = {
|
|
|
22
22
|
channels: {}
|
|
23
23
|
};
|
|
24
24
|
const DEFAULT_CONFIG = {
|
|
25
|
-
|
|
25
|
+
provider: "discord",
|
|
26
26
|
discord: { token: "" },
|
|
27
27
|
telegram: { token: "" },
|
|
28
28
|
access: DEFAULT_ACCESS,
|
|
29
|
-
channel: {
|
|
29
|
+
channel: {}
|
|
30
30
|
};
|
|
31
31
|
// Shared defaults layer. Merge semantics: user values win; defaults
|
|
32
32
|
// only fill missing fields. Helper is exported so the setup UI and
|
|
@@ -40,18 +40,18 @@ function applyDefaults(config) {
|
|
|
40
40
|
return out;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function
|
|
44
|
-
if (
|
|
45
|
-
return String(entry?.telegramChatId ||
|
|
43
|
+
function channelIdForProvider(entry = {}, provider = "discord") {
|
|
44
|
+
if (provider === "telegram") {
|
|
45
|
+
return String(entry?.telegramChatId || "");
|
|
46
46
|
}
|
|
47
|
-
return String(entry?.discordChannelId ||
|
|
47
|
+
return String(entry?.discordChannelId || "");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
// Resolve the single active-
|
|
50
|
+
// Resolve the single active-provider channel id from the config's `channel`
|
|
51
51
|
// section. Disk config carries a single `channel` object only.
|
|
52
|
-
function resolveChannelId(raw = {},
|
|
52
|
+
function resolveChannelId(raw = {}, provider = "discord") {
|
|
53
53
|
const channel = raw.channel && typeof raw.channel === "object" ? raw.channel : null;
|
|
54
|
-
if (channel) return
|
|
54
|
+
if (channel) return channelIdForProvider(channel, provider);
|
|
55
55
|
return "";
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -88,20 +88,20 @@ async function loadConfig({ freshSecrets = false } = {}) {
|
|
|
88
88
|
if (discordTokenProblem) {
|
|
89
89
|
process.stderr.write(`mixdog: discord token ignored: ${discordTokenProblem}\n`);
|
|
90
90
|
}
|
|
91
|
-
// Single-
|
|
91
|
+
// Single-provider select: config.provider picks ONE of discord|telegram.
|
|
92
92
|
// Anything else falls back to the discord default.
|
|
93
|
-
const
|
|
93
|
+
const provider = raw.provider === "telegram" ? "telegram" : "discord";
|
|
94
94
|
const telegramToken = getTelegramToken();
|
|
95
|
-
const channelId = resolveChannelId(raw,
|
|
95
|
+
const channelId = resolveChannelId(raw, provider);
|
|
96
96
|
// The runtime reads only the resolved `channelId`; disk carries a single
|
|
97
97
|
// `channel` object, so spread `raw` directly.
|
|
98
98
|
return applyDefaults({
|
|
99
99
|
...DEFAULT_CONFIG,
|
|
100
100
|
...raw,
|
|
101
|
-
|
|
101
|
+
provider,
|
|
102
102
|
channelId,
|
|
103
103
|
discord: { ...DEFAULT_CONFIG.discord, ...(({ token: _, ...rest }) => rest)(raw.discord || {}), ...(discordToken && !discordTokenProblem ? { token: discordToken } : {}) },
|
|
104
|
-
// Merge the keychain-resolved telegram token (harmless when
|
|
104
|
+
// Merge the keychain-resolved telegram token (harmless when provider is
|
|
105
105
|
// discord; the secret never lands in the on-disk config either way).
|
|
106
106
|
telegram: { ...DEFAULT_CONFIG.telegram, ...(({ token: _t, ...rest }) => rest)(raw.telegram || {}), ...(telegramToken ? { token: telegramToken } : {}) },
|
|
107
107
|
access: {
|
|
@@ -117,7 +117,7 @@ async function loadConfig({ freshSecrets = false } = {}) {
|
|
|
117
117
|
updateSection("channels", () => DEFAULT_CONFIG);
|
|
118
118
|
process.stderr.write(
|
|
119
119
|
`mixdog: default channels config created in ${MIXDOG_CONFIG_PATH}
|
|
120
|
-
edit
|
|
120
|
+
edit the active provider channel id to connect.
|
|
121
121
|
`
|
|
122
122
|
);
|
|
123
123
|
return applyDefaults(DEFAULT_CONFIG);
|
|
@@ -125,7 +125,7 @@ async function loadConfig({ freshSecrets = false } = {}) {
|
|
|
125
125
|
throw err;
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
const
|
|
128
|
+
const HEADLESS_PROVIDER = {
|
|
129
129
|
name: "headless",
|
|
130
130
|
MAX_MESSAGE_LENGTH: 2000,
|
|
131
131
|
formatOutgoing(t) {
|
|
@@ -156,31 +156,31 @@ const HEADLESS_BACKEND = {
|
|
|
156
156
|
on() {
|
|
157
157
|
}
|
|
158
158
|
};
|
|
159
|
-
function
|
|
159
|
+
function createProvider(config) {
|
|
160
160
|
// Channels-module toggle is MESSAGING-ONLY: automation (scheduler/webhooks)
|
|
161
|
-
// keeps the worker alive, so a disabled module runs the headless
|
|
161
|
+
// keeps the worker alive, so a disabled module runs the headless provider
|
|
162
162
|
// instead of blocking the whole runtime.
|
|
163
163
|
if (config.enabled === false) {
|
|
164
164
|
process.stderr.write("mixdog: channels messaging disabled; channel runtime running in headless mode\n");
|
|
165
|
-
return
|
|
165
|
+
return HEADLESS_PROVIDER;
|
|
166
166
|
}
|
|
167
|
-
// Single-
|
|
168
|
-
// config.
|
|
169
|
-
if (config.
|
|
167
|
+
// Single-provider select: exactly one provider is constructed based on
|
|
168
|
+
// config.provider (discord|telegram). The two are mutually exclusive.
|
|
169
|
+
if (config.provider === "telegram") {
|
|
170
170
|
const telegramToken = getTelegramToken();
|
|
171
171
|
if (!telegramToken) {
|
|
172
172
|
process.stderr.write("mixdog: telegram bot not configured; channel runtime running in headless mode\n");
|
|
173
|
-
return
|
|
173
|
+
return HEADLESS_PROVIDER;
|
|
174
174
|
}
|
|
175
175
|
const tgStateDir = config.telegram?.stateDir ?? join(DATA_DIR, "telegram");
|
|
176
176
|
mkdirSync(tgStateDir, { recursive: true });
|
|
177
|
-
return new
|
|
177
|
+
return new TelegramProvider({
|
|
178
178
|
...config.telegram,
|
|
179
179
|
configPath: CONFIG_FILE,
|
|
180
180
|
access: config.access,
|
|
181
181
|
// Single-source channel setup: the main chat is auto-allowed inside
|
|
182
|
-
//
|
|
183
|
-
//
|
|
182
|
+
// TelegramProvider.loadAccess() so the configured Telegram target is
|
|
183
|
+
// enough for both inbound gating and outbound.
|
|
184
184
|
mainChannelId: config.channelId
|
|
185
185
|
}, tgStateDir);
|
|
186
186
|
}
|
|
@@ -189,19 +189,19 @@ function createBackend(config) {
|
|
|
189
189
|
if (discordTokenProblem) {
|
|
190
190
|
process.stderr.write(`mixdog: discord token ignored: ${discordTokenProblem}\n`);
|
|
191
191
|
}
|
|
192
|
-
if (config.
|
|
192
|
+
if (config.provider !== "discord" || !discordToken || discordTokenProblem) {
|
|
193
193
|
process.stderr.write("mixdog: discord bot not configured; channel runtime running in headless mode\n");
|
|
194
|
-
return
|
|
194
|
+
return HEADLESS_PROVIDER;
|
|
195
195
|
}
|
|
196
196
|
const stateDir = config.discord.stateDir ?? join(DATA_DIR, "discord");
|
|
197
197
|
mkdirSync(stateDir, { recursive: true });
|
|
198
|
-
return new
|
|
198
|
+
return new DiscordProvider({
|
|
199
199
|
...config.discord,
|
|
200
200
|
configPath: CONFIG_FILE,
|
|
201
201
|
access: config.access,
|
|
202
202
|
// Single-source channel setup: the main channel is auto-allowed inside
|
|
203
|
-
//
|
|
204
|
-
//
|
|
203
|
+
// DiscordProvider.loadAccess() so the configured Discord target is enough
|
|
204
|
+
// for both inbound and outbound — no separate access.channels entry.
|
|
205
205
|
mainChannelId: config.channelId
|
|
206
206
|
}, stateDir);
|
|
207
207
|
}
|
|
@@ -215,7 +215,7 @@ function loadProfileConfig() {
|
|
|
215
215
|
}
|
|
216
216
|
export {
|
|
217
217
|
DATA_DIR,
|
|
218
|
-
|
|
218
|
+
createProvider,
|
|
219
219
|
getDiscordToken,
|
|
220
220
|
getTelegramToken,
|
|
221
221
|
loadConfig,
|
|
@@ -7,14 +7,14 @@ import {
|
|
|
7
7
|
import { refreshActiveInstance } from "./runtime-paths.mjs";
|
|
8
8
|
import { isNetworkError, retryOnNetwork } from "./network-retry.mjs";
|
|
9
9
|
// Inbound message pipeline extracted from channels/index.mjs (behavior-
|
|
10
|
-
// preserving): serial inbound queue,
|
|
10
|
+
// preserving): serial inbound queue, provider.onMessage transcript (re)bind +
|
|
11
11
|
// steal logic, and handleInbound voice-transcription + parent notify. Bound to
|
|
12
12
|
// live runtime getters and shared primitives.
|
|
13
13
|
function isImageAttachment(contentType) {
|
|
14
14
|
return typeof contentType === "string" && contentType.toLowerCase().startsWith("image/");
|
|
15
15
|
}
|
|
16
16
|
export function createInboundHandler({
|
|
17
|
-
|
|
17
|
+
getProvider,
|
|
18
18
|
getConfig,
|
|
19
19
|
getBridgeRuntimeConnected,
|
|
20
20
|
getChannelBridgeActive,
|
|
@@ -55,7 +55,7 @@ const inboundQueue = (() => {
|
|
|
55
55
|
};
|
|
56
56
|
})();
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
getProvider().onMessage = (msg) => {
|
|
59
59
|
const receivedAtMs = Number.isFinite(msg.receivedAtMs) ? msg.receivedAtMs : Date.now();
|
|
60
60
|
const onMessageAtMs = Date.now();
|
|
61
61
|
const gateBlocked = (!getBridgeRuntimeConnected() || !getBridgeOwnershipSnapshot().owned)
|
|
@@ -63,7 +63,7 @@ getBackend().onMessage = (msg) => {
|
|
|
63
63
|
: (!getChannelBridgeActive() ? "bridge-inactive" : "");
|
|
64
64
|
if (gateBlocked) {
|
|
65
65
|
if (gateBlocked === "bridge-seat") refreshBridgeOwnershipSafe();
|
|
66
|
-
//
|
|
66
|
+
// An inbound message is never silently swallowed. The bridge
|
|
67
67
|
// seat may be mid-handover or the channel bridge may still be activating
|
|
68
68
|
// (app relaunch / boot); retry with backoff before giving up, and make
|
|
69
69
|
// the final drop loud so a real loss stays diagnosable.
|
|
@@ -72,7 +72,7 @@ getBackend().onMessage = (msg) => {
|
|
|
72
72
|
msg.__bridgeGateRetries = attempt;
|
|
73
73
|
msg.receivedAtMs = receivedAtMs;
|
|
74
74
|
setTimeout(() => {
|
|
75
|
-
try {
|
|
75
|
+
try { getProvider().onMessage?.(msg); } catch { /* best-effort */ }
|
|
76
76
|
}, 2000 * attempt)?.unref?.();
|
|
77
77
|
} else {
|
|
78
78
|
try { process.stderr.write(`mixdog: inbound message dropped after ${attempt - 1} ${gateBlocked} retries (channel=${msg.chatId}, id=${msg.messageId})\n`); } catch {}
|
|
@@ -242,7 +242,7 @@ getBackend().onMessage = (msg) => {
|
|
|
242
242
|
});
|
|
243
243
|
void (async () => {
|
|
244
244
|
try {
|
|
245
|
-
await
|
|
245
|
+
await getProvider().react(msg.chatId, msg.messageId, "\u{1F914}");
|
|
246
246
|
} catch {
|
|
247
247
|
}
|
|
248
248
|
await bindingDone;
|
|
@@ -271,7 +271,7 @@ async function handleInbound(msg, route, options = {}) {
|
|
|
271
271
|
const files = await retryOnNetwork(
|
|
272
272
|
// Short per-attempt timeout (vs the 180s default) bounds worst-case
|
|
273
273
|
// blockage of the serial inboundQueue on a bad voice message.
|
|
274
|
-
() =>
|
|
274
|
+
() => getProvider().downloadAttachment(msg.chatId, msg.messageId, { timeoutMs: 20_000 }),
|
|
275
275
|
{ label: "voice.download" }
|
|
276
276
|
);
|
|
277
277
|
// concurrency handled inside transcribeVoice queue; loop is sequential so last att wins
|
|
@@ -312,7 +312,7 @@ async function handleInbound(msg, route, options = {}) {
|
|
|
312
312
|
if (imageAtts.length > 0) {
|
|
313
313
|
try {
|
|
314
314
|
const files = (await retryOnNetwork(
|
|
315
|
-
() =>
|
|
315
|
+
() => getProvider().downloadAttachment(msg.chatId, msg.messageId, {
|
|
316
316
|
timeoutMs: 20_000,
|
|
317
317
|
filter: (a) => isImageAttachment(a.contentType),
|
|
318
318
|
}),
|