mixdog 0.9.14 → 0.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/bench/cache-probe-tasks.json +1 -1
- package/scripts/bench/lead-review-tasks-r3.json +1 -1
- package/scripts/bench/r4-mixed-tasks.json +1 -1
- package/scripts/bench/review-tasks.json +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/explore-bench.mjs +5 -4
- package/scripts/ingest-pure-conversation-smoke.mjs +27 -0
- package/scripts/output-style-smoke.mjs +3 -3
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/termio-input-smoke.mjs +199 -0
- package/scripts/tool-efficiency-diag.mjs +88 -0
- package/scripts/tool-smoke.mjs +40 -24
- package/scripts/tui-frame-harness-shim.mjs +32 -0
- package/scripts/tui-frame-harness.mjs +306 -0
- package/src/agents/heavy-worker/AGENT.md +6 -7
- package/src/agents/worker/AGENT.md +6 -7
- package/src/lib/keychain-cjs.cjs +28 -11
- package/src/lib/rules-builder.cjs +6 -10
- package/src/mixdog-session-runtime.mjs +90 -20
- package/src/output-styles/simple.md +22 -24
- package/src/rules/agent/00-core.md +12 -11
- package/src/rules/agent/30-explorer.md +22 -21
- package/src/rules/lead/01-general.md +8 -8
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/lead/lead-brief.md +11 -12
- package/src/rules/shared/01-tool.md +25 -14
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +29 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +13 -5
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +37 -15
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +55 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +177 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +12 -19
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +22 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +13 -18
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/manager.mjs +59 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +33 -25
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +7 -5
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +73 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -10
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +21 -13
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -2
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +44 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -0
- package/src/runtime/channels/backends/telegram.mjs +29 -9
- package/src/runtime/channels/lib/event-queue.mjs +6 -2
- package/src/runtime/channels/lib/memory-client.mjs +66 -1
- package/src/runtime/channels/lib/webhook/deliveries.mjs +22 -1
- package/src/runtime/memory/index.mjs +95 -8
- package/src/runtime/memory/lib/cycle-scheduler.mjs +24 -5
- package/src/runtime/memory/lib/memory-cycle1.mjs +45 -3
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +7 -3
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +54 -10
- package/src/runtime/memory/lib/memory-cycle2.mjs +21 -8
- package/src/runtime/memory/lib/memory-embed.mjs +48 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +57 -13
- package/src/runtime/memory/lib/memory.mjs +88 -1
- package/src/runtime/memory/lib/session-ingest.mjs +34 -3
- package/src/runtime/memory/lib/trace-store.mjs +52 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +27 -4
- package/src/runtime/shared/child-guardian.mjs +4 -2
- package/src/runtime/shared/open-url.mjs +2 -1
- package/src/runtime/shared/singleton-owner.mjs +26 -0
- package/src/runtime/shared/tool-execution-contract.mjs +25 -0
- package/src/runtime/shared/transcript-writer.mjs +3 -1
- package/src/session-runtime/config-helpers.mjs +7 -2
- package/src/session-runtime/cwd-plugins.mjs +6 -1
- package/src/session-runtime/effort.mjs +6 -1
- package/src/session-runtime/plugin-mcp.mjs +116 -12
- package/src/session-runtime/settings-api.mjs +7 -1
- package/src/standalone/channel-worker.mjs +25 -3
- package/src/standalone/explore-tool.mjs +5 -5
- package/src/standalone/hook-bus/config.mjs +38 -2
- package/src/standalone/hook-bus/handlers.mjs +103 -11
- package/src/standalone/hook-bus.mjs +20 -6
- package/src/standalone/memory-runtime-proxy.mjs +40 -5
- package/src/tui/App.jsx +214 -30
- package/src/tui/app/core-memory-picker.mjs +6 -6
- package/src/tui/app/model-options.mjs +10 -4
- package/src/tui/app/settings-picker.mjs +5 -30
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/text-layout.mjs +57 -0
- package/src/tui/app/transcript-window.mjs +53 -2
- package/src/tui/app/use-mouse-input.mjs +60 -47
- package/src/tui/app/use-transcript-window.mjs +38 -10
- package/src/tui/components/PromptInput.jsx +18 -1
- package/src/tui/components/TextEntryPanel.jsx +97 -33
- package/src/tui/dist/index.mjs +567 -229
- package/src/tui/engine/tool-card-results.mjs +22 -5
- package/src/tui/engine.mjs +126 -7
- package/src/tui/index.jsx +4 -1
- package/src/workflows/default/WORKFLOW.md +27 -31
- package/vendor/ink/build/components/App.js +62 -17
- package/vendor/ink/build/ink.js +78 -3
- package/vendor/ink/build/input-parser.d.ts +9 -4
- package/vendor/ink/build/input-parser.js +45 -176
- package/vendor/ink/build/log-update.js +47 -2
- package/vendor/ink/build/termio-keypress.js +240 -0
- package/vendor/ink/build/termio-tokenize.js +253 -0
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
{ "id": "uc5-yesterday", "label": "UC5 calendar yesterday", "args": { "period": "yesterday", "limit": 10 }, "expect": "browse" },
|
|
16
16
|
{ "id": "uc5-thisweek", "label": "UC5 calendar this_week", "args": { "period": "this_week", "limit": 10 }, "expect": "browse" },
|
|
17
17
|
{ "id": "uc6-category", "label": "UC6 category decision 7d", "args": { "period": "7d", "category": "decision", "limit": 10 }, "expect": "browse" }
|
|
18
|
-
]
|
|
18
|
+
]
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
# smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
|
|
2
|
-
# runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
|
|
3
|
-
# boot, double-init refusal, hostile-env survival.
|
|
4
|
-
#
|
|
5
|
-
# Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
|
|
6
|
-
|
|
7
|
-
$ErrorActionPreference = 'Stop'
|
|
8
|
-
|
|
9
|
-
$Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
|
|
10
|
-
$Os = if ($env:OS) { $env:OS } else { 'win32' }
|
|
11
|
-
$Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
|
|
12
|
-
$ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
|
|
13
|
-
$PgVer = '16.4'
|
|
14
|
-
$PgvectorVer = '0.8.2'
|
|
15
|
-
|
|
16
|
-
$Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
|
|
17
|
-
$Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
|
|
18
|
-
|
|
19
|
-
$Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
|
|
23
|
-
|
|
24
|
-
Write-Host "==> Test 1: HEAD reaches release asset"
|
|
25
|
-
$resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
|
|
26
|
-
Write-Host " HTTP $($resp.StatusCode)"
|
|
27
|
-
|
|
28
|
-
Write-Host "==> Test 2: full download + sha256"
|
|
29
|
-
$TarPath = Join-Path $Work $Asset
|
|
30
|
-
Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
|
|
31
|
-
$sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
|
|
32
|
-
Write-Host " sha256=$sha"
|
|
33
|
-
|
|
34
|
-
Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
|
|
35
|
-
$CorruptPath = "$Work\corrupt.tar.gz"
|
|
36
|
-
Copy-Item $TarPath $CorruptPath
|
|
37
|
-
$bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
|
|
38
|
-
$stream = [System.IO.File]::OpenWrite($CorruptPath)
|
|
39
|
-
$stream.Position = 102400
|
|
40
|
-
$stream.Write($bytes, 0, 1)
|
|
41
|
-
$stream.Close()
|
|
42
|
-
$CorruptDir = "$Work\corrupt"
|
|
43
|
-
New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
|
|
44
|
-
$TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
|
|
45
|
-
if (-not $TarProc.WaitForExit(15000)) {
|
|
46
|
-
Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
|
|
47
|
-
Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
|
|
48
|
-
} elseif ($TarProc.ExitCode -eq 0) {
|
|
49
|
-
Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
|
|
50
|
-
} else {
|
|
51
|
-
Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
|
|
55
|
-
$FreshDir = "$Work\fresh"
|
|
56
|
-
New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
|
|
57
|
-
& tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
|
|
58
|
-
if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
|
|
59
|
-
|
|
60
|
-
$PgBin = "$FreshDir\bin"
|
|
61
|
-
$Data = "$FreshDir\pgdata"
|
|
62
|
-
$Log = "$FreshDir\pg.log"
|
|
63
|
-
$Port = 55897
|
|
64
|
-
|
|
65
|
-
# Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
|
|
66
|
-
$SavedPath = $env:PATH
|
|
67
|
-
$SavedPgRoot = $env:PGROOT
|
|
68
|
-
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
|
69
|
-
$env:PGROOT = $null
|
|
70
|
-
$env:PGDATA = $null
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
& "$PgBin\postgres.exe" --version
|
|
74
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
|
|
75
|
-
& "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
|
|
76
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
|
|
77
|
-
& "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
|
|
78
|
-
if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
& "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
|
|
82
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
|
|
83
|
-
$ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
|
|
84
|
-
if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
|
|
85
|
-
Write-Host " PASS: fresh-extract boot + vector extension"
|
|
86
|
-
} finally {
|
|
87
|
-
& "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
Write-Host "==> Test 5: second initdb on initialized dir refused"
|
|
91
|
-
$rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
|
|
92
|
-
if ($rc -eq 0) {
|
|
93
|
-
Write-Host " WARN: second initdb succeeded (unexpected)"
|
|
94
|
-
} else {
|
|
95
|
-
Write-Host " PASS: second initdb refused (exit $rc)"
|
|
96
|
-
}
|
|
97
|
-
} finally {
|
|
98
|
-
$env:PATH = $SavedPath
|
|
99
|
-
$env:PGROOT = $SavedPgRoot
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
Write-Host "==> All negative-path smokes: PASS"
|
|
103
|
-
}
|
|
104
|
-
finally {
|
|
105
|
-
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
|
106
|
-
}
|
|
1
|
+
# smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
|
|
2
|
+
# runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
|
|
3
|
+
# boot, double-init refusal, hostile-env survival.
|
|
4
|
+
#
|
|
5
|
+
# Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
|
|
9
|
+
$Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
|
|
10
|
+
$Os = if ($env:OS) { $env:OS } else { 'win32' }
|
|
11
|
+
$Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
|
|
12
|
+
$ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
|
|
13
|
+
$PgVer = '16.4'
|
|
14
|
+
$PgvectorVer = '0.8.2'
|
|
15
|
+
|
|
16
|
+
$Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
|
|
17
|
+
$Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
|
|
18
|
+
|
|
19
|
+
$Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
|
|
23
|
+
|
|
24
|
+
Write-Host "==> Test 1: HEAD reaches release asset"
|
|
25
|
+
$resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
|
|
26
|
+
Write-Host " HTTP $($resp.StatusCode)"
|
|
27
|
+
|
|
28
|
+
Write-Host "==> Test 2: full download + sha256"
|
|
29
|
+
$TarPath = Join-Path $Work $Asset
|
|
30
|
+
Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
|
|
31
|
+
$sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
|
|
32
|
+
Write-Host " sha256=$sha"
|
|
33
|
+
|
|
34
|
+
Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
|
|
35
|
+
$CorruptPath = "$Work\corrupt.tar.gz"
|
|
36
|
+
Copy-Item $TarPath $CorruptPath
|
|
37
|
+
$bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
|
|
38
|
+
$stream = [System.IO.File]::OpenWrite($CorruptPath)
|
|
39
|
+
$stream.Position = 102400
|
|
40
|
+
$stream.Write($bytes, 0, 1)
|
|
41
|
+
$stream.Close()
|
|
42
|
+
$CorruptDir = "$Work\corrupt"
|
|
43
|
+
New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
|
|
44
|
+
$TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
|
|
45
|
+
if (-not $TarProc.WaitForExit(15000)) {
|
|
46
|
+
Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
|
|
47
|
+
Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
|
|
48
|
+
} elseif ($TarProc.ExitCode -eq 0) {
|
|
49
|
+
Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
|
|
50
|
+
} else {
|
|
51
|
+
Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
|
|
55
|
+
$FreshDir = "$Work\fresh"
|
|
56
|
+
New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
|
|
57
|
+
& tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
|
|
58
|
+
if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
|
|
59
|
+
|
|
60
|
+
$PgBin = "$FreshDir\bin"
|
|
61
|
+
$Data = "$FreshDir\pgdata"
|
|
62
|
+
$Log = "$FreshDir\pg.log"
|
|
63
|
+
$Port = 55897
|
|
64
|
+
|
|
65
|
+
# Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
|
|
66
|
+
$SavedPath = $env:PATH
|
|
67
|
+
$SavedPgRoot = $env:PGROOT
|
|
68
|
+
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
|
69
|
+
$env:PGROOT = $null
|
|
70
|
+
$env:PGDATA = $null
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
& "$PgBin\postgres.exe" --version
|
|
74
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
|
|
75
|
+
& "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
|
|
76
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
|
|
77
|
+
& "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
|
|
78
|
+
if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
& "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
|
|
82
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
|
|
83
|
+
$ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
|
|
84
|
+
if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
|
|
85
|
+
Write-Host " PASS: fresh-extract boot + vector extension"
|
|
86
|
+
} finally {
|
|
87
|
+
& "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Write-Host "==> Test 5: second initdb on initialized dir refused"
|
|
91
|
+
$rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
|
|
92
|
+
if ($rc -eq 0) {
|
|
93
|
+
Write-Host " WARN: second initdb succeeded (unexpected)"
|
|
94
|
+
} else {
|
|
95
|
+
Write-Host " PASS: second initdb refused (exit $rc)"
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
$env:PATH = $SavedPath
|
|
99
|
+
$env:PGROOT = $SavedPgRoot
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Write-Host "==> All negative-path smokes: PASS"
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
|
106
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* termio-input-smoke.mjs — throwaway smoke for the vendored termio input
|
|
4
|
+
* pipeline. Feeds synthetic chunk sequences through createInputParser() and
|
|
5
|
+
* asserts the typed event kinds / channel routing decisions.
|
|
6
|
+
*
|
|
7
|
+
* Run: node scripts/termio-input-smoke.mjs
|
|
8
|
+
*/
|
|
9
|
+
import { createInputParser } from '../vendor/ink/build/input-parser.js';
|
|
10
|
+
|
|
11
|
+
let failures = 0;
|
|
12
|
+
function check(name, cond, detail) {
|
|
13
|
+
if (cond) {
|
|
14
|
+
console.log(`ok - ${name}`);
|
|
15
|
+
} else {
|
|
16
|
+
failures++;
|
|
17
|
+
console.log(`FAIL - ${name}${detail ? ' :: ' + detail : ''}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Helper: push a list of chunks, collect all emitted events.
|
|
22
|
+
function run(chunks) {
|
|
23
|
+
const p = createInputParser();
|
|
24
|
+
const events = [];
|
|
25
|
+
for (const c of chunks) events.push(...p.push(c));
|
|
26
|
+
return { p, events };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 1. ESC split across two chunks -> single arrow-up key, no lone escape.
|
|
30
|
+
{
|
|
31
|
+
const { events } = run(['\x1b', '[A']);
|
|
32
|
+
check('split ESC -> one up key',
|
|
33
|
+
events.length === 1 && events[0].kind === 'key' && events[0].name === 'up',
|
|
34
|
+
JSON.stringify(events));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. SGR mouse click (button 0 press) -> ParsedMouse.
|
|
38
|
+
{
|
|
39
|
+
const { events } = run(['\x1b[<0;12;34M']);
|
|
40
|
+
const m = events[0];
|
|
41
|
+
check('SGR click -> mouse press',
|
|
42
|
+
events.length === 1 && m.kind === 'mouse' && m.action === 'press' &&
|
|
43
|
+
m.col === 12 && m.row === 34 && m.button === 0,
|
|
44
|
+
JSON.stringify(events));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. SGR wheel-up (button 64) -> key wheelup (stays a key, routes to mouse chan).
|
|
48
|
+
{
|
|
49
|
+
const { events } = run(['\x1b[<64;5;5M']);
|
|
50
|
+
const k = events[0];
|
|
51
|
+
check('SGR wheel -> wheelup key',
|
|
52
|
+
events.length === 1 && k.kind === 'key' && k.name === 'wheelup',
|
|
53
|
+
JSON.stringify(events));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 4. X10 mouse: CSI M + 3 payload bytes (wheel-up: Cb = 0x40+32 = 96 = '`').
|
|
57
|
+
{
|
|
58
|
+
const { events } = run(['\x1b[M\x60\x30\x30']);
|
|
59
|
+
const k = events[0];
|
|
60
|
+
check('X10 wheel -> wheelup key',
|
|
61
|
+
events.length === 1 && k.kind === 'key' && k.name === 'wheelup',
|
|
62
|
+
JSON.stringify(events));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 5. Bracketed paste split across 3 chunks -> one pasted key with full body.
|
|
66
|
+
{
|
|
67
|
+
const { events } = run(['\x1b[200~hel', 'lo wor', 'ld\x1b[201~']);
|
|
68
|
+
const paste = events.find((e) => e.isPasted);
|
|
69
|
+
check('split paste -> one pasted key "hello world"',
|
|
70
|
+
events.length === 1 && paste && paste.sequence === 'hello world',
|
|
71
|
+
JSON.stringify(events));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 6. Paste whose body ends with an X10 mouse tail `\x1b[M` — the ESC in the
|
|
75
|
+
// payload slot must NOT be consumed as X10 mouse; it stays paste content and
|
|
76
|
+
// PASTE_END still terminates the paste.
|
|
77
|
+
{
|
|
78
|
+
const { events } = run(['\x1b[200~abc\x1b[M12\x1b[201~']);
|
|
79
|
+
const paste = events.find((e) => e.isPasted);
|
|
80
|
+
check('paste with \\x1b[M tail stays paste',
|
|
81
|
+
events.length === 1 && paste && paste.sequence === 'abc\x1b[M12',
|
|
82
|
+
JSON.stringify(events));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 7. Terminal response (DA1) -> ParsedResponse.
|
|
86
|
+
{
|
|
87
|
+
const { events } = run(['\x1b[?62;1c']);
|
|
88
|
+
const r = events[0];
|
|
89
|
+
check('DA1 -> response',
|
|
90
|
+
events.length === 1 && r.kind === 'response' && r.response.type === 'da1',
|
|
91
|
+
JSON.stringify(events));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 8. Lone ESC buffered -> hasPendingEscape true; flushPendingEscape -> escape key.
|
|
95
|
+
{
|
|
96
|
+
const p = createInputParser();
|
|
97
|
+
const first = p.push('\x1b');
|
|
98
|
+
check('lone ESC buffers (no immediate events)', first.length === 0, JSON.stringify(first));
|
|
99
|
+
check('hasPendingEscape true for lone ESC', p.hasPendingEscape() === true);
|
|
100
|
+
const flushed = p.flushPendingEscape();
|
|
101
|
+
check('flushPendingEscape -> escape key',
|
|
102
|
+
Array.isArray(flushed) && flushed.length === 1 && flushed[0].name === 'escape',
|
|
103
|
+
JSON.stringify(flushed));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 9. Incomplete paste-start marker must NOT arm the escape flush timer.
|
|
107
|
+
{
|
|
108
|
+
const p = createInputParser();
|
|
109
|
+
p.push('\x1b[200');
|
|
110
|
+
check('partial paste-start does not arm flush', p.hasPendingEscape() === false);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 9b. Generic partial CSI (`\x1b[`, `\x1b[2`, `\x1b[20`) — none of these are
|
|
114
|
+
// paste-start prefixes/penultimate, so hasPendingEscape MUST arm to avoid
|
|
115
|
+
// buffering indefinitely.
|
|
116
|
+
{
|
|
117
|
+
for (const partial of ['\x1b[', '\x1b[2', '\x1b[20']) {
|
|
118
|
+
const p = createInputParser();
|
|
119
|
+
p.push(partial);
|
|
120
|
+
check(`generic partial CSI ${JSON.stringify(partial)} arms flush`,
|
|
121
|
+
p.hasPendingEscape() === true);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 9c. `\x1b[200` and `\x1b[200~`-prefixed pending must NOT arm the flush
|
|
126
|
+
// (paste-start marker / its penultimate prefix complete on their own).
|
|
127
|
+
{
|
|
128
|
+
const p1 = createInputParser();
|
|
129
|
+
p1.push('\x1b[200');
|
|
130
|
+
check('"\\x1b[200" does not arm flush', p1.hasPendingEscape() === false);
|
|
131
|
+
|
|
132
|
+
const p2 = createInputParser();
|
|
133
|
+
p2.push('\x1b[200~hel'); // still inside paste body once PASTE_START token completes
|
|
134
|
+
check('paste-start-prefixed pending does not arm flush', p2.hasPendingEscape() === false);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 9d. Held-backspace chunk splits into individual key events; `\r`/`\t` in
|
|
138
|
+
// text are NOT split.
|
|
139
|
+
{
|
|
140
|
+
const { events } = run(['abc\x7f\x7f\x7f']);
|
|
141
|
+
const names = events.map((e) => e.name || e.sequence);
|
|
142
|
+
check('abc + 3x backspace -> text key(s) + 3 separate backspace keys',
|
|
143
|
+
events.length === 4 &&
|
|
144
|
+
events[0].sequence === 'abc' &&
|
|
145
|
+
events.slice(1).every((e) => e.name === 'backspace' || e.sequence === '\x7f'),
|
|
146
|
+
JSON.stringify(names));
|
|
147
|
+
}
|
|
148
|
+
{
|
|
149
|
+
const { events } = run(['a\rb\tc']);
|
|
150
|
+
check('\\r and \\t inside text are not split (single key for whole run)',
|
|
151
|
+
events.length === 1 && events[0].sequence === 'a\rb\tc',
|
|
152
|
+
JSON.stringify(events));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 10. Plain text -> key events, sequence carries the character.
|
|
156
|
+
{
|
|
157
|
+
const { events } = run(['a']);
|
|
158
|
+
check('plain char -> key "a"',
|
|
159
|
+
events.length === 1 && events[0].kind === 'key' && events[0].sequence === 'a',
|
|
160
|
+
JSON.stringify(events));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 11. Consumer-level routing: synthetic 'mouse' channel events reach the
|
|
164
|
+
// use-mouse-input dispatch decisions exactly once each. We replicate the
|
|
165
|
+
// hook's branch keys (kind:'key' wheel with ctrl-mask from sequence vs.
|
|
166
|
+
// kind:'mouse' press) to guard against double-handling / mis-routing.
|
|
167
|
+
// MOUSE_CTRL_MASK = 16; wheel ctrl bit is read from the SGR button in
|
|
168
|
+
// the ParsedKey.sequence via /\x1b\[<(\d+);/ (no button field on wheel).
|
|
169
|
+
{
|
|
170
|
+
const MOUSE_CTRL_MASK = 16;
|
|
171
|
+
const WHEEL_SGR = /\x1b\[<(\d+);/;
|
|
172
|
+
// Feed real parser output so field shapes match production.
|
|
173
|
+
const wheelPlain = run(['\x1b[<64;5;5M']).events[0]; // wheelup, no ctrl
|
|
174
|
+
const wheelCtrl = run(['\x1b[<80;5;5M']).events[0]; // 64|16 -> ctrl wheelup
|
|
175
|
+
const click = run(['\x1b[<0;12;34M']).events[0]; // left press
|
|
176
|
+
|
|
177
|
+
const routes = { zoom: 0, scroll: 0, press: 0, ignored: 0 };
|
|
178
|
+
function route(event) {
|
|
179
|
+
if (!event || typeof event !== 'object') return;
|
|
180
|
+
if (event.kind === 'key') {
|
|
181
|
+
if (event.name !== 'wheelup' && event.name !== 'wheeldown') { routes.ignored++; return; }
|
|
182
|
+
const wm = WHEEL_SGR.exec(typeof event.sequence === 'string' ? event.sequence : '');
|
|
183
|
+
const ctrl = wm ? ((Number(wm[1]) & MOUSE_CTRL_MASK) !== 0) : false;
|
|
184
|
+
if (ctrl) routes.zoom++; else routes.scroll++;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (event.kind !== 'mouse') { routes.ignored++; return; }
|
|
188
|
+
const button = Number(event.button);
|
|
189
|
+
if ((button & 3) === 0 && event.action === 'press' && (button & 32) === 0) routes.press++;
|
|
190
|
+
}
|
|
191
|
+
for (const e of [wheelPlain, wheelCtrl, click]) route(e);
|
|
192
|
+
check('mouse routing: plain wheel -> scroll x1', routes.scroll === 1, JSON.stringify(routes));
|
|
193
|
+
check('mouse routing: ctrl wheel -> zoom x1', routes.zoom === 1, JSON.stringify(routes));
|
|
194
|
+
check('mouse routing: left press -> press x1 (no double)', routes.press === 1, JSON.stringify(routes));
|
|
195
|
+
check('mouse routing: nothing mis-routed', routes.ignored === 0, JSON.stringify(routes));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(failures === 0 ? '\nALL PASS' : `\n${failures} FAILURE(S)`);
|
|
199
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tool-usage efficiency diag for worker/heavy-worker (before/after rule changes).
|
|
3
|
+
// Usage: node scripts/tool-efficiency-diag.mjs [--since 24h|<epoch-ms>|ISO]
|
|
4
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
function sinceArg() {
|
|
9
|
+
const i = process.argv.indexOf('--since');
|
|
10
|
+
const raw = i >= 0 ? process.argv[i + 1] : '24h';
|
|
11
|
+
const rel = String(raw).match(/^(\d+(?:\.\d+)?)(h|m|d)$/i);
|
|
12
|
+
if (rel) {
|
|
13
|
+
const mult = { m: 60_000, h: 3_600_000, d: 86_400_000 }[rel[2].toLowerCase()];
|
|
14
|
+
return Date.now() - Number(rel[1]) * mult;
|
|
15
|
+
}
|
|
16
|
+
if (/^\d+$/.test(raw)) return Number(raw);
|
|
17
|
+
const p = Date.parse(raw);
|
|
18
|
+
return Number.isFinite(p) ? p : Date.now() - 86_400_000;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const dataDir = process.env.MIXDOG_DATA_DIR || resolve(homedir(), '.mixdog', 'data');
|
|
22
|
+
const files = [resolve(dataDir, 'history', 'agent-trace.jsonl.1'), resolve(dataDir, 'history', 'agent-trace.jsonl')];
|
|
23
|
+
const since = sinceArg();
|
|
24
|
+
const rows = [];
|
|
25
|
+
for (const f of files) {
|
|
26
|
+
if (!existsSync(f)) continue;
|
|
27
|
+
for (const line of readFileSync(f, 'utf8').split(/\r?\n/)) {
|
|
28
|
+
if (!line) continue;
|
|
29
|
+
try { const r = JSON.parse(line); if ((r.ts || 0) >= since) rows.push(r); } catch {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const F = (r, n) => (r[n] != null ? r[n] : (r.payload && r.payload[n] != null ? r.payload[n] : null));
|
|
33
|
+
const bySess = new Map();
|
|
34
|
+
for (const r of rows) {
|
|
35
|
+
const k = r.sessionId || r.session_id || '?';
|
|
36
|
+
if (!bySess.has(k)) bySess.set(k, []);
|
|
37
|
+
bySess.get(k).push(r);
|
|
38
|
+
}
|
|
39
|
+
console.log(`window since ${new Date(since).toISOString()}`);
|
|
40
|
+
for (const target of ['heavy-worker', 'worker']) {
|
|
41
|
+
let batch1 = 0, batchTot = 0, llmCalls = 0, sessions = 0, toolCalls = 0;
|
|
42
|
+
let grepContent = 0, grepContentWithCtx = 0, grepThenRead = 0, rereads = 0, outTok = 0;
|
|
43
|
+
for (const [, rs] of bySess) {
|
|
44
|
+
let agent = null;
|
|
45
|
+
for (const r of rs) { const x = F(r, 'agent'); if (x) { agent = x; break; } }
|
|
46
|
+
if (agent !== target) continue;
|
|
47
|
+
sessions++;
|
|
48
|
+
rs.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
49
|
+
for (const b of rs.filter((r) => r.kind === 'batch')) {
|
|
50
|
+
const c = Number(F(b, 'tool_call_count')) || 0;
|
|
51
|
+
batchTot++; if (c === 1) batch1++;
|
|
52
|
+
}
|
|
53
|
+
const usage = rs.filter((r) => r.kind === 'usage_raw');
|
|
54
|
+
llmCalls += usage.length;
|
|
55
|
+
outTok += usage.reduce((s, r) => s + (Number(F(r, 'output_tokens')) || 0), 0);
|
|
56
|
+
const tools = rs.filter((r) => r.kind === 'tool');
|
|
57
|
+
toolCalls += tools.length;
|
|
58
|
+
const seenRead = new Map();
|
|
59
|
+
for (let i = 0; i < tools.length; i++) {
|
|
60
|
+
const t = tools[i];
|
|
61
|
+
const n = String(F(t, 'tool_name'));
|
|
62
|
+
const a = F(t, 'tool_args_summary') || {};
|
|
63
|
+
if (n === 'read') {
|
|
64
|
+
const p = typeof a.path === 'string' ? a.path : JSON.stringify(a.path);
|
|
65
|
+
seenRead.set(p, (seenRead.get(p) || 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
if (n === 'grep' && ['content', 'content_with_context'].includes(String(a.output_mode || ''))) {
|
|
68
|
+
grepContent++;
|
|
69
|
+
if (a['-C'] != null || a['-A'] != null || a['-B'] != null) grepContentWithCtx++;
|
|
70
|
+
const gpath = typeof a.path === 'string' ? a.path : JSON.stringify(a.path || '');
|
|
71
|
+
for (let j = i + 1; j < Math.min(i + 4, tools.length); j++) {
|
|
72
|
+
const u = tools[j];
|
|
73
|
+
if (String(F(u, 'tool_name')) !== 'read') continue;
|
|
74
|
+
const ra = F(u, 'tool_args_summary') || {};
|
|
75
|
+
const rpath = typeof ra.path === 'string' ? ra.path : JSON.stringify(ra.path || '');
|
|
76
|
+
if (rpath && gpath && (rpath.includes(gpath) || gpath.includes(rpath))) { grepThenRead++; break; }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const [, c] of seenRead) if (c >= 3) rereads++;
|
|
81
|
+
}
|
|
82
|
+
const pct = (a, b) => (b ? Math.round((a / b) * 100) : 0);
|
|
83
|
+
console.log(`\n${target}: sessions=${sessions} llmCalls=${llmCalls} toolCalls=${toolCalls} outTok=${outTok}`);
|
|
84
|
+
console.log(` llmCalls/session=${sessions ? Math.round(llmCalls / sessions) : 0} tools/llmCall=${llmCalls ? (toolCalls / llmCalls).toFixed(2) : '-'}`);
|
|
85
|
+
console.log(` single-call batches: ${batch1}/${batchTot} (${pct(batch1, batchTot)}%)`);
|
|
86
|
+
console.log(` content greps with -C/-A/-B: ${grepContentWithCtx}/${grepContent} (${pct(grepContentWithCtx, grepContent)}%) grep->read follow-ups: ${grepThenRead}`);
|
|
87
|
+
console.log(` same-path re-reads >=3x: ${rereads}`);
|
|
88
|
+
}
|
package/scripts/tool-smoke.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import { mergeSessionRowsIntoGlobal } from '../src/runtime/memory/lib/memory-ses
|
|
|
42
42
|
import { TOOL_DEFS as SEARCH_TOOL_DEFS } from '../src/runtime/search/tool-defs.mjs';
|
|
43
43
|
import { TOOL_DEFS as CHANNEL_TOOL_DEFS } from '../src/runtime/channels/tool-defs.mjs';
|
|
44
44
|
import { AGENT_OWNER } from '../src/runtime/agent/orchestrator/agent-owner.mjs';
|
|
45
|
+
import { recursiveWrapperToolNameForPublicAgent } from '../src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs';
|
|
45
46
|
import { composeSystemPrompt } from '../src/runtime/agent/orchestrator/context/collect.mjs';
|
|
46
47
|
import { setInternalToolsProvider } from '../src/runtime/agent/orchestrator/internal-tools.mjs';
|
|
47
48
|
import { prepareAgentSession } from '../src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs';
|
|
@@ -386,7 +387,7 @@ assertOk('glob explicit src', explicitSrcGlobOut, /src[\\/].*engine\.mjs/i);
|
|
|
386
387
|
|
|
387
388
|
const globPathOnlyOut = await executeBuiltinTool('glob', {
|
|
388
389
|
path: 'scripts',
|
|
389
|
-
head_limit:
|
|
390
|
+
head_limit: 200,
|
|
390
391
|
}, root);
|
|
391
392
|
assertOk('glob path-only default *', globPathOnlyOut, /tool-smoke\.mjs/i);
|
|
392
393
|
|
|
@@ -398,8 +399,8 @@ const grepNoPatternGlobOut = await executeBuiltinTool('grep', {
|
|
|
398
399
|
assertOk('grep without pattern routes to glob', grepNoPatternGlobOut, /tool-smoke\.mjs/i);
|
|
399
400
|
|
|
400
401
|
const grepManyPatterns = [
|
|
401
|
-
...Array.from({ length: 20 }, (_, i) => `__tool_smoke_miss_${i}__`),
|
|
402
402
|
'tool-smoke',
|
|
403
|
+
...Array.from({ length: 20 }, (_, i) => `__tool_smoke_miss_${i}__`),
|
|
403
404
|
];
|
|
404
405
|
const grepManyPatternsOut = await executeBuiltinTool('grep', {
|
|
405
406
|
pattern: grepManyPatterns,
|
|
@@ -407,12 +408,12 @@ const grepManyPatternsOut = await executeBuiltinTool('grep', {
|
|
|
407
408
|
glob: '*.mjs',
|
|
408
409
|
head_limit: 5,
|
|
409
410
|
}, root);
|
|
410
|
-
if (/exceeds the
|
|
411
|
-
throw new Error(`grep should
|
|
411
|
+
if (/exceeds the \d+-pattern cap/i.test(String(grepManyPatternsOut))) {
|
|
412
|
+
throw new Error(`grep should truncate oversized pattern[] instead of error:\n${grepManyPatternsOut.slice(0, 400)}`);
|
|
412
413
|
}
|
|
413
|
-
assertOk('grep >
|
|
414
|
-
if (!/\[
|
|
415
|
-
throw new Error(`grep >
|
|
414
|
+
assertOk('grep >10 pattern cap keeps first patterns', grepManyPatternsOut, /tool-smoke\.mjs/i);
|
|
415
|
+
if (!/\[capped at 10 of 21 patterns\]/.test(String(grepManyPatternsOut))) {
|
|
416
|
+
throw new Error(`grep >10 patterns should emit cap note:\n${grepManyPatternsOut.slice(0, 400)}`);
|
|
416
417
|
}
|
|
417
418
|
|
|
418
419
|
function grepCountTotalMatches(body) {
|
|
@@ -431,9 +432,8 @@ if (singleCountTotal == null || singleCountTotal < 1) {
|
|
|
431
432
|
}
|
|
432
433
|
|
|
433
434
|
const grepCountOverlapPatterns = [
|
|
434
|
-
...Array.from({ length: 19 }, (_, i) => `__count_overlap_a_${i}__`),
|
|
435
435
|
'assertOk',
|
|
436
|
-
...Array.from({ length:
|
|
436
|
+
...Array.from({ length: 8 }, (_, i) => `__count_overlap_a_${i}__`),
|
|
437
437
|
'assertOk',
|
|
438
438
|
];
|
|
439
439
|
const grepCountOverlapOut = await executeBuiltinTool('grep', {
|
|
@@ -444,36 +444,37 @@ const grepCountOverlapOut = await executeBuiltinTool('grep', {
|
|
|
444
444
|
const overlapCountTotal = grepCountTotalMatches(grepCountOverlapOut);
|
|
445
445
|
if (overlapCountTotal !== singleCountTotal) {
|
|
446
446
|
throw new Error(
|
|
447
|
-
`
|
|
447
|
+
`multi-pattern count must not double-count overlapping lines (single=${singleCountTotal} overlap=${overlapCountTotal}):\n${grepCountOverlapOut.slice(0, 600)}`,
|
|
448
448
|
);
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
const grepChunkContextPatterns = [
|
|
452
|
-
...Array.from({ length: 20 }, (_, i) => `__ctx_chunk_miss_${i}__`),
|
|
453
452
|
'grepCountTotalMatches',
|
|
453
|
+
...Array.from({ length: 20 }, (_, i) => `__ctx_chunk_miss_${i}__`),
|
|
454
454
|
];
|
|
455
455
|
const grepChunkContextOut = await executeBuiltinTool('grep', {
|
|
456
456
|
pattern: grepChunkContextPatterns,
|
|
457
|
-
path: 'scripts
|
|
457
|
+
path: 'scripts',
|
|
458
|
+
glob: 'tool-smoke.mjs',
|
|
458
459
|
'-C': 1,
|
|
459
460
|
head_limit: 30,
|
|
460
461
|
}, root);
|
|
461
|
-
if (!/\[
|
|
462
|
-
throw new Error(`
|
|
462
|
+
if (!/\[capped at 10 of 21 patterns\]/.test(String(grepChunkContextOut))) {
|
|
463
|
+
throw new Error(`oversized -C pattern[] should emit cap note:\n${grepChunkContextOut.slice(0, 500)}`);
|
|
463
464
|
}
|
|
464
465
|
if (!/tool-smoke\.mjs:\d+:/.test(String(grepChunkContextOut))) {
|
|
465
|
-
throw new Error(`
|
|
466
|
+
throw new Error(`capped -C must emit path-prefixed match lines:\n${grepChunkContextOut.slice(0, 800)}`);
|
|
466
467
|
}
|
|
467
468
|
if (!/tool-smoke\.mjs-\d+-/.test(String(grepChunkContextOut))) {
|
|
468
|
-
throw new Error(`
|
|
469
|
+
throw new Error(`capped -C must keep path-prefixed context lines:\n${grepChunkContextOut.slice(0, 800)}`);
|
|
469
470
|
}
|
|
470
471
|
const ctxBodyLines = String(grepChunkContextOut).split('\n').filter((l) => l && !/^\[/.test(l) && !/^\(no matches\)/.test(l));
|
|
471
472
|
const orphanLineOnlyContext = ctxBodyLines.some((l) => /^\d+-/.test(l));
|
|
472
473
|
if (orphanLineOnlyContext) {
|
|
473
|
-
throw new Error(`
|
|
474
|
+
throw new Error(`capped -C must not leave line-only context orphans:\n${grepChunkContextOut.slice(0, 800)}`);
|
|
474
475
|
}
|
|
475
476
|
if (!/function grepCountTotalMatches/.test(String(grepChunkContextOut))) {
|
|
476
|
-
throw new Error(`
|
|
477
|
+
throw new Error(`capped -C should include match span:\n${grepChunkContextOut.slice(0, 800)}`);
|
|
477
478
|
}
|
|
478
479
|
|
|
479
480
|
const findOut = await executeBuiltinTool('find', {
|
|
@@ -1165,12 +1166,19 @@ setInternalToolsProvider({
|
|
|
1165
1166
|
if (!/Read-only retrieval role/i.test(visible) || /# environment/i.test(visible) || /git operations deferred to Lead/i.test(visible)) {
|
|
1166
1167
|
throw new Error(`explorer hidden retrieval context should stay slim: ${visible.slice(0, 1200)}`);
|
|
1167
1168
|
}
|
|
1168
|
-
if (!/# Role: explorer/i.test(systemVisible) || /# Role: explorer/i.test(userReminderVisible) || !/
|
|
1169
|
+
if (!/# Role: explorer/i.test(systemVisible) || /# Role: explorer/i.test(userReminderVisible) || !/Coordinate locator/i.test(systemVisible)) {
|
|
1169
1170
|
throw new Error(`explorer role md must ride BP2 system, not BP3 user reminder: system=${systemVisible.slice(0, 600)} user=${userReminderVisible.slice(0, 600)}`);
|
|
1170
1171
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1172
|
+
// System layers (BP1 tool policy + BP2 role md) are shared/frozen and sized
|
|
1173
|
+
// elsewhere; the slimness cap guards only the per-session injected layers
|
|
1174
|
+
// (BP3 user reminder etc.), so measure non-system messages only.
|
|
1175
|
+
const injectedVisible = (explorerSession.messages || [])
|
|
1176
|
+
.filter((m) => m?.role !== 'system')
|
|
1177
|
+
.map((m) => String(m.content || ''))
|
|
1178
|
+
.join('\n');
|
|
1179
|
+
const injectedBytes = Buffer.byteLength(injectedVisible, 'utf8');
|
|
1180
|
+
if (injectedBytes > 1800) {
|
|
1181
|
+
throw new Error(`explorer hidden retrieval context too large: ${injectedBytes} bytes (injected layers only)`);
|
|
1174
1182
|
}
|
|
1175
1183
|
} finally {
|
|
1176
1184
|
closeSession(explorerSession.id, 'tool-smoke');
|
|
@@ -1298,8 +1306,16 @@ setInternalToolsProvider({
|
|
|
1298
1306
|
if (!fullTools.includes('explore')) {
|
|
1299
1307
|
throw new Error(`full agent schema must expose explore: full=${fullTools.join(', ')}`);
|
|
1300
1308
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1309
|
+
// Unified-shard policy (v0.9.13): the explore wrapper stays IN the schema
|
|
1310
|
+
// for every read role — including the explore agent itself — so the
|
|
1311
|
+
// read-only bundle is bit-identical across roles (one cache group).
|
|
1312
|
+
// Recursion is broken at call time in pre-dispatch-deny.mjs via
|
|
1313
|
+
// recursiveWrapperToolNameForPublicAgent, not by schema stripping.
|
|
1314
|
+
if (JSON.stringify(publicExploreTools) !== JSON.stringify(expectedReadTools)) {
|
|
1315
|
+
throw new Error(`public explore role must ship the shared read-only bundle (incl. explore): expected=${expectedReadTools.join(', ')} actual=${publicExploreTools.join(', ')}`);
|
|
1316
|
+
}
|
|
1317
|
+
if (recursiveWrapperToolNameForPublicAgent('explore') !== 'explore') {
|
|
1318
|
+
throw new Error('call-time anti-recursion must map public explore agent to its own wrapper tool');
|
|
1303
1319
|
}
|
|
1304
1320
|
} finally {
|
|
1305
1321
|
closeSession(readAgentSession.id, 'tool-smoke');
|