From c78e583399ce97e9aa0919a6865dca868ffc39e5 Mon Sep 17 00:00:00 2001 From: Miguel Ferreira Date: Thu, 4 Jun 2026 21:09:45 +0100 Subject: [PATCH 1/5] feat: opt-in desktop setting to keep replication active in the background Replication is suspended when the Obsidian window becomes hidden (document.hidden), so LiveSync and Periodic stop syncing while minimised until the window is focused. Add keepReplicationActiveInBackground (default off, desktop only). When enabled, the window-visibility handler no longer suspends on hide, so replication keeps running while minimised. Becoming visible forces a teardown before reopen (LiveSync only) so a stalled, half-open channel is always replaced. Includes the setting definition (src/lib submodule), a desktop-only toggle in the Sync pane shown for LiveSync and Periodic, a docs/settings.md entry, and unit tests for the visibility handler. --- docs/settings.md | 5 + src/lib | 2 +- .../essentialObsidian/ModuleObsidianEvents.ts | 32 +++- .../ModuleObsidianEvents.unit.spec.ts | 174 ++++++++++++++++++ .../SettingDialogue/PaneSyncSettings.ts | 11 ++ 5 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 src/modules/essentialObsidian/ModuleObsidianEvents.unit.spec.ts diff --git a/docs/settings.md b/docs/settings.md index e0f2ca5..3727346 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -488,6 +488,11 @@ Automatically Sync all files when opening Obsidian. Setting key: syncAfterMerge Sync automatically after merging files +#### Keep replication active in the background + +Setting key: keepReplicationActiveInBackground +Desktop only; uses more battery and network. + ### 3. Update thinning #### Batch database update diff --git a/src/lib b/src/lib index 76d9167..309bd6c 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 76d91674c235c1ccf991a14802c737e82e144ef1 +Subproject commit 309bd6c8acc9b9940361ab296aa30c6e3b96bb50 diff --git a/src/modules/essentialObsidian/ModuleObsidianEvents.ts b/src/modules/essentialObsidian/ModuleObsidianEvents.ts index 638b960..1129ef0 100644 --- a/src/modules/essentialObsidian/ModuleObsidianEvents.ts +++ b/src/modules/essentialObsidian/ModuleObsidianEvents.ts @@ -2,7 +2,7 @@ import { AbstractObsidianModule } from "../AbstractObsidianModule.ts"; import { EVENT_FILE_RENAMED, EVENT_LEAF_ACTIVE_CHANGED, eventHub } from "../../common/events.js"; import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; import { scheduleTask } from "octagonal-wheels/concurrency/task"; -import { type TFile } from "../../deps.ts"; +import { Platform, type TFile } from "../../deps.ts"; import { fireAndForget } from "octagonal-wheels/promises"; import { type FilePathWithPrefix } from "../../lib/src/common/types.ts"; import { reactive, reactiveSource, type ReactiveSource } from "octagonal-wheels/dataobject/reactive"; @@ -138,12 +138,38 @@ export class ModuleObsidianEvents extends AbstractObsidianModule { await this.services.fileProcessing.commitPendingFileEvents(); + // Desktop opt-in (LiveSync/Periodic only): keep the background channel running while the + // window is hidden, instead of suspending on hide. On hide we skip the suspend for both + // modes (LiveSync's continuous replication and Periodic's timer both stall otherwise); + // becoming visible reopens normally, and for LiveSync additionally forces a teardown first + // (see the resume branch) so a stalled continuous channel is always replaced. + const keepActiveInBackground = + this.settings.keepReplicationActiveInBackground && + (this.settings.liveSync || this.settings.periodicReplication) && + Platform.isDesktopApp; + if (isHidden) { - await this.services.appLifecycle.onSuspending(); + if (!keepActiveInBackground) await this.services.appLifecycle.onSuspending(); } else { // suspend all temporary. if (this.services.appLifecycle.isSuspended()) return; - // Do not block resume by focus state here; visibility recovery should be enough. + // Only the continuous (LiveSync) channel can go stalled-but-not-terminated: PouchDB + // emits paused/retry while the replicator keeps its AbortController set, so the reopen + // below would no-op on exactly the channel that needs replacing. Force a teardown first + // so becoming visible always re-establishes a fresh channel (restoring the default's + // reset-on-visibility). Periodic mode has no such channel — its timer just resumes via + // the normal path below — so this teardown is gated on liveSync to avoid needlessly + // bouncing it. The teardown's closeReplication() aborts synchronously while the reopen is + // deferred (fireAndForget + awaited isReplicationReady/initializeDatabaseForReplication), + // so the aborted continuousReplication run (and its shareRunningResult lock) unwinds in + // microtasks before the reopen runs: it neither double-opens nor gets swallowed by the + // still-registered shared run. + if (keepActiveInBackground && this.settings.liveSync) { + await this.services.appLifecycle.onSuspending(); + } + // Resume is not gated on focus in this branch, but note the top-of-handler check + // (isLastHidden && !hasFocus) still defers the whole handler when the window becomes + // visible again while unfocused; in that case recovery happens on the next focus. await this.services.appLifecycle.onResuming(); await this.services.appLifecycle.onResumed(); } diff --git a/src/modules/essentialObsidian/ModuleObsidianEvents.unit.spec.ts b/src/modules/essentialObsidian/ModuleObsidianEvents.unit.spec.ts new file mode 100644 index 0000000..8324b18 --- /dev/null +++ b/src/modules/essentialObsidian/ModuleObsidianEvents.unit.spec.ts @@ -0,0 +1,174 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +// Unit tests stub out `obsidian`, so deps.ts (which re-exports from it) can't be loaded for real. +// ModuleObsidianEvents only needs `Platform` from deps at runtime; provide a mutable stub so each +// test can choose desktop vs mobile. +vi.mock("../../deps.ts", () => ({ Platform: { isDesktopApp: true } })); + +import { Platform } from "../../deps.ts"; +import { ModuleObsidianEvents } from "./ModuleObsidianEvents"; +import { DEFAULT_SETTINGS, REMOTE_COUCHDB } from "@lib/common/types"; + +type SetupOptions = { + settings?: Partial; + hidden: boolean; + isLastHidden?: boolean; + hasFocus?: boolean; + isSuspended?: boolean; +}; + +function setup(opts: SetupOptions) { + const appLifecycle = { + isReady: vi.fn(() => true), + isSuspended: vi.fn(() => opts.isSuspended ?? false), + onSuspending: vi.fn(async () => true), + onResuming: vi.fn(async () => true), + onResumed: vi.fn(async () => true), + }; + const fileProcessing = { commitPendingFileEvents: vi.fn(async () => true) }; + + const core = { + _services: { + API: { + addLog: vi.fn(), + addCommand: vi.fn(), + registerWindow: vi.fn(), + addRibbonIcon: vi.fn(), + registerProtocolHandler: vi.fn(), + }, + setting: { saveSettingData: vi.fn(async () => undefined) }, + appLifecycle, + fileProcessing, + }, + settings: { + ...DEFAULT_SETTINGS, + remoteType: REMOTE_COUCHDB, + isConfigured: true, + ...opts.settings, + }, + } as any; + Object.defineProperty(core, "services", { get: () => core._services }); + + const module = new ModuleObsidianEvents({} as any, core); + module.isLastHidden = opts.isLastHidden ?? false; + module.hasFocus = opts.hasFocus ?? true; + + // The handler reads `activeWindow.document.hidden`. + (globalThis as any).activeWindow = { document: { hidden: opts.hidden } }; + + return { module, appLifecycle, fileProcessing }; +} + +describe("watchWindowVisibilityAsync — keepReplicationActiveInBackground", () => { + beforeEach(() => { + (Platform as any).isDesktopApp = true; + }); + + afterEach(() => { + // The handler reads a global `activeWindow`; the Platform mock is module-scoped. Both would + // otherwise leak into sibling spec files running in the same worker, so reset them here. + delete (globalThis as any).activeWindow; + (Platform as any).isDesktopApp = true; + }); + + it("does NOT suspend on hide when enabled in LiveSync mode on the desktop app", async () => { + const { module, appLifecycle } = setup({ + settings: { keepReplicationActiveInBackground: true, liveSync: true }, + hidden: true, + }); + await module.watchWindowVisibilityAsync(); + expect(appLifecycle.onSuspending).not.toHaveBeenCalled(); + }); + + it("suspends on hide by default (setting off)", async () => { + const { module, appLifecycle } = setup({ + settings: { keepReplicationActiveInBackground: false, liveSync: true }, + hidden: true, + }); + await module.watchWindowVisibilityAsync(); + expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1); + }); + + it("forces onSuspending before the resume on becoming visible when enabled (LiveSync teardown)", async () => { + const { module, appLifecycle } = setup({ + settings: { keepReplicationActiveInBackground: true, liveSync: true }, + hidden: false, + isLastHidden: true, // hidden -> visible transition + }); + await module.watchWindowVisibilityAsync(); + // Decision-logic only: on visible + enabled + LiveSync the handler calls onSuspending (the + // forced teardown) before onResuming. The actual stalled-channel replacement is exercised by + // the manual integration test, not here. + expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1); + expect(appLifecycle.onResuming).toHaveBeenCalledTimes(1); + expect(appLifecycle.onResumed).toHaveBeenCalledTimes(1); + expect(appLifecycle.onSuspending.mock.invocationCallOrder[0]).toBeLessThan( + appLifecycle.onResuming.mock.invocationCallOrder[0] + ); + }); + + it("does not force a teardown on becoming visible by default (setting off)", async () => { + const { module, appLifecycle } = setup({ + settings: { keepReplicationActiveInBackground: false, liveSync: true }, + hidden: false, + isLastHidden: true, + }); + await module.watchWindowVisibilityAsync(); + expect(appLifecycle.onSuspending).not.toHaveBeenCalled(); + expect(appLifecycle.onResumed).toHaveBeenCalledTimes(1); + }); + + it("does not apply in On-Events mode even if the flag is set (no scope leak)", async () => { + const { module, appLifecycle } = setup({ + settings: { + keepReplicationActiveInBackground: true, + liveSync: false, + periodicReplication: false, + }, + hidden: true, + }); + await module.watchWindowVisibilityAsync(); + expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1); + }); + + it("does NOT suspend on hide when enabled in Periodic mode (the periodic timer also stalls otherwise)", async () => { + const { module, appLifecycle } = setup({ + settings: { + keepReplicationActiveInBackground: true, + liveSync: false, + periodicReplication: true, + }, + hidden: true, + }); + await module.watchWindowVisibilityAsync(); + expect(appLifecycle.onSuspending).not.toHaveBeenCalled(); + }); + + it("does NOT force a teardown on becoming visible in Periodic mode (only the continuous channel can stall)", async () => { + const { module, appLifecycle } = setup({ + settings: { + keepReplicationActiveInBackground: true, + liveSync: false, + periodicReplication: true, + }, + hidden: false, + isLastHidden: true, + }); + await module.watchWindowVisibilityAsync(); + // The teardown is gated on liveSync: a periodic timer doesn't go half-open, so bouncing it + // on every restore would be needless churn. Resume still runs normally. + expect(appLifecycle.onSuspending).not.toHaveBeenCalled(); + expect(appLifecycle.onResuming).toHaveBeenCalledTimes(1); + expect(appLifecycle.onResumed).toHaveBeenCalledTimes(1); + }); + + it("does not apply on a non-desktop app even if the flag is set", async () => { + (Platform as any).isDesktopApp = false; + const { module, appLifecycle } = setup({ + settings: { keepReplicationActiveInBackground: true, liveSync: true }, + hidden: true, + }); + await module.watchWindowVisibilityAsync(); + expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/modules/features/SettingDialogue/PaneSyncSettings.ts b/src/modules/features/SettingDialogue/PaneSyncSettings.ts index 394c7c1..49f2c95 100644 --- a/src/modules/features/SettingDialogue/PaneSyncSettings.ts +++ b/src/modules/features/SettingDialogue/PaneSyncSettings.ts @@ -11,6 +11,7 @@ import { EVENT_REQUEST_COPY_SETUP_URI, eventHub } from "../../../common/events.t import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { PageFunctions } from "./SettingPane.ts"; import { visibleOnly } from "./SettingPane.ts"; +import { Platform } from "../../../deps.ts"; export function paneSyncSettings( this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, @@ -189,6 +190,16 @@ export function paneSyncSettings( new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnFileOpen", { onUpdate: onlyOnNonLiveSync }); new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncOnStart", { onUpdate: onlyOnNonLiveSync }); new Setting(paneEl).setClass("wizardHidden").autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync }); + // Desktop app only, and only for the sync modes that keep a background replication channel + // (LiveSync and Periodic). Ignored on mobile, where suspending preserves battery. The + // visibility predicate mirrors the runtime guard in ModuleObsidianEvents. + if (Platform.isDesktopApp) { + new Setting(paneEl).setClass("wizardHidden").autoWireToggle("keepReplicationActiveInBackground", { + onUpdate: visibleOnly( + () => this.isConfiguredAs("syncMode", "LIVESYNC") || this.isConfiguredAs("syncMode", "PERIODIC") + ), + }); + } }); void addPanel( From 4cf4acf7e9003afa0a0856347674f81d15e8dd7f Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Tue, 9 Jun 2026 09:00:43 +0000 Subject: [PATCH 2/5] feat: Docker CI workflow to enhance image tagging and push logic based on branch and event type --- .github/workflows/cli-docker.yml | 41 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/.github/workflows/cli-docker.yml b/.github/workflows/cli-docker.yml index a47e58c..9bbd6bd 100644 --- a/.github/workflows/cli-docker.yml +++ b/.github/workflows/cli-docker.yml @@ -8,6 +8,8 @@ name: Build and Push CLI Docker Image on: push: + branches: + - main tags: - "*.*.*-cli" workflow_dispatch: @@ -41,14 +43,32 @@ jobs: id: meta run: | VERSION=$(jq -r '.version' manifest.json) - EPOCH=$(date +%s) - TAG="${VERSION}-${EPOCH}-cli" + SHORT_SHA=$(git rev-parse --short HEAD) IMAGE="ghcr.io/${{ github.repository_owner }}/livesync-cli" - echo "tag=${TAG}" >> $GITHUB_OUTPUT - echo "image=${IMAGE}" >> $GITHUB_OUTPUT - echo "full=${IMAGE}:${TAG}" >> $GITHUB_OUTPUT - echo "version=${IMAGE}:${VERSION}-cli" >> $GITHUB_OUTPUT - echo "latest=${IMAGE}:latest" >> $GITHUB_OUTPUT + + # Build tag list based on the event and git ref + TAGS="" + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + # Stable release builds + TAGS="${IMAGE}:${VERSION}-cli,${IMAGE}:latest,${IMAGE}:${VERSION}-sha-${SHORT_SHA}-cli" + elif [[ "${{ github.ref }}" == refs/heads/main ]]; then + # Bleeding-edge / nightly builds + TAGS="${IMAGE}:edge,${IMAGE}:${VERSION}-dev-sha-${SHORT_SHA}-cli" + else + # Other branches / manual run fallback + TAGS="${IMAGE}:${VERSION}-dev-sha-${SHORT_SHA}-cli" + fi + + # Determine if the image should be pushed + PUSH="true" + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + if [[ "${{ inputs.dry_run }}" == "true" ]]; then + PUSH="false" + fi + fi + + echo "tags=${TAGS}" >> $GITHUB_OUTPUT + echo "push=${PUSH}" >> $GITHUB_OUTPUT - name: Log in to GitHub Container Registry uses: docker/login-action@v3 @@ -92,10 +112,7 @@ jobs: with: context: . file: src/apps/cli/Dockerfile - push: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }} - tags: | - ${{ steps.meta.outputs.full }} - ${{ steps.meta.outputs.version }} - ${{ steps.meta.outputs.latest }} + push: ${{ steps.meta.outputs.push }} + tags: ${{ steps.meta.outputs.tags }} cache-from: type=gha cache-to: type=gha,mode=max From 292a6b9e1ed7ba412a8f5031631c533f140fab72 Mon Sep 17 00:00:00 2001 From: Miguel Ferreira Date: Wed, 10 Jun 2026 10:45:40 +0100 Subject: [PATCH 3/5] refactor: detect platform via APIService.isMobile() instead of Platform.isDesktopApp Address the maintainer review on #949: determine the platform through the plugin's own service layer (services.API.isMobile()) rather than Obsidian's Platform API directly, matching the existing call in ObsidianLiveSyncSettingTab. Applies to both PR-introduced sites: the runtime guard (ModuleObsidianEvents) and the settings-pane toggle (PaneSyncSettings). The TFile import becomes type-only so deps.ts is no longer pulled at runtime; the unit test drives the platform through the services.API.isMobile() mock. Co-Authored-By: Claude Opus 4.8 --- .../essentialObsidian/ModuleObsidianEvents.ts | 4 ++-- .../ModuleObsidianEvents.unit.spec.ts | 24 +++++++------------ .../SettingDialogue/PaneSyncSettings.ts | 3 +-- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/modules/essentialObsidian/ModuleObsidianEvents.ts b/src/modules/essentialObsidian/ModuleObsidianEvents.ts index 1129ef0..263e862 100644 --- a/src/modules/essentialObsidian/ModuleObsidianEvents.ts +++ b/src/modules/essentialObsidian/ModuleObsidianEvents.ts @@ -2,7 +2,7 @@ import { AbstractObsidianModule } from "../AbstractObsidianModule.ts"; import { EVENT_FILE_RENAMED, EVENT_LEAF_ACTIVE_CHANGED, eventHub } from "../../common/events.js"; import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; import { scheduleTask } from "octagonal-wheels/concurrency/task"; -import { Platform, type TFile } from "../../deps.ts"; +import type { TFile } from "../../deps.ts"; import { fireAndForget } from "octagonal-wheels/promises"; import { type FilePathWithPrefix } from "../../lib/src/common/types.ts"; import { reactive, reactiveSource, type ReactiveSource } from "octagonal-wheels/dataobject/reactive"; @@ -146,7 +146,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule { const keepActiveInBackground = this.settings.keepReplicationActiveInBackground && (this.settings.liveSync || this.settings.periodicReplication) && - Platform.isDesktopApp; + !this.services.API.isMobile(); if (isHidden) { if (!keepActiveInBackground) await this.services.appLifecycle.onSuspending(); diff --git a/src/modules/essentialObsidian/ModuleObsidianEvents.unit.spec.ts b/src/modules/essentialObsidian/ModuleObsidianEvents.unit.spec.ts index 8324b18..ed3b005 100644 --- a/src/modules/essentialObsidian/ModuleObsidianEvents.unit.spec.ts +++ b/src/modules/essentialObsidian/ModuleObsidianEvents.unit.spec.ts @@ -1,11 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, vi, afterEach } from "vitest"; -// Unit tests stub out `obsidian`, so deps.ts (which re-exports from it) can't be loaded for real. -// ModuleObsidianEvents only needs `Platform` from deps at runtime; provide a mutable stub so each -// test can choose desktop vs mobile. -vi.mock("../../deps.ts", () => ({ Platform: { isDesktopApp: true } })); - -import { Platform } from "../../deps.ts"; import { ModuleObsidianEvents } from "./ModuleObsidianEvents"; import { DEFAULT_SETTINGS, REMOTE_COUCHDB } from "@lib/common/types"; @@ -15,6 +9,8 @@ type SetupOptions = { isLastHidden?: boolean; hasFocus?: boolean; isSuspended?: boolean; + // Platform is read via services.API.isMobile(); default desktop (false) so the feature applies. + isMobile?: boolean; }; function setup(opts: SetupOptions) { @@ -35,6 +31,7 @@ function setup(opts: SetupOptions) { registerWindow: vi.fn(), addRibbonIcon: vi.fn(), registerProtocolHandler: vi.fn(), + isMobile: vi.fn(() => opts.isMobile ?? false), }, setting: { saveSettingData: vi.fn(async () => undefined) }, appLifecycle, @@ -60,15 +57,10 @@ function setup(opts: SetupOptions) { } describe("watchWindowVisibilityAsync — keepReplicationActiveInBackground", () => { - beforeEach(() => { - (Platform as any).isDesktopApp = true; - }); - afterEach(() => { - // The handler reads a global `activeWindow`; the Platform mock is module-scoped. Both would - // otherwise leak into sibling spec files running in the same worker, so reset them here. + // The handler reads a global `activeWindow`; clear it so it doesn't leak into sibling spec + // files running in the same worker. delete (globalThis as any).activeWindow; - (Platform as any).isDesktopApp = true; }); it("does NOT suspend on hide when enabled in LiveSync mode on the desktop app", async () => { @@ -162,11 +154,11 @@ describe("watchWindowVisibilityAsync — keepReplicationActiveInBackground", () expect(appLifecycle.onResumed).toHaveBeenCalledTimes(1); }); - it("does not apply on a non-desktop app even if the flag is set", async () => { - (Platform as any).isDesktopApp = false; + it("does not apply on mobile even if the flag is set", async () => { const { module, appLifecycle } = setup({ settings: { keepReplicationActiveInBackground: true, liveSync: true }, hidden: true, + isMobile: true, }); await module.watchWindowVisibilityAsync(); expect(appLifecycle.onSuspending).toHaveBeenCalledTimes(1); diff --git a/src/modules/features/SettingDialogue/PaneSyncSettings.ts b/src/modules/features/SettingDialogue/PaneSyncSettings.ts index 49f2c95..294b318 100644 --- a/src/modules/features/SettingDialogue/PaneSyncSettings.ts +++ b/src/modules/features/SettingDialogue/PaneSyncSettings.ts @@ -11,7 +11,6 @@ import { EVENT_REQUEST_COPY_SETUP_URI, eventHub } from "../../../common/events.t import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; import type { PageFunctions } from "./SettingPane.ts"; import { visibleOnly } from "./SettingPane.ts"; -import { Platform } from "../../../deps.ts"; export function paneSyncSettings( this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, @@ -193,7 +192,7 @@ export function paneSyncSettings( // Desktop app only, and only for the sync modes that keep a background replication channel // (LiveSync and Periodic). Ignored on mobile, where suspending preserves battery. The // visibility predicate mirrors the runtime guard in ModuleObsidianEvents. - if (Platform.isDesktopApp) { + if (!this.services.API.isMobile()) { new Setting(paneEl).setClass("wizardHidden").autoWireToggle("keepReplicationActiveInBackground", { onUpdate: visibleOnly( () => this.isConfiguredAs("syncMode", "LIVESYNC") || this.isConfiguredAs("syncMode", "PERIODIC") From 445a8c747cf4b0f9859f7d4722ed618bed1c5e8d Mon Sep 17 00:00:00 2001 From: Miguel Ferreira Date: Wed, 10 Jun 2026 10:45:40 +0100 Subject: [PATCH 4/5] chore(submodule): bump src/lib to commonlib main (#51 merged as e98d929) vrtmrz/livesync-commonlib#51 is merged into commonlib main as e98d929. Repoint the gitlink from the PR branch commit to that merge commit so this PR builds against upstream main. Co-Authored-By: Claude Opus 4.8 --- src/lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib b/src/lib index 309bd6c..e98d929 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 309bd6c8acc9b9940361ab296aa30c6e3b96bb50 +Subproject commit e98d929041027c3ea79e756f347b14adbb09a982 From c4faade30ccda39c4217cd9712fde2284a30785c Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sat, 13 Jun 2026 11:13:35 +0900 Subject: [PATCH 5/5] Update submodule pointer --- src/lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib b/src/lib index 53804cb..e98d929 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 53804cbaec7fed9591321e7fbe6dcc9092e51017 +Subproject commit e98d929041027c3ea79e756f347b14adbb09a982