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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ concurrency:
cancel-in-progress: true

env:
AISUITE_REVISION: d70f8484766728d4ecb650da7c683ced39d33ed5
AISUITE_REVISION: ff130d741ec50ed7dba1890f7981bc62f26f15db
SNODEC_REVISION: bc43179dbee2b5a0286420a61d8f1ceaef01530d
CMAKE_BUILD_PARALLEL_LEVEL: 2
CTEST_PARALLEL_LEVEL: 2
Expand Down Expand Up @@ -42,7 +42,7 @@ jobs:
ref: ${{ env.SNODEC_REVISION }}
path: _deps/snodec

- name: Check out required AISuite 0.5.0
- name: Check out required AISuite 0.6.0
uses: actions/checkout@v5
with:
repository: SNodeC/AISuite
Expand Down Expand Up @@ -87,7 +87,7 @@ jobs:
cmake --build _build/snodec --target all
cmake --install _build/snodec

- name: Build and install AISuite 0.5.0
- name: Build and install AISuite 0.6.0
run: |
cmake -S _deps/aisuite -B _build/aisuite -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
Expand All @@ -101,7 +101,7 @@ jobs:
-DAISUITE_ENABLE_CODEX_FRONTEND_RFCOMM=OFF
cmake --build _build/aisuite --target all
cmake --install _build/aisuite
grep -F 'set(PACKAGE_VERSION "0.5.0")' \
grep -F 'set(PACKAGE_VERSION "0.6.0")' \
_stage/aisuite/lib/cmake/AISuite/AISuiteConfigVersion.cmake

- name: Configure CodexUI
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ include(GNUInstallDirs)

set(CMAKE_AUTOMOC ON)

find_package(AISuite 0.5.0 CONFIG REQUIRED)
find_package(AISuite 0.6.0 CONFIG REQUIRED)
find_package(Qt6 REQUIRED COMPONENTS Concurrent Network Widgets)

qt_add_executable(
Expand Down
14 changes: 8 additions & 6 deletions src/app/AttachmentManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,19 +128,21 @@ bool copyFileAtomically(const QString& sourcePath,
}
if (cancelled && cancelled())
return cancelDestination();
if (!destination.commit()) {
// Apply the final private mode to QSaveFile's temporary inode before its
// atomic rename publishes that inode at destinationPath.
if (!destination.setPermissions(PrivateFilePermissions)) {
destination.cancelWriting();
if (errorMessage)
*errorMessage = errorWithPath(
QStringLiteral("Unable to finish the staged attachment: %1")
.arg(destination.errorString()),
QStringLiteral("Unable to make the staged attachment private."),
destinationPath);
return false;
}
if (!QFile::setPermissions(destinationPath, PrivateFilePermissions)) {
(void)QFile::remove(destinationPath);
if (!destination.commit()) {
if (errorMessage)
*errorMessage = errorWithPath(
QStringLiteral("Unable to make the staged attachment private."),
QStringLiteral("Unable to finish the staged attachment: %1")
.arg(destination.errorString()),
destinationPath);
return false;
}
Expand Down
35 changes: 32 additions & 3 deletions src/app/FrontendSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,25 @@ bool appendUniqueBounded(QStringList& destination, const QStringList& source)
void mergeScope(detail::StateUpdateScope& destination,
const detail::StateUpdateScope& source)
{
// A newer exact update for an identity supersedes an older removal. Apply
// this before appending the source tombstones so remove-after-upsert still
// wins while upsert-after-remove cannot clear a live selection.
for (const QString& threadId : source.affectedThreadIds) {
if (!source.removedThreadIds.contains(threadId))
destination.removedThreadIds.removeAll(threadId);
}
destination.allThreadsAffected |= source.allThreadsAffected;
destination.allInspectorsAffected |= source.allInspectorsAffected;
destination.allSidebarThreadsAffected |= source.allSidebarThreadsAffected;
destination.sidebarAffected |= source.sidebarAffected;
destination.hasPresentationChange |= source.hasPresentationChange;
destination.removedThreadIdsOverflowed |=
source.removedThreadIdsOverflowed;
if (!appendUniqueBounded(destination.removedThreadIds,
source.removedThreadIds)) {
destination.removedThreadIdsOverflowed = true;
destination.allThreadsAffected = true;
}
if (!destination.allThreadsAffected) {
if (!appendUniqueBounded(destination.affectedThreadIds,
source.affectedThreadIds)
Expand Down Expand Up @@ -151,6 +165,8 @@ void mergeScope(detail::StateUpdateScope& destination,
> detail::maximumCoalescedPresentationIdentities
|| destination.fullyAffectedThreadIds.size()
> detail::maximumCoalescedPresentationIdentities
|| destination.removedThreadIds.size()
> detail::maximumCoalescedPresentationIdentities
|| static_cast<qsizetype>(destination.affectedItemContents.size())
> detail::maximumCoalescedPresentationIdentities) {
destination.allThreadsAffected = true;
Expand All @@ -166,6 +182,9 @@ void mergeScope(detail::StateUpdateScope& destination,
if (destination.allThreadsAffected) {
destination.affectedThreadIds.clear();
destination.fullyAffectedThreadIds.clear();
// Keep exact removals even when the rest of the presentation scope
// degrades to an all-thread refresh. Global omission provenance makes
// a missing selected ID ambiguous without this bounded evidence.
destination.affectedItemContents.clear();
destination.coalescedContentDeltaBytes = 0;
}
Expand Down Expand Up @@ -538,6 +557,16 @@ class FrontendSession::Impl
latestState->state = std::move(publication.state);
latestState->archivedStatus = publication.archivedStatus;
mergeScope(latestState->scope, publication.scope);
// The newest immutable State is the final authority for any
// identity it actually retains. Capacity-omitted identities
// remain ambiguous and therefore keep their exact tombstone.
for (auto iterator = latestState->scope.removedThreadIds.begin();
iterator != latestState->scope.removedThreadIds.end();) {
if (latestState->state.thread(iterator->toStdString()))
iterator = latestState->scope.removedThreadIds.erase(iterator);
else
++iterator;
}
} else {
latestState = std::move(publication);
}
Expand Down Expand Up @@ -897,12 +926,12 @@ bool FrontendSession::ownsController() const noexcept
return projection.value && projection.value->ownedByThisClient;
}

void FrontendSession::loadThread(const QString& threadId)
void FrontendSession::loadThread(const QString& threadId, bool retryIncomplete)
{
if (impl->currentLifecycle != Lifecycle::Ready || threadId.isEmpty())
return;
impl->post([threadId](FrontendSessionWorker& worker) {
worker.loadThread(threadId);
impl->post([threadId, retryIncomplete](FrontendSessionWorker& worker) {
worker.loadThread(threadId, retryIncomplete);
});
}

Expand Down
10 changes: 9 additions & 1 deletion src/app/FrontendSession.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,18 @@ struct StateUpdateScope {

QStringList affectedThreadIds;
QStringList fullyAffectedThreadIds;
// Exact authoritative removals must survive mailbox coalescing. An
// omitted thread is otherwise indistinguishable from one deleted by an
// authoritative thread/read while the global snapshot remains bounded.
QStringList removedThreadIds;
QStringList affectedInspectorThreadIds;
QStringList affectedSidebarThreadIds;
std::vector<ItemContentIdentity> affectedItemContents;
std::uint64_t coalescedContentDeltaBytes = 0;
// The bounded list omitted at least one exact removal identity. The UI
// must verify any missing retained selection instead of treating global
// snapshot omission as either presence or deletion.
bool removedThreadIdsOverflowed = false;
bool allThreadsAffected = false;
bool allInspectorsAffected = false;
bool allSidebarThreadsAffected = false;
Expand Down Expand Up @@ -104,7 +112,7 @@ class FrontendSession : public QObject
[[nodiscard]] bool archivedThreadDiscoveryTerminal() const noexcept;
[[nodiscard]] ArchivedThreadDiscoveryStatus archivedThreadDiscoveryStatus() const noexcept;
[[nodiscard]] bool ownsController() const noexcept;
void loadThread(const QString& threadId);
void loadThread(const QString& threadId, bool retryIncomplete = false);
[[nodiscard]] std::optional<QString> acquireController(OperationCompletion completion);
[[nodiscard]] std::optional<QString> startThread(ThreadStartCompletion completion);
[[nodiscard]] std::optional<QString>
Expand Down
Loading