Skip to content

refactor: make isArchived the sole session archive authority - #3074

Merged
Astro-Han merged 7 commits into
mainfrom
refactor/2984-converge-archive-state
Aug 18, 2026
Merged

refactor: make isArchived the sole session archive authority#3074
Astro-Han merged 7 commits into
mainfrom
refactor/2984-converge-archive-state

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • make SessionHeader.isArchived the sole durable and public authority for Session archive state
  • remove archived from SessionStatus, remove Session archivedAt, and collapse duplicate archive guards
  • keep Runtime Host retirement as the only lifecycle orchestration path by deleting direct Runtime/Storage archive methods and excluding isArchived from generic header patches
  • delete the unused generic SessionManager.updateSession configuration path instead of preserving a second configuration authority
  • add SQLite metadata migration v25 to normalize legacy archived payloads and drop the unused status / status_updated_at shadow columns and index
  • preserve execution status across archive/restore, retain archived-row UI coverage, and isolate the wire break with compatibility epoch 21 and continuity schema 4

Refs #2984

Verification

  • node --test packages/storage/dist/__tests__/sqlite-session-metadata-store.test.js — 43 passed
  • focused Runtime Host protocol, retirement, resource, Goal, projector, and epoch-skew tests — 64 passed
  • node --test packages/runtime/dist/__tests__/session-manager.test.js packages/runtime/dist/__tests__/model-factory-thinking.test.js — 246 passed
  • node --test packages/cli/dist/__tests__/runtime-host-session-driver.test.js — 27 passed
  • focused Desktop continuity consumers — 78 passed
  • builds/typechecks passed for core, storage, runtime, runtime-host, CLI, Desktop main, and UI
  • npm run format:check
  • npm run lint

Full-repository tests were not run locally; CI owns that coverage.

Migration

Schema v25 maps legacy status: "archived" payloads to active, removes stale blockedReason, statusUpdatedAt, and archivedAt, increments their metadata revision, and then drops the two duplicate status projection columns. The indexed is_archived projection remains unchanged.

Mapping legacy archived rows to active matches the previous restore behavior: the old archive write overwrote the prior execution status, so no more precise state can be recovered. New archive and restore operations preserve the current execution status.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex performed the simplification audit, implemented the migration and authority consolidation, adjudicated review feedback, added and ran focused tests, and drafted this PR description. Claude and independent Codex subagents performed read-only adversarial reviews. The human contributor owns review and submission.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e36b7f5d-bfe3-4af7-9cdb-e3ebe09f287a

📥 Commits

Reviewing files that changed from the base of the PR and between 692478b and 85a1085.

📒 Files selected for processing (2)
  • packages/runtime-host/src/__tests__/handshake-compatibility.test.ts
  • packages/storage/src/__tests__/sqlite-session-metadata-store.test.ts

Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review.


📝 Walkthrough

Problem solved

The PR makes SessionHeader.isArchived the sole durable and public authority for archive state.

It removes SessionStatus.archived, Session.archivedAt, and direct Runtime and Storage archive APIs. Archive and restore preserve independent execution status.

It migrates legacy SQLite archive data, removes obsolete status projections, and updates protocol compatibility and continuity schemas.

Source of truth

The PR extends the existing SessionHeader.isArchived authority. It does not create a parallel archive-state path.

Runtime Host retirement remains the only lifecycle orchestration path. SessionHeaderPatch prevents generic header updates from modifying isArchived.

Complexity delta

The PR removes:

  • The archived session status.
  • The archivedAt field.
  • Direct Runtime and Storage archive APIs.
  • The generic SessionManager.updateSession path.
  • Status-based archive branches.
  • Obsolete SQLite status columns and indexes.
  • Unused test-store methods and configuration-update tests.

The PR adds:

  • A dedicated archive persistence operation.
  • Archive-specific migration logic.
  • Compatibility epoch and continuity schema changes.
  • Migration, archive, lifecycle, Bot adapter, CAS, and protocol tests.
  • Migration and fixture maintenance burden.

These changes remove duplicate authorities, lifecycle states, public mutation paths, and status branches. The migration and protocol changes support compatibility. Total maintenance complexity decreases, subject to verification of migration ordering and compatibility behavior.

Simplification opportunities

The PR already removes unused archive methods, the generic session configuration update path, obsolete status projections, and redundant test fixtures.

No further deletion is evident without weakening migration compatibility or behavioral regression coverage.

Risks and validation

Concrete risks include:

  • Existing consumers of archived, archivedAt, or removed archive APIs may fail to compile or decode.
  • Legacy SQLite rows may contain contradictory or partial archive indicators.
  • Migration ordering, downgrade fencing, timestamp precision, and status preservation require verification.
  • Protocol peers must support the updated compatibility epoch and continuity schema.
  • Archived sessions can retain statuses such as active, blocked, or done.
  • Archive and restore must preserve status, revisions, metadata, and continuity projections.
  • Archived-session execution and configuration updates must remain rejected.
  • CAS failures must not partially change archive state.
  • Runtime Host and Bot adapter lifecycle behavior must remain consistent.

The PR reports focused tests, builds, typechecks, formatting checks, and lint checks as passed. Full-repository tests were not run locally. Required checks remain unverified without direct CI evidence.

Review-relevant risks

The current diff affects public TypeScript contracts, SQLite persistence and migration behavior, protocol compatibility, and user-visible session archive and status behavior. Material changes in these areas require independent human review under repository policy.

No security, licensing, release, or governance effect was identified in the current diff summary.

The person performing the merge reviews the final diff. A maintainer makes the final determination.

Walkthrough

The change makes isArchived the sole archive-state field. It removes legacy status and timestamp coupling, adds restricted header patches, updates SQLite migration and persistence APIs, bumps protocol versions, and aligns runtime, desktop, test, and UI fixtures.

Changes

Session archive state migration

Layer / File(s) Summary
Protocol and header contracts
packages/core/src/session.ts, packages/runtime-host/src/protocol/*, packages/runtime-host/scripts/*, apps/desktop/src/main/__tests__/*, packages/cli/src/__tests__/*
Adds SessionHeaderPatch, updates protocol versions, removes legacy continuity data, and replaces hard-coded schema versions.
Storage API and SQLite migration
packages/storage/src/session-store.ts, packages/storage/src/sqlite-session-metadata-store.ts, packages/storage/src/sqlite-session-metadata-schema.ts, packages/storage/src/execution-stores.ts, packages/storage/src/__tests__/*
Uses boolean archive transitions and restricted header patches. Migration 26 removes status columns and legacy archive fields.
Runtime archive behavior
packages/runtime/src/*, packages/runtime-host/src/server/*, packages/runtime-host/src/__tests__/*, apps/desktop/src/main/runtime-host-bot-session-adapter.ts
Uses isArchived for availability, recovery, retirement, delivery, and revision checks. Archive operations use versioned persistence.
Fixtures and status presentation
apps/desktop/src/main/__tests__/*, apps/desktop/stories/*, packages/runtime/src/__tests__/*, packages/ui/src/*, packages/ui/stories/*
Preserves execution status for archived sessions and removes archived status presentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🟡 Moderate · up to 85a10

This refactor changes session archive authority and compatibility behavior, but archived sessions may still be able to accept configuration changes through a public update path, and peer compatibility is not directly behavior-tested. Those issues could allow invalid state changes or integration failures, so they should be fixed or explicitly accepted before merging.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Ai Use Disclosure ⚠️ Warning The PR selects substantive generative use and names Codex, but the introduced test commit changes tests and has no Generated-by trailer; only four earlier commits have one. Add Generated-by: Codex to each commit with material AI-authored content, including the final test commit, and ensure it survives squash or amend. See CONTRIBUTING.md, “Human ownership and AI attribution”.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely states the main change: making isArchived the sole authority for session archive state.
Description check ✅ Passed The description follows the template and includes the summary, issue reference, verification results, migration details, AI disclosure, and completed checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch refactor/2984-converge-archive-state
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/2984-converge-archive-state

Comment @coderabbitai help to get the list of available commands.

@Astro-Han
Astro-Han force-pushed the refactor/2984-converge-archive-state branch 5 times, most recently from 3f68c0a to ce8f729 Compare August 15, 2026 16:05
Migrate legacy archived Session payloads, remove archivedAt and the duplicate SQLite status projections, and preserve execution status across archive transitions.

Generated-by: Codex
@Astro-Han
Astro-Han force-pushed the refactor/2984-converge-archive-state branch from ce8f729 to 58ac73f Compare August 15, 2026 16:10
@Astro-Han
Astro-Han marked this pull request as ready for review August 15, 2026 16:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/runtime/src/session-manager.ts (1)

1569-1588: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Block configuration updates for archived Sessions.

updateSession writes configuration fields without checking isArchived. An archived Session can update model, backend, or permissionMode through this path. transitionSessionConfiguration rejects the same operation at Line 1145.

Apply the same archive guard before configuration writes, or route these updates through transitionSessionConfiguration.

As per path instructions, “Review the diff adversarially against the problem it claims to solve.”

Source: Path instructions

🧹 Nitpick comments (2)
packages/runtime-host/src/__tests__/protocol.test.ts (1)

43-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test the protocol boundary, not only the constants.

These assertions only compare exported constants with literals. They do not prove that epoch 20 or schema 3 frames are rejected, or that current frames are accepted. Add decoder or handshake behavior checks. Keep the numeric assertions only as a separate release-number check if the exact values are an explicit contract.

As per path instructions, flag tests that assert implementation details or do not protect observable behavior.

Also applies to: 55-55

Source: Path instructions

packages/ui/stories/session-list-panel.stories.tsx (1)

202-202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Preserve archived-row coverage.

isArchived remains an observable UI state. packages/ui/src/session-history-list.tsx, Line 588-694, selects archive or unarchive actions from session.isArchived. Keep a fixture with a valid execution status, such as done, and isArchived: true; otherwise this story no longer covers the archived-row rendering and unarchive action.

As per path instructions, flag tests or fixtures that do not protect observable behavior.

Source: Path instructions


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 23f076b0-b88e-4f97-959d-d657a12e3ba8

📥 Commits

Reviewing files that changed from the base of the PR and between 62cded2 and 58ac73f.

📒 Files selected for processing (57)
  • apps/desktop/src/main/__tests__/app-shell-session-purge.test.ts
  • apps/desktop/src/main/__tests__/desktop-transcript-range-store.test.ts
  • apps/desktop/src/main/__tests__/runtime-host-bot-session-adapter.test.ts
  • apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
  • apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts
  • apps/desktop/src/main/__tests__/runtime-host-session-execution-ipc-main.test.ts
  • apps/desktop/src/main/__tests__/runtime-host-session-observer.test.ts
  • apps/desktop/src/main/runtime-host-bot-session-adapter.ts
  • apps/desktop/stories/settings/settings-pages.stories.tsx
  • packages/cli/src/__tests__/runtime-host-session-driver.test.ts
  • packages/core/src/session.ts
  • packages/runtime-host/scripts/transcript-data-plane-benchmark.mjs
  • packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts
  • packages/runtime-host/src/__tests__/goal-coordinator.test.ts
  • packages/runtime-host/src/__tests__/host-kernel.test.ts
  • packages/runtime-host/src/__tests__/protocol.test.ts
  • packages/runtime-host/src/__tests__/runtime-resource-coordinator.test.ts
  • packages/runtime-host/src/__tests__/session-effect-coordinator.test.ts
  • packages/runtime-host/src/__tests__/session-projector.test.ts
  • packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts
  • packages/runtime-host/src/__tests__/session-retirement-protocol.test.ts
  • packages/runtime-host/src/protocol/index.ts
  • packages/runtime-host/src/protocol/session-continuity.ts
  • packages/runtime-host/src/protocol/session-retirement.ts
  • packages/runtime-host/src/server/canonical-session-projection.ts
  • packages/runtime-host/src/server/context-coordinator.ts
  • packages/runtime-host/src/server/deep-research-coordinator.ts
  • packages/runtime-host/src/server/execution-composition.ts
  • packages/runtime-host/src/server/goal-coordinator.ts
  • packages/runtime-host/src/server/hosted-execution-recovery.ts
  • packages/runtime-host/src/server/plan-coordinator.ts
  • packages/runtime-host/src/server/root-turn-coordinator.ts
  • packages/runtime-host/src/server/runtime-resource-coordinator.ts
  • packages/runtime-host/src/server/scheduled-task-coordinator.ts
  • packages/runtime-host/src/server/session-catalog-coordinator.ts
  • packages/runtime-host/src/server/session-effect-coordinator.ts
  • packages/runtime-host/src/server/session-retirement-coordinator.ts
  • packages/runtime-host/src/server/session-revision-coordinator.ts
  • packages/runtime/src/__tests__/runtime-event-read-model.test.ts
  • packages/runtime/src/__tests__/runtime-kernel-interaction.test.ts
  • packages/runtime/src/__tests__/session-manager-terminal-ledger.test.ts
  • packages/runtime/src/__tests__/session-manager.test.ts
  • packages/runtime/src/__tests__/stream-graph-coordinator.test.ts
  • packages/runtime/src/agent-run.ts
  • packages/runtime/src/runtime-kernel.ts
  • packages/runtime/src/session-manager.ts
  • packages/runtime/src/stream-graph-coordinator.ts
  • packages/storage/src/__tests__/goal-authority.test.ts
  • packages/storage/src/__tests__/session-store.test.ts
  • packages/storage/src/__tests__/sqlite-session-metadata-store.test.ts
  • packages/storage/src/execution-stores.ts
  • packages/storage/src/session-store.ts
  • packages/storage/src/sqlite-session-metadata-schema.ts
  • packages/storage/src/sqlite-session-metadata-store.ts
  • packages/ui/src/conversation-copy.ts
  • packages/ui/src/session-status-presentation.ts
  • packages/ui/stories/session-list-panel.stories.tsx
💤 Files with no reviewable changes (6)
  • packages/runtime/src/tests/session-manager-terminal-ledger.test.ts
  • packages/runtime/src/tests/runtime-kernel-interaction.test.ts
  • packages/runtime-host/src/tests/runtime-resource-coordinator.test.ts
  • packages/runtime-host/src/server/canonical-session-projection.ts
  • packages/runtime/src/tests/runtime-event-read-model.test.ts
  • apps/desktop/src/main/tests/runtime-host-client-uds.test.ts

Reject execution and workspace configuration changes through the legacy SessionManager update path when the Session is archived. Restore archived-row Storybook coverage with an independent done execution status.

Generated-by: Codex
Delete the uncalled generic SessionManager update path instead of preserving a second configuration authority. Remove its implementation-detail tests; the versioned configuration transition, permission, and relocation paths retain their behavioral coverage.

Generated-by: Codex
Combine main's removal of the legacy timestamp read marker with this branch's removal of direct archive and generic configuration mutation paths. Keep both deletions and retain the versioned message read marker and Runtime Host retirement authorities.

Generated-by: Codex
@YayoiNanoka

Copy link
Copy Markdown
Contributor

Thanks for the work here. I did another independent review pass focused on first principles, migration safety, Occam’s razor, and test quality.

The core model is correct:

  • isArchived should be the sole archive-state authority.
  • status should describe execution state only.
  • Archive/restore should preserve execution status.
  • Runtime Host retirement should remain the sole lifecycle orchestration path.
  • Generic metadata writers must not be able to mutate isArchived.

However, I don’t think the PR is merge-ready yet.

P1 — Must fix before merge

[P1] Schema version 25 conflicts with current main

This PR defines the archive migration as schema v25, but current main already uses v25 for the review / done migration.

These are incompatible database layouts sharing the same version number. Testing reproduced both failure directions:

  • PR code opening a current-main v25 database can fail with NOT NULL constraint failed: session_metadata.status.
  • Current-main code opening the PR’s v25 database can fail because the status column no longer exists.

Please preserve main’s v25 and move this migration to v26:

  1. v25: normalize review / done.
  2. v26: normalize legacy archive state and remove obsolete fields or projections.

Please add a real main-v25 → v26 upgrade test and verify that a v25 binary rejects a v26 database cleanly.

[P1] The migration can accidentally restore archived Sessions

The current migration does not reconcile all legacy representations:

  • JSON isArchived
  • JSON status
  • SQL is_archived
  • SQL status
  • archivedAt

For example:

isArchived: false
status: 'archived'

was previously treated as archived, but the migration produces:

isArchived: false
status: 'active'

That silently restores a previously archived Session.

Please define a conservative canonicalization rule. I suggest treating a Session as archived whenever any legacy archive indicator says so:

json.isArchived
OR json.status == archived
OR sql.is_archived
OR sql.status == archived
OR archivedAt exists

Then write the canonical result back to both JSON isArchived and SQL is_archived in the same transaction.

Please test payload-only, projection-only, contradictory, and archivedAt-only rows. Unchanged active rows should not receive a metadata-version bump.

[P1] Compatibility epoch must advance from current main

The PR still publishes epoch 21, while current main is already at epoch 23.

This changes the lifecycle wire contract. Retaining either 21 or 23 is unsafe because an old client expects an archived result to contain both isArchived=true and status='archived', while the new Host no longer returns an archived execution status.

After rebasing:

  • compatibility epoch should become 24;
  • continuity schema should remain 4;
  • main’s decode-only normalization for review / done should be preserved.

The protocol test should verify that an epoch-23 peer is rejected and the current epoch succeeds, rather than only asserting a numeric constant.

P2 — Should fix in this PR

[P2] Migration timestamps lose millisecond precision

The migration currently uses:

strftime('%s', 'now') * 1000

This truncates time to whole seconds and can leave committed_at unchanged even though metadata_version advances.

Please use the expression already adopted by current main:

CAST(unixepoch('now', 'subsec') * 1000 AS INTEGER)

Please also assert that changed rows receive a commit timestamp at or after migration start, while untouched rows preserve their existing timestamp.

[P2] Archive state uses a hidden input channel in the generic updater

Currently setArchivedVersioned() modifies archive state through a hidden archiveState option on updateHeaderSync().

This makes the authority boundary less explicit and requires public update() to reconstruct its options manually so callers cannot smuggle in archiveState.

I suggest separating the responsibilities:

  • updateHeaderSync() handles ordinary metadata only.
  • setArchivedSync() is the only method allowed to change isArchived.
  • Both call a shared low-level CAS persistence routine.

This would give archive state one visible writer and eliminate the hidden capability channel.

[P2] Migration and lifecycle integration coverage is incomplete

The current migration test covers only one fully consistent archived row. Please add:

  • main-v25 → v26 upgrade;
  • active row remains untouched;
  • JSON-only archive state;
  • SQL-projection-only archive state;
  • contradictory JSON/SQL state;
  • archivedAt-only state;
  • downgrade fencing;
  • CAS failure causes no partial archive changes.

Please also extend an existing two-client UDS lifecycle test to verify that execution status is unchanged before archive, after archive, through continuity, and after restore.

The Bot adapter should cover isArchived=true, status=active both on initial read and after a permission/configuration transition.

P3 — Quality improvements

[P3] Strengthen the patch type

The current type:

type SessionHeaderPatch = Partial<Omit<SessionHeader, 'isArchived'>>;

does not completely prevent a variable already typed as Partial<SessionHeader> from flowing into the patch API under TypeScript’s structural typing.

It can be strengthened to:

type SessionHeaderPatch =
  Partial<Omit<SessionHeader, 'isArchived'>> & {
    readonly isArchived?: never;
  };

The runtime guard should remain as defense in depth.

[P3] Remove or rewrite implementation-detail tests

Suggested cleanup:

  • Remove the test that force-casts public update() options to inject the private archiveState option. It represents no real caller and becomes unnecessary after introducing setArchivedSync().
  • Replace exact epoch/schema constant assertions with behavioral compatibility tests.
  • Convert generic-writer rejection checks into a compact table-driven test.
  • Prefer stable typed errors or error codes over matching implementation-specific error strings.

Storybook and fixture updates are necessary maintenance, but they should not be counted as behavioral coverage.

Scope assessment

I’m comfortable keeping the removal of archivedAt, the unused SQL status projection, and the unused SessionManager.updateSession path in this PR:

  • they have no repository production callers;
  • they reduce the final state and mutation surface;
  • the Runtime package is private;
  • updateSession was an unused configuration-authority bypass.

The merge blockers are the schema collision, unsafe legacy canonicalization, and stale compatibility epoch—not those deletions themselves.

With the migration moved to v26, split-brain rows normalized conservatively, epoch advanced to 24, and the dedicated archive writer and tests tightened, this should become a clean implementation of the single-authority model.

点击展开中文

感谢这里所做的工作。我又进行了一轮独立审查,重点检查了第一性原理、迁移安全性、奥卡姆剃刀和测试质量。

核心模型是正确的:

  • isArchived 应该是归档状态的唯一权威。
  • status 应该只描述执行状态。
  • 归档和恢复不应覆盖执行状态。
  • Runtime Host retirement 应该是唯一的生命周期编排入口。
  • 通用 metadata writer 不应能够修改 isArchived

不过,我认为这个 PR 目前还没有达到可合并状态。

P1 — 合并前必须修复

[P1] Schema v25 与当前 main 冲突

这个 PR 把归档迁移定义为 v25,但当前 main 已经使用 v25 迁移 review / done

两个不兼容的数据库结构使用了相同的版本号。测试中已经复现:

  • PR 代码打开当前 main 的 v25 数据库时,可能报 NOT NULL constraint failed: session_metadata.status
  • 当前 main 打开 PR 的 v25 数据库时,可能因为 status 列不存在而失败。

请保留 main 的 v25,并把本 PR 的迁移调整为 v26:

  1. v25:迁移 review / done
  2. v26:迁移旧归档状态,并删除废弃字段或投影。

还需要增加真正的 main-v25 → v26 升级测试,并验证旧 v25 binary 能正确拒绝 v26 数据库。

[P1] 当前迁移可能意外恢复已归档 Session

当前迁移没有统一处理所有历史表示:

  • JSON isArchived
  • JSON status
  • SQL is_archived
  • SQL status
  • archivedAt

例如:

isArchived: false
status: 'archived'

迁移前会被视为已归档,迁移后却会变成:

isArchived: false
status: 'active'

这会静默恢复原本已归档的 Session。

建议采用保守规则:只要任意历史归档信号为真,就将其视为已归档:

json.isArchived
OR json.status == archived
OR sql.is_archived
OR sql.status == archived
OR archivedAt exists

然后在同一个事务中同时更新 JSON isArchived 和 SQL is_archived

请覆盖 payload-only、projection-only、互相矛盾以及只有 archivedAt 的数据。未变化的 active row 不应增加 metadata version。

[P1] Compatibility epoch 必须基于当前 main 递增

这个 PR 仍然使用 epoch 21,而当前 main 已经是 epoch 23。

本 PR 修改了 lifecycle wire contract,因此保留 21 或 23 都不安全。旧客户端要求归档结果同时满足 isArchived=truestatus='archived',而新 Host 不再返回归档执行状态。

rebase 后:

  • compatibility epoch 应升级到 24;
  • continuity schema 保持 4;
  • 保留 main 对 review / done 的 decode-only 兼容处理。

协议测试应验证 epoch 23 会被拒绝、当前 epoch 可以成功,而不是只断言数字常量。

P2 — 建议在本 PR 修复

[P2] 迁移时间丢失毫秒精度

当前迁移使用:

strftime('%s', 'now') * 1000

这会把时间截断到整秒,可能出现 metadata version 已增加,但 committed_at 没有前进。

请使用当前 main 已采用的表达式:

CAST(unixepoch('now', 'subsec') * 1000 AS INTEGER)

还应验证发生变化的 row 获得不早于迁移开始时间的 commit timestamp,而未变化的 row 保留原时间。

[P2] 归档状态通过通用 updater 的隐藏通道写入

当前 setArchivedVersioned() 通过 updateHeaderSync() 的隐藏 archiveState option 修改归档状态。

这让权威边界不够明确,也迫使公开 update() 手动重建 options,以防调用方传入 archiveState

建议拆分:

  • updateHeaderSync() 只处理普通 metadata。
  • setArchivedSync() 是唯一允许修改 isArchived 的方法。
  • 两者共用底层 CAS 持久化方法。

这样归档状态只有一个清晰可见的 writer,也不再需要隐藏能力参数。

[P2] 迁移和 lifecycle 集成测试不足

当前迁移测试只覆盖一个完全一致的 archived row。请增加:

  • main-v25 → v26;
  • active row 不变化;
  • 仅 JSON 表示归档;
  • 仅 SQL projection 表示归档;
  • JSON/SQL 互相矛盾;
  • 只有 archivedAt
  • downgrade fencing;
  • CAS 失败不产生部分归档。

还应扩展现有 two-client UDS lifecycle 测试,验证归档前、归档后、continuity 和恢复后的执行状态都保持不变。

Bot adapter 应覆盖初次读取以及 permission/configuration transition 后出现 isArchived=true, status=active 的情况。

P3 — 质量优化

[P3] 加强 patch 类型

当前类型:

type SessionHeaderPatch = Partial<Omit<SessionHeader, 'isArchived'>>;

在 TypeScript 结构化类型下,不能完全阻止一个已经声明为 Partial<SessionHeader> 的变量流入。

可以加强为:

type SessionHeaderPatch =
  Partial<Omit<SessionHeader, 'isArchived'>> & {
    readonly isArchived?: never;
  };

运行时检查仍应保留,作为第二层防护。

[P3] 删除或重写实现细节测试

建议:

  • 删除通过强制类型转换向公开 update() 注入内部 archiveState option 的测试。它不对应真实调用方,并且在引入 setArchivedSync() 后自然失去意义。
  • 用实际兼容行为测试替代 epoch/schema 数字常量断言。
  • 把 generic writer rejection 改成紧凑的表驱动测试。
  • 优先断言稳定的 typed error 或 error code,而不是实现相关的错误字符串。

Storybook 和 fixture 更新属于必要维护,但不应算作行为测试覆盖。

范围判断

我认为可以在这个 PR 中继续删除 archivedAt、未使用的 SQL status projection,以及没有调用方的 SessionManager.updateSession

  • 它们没有仓库内生产调用方;
  • 能够减少最终状态和 mutation surface;
  • Runtime package 是 private;
  • updateSession 是一个未使用的配置权威绕过入口。

真正阻止合并的是 schema 冲突、不安全的历史数据归一化和过期的 compatibility epoch,而不是这些删除本身。

在迁移调整为 v26、保守修复历史分裂数据、epoch 升至 24,并完善专用归档 writer 和测试后,这个 PR 应该能成为一个干净的单一权威实现。

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@Astro-Han

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough pass, @YayoiNanoka — all points landed. Verified on head (7fb0f50) with evidence:

  • P1 schema collision — migration is now v27 (v25 = review/done from fix(session): migrate legacy session statuses #3159, v26 = refactor(runtime-host)!: remove unused Session catalog filters (#3071) #3165), migration chain 1..27 continuous with no gaps/duplicates, and main-v26 → v27 + downgrade-fencing tests exist.
  • P1 accidental unarchive — migration 27 canonicalizes conservatively: any legacy signal (json.isArchived OR json.status='archived' OR sql.is_archived OR sql.status='archived' OR archivedAt exists) → is_archived=1, written back to both JSON and SQL columns in the same UPDATE (one transaction). Tests cover json-only/sql-only/sql-status/json-status/archivedAt-only/contradictory/active-unchanged. I traced the old writer paths: legacy archive/unarchive always kept the SQL status column in sync and restore clears archivedAt + sets is_archived=0, so an active row can't retain stale archived traces — no false-archive path.
  • P1 epochRUNTIME_HOST_COMPATIBILITY_EPOCH = 24, consistent with refactor(runtime-host)!: remove unused Session catalog filters (#3071) #3165, with a behavioral rejection test (fake epoch-23 host rejected at handshake before any domain command, not a constant assertion).
  • P2 writer split — real: updateHeaderSync throws on any patch carrying isArchived, setArchivedSync is the sole persistence path, and the type layer adds isArchived?: never; update() and setExecutionBoundaryKindSync both route through the guarded writer, no smuggling channel. Bot adapter now reads only isArchived.
  • P2 millisecond precision — migration uses unixepoch('now','subsec')*1000; store uses Date.now(); units consistent.

Ran the relevant suites against head: sqlite-session-metadata-store (45), handshake-compatibility, session-retirement-protocol (2), session-retirement-coordinator (21), protocol (28), core/session-status, storage/session-store (16) — all green. Migration is single-transaction and runs once per version table; restore goes through #setLifecycle active preserving execution status; retirement-coordinator covers archive-restore-delete full flows.

One remaining P2 (I'd take the deferral): epoch 24 doesn't isolate this PR's own retirement wire change. Removing the (output.status==='archived')===archived check in session-retirement.ts:55 and the client's assertOutputForInput at connection.ts:514 (both in this diff) is a wire-contract relaxation that is epoch-gated but rides #3165's bump 23→24 rather than bumping again — a pre-PR client at epoch 24 connecting to this host would fail archive/restore after handshake with an invalidProtocolFrame instead of a clean rejection. Given host+client ship atomically from the single @maka/runtime-host package and mixed-version deployments are manual-only with recoverable operational errors (no data corruption), I propose deferring with this note rather than consuming the next epoch number — flagging in case reviewers prefer the stricter "every wire change bumps" discipline.

P3 (non-blocking): migration 27's ELSE branch removes only $.archivedAt and not $.blockedReason/$.statusUpdatedAt (symmetric-ish, no functional impact); legacy status='archived' maps lossily to active (documented — old archive overwrote execution state, unrecoverable).


AI-assisted review disclosure: this review was produced with AI assistance (pi review subagent on opencode-go/deepseek-v4-flash); the subagent built core/storage/runtime/runtime-host, ran the suites above, and traced the migration chain, writer split, and retirement wire. The remaining P2 is an epoch-gating analysis, not an observed failure. Please weigh these findings with your own judgment.

中文摘要

感谢 @YayoiNanoka 的深入审查,全部意见已落地(head 7fb0f50 已核实):① schema 冲突→迁移改为 v27(v25=#3159 review/done、v26=#3165),迁移链 1..27 连续无跳号,含 main-v26→v27 与降级护栏测试;② 意外恢复归档→迁移 27 保守归一化(任一历史信号:json.isArchived ∨ json.status='archived' ∨ sql.is_archived ∨ sql.status='archived' ∨ archivedAt 存在 → is_archived=1),JSON 与 SQL 同事务写回,测试覆盖 json-only/sql-only/status-only/archivedAt-only/矛盾/active-unchanged;旧 writer 路径核实:archive/unarchive 始终同步 SQL status 列、restore 清 archivedAt,active 行不会残留归档痕迹,无误归档路径;③ epoch→24 与 #3165 一致,且是行为拒绝测试(伪造 epoch-23 host 在握手期被拒)非常量断言;④ writer 拆分真实(updateHeaderSync 对携带 isArchived 的 patch 直接 throw、setArchivedSync 唯一落盘路径、类型层 isArchived?: never 双封,update()/setExecutionBoundaryKindSync 均经守卫,Bot adapter 只读 isArchived);⑤ 毫秒精度统一(unixepoch subsec*1000 / Date.now())。本地实跑相关 suites 全绿(metadata-store 45、handshake、retirement-protocol 2、retirement-coordinator 21、protocol 28、session-status、session-store 16)。剩余 1 个 P2(建议接受延后):epoch 24 未隔离本 PR 自身的 retirement wire 变更——移除 session-retirement.ts:55 的 (status==='archived')===archived 校验与客户端 connection.ts:514 的 assertOutputForInput 是对 wire 契约的放宽,只骑 #3165 的 23→24 bump 而未再 bump:epoch-24 旧客户端连本宿主时归档/恢复会在握手之后以 invalidProtocolFrame 失败而非干净拒绝。鉴于 host+client 从单一 @maka/runtime-host 原子发布、混合版本仅手动部署且失败是可恢复的操作错误(无数据损坏),提议显式延后并在评审注明;如评审偏好严格"每个 wire 变更都 bump"纪律也可 bump 25。P3(不阻塞):迁移 27 ELSE 分支只删 $.archivedAt 未删 $.blockedReason/$.statusUpdatedAt(对称性小瑕疵);旧 status='archived' 有损映射为 active(已文档化,旧归档覆盖了执行状态无法还原)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants