fix(history): merge connection into history.json immediately at provision time (#2177)

Previously, saveVmConnection wrote to a single last-connection.json temp
file that was only merged into history.json lazily when spawn ls was run.
This caused connections to be silently dropped when:
- Two servers spawned before running spawn ls (file overwritten)
- The last history record already had a connection (merge skipped)

Now saveVmConnection writes directly into history.json by finding the
most recent record matching the cloud with no connection yet. The temp
file is still written for backward compatibility but is no longer the
primary storage.

Also fixes saveLaunchCmd to update history.json directly, and
consolidates sprite's local saveVmConnection to use the shared one.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
This commit is contained in:
Ahmed Abushagur 2026-03-04 01:36:28 -08:00 committed by GitHub
parent cb91b5d236
commit c9ea6384da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -62,7 +62,10 @@ export function getConnectionPath(): string {
return join(getSpawnDir(), "last-connection.json");
}
/** Save VM connection info to last-connection.json for later reconnection/deletion. */
/** Save VM connection info directly into history.json.
* Finds the most recent record matching this cloud that has no connection yet
* and attaches the connection data to it. This is called during provisioning
* so the connection is persisted immediately not deferred to a lazy merge. */
export function saveVmConnection(
ip: string,
user: string,
@ -77,6 +80,38 @@ export function saveVmConnection(
recursive: true,
mode: 0o700,
});
const connData: VMConnection = {
ip,
user,
server_id: serverId || undefined,
server_name: serverName || undefined,
cloud: cloud || undefined,
launch_cmd: launchCmd || undefined,
metadata: metadata && Object.keys(metadata).length > 0 ? metadata : undefined,
};
// Merge directly into history — find the most recent record for this cloud
// that doesn't have a connection yet (the record created by saveSpawnRecord).
const history = loadHistory();
let merged = false;
for (let i = history.length - 1; i >= 0; i--) {
const r = history[i];
if (r.cloud === cloud && !r.connection) {
r.connection = connData;
merged = true;
break;
}
}
if (merged) {
writeFileSync(getHistoryPath(), JSON.stringify(history, null, 2) + "\n", {
mode: 0o600,
});
}
// Also write last-connection.json for backward compatibility
// (other tools or older CLI versions may still read it)
const json: Record<string, unknown> = {
ip,
user,
@ -101,8 +136,26 @@ export function saveVmConnection(
});
}
/** Save launch command to the last-connection.json file. */
/** Save launch command to the most recent history record's connection. */
export function saveLaunchCmd(launchCmd: string): void {
// Update history directly — find the most recent record with a connection
try {
const history = loadHistory();
for (let i = history.length - 1; i >= 0; i--) {
const conn = history[i].connection;
if (conn) {
conn.launch_cmd = launchCmd;
writeFileSync(getHistoryPath(), JSON.stringify(history, null, 2) + "\n", {
mode: 0o600,
});
break;
}
}
} catch {
// non-fatal
}
// Also update last-connection.json for backward compatibility
const connFile = getConnectionPath();
try {
const data = JSON.parse(readFileSync(connFile, "utf-8"));
@ -281,17 +334,22 @@ function mergeLastConnection(): void {
const history = loadHistory();
if (history.length > 0) {
// Update the most recent entry with connection info
const latest = history[history.length - 1];
if (!latest.connection) {
latest.connection = connData;
// Save updated history
writeFileSync(getHistoryPath(), JSON.stringify(history, null, 2) + "\n", {
mode: 0o600,
});
// Find the most recent entry without a connection and merge into it.
// Search backwards so we match the right record even if earlier records
// already have connections (e.g., from concurrent spawns).
let merged = false;
for (let i = history.length - 1; i >= 0; i--) {
if (!history[i].connection) {
history[i].connection = connData;
merged = true;
break;
}
}
if (merged) {
writeFileSync(getHistoryPath(), JSON.stringify(history, null, 2) + "\n", {
mode: 0o600,
});
}
// Clean up the connection file after merging
unlinkSync(connPath);