Compare commits

..

No commits in common. "experimental-companion" and "companion-v0.1.0" have entirely different histories.

13 changed files with 22 additions and 272 deletions

View File

@ -1,10 +1,5 @@
# Lumi changelog # Lumi changelog
## 0.2.27
- Completed bundled-plugin synchronization automatically on the first startup after a legacy core-only update, allowing production hosts on 0.2.25 to receive the Companion transcription and Song Overlay plugins in the same update flow.
- Added a second snapshot-backed plugin synchronization boundary with automatic restoration, exact-release verification, preserved plugin data, and an idempotent completion marker.
## 0.2.26 ## 0.2.26
- Added stable Lumi Companion distribution with a durable per-user installer, shared paired-device authentication, managed OBS integration, preserved identity, user-approved updates, complete legal notices, and matching application/tray branding. - Added stable Lumi Companion distribution with a durable per-user installer, shared paired-device authentication, managed OBS integration, preserved identity, user-approved updates, complete legal notices, and matching application/tray branding.

View File

@ -30,12 +30,9 @@ new branch.
## Test an experimental branch ## Test an experimental branch
First update production on `main` to the latest stable release through First update production on `main` to the latest stable release through
**Update from repository**. Stable `0.2.27` and newer synchronize core and **Update from repository**. Stable `0.2.26` and newer synchronize core and
bundled plugins in one snapshot-backed transaction. When the update begins on bundled plugins in one snapshot-backed transaction, so future switches in either
`0.2.25`, the newly started core automatically completes the exact-release direction use the same verified workflow.
bundled-plugin phase under a second rollback snapshot before plugins load, with
no repair action required. Future switches in either direction then use the same
verified workflow.
If a `0.2.24` host enters Experimental before taking the stable update, the If a `0.2.24` host enters Experimental before taking the stable update, the
guarded compatibility bootstrap completes bundled-plugin synchronization after guarded compatibility bootstrap completes bundled-plugin synchronization after
the older updater installs the newest `experimental-*` branch; expect a second the older updater installs the newest `experimental-*` branch; expect a second

View File

@ -155,12 +155,6 @@ cannot leave those layers on different releases. Core code still accesses
optional plugin capabilities through registered framework APIs, and plugins can optional plugin capabilities through registered framework APIs, and plugins can
be repaired, upgraded, or downgraded independently from their version picker. be repaired, upgraded, or downgraded independently from their version picker.
Hosts updating from a legacy core-only updater complete the bundled-plugin phase
at the start of the new core, before plugins are loaded. Lumi verifies the same
immutable release ref, takes a second core-and-plugin snapshot, preserves plugin
data and local-only plugins, restores automatically if synchronization fails,
and records an idempotent per-version completion marker.
## Runtime Dependencies ## Runtime Dependencies
Start Lumi through `run.js` (the `npm start` command does this). Before starting Start Lumi through `run.js` (the `npm start` command does this). Before starting

View File

@ -14,7 +14,7 @@ editable: false
Lumi is the core web UI and bot runtime. Lumi is the core web UI and bot runtime.
## Runtime ## Runtime
Package: lumi-bot Package: lumi-bot
Version: 0.2.27 Version: 0.2.26
## Routes ## Routes
- POST /api/diagnostics/v1/run - POST /api/diagnostics/v1/run
- GET /api/events - GET /api/events

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "lumi-bot", "name": "lumi-bot",
"version": "0.2.27", "version": "0.2.26",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lumi-bot", "name": "lumi-bot",
"version": "0.2.27", "version": "0.2.26",
"dependencies": { "dependencies": {
"adm-zip": "^0.6.0", "adm-zip": "^0.6.0",
"better-sqlite3": "^11.5.0", "better-sqlite3": "^11.5.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "lumi-bot", "name": "lumi-bot",
"version": "0.2.27", "version": "0.2.26",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {

View File

@ -2,38 +2,6 @@
"schema_version": 1, "schema_version": 1,
"channel": "stable", "channel": "stable",
"releases": [ "releases": [
{
"version": "0.2.27",
"ref": "refs/tags/v0.2.27",
"released_at": "2026-07-24",
"installable": true,
"rollback_safe": true,
"replaces_versions": [
"1.2.0"
],
"data_policy": "preserve",
"dependency_policy": "sync_on_restart",
"migration_notes": "Completes exact-release bundled-plugin synchronization automatically after an update performed by the legacy 0.2.25 core-only updater. The second phase is snapshot-backed, restores automatically on failure, preserves plugin data and local-only plugins, and is idempotent after completion.",
"plugins": {
"auto-vc": "0.1.6",
"birthday": "0.1.3",
"economy-framework": "0.2.10",
"economy-games": "0.1.7",
"expression-interaction": "0.2.1",
"lumi_ai": "0.8.5",
"lumi_transcription": "0.1.0",
"moderation": "0.1.5",
"now_playing": "0.1.2",
"okf": "0.1.2",
"quotes": "0.1.2",
"sample-plugin": "0.1.0",
"throne_wishlist": "0.1.2",
"welcome_messages": "0.1.1"
},
"tools": {
"lumi_ai_web_search": "0.1.1"
}
},
{ {
"version": "0.2.26", "version": "0.2.26",
"ref": "refs/tags/v0.2.26", "ref": "refs/tags/v0.2.26",

View File

@ -4,9 +4,9 @@ const path = require("path");
const { findSafeTarget } = require("../src/services/versioning"); const { findSafeTarget } = require("../src/services/versioning");
const root = path.join(__dirname, ".."); const root = path.join(__dirname, "..");
const releaseVersion = "0.2.27"; const releaseVersion = "0.2.26";
const previousStableVersion = "0.2.26"; const previousStableVersion = "0.2.25";
const priorStableVersion = "0.2.25"; const priorStableVersion = "0.2.24";
const earliestCompatibleCoreVersion = "0.1.9"; const earliestCompatibleCoreVersion = "0.1.9";
const introducedPlugins = { const introducedPlugins = {
lumi_transcription: { version: "0.1.0", knowledge: "lumi-transcription" }, lumi_transcription: { version: "0.1.0", knowledge: "lumi-transcription" },
@ -82,4 +82,4 @@ assert.equal(webSearch.minimum_lumi_version, "0.2.0");
assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2"); assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2");
assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true); assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true);
console.log("Release metadata verification passed: stable core 0.2.27 after 0.2.26 with synchronized Companion plugin metadata."); console.log("Release metadata verification passed: stable core 0.2.26 after 0.2.25 with synchronized Companion plugin metadata.");

View File

@ -13,10 +13,7 @@ const {
parseDeploymentBranches parseDeploymentBranches
} = require("../src/services/update-repository"); } = require("../src/services/update-repository");
const { buildCorePreservePaths } = require("../src/services/update-manager"); const { buildCorePreservePaths } = require("../src/services/update-manager");
const { const { pendingDeploymentBranch } = require("../src/services/updater");
pendingBundledPluginSync,
pendingDeploymentBranch
} = require("../src/services/updater");
function readJson(relativePath) { function readJson(relativePath) {
return JSON.parse(fs.readFileSync(path.join(root, relativePath), "utf8")); return JSON.parse(fs.readFileSync(path.join(root, relativePath), "utf8"));
@ -24,7 +21,7 @@ function readJson(relativePath) {
const releaseIndex = readJson("release-index.json"); const releaseIndex = readJson("release-index.json");
const releaseVersions = releaseIndex.releases.map((release) => release.version); const releaseVersions = releaseIndex.releases.map((release) => release.version);
assert.deepEqual(releaseVersions, ["0.2.27", "0.2.26", "0.2.25", "0.2.24", "0.2.23", "0.2.22", "0.2.21", "0.2.20", "0.2.19", "0.2.18", "0.2.17", "0.2.16", "0.2.15", "0.2.14", "0.2.13", "0.2.12", "0.2.11", "0.2.10", "0.2.9", "0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]); assert.deepEqual(releaseVersions, ["0.2.26", "0.2.25", "0.2.24", "0.2.23", "0.2.22", "0.2.21", "0.2.20", "0.2.19", "0.2.18", "0.2.17", "0.2.16", "0.2.15", "0.2.14", "0.2.13", "0.2.12", "0.2.11", "0.2.10", "0.2.9", "0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique"); assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique");
for (const release of releaseIndex.releases) { for (const release of releaseIndex.releases) {
assert.equal(normalizeRepositoryRef(release.ref), release.ref); assert.equal(normalizeRepositoryRef(release.ref), release.ref);
@ -37,8 +34,8 @@ const packageVersion = readJson("package.json").version;
const coreManifest = readJson("update-manifest.json"); const coreManifest = readJson("update-manifest.json");
assert.equal(packageVersion, coreManifest.version); assert.equal(packageVersion, coreManifest.version);
assert.equal(coreManifest.channel, "stable"); assert.equal(coreManifest.channel, "stable");
assert.equal(packageVersion, "0.2.27"); assert.equal(packageVersion, "0.2.26");
assert.equal(currentRelease.version, "0.2.27"); assert.equal(currentRelease.version, "0.2.26");
assert.deepEqual(currentRelease.replaces_versions, ["1.2.0"]); assert.deepEqual(currentRelease.replaces_versions, ["1.2.0"]);
for (const [pluginId, version] of Object.entries(currentRelease.plugins)) { for (const [pluginId, version] of Object.entries(currentRelease.plugins)) {
assert.equal(readJson(`plugins/${pluginId}/plugin.json`).version, version, `${pluginId} release catalog version`); assert.equal(readJson(`plugins/${pluginId}/plugin.json`).version, version, `${pluginId} release catalog version`);
@ -50,7 +47,6 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) {
const baseTarget = { const baseTarget = {
current_version: "0.2.4", current_version: "0.2.4",
available_versions: [ available_versions: [
{ version: "0.2.27", ref: "refs/tags/v0.2.27", rollback_safe: true },
{ version: "0.2.26", ref: "refs/tags/v0.2.26", rollback_safe: true }, { version: "0.2.26", ref: "refs/tags/v0.2.26", rollback_safe: true },
{ version: "0.2.25", ref: "refs/tags/v0.2.25", rollback_safe: true }, { version: "0.2.25", ref: "refs/tags/v0.2.25", rollback_safe: true },
{ version: "0.2.24", ref: "refs/tags/v0.2.24", rollback_safe: true }, { version: "0.2.24", ref: "refs/tags/v0.2.24", rollback_safe: true },
@ -113,31 +109,6 @@ assert.equal(pendingDeploymentBranch({ branch: "experimental-ux" }, "experimenta
assert.equal(pendingDeploymentBranch({ ...completedExperimentalUpdate, deployed_branch: "experimental-ux" }, "experimental", "0.2.25"), null, "completed deployments must not loop"); assert.equal(pendingDeploymentBranch({ ...completedExperimentalUpdate, deployed_branch: "experimental-ux" }, "experimental", "0.2.25"), null, "completed deployments must not loop");
assert.equal(pendingDeploymentBranch({ ...completedExperimentalUpdate, deployed_branch: "main" }, "experimental", "0.2.25"), "experimental-ux", "stable re-entry must complete the experimental deployment"); assert.equal(pendingDeploymentBranch({ ...completedExperimentalUpdate, deployed_branch: "main" }, "experimental", "0.2.25"), "experimental-ux", "stable re-entry must complete the experimental deployment");
assert.equal(pendingDeploymentBranch(completedExperimentalUpdate, "main", "0.2.25"), null, "stable configuration must not bootstrap experimental code"); assert.equal(pendingDeploymentBranch(completedExperimentalUpdate, "main", "0.2.25"), null, "stable configuration must not bootstrap experimental code");
const completedLegacyCoreUpdate = {
branch: "refs/tags/v0.2.27",
last_update_status: "complete",
last_target_kind: "core",
last_target_version: "0.2.27",
last_snapshot_id: "snapshot-legacy-core"
};
assert.equal(
pendingBundledPluginSync(completedLegacyCoreUpdate, "0.2.27"),
"refs/tags/v0.2.27",
"a core update completed by the legacy updater must synchronize its bundled plugins on startup"
);
assert.equal(
pendingBundledPluginSync({
...completedLegacyCoreUpdate,
bundled_plugins_synced_for_version: "0.2.27"
}, "0.2.27"),
null,
"completed bundled-plugin synchronization must be idempotent"
);
assert.equal(
pendingBundledPluginSync({ branch: "refs/tags/v0.2.27" }, "0.2.27"),
null,
"repository metadata alone must never trigger startup writes"
);
const corrected = buildStatus({ const corrected = buildStatus({
kind: "core", kind: "core",
@ -148,7 +119,7 @@ const corrected = buildStatus({
channel: "stable" channel: "stable"
}); });
assert.equal(corrected.version_correction, true); assert.equal(corrected.version_correction, true);
assert.equal(corrected.safe_target_version, "0.2.27"); assert.equal(corrected.safe_target_version, "0.2.26");
assert.equal(corrected.update_available, true); assert.equal(corrected.update_available, true);
assert.equal(corrected.blocked, false); assert.equal(corrected.blocked, false);
@ -175,11 +146,6 @@ assert.match(
/async function applyCoreUpdate[\s\S]*include_plugin_code: true[\s\S]*target_plugin_ids: targetPluginIds[\s\S]*applyCoreFilesFromDirectory\(managed\.path\)[\s\S]*applyPluginFiles\(path\.join\(managed\.path, "plugins", pluginId\), pluginId, \{ preserveData: true \}\)[\s\S]*syncPluginRegistry\(\)/, /async function applyCoreUpdate[\s\S]*include_plugin_code: true[\s\S]*target_plugin_ids: targetPluginIds[\s\S]*applyCoreFilesFromDirectory\(managed\.path\)[\s\S]*applyPluginFiles\(path\.join\(managed\.path, "plugins", pluginId\), pluginId, \{ preserveData: true \}\)[\s\S]*syncPluginRegistry\(\)/,
"stable core updates must transactionally carry bundled plugins and newly introduced Companion plugins" "stable core updates must transactionally carry bundled plugins and newly introduced Companion plugins"
); );
assert.match(
repoUpdateSource,
/async function synchronizeBundledPluginsForCompletedCoreUpdate[\s\S]*ensureManagedRepo\(configuredRemote, sourceRef\)[\s\S]*include_plugin_code: true[\s\S]*applyPluginFiles\(path\.join\(managed\.path, "plugins", pluginId\), pluginId, \{ preserveData: true \}\)[\s\S]*bundled_plugins_synced_for_version: currentVersion[\s\S]*restoreSnapshot\(snapshotRecord\.id/,
"legacy core updates must complete bundled-plugin synchronization transactionally on startup"
);
const updateManagerSource = fs.readFileSync(path.join(root, "src", "services", "update-manager.js"), "utf8"); const updateManagerSource = fs.readFileSync(path.join(root, "src", "services", "update-manager.js"), "utf8");
assert.match(updateManagerSource, /from_source_branch: snapshot\.metadata\?\.from_source_branch/); assert.match(updateManagerSource, /from_source_branch: snapshot\.metadata\?\.from_source_branch/);
assert.match(updateManagerSource, /target_commit: snapshot\.metadata\?\.target_commit/); assert.match(updateManagerSource, /target_commit: snapshot\.metadata\?\.target_commit/);
@ -191,11 +157,9 @@ const updaterSource = fs.readFileSync(path.join(root, "src", "services", "update
const mainSource = fs.readFileSync(path.join(root, "src", "main.js"), "utf8"); const mainSource = fs.readFileSync(path.join(root, "src", "main.js"), "utf8");
assert.match(updaterSource, /function registerRestartHandler/); assert.match(updaterSource, /function registerRestartHandler/);
assert.match(updaterSource, /async function completePendingBranchDeployment/); assert.match(updaterSource, /async function completePendingBranchDeployment/);
assert.match(updaterSource, /async function completePendingBundledPluginSync/);
assert.match(updaterSource, /state\.last_update_status === "complete"[\s\S]*state\.last_target_kind === "core"[\s\S]*state\.last_target_version === localVersion/); assert.match(updaterSource, /state\.last_update_status === "complete"[\s\S]*state\.last_target_kind === "core"[\s\S]*state\.last_target_version === localVersion/);
assert.match(mainSource, /registerRestartHandler\(\(\) => shutdown\(10\)\)/); assert.match(mainSource, /registerRestartHandler\(\(\) => shutdown\(10\)\)/);
assert.match(mainSource, /await completePendingBranchDeployment\(configuredRemote, configuredBranch\)/); assert.match(mainSource, /await completePendingBranchDeployment\(configuredRemote, configuredBranch\)/);
assert.match(mainSource, /await completePendingBundledPluginSync\(configuredRemote\)/);
assert.match(mainSource, /overlayConnectorManager\.stop\(\)[\s\S]*stopPlugins\(\)[\s\S]*process\.exit\(Number\(exitCode\) === 10 \? 10 : 0\)/); assert.match(mainSource, /overlayConnectorManager\.stop\(\)[\s\S]*stopPlugins\(\)[\s\S]*process\.exit\(Number\(exitCode\) === 10 \? 10 : 0\)/);
const updatesView = fs.readFileSync(path.join(root, "src", "web", "views", "admin-updates.ejs"), "utf8"); const updatesView = fs.readFileSync(path.join(root, "src", "web", "views", "admin-updates.ejs"), "utf8");
assert.match(serverSource, /app\.post\("\/admin\/updates\/core\/deploy-branch"/); assert.match(serverSource, /app\.post\("\/admin\/updates\/core\/deploy-branch"/);

View File

@ -11,14 +11,7 @@ const stopPlugins =
typeof pluginService.stopPlugins === "function" typeof pluginService.stopPlugins === "function"
? pluginService.stopPlugins ? pluginService.stopPlugins
: async () => {}; : async () => {};
const { const { checkForUpdates, completePendingBranchDeployment, pullUpdates, registerRestartHandler, requestRestart } = require("./services/updater");
checkForUpdates,
completePendingBranchDeployment,
completePendingBundledPluginSync,
pullUpdates,
registerRestartHandler,
requestRestart
} = require("./services/updater");
const { createCommandRouter } = require("./services/command-router"); const { createCommandRouter } = require("./services/command-router");
const { registerTopCommand } = require("./services/top"); const { registerTopCommand } = require("./services/top");
const logger = require("./services/logger"); const logger = require("./services/logger");
@ -44,13 +37,6 @@ async function main() {
requestRestart({ delayMs: 0 }); requestRestart({ delayMs: 0 });
return; return;
} }
const completedPluginSync = await completePendingBundledPluginSync(configuredRemote);
if (completedPluginSync) {
console.log(
`Completed bundled plugin synchronization for Lumi ${completedPluginSync.version} ` +
`(${completedPluginSync.plugin_ids.length} plugins).`
);
}
registerCorePlaceholders(); registerCorePlaceholders();
logger.hookConsole(); logger.hookConsole();
const runtimeLog = logger.createLogger("core:runtime", { category: "lifecycle" }); const runtimeLog = logger.createLogger("core:runtime", { category: "lifecycle" });

View File

@ -205,11 +205,7 @@ async function applyCoreUpdate({ source = "stable", remote = null, version = nul
last_error: null, last_error: null,
last_snapshot_id: record.id, last_snapshot_id: record.id,
last_target_kind: "core", last_target_kind: "core",
last_target_version: target.safe_target_version, last_target_version: target.safe_target_version
bundled_plugins_sync_status: "complete",
bundled_plugins_synced_for_version: target.safe_target_version,
bundled_plugins_synced_commit: managed.commit,
bundled_plugins_synced_at: new Date().toISOString()
}); });
emitProgress(publish, "update:restart_required", { target: "core" }); emitProgress(publish, "update:restart_required", { target: "core" });
emitProgress(publish, "update:complete", { target: "core", snapshot_id: record.id }); emitProgress(publish, "update:complete", { target: "core", snapshot_id: record.id });
@ -245,107 +241,6 @@ async function applyCoreUpdate({ source = "stable", remote = null, version = nul
}); });
} }
async function synchronizeBundledPluginsForCompletedCoreUpdate({
remote = null,
sourceRef,
expectedVersion
} = {}) {
return withOperation("core:bundled-plugin-sync", async () => {
const currentVersion = verifyCoreFiles(repoRoot, expectedVersion);
const configuredRemote = remote || getSetting("git_remote", "origin");
let managed = null;
let marker = null;
let snapshot = null;
let snapshotRecord = null;
let targetPluginIds = [];
let stage = "opening the completed core release";
try {
managed = ensureManagedRepo(configuredRemote, sourceRef);
verifyCoreFiles(managed.path, currentVersion);
targetPluginIds = bundledPluginIds(managed.path);
for (const pluginId of targetPluginIds) verifyPluginFiles(pluginId, managed.path);
stage = "preparing a bundled-plugin rollback snapshot";
marker = createRecoveryMarker({
target_kind: "core",
target_id: "bundled_plugins",
from_version: currentVersion,
to_version: currentVersion,
source_branch: managed.branch,
update_method: "startup_bundled_plugin_sync",
rollback_safe: true,
major_crossing: false
});
updateRecoveryMarker({ status: "applying" });
snapshot = await createSnapshot({
type: "bot",
metadata: {
target_kind: "core",
target_id: "bundled_plugins",
from_version: currentVersion,
to_version: currentVersion,
source_branch: managed.branch,
target_commit: managed.commit,
update_method: "startup_bundled_plugin_sync",
rollback_safe: true,
recovery_marker_id: marker.id,
include_plugin_code: true,
target_plugin_ids: targetPluginIds
}
});
snapshotRecord = finalizeSnapshot(snapshot);
stage = "synchronizing bundled plugins";
for (const pluginId of targetPluginIds) {
applyPluginFiles(path.join(managed.path, "plugins", pluginId), pluginId, { preserveData: true });
}
updateRecoveryMarker({ status: "verifying" });
for (const pluginId of targetPluginIds) verifyPluginFiles(pluginId, repoRoot);
syncPluginRegistry();
markRecoveryMarkerComplete({ snapshot_id: snapshotRecord.id });
writeUpdateState({
bundled_plugins_sync_status: "complete",
bundled_plugins_sync_error: null,
bundled_plugins_synced_for_version: currentVersion,
bundled_plugins_synced_commit: managed.commit,
bundled_plugins_synced_at: new Date().toISOString()
});
return {
status: "complete",
version: currentVersion,
source_ref: managed.branch,
commit: managed.commit,
plugin_ids: targetPluginIds,
snapshot: snapshotRecord
};
} catch (error) {
const originalMessage = error?.message || String(error || "Unknown synchronization error.");
error.message = `Bundled plugin startup synchronization failed while ${stage}: ${originalMessage}`;
if (snapshotRecord) {
try {
restoreSnapshot(snapshotRecord.id, {
expectedType: "bot",
currentVersion,
allowUnsafeMajorRollback: true
});
syncPluginRegistry();
} catch (restoreError) {
error.message += ` Automatic restore also failed: ${restoreError.message}`;
}
} else if (snapshot) {
try { discardSnapshot(snapshot); } catch {}
}
if (marker) markRecoveryMarkerFailed(error);
writeUpdateState({
bundled_plugins_sync_status: "failed",
bundled_plugins_sync_error: error.message,
bundled_plugins_sync_failed_at: new Date().toISOString()
});
throw error;
}
});
}
async function deployCoreBranch({ branch, remote = null, publish } = {}) { async function deployCoreBranch({ branch, remote = null, publish } = {}) {
return withOperation("core", async () => { return withOperation("core", async () => {
const targetBranch = normalizeDeploymentBranch(branch); const targetBranch = normalizeDeploymentBranch(branch);
@ -451,11 +346,7 @@ async function deployCoreBranch({ branch, remote = null, publish } = {}) {
last_snapshot_id: snapshotRecord.id, last_snapshot_id: snapshotRecord.id,
last_target_kind: "core", last_target_kind: "core",
last_target_version: target.version, last_target_version: target.version,
last_deployment_method: "branch_deploy", last_deployment_method: "branch_deploy"
bundled_plugins_sync_status: "complete",
bundled_plugins_synced_for_version: target.version,
bundled_plugins_synced_commit: managed.commit,
bundled_plugins_synced_at: new Date().toISOString()
}); });
emitProgress(publish, "update:restart_required", { target: "core", deployment }); emitProgress(publish, "update:restart_required", { target: "core", deployment });
emitProgress(publish, "update:complete", { target: "core", snapshot_id: snapshotRecord.id, deployment }); emitProgress(publish, "update:complete", { target: "core", snapshot_id: snapshotRecord.id, deployment });
@ -673,7 +564,6 @@ function disablePluginForRecovery(pluginId, publish) {
module.exports = { module.exports = {
applyCoreUpdate, applyCoreUpdate,
synchronizeBundledPluginsForCompletedCoreUpdate,
deployCoreBranch, deployCoreBranch,
applyPluginUpdateFromRepo, applyPluginUpdateFromRepo,
targetForRequestedVersion, targetForRequestedVersion,

View File

@ -1,9 +1,5 @@
const { getUpdateStatus } = require("./update-index"); const { getUpdateStatus } = require("./update-index");
const { const { applyCoreUpdate, deployCoreBranch } = require("./repo-update");
applyCoreUpdate,
deployCoreBranch,
synchronizeBundledPluginsForCompletedCoreUpdate
} = require("./repo-update");
const { const {
normalizeRepositoryTarget, normalizeRepositoryTarget,
readUpdateState, readUpdateState,
@ -61,32 +57,6 @@ async function completePendingBranchDeployment(remote, configuredBranch) {
return deployCoreBranch({ branch: targetBranch, remote }); return deployCoreBranch({ branch: targetBranch, remote });
} }
function pendingBundledPluginSync(state, localVersion) {
const confirmedCoreUpdate =
state.last_update_status === "complete" &&
state.last_target_kind === "core" &&
state.last_target_version === localVersion &&
Boolean(state.last_snapshot_id);
if (
!confirmedCoreUpdate ||
state.bundled_plugins_synced_for_version === localVersion
) return null;
const sourceRef = String(state.branch || "").trim();
return sourceRef || null;
}
async function completePendingBundledPluginSync(remote) {
const state = readUpdateState();
const localVersion = require("../../package.json").version;
const sourceRef = pendingBundledPluginSync(state, localVersion);
if (!sourceRef) return null;
return synchronizeBundledPluginsForCompletedCoreUpdate({
remote,
sourceRef,
expectedVersion: localVersion
});
}
function registerRestartHandler(handler) { function registerRestartHandler(handler) {
restartHandler = typeof handler === "function" ? handler : null; restartHandler = typeof handler === "function" ? handler : null;
return () => { return () => {
@ -111,11 +81,9 @@ function requestRestart(options = {}) {
module.exports = { module.exports = {
checkForUpdates, checkForUpdates,
completePendingBranchDeployment, completePendingBranchDeployment,
completePendingBundledPluginSync,
pullUpdates, pullUpdates,
normalizeRepositoryTarget, normalizeRepositoryTarget,
pendingDeploymentBranch, pendingDeploymentBranch,
pendingBundledPluginSync,
resolveUpdateBranch, resolveUpdateBranch,
registerRestartHandler, registerRestartHandler,
requestRestart requestRestart

View File

@ -1,6 +1,6 @@
{ {
"name": "Lumi Core", "name": "Lumi Core",
"version": "0.2.27", "version": "0.2.26",
"channel": "stable", "channel": "stable",
"released_at": "2026-07-24", "released_at": "2026-07-24",
"compatible_from": "0.1.9", "compatible_from": "0.1.9",
@ -8,7 +8,7 @@
"replaces_versions": [ "replaces_versions": [
"1.2.0" "1.2.0"
], ],
"migration_notes": "Completes bundled-plugin synchronization automatically after updates performed by the legacy 0.2.25 core-only updater. The synchronization is snapshot-backed, exact-release verified, idempotent, and preserves settings, databases, pairing records, tokens, overlays, uploads, models, secrets, plugin data, and local-only plugins.", "migration_notes": "Adds the stable Lumi Companion, server-hosted transcription, Song Overlay, shared paired-device authentication, and synchronized core-plus-bundled-plugin updates. Existing settings, databases, pairing records, tokens, overlays, uploads, models, secrets, plugin data, and local-only plugins are preserved.",
"rollback_safe": true, "rollback_safe": true,
"requirements": [ "requirements": [
"Node.js 18 or newer" "Node.js 18 or newer"
@ -325,18 +325,6 @@
], ],
"rollback_safe": true, "rollback_safe": true,
"migration_notes": "Adds the shared DesignMotionHQ UX foundation, repaired overlay and resource-library workflows, and verified snapshot-backed switching between main and exact experimental branches. Existing content, settings, tokens, overlays, databases, uploads, models, feedback, secrets, plugin data, and local-only plugins are preserved." "migration_notes": "Adds the shared DesignMotionHQ UX foundation, repaired overlay and resource-library workflows, and verified snapshot-backed switching between main and exact experimental branches. Existing content, settings, tokens, overlays, databases, uploads, models, feedback, secrets, plugin data, and local-only plugins are preserved."
},
{
"version": "0.2.26",
"channel": "stable",
"released_at": "2026-07-24",
"compatible_from": "0.1.9",
"migration_kind": "patch",
"replaces_versions": [
"1.2.0"
],
"rollback_safe": true,
"migration_notes": "Adds the stable Lumi Companion, server-hosted transcription, Song Overlay, shared paired-device authentication, and synchronized core-plus-bundled-plugin updates. Existing settings, databases, pairing records, tokens, overlays, uploads, models, secrets, plugin data, and local-only plugins are preserved."
} }
] ]
} }