codebuff 1.0.685 → 1.0.686

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.
Files changed (3) hide show
  1. package/index.js +1 -0
  2. package/launcher.js +372 -120
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -21,6 +21,7 @@ const { createLauncher } = require(
21
21
  const launcher = createLauncher({
22
22
  packageName: 'codebuff',
23
23
  displayName: 'Codebuff',
24
+ wrapperVersion: require('./package.json').version,
24
25
  tempDownloadDirName: '.download-temp',
25
26
  })
26
27
 
package/launcher.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn, execFileSync } = require('child_process')
3
+ const { spawn } = require('child_process')
4
4
  const fs = require('fs')
5
5
  const http = require('http')
6
6
  const https = require('https')
@@ -16,11 +16,15 @@ function createLauncher(productConfig) {
16
16
  const {
17
17
  packageName,
18
18
  displayName,
19
+ wrapperVersion = null,
19
20
  includeTreeSitterWasm = true,
20
21
  startupBanner = [],
21
22
  telemetryEvent = 'cli.update_codebuff_failed',
22
23
  telemetryProperties = {},
23
24
  tempDownloadDirName = `.${packageName}-download-temp`,
25
+ // Tests only. os.homedir() ignores $HOME under `bun test`, so pointing HOME
26
+ // at a temp dir is not enough to keep a test off the real ~/.config.
27
+ configDir: configDirOverride = null,
24
28
  } = productConfig
25
29
 
26
30
  /**
@@ -68,6 +72,16 @@ function createLauncher(productConfig) {
68
72
  }
69
73
  }
70
74
 
75
+ /**
76
+ * How long a binary has to survive before a crash stops looking like a
77
+ * startup failure. Used to bound the STATUS_STACK_BUFFER_OVERRUN heuristic
78
+ * below, which is only trustworthy for deaths during startup.
79
+ */
80
+ const STARTUP_CRASH_WINDOW_MS = 10000
81
+
82
+ /** Bytes of the binary's stderr kept for the crash report. */
83
+ const STDERR_TAIL_BYTES = 8192
84
+
71
85
  function getUnsignedExitCode(code) {
72
86
  return code != null && code < 0 ? code >>> 0 : code
73
87
  }
@@ -94,9 +108,38 @@ function createLauncher(productConfig) {
94
108
  )
95
109
  }
96
110
 
111
+ /**
112
+ * A startup death that is probably this machine failing to run the optimized
113
+ * build, reported under the *other* Windows spelling.
114
+ *
115
+ * Bun is written in Zig, and a Zig panic on Windows reports itself with
116
+ * __fastfail(FAST_FAIL_FATAL_APP_EXIT) — NTSTATUS 0xC0000409
117
+ * (STATUS_STACK_BUFFER_OVERRUN, exit code 3221226505), not
118
+ * STATUS_ILLEGAL_INSTRUCTION. So a CPU without AVX2 does not reliably die on
119
+ * the illegal instruction itself: Bun detects the missing feature, then
120
+ * panics while starting up anyway (oven-sh/bun#28399), and the crash arrives
121
+ * as 0xC0000409. Only the SIGILL spelling was wired to the baseline
122
+ * fallback, which is why these machines crash-looped forever.
123
+ *
124
+ * On its own 0xC0000409 only means "the binary aborted", so this is bounded
125
+ * to deaths during startup. A panic ten minutes into a session is an
126
+ * ordinary bug, and reading it as a missing instruction set would send a
127
+ * perfectly capable machine to the slower build.
128
+ */
129
+ function isStartupCpuFeatureCrash(code, signal, msAlive) {
130
+ return (
131
+ process.platform === 'win32' &&
132
+ getUnsignedExitCode(code) === 0xc0000409 &&
133
+ !signal &&
134
+ typeof msAlive === 'number' &&
135
+ msAlive < STARTUP_CRASH_WINDOW_MS
136
+ )
137
+ }
138
+
97
139
  function createConfig(packageName) {
98
140
  const homeDir = os.homedir()
99
- const configDir = path.join(homeDir, '.config', 'manicode')
141
+ const configDir =
142
+ configDirOverride || path.join(homeDir, '.config', 'manicode')
100
143
  const binaryName =
101
144
  process.platform === 'win32' ? `${packageName}.exe` : packageName
102
145
 
@@ -245,35 +288,6 @@ function createLauncher(productConfig) {
245
288
  }
246
289
  }
247
290
 
248
- // Returns true (AVX2 present), false (absent), or null (couldn't determine).
249
- // Ask the OS directly via IsProcessorFeaturePresent (kernel32), which is
250
- // backed by CPUID — far more reliable than matching CPU model names, and it
251
- // works on the stock Windows PowerShell that ships with every supported
252
- // Windows version. Feature 40 = PF_AVX2_INSTRUCTIONS_AVAILABLE.
253
- function probeWindowsAvx2() {
254
- const script =
255
- '$f = Add-Type -MemberDefinition \'[DllImport("kernel32.dll")] ' +
256
- "public static extern bool IsProcessorFeaturePresent(uint feature);' " +
257
- '-Name Cpu -Namespace Win32 -PassThru; $f::IsProcessorFeaturePresent(40)'
258
- try {
259
- const out = execFileSync(
260
- 'powershell.exe',
261
- ['-NoProfile', '-NonInteractive', '-Command', script],
262
- {
263
- encoding: 'utf8',
264
- timeout: 5000,
265
- stdio: ['ignore', 'pipe', 'ignore'],
266
- },
267
- ).trim()
268
- if (out === 'True') return true
269
- if (out === 'False') return false
270
- return null
271
- } catch {
272
- // No PowerShell, locked-down policy, timeout, etc. — inconclusive.
273
- return null
274
- }
275
- }
276
-
277
291
  let _hasAvx2Cache
278
292
 
279
293
  function machineHasAvx2() {
@@ -288,31 +302,41 @@ function createLauncher(productConfig) {
288
302
  return true
289
303
  }
290
304
 
291
- // Linux detection is a cheap file read, so we don't bother persisting it.
305
+ // A recorded illegal-instruction crash outranks everything below it: the
306
+ // binary actually failed on this machine, which beats any inference we
307
+ // could make about the CPU.
308
+ const recorded = readCachedAvx2()
309
+ if (recorded !== null) {
310
+ return recorded
311
+ }
312
+
313
+ // Linux can just ask, and a file read is cheap enough not to cache.
292
314
  if (process.platform === 'linux') {
293
315
  return linuxCpuHasAvx2()
294
316
  }
295
317
 
296
- if (process.platform !== 'win32') {
297
- return true
298
- }
318
+ // Everything else assumes AVX2 — true of every x64 CPU since ~2013.
319
+ //
320
+ // Windows is the case that matters, since it's the only other platform with
321
+ // a baseline build. It has no /proc/cpuinfo equivalent we can read without
322
+ // spawning something, and the probe that used to fill the gap asked
323
+ // PowerShell to compile a C# stub and P/Invoke
324
+ // kernel32!IsProcessorFeaturePresent — accurate, but a textbook malware
325
+ // shape that Windows Defender flagged as a "Suspicious PowerShell command
326
+ // line" on real user machines. The illegal-instruction handler corrects us
327
+ // instead: tryFallbackToBaseline() calls recordMachineLacksAvx2(), so a
328
+ // machine without AVX2 pays exactly one failed launch and is answered by
329
+ // the cache read above from then on.
330
+ return true
331
+ }
299
332
 
300
- // Windows detection shells out to PowerShell. getDefaultTargetKey runs on
301
- // every launch (via the version check), so cache the result on disk to keep
302
- // startup fast after the first probe.
303
- const cached = readCachedAvx2()
304
- if (cached !== null) {
305
- return cached
306
- }
307
- const detected = probeWindowsAvx2()
308
- if (detected === null) {
309
- // Inconclusive probe: assume AVX2 for this launch and rely on the SIGILL
310
- // fallback, but don't persist it — a transient failure must not lock in a
311
- // wrong answer for the lifetime of the install. We'll re-probe next launch.
312
- return true
313
- }
314
- writeCachedAvx2(detected)
315
- return detected
333
+ // Called from the illegal-instruction fallback. Persisting here is what keeps
334
+ // the optimistic assumption above from costing more than a single crash and
335
+ // it is deliberately separate from the metadata target, so a lost or rewritten
336
+ // metadata file can't resurrect the AVX2 build on a CPU that can't run it.
337
+ function recordMachineLacksAvx2() {
338
+ _hasAvx2Cache = false
339
+ writeCachedAvx2(false)
316
340
  }
317
341
 
318
342
  function getCpuFeatureCachePath() {
@@ -349,11 +373,12 @@ function createLauncher(productConfig) {
349
373
  }
350
374
 
351
375
  const platformKey = getPlatformKey()
352
- // Select the binary up front from explicit CPU feature detection rather than
353
- // optimistically launching the AVX2 build and waiting for it to crash with
354
- // an illegal instruction. The crash isn't always a clean immediate failure —
355
- // it can surface later from a deeper code path — so older CPUs (e.g. an
356
- // Intel Xeon with AVX but no AVX2) are safer on baseline from the start.
376
+ // Linux still detects up front (reading /proc/cpuinfo is free). Windows
377
+ // cannot without spawning a process, and the PowerShell probe that used to
378
+ // do it tripped Defender, so Windows is optimistic-then-corrected: see
379
+ // detectMachineHasAvx2() and tryFallbackToBaseline(). Once a machine has
380
+ // failed once, readCachedAvx2() answers here and baseline is chosen up front
381
+ // exactly as it used to be.
357
382
  //
358
383
  // This assumes every baseline target is gated on AVX2 specifically, which
359
384
  // holds today (only linux-x64 and win32-x64 have baseline builds, both
@@ -439,12 +464,18 @@ function createLauncher(productConfig) {
439
464
  if (!isTargetAllowedForThisMachine(metadataTarget)) {
440
465
  return null
441
466
  }
442
- return metadata.version || null
467
+ return getMetadataVersion(metadata)
443
468
  } catch (error) {
444
469
  return null
445
470
  }
446
471
  }
447
472
 
473
+ function getMetadataVersion(metadata) {
474
+ return typeof metadata?.version === 'string' && metadata.version
475
+ ? metadata.version
476
+ : null
477
+ }
478
+
448
479
  function getCurrentMetadata() {
449
480
  try {
450
481
  if (!fs.existsSync(CONFIG.metadataPath)) {
@@ -456,68 +487,63 @@ function createLauncher(productConfig) {
456
487
  }
457
488
  }
458
489
 
459
- function compareVersions(v1, v2) {
460
- if (!v1 || !v2) return 0
490
+ function parseVersion(version) {
491
+ if (typeof version !== 'string') return null
461
492
 
462
- // Always update if the current version is not a valid semver
463
- // e.g. 1.0.420-beta.1
464
- if (!v1.match(/^\d+(\.\d+)*$/)) {
465
- return -1
466
- }
493
+ const match = version.match(
494
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/,
495
+ )
496
+ if (!match) return null
497
+
498
+ const prerelease = match[4]?.split('.') ?? []
499
+ if (prerelease.some((part) => /^0\d+$/.test(part))) return null
467
500
 
468
- const parseVersion = (version) => {
469
- const parts = version.split('-')
470
- const mainParts = parts[0].split('.').map(Number)
471
- const prereleaseParts = parts[1] ? parts[1].split('.') : []
472
- return { main: mainParts, prerelease: prereleaseParts }
501
+ return {
502
+ main: match.slice(1, 4).map(BigInt),
503
+ prerelease,
473
504
  }
505
+ }
474
506
 
507
+ function compareVersions(v1, v2) {
475
508
  const p1 = parseVersion(v1)
476
509
  const p2 = parseVersion(v2)
477
510
 
478
- for (let i = 0; i < Math.max(p1.main.length, p2.main.length); i++) {
479
- const n1 = p1.main[i] || 0
480
- const n2 = p2.main[i] || 0
511
+ // Published package versions are valid semver. Treat malformed cached
512
+ // metadata as stale so the wrapper repairs it instead of trusting it.
513
+ if (!p1) return -1
514
+ if (!p2) return 1
481
515
 
482
- if (n1 < n2) return -1
483
- if (n1 > n2) return 1
516
+ for (let i = 0; i < p1.main.length; i++) {
517
+ if (p1.main[i] < p2.main[i]) return -1
518
+ if (p1.main[i] > p2.main[i]) return 1
484
519
  }
485
520
 
486
- if (p1.prerelease.length === 0 && p2.prerelease.length === 0) {
487
- return 0
488
- } else if (p1.prerelease.length === 0) {
489
- return 1
490
- } else if (p2.prerelease.length === 0) {
491
- return -1
492
- } else {
493
- for (
494
- let i = 0;
495
- i < Math.max(p1.prerelease.length, p2.prerelease.length);
496
- i++
497
- ) {
498
- const pr1 = p1.prerelease[i] || ''
499
- const pr2 = p2.prerelease[i] || ''
500
-
501
- const isNum1 = !isNaN(parseInt(pr1))
502
- const isNum2 = !isNaN(parseInt(pr2))
503
-
504
- if (isNum1 && isNum2) {
505
- const num1 = parseInt(pr1)
506
- const num2 = parseInt(pr2)
507
- if (num1 < num2) return -1
508
- if (num1 > num2) return 1
509
- } else if (isNum1 && !isNum2) {
510
- return 1
511
- } else if (!isNum1 && isNum2) {
512
- return -1
513
- } else if (pr1 < pr2) {
514
- return -1
515
- } else if (pr1 > pr2) {
516
- return 1
517
- }
521
+ if (p1.prerelease.length === 0) {
522
+ return p2.prerelease.length === 0 ? 0 : 1
523
+ }
524
+ if (p2.prerelease.length === 0) return -1
525
+
526
+ for (
527
+ let i = 0;
528
+ i < Math.max(p1.prerelease.length, p2.prerelease.length);
529
+ i++
530
+ ) {
531
+ const identifier1 = p1.prerelease[i]
532
+ const identifier2 = p2.prerelease[i]
533
+ if (identifier1 === undefined) return -1
534
+ if (identifier2 === undefined) return 1
535
+ if (identifier1 === identifier2) continue
536
+
537
+ const numeric1 = /^\d+$/.test(identifier1)
538
+ const numeric2 = /^\d+$/.test(identifier2)
539
+ if (numeric1 && numeric2) {
540
+ return BigInt(identifier1) < BigInt(identifier2) ? -1 : 1
518
541
  }
519
- return 0
542
+ if (numeric1 !== numeric2) return numeric1 ? -1 : 1
543
+ return identifier1 < identifier2 ? -1 : 1
520
544
  }
545
+
546
+ return 0
521
547
  }
522
548
 
523
549
  function formatBytes(bytes) {
@@ -860,13 +886,31 @@ function createLauncher(productConfig) {
860
886
  installStagedBinary(stagedBinary)
861
887
  }
862
888
 
863
- async function ensureBinaryExists() {
889
+ function getRequiredWrapperVersion(currentVersion) {
890
+ if (
891
+ !wrapperVersion ||
892
+ (currentVersion !== null &&
893
+ compareVersions(currentVersion, wrapperVersion) >= 0)
894
+ ) {
895
+ return null
896
+ }
897
+ return wrapperVersion
898
+ }
899
+
900
+ async function ensureBinaryReady() {
864
901
  const currentVersion = getCurrentVersion()
865
- if (currentVersion !== null) {
902
+ const requiredWrapperVersion = getRequiredWrapperVersion(currentVersion)
903
+
904
+ if (currentVersion !== null && requiredWrapperVersion === null) {
866
905
  return
867
906
  }
868
907
 
869
- const version = await getLatestVersion()
908
+ // npm installs update this JavaScript wrapper but intentionally preserve the
909
+ // downloaded binary. If that binary exits before the background update
910
+ // check starts, it can otherwise remain stuck forever. The wrapper and its
911
+ // release binary share a version, so repair that stale cache synchronously
912
+ // without adding a registry lookup to healthy launches.
913
+ const version = requiredWrapperVersion ?? (await getLatestVersion())
870
914
  if (!version) {
871
915
  console.error('❌ Failed to determine latest version')
872
916
  console.error('Please check your internet connection and try again')
@@ -878,6 +922,12 @@ function createLauncher(productConfig) {
878
922
  } catch (error) {
879
923
  term.clearLine()
880
924
  printDownloadFailure(error)
925
+ if (currentVersion !== null) {
926
+ console.error(
927
+ `Continuing with cached ${packageName} ${currentVersion}.`,
928
+ )
929
+ return
930
+ }
881
931
  process.exit(1)
882
932
  }
883
933
  }
@@ -921,6 +971,16 @@ function createLauncher(productConfig) {
921
971
  }
922
972
 
923
973
  async function checkForUpdates(runningProcess, exitListener) {
974
+ // main() schedules this 100ms after launch, so the binary it was handed can
975
+ // already be dead — a startup crash that handed off to the baseline
976
+ // fallback, most of all. Updating around a corpse would race that
977
+ // relaunch's download for the shared temp directory (prepareTempDownloadDir
978
+ // rmSyncs it) and then spend six seconds SIGKILLing a process that has
979
+ // already exited.
980
+ if (runningProcess.exitCode !== null || runningProcess.signalCode !== null) {
981
+ return
982
+ }
983
+
924
984
  let stoppedForUpdate = false
925
985
 
926
986
  try {
@@ -979,7 +1039,32 @@ function createLauncher(productConfig) {
979
1039
  }
980
1040
  }
981
1041
 
982
- function printCrashDiagnostics(code, signal) {
1042
+ /**
1043
+ * Make captured output safe to print back to the terminal.
1044
+ *
1045
+ * The tail is replayed *after* resetTerminal() has put the terminal back in
1046
+ * order, so it must not be able to undo that: a stray \x1b[?1049h or
1047
+ * \x1b[?1003h in what the binary printed would re-enter the alternate screen
1048
+ * or re-enable mouse reporting, hiding the very report it is part of. Strip
1049
+ * the escapes and keep the words.
1050
+ */
1051
+ function sanitizeForReplay(text) {
1052
+ if (!text) return ''
1053
+ return (
1054
+ text
1055
+ .replace(/\r\n?/g, '\n')
1056
+ // CSI sequences — the ones that actually change terminal state.
1057
+ .replace(/\x1b\[[0-9;:?<>=]*[ -/]*[@-~]/g, '')
1058
+ // Everything else: strip the control bytes and keep the text. An OSC
1059
+ // or a lone escape loses its introducer and degrades to inert
1060
+ // characters, which is all a crash report needs it to be.
1061
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
1062
+ .replace(/\s+$/, '')
1063
+ )
1064
+ }
1065
+
1066
+ function printCrashDiagnostics(code, signal, context = {}) {
1067
+ const { msAlive = null, stderrTail = '' } = context
983
1068
  // Windows NTSTATUS codes (unsigned DWORD)
984
1069
  const unsignedCode = getUnsignedExitCode(code)
985
1070
  const isIllegalInstruction = isIllegalInstructionExit(code, signal)
@@ -1014,11 +1099,46 @@ function createLauncher(productConfig) {
1014
1099
  console.error('This may indicate a platform compatibility issue.')
1015
1100
  console.error('')
1016
1101
  } else if (isAbort) {
1017
- console.error('The binary crashed with an abort signal.')
1102
+ const startupCpuCrash = isStartupCpuFeatureCrash(code, signal, msAlive)
1103
+ console.error(
1104
+ startupCpuCrash
1105
+ ? 'The binary aborted while starting up.'
1106
+ : 'The binary crashed with an abort signal.',
1107
+ )
1018
1108
  console.error('')
1109
+ // Reached only once the baseline fallback has declined or failed, so name
1110
+ // the case the user can still act on instead of leaving them with an
1111
+ // abort code and a bug-tracker link.
1112
+ if (startupCpuCrash) {
1113
+ if (getCurrentMetadata()?.target === getBaselineFallbackTargetKey()) {
1114
+ console.error(
1115
+ 'This is already the older-CPU (baseline) build, so a missing AVX2',
1116
+ )
1117
+ console.error(
1118
+ 'instruction set is not the whole story — please include the output',
1119
+ )
1120
+ console.error('below in your report.')
1121
+ console.error('')
1122
+ } else {
1123
+ console.error(
1124
+ 'On x64 Windows this is usually a CPU without AVX2 support, which',
1125
+ )
1126
+ console.error('the standard build requires.')
1127
+ console.error('')
1128
+ printBaselineOverrideHint()
1129
+ }
1130
+ }
1019
1131
  }
1020
1132
 
1021
1133
  printSystemInfo()
1134
+ const tailText = sanitizeForReplay(stderrTail)
1135
+ if (tailText) {
1136
+ console.error('')
1137
+ console.error(`Last output from ${packageName}:`)
1138
+ for (const line of tailText.split('\n')) {
1139
+ console.error(` ${line}`)
1140
+ }
1141
+ }
1022
1142
  console.error('')
1023
1143
  console.error('Please report this issue at:')
1024
1144
  console.error(' https://github.com/CodebuffAI/codebuff/issues')
@@ -1035,13 +1155,28 @@ function createLauncher(productConfig) {
1035
1155
  console.error('')
1036
1156
  }
1037
1157
 
1158
+ /**
1159
+ * What we actually know about AVX2, not what we assume.
1160
+ *
1161
+ * The old line printed a flat "yes" from machineHasAvx2(), which on Windows
1162
+ * is an optimistic default and not a measurement (see detectMachineHasAvx2).
1163
+ * Every crash report that reached us therefore claimed AVX2 was present on
1164
+ * machines we had never asked, which is exactly the evidence that would have
1165
+ * pointed at the CPU.
1166
+ */
1167
+ function describeAvx2Support() {
1168
+ if (readCachedAvx2() === false) return 'no (recorded crash)'
1169
+ if (process.platform === 'linux') return machineHasAvx2() ? 'yes' : 'no'
1170
+ return 'not checked (assumed present)'
1171
+ }
1172
+
1038
1173
  function printSystemInfo() {
1039
1174
  const metadata = getCurrentMetadata()
1040
1175
  console.error('System info:')
1041
1176
  console.error(` Platform: ${process.platform} ${process.arch}`)
1042
1177
  console.error(` Node: ${process.version}`)
1043
1178
  if (process.arch === 'x64') {
1044
- console.error(` AVX2: ${machineHasAvx2() ? 'yes' : 'no'}`)
1179
+ console.error(` AVX2: ${describeAvx2Support()}`)
1045
1180
  }
1046
1181
  console.error(` Target: ${metadata?.target || getDefaultTargetKey()}`)
1047
1182
  console.error(` Binary: ${CONFIG.binaryPath}`)
@@ -1112,7 +1247,12 @@ function createLauncher(productConfig) {
1112
1247
  try {
1113
1248
  const { env: optionEnv, ...spawnOptions } = options
1114
1249
  child = spawn(CONFIG.binaryPath, process.argv.slice(2), {
1115
- stdio: 'inherit',
1250
+ // stdin/stdout stay inherited — the TUI owns the terminal and must see
1251
+ // a real tty. stderr is teed on Windows only; see watchLaunch.
1252
+ stdio:
1253
+ process.platform === 'win32'
1254
+ ? ['inherit', 'inherit', 'pipe']
1255
+ : ['inherit', 'inherit', 'inherit'],
1116
1256
  ...spawnOptions,
1117
1257
  env: {
1118
1258
  ...process.env,
@@ -1125,12 +1265,72 @@ function createLauncher(productConfig) {
1125
1265
  }
1126
1266
 
1127
1267
  child.on('error', exitOnSpawnFailure)
1268
+ child.launch = watchLaunch(child)
1128
1269
 
1129
1270
  return child
1130
1271
  }
1131
1272
 
1132
- async function tryFallbackToBaseline(code, signal) {
1133
- if (!isIllegalInstructionExit(code, signal)) {
1273
+ /**
1274
+ * Watch a launch so its death can be explained: how long the binary lived,
1275
+ * and what it printed on the way out.
1276
+ *
1277
+ * The stderr tee is what makes a crash report self-contained. When the binary
1278
+ * dies natively we reset the terminal, and that reset leaves the alternate
1279
+ * screen with \x1b[?1049l — discarding its contents, including any panic Bun
1280
+ * printed there. (The CLI's own terminal watchdog sends the same sequence, so
1281
+ * dropping it here wouldn't help.) Every report of codebuff#792 is a launcher
1282
+ * message with no panic text above it; keeping a copy means the next one
1283
+ * arrives with Bun's own words in it.
1284
+ *
1285
+ * Only Windows pipes stderr — that's where the reports come from, and
1286
+ * everywhere else stderr stays a real tty.
1287
+ */
1288
+ function watchLaunch(child) {
1289
+ const startedAt = Date.now()
1290
+ const chunks = []
1291
+ let bufferedBytes = 0
1292
+
1293
+ child.stderr?.on('data', (chunk) => {
1294
+ try {
1295
+ // Straight through: as far as the binary and the user are concerned,
1296
+ // this is still its terminal.
1297
+ process.stderr.write(chunk)
1298
+ } catch {
1299
+ // stderr may be closed
1300
+ }
1301
+ chunks.push(chunk)
1302
+ bufferedBytes += chunk.length
1303
+ while (bufferedBytes > STDERR_TAIL_BYTES && chunks.length > 1) {
1304
+ bufferedBytes -= chunks.shift().length
1305
+ }
1306
+ })
1307
+
1308
+ return {
1309
+ msAlive: () => Date.now() - startedAt,
1310
+ stderrTail: () =>
1311
+ Buffer.concat(chunks).toString('utf8').slice(-STDERR_TAIL_BYTES),
1312
+ // 'exit' can beat the last bytes through the pipe; 'close' is the event
1313
+ // that means the stdio is drained too. The timeout is a bound on a stuck
1314
+ // pipe and is deliberately NOT unref'd — an unref'd timer lets the
1315
+ // process exit before it fires, which would swallow the crash report
1316
+ // entirely. Already-drained streams resolve without waiting for it.
1317
+ drained: () =>
1318
+ child.stderr && !child.stderr.readableEnded
1319
+ ? new Promise((resolve) => {
1320
+ child.once('close', resolve)
1321
+ child.stderr.once('end', resolve)
1322
+ setTimeout(resolve, 250)
1323
+ })
1324
+ : Promise.resolve(),
1325
+ }
1326
+ }
1327
+
1328
+ async function tryFallbackToBaseline(code, signal, msAlive) {
1329
+ // Two spellings of one failure, at two levels of certainty. SIGILL /
1330
+ // STATUS_ILLEGAL_INSTRUCTION is proof this CPU cannot run this build; a
1331
+ // Windows startup abort is a strong suspicion (see isStartupCpuFeatureCrash).
1332
+ const confirmed = isIllegalInstructionExit(code, signal)
1333
+ if (!confirmed && !isStartupCpuFeatureCrash(code, signal, msAlive)) {
1134
1334
  return false
1135
1335
  }
1136
1336
 
@@ -1139,12 +1339,28 @@ function createLauncher(productConfig) {
1139
1339
  return false
1140
1340
  }
1141
1341
 
1342
+ // An explicit target is the user's decision; don't download over it.
1343
+ if (getTargetOverride()) {
1344
+ return false
1345
+ }
1346
+
1142
1347
  const metadata = getCurrentMetadata()
1143
1348
  const currentTarget = metadata?.target || getDefaultTargetKey()
1144
1349
  if (currentTarget === fallbackTarget) {
1145
1350
  return false
1146
1351
  }
1147
1352
 
1353
+ // Only a confirmed illegal instruction gets written down. Persisting it
1354
+ // before the download is what caps the cost at one crash: even if the
1355
+ // download or the relaunch fails, we never optimistically pick the AVX2
1356
+ // build again. A suspected crash records nothing — installing the baseline
1357
+ // already keeps this machine on it, so a guess that turns out to be wrong
1358
+ // costs the slower build instead of leaving cpu-features.json asserting a
1359
+ // CPU limitation we never observed.
1360
+ if (confirmed) {
1361
+ recordMachineLacksAvx2()
1362
+ }
1363
+
1148
1364
  const version = metadata?.version || (await getLatestVersion())
1149
1365
  if (!version) {
1150
1366
  return false
@@ -1155,7 +1371,9 @@ function createLauncher(productConfig) {
1155
1371
  })
1156
1372
  console.error('')
1157
1373
  console.error(
1158
- `${packageName} is switching to the older-CPU binary for this machine.`,
1374
+ confirmed
1375
+ ? `${packageName} is switching to the older-CPU binary for this machine.`
1376
+ : `${packageName} crashed on startup; trying the older-CPU binary.`,
1159
1377
  )
1160
1378
 
1161
1379
  try {
@@ -1173,9 +1391,20 @@ function createLauncher(productConfig) {
1173
1391
 
1174
1392
  function attachExitHandler(child, allowBaselineFallback = true) {
1175
1393
  const exitListener = async (code, signal) => {
1394
+ // A child we never watched (only reachable from a test) reports no age
1395
+ // rather than a suspiciously young one: absent evidence must not be read
1396
+ // as a startup crash and trigger a download.
1397
+ const msAlive = child.launch ? child.launch.msAlive() : Infinity
1398
+
1399
+ let stderrTail = ''
1400
+ if (child.launch && (isWindowsNativeCrashCode(code) || signal)) {
1401
+ await child.launch.drained()
1402
+ stderrTail = child.launch.stderrTail()
1403
+ }
1404
+
1176
1405
  if (
1177
1406
  allowBaselineFallback &&
1178
- (await tryFallbackToBaseline(code, signal))
1407
+ (await tryFallbackToBaseline(code, signal, msAlive))
1179
1408
  ) {
1180
1409
  return
1181
1410
  }
@@ -1183,7 +1412,7 @@ function createLauncher(productConfig) {
1183
1412
  resetTerminal({
1184
1413
  exitAlternateScreen: shouldExitAlternateScreen(code, signal),
1185
1414
  })
1186
- printCrashDiagnostics(code, signal)
1415
+ printCrashDiagnostics(code, signal, { msAlive, stderrTail })
1187
1416
  process.exit(signal ? 1 : code || 0)
1188
1417
  }
1189
1418
 
@@ -1197,7 +1426,7 @@ function createLauncher(productConfig) {
1197
1426
  }
1198
1427
  if (startupBanner.length > 0) console.log('')
1199
1428
 
1200
- await ensureBinaryExists()
1429
+ await ensureBinaryReady()
1201
1430
 
1202
1431
  const child = spawnInstalledBinary()
1203
1432
  const exitListener = attachExitHandler(child)
@@ -1211,6 +1440,29 @@ function createLauncher(productConfig) {
1211
1440
  config: productConfig,
1212
1441
  main,
1213
1442
  stopRunningProcess,
1443
+ // Internals exposed for tests only. The AVX2 path is optimistic-then-
1444
+ // corrected (see detectMachineHasAvx2), so the correction has to be
1445
+ // exercised directly — there is no way to make a CI runner lack AVX2.
1446
+ __testing: {
1447
+ detectMachineHasAvx2,
1448
+ recordMachineLacksAvx2,
1449
+ readCachedAvx2,
1450
+ isIllegalInstructionExit,
1451
+ isStartupCpuFeatureCrash,
1452
+ tryFallbackToBaseline,
1453
+ printCrashDiagnostics,
1454
+ checkForUpdates,
1455
+ spawnInstalledBinary,
1456
+ attachExitHandler,
1457
+ getDefaultTargetKey,
1458
+ getCpuFeatureCachePath,
1459
+ getCurrentVersion,
1460
+ getMetadataVersion,
1461
+ getRequiredWrapperVersion,
1462
+ ensureBinaryReady,
1463
+ isTargetAllowedForThisMachine,
1464
+ CONFIG,
1465
+ },
1214
1466
  }
1215
1467
  }
1216
1468
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebuff",
3
- "version": "1.0.685",
3
+ "version": "1.0.686",
4
4
  "description": "AI coding agent",
5
5
  "license": "MIT",
6
6
  "bin": {