muonroi-cli 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
- package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
- package/dist/packages/agent-harness-core/src/selector.js +2 -2
- package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
- package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
- package/dist/packages/agent-harness-opentui/src/install.js +1 -1
- package/dist/src/__test-stubs__/vitest-setup.js +4 -0
- package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
- package/dist/src/agent-harness/mock-model.d.ts +1 -1
- package/dist/src/agent-harness/mock-model.js +32 -3
- package/dist/src/agent-harness/test-spawn.js +3 -1
- package/dist/src/chat/chat-keychain.js +9 -1
- package/dist/src/cli/cost-forensics.js +12 -12
- package/dist/src/cli/keys.d.ts +2 -2
- package/dist/src/cli/keys.js +107 -28
- package/dist/src/cli/usage-report.js +2 -2
- package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
- package/dist/src/council/__tests__/abort-threading.test.js +193 -0
- package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
- package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
- package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
- package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
- package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
- package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
- package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
- package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
- package/dist/src/council/__tests__/research-tools.test.js +4 -0
- package/dist/src/council/__tests__/round-tools.test.js +6 -0
- package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
- package/dist/src/council/clarifier-question-cap.test.js +73 -0
- package/dist/src/council/clarifier.d.ts +29 -1
- package/dist/src/council/clarifier.js +64 -1
- package/dist/src/council/debate-planner.d.ts +2 -1
- package/dist/src/council/debate-planner.js +5 -3
- package/dist/src/council/debate.d.ts +9 -0
- package/dist/src/council/debate.js +88 -8
- package/dist/src/council/index.d.ts +46 -1
- package/dist/src/council/index.js +125 -19
- package/dist/src/council/llm.js +41 -13
- package/dist/src/council/prompts.js +56 -21
- package/dist/src/council/types.d.ts +14 -1
- package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
- package/dist/src/ee/bridge.d.ts +3 -1
- package/dist/src/ee/bridge.js +9 -3
- package/dist/src/ee/bridge.test.js +1 -2
- package/dist/src/ee/client.js +15 -2
- package/dist/src/ee/recall-ledger.d.ts +20 -0
- package/dist/src/ee/recall-ledger.js +40 -1
- package/dist/src/ee/search.d.ts +25 -0
- package/dist/src/ee/search.js +73 -0
- package/dist/src/ee/types.d.ts +15 -1
- package/dist/src/ee/who-am-i.d.ts +41 -0
- package/dist/src/ee/who-am-i.js +143 -0
- package/dist/src/ee/who-am-i.test.d.ts +1 -0
- package/dist/src/ee/who-am-i.test.js +101 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/headless/council-answers.js +1 -0
- package/dist/src/headless/output.d.ts +30 -1
- package/dist/src/headless/output.js +180 -0
- package/dist/src/headless/output.test.js +61 -1
- package/dist/src/hooks/index.js +48 -2
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +143 -104
- package/dist/src/lsp/client-normalize.test.d.ts +12 -0
- package/dist/src/lsp/client-normalize.test.js +34 -0
- package/dist/src/lsp/client.d.ts +1 -0
- package/dist/src/lsp/client.js +10 -2
- package/dist/src/lsp/manager.d.ts +1 -1
- package/dist/src/lsp/manager.js +2 -2
- package/dist/src/lsp/manager.test.js +2 -2
- package/dist/src/lsp/runtime.d.ts +1 -1
- package/dist/src/lsp/runtime.js +2 -2
- package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
- package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
- package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
- package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
- package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
- package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
- package/dist/src/mcp/client-pool.d.ts +1 -1
- package/dist/src/mcp/client-pool.js +25 -9
- package/dist/src/mcp/ee-tools.d.ts +10 -0
- package/dist/src/mcp/ee-tools.js +34 -0
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/self-verify-jobs.js +1 -3
- package/dist/src/mcp/setup-guide-text.js +74 -74
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/models/catalog-gemini.test.js +12 -10
- package/dist/src/models/catalog.json +106 -38
- package/dist/src/ops/doctor.js +7 -7
- package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
- package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
- package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
- package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
- package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
- package/dist/src/orchestrator/agent.test.js +5 -2
- package/dist/src/orchestrator/auto-commit.d.ts +110 -0
- package/dist/src/orchestrator/auto-commit.js +364 -0
- package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
- package/dist/src/orchestrator/batch-turn-runner.js +2 -1
- package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
- package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
- package/dist/src/orchestrator/compaction.d.ts +22 -0
- package/dist/src/orchestrator/compaction.js +42 -0
- package/dist/src/orchestrator/delegations.d.ts +1 -0
- package/dist/src/orchestrator/delegations.js +41 -0
- package/dist/src/orchestrator/error-utils.js +27 -2
- package/dist/src/orchestrator/message-processor.d.ts +21 -5
- package/dist/src/orchestrator/message-processor.js +219 -2573
- package/dist/src/orchestrator/orchestrator.d.ts +8 -2
- package/dist/src/orchestrator/orchestrator.js +187 -34
- package/dist/src/orchestrator/preprocessor.d.ts +13 -0
- package/dist/src/orchestrator/preprocessor.js +145 -0
- package/dist/src/orchestrator/prompts.d.ts +8 -0
- package/dist/src/orchestrator/prompts.js +76 -60
- package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
- package/dist/src/orchestrator/safety-askcard.js +74 -0
- package/dist/src/orchestrator/sandbox.test.js +8 -5
- package/dist/src/orchestrator/scope-ceiling.js +1 -1
- package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
- package/dist/src/orchestrator/stall-watchdog.js +23 -0
- package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
- package/dist/src/orchestrator/stream-runner.js +52 -30
- package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
- package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
- package/dist/src/orchestrator/subagent-compactor.js +83 -10
- package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
- package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
- package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
- package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
- package/dist/src/orchestrator/tool-args-repair.js +1 -1
- package/dist/src/orchestrator/tool-engine.d.ts +151 -0
- package/dist/src/orchestrator/tool-engine.js +3079 -0
- package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
- package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
- package/dist/src/pil/__tests__/discovery.test.js +89 -145
- package/dist/src/pil/__tests__/dual-run.test.js +2 -0
- package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
- package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
- package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
- package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
- package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
- package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
- package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
- package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
- package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
- package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
- package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
- package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
- package/dist/src/pil/__tests__/pipeline.test.js +11 -8
- package/dist/src/pil/__tests__/response-tools.test.js +52 -0
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +5 -3
- package/dist/src/pil/agent-operating-contract.test.js +2 -2
- package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
- package/dist/src/pil/cheap-model-playbook.js +1 -1
- package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
- package/dist/src/pil/cheap-model-workbooks.js +1 -1
- package/dist/src/pil/config.d.ts +20 -0
- package/dist/src/pil/config.js +35 -0
- package/dist/src/pil/discovery-types.d.ts +32 -9
- package/dist/src/pil/discovery.d.ts +3 -3
- package/dist/src/pil/discovery.js +138 -244
- package/dist/src/pil/index.d.ts +1 -1
- package/dist/src/pil/index.js +1 -1
- package/dist/src/pil/layer1-intent.d.ts +9 -1
- package/dist/src/pil/layer1-intent.js +99 -44
- package/dist/src/pil/layer1-intent.test.js +112 -2
- package/dist/src/pil/layer15-context-scan.js +1 -1
- package/dist/src/pil/layer16-clarity.d.ts +21 -25
- package/dist/src/pil/layer16-clarity.js +52 -88
- package/dist/src/pil/layer18-acceptance.d.ts +13 -3
- package/dist/src/pil/layer18-acceptance.js +1 -75
- package/dist/src/pil/layer2-personality.js +7 -8
- package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
- package/dist/src/pil/layer2_5-ponytail.js +41 -0
- package/dist/src/pil/layer3-ee-injection.js +180 -14
- package/dist/src/pil/layer4-gsd.js +22 -8
- package/dist/src/pil/layer5-context.d.ts +1 -0
- package/dist/src/pil/layer5-context.js +26 -11
- package/dist/src/pil/layer6-output.js +23 -5
- package/dist/src/pil/llm-classify.js +3 -3
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +4 -3
- package/dist/src/pil/pipeline.js +15 -1
- package/dist/src/pil/response-tools.d.ts +16 -0
- package/dist/src/pil/response-tools.js +41 -0
- package/dist/src/pil/schema.d.ts +8 -0
- package/dist/src/pil/schema.js +24 -3
- package/dist/src/pil/task-tier-map.js +7 -7
- package/dist/src/pil/types.d.ts +9 -3
- package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
- package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
- package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
- package/dist/src/product-loop/discovery-interview.d.ts +20 -0
- package/dist/src/product-loop/discovery-interview.js +121 -7
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/gather.js +19 -5
- package/dist/src/product-loop/loop-driver.js +20 -20
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/product-loop/sprint-self-verify.js +2 -2
- package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
- package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
- package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
- package/dist/src/providers/auth/gcloud.d.ts +28 -0
- package/dist/src/providers/auth/gcloud.js +102 -0
- package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
- package/dist/src/providers/auth/gemini-oauth.js +292 -105
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
- package/dist/src/providers/auth/grok-oauth.js +1 -1
- package/dist/src/providers/auth/registry.js +28 -6
- package/dist/src/providers/gemini.d.ts +1 -1
- package/dist/src/providers/gemini.js +10 -3
- package/dist/src/providers/keychain.d.ts +2 -2
- package/dist/src/providers/keychain.js +20 -2
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/providers/openai.js +5 -1
- package/dist/src/providers/runtime.d.ts +1 -1
- package/dist/src/providers/runtime.js +11 -2
- package/dist/src/providers/siliconflow-sse-repair.js +1 -1
- package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
- package/dist/src/providers/strategies/google.strategy.js +159 -9
- package/dist/src/providers/strategies/thinking-mode.js +19 -6
- package/dist/src/providers/types.d.ts +1 -1
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/router/decide.test.js +40 -8
- package/dist/src/router/step-router.d.ts +20 -0
- package/dist/src/router/step-router.js +73 -0
- package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +19 -19
- package/dist/src/self-qa/orchestrator.js +5 -5
- package/dist/src/self-qa/scenario-planner.js +1 -1
- package/dist/src/self-qa/spec-emitter.js +4 -4
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +122 -122
- package/dist/src/storage/session-experience-store.js +4 -4
- package/dist/src/storage/sessions.d.ts +6 -0
- package/dist/src/storage/sessions.js +49 -42
- package/dist/src/storage/transcript-view.js +7 -1
- package/dist/src/storage/transcript.d.ts +1 -0
- package/dist/src/storage/transcript.js +55 -0
- package/dist/src/storage/ui-interaction-log.d.ts +17 -0
- package/dist/src/storage/usage.js +14 -14
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/__tests__/native-tools.test.js +6 -0
- package/dist/src/tools/bash.d.ts +36 -23
- package/dist/src/tools/bash.js +59 -301
- package/dist/src/tools/bash.test.js +2 -257
- package/dist/src/tools/file-tracker.d.ts +5 -0
- package/dist/src/tools/file-tracker.js +14 -0
- package/dist/src/tools/file.js +2 -0
- package/dist/src/tools/git-safety.d.ts +38 -1
- package/dist/src/tools/git-safety.js +94 -4
- package/dist/src/tools/git-safety.test.js +45 -1
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +56 -0
- package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
- package/dist/src/tools/registry-git-safety.test.js +17 -6
- package/dist/src/tools/registry.d.ts +5 -0
- package/dist/src/tools/registry.js +185 -11
- package/dist/src/types/index.d.ts +7 -1
- package/dist/src/ui/app.d.ts +2 -1
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.js +37 -3
- package/dist/src/ui/components/structured-response-view.js +7 -0
- package/dist/src/ui/components/task-list-panel.js +3 -12
- package/dist/src/ui/containers/chat-feed.d.ts +40 -0
- package/dist/src/ui/containers/chat-feed.js +66 -0
- package/dist/src/ui/containers/modals-layer.d.ts +87 -0
- package/dist/src/ui/containers/modals-layer.js +18 -0
- package/dist/src/ui/modals/session-picker-modal.js +14 -9
- package/dist/src/ui/modals/update-modal.js +2 -1
- package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
- package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
- package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
- package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/export.js +16 -1
- package/dist/src/ui/slash/menu-items.js +1 -0
- package/dist/src/ui/slash/ponytail.d.ts +2 -0
- package/dist/src/ui/slash/ponytail.js +19 -0
- package/dist/src/ui/status-bar/store.js +0 -1
- package/dist/src/ui/status-bar/store.test.js +2 -2
- package/dist/src/ui/types.d.ts +7 -0
- package/dist/src/ui/use-app-logic.d.ts +26 -0
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/ui/utils/relaunch.d.ts +17 -0
- package/dist/src/ui/utils/relaunch.js +28 -4
- package/dist/src/ui/utils/relaunch.test.js +50 -0
- package/dist/src/ui/utils/text.d.ts +11 -0
- package/dist/src/ui/utils/text.js +21 -1
- package/dist/src/ui/utils/text.test.js +25 -1
- package/dist/src/ui/utils/tools.js +3 -1
- package/dist/src/usage/cost-log.js +2 -2
- package/dist/src/usage/estimator.d.ts +14 -0
- package/dist/src/usage/estimator.js +22 -0
- package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
- package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
- package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
- package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/llm-deadline.d.ts +9 -1
- package/dist/src/utils/llm-deadline.js +31 -2
- package/dist/src/utils/permission-mode.d.ts +33 -0
- package/dist/src/utils/permission-mode.js +164 -3
- package/dist/src/utils/permission-mode.test.js +92 -1
- package/dist/src/utils/settings.d.ts +27 -31
- package/dist/src/utils/settings.js +100 -109
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- package/package.json +1 -1
|
@@ -20,22 +20,28 @@ import { spawn } from "node:child_process";
|
|
|
20
20
|
* `process.argv[0]` itself. We sanitize the WHOLE args portion in one pass.
|
|
21
21
|
*/
|
|
22
22
|
export function sanitizeArgvForResume(args, sessionId) {
|
|
23
|
-
if (!sessionId
|
|
23
|
+
if (!sessionId?.trim()) {
|
|
24
24
|
throw new Error("sanitizeArgvForResume: sessionId is required");
|
|
25
25
|
}
|
|
26
26
|
const out = [];
|
|
27
27
|
for (let i = 0; i < args.length; i++) {
|
|
28
28
|
const a = args[i];
|
|
29
|
-
if (a === "-s" || a === "--session") {
|
|
29
|
+
if (a === "-s" || a === "--session" || a === "--mock-llm") {
|
|
30
30
|
// skip the flag AND its value (if present and not another flag)
|
|
31
31
|
const next = args[i + 1];
|
|
32
32
|
if (next !== undefined && !next.startsWith("-"))
|
|
33
33
|
i++;
|
|
34
34
|
continue;
|
|
35
35
|
}
|
|
36
|
-
if (a.startsWith("--session=")) {
|
|
36
|
+
if (a.startsWith("--session=") || a.startsWith("--mock-llm=")) {
|
|
37
37
|
continue; // skip the combined form
|
|
38
38
|
}
|
|
39
|
+
// Transient launch-mode flags must NOT survive a resume — re-entering
|
|
40
|
+
// agent-mode (named-pipe transport) on a user-driven resume strands the
|
|
41
|
+
// child with no harness server. Drop them.
|
|
42
|
+
if (a === "--agent-mode") {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
39
45
|
out.push(a);
|
|
40
46
|
}
|
|
41
47
|
out.push("--session", sessionId);
|
|
@@ -60,12 +66,30 @@ export function relaunchWithSession(sessionId, opts = {}) {
|
|
|
60
66
|
const exit = opts.onExit ?? ((code) => process.exit(code));
|
|
61
67
|
const spawnImpl = opts.spawnFn ?? spawn;
|
|
62
68
|
const args = sanitizeArgvForResume(argv.slice(1), sessionId);
|
|
69
|
+
// Tear down the current TUI and restore the terminal BEFORE spawning so the
|
|
70
|
+
// child inherits a clean TTY (no raw mode / mouse tracking / alt-screen).
|
|
71
|
+
if (opts.beforeSpawn) {
|
|
72
|
+
try {
|
|
73
|
+
opts.beforeSpawn();
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error(`[relaunch] beforeSpawn teardown failed: ${err?.message ?? err}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
63
79
|
const child = spawnImpl(exec, args, { stdio: "inherit", detached: false });
|
|
64
80
|
child.once("error", (err) => {
|
|
65
81
|
console.error(`[relaunch] spawn failed: ${err?.message ?? err}`);
|
|
66
82
|
exit(1);
|
|
67
83
|
});
|
|
68
|
-
|
|
84
|
+
if (opts.supervise) {
|
|
85
|
+
// Stay alive until the child exits so any intermediate launcher (bun run
|
|
86
|
+
// wrapper / bin shim) keeps the terminal foreground for the child instead
|
|
87
|
+
// of letting the shell reclaim it mid-restart.
|
|
88
|
+
child.once("exit", (code) => exit(code ?? 0));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Legacy fast-handoff: exit the instant the child spawns. Correct only when
|
|
92
|
+
// there is no intervening launcher waiting on this process.
|
|
69
93
|
child.once("spawn", () => exit(0));
|
|
70
94
|
}
|
|
71
95
|
//# sourceMappingURL=relaunch.js.map
|
|
@@ -40,6 +40,24 @@ describe("sanitizeArgvForResume", () => {
|
|
|
40
40
|
expect(() => sanitizeArgvForResume([], "")).toThrow(/sessionId is required/);
|
|
41
41
|
expect(() => sanitizeArgvForResume([], " ")).toThrow(/sessionId is required/);
|
|
42
42
|
});
|
|
43
|
+
it("strips transient launch-mode flags (--agent-mode) so resume does not re-enter agent-mode", () => {
|
|
44
|
+
expect(sanitizeArgvForResume(["--agent-mode", "-m", "grok-build-0.1"], "new-id")).toEqual([
|
|
45
|
+
"-m",
|
|
46
|
+
"grok-build-0.1",
|
|
47
|
+
"--session",
|
|
48
|
+
"new-id",
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
it("strips `--mock-llm <dir>` and the combined form", () => {
|
|
52
|
+
expect(sanitizeArgvForResume(["--mock-llm", "/fix/dir", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
|
|
53
|
+
expect(sanitizeArgvForResume(["--mock-llm=/fix/dir", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
|
|
54
|
+
});
|
|
55
|
+
it("strips agent-mode + mock-llm together (harness-launched resume → clean user CLI)", () => {
|
|
56
|
+
expect(sanitizeArgvForResume(["--agent-mode", "--mock-llm", "/fx", "--session", "old"], "new")).toEqual([
|
|
57
|
+
"--session",
|
|
58
|
+
"new",
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
43
61
|
});
|
|
44
62
|
describe("relaunchWithSession", () => {
|
|
45
63
|
it("spawns the same executable with the sanitized argv + session id, then exits 0", () => {
|
|
@@ -72,6 +90,38 @@ describe("relaunchWithSession", () => {
|
|
|
72
90
|
expect(errMock).toHaveBeenCalled();
|
|
73
91
|
errMock.mockRestore();
|
|
74
92
|
});
|
|
93
|
+
it("runs beforeSpawn teardown before spawning", () => {
|
|
94
|
+
const calls = [];
|
|
95
|
+
const child = new EventEmitter();
|
|
96
|
+
const spawnMock = vi.fn(() => {
|
|
97
|
+
calls.push("spawn");
|
|
98
|
+
return child;
|
|
99
|
+
});
|
|
100
|
+
relaunchWithSession("sess", {
|
|
101
|
+
argv: ["/bin/bun", "src/index.ts"],
|
|
102
|
+
onExit: () => { },
|
|
103
|
+
spawnFn: spawnMock,
|
|
104
|
+
beforeSpawn: () => calls.push("teardown"),
|
|
105
|
+
});
|
|
106
|
+
expect(calls).toEqual(["teardown", "spawn"]);
|
|
107
|
+
});
|
|
108
|
+
it("supervise: stays alive until the child exits, then propagates the exit code", () => {
|
|
109
|
+
const exitMock = vi.fn();
|
|
110
|
+
const child = new EventEmitter();
|
|
111
|
+
const spawnMock = vi.fn(() => child);
|
|
112
|
+
relaunchWithSession("sess", {
|
|
113
|
+
argv: ["/bin/bun", "src/index.ts"],
|
|
114
|
+
onExit: exitMock,
|
|
115
|
+
spawnFn: spawnMock,
|
|
116
|
+
supervise: true,
|
|
117
|
+
});
|
|
118
|
+
// Does NOT exit on spawn when supervising.
|
|
119
|
+
child.emit("spawn");
|
|
120
|
+
expect(exitMock).not.toHaveBeenCalled();
|
|
121
|
+
// Exits with the child's code when the child finally exits.
|
|
122
|
+
child.emit("exit", 3);
|
|
123
|
+
expect(exitMock).toHaveBeenCalledWith(3);
|
|
124
|
+
});
|
|
75
125
|
it("throws when argv[0] is missing (cannot relaunch without an executable)", () => {
|
|
76
126
|
expect(() => relaunchWithSession("sess", {
|
|
77
127
|
argv: [],
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
export declare function formatTokenCount(tokens: number): string;
|
|
2
|
+
/**
|
|
3
|
+
* Strip invisible/control Unicode characters that leak into terminal output.
|
|
4
|
+
* Keeps \t \n \r (needed for rendering); removes everything else that most
|
|
5
|
+
* terminals cannot display safely: zero-width spaces, soft hyphens, BiDi
|
|
6
|
+
* overrides, C0/C1 controls, BOM, and other default-ignorable code points.
|
|
7
|
+
* Fast-pathed: returns the input unchanged when no match is found.
|
|
8
|
+
*
|
|
9
|
+
* Uses `new RegExp()` to avoid Biome's `noControlCharactersInRegex` lint
|
|
10
|
+
* (control-char escapes in a regex literal are blocked).
|
|
11
|
+
*/
|
|
12
|
+
export declare function stripInvisibleChars(text: string): string;
|
|
2
13
|
export declare function trunc(s: string, n: number): string;
|
|
3
14
|
export declare function truncateLine(s: string, n: number): string;
|
|
4
15
|
export declare function truncateBlock(text: string, maxLines: number): string;
|
|
@@ -5,6 +5,26 @@ export function formatTokenCount(tokens) {
|
|
|
5
5
|
return `${Math.round(tokens / 1_000)}K`;
|
|
6
6
|
return String(tokens);
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Strip invisible/control Unicode characters that leak into terminal output.
|
|
10
|
+
* Keeps \t \n \r (needed for rendering); removes everything else that most
|
|
11
|
+
* terminals cannot display safely: zero-width spaces, soft hyphens, BiDi
|
|
12
|
+
* overrides, C0/C1 controls, BOM, and other default-ignorable code points.
|
|
13
|
+
* Fast-pathed: returns the input unchanged when no match is found.
|
|
14
|
+
*
|
|
15
|
+
* Uses `new RegExp()` to avoid Biome's `noControlCharactersInRegex` lint
|
|
16
|
+
* (control-char escapes in a regex literal are blocked).
|
|
17
|
+
*/
|
|
18
|
+
export function stripInvisibleChars(text) {
|
|
19
|
+
if (!text)
|
|
20
|
+
return text;
|
|
21
|
+
// Constructed via new RegExp to bypass Biome lint for control-char patterns.
|
|
22
|
+
// U+034F (Combining Grapheme Joiner) is separated via alternation per
|
|
23
|
+
// Biome's noMisleadingCharacterClass rule.
|
|
24
|
+
const re = new RegExp("[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u00AD\u061C\u070F\u200B-\u200F\u2028-\u202F\u2060-\u2069\uFEFF\uFFF9-\uFFFB]|" +
|
|
25
|
+
"\u034F|\u{1BCA0}-\u{1BCA3}|\u{E0001}|\u{E0020}-\u{E007F}", "gu");
|
|
26
|
+
return re.test(text) ? text.replace(re, "") : text;
|
|
27
|
+
}
|
|
8
28
|
export function trunc(s, n) {
|
|
9
29
|
const str = String(s ?? "");
|
|
10
30
|
return str.length <= n ? str : `${str.slice(0, n)}…`;
|
|
@@ -37,7 +57,7 @@ export function sanitizeContent(raw) {
|
|
|
37
57
|
* answer (e.g. `\frac{a}{b}`) is untouched. Fast-pathed: no work when absent.
|
|
38
58
|
*/
|
|
39
59
|
export function stripStrayModelMacros(text) {
|
|
40
|
-
if (!text
|
|
60
|
+
if (!text?.includes("\\confidence"))
|
|
41
61
|
return text;
|
|
42
62
|
// Trailing form (most common) — also swallow the whitespace/newline before it.
|
|
43
63
|
let out = text.replace(/\s*\\confidence\s*\{[^}]*\}\s*$/i, "");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { stripStrayModelMacros } from "./text.js";
|
|
2
|
+
import { stripInvisibleChars, stripStrayModelMacros } from "./text.js";
|
|
3
3
|
describe("stripStrayModelMacros", () => {
|
|
4
4
|
it("strips a trailing \\confidence{NN} macro and the blank line before it", () => {
|
|
5
5
|
const input = "Root cause: parseInt radix.\n\nFix: use radix 10.\n\n\\confidence{85}";
|
|
@@ -20,4 +20,28 @@ describe("stripStrayModelMacros", () => {
|
|
|
20
20
|
expect(stripStrayModelMacros("")).toBe("");
|
|
21
21
|
});
|
|
22
22
|
});
|
|
23
|
+
describe("stripInvisibleChars", () => {
|
|
24
|
+
it("strips zero-width spaces (U+200B)", () => {
|
|
25
|
+
expect(stripInvisibleChars("hello\u200Bworld")).toBe("helloworld");
|
|
26
|
+
});
|
|
27
|
+
it("strips soft hyphen (U+00AD)", () => {
|
|
28
|
+
expect(stripInvisibleChars("hy\u00ADphen")).toBe("hyphen");
|
|
29
|
+
});
|
|
30
|
+
it("strips BOM (U+FEFF)", () => {
|
|
31
|
+
expect(stripInvisibleChars("\uFEFFHello")).toBe("Hello");
|
|
32
|
+
});
|
|
33
|
+
it("strips BiDi overrides (U+202E)", () => {
|
|
34
|
+
expect(stripInvisibleChars("abc\u202Edef")).toBe("abcdef");
|
|
35
|
+
});
|
|
36
|
+
it("strips C0 controls except \\t \\n \\r", () => {
|
|
37
|
+
expect(stripInvisibleChars("a\u0001b\rc\td")).toBe("ab\rc\td");
|
|
38
|
+
});
|
|
39
|
+
it("keeps normal text unchanged (fast path)", () => {
|
|
40
|
+
const normal = "Hello world, how are you?";
|
|
41
|
+
expect(stripInvisibleChars(normal)).toBe(normal);
|
|
42
|
+
});
|
|
43
|
+
it("handles empty string", () => {
|
|
44
|
+
expect(stripInvisibleChars("")).toBe("");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
23
47
|
//# sourceMappingURL=text.test.js.map
|
|
@@ -27,7 +27,9 @@ export function toolArgs(tc) {
|
|
|
27
27
|
try {
|
|
28
28
|
const a = JSON.parse(tc.function.arguments);
|
|
29
29
|
if (tc.function.name === "bash")
|
|
30
|
-
return String(a.command || "")
|
|
30
|
+
return String(a.command || "")
|
|
31
|
+
.replace(/\n/g, " ")
|
|
32
|
+
.trim();
|
|
31
33
|
if (tc.function.name === "read_file" || tc.function.name === "write_file" || tc.function.name === "edit_file")
|
|
32
34
|
return String(a.file_path || a.path || "");
|
|
33
35
|
if (describeMcpFsTool(tc.function.name)) {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { promises as fs } from "node:fs";
|
|
15
15
|
import * as os from "node:os";
|
|
16
16
|
import * as path from "node:path";
|
|
17
|
-
import { projectCostUSD } from "./estimator.js";
|
|
17
|
+
import { projectCostUSD, sanitizeInputTokens } from "./estimator.js";
|
|
18
18
|
function muonroiHome(homeOverride) {
|
|
19
19
|
return homeOverride ?? process.env.MUONROI_CLI_HOME ?? path.join(os.homedir(), ".muonroi-cli");
|
|
20
20
|
}
|
|
@@ -57,7 +57,7 @@ export async function meteredGenerate(args, fn) {
|
|
|
57
57
|
const startedAt = Date.now();
|
|
58
58
|
try {
|
|
59
59
|
const { result, actualInputTokens, actualOutputTokens, cachedInputTokens, stepCount } = await fn();
|
|
60
|
-
const inTok = actualInputTokens
|
|
60
|
+
const inTok = sanitizeInputTokens(actualInputTokens, estIn);
|
|
61
61
|
const outTok = actualOutputTokens ?? Math.max(1, Math.ceil(estOut / 4));
|
|
62
62
|
const usd = projectCostUSD(args.provider, args.model, inTok, outTok);
|
|
63
63
|
await appendCostLog({
|
|
@@ -10,6 +10,20 @@
|
|
|
10
10
|
* Acceptable for cap projection only -- not for billing reconciliation.
|
|
11
11
|
*/
|
|
12
12
|
export declare function estimateTokensFromChars(chars: number): number;
|
|
13
|
+
/**
|
|
14
|
+
* Sanitize an actual input-token count by comparing it to the estimated
|
|
15
|
+
* (chars/4) count. Some providers (SiliconFlow, possibly others) return
|
|
16
|
+
* implausibly low `prompt_tokens` (e.g. 10) regardless of actual prompt size,
|
|
17
|
+
* which under-reports cost and inflates apparent cache-hit ratios.
|
|
18
|
+
*
|
|
19
|
+
* Rules:
|
|
20
|
+
* - When `actual === undefined`: return `estimated` (no data = use estimate).
|
|
21
|
+
* - When `actual === 0`: return 0 (preserve mock / failed-call semantics).
|
|
22
|
+
* - When `estimated > 0` and `actual < estimated * 0.1`: the value is likely
|
|
23
|
+
* bogus — return `estimated` so cost projections stay accurate.
|
|
24
|
+
* - Otherwise: return `actual` as-is.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sanitizeInputTokens(actual: number | undefined, estimated: number): number;
|
|
13
27
|
/**
|
|
14
28
|
* Project the USD cost of a request given token estimates and the static pricing table.
|
|
15
29
|
* Returns 0 for unknown provider/model -- caller decides how to handle (ledger treats as 0 risk).
|
|
@@ -13,6 +13,28 @@ import { lookupPricing } from "../providers/pricing.js";
|
|
|
13
13
|
export function estimateTokensFromChars(chars) {
|
|
14
14
|
return Math.ceil(chars / 4);
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize an actual input-token count by comparing it to the estimated
|
|
18
|
+
* (chars/4) count. Some providers (SiliconFlow, possibly others) return
|
|
19
|
+
* implausibly low `prompt_tokens` (e.g. 10) regardless of actual prompt size,
|
|
20
|
+
* which under-reports cost and inflates apparent cache-hit ratios.
|
|
21
|
+
*
|
|
22
|
+
* Rules:
|
|
23
|
+
* - When `actual === undefined`: return `estimated` (no data = use estimate).
|
|
24
|
+
* - When `actual === 0`: return 0 (preserve mock / failed-call semantics).
|
|
25
|
+
* - When `estimated > 0` and `actual < estimated * 0.1`: the value is likely
|
|
26
|
+
* bogus — return `estimated` so cost projections stay accurate.
|
|
27
|
+
* - Otherwise: return `actual` as-is.
|
|
28
|
+
*/
|
|
29
|
+
export function sanitizeInputTokens(actual, estimated) {
|
|
30
|
+
if (actual === undefined)
|
|
31
|
+
return estimated;
|
|
32
|
+
if (actual === 0)
|
|
33
|
+
return 0;
|
|
34
|
+
if (estimated > 0 && actual < estimated * 0.1)
|
|
35
|
+
return estimated;
|
|
36
|
+
return actual;
|
|
37
|
+
}
|
|
16
38
|
/**
|
|
17
39
|
* Project the USD cost of a request given token estimates and the static pricing table.
|
|
18
40
|
* Returns 0 for unknown provider/model -- caller decides how to handle (ledger treats as 0 risk).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/utils/__tests__/compaction-caps.test.ts
|
|
3
|
+
*
|
|
4
|
+
* G4 regression guard for the token-thrift compaction caps.
|
|
5
|
+
*
|
|
6
|
+
* The compaction caps themselves are wired correctly (cost-leak-b3/b4 specs
|
|
7
|
+
* cover the algorithm; the *-tui specs cover the live wiring). The hole this
|
|
8
|
+
* file closes is DRIFT: nothing pinned the PRODUCTION DEFAULT knob values, and
|
|
9
|
+
* the fast cost-leak specs drive `compactSubAgentMessages` with HARDCODED
|
|
10
|
+
* options (thresholdChars: 60_000, keepLastTurns: 1) that are decoupled from
|
|
11
|
+
* the real getters. So a regression that widens a default (e.g. reverting the
|
|
12
|
+
* Phase C5 40_000 back to 200_000, or fat-fingering a clamp range) would sail
|
|
13
|
+
* past the entire fast suite and only be caught by the slow ~30-120s TUI specs.
|
|
14
|
+
*
|
|
15
|
+
* Part 1 pins each getter's documented contract (default / clamp / honor).
|
|
16
|
+
* Part 2 feeds the REAL getter outputs into the compactor over a synthetic
|
|
17
|
+
* load sized BETWEEN the default threshold and a plausible widened drift, so a
|
|
18
|
+
* default-value regression fails behaviourally too — not just at the value
|
|
19
|
+
* assertion.
|
|
20
|
+
*/
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/utils/__tests__/compaction-caps.test.ts
|
|
3
|
+
*
|
|
4
|
+
* G4 regression guard for the token-thrift compaction caps.
|
|
5
|
+
*
|
|
6
|
+
* The compaction caps themselves are wired correctly (cost-leak-b3/b4 specs
|
|
7
|
+
* cover the algorithm; the *-tui specs cover the live wiring). The hole this
|
|
8
|
+
* file closes is DRIFT: nothing pinned the PRODUCTION DEFAULT knob values, and
|
|
9
|
+
* the fast cost-leak specs drive `compactSubAgentMessages` with HARDCODED
|
|
10
|
+
* options (thresholdChars: 60_000, keepLastTurns: 1) that are decoupled from
|
|
11
|
+
* the real getters. So a regression that widens a default (e.g. reverting the
|
|
12
|
+
* Phase C5 40_000 back to 200_000, or fat-fingering a clamp range) would sail
|
|
13
|
+
* past the entire fast suite and only be caught by the slow ~30-120s TUI specs.
|
|
14
|
+
*
|
|
15
|
+
* Part 1 pins each getter's documented contract (default / clamp / honor).
|
|
16
|
+
* Part 2 feeds the REAL getter outputs into the compactor over a synthetic
|
|
17
|
+
* load sized BETWEEN the default threshold and a plausible widened drift, so a
|
|
18
|
+
* default-value regression fails behaviourally too — not just at the value
|
|
19
|
+
* assertion.
|
|
20
|
+
*/
|
|
21
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
22
|
+
import { compactSubAgentMessages, cumulativeMessageChars } from "../../orchestrator/subagent-compactor.js";
|
|
23
|
+
import { getSubAgentBudgetChars, getSubAgentCompactKeepLast, getSubAgentCompactThresholdChars, getTopLevelCompactKeepLast, getTopLevelCompactThresholdChars, getTopLevelToolBudgetChars, loadUserSettings, } from "../settings.js";
|
|
24
|
+
const ENV_KEYS = [
|
|
25
|
+
"MUONROI_SUB_AGENT_BUDGET_CHARS",
|
|
26
|
+
"MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS",
|
|
27
|
+
"MUONROI_SUBAGENT_COMPACT_KEEP_LAST",
|
|
28
|
+
"MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS",
|
|
29
|
+
"MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST",
|
|
30
|
+
"MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS",
|
|
31
|
+
];
|
|
32
|
+
const saved = {};
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
for (const k of ENV_KEYS) {
|
|
35
|
+
saved[k] = process.env[k];
|
|
36
|
+
delete process.env[k];
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
for (const k of ENV_KEYS) {
|
|
41
|
+
if (saved[k] === undefined)
|
|
42
|
+
delete process.env[k];
|
|
43
|
+
else
|
|
44
|
+
process.env[k] = saved[k];
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
describe("compaction cap getters — production contract (G4 drift guard)", () => {
|
|
48
|
+
describe("compact thresholds & keepLast (Phase C5 defaults — no user-settings layer)", () => {
|
|
49
|
+
it("return the documented defaults when env is unset", () => {
|
|
50
|
+
expect(getSubAgentCompactThresholdChars()).toBe(40_000);
|
|
51
|
+
expect(getSubAgentCompactKeepLast()).toBe(3);
|
|
52
|
+
expect(getTopLevelCompactThresholdChars()).toBe(100_000);
|
|
53
|
+
expect(getTopLevelCompactKeepLast()).toBe(5);
|
|
54
|
+
});
|
|
55
|
+
it("clamp out-of-range env back to the default", () => {
|
|
56
|
+
process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "999999999"; // > 500_000
|
|
57
|
+
expect(getSubAgentCompactThresholdChars()).toBe(40_000);
|
|
58
|
+
process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "1000"; // < 20_000
|
|
59
|
+
expect(getSubAgentCompactThresholdChars()).toBe(40_000);
|
|
60
|
+
process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "0"; // < 1
|
|
61
|
+
expect(getSubAgentCompactKeepLast()).toBe(3);
|
|
62
|
+
process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "99"; // > 20
|
|
63
|
+
expect(getSubAgentCompactKeepLast()).toBe(3);
|
|
64
|
+
process.env.MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS = "10000"; // < 50_000
|
|
65
|
+
expect(getTopLevelCompactThresholdChars()).toBe(100_000);
|
|
66
|
+
process.env.MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST = "99"; // > 30
|
|
67
|
+
expect(getTopLevelCompactKeepLast()).toBe(5);
|
|
68
|
+
});
|
|
69
|
+
it("honor valid in-range env overrides", () => {
|
|
70
|
+
process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "60000";
|
|
71
|
+
expect(getSubAgentCompactThresholdChars()).toBe(60_000);
|
|
72
|
+
process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "5";
|
|
73
|
+
expect(getSubAgentCompactKeepLast()).toBe(5);
|
|
74
|
+
process.env.MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS = "250000";
|
|
75
|
+
expect(getTopLevelCompactThresholdChars()).toBe(250_000);
|
|
76
|
+
process.env.MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST = "8";
|
|
77
|
+
expect(getTopLevelCompactKeepLast()).toBe(8);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("budget getters (env override → user-settings → default)", () => {
|
|
81
|
+
it("getSubAgentBudgetChars: default 120_000 when env unset and no user override", () => {
|
|
82
|
+
// Deterministic on a clean checkout / CI (no user-settings.json). When a
|
|
83
|
+
// dev machine HAS an explicit override the getter must honor it instead —
|
|
84
|
+
// the default-drift case is what CI guards.
|
|
85
|
+
const override = loadUserSettings().subAgentBudgetChars;
|
|
86
|
+
if (typeof override === "number" && override >= 20_000 && override <= 600_000) {
|
|
87
|
+
expect(getSubAgentBudgetChars()).toBe(Math.floor(override));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
expect(getSubAgentBudgetChars()).toBe(120_000);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
it("getTopLevelToolBudgetChars: default 200_000 when env unset and no user override", () => {
|
|
94
|
+
const override = loadUserSettings().topLevelToolBudgetChars;
|
|
95
|
+
if (typeof override === "number" && override >= 50_000 && override <= 1_500_000) {
|
|
96
|
+
expect(getTopLevelToolBudgetChars()).toBe(Math.floor(override));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
expect(getTopLevelToolBudgetChars()).toBe(200_000);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
it("honor valid in-range env overrides (env short-circuits user-settings)", () => {
|
|
103
|
+
process.env.MUONROI_SUB_AGENT_BUDGET_CHARS = "300000";
|
|
104
|
+
expect(getSubAgentBudgetChars()).toBe(300_000);
|
|
105
|
+
process.env.MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS = "800000";
|
|
106
|
+
expect(getTopLevelToolBudgetChars()).toBe(800_000);
|
|
107
|
+
});
|
|
108
|
+
it("reject out-of-range env (result stays within the documented range)", () => {
|
|
109
|
+
process.env.MUONROI_SUB_AGENT_BUDGET_CHARS = "999999999"; // > 600_000
|
|
110
|
+
const sub = getSubAgentBudgetChars();
|
|
111
|
+
expect(sub).not.toBe(999_999_999);
|
|
112
|
+
expect(sub).toBeGreaterThanOrEqual(20_000);
|
|
113
|
+
expect(sub).toBeLessThanOrEqual(600_000);
|
|
114
|
+
process.env.MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS = "1000"; // < 50_000
|
|
115
|
+
const top = getTopLevelToolBudgetChars();
|
|
116
|
+
expect(top).not.toBe(1000);
|
|
117
|
+
expect(top).toBeGreaterThanOrEqual(50_000);
|
|
118
|
+
expect(top).toBeLessThanOrEqual(1_500_000);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
/**
|
|
123
|
+
* Build N tool turns (assistant tool-call + tool result), each result
|
|
124
|
+
* `resultChars` long, using a tool name that is NOT high-value (so the
|
|
125
|
+
* compactor is allowed to elide it). Mirrors the cost-leak-b3 message shape.
|
|
126
|
+
*/
|
|
127
|
+
function buildToolTurns(n, resultChars) {
|
|
128
|
+
const msgs = [
|
|
129
|
+
{ role: "system", content: "You are the Explore sub-agent." },
|
|
130
|
+
{ role: "user", content: "trace auth wiring" },
|
|
131
|
+
];
|
|
132
|
+
for (let i = 0; i < n; i++) {
|
|
133
|
+
const id = `c${i}`;
|
|
134
|
+
msgs.push({
|
|
135
|
+
role: "assistant",
|
|
136
|
+
content: [{ type: "tool-call", toolCallId: id, toolName: "fake_read", input: { path: `/tmp/${id}.txt` } }],
|
|
137
|
+
});
|
|
138
|
+
msgs.push({
|
|
139
|
+
role: "tool",
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: "tool-result",
|
|
143
|
+
toolCallId: id,
|
|
144
|
+
toolName: "fake_read",
|
|
145
|
+
output: { type: "text", value: "x".repeat(resultChars) },
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return msgs;
|
|
151
|
+
}
|
|
152
|
+
describe("compaction fires with the REAL default getter values (G4 behavioural guard)", () => {
|
|
153
|
+
// Load sized BETWEEN the default thresholds (40K sub / 100K top) and a
|
|
154
|
+
// plausible widened drift (200K). So: with the real defaults it compacts;
|
|
155
|
+
// if a default were reverted to 200_000 the load would fall below threshold
|
|
156
|
+
// and these assertions would fail — catching drift behaviourally.
|
|
157
|
+
it("sub-agent: default threshold (40K) elides older tool results on a ~120K load", () => {
|
|
158
|
+
const messages = buildToolTurns(6, 20_000); // ~120K cumulative, 6 tool turns
|
|
159
|
+
const cumulative = cumulativeMessageChars(messages);
|
|
160
|
+
expect(cumulative).toBeGreaterThan(40_000);
|
|
161
|
+
expect(cumulative).toBeLessThan(200_000);
|
|
162
|
+
const compacted = compactSubAgentMessages(messages, {
|
|
163
|
+
thresholdChars: getSubAgentCompactThresholdChars(),
|
|
164
|
+
keepLastTurns: getSubAgentCompactKeepLast(),
|
|
165
|
+
});
|
|
166
|
+
expect(compacted).not.toBe(messages); // identity differs → compaction happened
|
|
167
|
+
const before = JSON.stringify(messages).length;
|
|
168
|
+
const after = JSON.stringify(compacted).length;
|
|
169
|
+
expect(after).toBeLessThan(before);
|
|
170
|
+
expect(JSON.stringify(compacted)).toContain("elided by sub-agent compactor");
|
|
171
|
+
});
|
|
172
|
+
it("top-level: default threshold (100K) elides older tool results on a ~160K load", () => {
|
|
173
|
+
const messages = buildToolTurns(8, 20_000); // ~160K cumulative, 8 tool turns
|
|
174
|
+
const cumulative = cumulativeMessageChars(messages);
|
|
175
|
+
expect(cumulative).toBeGreaterThan(100_000);
|
|
176
|
+
expect(cumulative).toBeLessThan(200_000);
|
|
177
|
+
const compacted = compactSubAgentMessages(messages, {
|
|
178
|
+
thresholdChars: getTopLevelCompactThresholdChars(),
|
|
179
|
+
keepLastTurns: getTopLevelCompactKeepLast(),
|
|
180
|
+
label: "top-level",
|
|
181
|
+
});
|
|
182
|
+
expect(compacted).not.toBe(messages);
|
|
183
|
+
expect(JSON.stringify(compacted).length).toBeLessThan(JSON.stringify(messages).length);
|
|
184
|
+
expect(JSON.stringify(compacted)).toContain("elided by top-level compactor");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
//# sourceMappingURL=compaction-caps.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* withDeadlineRace abort-grace: a provider that ignores its abortSignal mid-call
|
|
3
|
+
* must not keep the caller blocked until the (minutes-long) wall-clock deadline.
|
|
4
|
+
* When the user-abort signal fires, the race rejects within a short grace window.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { withDeadlineRace } from "../llm-deadline.js";
|
|
8
|
+
describe("withDeadlineRace — abort grace", () => {
|
|
9
|
+
it("rejects shortly after abort even when fn() never settles", async () => {
|
|
10
|
+
const ac = new AbortController();
|
|
11
|
+
const neverSettles = () => new Promise(() => { }); // simulates a stalled provider
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
setTimeout(() => ac.abort(new Error("user pressed Esc")), 40);
|
|
14
|
+
await expect(withDeadlineRace(neverSettles, 60_000, "test", ac.signal, 80)).rejects.toThrow(/aborted by user/);
|
|
15
|
+
// Must unblock far before the 60s deadline.
|
|
16
|
+
expect(Date.now() - start).toBeLessThan(2_000);
|
|
17
|
+
});
|
|
18
|
+
it("rejects immediately when the signal is already aborted", async () => {
|
|
19
|
+
const ac = new AbortController();
|
|
20
|
+
ac.abort(new Error("already cancelled"));
|
|
21
|
+
const neverSettles = () => new Promise(() => { });
|
|
22
|
+
await expect(withDeadlineRace(neverSettles, 60_000, "test", ac.signal, 50)).rejects.toThrow(/aborted by user/);
|
|
23
|
+
});
|
|
24
|
+
it("returns the fn() result when it settles before any abort", async () => {
|
|
25
|
+
const ac = new AbortController();
|
|
26
|
+
await expect(withDeadlineRace(() => Promise.resolve("ok"), 60_000, "test", ac.signal, 80)).resolves.toBe("ok");
|
|
27
|
+
});
|
|
28
|
+
it("still enforces the wall-clock deadline when no signal is passed (back-compat)", async () => {
|
|
29
|
+
const neverSettles = () => new Promise(() => { });
|
|
30
|
+
await expect(withDeadlineRace(neverSettles, 60, "test")).rejects.toThrow(/deadline/);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=llm-deadline-abort.test.js.map
|
|
@@ -25,24 +25,24 @@ function readWin32() {
|
|
|
25
25
|
try {
|
|
26
26
|
const escaped = tmpFile.replace(/\\/g, "\\\\");
|
|
27
27
|
// Run in STA thread with retry — clipboard can be locked by other processes
|
|
28
|
-
const ps = `
|
|
29
|
-
Add-Type -AssemblyName System.Windows.Forms
|
|
30
|
-
Add-Type -AssemblyName System.Drawing
|
|
31
|
-
$maxRetries = 3
|
|
32
|
-
$img = $null
|
|
33
|
-
for ($i = 0; $i -lt $maxRetries; $i++) {
|
|
34
|
-
try {
|
|
35
|
-
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
36
|
-
if ($img -ne $null) { break }
|
|
37
|
-
} catch {
|
|
38
|
-
# clipboard locked — wait and retry
|
|
39
|
-
}
|
|
40
|
-
Start-Sleep -Milliseconds 100
|
|
41
|
-
}
|
|
42
|
-
if ($img -ne $null) {
|
|
43
|
-
$img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
44
|
-
$img.Dispose()
|
|
45
|
-
}
|
|
28
|
+
const ps = `
|
|
29
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
30
|
+
Add-Type -AssemblyName System.Drawing
|
|
31
|
+
$maxRetries = 3
|
|
32
|
+
$img = $null
|
|
33
|
+
for ($i = 0; $i -lt $maxRetries; $i++) {
|
|
34
|
+
try {
|
|
35
|
+
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
36
|
+
if ($img -ne $null) { break }
|
|
37
|
+
} catch {
|
|
38
|
+
# clipboard locked — wait and retry
|
|
39
|
+
}
|
|
40
|
+
Start-Sleep -Milliseconds 100
|
|
41
|
+
}
|
|
42
|
+
if ($img -ne $null) {
|
|
43
|
+
$img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
44
|
+
$img.Dispose()
|
|
45
|
+
}
|
|
46
46
|
`;
|
|
47
47
|
const result = spawnSync("powershell", ["-NoProfile", "-STA", "-Command", ps], { timeout: 8000 });
|
|
48
48
|
if (result.error || result.status !== 0)
|
|
@@ -81,11 +81,11 @@ function readDarwin() {
|
|
|
81
81
|
const pngpaste = spawnSync("pngpaste", [tmpFile], { timeout: 5000 });
|
|
82
82
|
if (pngpaste.status !== 0) {
|
|
83
83
|
spawnSync("screencapture", ["-c", "-x"]);
|
|
84
|
-
const osascript = `
|
|
85
|
-
set imgData to the clipboard as «class PNGf»
|
|
86
|
-
set f to open for access POSIX file "${tmpFile}" with write permission
|
|
87
|
-
write imgData to f
|
|
88
|
-
close access f
|
|
84
|
+
const osascript = `
|
|
85
|
+
set imgData to the clipboard as «class PNGf»
|
|
86
|
+
set f to open for access POSIX file "${tmpFile}" with write permission
|
|
87
|
+
write imgData to f
|
|
88
|
+
close access f
|
|
89
89
|
`;
|
|
90
90
|
spawnSync("osascript", ["-e", osascript], { timeout: 5000 });
|
|
91
91
|
}
|
|
@@ -41,4 +41,12 @@ export declare function withTimeoutSignal(parent: AbortSignal | undefined, timeo
|
|
|
41
41
|
* still be aborted via `withTimeoutSignal` for cleanup; this layer just ensures
|
|
42
42
|
* the caller is never blocked past the deadline.
|
|
43
43
|
*/
|
|
44
|
-
export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: number, label: string
|
|
44
|
+
export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: number, label: string,
|
|
45
|
+
/**
|
|
46
|
+
* User-abort signal. When it fires, the race rejects within `abortGraceMs`
|
|
47
|
+
* even if `fn()` hasn't settled — so a provider that ignores its abortSignal
|
|
48
|
+
* mid-call (observed with DeepSeek/grok socket stalls) can't keep the caller
|
|
49
|
+
* (and, for council, the locked composer) blocked until the full `deadlineMs`.
|
|
50
|
+
* The short grace lets the normal fetch-level abort win first when it's quick.
|
|
51
|
+
*/
|
|
52
|
+
abortSignal?: AbortSignal, abortGraceMs?: number): Promise<T>;
|