Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Make cold-start archive discovery and commit failure-safe

Status: implemented
Translation: current

Contract: [Session relations and operation targets](../../../../specs/session-relations.md)
Implementation: [#658](https://github.com/LodyAI/Lody/pull/658)

[中文](2026-09-13-session-archive-complete-metadata-query.zh.md)

## Abstract

An interactive root Session could be archived before the client metadata projection
contained its direct child Tabs. Archive now discovers targets from a per-action
repository metadata query and commits direct children before the root. Failed writes
enter compensation before the action rejects, and terminal cleanup starts only after
the metadata set commits.

## Decision

The archive action obtains the workspace metadata index before authoring any state
change. It normalizes Session ids from room ids, selects only direct `parentSessionId`
children, and rechecks that the captured workspace runtime is still active before
writing. The rendered root remains a fallback when the query lags that already-visible
document, but descendant discovery never falls back to the UI cache. “Complete” means
the repository snapshot observed by the query, not children created after it.

Waiting for `docMetaCacheReadyAtom` was rejected. Readiness belongs to an asynchronous
UI projection whose live-event metadata fetch can fail or remain unresolved; making a
user action wait for that global signal would introduce an unbounded pending state.
The repository index is already the source used to build that projection and gives the
archive action an explicit success or failure boundary.

LoroRepo does not provide a cross-document rollback transaction. The action therefore
writes children before the root and treats the root write as its final commit point. On
failure it attempts to restore every attempted target's prior `isArchived` and `status`
values. Root compensation happens first; if it fails, children remain archived and an
error reports both the write and rollback failures, preserving the root-archived
implication. The captured runtime stays authoritative after the first write, so a
workspace switch cannot split one commit across runtimes. Terminal closure is a
best-effort post-commit cleanup: metadata failure closes no terminal, while an IPC
failure cannot undo or hide an already durable archive.

This change intentionally leaves restore and archived-root deletion behavior unchanged.
It does not expand lifecycle ownership to `openedBySessionId` or
`openedByRootSessionId`; independent Sessions continue to survive opener archive.

## Verification

The owning hook suite exercises the production `getMeta().scan()` path with a UI cache
containing only the root while the repository contains its direct child and independently
opened Sessions. It verifies the target set and terminal set, child-write and final root
write compensation, zero terminal effects on metadata failure, and both workspace switch
boundaries: abort before the first write and finish against the captured runtime after it.

This was the initial mitigation for [#574](https://github.com/LodyAI/Lody/issues/574).
The coordinated local topology now supersedes its compensation path with the
[durable lifecycle operation design](../../proposed/architecture/2026-09-13-session-lifecycle-commit.md);
cloud and dual topologies retain this implementation until their independently
deployed writers can be fenced. The containment decision remains in
[Keep opened Sessions outside opener state cascades](2026-09-10-session-containment-lifecycle.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 让冷启动归档的目标发现与提交具备失败安全性

Status: implemented
Translation: current

Contract: [Session 关系与操作目标](../../../../specs/session-relations.md)
实现:[PR #658](https://github.com/LodyAI/Lody/pull/658)

[English](2026-09-13-session-archive-complete-metadata-query.md)

## 摘要

根 Session 已可交互时,客户端元数据投影可能还没有包含它的直接子 Tab,导致过早归档。
现在每次归档都会查询仓库元数据来发现目标,并按先直接子项、后根 Session 的顺序提交。
写入失败会先进入补偿再拒绝操作;只有整组元数据提交成功后,才开始清理终端。

## 决策

归档操作会在写入任何状态之前取得工作区元数据索引。它从 room id 补全 Session id,只选择直接的
`parentSessionId` 子项,并在写入前重新确认捕获的工作区 runtime 仍然处于活动状态。当索引暂时落后于
已经可见的根 Session 时,仍可回退到已渲染的根元数据;但发现后代时绝不回退到 UI 缓存。
这里的“完整”仅指本次查询所观察到的仓库快照,不包括快照之后新建的子项。

我们没有选择等待 `docMetaCacheReadyAtom`。就绪状态属于异步 UI 投影;实时事件触发的元数据读取可能失败或
长期不返回。让用户操作等待这个全局信号会引入无期限 pending 状态。仓库索引本就是构建该投影的数据源,
并且能为归档操作提供明确的成功或失败边界。

LoroRepo 不提供跨文档回滚事务。因此归档先写子项,最后写根 Session,并把根写入作为最终提交点。
发生失败时,会尝试恢复所有已尝试目标原有的 `isArchived` 与 `status`。根 Session 会先补偿;若根补偿
也失败,则保留子项的已归档状态,并在同一错误中同时报告写入与补偿失败,从而保持“根已归档则子项也已归档”。
第一笔写入开始后,操作始终固定在捕获到的 runtime 上,因此切换工作区不会把同一次提交拆到两个
runtime。终端关闭是提交后的尽力清理:元数据失败不会关闭任何终端,而 IPC 失败也不会撤销或掩盖已经
持久化的归档。

本次改动刻意不改变恢复与归档根永久删除的行为。它也不会把生命周期所有权扩大到
`openedBySessionId` 或 `openedByRootSessionId`;独立 Session 在开启者被归档后继续存活。

## 验证

所属 hook 测试通过生产 `getMeta().scan()` 路径执行归档:UI 缓存只包含根 Session,而仓库同时包含
直接子项和独立打开的 Session。测试覆盖目标与终端集合、子项写入失败与最终根写入失败的补偿、
元数据失败时不关闭终端,以及切换工作区的两个边界:首笔写入前中止,首笔写入后继续在捕获的 runtime
完成提交。

这是 [#574](https://github.com/LodyAI/Lody/issues/574) 的初始缓解方案。协调升级的本地拓扑现在以
[持久 lifecycle 操作设计](../../proposed/architecture/2026-09-13-session-lifecycle-commit.zh.md)
取代其补偿路径;cloud 与 dual 拓扑会保留本实现,直到能够隔离独立发布的 writer。包含关系决策仍由
[让被打开的 Session 不受开启者状态级联影响](2026-09-10-session-containment-lifecycle.zh.md)维护。
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Commit Session lifecycle operations as one durable fact

Status: proposed
Translation: current

Contract: [Session relations](../../../../specs/session-relations.md)

[中文](2026-09-13-session-lifecycle-commit.zh.md)

## Abstract

The legacy product topology compensates failed metadata writes by restoring a previously
read snapshot, which can overwrite legitimate concurrent writes and can itself leave
only some targets changed. The local OSS topology now records each archive or restore as one
immutable operation and derives effective Session lifecycle state through a shared
repository projection. The operation is the unit of conflict resolution, persistence,
and publication; resource cleanup follows the resulting state. Real dependency probes
establish local WASM rollback, and real IndexedDB, SQLite, and LoroRepo tests cover the
replacement boundary. Product mixed-client admission remains unavailable, so the wider
rollout remains proposed and #574 is not complete for that topology.

## Decision and scope

Keep repository-based discovery of the selected Session and direct `parentSessionId`
children, explicit workspace ownership, and post-commit terminal cleanup. Replace
`writeArchiveStateFailureSafe`, ordering-dependent writes, `attemptedTargets`, old-value
compensation, and rollback-error aggregation with a repository lifecycle command.
Archive and restore move together because both write the same authority.

One operation freezes its target ids, desired archived state, stable identity, and
ordering information. Its payload is immutable across retries. The shared resolver
orders whole operations and publishes one effective metadata revision; it never
persists an independently authoritative flag for every target. Independent Tab actions
remain valid and use the same operation model with a singleton target set. Execution
status remains runtime-owned and is never restored from a lifecycle snapshot.

Each target takes the highest ordered operation covering it. Identical frozen root
sets choose one winner together; different sets retain earlier results for targets
absent from the newer operation. This is deterministic operation precedence, not a
promise that all children always have the root's state.

Durable admission includes crash-safe local order allocation and recovery of admitted
but unpublished records before accepting new commands. Publication and recovery allow
duplicate delivery of the same operation or revision; subscribers and resource effects
are idempotent rather than claiming exactly-once notifications. Resource work must also
avoid tearing down a newer runtime generation after restore.

This is a lifecycle-specific protocol, not a generic saga, command queue, or distributed
database transaction framework. A compatible client still authors locally against its
own repository. No daemon proxy author, authenticated cloud requirement, or hosted
implementation is introduced into the public desktop.

The frozen v1 wire and admission layout are:

| Boundary | v1 contract |
| --- | --- |
| Replicated record | One canonical JSON string per immutable operation at metadata document `_lody/session-lifecycle-operations/v1`, field `operation:<operationId>`. |
| Ordering | Canonical non-negative decimal Lamport counter, then UTF-8 byte order of `actorId` and `operationId`. |
| Browser admission | IndexedDB `lody-session-lifecycle-v1:<workspaceId>`, with `admissions` and `state` object stores. |
| CLI admission | Dedicated `session-lifecycle.sqlite3` under the workspace Loro storage directory, with operation and high-water tables. |
| Result | A durable receipt distinguishes `published` from `pending`; an unconfirmed storage result carries the same queryable operation id. |
| Migration | Existing archived rows seed deterministic counter-zero `baseline:v1:<sessionId>` operations. Active rows remain the default baseline. |

Admission and local high-water allocation share one storage transaction. The owner
installs the complete resolver revision before notifying per-Session readers, replays
unpublished admissions at startup, rejects conflicting reuse of an operation id, and
does not compact v1 history. Direct legacy `isArchived` writes are rejected after local
activation, except an initial `false` for a Session with no lifecycle winner.

## Dependency evidence

Inspection used Lody commit `54623883be77bd17f9dab18ef5a60cc2a9b156ef` and installed
artifacts matching `loro-repo@0.20.0` and `@loro-dev/flock-wasm@0.4.3`. Synthetic Node
probes used in-memory replicas; they did not operate on product Session data.
The baseline probe checked rollback and repository publication against those pinned
versions; the durable contract now lives in the owning shared, browser, and CLI tests
listed below.

| Boundary | Observed behavior | Consequence |
| ------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Metadata storage | LoroRepo imports WASM Flock and stores fields at `m/docId/field` in one meta Flock. | Session documents are not separate metadata transaction stores. |
| WASM callback throws | Staged values disappear; no event or exported change remains. A peer's already imported update survives. | Local rollback can avoid authoring stale compensation. |
| Other Flock implementation | `@loro-dev/flock@4.4.4` retains data after the same throwing callback. | Pin and test the actual adapter, not the `txn` method name. |
| Direct raw metadata transaction | Raw values change while primed LoroRepo caches stay old and no repo patch is emitted. | Application hooks must not bypass the repository cache/event owner. |
| Remote repository import | One raw batch becomes per-document callbacks; a callback can read child archived and root active. | Atomic publication must include repository and consumer projections. |
| Concurrent raw transactions | Independent per-key clocks can converge to a mixed root/child tuple. | A local multi-key transaction is not whole-operation conflict resolution. |
| Durability | `upsertDocMeta` does not await persistence; `persistMetaNow` is separate. | Acceptance, local durability, and remote acknowledgement need distinct results. |

The WASM package's shipped comments warn that data does not roll back, contradicting
the tested binary. Treat the observed behavior as version-specific evidence and keep
a dependency characterization gate. Importing into an active WASM transaction can
auto-commit it; transaction callbacks must contain no import or async work.

The frontend explicitly disables metadata auto-debounce in
[`create-workspace-runtime.ts`](../../../../packages/components/src/providers/create-workspace-runtime.ts).
That makes raw `txn` callable there, but does not repair the cache/event bypass.
The repository's existing [dependency patch](../../../../patches/loro-repo.patch)
only changes live-monitor startup, not transaction semantics.

## Alternatives and limits

Reordering compensation cannot determine ownership of current values. A local
operation-id comparison followed by a blind write cannot account for a remote edit
that has not arrived, and conditional per-target rollback still permits partial
completion. Neither alternative supplies the required operation boundary.

A repository-owned multi-key batch is useful infrastructure, but alone does not
preserve a transaction through per-key CRDT conflict resolution. Use native atomic
staging where necessary; do not make a general batch API a prerequisite if writing
one complete lifecycle record supplies the smaller boundary.

A background reconciler can rebuild derived state from durable operations. It cannot
infer whether `root active / child archived` is an intentional Tab action or failed
compensation from those flags alone. Existing worktree GC reconciles disk resources,
not lifecycle metadata, and remains responsible only for root-owned worktrees.

The local OSS renderer and daemon are one coordinated distribution and enable the new
authority only when the runtime is local-only. Cloud and dual runtimes keep the legacy
path because a daemon capability cannot fence independently authoring old renderers.
The public repository does not contain all product clients or a workspace-wide writer
admission mechanism; product activation requires that external evidence and must not
use a weaker dual-write mode.

## Relationship to earlier decisions

This proposal retains the containment decision in
[Keep opened Sessions outside opener state cascades](../../implemented/bug-fix/2026-09-10-session-containment-lifecycle.md).
It proposes replacing the compensation decision in
[Make cold-start archive discovery and commit failure-safe](../../implemented/bug-fix/2026-09-13-session-archive-complete-metadata-query.md),
while preserving that change's repository discovery. The older implemented note
records its historical implementation; it is not approval of this replacement.

## Verification and rollout

The local implementation has deterministic parser/resolver tests, real browser
IndexedDB reload coverage, real SQLite close/reopen and multi-connection allocation,
and a two-LoroRepo test proving the first projected read sees every target together.
Renderer and CLI producer suites assert one lifecycle commit; the UI cache installs a
revision in one write. Resource tests hold an old runtime termination across a newer
restore and prove the replacement generation is not archived or assigned idle status.
The `LODY-SESSION-004` desktop journey injects publication failure after durable browser
admission, reloads, and verifies that the same operation is replayed for the root and
direct child while independently opened Sessions remain active.

These checks authorize the local-only switch, not product activation. Cloud and dual
runtimes deliberately retain the legacy implementation until every independently
deployed writer can be admitted or rejected by a shared compatibility boundary.
Worktree cleanup and terminal disposal follow effective committed state and cannot make
lifecycle commits reversible.

[#658](https://github.com/LodyAI/Lody/pull/658) is the affected implementation.
[#574](https://github.com/LodyAI/Lody/issues/574) remains open until the product
compatibility gate has evidence. Local documentation checks may report links into
uninitialized ACP submodules; those findings are separate from implementation
verification.
Loading