Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/docs/reference/security-faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,16 @@ A single `first_launch` event is sent containing only:

- The installed version (e.g., "0.5.9")
- Whether this is a fresh install or upgrade (boolean)
- Which installer was used (`curl`, `powershell`, `npm`, `vscode-extension`, `local`, or `unknown` — what every upgrade from a version predating this field reports)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Non-blocking (NIT) — the new unknown gloss overstates when it occurs

"what every upgrade from a version predating this field reports" is not quite right. An upgrade performed by a current curl / PowerShell / npm writer records its current method — the writer is what decides, not the version being upgraded from.

unknown is what the reader falls back to when the companion is missing, unreadable, or unrecognised. The predating-the-field case reaches it only through an unconsumed marker written before the field existed.

Listing unknown closes m7; only the explanation needs narrowing. Something like: "reported when the installer wrote no source marker, or wrote one this version does not recognise."

- Your anonymous machine ID (random UUID)

No code, queries, file paths, or personal information is included. This event helps us understand adoption and is fully opt-out-able.

The install scripts (`altimate.sh/install`, `install.ps1`), the npm postinstall, and the VS Code extension's installer send nothing themselves and contact no telemetry endpoint. They only record the version and installer name to a local file that the CLI reads on its next run, so the opt-out above decides whether anything is ever transmitted.

!!! warning "One caveat on the config-file opt-out"
The environment variables (`ALTIMATE_TELEMETRY_DISABLED`, `OPENCODE_DISABLE_TELEMETRY`) are always honoured. The `telemetry.disabled` **config key** is read during telemetry startup, which can run before the CLI's config is resolvable — and in that case startup currently proceeds with telemetry enabled. A user who has opted out via the config key alone may therefore still have this event transmitted. Use an environment variable if you need a guarantee.

## What happens when I authenticate via a well-known URL?

When you run `altimate auth login <url>`, the CLI fetches `<url>/.well-known/altimate-code` to discover the server's auth command. Before executing anything:
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ We collect the following categories of events:
| `feature_suggestion` | A post-connection feature suggestion is shown (suggestion_type, suggestions_shown, warehouse_type — no user input) |
| `sql_execute_failure` | A SQL execution fails (warehouse type, query type, error message, PII-masked SQL — no raw values) |
| `core_failure` | An internal tool error occurs (tool name, category, error class, truncated error message, PII-safe input signature, and optionally masked arguments — no raw values or credentials) |
| `first_launch` | Fired once on first CLI run after installation. Contains version and is_upgrade flag. No PII. |
| `first_launch` | Fired once on the first CLI run after an install or upgrade, triggered by a marker file the installer wrote — the installers themselves send nothing and contact no telemetry endpoint. Contains the installed version, `is_upgrade`, and `install_method` (`curl`, `powershell`, `npm`, `vscode-extension`, `local` for `install --binary`, or `unknown` for markers written before the field existed). `vscode-extension` starts appearing only once an extension build containing the marker write ships, so a zero share for it means the extension has not rolled out yet rather than no extension installs. No PII. **Reading `is_upgrade`:** it means "this machine had run altimate-code before", probed as whether `~/.altimate/machine-id` already existed — *not* "a binary was already present". A reinstall onto a machine that ever ran the CLI reports `is_upgrade: true`, and `altimate uninstall` leaves `machine-id` in place, so a metric excluding upgrades counts installs **per previously-unseen machine** and undercounts reinstalls onto known ones. (`is_upgrade` is a boolean in the event schema; it arrives in Application Insights `customDimensions` as a string, so KQL filters read `tostring(customDimensions.is_upgrade) != "true"`.) Delivery is at-most-once: the marker is deleted before the event flushes, so a process that dies first loses that install rather than re-firing it every launch. Local `--binary` installs report `version: "local"`. |
| `task_outcome_signal` | Behavioral quality signal at session end — accepted, error, abandoned, or cancelled. Includes tool count, step count, duration, and last tool category. No user content. |
| `task_classified` | Intent classification of the first user message using keyword matching — category (e.g. `debug_dbt`, `write_sql`, `optimize_query`), confidence score, and detected warehouse type. No user text is sent — only the classified category. |
| `tool_chain_outcome` | Aggregated tool execution sequence at session end — ordered tool names (capped at 50), error count, recovery count, final outcome, duration, and cost. No tool arguments or outputs. |
Expand Down
44 changes: 44 additions & 0 deletions install
Original file line number Diff line number Diff line change
Expand Up @@ -487,11 +487,55 @@ install_from_binary() {
chmod 755 "$dest_path"
}

# Write the same post-install marker that npm's postinstall.mjs writes, so the
# CLI emits its `first_launch` telemetry event on the next run. Without this the
# curl install path — the one advertised at altimate.sh/install — produces no
# install event at all, and every curl user is invisible in install metrics.
#
# The path MUST match welcome.ts's data-dir resolution ($XDG_DATA_HOME, falling
# back to ~/.local/share) on every platform, including Windows: the CLI reads it
# via Node's os.homedir() and never consults %LOCALAPPDATA%.
#
# No network call and no identifier is written here — this only hands the CLI the
# version it was installed at. Whether anything is ever sent remains entirely up
# to the CLI's existing telemetry opt-out gates.
# $1 — install_method to record. Must be a value in the CLI's allowlist
# (packages/opencode/src/cli/welcome.ts); anything else reports as "unknown".
write_install_marker() {
local marker_source="$1"
local data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/altimate-code"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: On the Windows bash path this marker lands where the CLI never reads it. The install script explicitly supports Windows (MINGW/MSYS/CYGWIN resolve os="windows", seen around line 92), but write_install_marker resolves the data dir from $HOME, while welcome.ts resolves it via Node's os.homedir() (getDataDir(): process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share")). Under MSYS2/Cygwin $HOME is the POSIX home (/home/<user>), which does not match Windows' os.homedir() (%USERPROFILE%), so the .installed-version/.install-source files would be written to a location the CLI never checks — the exact silent failure this PR intends to fix. The in-diff comment claims the path "MUST match ... on every platform, including Windows", which is not guaranteed on the bash path. Consider deriving the fallback from the user profile on the Windows branches (e.g. test "$os" = windows using $USERPROFILE instead of $HOME), or document/limit the bash installer's Windows support.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install, line 503:

<comment>On the Windows bash path this marker lands where the CLI never reads it. The `install` script explicitly supports Windows (MINGW/MSYS/CYGWIN resolve `os="windows"`, seen around line 92), but `write_install_marker` resolves the data dir from `$HOME`, while welcome.ts resolves it via Node's `os.homedir()` (`getDataDir()`: `process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share")`). Under MSYS2/Cygwin `$HOME` is the POSIX home (`/home/<user>`), which does not match Windows' `os.homedir()` (`%USERPROFILE%`), so the `.installed-version`/`.install-source` files would be written to a location the CLI never checks — the exact silent failure this PR intends to fix. The in-diff comment claims the path "MUST match ... on every platform, including Windows", which is not guaranteed on the bash path. Consider deriving the fallback from the user profile on the Windows branches (e.g. `test "$os" = windows` using `$USERPROFILE` instead of `$HOME`), or document/limit the bash installer's Windows support.</comment>

<file context>
@@ -487,13 +487,40 @@ install_from_binary() {
+# version it was installed at. Whether anything is ever sent remains entirely up
+# to the CLI's existing telemetry opt-out gates.
+write_install_marker() {
+    local data_dir="${XDG_DATA_HOME:-$HOME/.local/share}/altimate-code"
+    # An empty marker is deleted unread by the CLI, so fall back to "unknown"
+    # rather than losing the install: $specific_version is empty whenever the
</file context>

# An empty marker is deleted unread by the CLI, so fall back to "unknown"
# rather than losing the install: $specific_version is empty whenever the
# GitHub API could not be reached (see check_version).
local marker_version="${specific_version:-unknown}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Binary installs now report the literal version local in telemetry. On the --binary path specific_version="local" is set (install line 77), so write_install_marker writes .installed-version = local. welcome.ts then emits first_launch with version: "local" (and the banner reads vlocal installed). Since this change is specifically about counting/measuring installs, local pollutes the version dimension for every --binary install. Either skip the marker on the binary path, or map it to unknown rather than local.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install, line 507:

<comment>Binary installs now report the literal version `local` in telemetry. On the `--binary` path `specific_version="local"` is set (install line 77), so `write_install_marker` writes `.installed-version` = `local`. welcome.ts then emits `first_launch` with `version: "local"` (and the banner reads `vlocal installed`). Since this change is specifically about *counting/measuring* installs, `local` pollutes the version dimension for every `--binary` install. Either skip the marker on the binary path, or map it to `unknown` rather than `local`.</comment>

<file context>
@@ -487,13 +487,40 @@ install_from_binary() {
+    # An empty marker is deleted unread by the CLI, so fall back to "unknown"
+    # rather than losing the install: $specific_version is empty whenever the
+    # GitHub API could not be reached (see check_version).
+    local marker_version="${specific_version:-unknown}"
+    mkdir -p "$data_dir" 2>/dev/null || return 0
+    printf '%s' "${marker_version#v}" > "$data_dir/.installed-version" 2>/dev/null || return 0
</file context>

mkdir -p "$data_dir" 2>/dev/null || return 0
# Companion first, trigger last: the CLI returns early unless .installed-version
# exists, then consumes .install-source. Trigger-first would let a CLI starting in
# between report install_method "unknown", and a truncated .installed-version is
# deleted unread — losing the install rather than just its attribution.
printf '%s' "$marker_source" > "$data_dir/.install-source" 2>/dev/null || return 0
# The trigger is published atomically. Companion-first alone only closes the
# "attribution lost" window; a plain redirect truncates before filling, so a CLI
# starting mid-write can still observe an EMPTY .installed-version, which it
# deletes unread — losing the install itself. mv within one directory is atomic.
local tmp="$data_dir/.installed-version.$$"
printf '%s' "${marker_version#v}" > "$tmp" 2>/dev/null || return 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Non-blocking (NIT) — the temp is not cleaned when the temp write itself fails

|| return 0 returns before the rm -f on the next line, so a failure that still creates the file (disk full, quota) leaves a stale .installed-version.$$ behind. One inert dotfile in the data dir that nothing reads — but line 523 already has the cleanup idiom, so it is a small asymmetry rather than a design question.

The same gap exists in the other two writers, where nothing is cleaned on either write or rename failure. A single best-effort cleanup path covering both stages would cover all three.

mv -f "$tmp" "$data_dir/.installed-version" 2>/dev/null || { rm -f "$tmp" 2>/dev/null; return 0; }
}

if [ -n "$binary_path" ]; then
install_from_binary
# Attributed as "local", not "curl": --binary installs a file the caller already
# had (dev build, air-gapped artifact) and sets specific_version="local", so
# folding it into the curl metric would misreport both source and version.
# Still recorded — it is a real install — just not a curl one.
write_install_marker "local"
else
check_version
download_and_install
# Only reached when an install actually happened: check_version exits 0 early
# when the requested version is already present.
write_install_marker "curl"
fi


Expand Down
65 changes: 65 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,71 @@ if (-not $needsBaseline) {
}
}

# ---------------------------------------------------------------------------
# Post-install marker (install telemetry)
# ---------------------------------------------------------------------------
# Mirrors npm's postinstall.mjs (and ./install's write_install_marker) so the CLI
# emits its `first_launch` event on the next run; without it this install path is
# invisible in install metrics.
#
# A function, not inline code, so the Pester suite can AST-extract and execute it
# against a temp profile the same way it does Test-Checksum. The subprocess tests
# deliberately stop the installer before this point, so inline code here would have
# no runtime coverage on the riskiest of the writers.
function Write-InstallMarker {
param([string]$Version)

# The directory MUST match welcome.ts's resolution - $XDG_DATA_HOME, else
# <home>\.local\share - because the CLI reads it through Node's os.homedir() and
# never looks at %LOCALAPPDATA%. Writing to LOCALAPPDATA here would be silently
# ignored at read time.
#
# No network call and no identifier is written; only the installed version is
# recorded. The CLI's existing telemetry opt-out gates still decide whether
# anything is ever sent.
#
# EVERYTHING is inside the try, path computation included. $ErrorActionPreference is
# "Stop", and Join-Path resolves provider-qualified paths - so a null or empty
# $env:USERPROFILE (pwsh on non-Windows, a stripped service profile) or an
# XDG_DATA_HOME naming a non-existent PSDrive raises a TERMINATING error. Computed
# outside the try, that error would abort the installer after the binary is placed
# but before the PATH registry write and the "Get started" output, leaving the user
# with an installed binary that is not on PATH. [IO.Path]::Combine also keeps
# PSDrive resolution out of it entirely.
try {
$dataRoot = if ($env:XDG_DATA_HOME) { $env:XDG_DATA_HOME } else { [IO.Path]::Combine($env:USERPROFILE, ".local", "share") }
$dataDir = [IO.Path]::Combine($dataRoot, "altimate-code")
New-Item -ItemType Directory -Force -Path $dataDir | Out-Null
# The CLI deletes an empty marker without reporting, so fall back to "unknown"
# when the version could not be resolved (GitHub API unreachable).
$markerVersion = if ($Version) { $Version -replace '^v', '' } else { "unknown" }
# -NoNewline: the CLI trims, but keep the file byte-identical to the npm path.
#
# -Encoding ascii, not utf8: the documented entrypoint is `powershell -c "irm ... | iex"`,
# i.e. Windows PowerShell 5.1, where `-Encoding utf8` prepends a UTF-8 BOM. Both values are
# ASCII by construction, so ascii is lossless here and cannot emit one. The CLI's .trim()
# happens to strip a leading BOM (U+FEFF is JS whitespace), but the install-source value is
# matched against a fixed allowlist and must not depend on that.
#
# Companion first, trigger last. The CLI returns early unless .installed-version
# exists, then consumes .install-source - so writing the trigger first would let a
# CLI starting in between report install_method "unknown". Set-Content also
# truncates before writing, and an empty .installed-version is deleted unread,
# which would lose the install outright.
Set-Content -Path ([IO.Path]::Combine($dataDir, ".install-source")) -Value "powershell" -NoNewline -Encoding ascii
# Trigger published atomically: Set-Content truncates before writing, so a CLI
# starting mid-write could observe an EMPTY .installed-version and delete it
# unread, losing the install. Move-Item within one directory is atomic.
$tmpMarker = [IO.Path]::Combine($dataDir, ".installed-version.tmp")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Temp marker file is left behind when Move-Item fails

The bash installer removes its temp file on a failed publish (mv -f "$tmp" ... || { rm -f "$tmp" ...; return 0; }), but this catch swallows the error without removing .installed-version.tmp. A Move-Item failure (e.g. .installed-version already exists as a directory) then leaves a stray dotfile in the data dir. For consistency, remove the temp in the catch.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The temp marker uses a fixed name (.installed-version.tmp), while the matching writers in install and postinstall.mjs use per-process unique names ($$ and ${process.pid}). Two concurrent install.ps1 runs on the same profile write to and Move-Item the same temp file, so one run can publish the other's partially-written content or fail its own Move-Item because the temp file was already moved. Use $PID to match the other writers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install.ps1, line 362:

<comment>The temp marker uses a fixed name (`.installed-version.tmp`), while the matching writers in `install` and `postinstall.mjs` use per-process unique names (`$$` and `${process.pid}`). Two concurrent install.ps1 runs on the same profile write to and Move-Item the same temp file, so one run can publish the other's partially-written content or fail its own Move-Item because the temp file was already moved. Use `$PID` to match the other writers.</comment>

<file context>
@@ -356,7 +356,12 @@ function Write-InstallMarker {
+    # Trigger published atomically: Set-Content truncates before writing, so a CLI
+    # starting mid-write could observe an EMPTY .installed-version and delete it
+    # unread, losing the install. Move-Item within one directory is atomic.
+    $tmpMarker = [IO.Path]::Combine($dataDir, ".installed-version.tmp")
+    Set-Content -Path $tmpMarker -Value $markerVersion -NoNewline -Encoding ascii
+    Move-Item -Force -Path $tmpMarker -Destination ([IO.Path]::Combine($dataDir, ".installed-version"))
</file context>
Suggested change
$tmpMarker = [IO.Path]::Combine($dataDir, ".installed-version.tmp")
$tmpMarker = [IO.Path]::Combine($dataDir, ".installed-version.$PID.tmp")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Non-blocking (MINOR) — fixed temp name, and no temp cleanup in the catch

.installed-version.tmp carries no PID or GUID, where bash uses .$$ (install:521) and npm uses .${process.pid}.tmp (postinstall.mjs:252). Two concurrent install.ps1 runs share one temp path: A writes it, B truncates it, A renames a partially-written file into place — the exact failure the atomic publish exists to prevent, moved one file over.

Concurrent irm | iex by the same user is the only way to reach it and the try/catch keeps it non-fatal, so this is not a blocker. It is the one place where this writer is weaker than its two siblings.

There is also no cleanup of the temp when Move-Item fails — the catch swallows it and .installed-version.tmp stays behind, where bash explicitly does rm -f "$tmp". And the executed "no temp left behind" assertion exists for bash (install-telemetry.test.ts:191-201) and npm (postinstall.test.ts:145) but not here, where only a source-level -match is checked.

$tmpMarker = [IO.Path]::Combine($dataDir, ".installed-version.$PID.tmp")

and in the catch:

Remove-Item -Force -ErrorAction SilentlyContinue $tmpMarker

A Get-ChildItem assertion in the Pester "writes both marker files" case would close the test-coverage half.

Set-Content -Path $tmpMarker -Value $markerVersion -NoNewline -Encoding ascii
Move-Item -Force -Path $tmpMarker -Destination ([IO.Path]::Combine($dataDir, ".installed-version"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When Move-Item fails (destination locked, transient IO error), the catch silently swallows it and leaves .installed-version.tmp in the data directory. The parallel bash installer cleans up its temp marker on failure (rm -f "$tmp" before returning). Add the same cleanup so a failed publish doesn't leave a stale dotfile that every future successful install must re-truncate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install.ps1, line 364:

<comment>When Move-Item fails (destination locked, transient IO error), the catch silently swallows it and leaves `.installed-version.tmp` in the data directory. The parallel bash installer cleans up its temp marker on failure (`rm -f "$tmp"` before returning). Add the same cleanup so a failed publish doesn't leave a stale dotfile that every future successful install must re-truncate.</comment>

<file context>
@@ -356,7 +356,12 @@ function Write-InstallMarker {
+    # unread, losing the install. Move-Item within one directory is atomic.
+    $tmpMarker = [IO.Path]::Combine($dataDir, ".installed-version.tmp")
+    Set-Content -Path $tmpMarker -Value $markerVersion -NoNewline -Encoding ascii
+    Move-Item -Force -Path $tmpMarker -Destination ([IO.Path]::Combine($dataDir, ".installed-version"))
   } catch {
     # Non-fatal - a missing marker only costs us the install event, never the install.
</file context>

} catch {
# Non-fatal - a missing marker only costs us the install event, never the install.
}
}

Write-InstallMarker -Version $specificVersion

# ---------------------------------------------------------------------------
# PATH (user scope, via registry + broadcast)
# ---------------------------------------------------------------------------
Expand Down
15 changes: 14 additions & 1 deletion packages/opencode/script/postinstall.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,20 @@ function writeUpgradeMarker(version) {
const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share")
const dataDir = path.join(xdgData, "altimate-code")
fs.mkdirSync(dataDir, { recursive: true })
fs.writeFileSync(path.join(dataDir, ".installed-version"), version.replace(/^v/, ""))
// Companion first, trigger last. `.installed-version` is what the CLI keys on:
// it returns early unless that file exists, then consumes `.install-source`.
// Trigger-first left two windows — a CLI starting in between reports
// install_method "unknown", and writeFileSync truncates before writing, so a
// reader could observe an EMPTY `.installed-version` and delete it unread,
// losing the install outright. Matches `install` and `install.ps1`.
fs.writeFileSync(path.join(dataDir, ".install-source"), "npm")

@sahrizvi sahrizvi Aug 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 BLOCKING

MAJOR — the write-order fix was not applied to npm, though the commit says it was

The response commit states: "Companion written before trigger, in all three writers." Two were changed. npm still writes the trigger first:

fs.writeFileSync(path.join(dataDir, ".installed-version"), version.replace(/^v/, ""))   // trigger
fs.writeFileSync(path.join(dataDir, ".install-source"), "npm")                          // companion

Both failure modes closed for install and install.ps1 are still live here, and by the rationale given for the flip they matter:

  • a CLI starting between the two writes sees .installed-version present and .install-source absent, so the npm install reports install_method: "unknown";
  • fs.writeFileSync truncates before writing, so a reader can observe an empty .installed-version, which welcome.ts:83-91 deletes unread — losing the npm install outright.

npm is the only channel that was ever counted before this PR, so this is not a leftover on a dead path.

Both new order tests are per-writer source assertions (install-telemetry.test.ts:76-89 for bash, :210-218 for PowerShell). Nothing covers postinstall.mjs, and postinstall.test.ts:107-114 asserts only that both files exist.

Fix: swap the two lines, and add the matching assertion to postinstall.test.ts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b69530b — and you were right that the commit message overclaimed.

postinstall.mjs now writes the companion first, and postinstall.test.ts gained the assertion you asked for. It checks real output (mtime ordering plus a readdirSync of the marker dir), with a source-index guard because mtimes can tie on fast filesystems.

While fixing it I found the reorder was only half the fix. Companion-first closes the "attribution lost" window; it does nothing about the truncation window you also named. A plain write truncates before filling, so a reader mid-write can still observe an empty .installed-version, which welcome.ts deletes unread — losing the install itself. Only the extension published atomically, so all three CLI writers still had that window open. That's the same partial-application pattern as the write-order fix, one level down.

All four writers now publish the trigger via temp+rename: mv -f in install, Move-Item -Force in install.ps1, renameSync in postinstall.mjs, matching the extension. Covered by a no-temp-residue assertion in the executed bash test and source assertions per writer.

// Trigger published atomically: writeFileSync truncates before filling, so a CLI
// starting mid-write could observe an EMPTY `.installed-version` and delete it
// unread, losing the install. renameSync within one directory is atomic.
const versionPath = path.join(dataDir, ".installed-version")
const tmpPath = `${versionPath}.${process.pid}.tmp`
fs.writeFileSync(tmpPath, version.replace(/^v/, ""))
fs.renameSync(tmpPath, versionPath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Temp marker file is left behind when renameSync throws

Unlike install, which cleans up its temp on a failed move (rm -f "$tmp"), this catch leaves ${versionPath}.${process.pid}.tmp behind if renameSync fails. Because the PID is unique per process, each failed publish leaves a distinct stray dotfile. Consider removing tmpPath in the catch for parity with the other writers.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment on lines +253 to +254

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When the temp write or rename fails, the outer catch swallows the error without deleting tmpPath. Repeated failed postinstalls leave .installed-version.<pid>.tmp files in the persistent marker directory; remove the temp file on every failed publish.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/script/postinstall.mjs, line 253:

<comment>When the temp write or rename fails, the outer catch swallows the error without deleting `tmpPath`. Repeated failed postinstalls leave `.installed-version.<pid>.tmp` files in the persistent marker directory; remove the temp file on every failed publish.</comment>

<file context>
@@ -238,10 +238,20 @@ function writeUpgradeMarker(version) {
+    // unread, losing the install. renameSync within one directory is atomic.
+    const versionPath = path.join(dataDir, ".installed-version")
+    const tmpPath = `${versionPath}.${process.pid}.tmp`
+    fs.writeFileSync(tmpPath, version.replace(/^v/, ""))
+    fs.renameSync(tmpPath, versionPath)
   } catch {
</file context>
Suggested change
fs.writeFileSync(tmpPath, version.replace(/^v/, ""))
fs.renameSync(tmpPath, versionPath)
try {
fs.writeFileSync(tmpPath, version.replace(/^v/, ""))
fs.renameSync(tmpPath, versionPath)
} catch (error) {
try {
fs.rmSync(tmpPath, { force: true })
} catch {}
throw error
}

} catch {
// Non-fatal — the CLI just won't show a welcome banner
}
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/altimate/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,11 @@ export namespace Telemetry {
session_id: string
version: string
is_upgrade: boolean
// altimate_change — which installer wrote the marker. Recorded by the
// installer itself; "unknown" when the marker predates this field or the
// source file was unreadable. Without it, curl and npm installs are
// indistinguishable in the same metric.
install_method: "curl" | "powershell" | "npm" | "vscode-extension" | "local" | "unknown"
}
// altimate_change end
// altimate_change start — telemetry for skill management operations
Expand Down
Loading
Loading