mixdog 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -3
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +30 -67
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +5 -2
- package/src/agents/heavy-worker/AGENT.md +21 -11
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +3 -2
- package/src/agents/worker/AGENT.md +21 -11
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +933 -3731
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/repl.mjs +5 -5
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +2 -9
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +47 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +409 -1304
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +551 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1080 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +241 -894
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +258 -2050
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +238 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +6 -14
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +52 -2
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +291 -0
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +126 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +155 -677
- package/src/standalone/channel-worker.mjs +7 -9
- package/src/standalone/explore-tool.mjs +40 -12
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +110 -746
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +125 -0
- package/src/standalone/provider-admin.mjs +15 -19
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +1163 -7571
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +259 -80
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +56 -588
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +18034 -17188
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +405 -1385
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +60 -10
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +11 -9
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +77 -462
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Recall-fasttrack compaction pipeline, extracted from loop.mjs.
|
|
2
|
+
// Hydrates the session transcript into the memory pipeline (ingest_session),
|
|
3
|
+
// dumps chunked/raw roots, drains cycle1 until no raw rows remain, and folds
|
|
4
|
+
// the combined recall text back into a compacted message array capped at
|
|
5
|
+
// CONTEXT_SHARE_RATIO of the model context window. No behavior change: this is
|
|
6
|
+
// the same body that lived inline in loop.mjs, re-exported via the facade so
|
|
7
|
+
// existing importers keep working.
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
import { executeInternalTool } from '../../internal-tools.mjs';
|
|
10
|
+
import { loadConfig as loadOrchestratorConfig } from '../../config.mjs';
|
|
11
|
+
import {
|
|
12
|
+
recallFastTrackCompactMessages,
|
|
13
|
+
CONTEXT_SHARE_RATIO,
|
|
14
|
+
RECALL_TOKEN_CAP_FLOOR_TOKENS,
|
|
15
|
+
drainSessionCycle1,
|
|
16
|
+
countRawPendingRows,
|
|
17
|
+
} from '../compact.mjs';
|
|
18
|
+
import {
|
|
19
|
+
compactDiagnosticError,
|
|
20
|
+
compactByteLength,
|
|
21
|
+
compactDebugLog,
|
|
22
|
+
} from './compact-debug.mjs';
|
|
23
|
+
import { positiveTokenInt } from './env.mjs';
|
|
24
|
+
|
|
25
|
+
export async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTokens, compactPolicy, sessionId, signal }) {
|
|
26
|
+
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
27
|
+
const startedAt = Date.now();
|
|
28
|
+
const diagnostics = {
|
|
29
|
+
hydrateLimit: null,
|
|
30
|
+
ingestMs: null,
|
|
31
|
+
ingestSkipped: false,
|
|
32
|
+
ingestError: null,
|
|
33
|
+
initialDumpMs: null,
|
|
34
|
+
initialDumpBytes: null,
|
|
35
|
+
initialDumpChars: null,
|
|
36
|
+
initialRawPending: null,
|
|
37
|
+
cycle1Ms: null,
|
|
38
|
+
cycle1Skipped: false,
|
|
39
|
+
cycle1SkipReason: null,
|
|
40
|
+
cycle1Passes: null,
|
|
41
|
+
cycle1RawRemaining: null,
|
|
42
|
+
cycle1TextBytes: null,
|
|
43
|
+
cycle1Error: null,
|
|
44
|
+
finalRecallBytes: null,
|
|
45
|
+
finalRecallChars: null,
|
|
46
|
+
totalMs: null,
|
|
47
|
+
};
|
|
48
|
+
const query = `session:${sessionId}:all-chunks`;
|
|
49
|
+
const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
50
|
+
const callerCtx = {
|
|
51
|
+
callerSessionId: sessionId || null,
|
|
52
|
+
callerCwd: sessionRef?.cwd || undefined,
|
|
53
|
+
routingSessionId: sessionId || null,
|
|
54
|
+
clientHostPid: sessionRef?.clientHostPid,
|
|
55
|
+
signal: signal || null,
|
|
56
|
+
};
|
|
57
|
+
const hydrateLimit = positiveTokenInt(sessionRef?.compaction?.recallIngestLimit)
|
|
58
|
+
|| Math.max(500, Math.min(5000, messages.length || 0));
|
|
59
|
+
diagnostics.hydrateLimit = hydrateLimit;
|
|
60
|
+
let t0 = Date.now();
|
|
61
|
+
try {
|
|
62
|
+
await executeInternalTool('memory', {
|
|
63
|
+
action: 'ingest_session',
|
|
64
|
+
sessionId,
|
|
65
|
+
messages,
|
|
66
|
+
cwd: sessionRef?.cwd,
|
|
67
|
+
limit: hydrateLimit,
|
|
68
|
+
}, callerCtx);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
diagnostics.ingestSkipped = true;
|
|
71
|
+
diagnostics.ingestError = compactDiagnosticError(err);
|
|
72
|
+
try { process.stderr.write(`[loop] recall-fasttrack ingest skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
73
|
+
} finally {
|
|
74
|
+
diagnostics.ingestMs = Date.now() - t0;
|
|
75
|
+
}
|
|
76
|
+
const dumpArgs = {
|
|
77
|
+
action: 'dump_session_roots',
|
|
78
|
+
sessionId,
|
|
79
|
+
includeRaw: true,
|
|
80
|
+
limit: positiveTokenInt(sessionRef?.compaction?.recallChunkLimit ?? sessionRef?.compaction?.recallLimit) || hydrateLimit,
|
|
81
|
+
};
|
|
82
|
+
const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
|
|
83
|
+
t0 = Date.now();
|
|
84
|
+
let recallText = await executeInternalTool('memory', dumpArgs, callerCtx);
|
|
85
|
+
diagnostics.initialDumpMs = Date.now() - t0;
|
|
86
|
+
diagnostics.initialDumpChars = String(recallText || '').length;
|
|
87
|
+
diagnostics.initialDumpBytes = compactByteLength(recallText);
|
|
88
|
+
diagnostics.initialRawPending = countRawPendingRows(recallText);
|
|
89
|
+
let cycle1Text = '';
|
|
90
|
+
const hasRawRows = /(?:^|\n)# raw_pending\s+\d+\s+id=/i.test(String(recallText || ''));
|
|
91
|
+
// Recap off = NO memory-pipeline LLM calls: skip the cycle1 drain entirely
|
|
92
|
+
// and let the dump's raw transcript lines (includeRaw:true above) ride into
|
|
93
|
+
// the injected summary as-is. Poll-on-use: re-read the flag per compact so
|
|
94
|
+
// a runtime toggle applies without restart. Default on if config read fails.
|
|
95
|
+
let recapOn = true;
|
|
96
|
+
try { recapOn = loadOrchestratorConfig({ secrets: false })?.recap?.enabled !== false; } catch { /* default on */ }
|
|
97
|
+
if (hasRawRows && !recapOn) {
|
|
98
|
+
diagnostics.cycle1Skipped = true;
|
|
99
|
+
diagnostics.cycle1SkipReason = 'recap disabled';
|
|
100
|
+
diagnostics.cycle1Passes = 0;
|
|
101
|
+
diagnostics.cycle1RawRemaining = countRawPendingRows(recallText);
|
|
102
|
+
cycle1Text = 'cycle1: skipped (recap disabled — raw transcript lines kept as-is)';
|
|
103
|
+
} else if (hasRawRows) {
|
|
104
|
+
t0 = Date.now();
|
|
105
|
+
try {
|
|
106
|
+
// Drain this session's cycle1 in window×concurrency units until no
|
|
107
|
+
// raw rows remain, so the injected root is fully chunked rather than
|
|
108
|
+
// carrying the unprocessed transcript tail (single-pass left raw in).
|
|
109
|
+
const drained = await drainSessionCycle1(runTool, {
|
|
110
|
+
sessionId,
|
|
111
|
+
dumpArgs,
|
|
112
|
+
deadlineMs: positiveTokenInt(sessionRef?.compaction?.recallCycle1DeadlineMs) || 120_000,
|
|
113
|
+
maxPasses: positiveTokenInt(sessionRef?.compaction?.recallCycle1MaxPasses) || 0,
|
|
114
|
+
cycleArgs: {
|
|
115
|
+
min_batch: 1,
|
|
116
|
+
session_cap: 1,
|
|
117
|
+
batch_size: positiveTokenInt(sessionRef?.compaction?.recallCycle1BatchSize) || 100,
|
|
118
|
+
rows_per_session: positiveTokenInt(sessionRef?.compaction?.recallRowsPerSession) || 100,
|
|
119
|
+
window_size: positiveTokenInt(sessionRef?.compaction?.recallWindowSize) || 20,
|
|
120
|
+
concurrency: positiveTokenInt(sessionRef?.compaction?.recallConcurrency) || 5,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
recallText = drained.recallText;
|
|
124
|
+
cycle1Text = drained.cycle1Text;
|
|
125
|
+
diagnostics.cycle1Passes = drained.passes;
|
|
126
|
+
diagnostics.cycle1RawRemaining = drained.rawRemaining;
|
|
127
|
+
diagnostics.cycle1TextBytes = compactByteLength(cycle1Text);
|
|
128
|
+
if (drained.error) {
|
|
129
|
+
diagnostics.cycle1Error = drained.error;
|
|
130
|
+
try { process.stderr.write(`[loop] recall-fasttrack cycle1 error (sess=${sessionId || 'unknown'}): ${drained.error}\n`); } catch {}
|
|
131
|
+
}
|
|
132
|
+
if (drained.rawRemaining > 0) {
|
|
133
|
+
try { process.stderr.write(`[loop] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId || 'unknown'})\n`); } catch {}
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
diagnostics.cycle1Error = compactDiagnosticError(err);
|
|
137
|
+
try { process.stderr.write(`[loop] recall-fasttrack cycle1 skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
138
|
+
} finally {
|
|
139
|
+
diagnostics.cycle1Ms = Date.now() - t0;
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
diagnostics.cycle1Skipped = true;
|
|
143
|
+
diagnostics.cycle1SkipReason = 'session chunks already hydrated';
|
|
144
|
+
diagnostics.cycle1Passes = 0;
|
|
145
|
+
diagnostics.cycle1RawRemaining = 0;
|
|
146
|
+
cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
|
|
147
|
+
}
|
|
148
|
+
const combinedRecallText = [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n');
|
|
149
|
+
diagnostics.finalRecallChars = combinedRecallText.length;
|
|
150
|
+
diagnostics.finalRecallBytes = compactByteLength(combinedRecallText);
|
|
151
|
+
// Recall injection (chunked-summary + raw-fallback text, combined above)
|
|
152
|
+
// never exceeds CONTEXT_SHARE_RATIO (5%) of the model context window
|
|
153
|
+
// (floor RECALL_TOKEN_CAP_FLOOR_TOKENS); the rest of the budget belongs to
|
|
154
|
+
// live conversation. Same unified 5% as the compact target ratio
|
|
155
|
+
// (compact-policy.mjs COMPACT_TARGET_RATIO). Omitted when contextWindow is
|
|
156
|
+
// unknown/0 so current no-cap behavior is preserved.
|
|
157
|
+
const _recallCapWindow = Number(compactPolicy.contextWindow) || 0;
|
|
158
|
+
const recallTokenCap = _recallCapWindow > 0
|
|
159
|
+
? Math.max(RECALL_TOKEN_CAP_FLOOR_TOKENS, Math.floor(_recallCapWindow * CONTEXT_SHARE_RATIO))
|
|
160
|
+
: undefined;
|
|
161
|
+
const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
|
|
162
|
+
reserveTokens: compactPolicy.reserveTokens,
|
|
163
|
+
force: true,
|
|
164
|
+
recallText: combinedRecallText,
|
|
165
|
+
query,
|
|
166
|
+
querySha,
|
|
167
|
+
allowEmptyRecall: true,
|
|
168
|
+
tailTurns: compactPolicy.tailTurns,
|
|
169
|
+
keepTokens: compactPolicy.keepTokens,
|
|
170
|
+
preserveRecentTokens: compactPolicy.preserveRecentTokens,
|
|
171
|
+
recallTokenCap,
|
|
172
|
+
});
|
|
173
|
+
diagnostics.totalMs = Date.now() - startedAt;
|
|
174
|
+
if (result && typeof result === 'object') {
|
|
175
|
+
result.diagnostics = {
|
|
176
|
+
...(result.diagnostics || {}),
|
|
177
|
+
pipeline: diagnostics,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
compactDebugLog('recall-fasttrack pipeline', diagnostics);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Completion-first steering ladder (worker runaway prevention), extracted from
|
|
2
|
+
// loop.mjs. Owns the mutable ladder counters and the post-batch steering-hint
|
|
3
|
+
// emitters. State is threaded live via a context object of getters/setters so
|
|
4
|
+
// no snapshot goes stale — the loop mutates `messages`/`iterations` in place and
|
|
5
|
+
// this module reads them through the accessors on each call. No behavior change:
|
|
6
|
+
// the counters, thresholds, and emitted messages are verbatim from agentLoop.
|
|
7
|
+
import { appendAgentTrace } from '../../agent-trace.mjs';
|
|
8
|
+
import { level2SteerMessage } from './completion-guards.mjs';
|
|
9
|
+
import { isEagerDispatchable } from './tool-helpers.mjs';
|
|
10
|
+
|
|
11
|
+
// Consecutive ignored level-2 steers (zero edits) that force the wrap-up early.
|
|
12
|
+
export const EARLY_SOFT_CAP_LEVEL2_FIRES = 3;
|
|
13
|
+
|
|
14
|
+
// Build the completion-first steering-ladder controller. `ctx` supplies live
|
|
15
|
+
// accessors so every read reflects the loop's current mutable state:
|
|
16
|
+
// - messages, sessionId, sessionAgent, tools (stable refs/values)
|
|
17
|
+
// - getIterations() (current iteration)
|
|
18
|
+
// - softCapEnabled (constant per loop)
|
|
19
|
+
// - getEditCount() (mutated by the loop)
|
|
20
|
+
// - pushSystemReminder(text) → push a meta:'hook' user message
|
|
21
|
+
// - pushUserMessage(msg) → push a raw user message (level-2 latch text)
|
|
22
|
+
export function createSteeringLadder(ctx) {
|
|
23
|
+
const {
|
|
24
|
+
sessionId,
|
|
25
|
+
sessionAgent,
|
|
26
|
+
tools,
|
|
27
|
+
getIterations,
|
|
28
|
+
softCapEnabled,
|
|
29
|
+
getEditCount,
|
|
30
|
+
} = ctx;
|
|
31
|
+
const pushSystemReminder = ctx.pushSystemReminder;
|
|
32
|
+
const pushUserMessage = ctx.pushUserMessage;
|
|
33
|
+
// Permission-based role detection (agent names are user-definable):
|
|
34
|
+
// read-permission sessions legitimately never edit, so they get the
|
|
35
|
+
// report-oriented level-2 text and never arm the early soft-cap.
|
|
36
|
+
const readOnlyRole = ctx.readOnlyRole === true;
|
|
37
|
+
|
|
38
|
+
// Step 1: escalation ladder. _level1FireCount is CUMULATIVE (never reset)
|
|
39
|
+
// so repeated batching reminders accumulate across the whole session.
|
|
40
|
+
// _level2LatchAtIteration latches level-2 steering to at most once / 5 turns.
|
|
41
|
+
let _level1FireCount = 0;
|
|
42
|
+
let _level2LatchAtIteration = -Infinity;
|
|
43
|
+
// Independent ladder counter: consecutive turns where EVERY call is
|
|
44
|
+
// read-only (any count) with zero edits. Catches multi-call read-only
|
|
45
|
+
// turns that the single-call level-1 streak misses. Reset on any edit.
|
|
46
|
+
let _allReadOnlyStreak = 0;
|
|
47
|
+
// Tracks consecutive assistant turns that ran exactly one read-only tool
|
|
48
|
+
// call (missed parallelism). Not reset per-iteration — only by the
|
|
49
|
+
// steering-hint fire below or by a turn that batches/edits.
|
|
50
|
+
let _serialReadOnlyStreak = 0;
|
|
51
|
+
// Tracks consecutive grep calls scoped to the SAME path (any patterns) —
|
|
52
|
+
// the "serial rewording" spiral: re-grepping one file with reworded
|
|
53
|
+
// patterns instead of reading it. Only counts turns whose every call is a
|
|
54
|
+
// grep on that path; any other tool/path resets it.
|
|
55
|
+
let _sameFileGrepStreak = 0;
|
|
56
|
+
let _sameFileGrepPath = null;
|
|
57
|
+
// Early soft-cap: N ignored level-2 steers with zero edits forces the
|
|
58
|
+
// wrap-up early; any edit disarms it (level-2 requires editCount === 0).
|
|
59
|
+
let _level2FireCount = 0;
|
|
60
|
+
|
|
61
|
+
// Level-2 steering emitter shared by both ladder paths (single-call
|
|
62
|
+
// level-1 streak and the independent all-read-only streak). Sets the latch
|
|
63
|
+
// so it fires at most once per 5 turns regardless of which path triggered.
|
|
64
|
+
const _emitLevel2Steer = () => {
|
|
65
|
+
const iterations = getIterations();
|
|
66
|
+
_level2LatchAtIteration = iterations;
|
|
67
|
+
_level2FireCount += 1;
|
|
68
|
+
// When this fire arms the early soft-cap, the wrap-up injected at the
|
|
69
|
+
// next loop head supersedes the level-2 text — skip the redundant hint.
|
|
70
|
+
const _armsEarlyCap = !readOnlyRole && softCapEnabled && _level2FireCount >= EARLY_SOFT_CAP_LEVEL2_FIRES && getEditCount() === 0;
|
|
71
|
+
if (!_armsEarlyCap) pushUserMessage({ role: 'user', content: level2SteerMessage(_level1FireCount, readOnlyRole), meta: 'hook' });
|
|
72
|
+
try {
|
|
73
|
+
appendAgentTrace({
|
|
74
|
+
sessionId,
|
|
75
|
+
iteration: iterations,
|
|
76
|
+
kind: 'steer',
|
|
77
|
+
payload: { tag: 'level2_steer', level1_fires: _level1FireCount, level2_fires: _level2FireCount, edit_count: getEditCount(), all_read_only_streak: _allReadOnlyStreak },
|
|
78
|
+
agent: sessionAgent || null,
|
|
79
|
+
});
|
|
80
|
+
} catch { /* best-effort */ }
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
// Loop head reads: is the early soft-cap armed?
|
|
85
|
+
get level2FireCount() { return _level2FireCount; },
|
|
86
|
+
earlySoftCapArmed() {
|
|
87
|
+
return !readOnlyRole && _level2FireCount >= EARLY_SOFT_CAP_LEVEL2_FIRES && getEditCount() === 0;
|
|
88
|
+
},
|
|
89
|
+
// Post-batch steering hint gate. `hintAlreadyFired` seeds the once-per-
|
|
90
|
+
// turn latch (soft-cap active suppresses all hints). Returns nothing;
|
|
91
|
+
// pushes at most one steering message via the ctx push callbacks.
|
|
92
|
+
emitPostBatchSteering(calls, hintAlreadyFired) {
|
|
93
|
+
const iterations = getIterations();
|
|
94
|
+
const editCount = getEditCount();
|
|
95
|
+
// Steering hint gate: at most ONE hint per turn (priority: soft-cap >
|
|
96
|
+
// level-2 > same-file grep > level-1), and none once the soft-cap
|
|
97
|
+
// wrap-up is active — its "text only" directive must not share a send
|
|
98
|
+
// with a "keep exploring" hint.
|
|
99
|
+
let _hintFiredThisTurn = hintAlreadyFired;
|
|
100
|
+
// Missed-parallelism steering: 3+ consecutive turns of a single
|
|
101
|
+
// read-only tool call suggest the model isn't batching independent
|
|
102
|
+
// lookups. Nudge once, then reset (fires again after 3 more).
|
|
103
|
+
if (calls.length === 1 && isEagerDispatchable(calls[0].name, tools)) {
|
|
104
|
+
_serialReadOnlyStreak += 1;
|
|
105
|
+
if (_serialReadOnlyStreak >= 3 && !_hintFiredThisTurn) {
|
|
106
|
+
_serialReadOnlyStreak = 0;
|
|
107
|
+
// Escalation ladder (Step 1). Cumulative level-1 fires are
|
|
108
|
+
// tracked and NEVER reset. Once level-1 has fired >=3 times with
|
|
109
|
+
// ZERO edits, escalate to level-2 steering (blocked-report is a
|
|
110
|
+
// valid completion) instead of the batching nudge — latched to at
|
|
111
|
+
// most once per 5 turns.
|
|
112
|
+
_level1FireCount += 1;
|
|
113
|
+
if (_level1FireCount >= 3 && editCount === 0 && (iterations - _level2LatchAtIteration) >= 5) {
|
|
114
|
+
_emitLevel2Steer();
|
|
115
|
+
} else {
|
|
116
|
+
pushSystemReminder('Last 3 turns each ran a single read-only tool. Batch independent lookups (read/grep/glob/code_graph) into ONE turn, or start editing if you have enough context.');
|
|
117
|
+
}
|
|
118
|
+
_hintFiredThisTurn = true;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
_serialReadOnlyStreak = 0;
|
|
122
|
+
}
|
|
123
|
+
// Independent all-read-only escalation (audit finding): the level-1
|
|
124
|
+
// streak above only counts single-call turns, so a worker that runs
|
|
125
|
+
// 2+ read-only calls per turn escapes the ladder entirely. Track a
|
|
126
|
+
// cumulative count of consecutive turns where EVERY call is read-only
|
|
127
|
+
// (any count) and no edit has been made; at 12 such turns fire level-2
|
|
128
|
+
// directly (same once-per-5-turn latch), reset on any edit.
|
|
129
|
+
{
|
|
130
|
+
const _allReadOnly = calls.length > 0 && calls.every((c) => isEagerDispatchable(c.name, tools));
|
|
131
|
+
if (_allReadOnly && editCount === 0) {
|
|
132
|
+
_allReadOnlyStreak += 1;
|
|
133
|
+
if (_allReadOnlyStreak >= 12 && (iterations - _level2LatchAtIteration) >= 5 && !_hintFiredThisTurn) {
|
|
134
|
+
_emitLevel2Steer();
|
|
135
|
+
_hintFiredThisTurn = true;
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
_allReadOnlyStreak = 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Serial-rewording steering: 4+ consecutive turns grepping the SAME
|
|
142
|
+
// path with reworded patterns = a search spiral that single-call
|
|
143
|
+
// batching cannot catch. Crisis-only: fires once per spiral, then
|
|
144
|
+
// resets. Read-the-file is almost always the answer at that point.
|
|
145
|
+
{
|
|
146
|
+
const _grepPathOf = (c) => {
|
|
147
|
+
if (c?.name !== 'grep') return null;
|
|
148
|
+
const p = c?.arguments?.path;
|
|
149
|
+
return typeof p === 'string' && p ? p : null;
|
|
150
|
+
};
|
|
151
|
+
const _turnPaths = calls.map(_grepPathOf);
|
|
152
|
+
const _uniq = [...new Set(_turnPaths)];
|
|
153
|
+
if (_uniq.length === 1 && _uniq[0] !== null) {
|
|
154
|
+
if (_uniq[0] === _sameFileGrepPath) _sameFileGrepStreak += 1;
|
|
155
|
+
else { _sameFileGrepPath = _uniq[0]; _sameFileGrepStreak = 1; }
|
|
156
|
+
if (_sameFileGrepStreak >= 4 && !_hintFiredThisTurn) {
|
|
157
|
+
pushSystemReminder(`4+ consecutive grep turns on the same path (${_sameFileGrepPath}). Rewording patterns is not converging — read the relevant span directly (read with offset/limit) or act on what you have.`);
|
|
158
|
+
_sameFileGrepStreak = 0;
|
|
159
|
+
_sameFileGrepPath = null;
|
|
160
|
+
_hintFiredThisTurn = true;
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
_sameFileGrepStreak = 0;
|
|
164
|
+
_sameFileGrepPath = null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
// Reviewer fix: a zero-tool turn must not bridge the all-read-only
|
|
169
|
+
// streak across non-tool turns — that would fire level-2 early on a
|
|
170
|
+
// worker that paused to synthesize text mid-run.
|
|
171
|
+
resetAllReadOnlyStreak() { _allReadOnlyStreak = 0; },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Steering-message normalization/merge helpers extracted from loop.mjs.
|
|
2
|
+
// Merges queued steering entries into a single content payload + display text.
|
|
3
|
+
|
|
4
|
+
export function steeringContentText(content) {
|
|
5
|
+
if (typeof content === 'string') return content;
|
|
6
|
+
if (Array.isArray(content)) {
|
|
7
|
+
return content.map((part) => {
|
|
8
|
+
if (typeof part === 'string') return part;
|
|
9
|
+
if (part?.type === 'text') return part.text || '';
|
|
10
|
+
if (part?.type === 'image') return '[Image]';
|
|
11
|
+
return part?.text || '';
|
|
12
|
+
}).filter(Boolean).join('\n');
|
|
13
|
+
}
|
|
14
|
+
return String(content ?? '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeSteeringEntry(entry) {
|
|
18
|
+
if (typeof entry === 'string') {
|
|
19
|
+
const text = entry.trim();
|
|
20
|
+
return text ? { content: text, text } : null;
|
|
21
|
+
}
|
|
22
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
23
|
+
const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : entry;
|
|
24
|
+
const text = typeof entry.text === 'string' ? entry.text.trim() : steeringContentText(content).trim();
|
|
25
|
+
if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
|
|
26
|
+
if (typeof content === 'string') {
|
|
27
|
+
const value = content.trim();
|
|
28
|
+
return value ? { content: value, text: text || value } : null;
|
|
29
|
+
}
|
|
30
|
+
const fallback = steeringContentText(content).trim();
|
|
31
|
+
return fallback ? { content: fallback, text: text || fallback } : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function mergeSteeringEntries(entries) {
|
|
35
|
+
const normalized = (Array.isArray(entries) ? entries : [])
|
|
36
|
+
.map(normalizeSteeringEntry)
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
if (normalized.length === 0) return null;
|
|
39
|
+
const displayText = normalized.map((entry) => entry.text || steeringContentText(entry.content))
|
|
40
|
+
.filter((text) => String(text || '').trim())
|
|
41
|
+
.join('\n');
|
|
42
|
+
if (normalized.every((entry) => typeof entry.content === 'string')) {
|
|
43
|
+
return {
|
|
44
|
+
content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
|
|
45
|
+
text: displayText,
|
|
46
|
+
count: normalized.length,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const parts = [];
|
|
50
|
+
for (const entry of normalized) {
|
|
51
|
+
if (typeof entry.content === 'string') {
|
|
52
|
+
if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
|
|
53
|
+
} else if (Array.isArray(entry.content)) {
|
|
54
|
+
parts.push(...entry.content);
|
|
55
|
+
} else {
|
|
56
|
+
const text = steeringContentText(entry.content);
|
|
57
|
+
if (text.trim()) parts.push({ type: 'text', text });
|
|
58
|
+
}
|
|
59
|
+
parts.push({ type: 'text', text: '\n' });
|
|
60
|
+
}
|
|
61
|
+
while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
|
|
62
|
+
return { content: parts, text: displayText || steeringContentText(parts), count: normalized.length };
|
|
63
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Stored tool-call argument compaction/restoration, extracted from loop.mjs.
|
|
2
|
+
// Long body/command args are truncated with a sha256-tagged head/tail preview
|
|
3
|
+
// when persisted into assistant history; the FULL body of a single call can be
|
|
4
|
+
// restored on retry (e.g. a failed edit) so the model sees the original patch.
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
|
|
7
|
+
const STORED_TOOL_ARG_BODY_KEY_RE = /^(?:content|old_string|new_string|patch|rewrite)$/i;
|
|
8
|
+
const STORED_TOOL_ARG_LONG_KEY_RE = /^(?:command|script)$/i;
|
|
9
|
+
const STORED_TOOL_ARG_BODY_LIMIT = 2_000;
|
|
10
|
+
const STORED_TOOL_ARG_LONG_LIMIT = 8_000;
|
|
11
|
+
const STORED_TOOL_ARG_PREVIEW_HEAD = 360;
|
|
12
|
+
const STORED_TOOL_ARG_PREVIEW_TAIL = 160;
|
|
13
|
+
|
|
14
|
+
function compactStoredToolArgString(value, key = '') {
|
|
15
|
+
if (typeof value !== 'string') return value;
|
|
16
|
+
const isBody = STORED_TOOL_ARG_BODY_KEY_RE.test(key);
|
|
17
|
+
const isLong = isBody || STORED_TOOL_ARG_LONG_KEY_RE.test(key);
|
|
18
|
+
const limit = isBody ? STORED_TOOL_ARG_BODY_LIMIT : (isLong ? STORED_TOOL_ARG_LONG_LIMIT : Infinity);
|
|
19
|
+
if (value.length <= limit) return value;
|
|
20
|
+
const hash = createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
21
|
+
const head = value.slice(0, STORED_TOOL_ARG_PREVIEW_HEAD).replace(/\r\n/g, '\n');
|
|
22
|
+
const tail = value.slice(-STORED_TOOL_ARG_PREVIEW_TAIL).replace(/\r\n/g, '\n');
|
|
23
|
+
return `[mixdog compacted ${key || 'string'}: ${value.length} chars, sha256:${hash}]\n${head}\n... [middle omitted from stored tool-call args] ...\n${tail}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function compactStoredToolArgValue(value, key = '', depth = 0) {
|
|
27
|
+
if (value === null || value === undefined) return value;
|
|
28
|
+
if (typeof value === 'string') return compactStoredToolArgString(value, key);
|
|
29
|
+
if (typeof value !== 'object') return value;
|
|
30
|
+
if (depth >= 6) return Array.isArray(value) ? `[${value.length} items]` : '{...}';
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return value.map((item) => compactStoredToolArgValue(item, key, depth + 1));
|
|
33
|
+
}
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [k, v] of Object.entries(value)) {
|
|
36
|
+
out[k] = compactStoredToolArgValue(v, k, depth + 1);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function compactToolCallsForHistory(calls) {
|
|
42
|
+
if (!Array.isArray(calls)) return calls;
|
|
43
|
+
return calls.map((call) => {
|
|
44
|
+
if (!call || typeof call !== 'object') return call;
|
|
45
|
+
return {
|
|
46
|
+
...call,
|
|
47
|
+
arguments: compactStoredToolArgValue(call.arguments),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Restore the FULL body of ONE tool call inside a history assistant message
|
|
53
|
+
// whose toolCalls were compacted at push time. Used for a failed edit call so
|
|
54
|
+
// the model sees the original patch/old_string on retry instead of a
|
|
55
|
+
// `[mixdog compacted …]` placeholder it cannot act on. Must run BEFORE the
|
|
56
|
+
// message is first transmitted so it never mutates an already-cached prefix
|
|
57
|
+
// (the prompt cache is content-prefix matched).
|
|
58
|
+
//
|
|
59
|
+
// Only the compactable body/long keys (patch, old_string, new_string, content,
|
|
60
|
+
// rewrite, command, script) are restored, and at ANY depth — compaction is
|
|
61
|
+
// recursive (compactStoredToolArgValue), so batch shapes like edits[].old_string
|
|
62
|
+
// or writes[].content carry nested compacted bodies too. Every other field
|
|
63
|
+
// (e.g. `path`, which a tool may mutate in place during execution) is taken from
|
|
64
|
+
// the compacted snapshot captured at push time, before any mutation. The
|
|
65
|
+
// compacted args tree is built fresh by compactToolCallsForHistory and is not
|
|
66
|
+
// shared with originalCalls, so rebuilding it here is safe.
|
|
67
|
+
export function restoreToolCallBodyForId(assistantMsg, originalCalls, callId) {
|
|
68
|
+
if (!assistantMsg || !Array.isArray(assistantMsg.toolCalls) || !callId) return;
|
|
69
|
+
if (!Array.isArray(originalCalls)) return;
|
|
70
|
+
const tc = assistantMsg.toolCalls.find((t) => t && t.id === callId);
|
|
71
|
+
const orig = originalCalls.find((c) => c && c.id === callId);
|
|
72
|
+
if (!tc || !orig) return;
|
|
73
|
+
if (!tc.arguments || typeof tc.arguments !== 'object'
|
|
74
|
+
|| !orig.arguments || typeof orig.arguments !== 'object') return;
|
|
75
|
+
tc.arguments = _restoreCompactedBodies(tc.arguments, orig.arguments, '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Recursively rebuild a compacted args tree: replace ONLY compactable body/long
|
|
79
|
+
// string fields (matched by key at any depth) with their full originals, and
|
|
80
|
+
// keep every other field from the compacted snapshot. tcVal and origVal share
|
|
81
|
+
// the same structure (compaction only shortens body strings), so the walk
|
|
82
|
+
// descends them in parallel; a missing or non-object origVal falls back to the
|
|
83
|
+
// compacted value rather than throwing.
|
|
84
|
+
function _restoreCompactedBodies(tcVal, origVal, key) {
|
|
85
|
+
if ((STORED_TOOL_ARG_BODY_KEY_RE.test(key) || STORED_TOOL_ARG_LONG_KEY_RE.test(key))
|
|
86
|
+
&& typeof origVal === 'string') {
|
|
87
|
+
return origVal;
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(tcVal) && Array.isArray(origVal)) {
|
|
90
|
+
return tcVal.map((item, i) => _restoreCompactedBodies(item, origVal[i], key));
|
|
91
|
+
}
|
|
92
|
+
if (tcVal && typeof tcVal === 'object' && origVal && typeof origVal === 'object') {
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const k of Object.keys(tcVal)) {
|
|
95
|
+
out[k] = (k in origVal) ? _restoreCompactedBodies(tcVal[k], origVal[k], k) : tcVal[k];
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
return tcVal;
|
|
100
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Loop termination-reason classification, extracted from loop.mjs.
|
|
2
|
+
// Pure function over the final response + loop-end flags. No behavior change:
|
|
3
|
+
// the classification ladder is verbatim from the tail of agentLoop.
|
|
4
|
+
import { HIDDEN_AGENT_NAMES } from './hidden-agents.mjs';
|
|
5
|
+
|
|
6
|
+
// Stop reasons that signal the turn was cut short mid-synthesis (token cap,
|
|
7
|
+
// provider pause). Empty content + one of these reasons means the worker
|
|
8
|
+
// was not done. Covers Anthropic (pause_turn, max_tokens), OpenAI (length),
|
|
9
|
+
// Gemini (MAX_TOKENS, OTHER), and case variants.
|
|
10
|
+
export const INCOMPLETE_STOP_REASONS = new Set([
|
|
11
|
+
'pause_turn', 'max_tokens', 'length', 'MAX_TOKENS', 'OTHER',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
// Classify WHY the loop ended so agent-tool can promote an empty/abnormal
|
|
15
|
+
// finish to an explicit Lead-facing error instead of a silent empty
|
|
16
|
+
// "completed". Determine "has content" exactly the way the no-tool-call
|
|
17
|
+
// branch in agentLoop does (trimmed string content, or any reasoning content).
|
|
18
|
+
export function classifyTerminationReason(response, {
|
|
19
|
+
terminatedByCap,
|
|
20
|
+
terminatedBySoftCap,
|
|
21
|
+
softCapActive,
|
|
22
|
+
sessionAgent,
|
|
23
|
+
} = {}) {
|
|
24
|
+
const _finalHasContent = (typeof response?.content === 'string' && response.content.trim().length > 0)
|
|
25
|
+
|| (typeof response?.reasoningContent === 'string' && response.reasoningContent.trim().length > 0);
|
|
26
|
+
const _finalStopReason = response?.stopReason ?? response?.stop_reason ?? null;
|
|
27
|
+
const _finalIncompleteStop = _finalStopReason && INCOMPLETE_STOP_REASONS.has(_finalStopReason);
|
|
28
|
+
const _finalIsHidden = HIDDEN_AGENT_NAMES.has(sessionAgent);
|
|
29
|
+
if (terminatedByCap) {
|
|
30
|
+
// Real problem regardless of hidden/public: the loop never terminated
|
|
31
|
+
// on its own contract.
|
|
32
|
+
return 'iteration_cap';
|
|
33
|
+
}
|
|
34
|
+
if (terminatedBySoftCap || softCapActive) {
|
|
35
|
+
// Worker soft-cap wrap-up path: non-lead session hit the exploration
|
|
36
|
+
// budget and was steered to a text-only finish. Distinct from the hard
|
|
37
|
+
// cap (iteration_cap) — this is an expected, budget-driven termination,
|
|
38
|
+
// not a runaway. Covers BOTH the grace-exhausted path
|
|
39
|
+
// (terminatedBySoftCap) AND the compliant path where the model emitted
|
|
40
|
+
// the final text with no more tool calls while the soft cap was active
|
|
41
|
+
// (loop ends via the no-tool-call break, so terminatedBySoftCap stays
|
|
42
|
+
// false but softCapActive is still true).
|
|
43
|
+
return 'soft_cap_wrapup';
|
|
44
|
+
}
|
|
45
|
+
if (!_finalHasContent && _finalIncompleteStop) {
|
|
46
|
+
// Cut short mid-synthesis (token cap / provider pause). Real problem
|
|
47
|
+
// for hidden agents too.
|
|
48
|
+
return 'truncated';
|
|
49
|
+
}
|
|
50
|
+
if (!_finalHasContent && !_finalIsHidden) {
|
|
51
|
+
// Empty terminal turn. Only public agents violate their contract by
|
|
52
|
+
// finishing empty — hidden agents (explorer/cycle/…) legitimately emit
|
|
53
|
+
// text-only/empty terminal turns per their own role contract, so leave
|
|
54
|
+
// terminationReason undefined for them.
|
|
55
|
+
return 'empty';
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Tool-name classification + intra-turn signature helpers, extracted from
|
|
2
|
+
// loop.mjs. These drive cross-turn read dedup, scoped caching, shell routing,
|
|
3
|
+
// and duplicate-call detection. Strips the MCP prefix so direct calls and
|
|
4
|
+
// MCP-wrapped calls share the same cache.
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
|
|
7
|
+
export const MCP_TOOL_PREFIX = 'mcp__plugin_mixdog_mixdog__';
|
|
8
|
+
|
|
9
|
+
export function _stripMcpPrefix(name) {
|
|
10
|
+
return typeof name === 'string' && name.startsWith(MCP_TOOL_PREFIX)
|
|
11
|
+
? name.slice(MCP_TOOL_PREFIX.length) : name;
|
|
12
|
+
}
|
|
13
|
+
export function _isReadTool(name) {
|
|
14
|
+
return _stripMcpPrefix(name) === 'read';
|
|
15
|
+
}
|
|
16
|
+
export function _isMutationTool(name) {
|
|
17
|
+
const n = _stripMcpPrefix(name);
|
|
18
|
+
return n === 'apply_patch';
|
|
19
|
+
}
|
|
20
|
+
export const SCOPED_CACHEABLE_TOOLS = new Set([
|
|
21
|
+
'code_graph',
|
|
22
|
+
'grep',
|
|
23
|
+
'list',
|
|
24
|
+
'glob',
|
|
25
|
+
]);
|
|
26
|
+
export function _isScopedCacheableTool(name) {
|
|
27
|
+
const n = _stripMcpPrefix(name);
|
|
28
|
+
return SCOPED_CACHEABLE_TOOLS.has(n);
|
|
29
|
+
}
|
|
30
|
+
export function _isShellTool(name) {
|
|
31
|
+
const n = _stripMcpPrefix(name);
|
|
32
|
+
return n === 'shell' || n === 'bash_session';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Canonical signature for intra-turn duplicate detection. Sorting keys
|
|
36
|
+
// produces a stable hash regardless of arg-object key order. Anything
|
|
37
|
+
// non-serializable falls back to String(args) — still deterministic for
|
|
38
|
+
// the model's typical structured-arg shape.
|
|
39
|
+
export function _canonicalArgs(args) {
|
|
40
|
+
if (args == null || typeof args !== 'object') {
|
|
41
|
+
try { return JSON.stringify(args); } catch { return String(args); }
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const keys = Object.keys(args).sort();
|
|
45
|
+
const sorted = {};
|
|
46
|
+
for (const k of keys) sorted[k] = args[k];
|
|
47
|
+
return JSON.stringify(sorted);
|
|
48
|
+
} catch { return String(args); }
|
|
49
|
+
}
|
|
50
|
+
export function _intraTurnSig(name, args) {
|
|
51
|
+
return createHash('sha256').update(`${name}:${_canonicalArgs(args)}`).digest('hex').slice(0, 16);
|
|
52
|
+
}
|