freebuff 0.0.141 → 0.0.145

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 (2) hide show
  1. package/launcher.js +231 -13
  2. package/package.json +1 -1
package/launcher.js CHANGED
@@ -72,6 +72,16 @@ function createLauncher(productConfig) {
72
72
  }
73
73
  }
74
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
+
75
85
  function getUnsignedExitCode(code) {
76
86
  return code != null && code < 0 ? code >>> 0 : code
77
87
  }
@@ -98,6 +108,34 @@ function createLauncher(productConfig) {
98
108
  )
99
109
  }
100
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
+
101
139
  function createConfig(packageName) {
102
140
  const homeDir = os.homedir()
103
141
  const configDir =
@@ -933,6 +971,16 @@ function createLauncher(productConfig) {
933
971
  }
934
972
 
935
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
+
936
984
  let stoppedForUpdate = false
937
985
 
938
986
  try {
@@ -991,7 +1039,32 @@ function createLauncher(productConfig) {
991
1039
  }
992
1040
  }
993
1041
 
994
- 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
995
1068
  // Windows NTSTATUS codes (unsigned DWORD)
996
1069
  const unsignedCode = getUnsignedExitCode(code)
997
1070
  const isIllegalInstruction = isIllegalInstructionExit(code, signal)
@@ -1026,11 +1099,46 @@ function createLauncher(productConfig) {
1026
1099
  console.error('This may indicate a platform compatibility issue.')
1027
1100
  console.error('')
1028
1101
  } else if (isAbort) {
1029
- 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
+ )
1030
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
+ }
1031
1131
  }
1032
1132
 
1033
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
+ }
1034
1142
  console.error('')
1035
1143
  console.error('Please report this issue at:')
1036
1144
  console.error(' https://github.com/CodebuffAI/codebuff/issues')
@@ -1047,13 +1155,28 @@ function createLauncher(productConfig) {
1047
1155
  console.error('')
1048
1156
  }
1049
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
+
1050
1173
  function printSystemInfo() {
1051
1174
  const metadata = getCurrentMetadata()
1052
1175
  console.error('System info:')
1053
1176
  console.error(` Platform: ${process.platform} ${process.arch}`)
1054
1177
  console.error(` Node: ${process.version}`)
1055
1178
  if (process.arch === 'x64') {
1056
- console.error(` AVX2: ${machineHasAvx2() ? 'yes' : 'no'}`)
1179
+ console.error(` AVX2: ${describeAvx2Support()}`)
1057
1180
  }
1058
1181
  console.error(` Target: ${metadata?.target || getDefaultTargetKey()}`)
1059
1182
  console.error(` Binary: ${CONFIG.binaryPath}`)
@@ -1124,7 +1247,12 @@ function createLauncher(productConfig) {
1124
1247
  try {
1125
1248
  const { env: optionEnv, ...spawnOptions } = options
1126
1249
  child = spawn(CONFIG.binaryPath, process.argv.slice(2), {
1127
- 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'],
1128
1256
  ...spawnOptions,
1129
1257
  env: {
1130
1258
  ...process.env,
@@ -1137,12 +1265,72 @@ function createLauncher(productConfig) {
1137
1265
  }
1138
1266
 
1139
1267
  child.on('error', exitOnSpawnFailure)
1268
+ child.launch = watchLaunch(child)
1140
1269
 
1141
1270
  return child
1142
1271
  }
1143
1272
 
1144
- async function tryFallbackToBaseline(code, signal) {
1145
- 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)) {
1146
1334
  return false
1147
1335
  }
1148
1336
 
@@ -1151,16 +1339,27 @@ function createLauncher(productConfig) {
1151
1339
  return false
1152
1340
  }
1153
1341
 
1342
+ // An explicit target is the user's decision; don't download over it.
1343
+ if (getTargetOverride()) {
1344
+ return false
1345
+ }
1346
+
1154
1347
  const metadata = getCurrentMetadata()
1155
1348
  const currentTarget = metadata?.target || getDefaultTargetKey()
1156
1349
  if (currentTarget === fallbackTarget) {
1157
1350
  return false
1158
1351
  }
1159
1352
 
1160
- // The optimized build just died on an illegal instruction, so this machine
1161
- // cannot run it. Persist that before downloading, so even if the download
1162
- // or the relaunch fails we never optimistically pick the AVX2 build again.
1163
- recordMachineLacksAvx2()
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
+ }
1164
1363
 
1165
1364
  const version = metadata?.version || (await getLatestVersion())
1166
1365
  if (!version) {
@@ -1172,7 +1371,9 @@ function createLauncher(productConfig) {
1172
1371
  })
1173
1372
  console.error('')
1174
1373
  console.error(
1175
- `${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.`,
1176
1377
  )
1177
1378
 
1178
1379
  try {
@@ -1190,9 +1391,20 @@ function createLauncher(productConfig) {
1190
1391
 
1191
1392
  function attachExitHandler(child, allowBaselineFallback = true) {
1192
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
+
1193
1405
  if (
1194
1406
  allowBaselineFallback &&
1195
- (await tryFallbackToBaseline(code, signal))
1407
+ (await tryFallbackToBaseline(code, signal, msAlive))
1196
1408
  ) {
1197
1409
  return
1198
1410
  }
@@ -1200,7 +1412,7 @@ function createLauncher(productConfig) {
1200
1412
  resetTerminal({
1201
1413
  exitAlternateScreen: shouldExitAlternateScreen(code, signal),
1202
1414
  })
1203
- printCrashDiagnostics(code, signal)
1415
+ printCrashDiagnostics(code, signal, { msAlive, stderrTail })
1204
1416
  process.exit(signal ? 1 : code || 0)
1205
1417
  }
1206
1418
 
@@ -1236,6 +1448,12 @@ function createLauncher(productConfig) {
1236
1448
  recordMachineLacksAvx2,
1237
1449
  readCachedAvx2,
1238
1450
  isIllegalInstructionExit,
1451
+ isStartupCpuFeatureCrash,
1452
+ tryFallbackToBaseline,
1453
+ printCrashDiagnostics,
1454
+ checkForUpdates,
1455
+ spawnInstalledBinary,
1456
+ attachExitHandler,
1239
1457
  getDefaultTargetKey,
1240
1458
  getCpuFeatureCachePath,
1241
1459
  getCurrentVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freebuff",
3
- "version": "0.0.141",
3
+ "version": "0.0.145",
4
4
  "description": "The world's strongest free coding agent",
5
5
  "license": "MIT",
6
6
  "bin": {