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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import {
|
|
5
|
+
import { AGENT_TOOL, createStandaloneAgent } from '../src/standalone/agent-tool.mjs';
|
|
6
6
|
import { executePatchTool } from '../src/runtime/agent/orchestrator/tools/patch.mjs';
|
|
7
7
|
import { executeBuiltinTool } from '../src/runtime/agent/orchestrator/tools/builtin.mjs';
|
|
8
8
|
import { initProviders } from '../src/runtime/agent/orchestrator/providers/registry.mjs';
|
|
@@ -28,27 +28,19 @@ function sleep(ms) {
|
|
|
28
28
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function
|
|
32
|
-
return String(text).match(/
|
|
31
|
+
function taskId(text) {
|
|
32
|
+
return String(text).match(/agent task: (\S+)/)?.[1] || null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
async function main() {
|
|
36
|
-
const leadToolRules = readFileSync('src/rules/lead/
|
|
37
|
-
const workflowRules = readFileSync('src/
|
|
38
|
-
assert(/Use `
|
|
39
|
-
assert(/
|
|
40
|
-
assert(/
|
|
41
|
-
assert(/Prefer async by default/i.test(BRIDGE_TOOL.description || '') && /distinct tags/i.test(BRIDGE_TOOL.description || '') && /completion notification/i.test(BRIDGE_TOOL.description || ''), 'bridge tool description must expose async parallel tags');
|
|
36
|
+
const leadToolRules = readFileSync('src/rules/lead/lead-tool.md', 'utf8');
|
|
37
|
+
const workflowRules = readFileSync('src/workflows/default/WORKFLOW.md', 'utf8');
|
|
38
|
+
assert(/Use `agent` for scoped implementation/i.test(leadToolRules), 'lead rules must direct scoped work to agents');
|
|
39
|
+
assert(/delegat(?:es|ing) them concurrently/i.test(workflowRules), 'workflow rules must keep independent work parallel');
|
|
40
|
+
assert(/always start background tasks/i.test(AGENT_TOOL.description || '') && /distinct tags/i.test(AGENT_TOOL.description || '') && /completion notification/i.test(AGENT_TOOL.description || ''), 'agent tool description must expose async parallel tags');
|
|
42
41
|
|
|
43
42
|
mkdirSync(dataDir, { recursive: true });
|
|
44
43
|
await initProviders({ 'openai-oauth': { enabled: true } });
|
|
45
|
-
writeFileSync(join(dataDir, 'user-workflow.json'), JSON.stringify({
|
|
46
|
-
roles: [
|
|
47
|
-
{ name: 'worker', preset: 'fake-worker', permission: 'full', maxLoopIterations: 3, idleTimeoutMs: 5000 },
|
|
48
|
-
{ name: 'reviewer', preset: 'fake-reviewer', permission: 'read' },
|
|
49
|
-
{ name: 'debugger', preset: 'fake-debugger', permission: 'read' },
|
|
50
|
-
],
|
|
51
|
-
}));
|
|
52
44
|
writeFileSync(join(root, 'feature.txt'), 'alpha\nbeta\n', 'utf8');
|
|
53
45
|
writeFileSync(join(root, 'notes.txt'), 'TODO investigate timeout\n', 'utf8');
|
|
54
46
|
|
|
@@ -56,6 +48,11 @@ async function main() {
|
|
|
56
48
|
loadConfig() {
|
|
57
49
|
return {
|
|
58
50
|
providers: { 'openai-oauth': { enabled: true } },
|
|
51
|
+
agents: {
|
|
52
|
+
worker: { provider: 'openai-oauth', model: 'gpt-5.5', effort: 'medium', fast: true },
|
|
53
|
+
reviewer: { provider: 'openai-oauth', model: 'gpt-5.5', effort: 'low' },
|
|
54
|
+
debugger: { provider: 'openai-oauth', model: 'gpt-5.5', effort: 'low' },
|
|
55
|
+
},
|
|
59
56
|
presets: [
|
|
60
57
|
{ id: 'fake-worker', name: 'Fake Worker', provider: 'openai-oauth', model: 'gpt-5.5', tools: 'full', effort: 'medium', fast: true },
|
|
61
58
|
{ id: 'fake-reviewer', name: 'Fake Reviewer', provider: 'openai-oauth', model: 'gpt-5.5', tools: 'readonly', effort: 'low' },
|
|
@@ -64,7 +61,7 @@ async function main() {
|
|
|
64
61
|
};
|
|
65
62
|
},
|
|
66
63
|
resolveRuntimeSpec(preset, ctx) {
|
|
67
|
-
return { lane: '
|
|
64
|
+
return { lane: 'agent', scopeKey: `smoke:${ctx.agentId}`, provider: preset.provider, model: preset.model };
|
|
68
65
|
},
|
|
69
66
|
};
|
|
70
67
|
const reg = { initProviders };
|
|
@@ -130,14 +127,14 @@ async function main() {
|
|
|
130
127
|
getSessionLastProgressAt,
|
|
131
128
|
askSession: fakeAskSession,
|
|
132
129
|
};
|
|
133
|
-
const
|
|
130
|
+
const agentRunner = createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: root, defaultMode: 'async' });
|
|
134
131
|
|
|
135
132
|
async function waitJob(out, pattern, label) {
|
|
136
|
-
const id =
|
|
137
|
-
assert(id, `missing
|
|
133
|
+
const id = taskId(out);
|
|
134
|
+
assert(id, `missing task ID for ${label}: ${out}`);
|
|
138
135
|
let last = '';
|
|
139
136
|
for (let i = 0; i < 40; i += 1) {
|
|
140
|
-
last = await
|
|
137
|
+
last = await agentRunner.execute({ type: 'read', task_id: id }, { invocationSource: 'model-tool', cwd: root });
|
|
141
138
|
if (pattern.test(last)) return last;
|
|
142
139
|
if (/^Error[\s:[]/.test(last) || /status: error/.test(last)) break;
|
|
143
140
|
await sleep(100);
|
|
@@ -145,7 +142,15 @@ async function main() {
|
|
|
145
142
|
throw new Error(`${label} did not finish with ${pattern}: ${last}`);
|
|
146
143
|
}
|
|
147
144
|
|
|
148
|
-
|
|
145
|
+
async function waitAgentTag(tag, label) {
|
|
146
|
+
for (let i = 0; i < 20; i += 1) {
|
|
147
|
+
if (listSessions().some((session) => session?.agentTag === tag && !session.closed)) return;
|
|
148
|
+
await sleep(25);
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`${label} did not register agent tag ${tag}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const spawnOut = await agentRunner.execute({
|
|
149
154
|
type: 'spawn',
|
|
150
155
|
role: 'worker',
|
|
151
156
|
tag: 'impl1',
|
|
@@ -154,22 +159,22 @@ async function main() {
|
|
|
154
159
|
}, { invocationSource: 'model-tool', cwd: root });
|
|
155
160
|
await waitJob(spawnOut, /worker wrote feature/, 'worker write');
|
|
156
161
|
|
|
157
|
-
const sendOut = await
|
|
162
|
+
const sendOut = await agentRunner.execute({
|
|
158
163
|
type: 'send',
|
|
159
164
|
tag: 'impl1',
|
|
160
165
|
message: 'run verification: inspect feature.txt using bash',
|
|
161
166
|
}, { invocationSource: 'model-tool', cwd: root });
|
|
162
|
-
assert(/
|
|
167
|
+
assert(/agent task:/.test(sendOut), `completed worker send should be async task, got ${sendOut}`);
|
|
163
168
|
await waitJob(sendOut, /beta from worker/, 'worker verify');
|
|
164
169
|
|
|
165
|
-
const reviewOut = await
|
|
170
|
+
const reviewOut = await agentRunner.execute({
|
|
166
171
|
type: 'spawn',
|
|
167
172
|
role: 'reviewer',
|
|
168
173
|
tag: 'rev1',
|
|
169
174
|
cwd: root,
|
|
170
175
|
prompt: 'review feature.txt for the worker change',
|
|
171
176
|
}, { invocationSource: 'model-tool', cwd: root });
|
|
172
|
-
const debugOut = await
|
|
177
|
+
const debugOut = await agentRunner.execute({
|
|
173
178
|
type: 'spawn',
|
|
174
179
|
role: 'debugger',
|
|
175
180
|
tag: 'dbg1',
|
|
@@ -180,21 +185,21 @@ async function main() {
|
|
|
180
185
|
await waitJob(debugOut, /debugger TODO/, 'debugger');
|
|
181
186
|
|
|
182
187
|
const parallelSpawns = await Promise.all([
|
|
183
|
-
|
|
188
|
+
agentRunner.execute({
|
|
184
189
|
type: 'spawn',
|
|
185
190
|
role: 'worker',
|
|
186
191
|
tag: 'parWorker',
|
|
187
192
|
cwd: root,
|
|
188
193
|
prompt: 'parallel slow worker task',
|
|
189
194
|
}, { invocationSource: 'model-tool', cwd: root }),
|
|
190
|
-
|
|
195
|
+
agentRunner.execute({
|
|
191
196
|
type: 'spawn',
|
|
192
197
|
role: 'reviewer',
|
|
193
198
|
tag: 'parReviewer',
|
|
194
199
|
cwd: root,
|
|
195
200
|
prompt: 'parallel slow review task',
|
|
196
201
|
}, { invocationSource: 'model-tool', cwd: root }),
|
|
197
|
-
|
|
202
|
+
agentRunner.execute({
|
|
198
203
|
type: 'spawn',
|
|
199
204
|
role: 'debugger',
|
|
200
205
|
tag: 'parDebugger',
|
|
@@ -202,33 +207,34 @@ async function main() {
|
|
|
202
207
|
prompt: 'parallel slow debug task',
|
|
203
208
|
}, { invocationSource: 'model-tool', cwd: root }),
|
|
204
209
|
]);
|
|
205
|
-
assert(parallelSpawns.every((out) => /
|
|
210
|
+
assert(parallelSpawns.every((out) => /agent task:/.test(out)), `parallel spawns must return tasks: ${parallelSpawns.join('\n---\n')}`);
|
|
206
211
|
await Promise.all([
|
|
207
212
|
waitJob(parallelSpawns[0], /ack worker/, 'parallel worker'),
|
|
208
213
|
waitJob(parallelSpawns[1], /reviewer ok/, 'parallel reviewer'),
|
|
209
214
|
waitJob(parallelSpawns[2], /debugger TODO/, 'parallel debugger'),
|
|
210
215
|
]);
|
|
211
|
-
assert(maxActiveAsks >= 2, `
|
|
216
|
+
assert(maxActiveAsks >= 2, `agents did not overlap; maxActiveAsks=${maxActiveAsks}`);
|
|
212
217
|
|
|
213
|
-
const busyOut = await
|
|
218
|
+
const busyOut = await agentRunner.execute({
|
|
214
219
|
type: 'spawn',
|
|
215
220
|
role: 'worker',
|
|
216
221
|
tag: 'busy1',
|
|
217
222
|
cwd: root,
|
|
218
223
|
prompt: 'long busy worker task',
|
|
219
224
|
}, { invocationSource: 'model-tool', cwd: root });
|
|
220
|
-
|
|
225
|
+
await waitAgentTag('busy1', 'busy worker');
|
|
226
|
+
const queued = await agentRunner.execute({
|
|
221
227
|
type: 'send',
|
|
222
228
|
tag: 'busy1',
|
|
223
229
|
message: 'follow-up while still busy',
|
|
224
230
|
}, { invocationSource: 'model-tool', cwd: root });
|
|
225
|
-
assert(/
|
|
231
|
+
assert(/agent message queued/.test(queued), `busy send should queue, got ${queued}`);
|
|
226
232
|
await waitJob(busyOut, /ack worker/, 'busy worker');
|
|
227
233
|
|
|
228
|
-
const missing = await
|
|
229
|
-
assert(/^Error[\s:[]/.test(missing), 'missing
|
|
234
|
+
const missing = await agentRunner.execute({ type: 'read', task_id: 'task_missing_live_smoke' }, { invocationSource: 'model-tool', cwd: root });
|
|
235
|
+
assert(/^Error[\s:[]/.test(missing), 'missing agent task should be Error result');
|
|
230
236
|
assert(readFileSync(join(root, 'feature.txt'), 'utf8').includes('beta from worker'), 'final file missing worker edit');
|
|
231
|
-
|
|
237
|
+
agentRunner.closeAll('live-worker-smoke-end');
|
|
232
238
|
process.stdout.write('live worker smoke passed\n');
|
|
233
239
|
}
|
|
234
240
|
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
function argValue(name, fallback = null) {
|
|
7
|
+
const idx = process.argv.indexOf(name);
|
|
8
|
+
if (idx >= 0 && idx + 1 < process.argv.length) return process.argv[idx + 1];
|
|
9
|
+
const pref = `${name}=`;
|
|
10
|
+
const hit = process.argv.find((arg) => arg.startsWith(pref));
|
|
11
|
+
return hit ? hit.slice(pref.length) : fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function intArg(name, fallback) {
|
|
15
|
+
const n = Number.parseInt(argValue(name, String(fallback)), 10);
|
|
16
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const pathArg = argValue('--path', null);
|
|
20
|
+
const dataDir = argValue('--data-dir', null);
|
|
21
|
+
const sinceArg = argValue('--since', null);
|
|
22
|
+
const kindFilter = argValue('--kind', null);
|
|
23
|
+
const last = intArg('--last', 5000);
|
|
24
|
+
const limit = intArg('--limit', 20);
|
|
25
|
+
const slowMs = intArg('--slow-ms', 3000);
|
|
26
|
+
const jsonMode = process.argv.includes('--json');
|
|
27
|
+
|
|
28
|
+
const mixdogHome = process.env.MIXDOG_HOME || resolve(homedir(), '.mixdog');
|
|
29
|
+
const mixdogDataDir = process.env.MIXDOG_DATA_DIR || resolve(mixdogHome, 'data');
|
|
30
|
+
|
|
31
|
+
function unique(values) {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const value of values) {
|
|
35
|
+
const key = String(value || '');
|
|
36
|
+
if (!key || seen.has(key)) continue;
|
|
37
|
+
seen.add(key);
|
|
38
|
+
out.push(value);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function defaultTraceFiles() {
|
|
44
|
+
if (pathArg) return [resolve(pathArg)];
|
|
45
|
+
const dirs = dataDir
|
|
46
|
+
? [resolve(dataDir)]
|
|
47
|
+
: [resolve(process.cwd(), '.mixdog', 'data'), mixdogDataDir];
|
|
48
|
+
return unique(dirs.flatMap((dir) => [
|
|
49
|
+
resolve(dir, 'history', 'agent-trace.jsonl.1'),
|
|
50
|
+
resolve(dir, 'history', 'agent-trace.jsonl'),
|
|
51
|
+
resolve(dir, 'history', 'agent-trace.jsonl.1'),
|
|
52
|
+
resolve(dir, 'history', 'agent-trace.jsonl'),
|
|
53
|
+
]));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseSince(value) {
|
|
57
|
+
const raw = String(value || '').trim();
|
|
58
|
+
if (!raw) return null;
|
|
59
|
+
if (/^now$/i.test(raw)) return Date.now();
|
|
60
|
+
if (/^\d+$/.test(raw)) {
|
|
61
|
+
const n = Number(raw);
|
|
62
|
+
return n > 10_000_000_000 ? n : n * 1000;
|
|
63
|
+
}
|
|
64
|
+
const rel = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i);
|
|
65
|
+
if (rel) {
|
|
66
|
+
const n = Number(rel[1]);
|
|
67
|
+
const unit = rel[2].toLowerCase();
|
|
68
|
+
const mult = unit === 'ms' ? 1 : unit === 's' ? 1000 : unit === 'm' ? 60_000 : unit === 'h' ? 3_600_000 : 86_400_000;
|
|
69
|
+
return Date.now() - n * mult;
|
|
70
|
+
}
|
|
71
|
+
const parsed = Date.parse(raw);
|
|
72
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readRows(file) {
|
|
76
|
+
if (!existsSync(file)) return [];
|
|
77
|
+
return readFileSync(file, 'utf8')
|
|
78
|
+
.split(/\r?\n/)
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.map((line) => {
|
|
81
|
+
try {
|
|
82
|
+
return { file, ...JSON.parse(line) };
|
|
83
|
+
} catch {
|
|
84
|
+
return { file, kind: 'parse_error', payload: { line: line.slice(0, 300) } };
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function payload(row) {
|
|
90
|
+
return row && row.payload && typeof row.payload === 'object' ? row.payload : {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function field(row, name) {
|
|
94
|
+
if (row && row[name] != null) return row[name];
|
|
95
|
+
const p = payload(row);
|
|
96
|
+
return p[name] != null ? p[name] : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function numberField(row, name) {
|
|
100
|
+
const n = Number(field(row, name));
|
|
101
|
+
return Number.isFinite(n) ? n : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function countBy(rows, fn) {
|
|
105
|
+
const map = new Map();
|
|
106
|
+
for (const row of rows) {
|
|
107
|
+
const key = String(fn(row) ?? '(none)');
|
|
108
|
+
map.set(key, (map.get(key) || 0) + 1);
|
|
109
|
+
}
|
|
110
|
+
return Object.fromEntries([...map.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function values(rows, name) {
|
|
114
|
+
return rows.map((row) => numberField(row, name)).filter((n) => n != null);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function percentile(sorted, p) {
|
|
118
|
+
if (sorted.length === 0) return null;
|
|
119
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
|
|
120
|
+
return sorted[idx];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function stats(nums) {
|
|
124
|
+
const arr = nums.filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
|
|
125
|
+
if (arr.length === 0) return null;
|
|
126
|
+
const sum = arr.reduce((a, b) => a + b, 0);
|
|
127
|
+
return {
|
|
128
|
+
n: arr.length,
|
|
129
|
+
sum,
|
|
130
|
+
avg: Math.round(sum / arr.length),
|
|
131
|
+
p50: percentile(arr, 50),
|
|
132
|
+
p90: percentile(arr, 90),
|
|
133
|
+
p99: percentile(arr, 99),
|
|
134
|
+
max: arr[arr.length - 1],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatStats(s) {
|
|
139
|
+
if (!s) return 'n=0';
|
|
140
|
+
return `n=${s.n} avg=${s.avg}ms p50=${s.p50}ms p90=${s.p90}ms p99=${s.p99}ms max=${s.max}ms`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function short(value, max = 140) {
|
|
144
|
+
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
145
|
+
return text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function timeLabel(ts) {
|
|
149
|
+
const n = Number(ts);
|
|
150
|
+
if (!Number.isFinite(n) || n <= 0) return '-';
|
|
151
|
+
try {
|
|
152
|
+
return new Date(n).toISOString();
|
|
153
|
+
} catch {
|
|
154
|
+
return String(ts);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function topStatsBy(rows, groupName, valueName, minValue = null) {
|
|
159
|
+
const groups = new Map();
|
|
160
|
+
for (const row of rows) {
|
|
161
|
+
const value = numberField(row, valueName);
|
|
162
|
+
if (value == null) continue;
|
|
163
|
+
if (minValue != null && value < minValue) continue;
|
|
164
|
+
const key = String(field(row, groupName) || '(none)');
|
|
165
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
166
|
+
groups.get(key).push(value);
|
|
167
|
+
}
|
|
168
|
+
return [...groups.entries()]
|
|
169
|
+
.map(([key, nums]) => ({ key, ...stats(nums) }))
|
|
170
|
+
.sort((a, b) => b.max - a.max || b.p90 - a.p90 || b.n - a.n);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function topStatsByTotal(rows, groupName, valueName, minValue = null) {
|
|
174
|
+
return topStatsBy(rows, groupName, valueName, minValue)
|
|
175
|
+
.sort((a, b) => b.sum - a.sum || b.max - a.max || b.p90 - a.p90 || b.n - a.n);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function printCounts(label, obj, max = 12) {
|
|
179
|
+
const parts = Object.entries(obj).slice(0, max).map(([k, v]) => `${k}:${v}`);
|
|
180
|
+
console.log(`${label}: ${parts.join(', ') || '(none)'}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const files = defaultTraceFiles();
|
|
184
|
+
const sinceTs = parseSince(sinceArg);
|
|
185
|
+
const allRows = files.flatMap(readRows)
|
|
186
|
+
.filter((row) => sinceTs == null || Number(row.ts || 0) >= sinceTs)
|
|
187
|
+
.filter((row) => !kindFilter || String(row.kind || '') === kindFilter)
|
|
188
|
+
.sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
|
|
189
|
+
const rows = allRows.slice(-last);
|
|
190
|
+
|
|
191
|
+
const kindCounts = countBy(rows, (row) => row.kind || 'unknown');
|
|
192
|
+
const transportRows = rows.filter((row) => row.kind === 'transport');
|
|
193
|
+
const sseRows = rows.filter((row) => row.kind === 'sse');
|
|
194
|
+
const fetchRows = rows.filter((row) => row.kind === 'fetch');
|
|
195
|
+
const toolRows = rows.filter((row) => row.kind === 'tool');
|
|
196
|
+
const toolSlowRows = rows.filter((row) => row.kind === 'tool_slow' || (row.kind === 'tool' && (numberField(row, 'tool_ms') || 0) >= slowMs));
|
|
197
|
+
const cacheSlowRows = rows.filter((row) => row.kind === 'cache_lane_slow');
|
|
198
|
+
const explicitCacheBreakRows = rows.filter((row) => row.kind === 'cache_break');
|
|
199
|
+
const explicitCacheBreakKeys = new Set(explicitCacheBreakRows.map((row) => `${row.session_id || ''}:${row.iteration ?? ''}`));
|
|
200
|
+
const cacheBreakRows = explicitCacheBreakRows
|
|
201
|
+
.concat(transportRows.filter((row) => field(row, 'chain_delta_reason') && !explicitCacheBreakKeys.has(`${row.session_id || ''}:${row.iteration ?? ''}`)));
|
|
202
|
+
const fallbackRows = rows.filter((row) => row.kind === 'transport_fallback');
|
|
203
|
+
|
|
204
|
+
const report = {
|
|
205
|
+
analyzed: rows.length,
|
|
206
|
+
matched: allRows.length,
|
|
207
|
+
since: sinceTs ? new Date(sinceTs).toISOString() : null,
|
|
208
|
+
filters: {
|
|
209
|
+
kind: kindFilter,
|
|
210
|
+
last,
|
|
211
|
+
slow_ms: slowMs,
|
|
212
|
+
},
|
|
213
|
+
sources: files.filter(existsSync),
|
|
214
|
+
kinds: kindCounts,
|
|
215
|
+
transport: {
|
|
216
|
+
modes: countBy(transportRows, (row) => field(row, 'ws_mode') || field(row, 'transport') || '(unknown)'),
|
|
217
|
+
rate_policies: countBy(transportRows.filter((row) => field(row, 'cache_lane_rate_policy')), (row) => field(row, 'cache_lane_rate_policy')),
|
|
218
|
+
delta_reasons: countBy(transportRows.filter((row) => field(row, 'chain_delta_reason')), (row) => field(row, 'chain_delta_reason')),
|
|
219
|
+
cache_lane_rate_wait_ms: stats(values(transportRows, 'cache_lane_rate_wait_ms')),
|
|
220
|
+
cache_lane_rate_reacquire_wait_ms: stats(values(transportRows, 'cache_lane_rate_reacquire_wait_ms')),
|
|
221
|
+
cache_lane_wait_ms: stats(values(transportRows, 'cache_lane_wait_ms')),
|
|
222
|
+
cache_lane_slow: cacheSlowRows.length,
|
|
223
|
+
fallback: countBy(fallbackRows, (row) => field(row, 'reason') || field(row, 'fallback_reason') || '(unknown)'),
|
|
224
|
+
},
|
|
225
|
+
sse: {
|
|
226
|
+
ttft_ms: stats(values(sseRows, 'ttft_ms')),
|
|
227
|
+
sse_parse_ms: stats(values(sseRows, 'sse_parse_ms')),
|
|
228
|
+
},
|
|
229
|
+
fetch: {
|
|
230
|
+
headers_ms: stats(values(fetchRows, 'headers_ms')),
|
|
231
|
+
},
|
|
232
|
+
tools: {
|
|
233
|
+
slow_ms: slowMs,
|
|
234
|
+
by_tool: topStatsBy(toolRows, 'tool_name', 'tool_ms'),
|
|
235
|
+
by_tool_total: topStatsByTotal(toolRows, 'tool_name', 'tool_ms'),
|
|
236
|
+
slow_by_tool: topStatsBy(toolRows, 'tool_name', 'tool_ms', slowMs),
|
|
237
|
+
},
|
|
238
|
+
cache_breaks: {
|
|
239
|
+
count: cacheBreakRows.length,
|
|
240
|
+
reasons: countBy(cacheBreakRows, (row) => field(row, 'reason') || field(row, 'chain_delta_reason') || '(unknown)'),
|
|
241
|
+
},
|
|
242
|
+
samples: {
|
|
243
|
+
slow_tools: toolSlowRows
|
|
244
|
+
.slice()
|
|
245
|
+
.sort((a, b) => Number(numberField(b, 'tool_ms') || 0) - Number(numberField(a, 'tool_ms') || 0))
|
|
246
|
+
.slice(0, limit)
|
|
247
|
+
.map((row) => ({
|
|
248
|
+
ts: timeLabel(row.ts),
|
|
249
|
+
session_id: row.session_id || null,
|
|
250
|
+
iteration: row.iteration ?? null,
|
|
251
|
+
tool_name: field(row, 'tool_name'),
|
|
252
|
+
tool_ms: numberField(row, 'tool_ms'),
|
|
253
|
+
role: field(row, 'role'),
|
|
254
|
+
model: field(row, 'model'),
|
|
255
|
+
args: payload(row).tool_args || row.tool_args || null,
|
|
256
|
+
})),
|
|
257
|
+
cache_lane_slow: cacheSlowRows.slice(-limit).map((row) => ({
|
|
258
|
+
ts: timeLabel(row.ts),
|
|
259
|
+
session_id: row.session_id || null,
|
|
260
|
+
iteration: row.iteration ?? null,
|
|
261
|
+
event: field(row, 'event'),
|
|
262
|
+
provider: field(row, 'provider'),
|
|
263
|
+
model: field(row, 'model'),
|
|
264
|
+
payload: payload(row),
|
|
265
|
+
})),
|
|
266
|
+
cache_breaks: cacheBreakRows.slice(-limit).map((row) => ({
|
|
267
|
+
ts: timeLabel(row.ts),
|
|
268
|
+
session_id: row.session_id || null,
|
|
269
|
+
iteration: row.iteration ?? null,
|
|
270
|
+
reason: field(row, 'reason') || field(row, 'chain_delta_reason'),
|
|
271
|
+
provider: field(row, 'provider'),
|
|
272
|
+
model: field(row, 'model'),
|
|
273
|
+
payload: payload(row),
|
|
274
|
+
})),
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if (jsonMode) {
|
|
279
|
+
console.log(JSON.stringify(report, null, 2));
|
|
280
|
+
process.exit(0);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log(`llm trace: ${rows.length}/${allRows.length} rows analyzed`);
|
|
284
|
+
if (sinceTs) console.log(`since: ${new Date(sinceTs).toISOString()}`);
|
|
285
|
+
if (kindFilter) console.log(`filter: kind=${kindFilter}`);
|
|
286
|
+
console.log(`sources: ${report.sources.join(', ') || '(none)'}`);
|
|
287
|
+
printCounts('kinds', report.kinds);
|
|
288
|
+
printCounts('transport modes', report.transport.modes);
|
|
289
|
+
printCounts('cache lane rate policies', report.transport.rate_policies);
|
|
290
|
+
printCounts('delta/full reasons', report.transport.delta_reasons);
|
|
291
|
+
printCounts('transport fallback', report.transport.fallback);
|
|
292
|
+
console.log(`cache lane rate wait: ${formatStats(report.transport.cache_lane_rate_wait_ms)}`);
|
|
293
|
+
console.log(`cache lane rate reacquire wait: ${formatStats(report.transport.cache_lane_rate_reacquire_wait_ms)}`);
|
|
294
|
+
console.log(`cache lane queue wait: ${formatStats(report.transport.cache_lane_wait_ms)}`);
|
|
295
|
+
console.log(`cache lane slow rows: ${report.transport.cache_lane_slow}`);
|
|
296
|
+
console.log(`ttft: ${formatStats(report.sse.ttft_ms)}`);
|
|
297
|
+
console.log(`sse parse: ${formatStats(report.sse.sse_parse_ms)}`);
|
|
298
|
+
console.log(`fetch headers: ${formatStats(report.fetch.headers_ms)}`);
|
|
299
|
+
console.log(`cache breaks: ${report.cache_breaks.count}`);
|
|
300
|
+
printCounts('cache break reasons', report.cache_breaks.reasons);
|
|
301
|
+
console.log('tools by total time:');
|
|
302
|
+
for (const row of report.tools.by_tool_total.slice(0, 12)) {
|
|
303
|
+
console.log(`- ${row.key}: n=${row.n} total=${Math.round(row.sum)}ms avg=${row.avg}ms p90=${row.p90}ms max=${row.max}ms`);
|
|
304
|
+
}
|
|
305
|
+
if (report.tools.by_tool_total.length === 0) console.log('- (none)');
|
|
306
|
+
console.log('slow tools:');
|
|
307
|
+
for (const row of report.tools.slow_by_tool.slice(0, 12)) {
|
|
308
|
+
console.log(`- ${row.key}: n=${row.n} avg=${row.avg}ms p90=${row.p90}ms max=${row.max}ms`);
|
|
309
|
+
}
|
|
310
|
+
if (report.tools.slow_by_tool.length === 0) console.log('- (none)');
|
|
311
|
+
console.log('slow tool samples:');
|
|
312
|
+
for (const row of report.samples.slow_tools) {
|
|
313
|
+
console.log(`- ${row.ts} ${row.tool_name} ${row.tool_ms}ms role=${row.role || '-'} args=${short(JSON.stringify(row.args || {}), 180)}`);
|
|
314
|
+
}
|
|
315
|
+
if (report.samples.slow_tools.length === 0) console.log('- (none)');
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regression: mergeMetaValue uses jsonb shallow merge (not full replace).
|
|
3
|
+
import test from 'node:test'
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
import { mergeMetaValue } from '../src/runtime/memory/lib/memory.mjs'
|
|
6
|
+
|
|
7
|
+
test('mergeMetaValue issues INSERT ... ON CONFLICT DO UPDATE with jsonb ||', async () => {
|
|
8
|
+
const calls = []
|
|
9
|
+
const db = {
|
|
10
|
+
async query(sql, params) {
|
|
11
|
+
calls.push({ sql, params })
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
await mergeMetaValue(db, 'state.cycle_last_run', { cycle1: 100, cycle2_last_error: '' })
|
|
15
|
+
assert.equal(calls.length, 1)
|
|
16
|
+
assert.match(calls[0].sql, /COALESCE\(meta\.value/)
|
|
17
|
+
assert.match(calls[0].sql, /\|\| EXCLUDED\.value/)
|
|
18
|
+
assert.deepEqual(calls[0].params, ['state.cycle_last_run', '{"cycle1":100,"cycle2_last_error":""}'])
|
|
19
|
+
})
|
|
20
|
+
|
|
@@ -13,14 +13,19 @@ function assert(condition, message) {
|
|
|
13
13
|
if (!condition) throw new Error(message);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
const DEFAULT_REPORT_LABELS = new Set(['바뀐 점', '확인한 것', '남은 리스크/다음 단계', '다음 단계']);
|
|
17
|
+
|
|
18
|
+
function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedLabels = new Set() } = {}) {
|
|
17
19
|
const text = String(value || '').trim();
|
|
18
20
|
assert(text, `${name}: empty output`);
|
|
19
21
|
assert(!/\n\s*\n/.test(text), `${name}: multiple paragraphs are not compact`);
|
|
20
22
|
assert(!/^\s*#{1,6}\s/m.test(text), `${name}: heading found`);
|
|
21
23
|
assert(!/^\s*\d+[.)]\s/m.test(text), `${name}: numbered list found`);
|
|
22
24
|
assert(!/^\s{2,}[-*]\s/m.test(text), `${name}: nested bullet found`);
|
|
23
|
-
|
|
25
|
+
for (const line of text.split(/\r?\n/)) {
|
|
26
|
+
const match = line.match(/^\s*(?:[-*]\s+)?([\p{L}\p{N}][\p{L}\p{N} _/-]{0,30}):\s/u);
|
|
27
|
+
if (match) assert(allowedLabels.has(match[1]), `${name}: disallowed report label found`);
|
|
28
|
+
}
|
|
24
29
|
assert(!/\b(?:tool trace|session metadata|model metadata|searched-path list)\b/i.test(text), `${name}: tool/meta trace found`);
|
|
25
30
|
assert(!/\b(?:Mapped|Searched|Read|Called) for \d/i.test(text), `${name}: timing/tool status found`);
|
|
26
31
|
assert(!/^(?:네|예|맞아요|좋아요|알겠습니다|Sure|Okay|Understood)[,.\s]/i.test(text), `${name}: acknowledgment preface found`);
|
|
@@ -43,15 +48,42 @@ function assertCleanOutput(name, value, { maxLines = 2, maxBullets = 3 } = {}) {
|
|
|
43
48
|
const defaultStyle = readFileSync(join(root, 'src', 'output-styles', 'default.md'), 'utf8');
|
|
44
49
|
for (const required of [
|
|
45
50
|
'name: default',
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
+
'Concise engineering summaries',
|
|
52
|
+
'Mixdog default — concise engineering summaries',
|
|
53
|
+
'Use labels such as',
|
|
54
|
+
'`바뀐 점`, `확인한 것`,',
|
|
55
|
+
'Synthesize agent or retrieval results',
|
|
56
|
+
'Do not hide blockers',
|
|
51
57
|
]) {
|
|
52
58
|
assert(defaultStyle.includes(required), `default.md missing: ${required}`);
|
|
53
59
|
}
|
|
54
60
|
assert(!defaultStyle.includes('Claude Code-compact'), 'default.md must not reference the old compact style name');
|
|
61
|
+
assert(!defaultStyle.includes('Hard cap user-visible replies'), 'default.md must not hard-cap replies to two short sentences');
|
|
62
|
+
assert(!defaultStyle.includes('Be short and direct.'), 'default.md must not keep the old generic concise preset');
|
|
63
|
+
assert(!defaultStyle.includes('Practical concise — outcome-first'), 'default.md must not use the simple preset body');
|
|
64
|
+
|
|
65
|
+
const simpleStyle = readFileSync(join(root, 'src', 'output-styles', 'simple.md'), 'utf8');
|
|
66
|
+
for (const required of [
|
|
67
|
+
'name: simple',
|
|
68
|
+
'Outcome-first concise handoffs for coding work',
|
|
69
|
+
'Practical concise — outcome-first handoffs',
|
|
70
|
+
'file_path:line_number',
|
|
71
|
+
'controlled detail',
|
|
72
|
+
'Synthesize agent or retrieval results',
|
|
73
|
+
'Do not hide blockers',
|
|
74
|
+
'if verification was',
|
|
75
|
+
]) {
|
|
76
|
+
assert(simpleStyle.includes(required), `simple.md missing: ${required}`);
|
|
77
|
+
}
|
|
78
|
+
assert(!simpleStyle.includes('Mixdog default — concise engineering summaries'), 'simple.md must not duplicate default preset body');
|
|
79
|
+
for (const [name, style] of Object.entries({
|
|
80
|
+
default: defaultStyle,
|
|
81
|
+
simple: simpleStyle,
|
|
82
|
+
extreme: readFileSync(join(root, 'src', 'output-styles', 'extreme-simple.md'), 'utf8'),
|
|
83
|
+
})) {
|
|
84
|
+
assert(!style.includes('pre-tool preamble'), `${name} output style must not own language preamble rules`);
|
|
85
|
+
assert(!style.includes('selected/default user language'), `${name} output style must not duplicate profile language rules`);
|
|
86
|
+
}
|
|
55
87
|
|
|
56
88
|
const dataDir = mkdtempSync(join(tmpdir(), 'mixdog-output-style-smoke-'));
|
|
57
89
|
try {
|
|
@@ -62,23 +94,31 @@ try {
|
|
|
62
94
|
assert(outputStyleIndex > leadRules.lastIndexOf('# User Workflow'), 'Lead output style must be injected after workflow/user context');
|
|
63
95
|
|
|
64
96
|
mkdirSync(join(dataDir, 'output-styles'), { recursive: true });
|
|
65
|
-
writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({
|
|
97
|
+
writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({
|
|
98
|
+
agent: { profile: { title: '재영님', language: 'system' } },
|
|
99
|
+
outputStyle: 'custom-smoke',
|
|
100
|
+
}));
|
|
66
101
|
writeFileSync(join(dataDir, 'output-styles', 'custom-smoke.md'), '---\nname: custom-smoke\n---\n\n# Custom Output Style\n\ncustom smoke style\n');
|
|
67
102
|
const customRules = rulesBuilder.buildInjectionContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
|
|
68
103
|
assert(customRules.includes('# Custom Output Style'), 'configured outputStyle must select custom style');
|
|
69
|
-
assert(!customRules.includes('
|
|
104
|
+
assert(!customRules.includes('Mixdog default — concise engineering summaries'), 'custom outputStyle should not append default style');
|
|
105
|
+
const profileMeta = rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
|
|
106
|
+
assert(profileMeta.includes('Use "재영님" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
|
|
107
|
+
assert(profileMeta.includes('Do not repeat it in routine progress updates or pre-tool preambles'), 'profile title must not encourage title in preambles');
|
|
108
|
+
assert(/Default user-facing response language from system locale/.test(profileMeta), 'system profile language must resolve from system locale');
|
|
109
|
+
assert(profileMeta.includes('including pre-tool preambles'), 'profile language must cover pre-tool preambles');
|
|
70
110
|
} finally {
|
|
71
111
|
rmSync(dataDir, { recursive: true, force: true });
|
|
72
112
|
}
|
|
73
113
|
|
|
74
114
|
const goodOutputs = {
|
|
75
|
-
explanation: '기본 출력은 한
|
|
76
|
-
implementation: 'default
|
|
77
|
-
crowded: '- default
|
|
115
|
+
explanation: '기본 출력은 결과 한 줄과 근거 한 가지로 끝내고, 최종 보고만 짧은 bullet 라벨을 씁니다.',
|
|
116
|
+
implementation: '- 바뀐 점: `src/output-styles/default.md`에 이전 simple 프리셋을 반영했습니다.\n- 확인한 것: `node scripts/output-style-smoke.mjs`를 실행했습니다.',
|
|
117
|
+
crowded: '- 바뀐 점: default는 요약 라벨 보고, simple은 handoff용 controlled detail을 씁니다.\n- 확인한 것: smoke 예시가 compact guardrail을 유지합니다.',
|
|
78
118
|
blocker: '`src/output-styles/default.md`를 찾을 수 없어 변경이 막혔습니다.',
|
|
79
|
-
semicolon: '`default.md`를
|
|
119
|
+
semicolon: '`default.md`를 갱신했고; 출력 스모크가 예시 응답 모양을 검증합니다.',
|
|
80
120
|
};
|
|
81
|
-
for (const [name, output] of Object.entries(goodOutputs)) assertCleanOutput(name, output);
|
|
121
|
+
for (const [name, output] of Object.entries(goodOutputs)) assertCleanOutput(name, output, { allowedLabels: DEFAULT_REPORT_LABELS });
|
|
82
122
|
|
|
83
123
|
const badOutputs = {
|
|
84
124
|
heading: '## Summary\nDone.',
|
|
@@ -91,10 +131,11 @@ const badOutputs = {
|
|
|
91
131
|
timing: 'Mapped for 2m 32s\n\ndefault 스타일을 수정했습니다.',
|
|
92
132
|
tooManySentences: '수정했습니다. 검증했습니다. 보고했습니다.',
|
|
93
133
|
mixed: '수정했습니다.\n- 검증했습니다.',
|
|
134
|
+
tooManyBullets: '- 바뀐 점: A\n- 확인한 것: B\n- 다음 단계: C\n- 남은 리스크/다음 단계: D',
|
|
94
135
|
};
|
|
95
136
|
for (const [name, output] of Object.entries(badOutputs)) {
|
|
96
137
|
let failed = false;
|
|
97
|
-
try { assertCleanOutput(`bad:${name}`, output); } catch { failed = true; }
|
|
138
|
+
try { assertCleanOutput(`bad:${name}`, output, { allowedLabels: DEFAULT_REPORT_LABELS }); } catch { failed = true; }
|
|
98
139
|
assert(failed, `bad sample unexpectedly passed: ${name}`);
|
|
99
140
|
}
|
|
100
141
|
|