mixdog 0.9.21 → 0.9.22

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.21",
3
+ "version": "0.9.22",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -1,242 +1,242 @@
1
- # build-runtime-windows.ps1 — Build PostgreSQL 16 + pgvector runtime on Windows.
2
- # Uses windows-2022 GHA runner's preinstalled PostgreSQL 16 (consistent
3
- # pg_config + postgres.exe from same package) — avoids EDB zip's split-version
4
- # packaging bug that linked pgvector against PG 14 ABI.
5
- # Builds pgvector from source via MSVC/nmake, then assembles a self-contained
6
- # runtime tree (bin + lib + share at root). Final smoke: initdb + CREATE
7
- # EXTENSION vector + distance query.
8
- # Produces: dist\mixdog-runtime-win32-x64-pg{pgver}-pgvector{vecver}.tar.gz
9
-
10
- $ErrorActionPreference = 'Stop'
11
-
12
- $PG_VERSION = '16.4'
13
- $PGVECTOR_VERSION = '0.8.2'
14
- $TARGET_OS = $env:TARGET_OS ?? 'win32'
15
- $TARGET_ARCH = $env:TARGET_ARCH ?? 'x64'
16
-
17
- # Auto-detect highest preinstalled PG ≥ 16 OR install via chocolatey.
18
- $PgInstallRoot = 'C:\Program Files\PostgreSQL'
19
-
20
- function Find-PgRoot {
21
- if (-not (Test-Path $PgInstallRoot)) { return $null }
22
- $cands = Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue |
23
- Where-Object { $_.Name -match '^(\d+)' } |
24
- Sort-Object { [int]([regex]::Match($_.Name, '^(\d+)').Groups[1].Value) } -Descending
25
- foreach ($c in $cands) {
26
- $major = [int]([regex]::Match($c.Name, '^(\d+)').Groups[1].Value)
27
- if ($major -ge 16 -and (Test-Path "$($c.FullName)\bin\pg_config.exe")) {
28
- return $c.FullName
29
- }
30
- }
31
- return $null
32
- }
33
-
34
- $PgRoot = Find-PgRoot
35
- if (-not $PgRoot) {
36
- Write-Host "==> No preinstalled PG ≥ 16 found. Installing via chocolatey..."
37
- if (Test-Path $PgInstallRoot) {
38
- Write-Host "Existing PG dirs (none usable):"
39
- Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue | Select-Object Name
40
- }
41
- choco install postgresql16 --version=16.4.0 --params '/Password:postgres' -y --no-progress 2>&1 | Out-Host
42
- if ($LASTEXITCODE -ne 0) { Write-Error "choco install postgresql16 failed (exit $LASTEXITCODE)"; exit 1 }
43
- $PgRoot = Find-PgRoot
44
- if (-not $PgRoot) {
45
- Write-Error "ASSERT FAILED: chocolatey install completed but PG ≥ 16 still not found under $PgInstallRoot"
46
- Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue | Select-Object Name
47
- exit 1
48
- }
49
- }
50
-
51
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
52
- $RootDir = (Resolve-Path "$ScriptDir\..").Path
53
- $BuildDir = "$RootDir\build\runtime-win32-$TARGET_ARCH"
54
- $DistDir = "$RootDir\dist"
55
- $RuntimeDir = "$BuildDir\runtime"
56
-
57
- $PgBin = "$PgRoot\bin"
58
- $PgConfig = "$PgBin\pg_config.exe"
59
-
60
- $OutputName = "mixdog-runtime-${TARGET_OS}-${TARGET_ARCH}-pg${PG_VERSION}-pgvector${PGVECTOR_VERSION}.tar.gz"
61
-
62
- Write-Host "==> Using preinstalled PG: $PgRoot"
63
- & $PgConfig --version
64
- $RealVersion = (& $PgConfig --version) -replace 'PostgreSQL ', ''
65
- Write-Host " pg_config reports version: $RealVersion"
66
-
67
- if (Test-Path $RuntimeDir) { Remove-Item -Recurse -Force $RuntimeDir }
68
- New-Item -ItemType Directory -Force -Path $BuildDir, $DistDir,
69
- "$RuntimeDir\bin", "$RuntimeDir\lib", "$RuntimeDir\share" | Out-Null
70
-
71
- Write-Host "==> Cloning pgvector $PGVECTOR_VERSION"
72
- $PgVectorDir = "$BuildDir\pgvector"
73
- $VectorDllBuilt = "$PgVectorDir\vector.dll"
74
-
75
- if (Test-Path $VectorDllBuilt) {
76
- Write-Host " Cache hit: vector.dll already built at $VectorDllBuilt"
77
- } else {
78
- if (Test-Path $PgVectorDir) { Remove-Item -Recurse -Force $PgVectorDir }
79
- git clone --branch "v$PGVECTOR_VERSION" --depth 1 `
80
- https://github.com/pgvector/pgvector.git $PgVectorDir
81
-
82
- Write-Host "==> Building pgvector (MSVC/nmake against system PG 16)"
83
- Push-Location $PgVectorDir
84
- try {
85
- $VsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
86
- $VcVarsAll = & $VsWhere -latest -find 'VC\Auxiliary\Build\vcvarsall.bat' 2>$null | Select-Object -First 1
87
- if (-not $VcVarsAll) {
88
- Write-Error "vswhere could not locate vcvarsall.bat — Visual Studio Build Tools required."
89
- exit 1
90
- }
91
- # CRITICAL: prepend $PgRoot\bin to PATH so any bare `pg_config` call
92
- # inside Makefile.win resolves to PG 16 — runner has PG 14/15
93
- # preinstalled and would otherwise win the PATH lookup, producing
94
- # PG14-ABI vector.dll that fails to load in our PG 16 postgres.exe.
95
- # Set in PowerShell so cmd /c inherits; setting inside the cmd batch
96
- # via %PATH% loses vcvarsall's additions due to parse-time expansion.
97
- $env:PATH = "$PgRoot\bin;$env:PATH"
98
- $env:PGROOT = $PgRoot
99
- $BuildCmd = "`"$VcVarsAll`" amd64 && nmake /F Makefile.win PG_CONFIG=`"$PgConfig`""
100
- cmd /c $BuildCmd
101
- if ($LASTEXITCODE -ne 0) {
102
- Write-Error "pgvector nmake build failed (exit $LASTEXITCODE)"
103
- exit 1
104
- }
105
- } finally {
106
- Pop-Location
107
- }
108
- }
109
-
110
- Write-Host "==> Assembling runtime layout — copy bin (.exe + .dll), lib, share from $PgRoot"
111
- # Copy ALL .exe + .dll from PG bin so postgres.exe + libpq.dll + libcrypto/libssl/
112
- # libintl/libiconv/icu*/libxml2/libxslt/libwinpthread/libecpg/libpgtypes etc. all
113
- # ship together. PG 16 install layout puts these all in bin\.
114
- Copy-Item "$PgBin\*.exe","$PgBin\*.dll" "$RuntimeDir\bin\" -Force
115
-
116
- # lib\: PG extension modules (incl. contrib like pgcrypto.dll). vector.dll
117
- # placed here below — PG looks for $libdir/<ext>.dll which resolves to lib\.
118
- Copy-Item -Recurse -Force "$PgRoot\lib\*" "$RuntimeDir\lib\" -ErrorAction SilentlyContinue
119
- # share\: extension SQL/control, locale, timezone data, conf samples.
120
- Copy-Item -Recurse -Force "$PgRoot\share\*" "$RuntimeDir\share\" -ErrorAction SilentlyContinue
121
-
122
- Write-Host "==> Manually staging pgvector artifacts (avoid pg_config-derived install paths)"
123
- $RuntimeExtDir = "$RuntimeDir\share\extension"
124
- New-Item -ItemType Directory -Force -Path $RuntimeExtDir | Out-Null
125
- Copy-Item "$PgVectorDir\vector.dll" "$RuntimeDir\lib\" -Force
126
- Copy-Item "$PgVectorDir\vector.control" $RuntimeExtDir -Force
127
- Copy-Item "$PgVectorDir\sql\vector--*.sql" $RuntimeExtDir -Force
128
-
129
- Write-Host "==> Asserting runtime layout"
130
- $VectorControl = "$RuntimeDir\share\extension\vector.control"
131
- if (-not (Test-Path $VectorControl)) { Write-Error "ASSERT FAILED: $VectorControl not found"; exit 1 }
132
- $VectorSql = "$RuntimeDir\share\extension\vector--$PGVECTOR_VERSION.sql"
133
- if (-not (Test-Path $VectorSql)) { Write-Error "ASSERT FAILED: $VectorSql not found"; exit 1 }
134
- if (-not (Test-Path "$RuntimeDir\lib\vector.dll")) {
135
- Write-Error "ASSERT FAILED: vector.dll not found in lib\"
136
- exit 1
137
- }
138
- Write-Host " PASS runtime layout"
139
-
140
- # Licenses
141
- if (Test-Path "$PgRoot\doc\postgresql\COPYRIGHT") {
142
- Copy-Item "$PgRoot\doc\postgresql\COPYRIGHT" "$RuntimeDir\LICENSE.postgresql" -Force
143
- } elseif (Test-Path "$PgRoot\doc\COPYRIGHT") {
144
- Copy-Item "$PgRoot\doc\COPYRIGHT" "$RuntimeDir\LICENSE.postgresql" -Force
145
- }
146
- if (Test-Path "$PgVectorDir\LICENSE") {
147
- Copy-Item "$PgVectorDir\LICENSE" "$RuntimeDir\LICENSE.pgvector" -Force
148
- }
149
-
150
- Write-Host "==> Self-contained smoke test (initdb + CREATE EXTENSION vector + distance query)"
151
- & "$RuntimeDir\bin\postgres.exe" --version
152
- if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: postgres.exe --version exit $LASTEXITCODE"; exit 1 }
153
-
154
- $SmokeData = "$BuildDir\smoke-pgdata"
155
- $SmokeLog = "$BuildDir\smoke-pg.log"
156
- $SmokePort = 55899
157
- if (Test-Path $SmokeData) { Remove-Item -Recurse -Force $SmokeData }
158
-
159
- & "$RuntimeDir\bin\initdb.exe" -D $SmokeData --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
160
- if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: initdb"; exit 1 }
161
-
162
- & "$RuntimeDir\bin\pg_ctl.exe" -D $SmokeData -o "-p $SmokePort -h 127.0.0.1" -l $SmokeLog -w start
163
- if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: pg_ctl start (see $SmokeLog)"; Get-Content $SmokeLog | Select-Object -Last 30; exit 1 }
164
-
165
- try {
166
- & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
167
- if ($LASTEXITCODE -ne 0) { throw "CREATE EXTENSION vector failed" }
168
- $ExtV = & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
169
- $Dist = & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -tAc "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector;"
170
- Write-Host " vector extension version: $ExtV"
171
- Write-Host " distance query result: $Dist"
172
- if ($ExtV.Trim() -ne $PGVECTOR_VERSION) {
173
- Write-Error "FAIL: extversion='$ExtV' expected='$PGVECTOR_VERSION'"
174
- exit 1
175
- }
176
- Write-Host " PASS smoke (extension load + vector distance)"
177
- }
178
- finally {
179
- & "$RuntimeDir\bin\pg_ctl.exe" -D $SmokeData -m fast stop 2>$null | Out-Null
180
- Remove-Item -Recurse -Force $SmokeData -ErrorAction SilentlyContinue
181
- }
182
-
183
- Write-Host "==> Creating tarball: $OutputName"
184
- $DistDirFwd = $DistDir.Replace('\', '/')
185
- $RuntimeDirFwd = $RuntimeDir.Replace('\', '/')
186
- & tar -czf "$DistDirFwd/$OutputName" -C "$RuntimeDirFwd" .
187
- if ($LASTEXITCODE -ne 0) { Write-Error "tar failed (exit $LASTEXITCODE)"; exit 1 }
188
-
189
- Write-Host "==> Generating sha256 sidecar"
190
- Push-Location $DistDir
191
- $Hash = (Get-FileHash -Algorithm SHA256 $OutputName).Hash.ToLower()
192
- "$Hash $OutputName" | Out-File -Encoding ascii "${OutputName}.sha256"
193
- Pop-Location
194
-
195
- # ---------------------------------------------------------------------------
196
- # Phase A: Re-smoke from EXTRACTED tarball with hostile env (cleared PATH,
197
- # only system32). Catches false-pass where the build host has VC redist /
198
- # preinstalled DLLs that the tarball might be missing.
199
- # ---------------------------------------------------------------------------
200
- Write-Host "==> Re-smoke from extracted tarball (hostile env)"
201
- $ExtractDir = "$BuildDir\extract-smoke"
202
- if (Test-Path $ExtractDir) { Remove-Item -Recurse -Force $ExtractDir }
203
- New-Item -ItemType Directory -Force -Path $ExtractDir | Out-Null
204
- & tar -xzf "$DistDirFwd/$OutputName" -C ($ExtractDir.Replace('\','/'))
205
- if ($LASTEXITCODE -ne 0) { Write-Error "extract failed"; exit 1 }
206
-
207
- $ExtractData = "$ExtractDir\extract-pgdata"
208
- $ExtractLog = "$ExtractDir\extract-pg.log"
209
- $ExtractPort = 55898
210
-
211
- # Snapshot current env, then strip to minimal Windows PATH (no PG14/15/16
212
- # preinstalled bin, no chocolatey, no MSVC tools).
213
- $SavedPath = $env:PATH
214
- $SavedPgRoot = $env:PGROOT
215
- $env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
216
- $env:PGROOT = $null
217
- $env:PGDATA = $null
218
-
219
- try {
220
- & "$ExtractDir\bin\postgres.exe" --version
221
- if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version under hostile env" }
222
- & "$ExtractDir\bin\initdb.exe" -D $ExtractData --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
223
- if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb under hostile env" }
224
- & "$ExtractDir\bin\pg_ctl.exe" -D $ExtractData -o "-p $ExtractPort -h 127.0.0.1" -l $ExtractLog -w start
225
- if ($LASTEXITCODE -ne 0) { Get-Content $ExtractLog | Select-Object -Last 30; throw "FAIL: pg_ctl start under hostile env" }
226
- try {
227
- & "$ExtractDir\bin\psql.exe" -h 127.0.0.1 -p $ExtractPort -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
228
- if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION vector under hostile env" }
229
- $ExtV2 = & "$ExtractDir\bin\psql.exe" -h 127.0.0.1 -p $ExtractPort -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
230
- if ($ExtV2.Trim() -ne $PGVECTOR_VERSION) { throw "FAIL: extracted-smoke extversion='$ExtV2'" }
231
- Write-Host " PASS extracted-tarball smoke (hostile env)"
232
- } finally {
233
- & "$ExtractDir\bin\pg_ctl.exe" -D $ExtractData -m fast stop 2>$null | Out-Null
234
- }
235
- } finally {
236
- $env:PATH = $SavedPath
237
- $env:PGROOT = $SavedPgRoot
238
- Remove-Item -Recurse -Force $ExtractDir -ErrorAction SilentlyContinue
239
- }
240
-
241
- Write-Host "==> Done: $DistDir\$OutputName"
242
- Get-Item "$DistDir\$OutputName" | Select-Object Name, Length
1
+ # build-runtime-windows.ps1 — Build PostgreSQL 16 + pgvector runtime on Windows.
2
+ # Uses windows-2022 GHA runner's preinstalled PostgreSQL 16 (consistent
3
+ # pg_config + postgres.exe from same package) — avoids EDB zip's split-version
4
+ # packaging bug that linked pgvector against PG 14 ABI.
5
+ # Builds pgvector from source via MSVC/nmake, then assembles a self-contained
6
+ # runtime tree (bin + lib + share at root). Final smoke: initdb + CREATE
7
+ # EXTENSION vector + distance query.
8
+ # Produces: dist\mixdog-runtime-win32-x64-pg{pgver}-pgvector{vecver}.tar.gz
9
+
10
+ $ErrorActionPreference = 'Stop'
11
+
12
+ $PG_VERSION = '16.4'
13
+ $PGVECTOR_VERSION = '0.8.2'
14
+ $TARGET_OS = $env:TARGET_OS ?? 'win32'
15
+ $TARGET_ARCH = $env:TARGET_ARCH ?? 'x64'
16
+
17
+ # Auto-detect highest preinstalled PG ≥ 16 OR install via chocolatey.
18
+ $PgInstallRoot = 'C:\Program Files\PostgreSQL'
19
+
20
+ function Find-PgRoot {
21
+ if (-not (Test-Path $PgInstallRoot)) { return $null }
22
+ $cands = Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue |
23
+ Where-Object { $_.Name -match '^(\d+)' } |
24
+ Sort-Object { [int]([regex]::Match($_.Name, '^(\d+)').Groups[1].Value) } -Descending
25
+ foreach ($c in $cands) {
26
+ $major = [int]([regex]::Match($c.Name, '^(\d+)').Groups[1].Value)
27
+ if ($major -ge 16 -and (Test-Path "$($c.FullName)\bin\pg_config.exe")) {
28
+ return $c.FullName
29
+ }
30
+ }
31
+ return $null
32
+ }
33
+
34
+ $PgRoot = Find-PgRoot
35
+ if (-not $PgRoot) {
36
+ Write-Host "==> No preinstalled PG ≥ 16 found. Installing via chocolatey..."
37
+ if (Test-Path $PgInstallRoot) {
38
+ Write-Host "Existing PG dirs (none usable):"
39
+ Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue | Select-Object Name
40
+ }
41
+ choco install postgresql16 --version=16.4.0 --params '/Password:postgres' -y --no-progress 2>&1 | Out-Host
42
+ if ($LASTEXITCODE -ne 0) { Write-Error "choco install postgresql16 failed (exit $LASTEXITCODE)"; exit 1 }
43
+ $PgRoot = Find-PgRoot
44
+ if (-not $PgRoot) {
45
+ Write-Error "ASSERT FAILED: chocolatey install completed but PG ≥ 16 still not found under $PgInstallRoot"
46
+ Get-ChildItem $PgInstallRoot -Directory -ErrorAction SilentlyContinue | Select-Object Name
47
+ exit 1
48
+ }
49
+ }
50
+
51
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
52
+ $RootDir = (Resolve-Path "$ScriptDir\..").Path
53
+ $BuildDir = "$RootDir\build\runtime-win32-$TARGET_ARCH"
54
+ $DistDir = "$RootDir\dist"
55
+ $RuntimeDir = "$BuildDir\runtime"
56
+
57
+ $PgBin = "$PgRoot\bin"
58
+ $PgConfig = "$PgBin\pg_config.exe"
59
+
60
+ $OutputName = "mixdog-runtime-${TARGET_OS}-${TARGET_ARCH}-pg${PG_VERSION}-pgvector${PGVECTOR_VERSION}.tar.gz"
61
+
62
+ Write-Host "==> Using preinstalled PG: $PgRoot"
63
+ & $PgConfig --version
64
+ $RealVersion = (& $PgConfig --version) -replace 'PostgreSQL ', ''
65
+ Write-Host " pg_config reports version: $RealVersion"
66
+
67
+ if (Test-Path $RuntimeDir) { Remove-Item -Recurse -Force $RuntimeDir }
68
+ New-Item -ItemType Directory -Force -Path $BuildDir, $DistDir,
69
+ "$RuntimeDir\bin", "$RuntimeDir\lib", "$RuntimeDir\share" | Out-Null
70
+
71
+ Write-Host "==> Cloning pgvector $PGVECTOR_VERSION"
72
+ $PgVectorDir = "$BuildDir\pgvector"
73
+ $VectorDllBuilt = "$PgVectorDir\vector.dll"
74
+
75
+ if (Test-Path $VectorDllBuilt) {
76
+ Write-Host " Cache hit: vector.dll already built at $VectorDllBuilt"
77
+ } else {
78
+ if (Test-Path $PgVectorDir) { Remove-Item -Recurse -Force $PgVectorDir }
79
+ git clone --branch "v$PGVECTOR_VERSION" --depth 1 `
80
+ https://github.com/pgvector/pgvector.git $PgVectorDir
81
+
82
+ Write-Host "==> Building pgvector (MSVC/nmake against system PG 16)"
83
+ Push-Location $PgVectorDir
84
+ try {
85
+ $VsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
86
+ $VcVarsAll = & $VsWhere -latest -find 'VC\Auxiliary\Build\vcvarsall.bat' 2>$null | Select-Object -First 1
87
+ if (-not $VcVarsAll) {
88
+ Write-Error "vswhere could not locate vcvarsall.bat — Visual Studio Build Tools required."
89
+ exit 1
90
+ }
91
+ # CRITICAL: prepend $PgRoot\bin to PATH so any bare `pg_config` call
92
+ # inside Makefile.win resolves to PG 16 — runner has PG 14/15
93
+ # preinstalled and would otherwise win the PATH lookup, producing
94
+ # PG14-ABI vector.dll that fails to load in our PG 16 postgres.exe.
95
+ # Set in PowerShell so cmd /c inherits; setting inside the cmd batch
96
+ # via %PATH% loses vcvarsall's additions due to parse-time expansion.
97
+ $env:PATH = "$PgRoot\bin;$env:PATH"
98
+ $env:PGROOT = $PgRoot
99
+ $BuildCmd = "`"$VcVarsAll`" amd64 && nmake /F Makefile.win PG_CONFIG=`"$PgConfig`""
100
+ cmd /c $BuildCmd
101
+ if ($LASTEXITCODE -ne 0) {
102
+ Write-Error "pgvector nmake build failed (exit $LASTEXITCODE)"
103
+ exit 1
104
+ }
105
+ } finally {
106
+ Pop-Location
107
+ }
108
+ }
109
+
110
+ Write-Host "==> Assembling runtime layout — copy bin (.exe + .dll), lib, share from $PgRoot"
111
+ # Copy ALL .exe + .dll from PG bin so postgres.exe + libpq.dll + libcrypto/libssl/
112
+ # libintl/libiconv/icu*/libxml2/libxslt/libwinpthread/libecpg/libpgtypes etc. all
113
+ # ship together. PG 16 install layout puts these all in bin\.
114
+ Copy-Item "$PgBin\*.exe","$PgBin\*.dll" "$RuntimeDir\bin\" -Force
115
+
116
+ # lib\: PG extension modules (incl. contrib like pgcrypto.dll). vector.dll
117
+ # placed here below — PG looks for $libdir/<ext>.dll which resolves to lib\.
118
+ Copy-Item -Recurse -Force "$PgRoot\lib\*" "$RuntimeDir\lib\" -ErrorAction SilentlyContinue
119
+ # share\: extension SQL/control, locale, timezone data, conf samples.
120
+ Copy-Item -Recurse -Force "$PgRoot\share\*" "$RuntimeDir\share\" -ErrorAction SilentlyContinue
121
+
122
+ Write-Host "==> Manually staging pgvector artifacts (avoid pg_config-derived install paths)"
123
+ $RuntimeExtDir = "$RuntimeDir\share\extension"
124
+ New-Item -ItemType Directory -Force -Path $RuntimeExtDir | Out-Null
125
+ Copy-Item "$PgVectorDir\vector.dll" "$RuntimeDir\lib\" -Force
126
+ Copy-Item "$PgVectorDir\vector.control" $RuntimeExtDir -Force
127
+ Copy-Item "$PgVectorDir\sql\vector--*.sql" $RuntimeExtDir -Force
128
+
129
+ Write-Host "==> Asserting runtime layout"
130
+ $VectorControl = "$RuntimeDir\share\extension\vector.control"
131
+ if (-not (Test-Path $VectorControl)) { Write-Error "ASSERT FAILED: $VectorControl not found"; exit 1 }
132
+ $VectorSql = "$RuntimeDir\share\extension\vector--$PGVECTOR_VERSION.sql"
133
+ if (-not (Test-Path $VectorSql)) { Write-Error "ASSERT FAILED: $VectorSql not found"; exit 1 }
134
+ if (-not (Test-Path "$RuntimeDir\lib\vector.dll")) {
135
+ Write-Error "ASSERT FAILED: vector.dll not found in lib\"
136
+ exit 1
137
+ }
138
+ Write-Host " PASS runtime layout"
139
+
140
+ # Licenses
141
+ if (Test-Path "$PgRoot\doc\postgresql\COPYRIGHT") {
142
+ Copy-Item "$PgRoot\doc\postgresql\COPYRIGHT" "$RuntimeDir\LICENSE.postgresql" -Force
143
+ } elseif (Test-Path "$PgRoot\doc\COPYRIGHT") {
144
+ Copy-Item "$PgRoot\doc\COPYRIGHT" "$RuntimeDir\LICENSE.postgresql" -Force
145
+ }
146
+ if (Test-Path "$PgVectorDir\LICENSE") {
147
+ Copy-Item "$PgVectorDir\LICENSE" "$RuntimeDir\LICENSE.pgvector" -Force
148
+ }
149
+
150
+ Write-Host "==> Self-contained smoke test (initdb + CREATE EXTENSION vector + distance query)"
151
+ & "$RuntimeDir\bin\postgres.exe" --version
152
+ if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: postgres.exe --version exit $LASTEXITCODE"; exit 1 }
153
+
154
+ $SmokeData = "$BuildDir\smoke-pgdata"
155
+ $SmokeLog = "$BuildDir\smoke-pg.log"
156
+ $SmokePort = 55899
157
+ if (Test-Path $SmokeData) { Remove-Item -Recurse -Force $SmokeData }
158
+
159
+ & "$RuntimeDir\bin\initdb.exe" -D $SmokeData --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
160
+ if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: initdb"; exit 1 }
161
+
162
+ & "$RuntimeDir\bin\pg_ctl.exe" -D $SmokeData -o "-p $SmokePort -h 127.0.0.1" -l $SmokeLog -w start
163
+ if ($LASTEXITCODE -ne 0) { Write-Error "FAIL: pg_ctl start (see $SmokeLog)"; Get-Content $SmokeLog | Select-Object -Last 30; exit 1 }
164
+
165
+ try {
166
+ & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
167
+ if ($LASTEXITCODE -ne 0) { throw "CREATE EXTENSION vector failed" }
168
+ $ExtV = & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
169
+ $Dist = & "$RuntimeDir\bin\psql.exe" -h 127.0.0.1 -p $SmokePort -U postgres -d postgres -tAc "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector;"
170
+ Write-Host " vector extension version: $ExtV"
171
+ Write-Host " distance query result: $Dist"
172
+ if ($ExtV.Trim() -ne $PGVECTOR_VERSION) {
173
+ Write-Error "FAIL: extversion='$ExtV' expected='$PGVECTOR_VERSION'"
174
+ exit 1
175
+ }
176
+ Write-Host " PASS smoke (extension load + vector distance)"
177
+ }
178
+ finally {
179
+ & "$RuntimeDir\bin\pg_ctl.exe" -D $SmokeData -m fast stop 2>$null | Out-Null
180
+ Remove-Item -Recurse -Force $SmokeData -ErrorAction SilentlyContinue
181
+ }
182
+
183
+ Write-Host "==> Creating tarball: $OutputName"
184
+ $DistDirFwd = $DistDir.Replace('\', '/')
185
+ $RuntimeDirFwd = $RuntimeDir.Replace('\', '/')
186
+ & tar -czf "$DistDirFwd/$OutputName" -C "$RuntimeDirFwd" .
187
+ if ($LASTEXITCODE -ne 0) { Write-Error "tar failed (exit $LASTEXITCODE)"; exit 1 }
188
+
189
+ Write-Host "==> Generating sha256 sidecar"
190
+ Push-Location $DistDir
191
+ $Hash = (Get-FileHash -Algorithm SHA256 $OutputName).Hash.ToLower()
192
+ "$Hash $OutputName" | Out-File -Encoding ascii "${OutputName}.sha256"
193
+ Pop-Location
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # Phase A: Re-smoke from EXTRACTED tarball with hostile env (cleared PATH,
197
+ # only system32). Catches false-pass where the build host has VC redist /
198
+ # preinstalled DLLs that the tarball might be missing.
199
+ # ---------------------------------------------------------------------------
200
+ Write-Host "==> Re-smoke from extracted tarball (hostile env)"
201
+ $ExtractDir = "$BuildDir\extract-smoke"
202
+ if (Test-Path $ExtractDir) { Remove-Item -Recurse -Force $ExtractDir }
203
+ New-Item -ItemType Directory -Force -Path $ExtractDir | Out-Null
204
+ & tar -xzf "$DistDirFwd/$OutputName" -C ($ExtractDir.Replace('\','/'))
205
+ if ($LASTEXITCODE -ne 0) { Write-Error "extract failed"; exit 1 }
206
+
207
+ $ExtractData = "$ExtractDir\extract-pgdata"
208
+ $ExtractLog = "$ExtractDir\extract-pg.log"
209
+ $ExtractPort = 55898
210
+
211
+ # Snapshot current env, then strip to minimal Windows PATH (no PG14/15/16
212
+ # preinstalled bin, no chocolatey, no MSVC tools).
213
+ $SavedPath = $env:PATH
214
+ $SavedPgRoot = $env:PGROOT
215
+ $env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
216
+ $env:PGROOT = $null
217
+ $env:PGDATA = $null
218
+
219
+ try {
220
+ & "$ExtractDir\bin\postgres.exe" --version
221
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version under hostile env" }
222
+ & "$ExtractDir\bin\initdb.exe" -D $ExtractData --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
223
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb under hostile env" }
224
+ & "$ExtractDir\bin\pg_ctl.exe" -D $ExtractData -o "-p $ExtractPort -h 127.0.0.1" -l $ExtractLog -w start
225
+ if ($LASTEXITCODE -ne 0) { Get-Content $ExtractLog | Select-Object -Last 30; throw "FAIL: pg_ctl start under hostile env" }
226
+ try {
227
+ & "$ExtractDir\bin\psql.exe" -h 127.0.0.1 -p $ExtractPort -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
228
+ if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION vector under hostile env" }
229
+ $ExtV2 = & "$ExtractDir\bin\psql.exe" -h 127.0.0.1 -p $ExtractPort -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
230
+ if ($ExtV2.Trim() -ne $PGVECTOR_VERSION) { throw "FAIL: extracted-smoke extversion='$ExtV2'" }
231
+ Write-Host " PASS extracted-tarball smoke (hostile env)"
232
+ } finally {
233
+ & "$ExtractDir\bin\pg_ctl.exe" -D $ExtractData -m fast stop 2>$null | Out-Null
234
+ }
235
+ } finally {
236
+ $env:PATH = $SavedPath
237
+ $env:PGROOT = $SavedPgRoot
238
+ Remove-Item -Recurse -Force $ExtractDir -ErrorAction SilentlyContinue
239
+ }
240
+
241
+ Write-Host "==> Done: $DistDir\$OutputName"
242
+ Get-Item "$DistDir\$OutputName" | Select-Object Name, Length
@@ -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
+ }
@@ -88,4 +88,4 @@ for (const target of ['heavy-worker', 'worker']) {
88
88
  console.log(` single-call batches: ${batch1}/${batchTot} (${pct(batch1, batchTot)}%)`);
89
89
  console.log(` content greps with -C/-A/-B: ${grepContentWithCtx}/${grepContent} (${pct(grepContentWithCtx, grepContent)}%) grep->read follow-ups: ${grepThenRead}`);
90
90
  console.log(` same-path re-reads >=3x: ${rereads}`);
91
- }
91
+ }
@@ -1,3 +1,3 @@
1
- # Channels
2
-
3
- - Channel features are handled by the runtime.
1
+ # Channels
2
+
3
+ - Channel features are handled by the runtime.
@@ -40,7 +40,7 @@ import { spawn } from 'node:child_process';
40
40
  import * as nodeUtil from 'node:util';
41
41
  import { existsSync } from 'node:fs';
42
42
  import { randomUUID } from 'node:crypto';
43
- import { invalidateBuiltinResultCache, analyzeShellCommandEffects, preflightShellLargeFileProbe } from './builtin.mjs';
43
+ import { invalidateBuiltinResultCache, analyzeShellCommandEffects } from './builtin.mjs';
44
44
  import { markCodeGraphDirtyPaths, drainCodeGraphCache } from './code-graph-state.mjs';
45
45
  import { maybeRewriteWmicProcessCommand } from './shell-policy.mjs';
46
46
  import { _maybeEncodePowerShellCommand } from './shell-command.mjs';
@@ -51,13 +51,15 @@ import { startChildGuardian } from '../../../shared/child-guardian.mjs';
51
51
 
52
52
  globalThis.__mixdogBashSessionRuntimeLoaded = true;
53
53
 
54
- // Default 600 s (10 min), max 1800 s. Aligned with the one-shot bash tool's
55
- // 600 s default (builtin/bash-tool.mjs); the persistent shell carries
56
- // longer-running pipelines (build/test/install) than one-shot calls, so a
57
- // 120 s default left those workflows timing out at 2 min. Per-call `timeout`
58
- // still overrides, capped at MAX_TIMEOUT_MS.
59
- const DEFAULT_TIMEOUT_MS = 600_000;
60
- const MAX_TIMEOUT_MS = 1_800_000;
54
+ // Claude Code parity (refs/claude-code src/utils/timeouts.ts): default 120 s
55
+ // (2 min), max 600 s (10 min), BASH_DEFAULT_TIMEOUT_MS / BASH_MAX_TIMEOUT_MS
56
+ // env overrides (max floored at default). Matches the one-shot bash tool
57
+ // (builtin/bash-tool.mjs); per-call `timeout` still overrides, capped at
58
+ // MAX_TIMEOUT_MS.
59
+ const _envDefaultTimeout = parseInt(process.env.BASH_DEFAULT_TIMEOUT_MS ?? '', 10);
60
+ const DEFAULT_TIMEOUT_MS = _envDefaultTimeout > 0 ? _envDefaultTimeout : 120_000;
61
+ const _envMaxTimeout = parseInt(process.env.BASH_MAX_TIMEOUT_MS ?? '', 10);
62
+ const MAX_TIMEOUT_MS = Math.max(_envMaxTimeout > 0 ? _envMaxTimeout : 600_000, DEFAULT_TIMEOUT_MS);
61
63
  const IDLE_TIMEOUT_MS = 5 * 60_000;
62
64
  const MAX_SESSIONS = 10;
63
65
  const STDERR_DRAIN_MS = 25;
@@ -643,8 +645,6 @@ async function bash_session(args, cwd = process.cwd(), opts = {}) {
643
645
  }
644
646
  return requestedCwd;
645
647
  })();
646
- const largeProbe = await preflightShellLargeFileProbe(command, baseCwd);
647
- if (largeProbe) return `Error: ${largeProbe.message}`;
648
648
  const rawTimeout = typeof args?.timeout === 'number' ? args.timeout : DEFAULT_TIMEOUT_MS;
649
649
  const timeoutMs = rawTimeout;
650
650
  const effectiveTimeout = Math.min(Math.max(timeoutMs, 1), wmicRewrite?.timeoutMs || MAX_TIMEOUT_MS);
@@ -21,7 +21,6 @@ import {
21
21
  import {
22
22
  analyzeShellCommandEffects,
23
23
  foregroundLongCommandHint,
24
- preflightShellLargeFileProbe,
25
24
  } from './shell-analysis.mjs';
26
25
  import {
27
26
  cancelBackgroundTask,
@@ -201,8 +200,6 @@ export async function executeBashTool(args, workDir, options = {}) {
201
200
  return _execPolicyBlock;
202
201
  }
203
202
 
204
- const largeProbe = await preflightShellLargeFileProbe(command, bashWorkDir);
205
- if (largeProbe) return `Error: ${largeProbe.message}`;
206
203
  let shellEffects;
207
204
  try {
208
205
  shellEffects = await analyzeShellCommandEffects(command, bashWorkDir);
@@ -211,9 +208,14 @@ export async function executeBashTool(args, workDir, options = {}) {
211
208
  }
212
209
  // Keep foreground commands on a long tool-owned timeout. The MCP dispatch
213
210
  // layer must not add a shorter fallback ceiling when timeout is omitted.
214
- const DEFAULT_BASH_TIMEOUT_MS = 600_000;
215
- const DEFAULT_BACKGROUND_BASH_TIMEOUT_MS = 600_000;
216
- const MAX_BASH_TIMEOUT_MS = 1_800_000;
211
+ // Claude Code parity (refs/claude-code src/utils/timeouts.ts): default
212
+ // 120 s (2 min), max 600 s (10 min); BASH_DEFAULT_TIMEOUT_MS /
213
+ // BASH_MAX_TIMEOUT_MS env overrides, max floored at default.
214
+ const _envDefaultTimeout = parseInt(process.env.BASH_DEFAULT_TIMEOUT_MS ?? '', 10);
215
+ const DEFAULT_BASH_TIMEOUT_MS = _envDefaultTimeout > 0 ? _envDefaultTimeout : 120_000;
216
+ const DEFAULT_BACKGROUND_BASH_TIMEOUT_MS = DEFAULT_BASH_TIMEOUT_MS;
217
+ const _envMaxTimeout = parseInt(process.env.BASH_MAX_TIMEOUT_MS ?? '', 10);
218
+ const MAX_BASH_TIMEOUT_MS = Math.max(_envMaxTimeout > 0 ? _envMaxTimeout : 600_000, DEFAULT_BASH_TIMEOUT_MS);
217
219
  const defaultTimeoutMs = runInBackground
218
220
  ? DEFAULT_BACKGROUND_BASH_TIMEOUT_MS
219
221
  : DEFAULT_BASH_TIMEOUT_MS;
@@ -12,6 +12,19 @@ import {
12
12
  executionModeSchemaDescription,
13
13
  } from '../../../../shared/background-tasks.mjs';
14
14
 
15
+ // Shell timeout envelope surfaced in the tool schema. Mirrors Claude Code:
16
+ // default 120 s / max 600 s, BASH_DEFAULT_TIMEOUT_MS / BASH_MAX_TIMEOUT_MS
17
+ // env overrides, max floored at default. Keep in sync with
18
+ // builtin/bash-tool.mjs and bash-session.mjs.
19
+ function _shellDefaultTimeoutMs() {
20
+ const parsed = parseInt(process.env.BASH_DEFAULT_TIMEOUT_MS ?? '', 10);
21
+ return parsed > 0 ? parsed : 120_000;
22
+ }
23
+ function _shellMaxTimeoutMs() {
24
+ const parsed = parseInt(process.env.BASH_MAX_TIMEOUT_MS ?? '', 10);
25
+ return Math.max(parsed > 0 ? parsed : 600_000, _shellDefaultTimeoutMs());
26
+ }
27
+
15
28
  export const BUILTIN_TOOLS = [
16
29
  {
17
30
  name: 'read',
@@ -59,7 +72,7 @@ export const BUILTIN_TOOLS = [
59
72
  properties: {
60
73
  command: { type: 'string', description: 'Command.' },
61
74
  cwd: { type: 'string', description: 'Working directory. Persists across shell calls in a session — omit to reuse the previous command\'s directory (e.g. after cd); pass an absolute path to change it.' },
62
- timeout: { type: 'number', description: 'Timeout ms.' },
75
+ timeout: { type: 'number', description: `Timeout ms. Default ${_shellDefaultTimeoutMs()} (${_shellDefaultTimeoutMs() / 60000} min); long-running commands may raise it up to ${_shellMaxTimeoutMs()} (${_shellMaxTimeoutMs() / 60000} min).` },
63
76
  merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
64
77
  mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
65
78
  shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows defaults to PowerShell; bash = Git Bash/POSIX.' },
@@ -25,7 +25,7 @@ import {
25
25
  } from './paths.mjs';
26
26
  import { ensureNativePatchBinaryAvailable } from './native-server.mjs';
27
27
  import { assertPathReachable } from '../builtin/fs-reachability.mjs';
28
- import { dispatchNativePatch } from './dispatch.mjs';
28
+ import { dispatchNativePatch, dispatchJsPatchEntries } from './dispatch.mjs';
29
29
  import { normalizeOutputPath } from '../builtin.mjs';
30
30
  import {
31
31
  planV4ARenameSections,
@@ -242,17 +242,12 @@ async function apply_patch(args, cwd, options = {}) {
242
242
  }
243
243
  const insideEntries = entries.filter((entry) => !isResolvedPathOutsideBase(entry.fullPath, basePath));
244
244
  const outsideEntries = entries.filter((entry) => isResolvedPathOutsideBase(entry.fullPath, basePath));
245
- if (outsideEntries.length > 0) {
246
- const rels = outsideEntries.map((e) => normalizeOutputPath(e.fullPath || e.displayPath || '(unknown)')).join(', ');
247
- return wrapPatchMutationOutput(
248
- `Error: apply_patch target resolves outside base path (../ or absolute not allowed): ${rels}`,
249
- mutationPlan,
250
- { backend: 'native-patch' },
251
- );
252
- }
253
245
  const parsedInside = (parsed || []).filter(
254
246
  (entry) => !isResolvedPathOutsideBase(parsedEntryResolvedPath(entry, basePath), basePath),
255
247
  );
248
+ const backend = outsideEntries.length > 0
249
+ ? (insideEntries.length > 0 ? 'native+js-patch' : 'js-patch')
250
+ : 'native-patch';
256
251
  const resultParts = [];
257
252
  if (insideEntries.length > 0) {
258
253
  const nativePatchStr = rewriteHeaderPaths(renderParsedUnifiedPatch(parsedInside), headerRewrites);
@@ -268,11 +263,26 @@ async function apply_patch(args, cwd, options = {}) {
268
263
  parsed: parsedInside,
269
264
  });
270
265
  if (isPatchErrorText(nativeResult)) {
271
- if (resultParts.length > 0) return wrapPatchMutationOutput(nativeResult, mutationPlan, { backend: 'native-patch' });
272
- return wrapPatchMutationOutput(nativeResult, mutationPlan, { backend: 'native-patch' });
266
+ return wrapPatchMutationOutput(nativeResult, mutationPlan, { backend });
273
267
  }
274
268
  resultParts.push(nativeResult);
275
269
  }
270
+ if (outsideEntries.length > 0) {
271
+ // Out-of-base targets are applied via the JS dispatcher (no base-path
272
+ // confinement); write permission is enforced at the hook layer.
273
+ const jsResult = await dispatchJsPatchEntries({
274
+ rows: outsideEntries,
275
+ parsed,
276
+ basePath,
277
+ dryRun,
278
+ fuzzy,
279
+ readStateScope,
280
+ });
281
+ if (isPatchErrorText(jsResult)) {
282
+ return wrapPatchMutationOutput(jsResult, mutationPlan, { backend });
283
+ }
284
+ resultParts.push(jsResult);
285
+ }
276
286
  let combined = resultParts.join('\n');
277
287
  const renameLines = formatV4ARenameSuccessLines(v4aRenameResults);
278
288
  if (renameLines.length > 0 && !isPatchErrorText(combined)) {
@@ -287,9 +297,9 @@ async function apply_patch(args, cwd, options = {}) {
287
297
  `hunk-level rejected (rejectPartial=false, V4A): ${rejectedV4AHunks.length}`,
288
298
  ...rejectedV4AHunks.map((r) => ` REJECT ${r.file || '(unknown)'} — ${String(r.reason || '').split(';')[0].trim()}`),
289
299
  ];
290
- return wrapPatchMutationOutput(`${combined}\n${tail.join('\n')}`, mutationPlan, { backend: 'native-patch' });
300
+ return wrapPatchMutationOutput(`${combined}\n${tail.join('\n')}`, mutationPlan, { backend });
291
301
  }
292
- return wrapPatchMutationOutput(combined, mutationPlan, { backend: 'native-patch' });
302
+ return wrapPatchMutationOutput(combined, mutationPlan, { backend });
293
303
  }));
294
304
  }
295
305
 
@@ -278,8 +278,24 @@ function _reclaimGuardMatchesToken(guardPath, token) {
278
278
  }
279
279
  }
280
280
 
281
- function _unlinkReclaimGuardIfPidMatches(guardPath, pid) {
282
- if (_readLockOwnerPid(guardPath) !== pid) return false;
281
+ function _readGuardContent(guardPath) {
282
+ try { return readFileSync(guardPath, 'utf8'); } catch { return null; }
283
+ }
284
+
285
+ function _parseStampPid(raw) {
286
+ if (raw === null) return null;
287
+ const tok = String(raw).trim().split(/\s+/)[0];
288
+ const pid = Number.parseInt(tok, 10);
289
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
290
+ }
291
+
292
+ // Unlink the guard only if its FULL content (pid ts token) is still the
293
+ // exact content the stale/dead decision was made against. A regenerated
294
+ // guard — even one whose pid was recycled to the same value (same-pid
295
+ // ABA) — carries a fresh ts+token and therefore never matches, so a
296
+ // fresh guard is never removed by a stale decision.
297
+ function _unlinkReclaimGuardIfContentMatches(guardPath, content) {
298
+ if (_readGuardContent(guardPath) !== content) return false;
283
299
  try { unlinkSync(guardPath); return true; } catch { return false; }
284
300
  }
285
301
 
@@ -289,10 +305,12 @@ function _unlinkUnreadableStaleReclaimGuard(guardPath, staleMs) {
289
305
  }
290
306
 
291
307
  function _tryClearStaleReclaimGuard(guardPath, staleMs) {
292
- const guardPid = _readLockOwnerPid(guardPath);
308
+ const guardContent = _readGuardContent(guardPath);
309
+ const guardPid = _parseStampPid(guardContent);
293
310
  if (guardPid !== null) {
294
- // Re-read the guard immediately before unlinking so a regenerated
295
- // guard with a different owner is not removed by a stale decision.
311
+ // Re-read the guard's full content immediately before unlinking so a
312
+ // regenerated guard (different owner OR same-pid ABA) is not removed
313
+ // by a stale decision.
296
314
  // This is still not a serialized primitive: correctness comes from
297
315
  // the lockPath owner-token reverify before unlinking lockPath, not
298
316
  // from making guard cleanup itself authoritative.
@@ -304,7 +322,7 @@ function _tryClearStaleReclaimGuard(guardPath, staleMs) {
304
322
  let guardStale = false;
305
323
  try { guardStale = Date.now() - statSync(guardPath).mtimeMs > staleMs; } catch {}
306
324
  if (_pidIsDead(guardPid) || guardStale) {
307
- _unlinkReclaimGuardIfPidMatches(guardPath, guardPid);
325
+ _unlinkReclaimGuardIfContentMatches(guardPath, guardContent);
308
326
  }
309
327
  return;
310
328
  }
@@ -13927,8 +13927,21 @@ function _reclaimGuardMatchesToken(guardPath, token) {
13927
13927
  return false;
13928
13928
  }
13929
13929
  }
13930
- function _unlinkReclaimGuardIfPidMatches(guardPath, pid) {
13931
- if (_readLockOwnerPid(guardPath) !== pid) return false;
13930
+ function _readGuardContent(guardPath) {
13931
+ try {
13932
+ return readFileSync2(guardPath, "utf8");
13933
+ } catch {
13934
+ return null;
13935
+ }
13936
+ }
13937
+ function _parseStampPid(raw) {
13938
+ if (raw === null) return null;
13939
+ const tok = String(raw).trim().split(/\s+/)[0];
13940
+ const pid = Number.parseInt(tok, 10);
13941
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
13942
+ }
13943
+ function _unlinkReclaimGuardIfContentMatches(guardPath, content) {
13944
+ if (_readGuardContent(guardPath) !== content) return false;
13932
13945
  try {
13933
13946
  unlinkSync2(guardPath);
13934
13947
  return true;
@@ -13946,7 +13959,8 @@ function _unlinkUnreadableStaleReclaimGuard(guardPath, staleMs) {
13946
13959
  }
13947
13960
  }
13948
13961
  function _tryClearStaleReclaimGuard(guardPath, staleMs) {
13949
- const guardPid = _readLockOwnerPid(guardPath);
13962
+ const guardContent = _readGuardContent(guardPath);
13963
+ const guardPid = _parseStampPid(guardContent);
13950
13964
  if (guardPid !== null) {
13951
13965
  let guardStale = false;
13952
13966
  try {
@@ -13954,7 +13968,7 @@ function _tryClearStaleReclaimGuard(guardPath, staleMs) {
13954
13968
  } catch {
13955
13969
  }
13956
13970
  if (_pidIsDead(guardPid) || guardStale) {
13957
- _unlinkReclaimGuardIfPidMatches(guardPath, guardPid);
13971
+ _unlinkReclaimGuardIfContentMatches(guardPath, guardContent);
13958
13972
  }
13959
13973
  return;
13960
13974
  }