mixdog 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -23
- package/package.json +34 -28
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +108 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +139 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +428 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +756 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/ink.js +54 -8
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
_applyCompactFailurePersistToSession,
|
|
6
|
+
_sessionMessagesAdvancedBeyondCompactedOutgoing,
|
|
7
|
+
} from './manager.mjs';
|
|
8
|
+
|
|
9
|
+
const compacted = [
|
|
10
|
+
{ role: 'user', content: 'summary turn' },
|
|
11
|
+
{ role: 'user', content: 'current task' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
test('sessionMessagesAdvancedBeyondCompactedOutgoing: true for compacted prefix plus final assistant', () => {
|
|
15
|
+
const current = [
|
|
16
|
+
...compacted,
|
|
17
|
+
{ role: 'assistant', content: 'done' },
|
|
18
|
+
];
|
|
19
|
+
assert.equal(_sessionMessagesAdvancedBeyondCompactedOutgoing(current, compacted), true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('sessionMessagesAdvancedBeyondCompactedOutgoing: true for compacted prefix plus empty-final assistant', () => {
|
|
23
|
+
const current = [
|
|
24
|
+
...compacted,
|
|
25
|
+
{ role: 'assistant', content: '', emptyFinal: true },
|
|
26
|
+
];
|
|
27
|
+
assert.equal(_sessionMessagesAdvancedBeyondCompactedOutgoing(current, compacted), true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('sessionMessagesAdvancedBeyondCompactedOutgoing: false when transcript has not advanced', () => {
|
|
31
|
+
assert.equal(_sessionMessagesAdvancedBeyondCompactedOutgoing(compacted, compacted), false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('sessionMessagesAdvancedBeyondCompactedOutgoing: false when extra message is not a final assistant', () => {
|
|
35
|
+
const current = [
|
|
36
|
+
...compacted,
|
|
37
|
+
{ role: 'user', content: 'steering follow-up' },
|
|
38
|
+
];
|
|
39
|
+
assert.equal(_sessionMessagesAdvancedBeyondCompactedOutgoing(current, compacted), false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('sessionMessagesAdvancedBeyondCompactedOutgoing: false when prefix diverges from compacted outgoing', () => {
|
|
43
|
+
const current = [
|
|
44
|
+
{ role: 'user', content: 'different summary' },
|
|
45
|
+
{ role: 'user', content: 'current task' },
|
|
46
|
+
{ role: 'assistant', content: 'done' },
|
|
47
|
+
];
|
|
48
|
+
assert.equal(_sessionMessagesAdvancedBeyondCompactedOutgoing(current, compacted), false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('applyCompactFailurePersistToSession: preserves providerState when transcript already advanced', () => {
|
|
52
|
+
const priorSanitized = [
|
|
53
|
+
...compacted,
|
|
54
|
+
{ role: 'assistant', content: 'done' },
|
|
55
|
+
];
|
|
56
|
+
const session = {
|
|
57
|
+
messages: priorSanitized,
|
|
58
|
+
providerState: { continuation: 'fresh' },
|
|
59
|
+
compaction: { lastStage: 'compacting' },
|
|
60
|
+
};
|
|
61
|
+
const applied = _applyCompactFailurePersistToSession(session, {
|
|
62
|
+
priorSanitized,
|
|
63
|
+
sanitized: compacted,
|
|
64
|
+
messagesAdvanced: true,
|
|
65
|
+
error: new Error('ask failed'),
|
|
66
|
+
});
|
|
67
|
+
assert.equal(applied, true);
|
|
68
|
+
assert.deepEqual(session.messages, priorSanitized);
|
|
69
|
+
assert.deepEqual(session.providerState, { continuation: 'fresh' });
|
|
70
|
+
assert.equal(session.compaction.lastStage, 'failed');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('applyCompactFailurePersistToSession: clears providerState when replacing messages with compacted outgoing', () => {
|
|
74
|
+
const priorSanitized = [
|
|
75
|
+
{ role: 'user', content: 'old long history' },
|
|
76
|
+
];
|
|
77
|
+
const session = {
|
|
78
|
+
messages: priorSanitized,
|
|
79
|
+
providerState: { stale: true },
|
|
80
|
+
compaction: { lastStage: 'compacting' },
|
|
81
|
+
};
|
|
82
|
+
const applied = _applyCompactFailurePersistToSession(session, {
|
|
83
|
+
priorSanitized,
|
|
84
|
+
sanitized: compacted,
|
|
85
|
+
messagesAdvanced: false,
|
|
86
|
+
error: { code: 'AGENT_CONTEXT_OVERFLOW' },
|
|
87
|
+
});
|
|
88
|
+
assert.equal(applied, true);
|
|
89
|
+
assert.deepEqual(session.messages, compacted);
|
|
90
|
+
assert.equal(session.providerState, undefined);
|
|
91
|
+
assert.equal(session.compaction.lastStage, 'overflow_failed');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('applyCompactFailurePersistToSession: no-op when messages unchanged and not advanced', () => {
|
|
95
|
+
const session = {
|
|
96
|
+
messages: compacted,
|
|
97
|
+
providerState: { keep: true },
|
|
98
|
+
};
|
|
99
|
+
const applied = _applyCompactFailurePersistToSession(session, {
|
|
100
|
+
priorSanitized: compacted,
|
|
101
|
+
sanitized: compacted,
|
|
102
|
+
messagesAdvanced: false,
|
|
103
|
+
});
|
|
104
|
+
assert.equal(applied, false);
|
|
105
|
+
assert.deepEqual(session.providerState, { keep: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
@@ -9,11 +9,49 @@ import { randomBytes } from 'crypto';
|
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { Worker } from 'worker_threads';
|
|
11
11
|
import { getPluginData } from '../config.mjs';
|
|
12
|
+
import { isAgentOwner } from '../agent-owner.mjs';
|
|
12
13
|
import { renameWithRetrySync, updateJsonAtomicSync, writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
14
|
+
import { sanitizeContentForStoredHistory } from '../providers/media-normalization.mjs';
|
|
13
15
|
|
|
14
16
|
const _lastSaveError = new Map(); // id -> { message, at }
|
|
15
17
|
const SESSION_SUMMARY_INDEX_VERSION = 1;
|
|
16
18
|
|
|
19
|
+
// The live in-memory session (and every model request)
|
|
20
|
+
// retains attached image bytes across turns so multi-turn recognition works.
|
|
21
|
+
// The persisted session JSON, however, replaces image content with a short
|
|
22
|
+
// text placeholder at serialization time — keeping session files small without
|
|
23
|
+
// starving the model of the image mid-conversation. Returns the same object
|
|
24
|
+
// reference when nothing changed (no-image sessions pay only a shallow scan).
|
|
25
|
+
function _sessionForDisk(session) {
|
|
26
|
+
// Strip the in-flight live-turn alias (askSession sets session.liveTurnMessages
|
|
27
|
+
// for the turn duration so contextStatus() can estimate live context growth).
|
|
28
|
+
// It is a transient duplicate of the working transcript and must never be
|
|
29
|
+
// serialized: mid-turn saves (persistIterationMetrics) would otherwise bloat
|
|
30
|
+
// the session file and persist a non-canonical message array.
|
|
31
|
+
const hasLiveTurn = session && typeof session === 'object'
|
|
32
|
+
&& Object.prototype.hasOwnProperty.call(session, 'liveTurnMessages');
|
|
33
|
+
const messages = Array.isArray(session?.messages) ? session.messages : null;
|
|
34
|
+
if (!messages || messages.length === 0) {
|
|
35
|
+
if (!hasLiveTurn) return session;
|
|
36
|
+
const { liveTurnMessages: _drop, ...rest } = session;
|
|
37
|
+
return rest;
|
|
38
|
+
}
|
|
39
|
+
let changed = false;
|
|
40
|
+
const out = messages.map((m) => {
|
|
41
|
+
if (!m || typeof m !== 'object') return m;
|
|
42
|
+
const content = sanitizeContentForStoredHistory(m.content);
|
|
43
|
+
if (content !== m.content) { changed = true; return { ...m, content }; }
|
|
44
|
+
return m;
|
|
45
|
+
});
|
|
46
|
+
if (!changed) {
|
|
47
|
+
if (!hasLiveTurn) return session;
|
|
48
|
+
const { liveTurnMessages: _drop, ...rest } = session;
|
|
49
|
+
return rest;
|
|
50
|
+
}
|
|
51
|
+
const { liveTurnMessages: _drop, ...rest } = session;
|
|
52
|
+
return { ...rest, messages: out };
|
|
53
|
+
}
|
|
54
|
+
|
|
17
55
|
function _renameWithRetrySync(tmp, target) {
|
|
18
56
|
return renameWithRetrySync(tmp, target);
|
|
19
57
|
}
|
|
@@ -77,7 +115,6 @@ function _isPreviewNoise(text) {
|
|
|
77
115
|
if (!value) return true;
|
|
78
116
|
if (value.startsWith('<system-reminder>')) return true;
|
|
79
117
|
if (/^Reference files:/i.test(value)) return true;
|
|
80
|
-
if (value.includes('<!-- bp3-sentinel -->')) return true;
|
|
81
118
|
return false;
|
|
82
119
|
}
|
|
83
120
|
|
|
@@ -133,7 +170,7 @@ function _sessionSummary(session) {
|
|
|
133
170
|
cwd: session.cwd || '',
|
|
134
171
|
provider: session.provider || null,
|
|
135
172
|
model: session.model || null,
|
|
136
|
-
|
|
173
|
+
agentTag: session.agentTag || null,
|
|
137
174
|
task_id: session.task_id || session.taskId || null,
|
|
138
175
|
permission: session.permission || null,
|
|
139
176
|
toolPermission: session.toolPermission || null,
|
|
@@ -163,7 +200,7 @@ function _normalizeSummaryRow(row) {
|
|
|
163
200
|
cwd: row.cwd || '',
|
|
164
201
|
provider: row.provider || null,
|
|
165
202
|
model: row.model || null,
|
|
166
|
-
|
|
203
|
+
agentTag: row.agentTag || null,
|
|
167
204
|
task_id: row.task_id || null,
|
|
168
205
|
permission: row.permission || null,
|
|
169
206
|
toolPermission: row.toolPermission || null,
|
|
@@ -278,19 +315,6 @@ function _clearDebounce(id) {
|
|
|
278
315
|
if (t) { clearTimeout(t); _debounceTimers.delete(id); }
|
|
279
316
|
}
|
|
280
317
|
|
|
281
|
-
// Flush all debounced sessions synchronously on process exit / SIGTERM.
|
|
282
|
-
// This prevents losing the last turn's tool-result writes.
|
|
283
|
-
function _drainAllDebounced() {
|
|
284
|
-
for (const [id, t] of _debounceTimers) {
|
|
285
|
-
clearTimeout(t);
|
|
286
|
-
_debounceTimers.delete(id);
|
|
287
|
-
const cur = _savePending.get(id);
|
|
288
|
-
if (cur && cur.debouncing && cur.payload) {
|
|
289
|
-
try { _doSaveSync(cur.payload); } catch { /* best-effort */ }
|
|
290
|
-
_savePending.delete(id);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
318
|
// Self-registered exit drain; bare 'exit' hook stays as idempotent backup. Use the more comprehensive
|
|
295
319
|
// drainSessionStore so debounce + scheduled + writing payloads all flush.
|
|
296
320
|
process.on('exit', drainSessionStore);
|
|
@@ -468,7 +492,7 @@ function _doSaveSync(payload) {
|
|
|
468
492
|
const target = sessionPath(id);
|
|
469
493
|
const tmp = target + '.' + randomBytes(6).toString('hex') + '.tmp';
|
|
470
494
|
try {
|
|
471
|
-
writeFileSync(tmp, JSON.stringify(session), 'utf-8');
|
|
495
|
+
writeFileSync(tmp, JSON.stringify(_sessionForDisk(session)), 'utf-8');
|
|
472
496
|
if (_shouldDrop(id, opts)) {
|
|
473
497
|
try { unlinkSync(tmp); } catch { /* ignore cleanup failure */ }
|
|
474
498
|
return;
|
|
@@ -490,7 +514,21 @@ function _shouldDrop(id, opts) {
|
|
|
490
514
|
const target = sessionPath(id);
|
|
491
515
|
if (!existsSync(target)) return false;
|
|
492
516
|
try {
|
|
493
|
-
const
|
|
517
|
+
const raw = readFileSync(target, 'utf-8');
|
|
518
|
+
// Tombstone check only needs `closed` and `generation`. Avoid a full
|
|
519
|
+
// JSON.parse of the entire session (which can be MBs on long chats and
|
|
520
|
+
// ran on every save). Fast-path a regex scan; only fall back to a full
|
|
521
|
+
// parse if the lightweight scan is inconclusive.
|
|
522
|
+
const closedMatch = /"closed"\s*:\s*(true|false)/.exec(raw);
|
|
523
|
+
if (closedMatch && closedMatch[1] === 'false') return false;
|
|
524
|
+
if (closedMatch && closedMatch[1] === 'true') {
|
|
525
|
+
const genMatch = /"generation"\s*:\s*(-?\d+)/.exec(raw);
|
|
526
|
+
const diskGen = genMatch ? Number(genMatch[1]) : 0;
|
|
527
|
+
return diskGen >= expected;
|
|
528
|
+
}
|
|
529
|
+
// No `closed` key found by scan — not a tombstone, so nothing to drop.
|
|
530
|
+
if (!closedMatch && !/"closed"/.test(raw)) return false;
|
|
531
|
+
const onDisk = JSON.parse(raw);
|
|
494
532
|
const diskGen = typeof onDisk.generation === 'number' ? onDisk.generation : 0;
|
|
495
533
|
return onDisk.closed === true && diskGen >= expected;
|
|
496
534
|
} catch {
|
|
@@ -557,7 +595,7 @@ async function _doSave(payload) {
|
|
|
557
595
|
const target = sessionPath(id);
|
|
558
596
|
const tmp = target + '.' + randomBytes(6).toString('hex') + '.tmp';
|
|
559
597
|
try {
|
|
560
|
-
await fsp.writeFile(tmp, JSON.stringify(session), 'utf-8');
|
|
598
|
+
await fsp.writeFile(tmp, JSON.stringify(_sessionForDisk(session)), 'utf-8');
|
|
561
599
|
// Second check: between the temp write and the rename, closeSession()
|
|
562
600
|
// may have planted a tombstone. Re-check on disk; if a newer tombstone
|
|
563
601
|
// now exists, discard our temp file rather than let rename clobber it.
|
|
@@ -596,7 +634,7 @@ export function markSessionClosed(id, reason = 'manual') {
|
|
|
596
634
|
const target = sessionPath(id);
|
|
597
635
|
const tmp = target + '.' + randomBytes(6).toString('hex') + '.tmp';
|
|
598
636
|
try {
|
|
599
|
-
writeFileSync(tmp, JSON.stringify(tombstone), 'utf-8');
|
|
637
|
+
writeFileSync(tmp, JSON.stringify(_sessionForDisk(tombstone)), 'utf-8');
|
|
600
638
|
_renameWithRetrySync(tmp, target);
|
|
601
639
|
} catch {
|
|
602
640
|
try { unlinkSync(tmp); } catch { /* ignore */ }
|
|
@@ -672,11 +710,11 @@ export function deleteSession(id) {
|
|
|
672
710
|
return removed;
|
|
673
711
|
}
|
|
674
712
|
const DEFAULT_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes idle — aligned with Anthropic 5m messages tier and OpenAI in-memory cache window
|
|
675
|
-
// Completed
|
|
713
|
+
// Completed agents (idle/done/error) live until terminal reap - must
|
|
676
714
|
// match TERMINAL_REAP_MS / _scheduleBridgeReap (3_600_000) in index.mjs and
|
|
677
|
-
//
|
|
678
|
-
const
|
|
679
|
-
const
|
|
715
|
+
// agent stall watchdog, so the store sweep is the durable 1h reaper.
|
|
716
|
+
const AGENT_TERMINAL_TTL_MS = 60 * 60 * 1000;
|
|
717
|
+
const AGENT_TERMINAL_STATUSES = new Set(['idle', 'done', 'error']);
|
|
680
718
|
// Hard wall-clock ceiling for sessions stuck in status='running'. The
|
|
681
719
|
// stream-watchdog should abort stalled streams within ~120s, but if it misses
|
|
682
720
|
// one (process crash, watchdog not started, provider never returned), this
|
|
@@ -807,9 +845,9 @@ export function sweepStaleSessions(ttlMs, options = {}) {
|
|
|
807
845
|
remaining++;
|
|
808
846
|
continue;
|
|
809
847
|
}
|
|
810
|
-
// Sweep
|
|
848
|
+
// Sweep agent-owned and ownerless (legacy) sessions; skip explicit
|
|
811
849
|
// user sessions before touching heartbeat sidecars.
|
|
812
|
-
if (typeof row.owner === 'string' && row.owner.length > 0 && row
|
|
850
|
+
if (typeof row.owner === 'string' && row.owner.length > 0 && !isAgentOwner(row)) {
|
|
813
851
|
remaining++;
|
|
814
852
|
continue;
|
|
815
853
|
}
|
|
@@ -829,9 +867,9 @@ export function sweepStaleSessions(ttlMs, options = {}) {
|
|
|
829
867
|
remaining++;
|
|
830
868
|
continue;
|
|
831
869
|
}
|
|
832
|
-
const
|
|
833
|
-
&&
|
|
834
|
-
const sessionMaxAge =
|
|
870
|
+
const isCompletedAgent = isAgentOwner(row)
|
|
871
|
+
&& AGENT_TERMINAL_STATUSES.has(row.status);
|
|
872
|
+
const sessionMaxAge = isCompletedAgent ? AGENT_TERMINAL_TTL_MS : maxAge;
|
|
835
873
|
if (now - lastActive > sessionMaxAge) {
|
|
836
874
|
try { markSessionClosed(row.id, 'idle-sweep'); }
|
|
837
875
|
catch (err) {
|
|
@@ -12,7 +12,7 @@ const TOOL_RESULT_SEARCH_THRESHOLD_CHARS = 50_000;
|
|
|
12
12
|
const TOOL_RESULT_GREP_THRESHOLD_CHARS = 20_000;
|
|
13
13
|
export const TOOL_RESULT_OFFLOAD_PREFIX = '[tool output offloaded:';
|
|
14
14
|
|
|
15
|
-
// Per-tool persistence limits mirror
|
|
15
|
+
// Per-tool persistence limits mirror reference per-tool maxResultSizeChars
|
|
16
16
|
// rather than a single global value: Grep persists at 20k (CC GrepTool), Glob
|
|
17
17
|
// and list/find_* at the 50k system default (CC DEFAULT_MAX_RESULT_SIZE_CHARS;
|
|
18
18
|
// tighter than CC Glob's 100k by design), and shell/bash_session/task at 30k
|
|
@@ -127,7 +127,7 @@ export async function maybeOffloadToolResult(sessionId, toolCallId, toolName, re
|
|
|
127
127
|
// tool-results/<sessionId>/*.txt entry, rmdirs the directory, and drops
|
|
128
128
|
// the sessionId entry from the module-level counter Map so a long-running
|
|
129
129
|
// mcp-server process doesn't accumulate per-session state across the
|
|
130
|
-
// lifetime of the
|
|
130
|
+
// lifetime of the agent runtime.
|
|
131
131
|
export async function clearOffloadSession(sessionId) {
|
|
132
132
|
if (!sessionId) return;
|
|
133
133
|
_offloadCounters.delete(sessionId);
|
|
@@ -6,12 +6,12 @@ const MIN_PROVIDER_TIMEOUT_MS = 30_000;
|
|
|
6
6
|
export const STALL_TICK_MS = 15_000;
|
|
7
7
|
export const DEFAULT_STALL_WARN_S = 300;
|
|
8
8
|
export const DEFAULT_STALL_ABORT_S = 600;
|
|
9
|
-
// First-byte (no-stream-delta) abort for the
|
|
9
|
+
// First-byte (no-stream-delta) abort for the agent stall watchdog. A wedged
|
|
10
10
|
// socket can sit at stage=requesting with zero server events. The 30s deadline
|
|
11
11
|
// trialed here false-aborted slow high-reasoning first bytes (e.g. gpt-5.5
|
|
12
12
|
// XHIGH, which can legitimately think >30s before the first delta) and, paired
|
|
13
13
|
// with dispatch auto-retry, produced premature aborts + duplicate re-dispatches.
|
|
14
|
-
// Auto-retry is now removed
|
|
14
|
+
// Auto-retry is now removed, so a single
|
|
15
15
|
// attempt must get a generous first-byte window: 300s (5 min). Env-overridable.
|
|
16
16
|
export const DEFAULT_STALL_FIRST_BYTE_ABORT_S = (() => {
|
|
17
17
|
const raw = process.env.MIXDOG_STALL_FIRST_BYTE_ABORT_S;
|
|
@@ -58,6 +58,14 @@ export function resolveTimeoutMs(envNames, fallbackMs, { minMs = 1_000, maxMs =
|
|
|
58
58
|
return Math.min(Math.max(fallbackMs, minMs), maxMs);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// While a tool call is in flight, bump lastProgressAt on this cadence so long
|
|
62
|
+
// executions stay "active" without emitting model-visible content.
|
|
63
|
+
export const DEFAULT_ACTIVITY_HEARTBEAT_MS = resolveTimeoutMs(
|
|
64
|
+
'MIXDOG_AGENT_ACTIVITY_HEARTBEAT_MS',
|
|
65
|
+
30_000,
|
|
66
|
+
{ minMs: 5_000, maxMs: 300_000 },
|
|
67
|
+
);
|
|
68
|
+
|
|
61
69
|
export const PROVIDER_FIRST_BYTE_TIMEOUT_MS = resolveTimeoutMs(
|
|
62
70
|
'MIXDOG_PROVIDER_FIRST_BYTE_TIMEOUT_MS',
|
|
63
71
|
Math.min(120_000, PROVIDER_MAX_BEFORE_WARN_MS),
|
|
@@ -94,19 +102,26 @@ export const PROVIDER_HTTP_RESPONSE_TIMEOUT_MS = resolveTimeoutMs(
|
|
|
94
102
|
{ minMs: 10_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
|
|
95
103
|
);
|
|
96
104
|
|
|
97
|
-
// Stream idle watchdog is
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
105
|
+
// Stream idle watchdog is ON by default. Earlier it was OFF (reference-agent
|
|
106
|
+
// parity) on the theory that a short inter-chunk timer would false-abort slow
|
|
107
|
+
// high-reasoning streams — but every provider's SSE loop resets the idle timer
|
|
108
|
+
// on EVERY chunk, including content-free keepalives (Anthropic `:ping`,
|
|
109
|
+
// OpenAI/compat reasoning_content deltas, Gemini chunks). So a live
|
|
110
|
+
// extended-thinking stream keeps the timer fresh; only a genuinely silent
|
|
111
|
+
// socket (no bytes at all for the whole window) trips it. With the watchdog
|
|
112
|
+
// OFF, such a wedged socket sat until the 600s agent stall backstop, which the
|
|
113
|
+
// trace logs showed as the dominant mid-stream hang (sse_parse_ms p99 ~183s,
|
|
114
|
+
// max ~411s). Defaulting ON with a generous 120s window cuts that tail ~5x
|
|
115
|
+
// while leaving genuine reasoning pauses (kept alive by keepalive frames)
|
|
116
|
+
// untouched. Force OFF with MIXDOG_ENABLE_STREAM_WATCHDOG=0.
|
|
103
117
|
const _sseWatchdogRaw = process.env.MIXDOG_ENABLE_STREAM_WATCHDOG;
|
|
104
|
-
export const PROVIDER_SSE_IDLE_WATCHDOG_ENABLED =
|
|
105
|
-
|
|
118
|
+
export const PROVIDER_SSE_IDLE_WATCHDOG_ENABLED = _sseWatchdogRaw === undefined || _sseWatchdogRaw === ''
|
|
119
|
+
? true
|
|
120
|
+
: (_sseWatchdogRaw === '1' || _sseWatchdogRaw === 'true' || _sseWatchdogRaw === 'yes');
|
|
106
121
|
|
|
107
122
|
export const PROVIDER_SSE_IDLE_TIMEOUT_MS = resolveTimeoutMs(
|
|
108
123
|
'MIXDOG_PROVIDER_SSE_IDLE_TIMEOUT_MS',
|
|
109
|
-
|
|
124
|
+
120_000,
|
|
110
125
|
{ minMs: 10_000, maxMs: STALL_WARN_MS },
|
|
111
126
|
);
|
|
112
127
|
|
|
@@ -143,15 +158,15 @@ export const PROVIDER_RETRY_JITTER_RATIO = (() => {
|
|
|
143
158
|
return 0.2;
|
|
144
159
|
})();
|
|
145
160
|
|
|
146
|
-
export function
|
|
161
|
+
export function resolveAgentStallThresholds(role, env = process.env) {
|
|
147
162
|
const cfg = role ? getHiddenRole(role) : null;
|
|
148
163
|
const cfgAbort = cfg?.stallCap?.idleSeconds > 0 ? cfg.stallCap.idleSeconds : STALL_ABORT_S;
|
|
149
164
|
const envOverride = envThresholdSeconds(env);
|
|
150
165
|
const abort = envOverride != null ? envOverride : cfgAbort;
|
|
151
|
-
// Mid-stream "slow" warning disabled —
|
|
166
|
+
// Mid-stream "slow" warning disabled — an agent stall now notifies ONLY at
|
|
152
167
|
// the abort deadline (10 min default). warn === abort means the watchdog's
|
|
153
168
|
// warn branch (verdict 'ok' && stale >= warn) can never fire before 'stall',
|
|
154
|
-
// so the only notification a stalled
|
|
169
|
+
// so the only notification a stalled agent worker emits is at the deadline.
|
|
155
170
|
const warn = abort;
|
|
156
171
|
// First-byte deadline: a request still in 'requesting' that never produced a
|
|
157
172
|
// single SSE delta is hung, not slow-reasoning. Abort on this shorter deadline
|
|
@@ -160,7 +175,7 @@ export function resolveBridgeStallThresholds(role, env = process.env) {
|
|
|
160
175
|
return { warn, abort, firstByteAbort };
|
|
161
176
|
}
|
|
162
177
|
|
|
163
|
-
export function
|
|
178
|
+
export function resolveAgentToolThresholdSeconds(role, thresholdSeconds) {
|
|
164
179
|
const cfg = role ? getHiddenRole(role) : null;
|
|
165
180
|
if (cfg?.stallCap?.toolRunningSeconds > 0) return cfg.stallCap.toolRunningSeconds;
|
|
166
181
|
return thresholdSeconds;
|
|
@@ -206,7 +221,7 @@ export function createTimeoutSignal(parentSignal, timeoutMs, label) {
|
|
|
206
221
|
// replaced-by-newer-request). Used for the streaming generation phase, where a fixed
|
|
207
222
|
// total-lifetime cap would false-abort a stream that is still emitting SSE deltas; the
|
|
208
223
|
// streaming phase is bounded instead by the per-attempt first-byte timeout, the parent
|
|
209
|
-
// signal, and the
|
|
224
|
+
// signal, and the agent stall watchdog (STALL_ABORT_S, progress-based). cleanup()
|
|
210
225
|
// detaches the parent listener so no listener leaks on the (long-lived) parent signal.
|
|
211
226
|
export function createPassthroughSignal(parentSignal) {
|
|
212
227
|
if (!parentSignal) return { signal: null, cleanup: () => {} };
|
|
@@ -1,229 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Soft-warn marker stripper.
|
|
3
3
|
*
|
|
4
4
|
* The tool-loop guard subsystem (repeated/fanned-out tool-use detection and its
|
|
5
|
-
* soft-warn sidecars) was removed once every warn path had been disabled.
|
|
6
|
-
*
|
|
7
|
-
* • classifyBashFileLookupCommand — detects a `bash` call whose first token is
|
|
8
|
-
* a file-lookup that has a dedicated in-process tool (read/grep/glob/list).
|
|
9
|
-
* Used by the bridge-worker permission gate to block the shell route.
|
|
10
|
-
* • classifyBridgeWorkerGitMutationCommand — detects bridge-worker shell
|
|
11
|
-
* calls that try to run git operations reserved for Lead.
|
|
5
|
+
* soft-warn sidecars) was removed once every warn path had been disabled. This
|
|
6
|
+
* module now retains a single utility:
|
|
12
7
|
* • stripSoftWarns — strips legacy soft-warn marker blocks from outbound
|
|
13
8
|
* bodies so older transcripts that still carry them stay clean.
|
|
14
9
|
*/
|
|
15
10
|
|
|
16
|
-
// File-lookup-via-shell detector. Matches a `bash` command whose first token
|
|
17
|
-
// is a unix find/grep/cat/ls/head/tail OR a powershell file-discovery cmdlet
|
|
18
|
-
// (Get-ChildItem / Select-String / Get-Content). These have dedicated tools
|
|
19
|
-
// (glob / grep / read / list) per rules/shared/01-tool.md; using bash routes
|
|
20
|
-
// around the in-process tool cache and the path-permission check, and the
|
|
21
|
-
// outputs were observed in PG telemetry running 100x slower than the dedicated
|
|
22
|
-
// path. Get-CimInstance / Win32_Process etc. are NOT matched — process
|
|
23
|
-
// enumeration genuinely needs the shell. One advisory per session per matched
|
|
24
|
-
// command class; subsequent matches stay silent so the model isn't drowned.
|
|
25
|
-
const SHELL_FILE_LOOKUP_COMMANDS = [
|
|
26
|
-
'find', 'ls', 'grep', 'rg', 'fd', 'cat', 'head', 'tail',
|
|
27
|
-
'sed', 'awk', 'less', 'more', 'wc', 'tree',
|
|
28
|
-
// File-metadata mirror paths called out in rules/shared/01-tool.md
|
|
29
|
-
// Tool Routing section. Added after probe HS-A3 surfaced that the
|
|
30
|
-
// rule explicitly lists `stat`, `du`, `file`, and `ls -l` as
|
|
31
|
-
// forbidden mirror paths but the policy under-enforced them.
|
|
32
|
-
'stat', 'du', 'file',
|
|
33
|
-
'dir', 'type', 'findstr',
|
|
34
|
-
'Get-ChildItem', 'gci', 'Select-String', 'sls', 'Get-Content', 'gc',
|
|
35
|
-
].join('|');
|
|
36
|
-
const BASH_FILE_LOOKUP_RE = new RegExp(
|
|
37
|
-
`^\\s*(?:git\\s+(?:grep|ls-files)\\b|(?:${SHELL_FILE_LOOKUP_COMMANDS})\\b)`,
|
|
38
|
-
'i',
|
|
39
|
-
);
|
|
40
|
-
// Wrapper / eval forms that route the same file-lookup intent through a host
|
|
41
|
-
// shell (`bash -c "grep ..."`, `pwsh -Command Get-Content ...`) or an
|
|
42
|
-
// inline interpreter eval (`node -e fs.readFileSync(...)`,
|
|
43
|
-
// `python -c "open(...)"`). Without these the advisory was bypassable just
|
|
44
|
-
// by quoting the call inside another command.
|
|
45
|
-
const BASH_WRAPPER_FILE_LOOKUP_RE = new RegExp(
|
|
46
|
-
`^\\s*(?:bash|sh|zsh|fish|dash)\\b[^\\n]*\\s-\\w*c\\w*\\s+["']?\\s*(?:${SHELL_FILE_LOOKUP_COMMANDS})\\b`,
|
|
47
|
-
'i',
|
|
48
|
-
);
|
|
49
|
-
const CMD_WRAPPER_FILE_LOOKUP_RE = /^\s*cmd(?:\.exe)?\s+\/[cs]\s+["']?\s*(?:dir|type|findstr)\b/i;
|
|
50
|
-
const PWSH_WRAPPER_FILE_LOOKUP_RE = /^\s*(?:pwsh|powershell)(?:\.exe)?\b[\s\S]*?(?:-|\/)(?:Command|c)\s+["']?\s*(?:Get-ChildItem|gci|dir|ls|Select-String|sls|Get-Content|gc|cat|type)\b/i;
|
|
51
|
-
const NODE_FS_EVAL_RE = /^\s*node(?:\.exe)?\b[\s\S]*?\s-e\s+["'](?![\s\S]*\bJSON\.parse\s*\()[\s\S]*?\b(?:fs\.|readFileSync|readFile|readdirSync|readdir|statSync|stat)\b/i;
|
|
52
|
-
const PYTHON_OPEN_EVAL_RE = /^\s*python3?(?:\.exe)?\b[\s\S]*?\s-c\s+["'][\s\S]*?\b(?:open\s*\(|Path\s*\(|read_text\s*\(|read_bytes\s*\(|iterdir\s*\()/i;
|
|
53
|
-
function _classifyBashFileLookup(command) {
|
|
54
|
-
if (typeof command !== 'string' || !command) return null;
|
|
55
|
-
const direct = command.match(BASH_FILE_LOOKUP_RE);
|
|
56
|
-
if (direct) return direct[0].trim().toLowerCase();
|
|
57
|
-
if (BASH_WRAPPER_FILE_LOOKUP_RE.test(command)) return 'bash-wrapped';
|
|
58
|
-
if (CMD_WRAPPER_FILE_LOOKUP_RE.test(command)) return 'cmd-wrapped';
|
|
59
|
-
if (PWSH_WRAPPER_FILE_LOOKUP_RE.test(command)) return 'pwsh-wrapped';
|
|
60
|
-
if (NODE_FS_EVAL_RE.test(command)) return 'node-fs-eval';
|
|
61
|
-
if (PYTHON_OPEN_EVAL_RE.test(command)) return 'python-open-eval';
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function classifyBashFileLookupCommand(command) {
|
|
66
|
-
return _classifyBashFileLookup(command);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const SHELL_WRAPPER_COMMANDS = new Set([
|
|
70
|
-
'bash', 'sh', 'zsh', 'fish', 'dash',
|
|
71
|
-
'cmd', 'cmd.exe',
|
|
72
|
-
'pwsh', 'pwsh.exe', 'powershell', 'powershell.exe',
|
|
73
|
-
]);
|
|
74
|
-
const POSIX_SHELL_WRAPPER_COMMANDS = new Set(['bash', 'sh', 'zsh', 'fish', 'dash']);
|
|
75
|
-
const CMD_SHELL_WRAPPER_COMMANDS = new Set(['cmd', 'cmd.exe']);
|
|
76
|
-
const POWERSHELL_WRAPPER_COMMANDS = new Set(['pwsh', 'pwsh.exe', 'powershell', 'powershell.exe']);
|
|
77
|
-
const SHELL_SEPARATORS = new Set([';', '\n', '&&', '||', '|', '&']);
|
|
78
|
-
const READONLY_GIT_SUBCOMMANDS = new Set([
|
|
79
|
-
'status', 'diff', 'show', 'log', 'rev-parse', 'ls-files', 'grep', 'blame', 'describe', 'merge-base',
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
function shellTokenizeLoose(command) {
|
|
83
|
-
const tokens = [];
|
|
84
|
-
let current = '';
|
|
85
|
-
let quote = null;
|
|
86
|
-
let escape = false;
|
|
87
|
-
const push = () => {
|
|
88
|
-
if (current !== '') tokens.push(current);
|
|
89
|
-
current = '';
|
|
90
|
-
};
|
|
91
|
-
const pushSep = (sep) => {
|
|
92
|
-
push();
|
|
93
|
-
tokens.push(sep);
|
|
94
|
-
};
|
|
95
|
-
for (let i = 0; i < command.length; i += 1) {
|
|
96
|
-
const ch = command[i];
|
|
97
|
-
if (escape) {
|
|
98
|
-
current += ch;
|
|
99
|
-
escape = false;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (ch === '\\') {
|
|
103
|
-
escape = true;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (quote) {
|
|
107
|
-
if (ch === quote) quote = null;
|
|
108
|
-
else current += ch;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (ch === '\'' || ch === '"') {
|
|
112
|
-
quote = ch;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (ch === '\n') {
|
|
116
|
-
pushSep('\n');
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
if (ch === ';') {
|
|
120
|
-
pushSep(';');
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if ((ch === '&' || ch === '|') && command[i + 1] === ch) {
|
|
124
|
-
pushSep(`${ch}${ch}`);
|
|
125
|
-
i += 1;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (ch === '|' || ch === '&') {
|
|
129
|
-
pushSep(ch);
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (/\s/.test(ch)) {
|
|
133
|
-
push();
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
current += ch;
|
|
137
|
-
}
|
|
138
|
-
push();
|
|
139
|
-
return tokens;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function isCommandPosition(tokens, index) {
|
|
143
|
-
for (let i = index - 1; i >= 0; i -= 1) {
|
|
144
|
-
if (!tokens[i]) continue;
|
|
145
|
-
return SHELL_SEPARATORS.has(tokens[i]);
|
|
146
|
-
}
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function skipGitGlobalOptions(tokens, index) {
|
|
151
|
-
let i = index;
|
|
152
|
-
while (i < tokens.length) {
|
|
153
|
-
const token = String(tokens[i] || '');
|
|
154
|
-
const lower = token.toLowerCase();
|
|
155
|
-
if (!token || SHELL_SEPARATORS.has(token)) return i;
|
|
156
|
-
if (lower === '-c' || lower === '-C' || lower === '--git-dir' || lower === '--work-tree' || lower === '--namespace') {
|
|
157
|
-
i += 2;
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (/^(?:--git-dir|--work-tree|--namespace)=/i.test(token)) {
|
|
161
|
-
i += 1;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
if (lower === '--no-pager' || lower === '-p' || lower === '--literal-pathspecs' || lower === '--glob-pathspecs' || lower === '--noglob-pathspecs' || lower === '--icase-pathspecs') {
|
|
165
|
-
i += 1;
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (/^-/.test(token)) {
|
|
169
|
-
i += 1;
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
return i;
|
|
173
|
-
}
|
|
174
|
-
return i;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function readonlyGitInvocation(tokens, subIndex) {
|
|
178
|
-
const sub = String(tokens[subIndex] || '').toLowerCase();
|
|
179
|
-
if (!READONLY_GIT_SUBCOMMANDS.has(sub)) return false;
|
|
180
|
-
if (sub === 'remote') {
|
|
181
|
-
const next = String(tokens[subIndex + 1] || '').toLowerCase();
|
|
182
|
-
return !next || next === '-v' || next === '--verbose' || next === 'show';
|
|
183
|
-
}
|
|
184
|
-
if (sub === 'branch') {
|
|
185
|
-
const rest = tokens.slice(subIndex + 1).filter((token) => token && !SHELL_SEPARATORS.has(token));
|
|
186
|
-
return rest.length === 0 || rest.every((token) => ['--show-current', '-a', '-r', '-v', '-vv', '--all', '--remotes', '--verbose', '--contains', '--merged', '--no-merged'].includes(String(token).toLowerCase()));
|
|
187
|
-
}
|
|
188
|
-
return true;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function isShellCommandArgFlag(wrapper, flag) {
|
|
192
|
-
const lower = String(flag || '').toLowerCase();
|
|
193
|
-
if (POSIX_SHELL_WRAPPER_COMMANDS.has(wrapper)) return /^-\w*c\w*$/.test(lower);
|
|
194
|
-
if (CMD_SHELL_WRAPPER_COMMANDS.has(wrapper)) return lower === '/c';
|
|
195
|
-
if (POWERSHELL_WRAPPER_COMMANDS.has(wrapper)) return lower === '-command' || lower === '/command' || lower === '-c';
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function classifyBridgeWorkerGitMutationCommand(command) {
|
|
200
|
-
if (typeof command !== 'string' || !command.trim()) return null;
|
|
201
|
-
const tokens = shellTokenizeLoose(command);
|
|
202
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
203
|
-
const token = String(tokens[i] || '').toLowerCase();
|
|
204
|
-
if (!token || SHELL_SEPARATORS.has(token) || !isCommandPosition(tokens, i)) continue;
|
|
205
|
-
if (SHELL_WRAPPER_COMMANDS.has(token)) {
|
|
206
|
-
for (let j = i + 1; j < tokens.length; j += 1) {
|
|
207
|
-
const flag = String(tokens[j] || '').toLowerCase();
|
|
208
|
-
if (SHELL_SEPARATORS.has(flag)) break;
|
|
209
|
-
if (!isShellCommandArgFlag(token, flag)) continue;
|
|
210
|
-
const nested = tokens.slice(j + 1).join(' ');
|
|
211
|
-
const nestedHit = classifyBridgeWorkerGitMutationCommand(nested || '');
|
|
212
|
-
if (nestedHit) return nestedHit;
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (token !== 'git' && token !== 'git.exe') continue;
|
|
218
|
-
const subIndex = skipGitGlobalOptions(tokens, i + 1);
|
|
219
|
-
const sub = String(tokens[subIndex] || '').toLowerCase();
|
|
220
|
-
if (!sub || SHELL_SEPARATORS.has(sub)) return 'git';
|
|
221
|
-
if (readonlyGitInvocation(tokens, subIndex)) continue;
|
|
222
|
-
return `git ${sub}`;
|
|
223
|
-
}
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
11
|
// Strip soft-warn marker blocks (header ⚠ <label> through next blank line / EOF)
|
|
228
12
|
// from outbound bodies. Never call on tool-result bodies fed back to the model.
|
|
229
13
|
// New compact format: each warn is a single line starting with `⚠ <Label>(` and ending at newline.
|