Skip to content

feat(daemon): persist session artifacts across restarts#6259

Closed
chiga0 wants to merge 95 commits into
QwenLM:mainfrom
chiga0:feat/session-artifacts-persistence-v2-design
Closed

feat(daemon): persist session artifacts across restarts#6259
chiga0 wants to merge 95 commits into
QwenLM:mainfrom
chiga0:feat/session-artifacts-persistence-v2-design

Conversation

@chiga0

@chiga0 chiga0 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Implements V2 daemon session artifact metadata persistence as a follow-up to #5895. The PR restores restorable artifact metadata across daemon restart/session replay, records durable tombstones and snapshots in JSONL, rebuilds artifact state during load/fork, remaps forked artifact ownership, and keeps ephemeral artifacts live-session-only.

The persistence boundary is metadata-only in this PR: workspace-backed artifacts record status metadata such as sha256/size/mtime for staleness detection, but artifact content is not retained, copied, quota-managed, or garbage-collected here. Content retention remains out of scope for #6259.

Why it's needed

V1 artifact APIs exposed only live-session state, so page refresh could recover from a live daemon but daemon restart or historical load could not restore artifact metadata. V2 gives clients a stable metadata restore path while keeping content retention separate from this base persistence change.

Reviewer Test Plan

How to verify

Review that ephemeral artifacts remain live-only, restorable artifact metadata survives JSONL rebuild/snapshot restore, explicit remove tombstones prevent deleted artifacts from coming back, restore/fork only replay artifact records attached to the active conversation branch, fork remap preserves metadata without leaking cross-session ownership, workspace-file status is metadata-only and marks stale/missing files correctly, and artifact add/remove calls enforce client ownership through x-qwen-client-id.

Evidence (Before & After)

Before: artifact state was daemon-memory-only and the PR contained only a design document. After: daemon sessions persist artifact metadata/events/snapshots, rebuild that metadata after daemon restart/session replay, and keep content retention outside this PR.

Tested on

OS Status
🍏 macOS ✅ targeted tests, typecheck, build
🪟 Windows N/A
🐧 Linux ⏳ GitHub Actions

Environment (optional)

Validated locally with cd packages/acp-bridge && npx vitest run src/sessionArtifacts.test.ts, cd packages/core && npx vitest run src/services/sessionService.test.ts src/services/sessionService.corruption.test.ts src/services/session-artifact-persistence.test.ts, cd packages/acp-bridge && npm run typecheck, cd packages/core && npm run typecheck, npm run build --workspace=packages/core, npx prettier --check docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md packages/acp-bridge/src/sessionArtifacts.ts packages/acp-bridge/src/sessionArtifacts.test.ts packages/acp-bridge/src/bridge.ts packages/core/src/services/session-artifact-persistence.ts packages/core/src/services/session-artifact-persistence.test.ts packages/core/src/services/sessionService.ts, and git diff --check.

Risk & Scope

  • Main risk or tradeoff: This adds persistent artifact metadata and ownership state, so restore/fork/delete behavior must preserve durable tombstones, avoid replaying abandoned branches, and keep client ownership boundaries intact.
  • Not validated / out of scope: Content retention, managed content storage, content-addressed manifests, quota/GC, cross-machine portability, UI pin/save affordances, and user-configurable content retention policy.
  • Breaking changes / migration notes: Existing read-only/live ephemeral behavior remains compatible, but artifact add/remove routes now require x-qwen-client-id so ownership can be enforced. Older sessions without artifact persistence records simply restore with an empty artifact list.

Linked Issues

Follow-up to #5895.

中文说明

What this PR does

这个 PR 在 #5895 之后实现 V2 daemon session artifact metadata 持久化。现在会在 daemon 重启或 session replay 后恢复 restorable artifact metadata,把 durable tombstone 和 snapshot 写入 JSONL,在 load/fork 时重建 artifact 状态,remap fork 后的 artifact ownership,并保持 ephemeral artifact 仍然只存在于 live session。

本 PR 的持久化边界是 metadata-only:workspace 文件类 artifact 会记录 sha256/size/mtime 等状态 metadata 用于判断 stale,但这里不会保留、复制、配额管理或 GC artifact 内容。Content retention 不属于 #6259 的范围。

Why it's needed

V1 artifact API 只有 live-session 状态:页面刷新且 daemon 还活着时能恢复,但 daemon 重启或历史 load 后不能恢复 artifact metadata。V2 提供稳定的 metadata restore 路径,同时把 content retention 从这个 base persistence change 中拆开。

Reviewer Test Plan

How to verify

请重点检查:ephemeral artifact 是否仍然只存在于 live session;restorable artifact metadata 是否能通过 JSONL rebuild/snapshot restore 恢复;显式删除的 tombstone 是否阻止旧 artifact 回来;restore/fork 是否只 replay 挂在 active conversation branch 上的 artifact records;fork remap 是否保留 metadata 且不泄露跨 session ownership;workspace 文件状态是否只记录 metadata 并正确标记 stale/missing;artifact add/remove 是否通过 x-qwen-client-id 强制 client ownership。

Evidence (Before & After)

Before:artifact state 只在 daemon 内存中,且 PR 只有设计文档。After:daemon session 会持久化 artifact metadata/events/snapshots,daemon 重启或 session replay 后能重建这些 metadata,并且 content retention 仍不在本 PR 内。

Tested on

OS Status
🍏 macOS ✅ targeted tests、typecheck、build
🪟 Windows N/A
🐧 Linux ⏳ GitHub Actions

Environment (optional)

本地已验证:cd packages/acp-bridge && npx vitest run src/sessionArtifacts.test.tscd packages/core && npx vitest run src/services/sessionService.test.ts src/services/sessionService.corruption.test.ts src/services/session-artifact-persistence.test.tscd packages/acp-bridge && npm run typecheckcd packages/core && npm run typechecknpm run build --workspace=packages/corenpx prettier --check docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md packages/acp-bridge/src/sessionArtifacts.ts packages/acp-bridge/src/sessionArtifacts.test.ts packages/acp-bridge/src/bridge.ts packages/core/src/services/session-artifact-persistence.ts packages/core/src/services/session-artifact-persistence.test.ts packages/core/src/services/sessionService.tsgit diff --check

Risk & Scope

  • Main risk or tradeoff: 这个 PR 增加了 artifact metadata 和 ownership 的持久化状态,所以 restore/fork/delete 行为必须保留 durable tombstone、避免 replay abandoned branch,并保持 client ownership 边界。
  • Not validated / out of scope: Content retention、managed content storage、content-addressed manifest、quota/GC、跨机器可移植性、UI pin/save 入口、用户可配置 content retention policy。
  • Breaking changes / migration notes: 现有只读/live ephemeral 行为保持兼容;但 artifact add/remove routes 现在要求 x-qwen-client-id,以便强制 ownership。没有 artifact persistence record 的旧 session 会恢复为空 artifact list。

Linked Issues

#5895 的后续实现。

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Stage 1: Gate (re-run #3)

Fresh triage against commit 096b217d.

Template looks good ✓ — all required headings present, bilingual, test plan with OS matrix.

Problem: observed and well-documented. V1 artifact APIs were live-session-only — daemon restart or historical session load lost all artifact metadata. This is a genuine gap following up on #5895.

Direction: aligned with the roadmap. Metadata persistence across daemon restarts and session load/replay is a natural next step after V1 live-session artifacts. Content retention was correctly split into a separate future PR (#6346). The design doc now explicitly defers sidecar cache, pin/save, and content archive to future iterations.

Size: approximately 3,627 production logic lines across core paths (packages/core/src/services/*, packages/acp-bridge/src/*, packages/cli/src/serve/*, packages/sdk-typescript/src/*), 3,957 test lines, and an 893-line design doc. Since this is a feat-type PR touching core infrastructure, it's not subject to the hard block. The large PR advisory (1000+ lines) applies — this is a substantial feature, but well-organized across clear module boundaries.

Approach: the scope feels well-calibrated. The JSONL-only approach (no sidecar cache) keeps the implementation focused. The design doc comprehensively maps to the implementation. The ~30 hardening commits since the maintainer's design review have addressed all 8 critical/suggestion comments:

  • contentRef validation: normalizeContentRef() validates kind/contentId pattern/sha256/sizeBytes; legacy contentRef is stripped on restore, not exposed as openable content
  • Sidecar validity: N/A — sidecar deferred to future work (§4.1)
  • Durable-first semantics: eviction and unpin write durable remove events; explicit DELETE uses live-first with best-effort tombstone and warning (§5.3)
  • file:// URL restore rule: file: allowed only for trusted published artifacts, validated via normalizeArtifactUrl(raw, allowFile) (§5.5)
  • Client POST retention: clients can request ephemeral or restorable; pinned is correctly rejected (§6.2)
  • Race-safe workspace access: O_NOFOLLOW open, pre/post-open stat comparison (isSameFile), stream-based hashing (§7.2)
  • URL/metadata privacy: hasSecretLikeUrlComponent and credential rejection on URLs; prototype key filtering and secret-key denylist on metadata (§7.3)
  • Live cap vs persisted cap alignment: both aligned at 200; durable eviction writes eviction remove events; restore prunes via same rules (§8.1)

One note: the stray .qwen/skills/triage/references/pr-workflow.md change (1 line) is unrelated — minor, but still worth splitting out.

Moving on to code review. 🔍

中文说明

感谢持续迭代。这是针对 commit 096b217d 的 re-run #3

模板完整 ✓

问题:已观测且文档充分。V1 artifact API 仅限 live-session,这是 #5895 之后 daemon session 模型中的真实缺口。

方向:与 roadmap 对齐。content retention 正确拆分到后续 PR。

规模:核心路径约 3,627 行生产逻辑,3,957 行测试,893 行设计文档。feat 类型不受 hard block,大 PR advisory 适用。

方案:范围合理。JSONL-only 方案保持聚焦。约 30 个 hardening commit 全面处理了维护者的 8 个 critical/suggestion 评审意见。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Stage 2: Code Review + Test (re-run #3)

Code Review

Independent proposal: for daemon session artifact metadata persistence, I would add JSONL system records (session_artifact_event / session_artifact_snapshot) to the existing chat recording service, a core persistence types/normalization/rebuild module, extend the SessionArtifactStore with a persistence hook, integrate restore into the daemon-side bridge createSessionEntry, and add SDK/REST types for the new optional fields. Workspace integrity would use stat-gated sha256 checks.

The PR's approach matches this closely. The implementation is split cleanly between packages/core (types, normalization, rebuild, chat recording integration) and packages/acp-bridge (store persistence hook, workspace integrity, fork remapping, cross-client authorization). The operation queue serializes mutations. State clone/restore provides atomicity. Fork remapping correctly recalculates artifact IDs and downgrades pinnedrestorable.

The ~30 hardening commits since the maintainer's design review addressed all 8 critical comments:

  • contentRef validation: normalizeContentRef() enforces kind=managed_copy, contentId hex-64-hex-16 pattern, sha256 hex-64 pattern, non-negative sizeBytes. Legacy contentRef is stripped during restore — never exposed as openable content.
  • Durable-first semantics: eviction writes durable remove event with reason: "eviction". Explicit DELETE is live-first with best-effort tombstone — tombstone failure returns warning. remove() rolls back live state on persistence failure when persistenceStrict.
  • file:// URL restore rule: normalizeArtifactUrl(raw, allowFile) gates file: protocol on trusted publisher status. Restore path passes allowFile = artifact.storage === 'published' && !isFileArtifactUrl(artifact.url).
  • Client POST retention: normalizeRetention accepts ephemeral or restorable, rejects pinned with validation error.
  • Race-safe workspace access: getWorkspaceStatus opens with O_RDONLY | O_NOFOLLOW, compares pre-open lstat with post-open fstat via isSameFile(dev, ino), hashes via stream, and detects symlink escapes.
  • URL/metadata privacy: hasSecretLikeUrlComponent scans query params and fragment for token/secret/password patterns. URL userinfo is rejected. Metadata prototype keys (__proto__, constructor, prototype) are filtered. Secret-like metadata values are checked.
  • Live cap alignment: both live and persisted caps are 200. Restore calls evictOverflow which writes eviction remove events for durable artifacts.

Critical findings: none.

Reuse check: the persistence module reuses the existing ChatRecordingService infrastructure for JSONL writes, the exported stableSessionArtifactId, and the existing operation queue pattern in SessionArtifactStore. No unnecessary parallel utilities.

Convention check: ESM, strict TypeScript, Prettier formatting, kebab-case file names, tests co-located. All consistent with project conventions.

Test Results

All targeted test suites pass:

✓ packages/core/session-artifact-persistence.test.ts  —  20/20 passed (16ms)
✓ packages/acp-bridge/sessionArtifacts.test.ts        —  83/83 passed (100ms)
✓ packages/acp-bridge/bridge.test.ts                  — 345/345 passed (3432ms)
✓ packages/core/sessionService.test.ts                —  93/93 passed (110ms)
✓ packages/core/chatRecordingService.test.ts          —  38/38 passed (41ms)
✓ packages/sdk-typescript/acpRouteTable.test.ts       — 101/101 passed (15ms)
✓ packages/cli/acpAgent.test.ts                       — 198/198 passed (8639ms)
✓ packages/cli/server.test.ts                         — 603/603 passed (10793ms)

Total: 1,481 tests, all passing.

Typecheck clean for both packages/core and packages/acp-bridge — no errors.

Real-Scenario Testing

This PR adds internal daemon infrastructure (artifact metadata persistence via JSONL records). The changes are not user-visible in the CLI output — there's no TUI change to capture with tmux. The 1,481 unit tests comprehensively cover the persistence semantics: durable events, snapshots, tombstones, sticky ephemeral overrides, workspace integrity (available/missing/changed), cross-client authorization, URL secret filtering, prototype metadata protection, fork remapping, snapshot backoff, restore failure rollback, and eviction event durability.

中文说明

代码审查

独立方案与 PR 实现吻合。维护者的 8 个 critical 评审意见均已通过代码处理。

关键发现:无。

测试结果

所有目标测试套件通过,共 1,481 测试全部通过。core 和 acp-bridge 类型检查无错误。

真实场景测试

此 PR 添加内部 daemon 基础设施。CLI 输出无用户可见变化。1,481 个单元测试全面覆盖了持久化语义。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Stage 3: Reflection + Verdict (re-run #3)

Stepping back to look at the whole picture.

This PR has been through an extraordinary review arc — a maintainer design review that found 8 substantive issues (7 critical, 1 suggestion) on the design doc, followed by ~30 hardening commits that comprehensively addressed every one of them. The design doc was rewritten to match the actual codebase architecture and now serves as a detailed contract between the implementation and the expected behavior.

My independent proposal from Stage 2a aligns closely with what the PR implements. The approach is sound — JSONL system records for journal/snapshot integrated into the existing chat recording service, core normalization module with thorough input validation (prototype key filtering, symlink containment, metadata budget enforcement, content ID pattern validation, secret-like URL component detection), persistence hook on the store with operation queue serialization, proper fork remapping, and correct daemon-side restore seeding at createSessionEntry. I don't see a materially simpler path that would cover the same ground.

The code quality is high: clean separation between core types and store integration, defense-in-depth input validation, atomic state transitions with clone/restore for error recovery, exponential backoff for snapshot failures, O_NOFOLLOW + fstat revalidation for race-safe workspace access, and proper eviction event durability for cap alignment.

1,481 tests pass across 8 test suites, typecheck is clean for both core and acp-bridge. The test coverage is deep — tombstones, sticky ephemeral overrides, eviction events, fork remapping, corrupt record handling, cross-client authorization boundaries, prototype pollution filtering, workspace integrity three-state checks, snapshot backoff, restore failure rollback, and URL secret filtering.

The main reservation remains scope: ~3,627 production lines across 4 packages. But the changes are well-organized, each file has a clear purpose, and the design doc provides the architectural map. Content retention was correctly deferred to a future PR, which shows good scope discipline.

One concern worth noting: the maintainer's CHANGES_REQUESTED review from July 3 has not been explicitly dismissed or responded to in review comments. The author addressed the feedback through code changes, but a direct response to the maintainer's review thread would help close the loop. This is a process note, not a code concern — all 8 issues are addressed in the implementation.

This is a well-executed feature addition that solves a real problem in the daemon session model. The maintainer's design review feedback has been comprehensively addressed through both code and design doc updates. Ready to ship. ✅

中文说明

回顾全局。

此 PR 经历了大量审查——维护者设计评审发现了 8 个实质性问题(7 个 critical,1 个 suggestion),作者通过约 30 个 hardening commit 全部处理。设计文档已重写以匹配实际代码库架构。

我的独立方案与 PR 实现紧密一致。方案合理——JSONL system records 集成到现有 chat recording service、core normalization 模块带全面输入校验、store 上的 persistence hook 带操作队列串行化、正确的 fork remapping、以及正确的 daemon-side restore seeding。

代码质量高:纵深防御输入校验、原子状态转换、snapshot 指数退避、O_NOFOLLOW + fstat revalidation、eviction event 持久化。

1,481 测试跨 8 个测试套件全部通过,core 和 acp-bridge 类型检查无错误。

一个流程提醒:维护者 7 月 3 日的 CHANGES_REQUESTED review 尚未显式关闭。作者通过代码变更处理了所有反馈,但在 review thread 中直接回复会有助于关闭循环。

执行良好的功能添加,解决了 daemon session 模型中的真实问题。可以合并。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@chiga0
chiga0 requested a review from wenshao July 3, 2026 10:00
@wenshao

wenshao commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Design review — checked against the V1 implementation on main

Thorough doc, and most of its V1 claims verified accurate against the code (live-store cap 200, 4096-byte metadata limit, workspace status batch 20 + TTL, status: available|missing, artifact_changed event name, operationQueue serialization, upsertMany()/remove(), ChatRecord.subtype/systemPayload extensibility, /branch copying the active record chain with per-record sessionId rewrite, the chatRecording setting gate). However, three claims about the existing code are wrong — two of them on load-bearing parts of the design (the restore integration point and the sidecar location) — plus five spec-internal issues. Line links point to this PR's file.

Where the doc mis-describes current code

1. §5.3 step 6 / §9 Milestone C — restore seeding targets the wrong process (L283, L615)
The doc seeds SessionArtifactStore from AcpAgent.createAndStoreSession(). No class AcpAgent exists (it's class QwenAgent, packages/cli/src/acp-integration/acpAgent.ts:2572), but the substantive problem is the process: the only production new SessionArtifactStore(...) is daemon-side — packages/acp-bridge/src/bridge.ts:2268 inside createSessionEntry — and GET /session/:id/artifacts reads exactly that store (packages/cli/src/serve/routes/session.ts:526bridge.getSessionArtifacts). The ACP child process where createAndStoreSession runs has zero references to SessionArtifactStore, so a seed implemented as written populates nothing the HTTP API can see. The existing child→daemon pathway (qwen/notify/session/artifact-event) can't substitute as-is either: handleArtifactEvent stamps source:'hook' and treats input as fresh ingestion (V2 fields like restoreState wouldn't round-trip), and it drops events until the SessionEntry is registered — which only happens after loadSession resolves. Suggest specifying the seed at the bridge (createSessionEntry / restore completion), or an explicit post-registration snapshot notification.

2. §4.3 — sidecar path is modeled on the legacy chats location (L176)
~/.qwen/tmp/<project_hash>/chats/<sessionId>.artifacts.json assumes chat JSONLs live under tmp/<hash>/chats/. They don't: the writer is packages/core/src/services/chatRecordingService.ts:627storage.getProjectDir() + 'chats', and getProjectDir() is ~/.qwen/projects/<sanitized-cwd> (packages/core/src/config/storage.ts:313-320, keyed by sanitized cwd, not a hash); tmp/<hash> is getProjectTempDir() (tool-results etc.). The tmp/<project_id>/chats/ strings survive only in stale comments (sessionService.ts:269, chatRecordingService.ts:476, sessionStorageUtils.ts:22) — the doc appears to have copied one. Consequence: sidecars would be written/validated in a directory the session loader never reads, and §4.3's "move together with the chat JSONL on archive" premise breaks. The projectHash cache key (L186/L200) inherits the same outdated model.

3. §5.4 — the http/https-only restore rule blocks every locally-published artifact (L293)
V1 deliberately allows file: URLs for trusted published artifacts (packages/acp-bridge/src/sessionArtifacts.ts:1241 normalizeArtifactUrl(raw, allowFile); call site :392 passes trustedPublisher), the default publisher returns file:// URLs (packages/core/src/tools/artifact/local-publisher.ts:40 pathToFileURL(...); local is the default), and url is the only locator for storage:'published' (validateLocator, sessionArtifacts.ts:1017-1027). Since §2.2 defaults published artifacts to restorable, every locally-published artifact would restore blocked/unopenable under §5.4 as written. Compounding: §4.2 forbids persisting trustedPublisher, so the restore path has no signal left to re-allow file: even after publisher-manifest re-validation — and §7.3's "不保存 host 绝对路径" is itself in tension with persisting file:///... URLs. Needs an explicit rule: allow file: when publisher-manifest trust re-validates, or rewrite published URLs to daemon-served ones.

Spec-internal issues

4. §7.1/§6.5 — session_owner and "用户确认" aren't mintable in the daemon's actual auth model (L452)
The daemon's HTTP trust boundary is one shared bearer token (run-qwen-serve.ts:1019; the artifact REST routes gate on global mutate({strict:true}) only). The one per-session primitive that exists, conn.ownsSession, can't ground session_owner: it's connection-lifetime (empty after daemon restart — exactly the persistence scenario), self-mintable by any token holder via session/load (sibling co-ownership is by design), and absent on the REST surface where §6.3's pin endpoint is specified. There is also no mechanism to hold a client-initiated REST mutation pending human confirmation (the permission-vote flow only covers agent-initiated tool calls during a prompt turn). As specified, the owner tier collapses to "holds the token". The doc should define how session_owner is established and persisted across restarts (a durable per-session capability minted at creation? an explicit downgrade to token-holder?) and what 用户确认 means mechanically for headless clients.

5. §6.2 + Milestone D vs §2.2 — the POST retention whitelist excludes the mandated ephemeral opt-out (L367, L621)
§6.2 "client 不能请求 pinned,只能请求 restorable" and Milestone D "POST /session/:id/artifacts 支持 retention: "restorable"" both enumerate {restorable} as the whitelist — but §2.2 (L56) requires background clients to explicitly request retention: "ephemeral" on that same endpoint (plus an SDK ephemeral helper), and §2.3 (L74) requires honoring explicit retention. An implementer deriving request validation from §6.2/Milestone D rejects exactly the request §2.2 mandates. Fix both spots to "ephemeralrestorable".

6. §6.4 — unpin's downgrade parameter is undefined, and unpin→ephemeral resurrects on next load (L411)
The target ("restorableephemeral,由请求参数决定") has no body/query schema (contrast §6.3's {"mode": ...}). Worse, by the doc's own rules — §5.1 journals only restorable/pinned changes, §8.1 "ephemeral 本来不写 journal", §5.3 replay applies only tombstones, and §4.2 requires tombstones only for deletion (which §6.4 says unpin is not: "不删除 metadata") — an unpin-to-ephemeral leaves the artifact's prior pinned upsert as its last journaled state, so it comes back on the next session load, contradicting §2.2 "ephemeral…不恢复". §4.2's reason:'unpin' enum value is consumed by the default (→restorable) upsert and assigns nothing to the ephemeral variant; §10 tests only the DELETE-tombstone case. Define the parameter, require a tombstone (or an explicit restore-skip rule) for the ephemeral target, and add a test.

7. §8.3 — TTL'd pinned content is excluded from the only expiry GC rule (L540)
§6.3's ttlDays produces pinned content with expiresAt (§3.1: "pinned 默认不设置" = settable), and §8.2 explicitly says pinned content is auto-deleted "除非用户设置 TTL" — but §8.3's only expiry-based rule is "删除超过 expiresAt 的 non-pinned content", which excludes pinned by its own wording, and the other rules all require a tombstone or session delete. Net: ttlDays is a dead parameter and expired pinned content accumulates until QUOTA_EXCEEDED. Add a GC rule for expired pinned content (or route TTL through unpin semantics).

8. §4.3/§4.4 — the sidecar buys almost nothing by the doc's own analysis, and its two sequence fields are redundant (L174, L205)
§8.5(4) concedes SessionService.loadSession() already parses the entire JSONL and can extract artifact records "在同一轮 parse 中顺手提取" (that is exactly how file_history_snapshot restore ships today, via readAllRecords), and fallback replay is already bounded by the 100-mutation/256 KB snapshot threshold; §4.4 concedes "sidecar 只解决性能,不承载协议正确性". Against that near-zero win, the spec spends: atomic writes, 6-field staleness validation, corruption fallback, archive/unarchive/delete co-movement, orphan-sidecar GC (§8.3), crash-consistency clauses (§8.4), and 3 dedicated tests (§10). Consider shipping V2 JSONL-only and adding the cache later if session load is measured slow. Separately, sequence vs sourceArtifactSequence: the doc admits "正常情况下两者相同" and no scenario in the doc makes them diverge (rebuild and compaction recompute both to the same value); moreover, validating sourceArtifactSequence against the JSONL's actual latest sequence requires parsing the JSONL — which defeats the cache. One field suffices (or drop the sidecar and the question disappears).

Minor

  • §3.1 (L100) "缺省按 V1 兼容处理为 live-only" vs the restorable defaults in §2.2/§2.3/§6.1: these coexist only if L100 is reader-side (interpreting V1-era records) while §2.2 governs ingest — the doc never states that scoping, and never requires V2 writers to materialize retention in §4.2 journal records (which reuse the same optional shape), so a field-less V2-written record would legitimately be read as live-only and dropped from restore. One clarifying sentence fixes it.
  • §4.5/§8.1: content gets a per-project cap but persisted metadata has no cross-session bound, and each 100-mutation snapshot appends another ≤1 MB copy into the same JSONL — the replay-cost mitigation grows the very file §4.5 admits "可能继续增长". Worth one sentence stating the accepted policy (metadata rides the chat-transcript lifecycle) or adding a journal-bytes ceiling.

Also for the record: doc language, directory placement, and formatting are all consistent with existing docs/design/ conventions, and the file passes prettier.

中文(完整对应)

设计评审 — 已对照 main 上的 V1 实现核查

文档很扎实,绝大多数关于 V1 的描述与代码一致(live store 上限 200、metadata 4096 字节上限、workspace 状态 batch 20 + TTL、status: available|missingartifact_changed 事件名、operationQueue 串行化、upsertMany()/remove()ChatRecord.subtype/systemPayload 可扩展、/branch 复制 active record chain 并逐条重写 sessionIdchatRecording 设置门控)。但有三处对现有代码的描述是错的 — 其中两处落在设计的承重部位(恢复集成点、sidecar 位置)— 另有五个规范内部问题。行号链接指向本 PR 的文件。

与现有代码不符

1. §5.3 第 6 步 / §9 Milestone C — 恢复 seeding 指向了错误的进程(L283、L615)
文档让 AcpAgent.createAndStoreSession() 用 snapshot 初始化 SessionArtifactStore。首先不存在 AcpAgent 类(实际是 class QwenAgentpackages/cli/src/acp-integration/acpAgent.ts:2572),但实质问题在进程:生产代码里唯一的 new SessionArtifactStore(...) 在 daemon 侧 — packages/acp-bridge/src/bridge.ts:2268createSessionEntry 内 — 而 GET /session/:id/artifacts 读的正是这个 store(packages/cli/src/serve/routes/session.ts:526bridge.getSessionArtifacts)。运行 createAndStoreSession 的 ACP 子进程里没有任何 SessionArtifactStore 引用,按文档实现的 seed 不会进入 HTTP API 可见的任何状态。现有的子进程→daemon 通路(qwen/notify/session/artifact-event)也不能直接顶上:handleArtifactEvent 会打上 source:'hook' 并按新摄入处理(restoreState 等 V2 字段无法往返),且在 SessionEntry 注册前(loadSession resolve 之后才注册)事件会被丢弃。建议把 seed 规定在 bridge 侧(createSessionEntry / 恢复完成时),或定义显式的注册后 snapshot 通知。

2. §4.3 — sidecar 路径按过时的 chats 位置建模(L176)
~/.qwen/tmp/<project_hash>/chats/<sessionId>.artifacts.json 假设 chat JSONL 在 tmp/<hash>/chats/ 下。实际不是:写入方为 packages/core/src/services/chatRecordingService.ts:627storage.getProjectDir() + 'chats',而 getProjectDir()~/.qwen/projects/<sanitized-cwd>packages/core/src/config/storage.ts:313-320,键是 sanitize 后的 cwd,不是 hash);tmp/<hash>getProjectTempDir()(tool-results 等)。tmp/<project_id>/chats/ 只残留在过时注释里(sessionService.ts:269chatRecordingService.ts:476sessionStorageUtils.ts:22)— 文档看起来是照抄了其中一条。后果:sidecar 会被写到/校验于 session loader 根本不读的目录,§4.3"archive 时与 chat JSONL 一起移动"的前提也不成立。示例中的 projectHash 缓存键(L186/L200)继承了同一个过时模型。

3. §5.4 — 只允许 http/https 的恢复规则会封死所有本地 published artifact(L293)
V1 有意允许 trusted published artifact 使用 file: URL(packages/acp-bridge/src/sessionArtifacts.ts:1241 normalizeArtifactUrl(raw, allowFile);调用点 :392 传入 trustedPublisher),默认 publisher 返回的就是 file:// URL(packages/core/src/tools/artifact/local-publisher.ts:40 pathToFileURL(...);local 是默认),且 urlstorage:'published'唯一 locator(validateLocator,sessionArtifacts.ts:1017-1027)。既然 §2.2 把 published 默认为 restorable,按 §5.4 原文实现,所有本地发布的 artifact 恢复后都会 blocked/不可打开。雪上加霜:§4.2 禁止持久化 trustedPublisher,恢复路径即便重新通过 publisher manifest 校验也没有任何信号可以重新放行 file: — 而 §7.3 的"不保存 host 绝对路径"本身也与持久化 file:///... URL 相冲突。需要显式规则:manifest 信任重新校验通过时允许 file:,或把 published URL 重写为 daemon 服务的 URL。

规范内部问题

4. §7.1/§6.5 — session_owner 和"用户确认"在 daemon 现有认证模型下无法铸造(L452)
daemon 的 HTTP 信任边界是单一共享 bearer token(run-qwen-serve.ts:1019;artifact REST 路由只有全局 mutate({strict:true}) 门)。现存的唯一 per-session 原语 conn.ownsSession 撑不起 session_owner:它只有连接生命周期(daemon 重启后为空 — 恰是持久化要解决的场景)、任何 token 持有者可通过 session/load 自铸(sibling 共同所有是设计使然)、且在 §6.3 pin endpoint 所在的 REST 面上根本不存在。也没有任何机制把 client 发起的 REST 变更挂起等待人工确认(permission-vote 流程只覆盖 prompt turn 中 agent 发起的工具调用)。按现文实现,owner 层级塌缩为"持有 token"。文档应定义 session_owner 如何建立并跨重启持久化(创建时铸造持久的 per-session capability?还是显式降级为 token-holder?),以及 headless client 的"用户确认"机制上指什么。

5. §6.2 + Milestone D vs §2.2 — POST retention 白名单排除了被强制要求的 ephemeral 退出口(L367、L621)
§6.2"client 不能请求 pinned,只能请求 restorable"与 Milestone D"POST /session/:id/artifacts 支持 retention: "restorable""都把白名单枚举为 {restorable} — 但 §2.2(L56)要求后台 client 在同一 endpoint 显式请求 retention: "ephemeral"(且 SDK 要提供 ephemeral helper),§2.3(L74)要求按显式 retention 处理。照 §6.2/Milestone D 写请求校验的实现者,会恰好拒绝 §2.2 强制要求的请求。两处都应改为"ephemeralrestorable"。

6. §6.4 — unpin 的降级参数未定义,且 unpin→ephemeral 会在下次 load 复活(L411)
降级目标("restorableephemeral,由请求参数决定")没有任何 body/query schema(对比 §6.3 的 {"mode": ...})。更糟的是,按文档自己的规则 — §5.1 只对 restorable/pinned 变更写 journal、§8.1"ephemeral 本来不写 journal"、§5.3 replay 只应用 tombstone、§4.2 只要求删除写 tombstone(而 §6.4 明确 unpin 不是删除:"不删除 metadata")— unpin 到 ephemeral 后,artifact 在 journal 里的最后状态仍是之前的 pinned upsert,下次 session load 就会复活,与 §2.2"ephemeral…不恢复"矛盾。§4.2 的 reason:'unpin' 枚举值被默认路径(→restorable 的 upsert)消耗掉了,并没有给 ephemeral 变体指定任何记录;§10 也只测了 DELETE tombstone 场景。应定义参数、要求 ephemeral 目标写 tombstone(或显式的恢复跳过规则),并补测试。

7. §8.3 — 设了 TTL 的 pinned content 被唯一的过期 GC 规则排除在外(L540)
§6.3 的 ttlDays 产生的是带 expiresAtpinned content(§3.1"pinned 默认不设置"= 可设置),§8.2 也明说 pinned content 会被自动删除"除非用户设置 TTL"(即设了 TTL 就该删)— 但 §8.3 唯一基于过期时间的规则是"删除超过 expiresAt 的 non-pinned content",措辞上恰好排除 pinned,其余规则又都要求 tombstone 或 session delete。结果:ttlDays 是死参数,过期的 pinned content 一直累积到 QUOTA_EXCEEDED。应补一条回收过期 pinned content 的 GC 规则(或让 TTL 走 unpin 语义)。

8. §4.3/§4.4 — 按文档自己的分析 sidecar 几乎没有收益,且两个 sequence 字段冗余(L174、L205)
§8.5(4) 自己承认 SessionService.loadSession() 已完整读取 JSONL,可"在同一轮 parse 中顺手提取"artifact records(今天的 file_history_snapshot 恢复正是这样通过 readAllRecords 实现的),且 fallback replay 已被 100 mutations / 256 KB snapshot 阈值限定;§4.4 也承认"sidecar 只解决性能,不承载协议正确性"。为了这个近乎为零的收益,规范却要付出:原子写、6 字段陈旧校验、损坏 fallback、archive/unarchive/delete 联动、orphan sidecar GC(§8.3)、crash 一致性条款(§8.4)和 §10 的 3 个专项测试。建议 V2 先只上 JSONL,实测 session load 慢了再加缓存。另外 sequence vs sourceArtifactSequence:文档自己说"正常情况下两者相同",全文找不到二者合法分叉的场景(rebuild 和 compaction 都会把两者重算成同一值);而且要校验 sourceArtifactSequence 是否匹配源 JSONL 的最新序号,必须先解析 JSONL — 这恰恰使缓存失去意义。留一个字段即可(或干脆去掉 sidecar,问题自然消失)。

次要

  • §3.1(L100)"缺省按 V1 兼容处理为 live-only"与 §2.2/§2.3/§6.1 的 restorable 默认值:两者只有在"L100 是读取侧(解释 V1 时代的记录)、§2.2 管摄入侧"的划分下才自洽 — 但文档从未说明这个划分,也从未要求 V2 写入方在 §4.2 journal record(复用同一 optional shape)里显式落 retention 字段,于是一条缺字段的 V2 记录会被合法地按 live-only 解读、从恢复中丢掉。加一句话即可修复。
  • §4.5/§8.1:content 有 per-project 上限,但 persisted metadata 没有任何跨 session 上限,且每 100 次 mutation 的 snapshot 都会往同一个 JSONL 追加一份 ≤1 MB 的完整拷贝 — 用来控制 replay 成本的机制本身在增长 §4.5 承认"可能继续增长"的那个文件。值得加一句明确接受的策略(metadata 随 chat transcript 生命周期)或补一个 journal 字节上限。

另外说明:文档语言、目录位置与格式均符合现有 docs/design/ 惯例,prettier 检查通过。

Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
@chiga0

chiga0 commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review feedback in commits 9b3a00ca1 and c376e1ff8.

Summary:

  • Fixed the restore integration point: the design now seeds SessionArtifactStore on the daemon-side bridge (createSessionEntry / restore completion), not in the ACP child process.
  • Removed sidecar from the V2 design. V2 is now JSONL-only; sidecar is explicitly deferred until measured load cost requires a separate cache design. This makes the stale sidecar path/cache-validation comments no longer applicable.
  • Preserved the V1 published-artifact exception: ordinary external_url stays http/https-only, while trusted manifest-validated published artifacts may restore file: locators when they resolve to daemon-managed storage.
  • Reworked auth language to match the current bearer-token route model; no unmintable durable session_owner is assumed in V2.
  • Allowed Client POST to request ephemeral or restorable, rejecting only pinned on that endpoint.
  • Defined unpin body semantics and required tombstone/restore-skip behavior for unpin-to-ephemeral.
  • Added durable-first/rollback semantics for DELETE, eviction, unpin-to-ephemeral, and restore_pruned tombstones.
  • Added contentRef ingest/restore validation through daemon-managed manifests with size/hash checks.
  • Added race-safe content-copy requirements, secret-bearing URL/metadata handling, V1-to-V2 live backfill, aligned persisted metadata cap with live cap, fork pinned-content downgrade, expired pinned-content GC, JSONL sequence/snapshot/tombstone invariants, observability, and expanded test coverage.

Validation run locally:

npx prettier --check docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md
git diff --check
awk '/```/{c++} END{if (c%2==0) print "fences_ok", c; else print "fences_bad", c}' docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md

All review threads have been replied to and resolved. reviewDecision may remain CHANGES_REQUESTED until a reviewer re-reviews.

Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

代码审查总结

审查了完整的设计文档,以下是发现的问题。其中 F1 为 Critical 级别——V2 设计移除了 V1 现有的 per-client delete 保护机制,但未引入替代方案。

高置信度发现 (已作为 inline comment 提交)

# 严重度 主题 位置
F1 Critical V2 移除 V1 per-client delete 保护 Section 6.4
F4 Suggestion deleteContent 授权标准未定义 Section 6.4
F5 Suggestion GC 孤儿 grace period 未指定 Section 8.3
F6 Suggestion 缺少诊断/修复工具规范 Section 8.6
F7 Suggestion V2→V1 回滚未覆盖测试 Section 10
F9 Suggestion ttlDays 无上限约束 Section 6.2
F11 Suggestion 工作区重定位导致 artifacts 静默失效 Section 3.3
F15 Suggestion 裁剪顺序使用「例如」导致非规范 Section 8.1
F3 Suggestion "restore-skip 记录"未在 schema 定义 Section 5.3

终端可见 (低置信度)

  • F2: GC TOCTOU 竞态(跨 session pin)
  • F8: expiresAt 在 restore 路径未检查
  • F12: workspace_reference 是死枚举变体
  • F14: Fork snapshot 记录处理未定义

— qwen3.7-max via Qwen Code /review

Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Session Artifacts Persistence v2 · 设计审计

本文档在 v1 内存 store 之上加持久化(journal upsert/remove event + snapshot + tombstone,靠 active-branch replay 恢复)。方向合理,但核心的「tombstone + active-branch replay」持久化模型存在多处会导致「删除项复活 / 持久化项静默消失」的正确性缺口,以及一处安全回归。高强度审计(24 agent,17 条确认)后按你的维度归纳如下,逐条 inline。

① 可行性 / 可实施性 🔴

  • restore 依赖 active-branch,但 reconstructHistory() 只走 leaf 链(sessionService.ts:897)——/rewind 后 turn N 的 artifact journal 记录落到废弃分支被丢弃:删除 tombstone 被丢而旧 upsert 存活 → 删除项复活;或持久化项静默消失。代码里 file_history_snapshot 已为此加了显式 「top up」(sessionService.ts:1402-1426),v2 artifact 记录需要等价机制但设计未写。
  • restore-prune tombstone 无 durable 落盘路径:prune 发生在 loadSession()(§5.4 step 1),而 store + operationQueue 到 step 6 才创建 → 首次 GET 与后续 replay 结果不一致。
  • ACP 进程边界未打通(PLAUSIBLE):snapshot 在 qwen --acp child 产出,但 daemon 侧 store 只能经 loadSession 的 ACP LoadSessionResponse RPC 拿到;Milestone C 只提到扩展 core 返回类型,未点名扩展 ACP wire 契约 → daemon 重启后 GET 返回空、v2 承诺失效。

② 副作用 / 安全回归 🔴

  • creator-only delete guard 每次重启失效:creator principal 故意不落盘(L210),restore 后 L496 分支必然命中 → 降级为「任意 session-mutate 持有者可删」,v1 的越权边界被静默移除(不是边界 case,是每次重启)。
  • v1 内存 eviction 现写永久 tombstone:v1 §7.1 eviction 只为限内存,v2 却给它写 durable remove tombstone → 一个仅因 source 超软配额被逐出的 restorable artifact 被永久删除、永不恢复,即便持久化配额(§8.1 的 200)还有空位 → 数据丢失。
  • live-first vs tombstone-first 自相矛盾(§5.2 vs §5.3):满 200 时 add 内触发 eviction,§5.2 先改内存、§5.3 要求先写 tombstone;tombstone 写失败时内存已改无法回滚 → 崩溃后复活、可能超 200。
  • unpin→ephemeral 被后续 upsert 复活:unpin 保留 live + 写 tombstone,但工具常重发同 identity → tombstone 之后又来 upsert → 下次 load 复活持久化,静默撤销用户「不要持久化」意图。
  • DELETE 耦合 durable tombstone → 可用性回归:v1 DELETE 是纯内存、保证成功;v2 journal 不可写(磁盘满/只读)时 DELETE 报错且 live 不变 → 用户无法移除误登记或敏感 artifact。
  • 内部字段丢失影响 eviction 公平性clientRetained/insertSeq 不入 journal(§4.2),restore 后重置为 false/新序号 → 重启后一波新 artifact 触顶会裁掉用户点过 keep 的项。

③ 性能消耗 🟠

  • tombstonedIds 无界增长:每个 snapshot「必须携带 tombstonedIds」但无过期/compaction → 频繁创建删除的长 session 让 snapshot 撑爆 100-400KB / 4MB working-set 预算(§4.4)→ journal_budget_exceeded、新 artifact 降级 ephemeral,即便 live artifact 远不足 200。

④ 是否有更好的替代方案

上面的复活/顺序/膨胀问题大多源于「把 artifact 事件塞进 chat-record 树、靠 active-branch replay 恢复」。可考虑:

  1. artifact 用独立的 per-session append-only log + 定期 compaction(不挂 chat-record 树),避开 /rewind 分支丢弃问题,tombstone 在 compaction 时 GC,天然解决 #1/#7;
  2. snapshot 为权威、event 只做增量并在每次 snapshot 后截断(snapshot-authoritative),tombstonedIds 只需覆盖「最近一次 snapshot 之后」的删除,不再无界;
  3. eviction 与持久化解耦:内存 eviction 只影响 live 视图、不写 tombstone;持久化用独立 GC/配额 —— 直接消除 #4
    建议在文档「Alternatives / 取舍」一节显式对比现方案与上述,尤其为何选择耦合 chat-record 树。

⑤ 生命周期 / GC

  • tombstone 无 compaction(#7)、restorable 配额与内存 eviction 语义纠缠(#4)——GC 未闭环。session 结束后持久化数据的保留/过期策略也未见明确 TTL。

⑥ v1→v2 迁移 / 兼容

  • 未见 schema 版本化与 v1(无持久化)→ v2 的升级路径说明;restore 对缺 sha256/缺内部字段的历史记录的兼容处理需补。

总评

尚不足以作为实现依据。 方向与数据模型基本成立,但 tombstone/replay 持久化模型有 3 类必须先解决的问题:(A) active-branch replay 与 /rewind 的复活/丢失(#1#2#5),(B) 安全回归 creator guard 失效(#3)+ eviction 永久删除 restorable(#4),(C) tombstonedIds 无界(#7)。建议先在文档层面:明确 restore 的 top-up/权威快照策略、把 eviction 与持久化 tombstone 解耦、给 creator principal 一个可验证的持久化/降级契约、给 tombstone 定 compaction,并补一节替代方案对比与 v1→v2 迁移。逐条见下方 inline。

Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Round-2 pass at e90c6c4 — I re-verified every earlier "Fixed in …" reply against the current text and against the V1 implementation on main; the prior findings are genuinely resolved. The new findings below are mostly architectural: the journal design models the chat JSONL as a linear log the daemon can append to, but the real transcript is a parentUuid tree written exclusively by the ACP child process — that assumption breaks the write path, rewind, and fork semantics (first three comments). The rest cover pinned-vs-eviction, the V1→V2 backfill, the delete-authorization model, and two spec-consistency items.

Two minor textual mismatches, no dedicated comment: §2.3 L77 / §5.5 L321 still describe restore as recovering the persisted published locator, while §7.2 L520 requires file: locators to be regenerated from the manifest and never taken from old JSONL — the older sections should say the persisted locator is opaque and the openable locator is manifest-derived. And the three content-quota surfaces disagree in naming/scope: §8.2 defines artifact/session/project tiers, §6.1 exposes maxPinnedArtifactBytes/maxPinnedSessionBytes (no project knob), §7.3's settings offer maxArtifactBytes/maxTotalBytes (1 GB matches the project tier but the name doesn't say so, and there is no session-level setting).

Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review Summary

Scope: 1 file, +843 lines — design document for session artifacts persistence V2

Findings: 19 verified (13 high-confidence, 6 low-confidence), after deduplication against 15+ existing review comments.

Overall this is a thorough and well-structured design document. The three-tier retention model, snapshot/tombstone invariants, and restore validation are all well thought out. The findings below focus on specification gaps that could cause ambiguity during implementation.

Critical (7)

  1. Line 220clientRetained used for sorting/eviction but no API/SDK/ingest field sets it
  2. Line 219PersistedSessionArtifact inherits runtime fields (restoreState, persistenceWarning) that would replay stale values
  3. Line 636 — GC TTL downgrade has no journal event reason enum value
  4. Line 454 — Pin/Unpin/Delete endpoints lack response body structure and HTTP status codes
  5. Line 749 — V1→V2 backfill described in §2.1 but no milestone in §9 delivers it
  6. Line 280expiresAt not listed as daemon-only at ingest — client could inject it
  7. Line 105persistenceWarning.message is free-form with no content constraint

Important (8)

8–19. See inline comments for details.

Positive

  • Clear three-tier retention lifecycle with well-defined invariants
  • Strong restore validation with defense-in-depth
  • Tombstone/snapshot consistency model is thorough
  • Privacy and security considerations are comprehensive
  • Pragmatic milestone decomposition with capability gating

Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Qwen Code Review — PR #6259

V2 session artifacts 持久化设计文档整体非常详尽,覆盖了 metadata restore、content retention、crash consistency、安全边界、配额和 GC 等关键维度。以下是 review 发现的需要关注的问题。

感谢作者的深入设计工作 🙏

Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

自动 Review 结果

本 PR 是一份详尽的 V2 持久化设计文档(1009 行)。整体架构清晰、安全边界考虑充分。以下是本轮 review 发现的新问题,不包含已有 18 条已修复评论的内容。

汇总: 5 Critical / 6 Suggestion

Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review — PR #6259

Model: qwen3.7-max | Mode: comment

Summary

This PR adds a comprehensive V2 design doc for daemon session artifact persistence. Overall the document is thorough and well-structured, covering a broad set of edge cases (fork semantics, crash consistency, authorization, quota/GC). Below are findings from automated review.


Findings delivered: 9 (3 Critical, 6 Suggestion)

  • Critical issues focus on restore correctness (snapshot ordering, status ambiguity) and observability gaps (operation queue stall).
  • Suggestions cover unbounded growth, degenerate ordering, API schema gaps, and cleanup protocols.

This review was generated automatically by Qwen Code.

Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
Comment thread docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md Outdated
@chiga0

chiga0 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Synced #6259 with latest origin/main in 7440b0806 using a normal merge commit.

Merge notes:

  • origin/main advanced to 880b06ed4.
  • The only manual conflict was packages/sdk-typescript/scripts/build.js, where both sides updated the daemon browser bundle size budget. I kept both SDK surface notes and set the combined budget to 140KB.
  • No artifact persistence semantics, recovery behavior, security boundary, or API contract conflict required manual design judgment.

Validation run locally:

  • npx prettier --check docs/design/daemon-session-artifacts/session-artifacts-persistence-v2-design.md packages/sdk-typescript/scripts/build.js
  • git diff --check
  • cd packages/sdk-typescript && npm run build
  • cd packages/core && npx vitest run src/services/session-artifact-persistence.test.ts src/services/sessionService.test.ts
  • cd packages/acp-bridge && npx vitest run src/sessionArtifacts.test.ts src/bridge.test.ts -t "artifact snapshots|periodic snapshots|rewind|tombstone"
  • cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts

Pushed normally to the fork branch and upstream helper branch; no force push.

Comment thread packages/acp-bridge/src/bridge.ts Outdated
if (replayUpdates.length > 0) {
await ci.client.seedSessionUpdates(entry, replayUpdates);
await ci.client.seedSessionUpdates(entry, replayUpdates, {
ingestArtifacts: restoredArtifactSnapshot === undefined,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] After a failed restore() (rollback), restoredArtifactSnapshot remains non-undefined because it was assigned as a const before restore() ran. This causes ingestArtifacts: false, so replay artifacts from seedSessionUpdates are silently dropped — even though the restore rolled back and the live store still has pre-restore state.

The fix: check for restore failure warnings (e.g. via isArtifactRestoreFailureWarning) and set ingestArtifacts: true when restore failed.

Suggested change
ingestArtifacts: restoredArtifactSnapshot === undefined,
ingestArtifacts:
restoredArtifactSnapshot === undefined ||
artifactRestoreWarnings.some((w) =>
isArtifactRestoreFailureWarning(w),
),

— qwen3.7-max via Qwen Code /review

: undefined,
};
const changes: SessionArtifactChange[] = [removeChange];
const warnings = await this.persistChanges(changes, false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] remove() uses strict=false, so when persistence fails the deletion succeeds in-memory but the tombstone is never written to the JSONL journal. On daemon restart, rebuildSessionArtifactSnapshot replays the journal and the deleted artifact reappears — a data resurrection bug.

The user got a success response with a warning, but the artifact comes back after every restart. Consider using strict=true for removals so the user gets a clear error when the tombstone can't be persisted.

Suggested change
const warnings = await this.persistChanges(changes, false);
const warnings = await this.persistChanges(changes, true);

— qwen3.7-max via Qwen Code /review

const stored = this.artifacts.get(change.artifactId);
if (!stored) continue;
stored.retention = 'ephemeral';
stored.persistenceWarning = 'persistence_unavailable';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] When persistence fails with strict=false, artifacts are silently downgraded from restorable to ephemeral. The warning string 'artifact persistence unavailable; durable artifacts kept ephemeral' is returned to the caller, but the data loss is irreversible: on session restart, these artifacts are gone because ephemeral artifacts are not persisted to JSONL.

Clients may not surface this warning prominently. Consider making strict=true the default for user-initiated artifact operations, or escalating this to a structured error that clients must surface.

— qwen3.7-max via Qwen Code /review

const normalized = normalizeEventPayload(record, warnings);
if (
!normalized ||
warnings.length > 0 ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] warnings.length > 0 rejects the entire batch if any single artifact produces a normalization warning. One malformed artifact (e.g. from a buggy tool/hook with a too-long title) causes all artifacts in the same batch to lose persistence via the downgradeDurableChanges fallback.

Consider either: (1) relaxing validation to accept artifacts with warnings (matching the normalization logic), or (2) splitting the persist call per-artifact so one failure doesn't affect others in the batch.

— qwen3.7-max via Qwen Code /review

if (!isSessionArtifactRecord(record)) {
continue;
}
if (record.parentUuid !== null && activeUuids.has(record.parentUuid)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] After a rewind, abandoned-branch artifact records can be incorrectly included here. An artifact record from the abandoned timeline whose parentUuid points to a conversation record still on the active branch (e.g., the rewind anchor) will pass this check and be replayed by rebuildSessionArtifactSnapshot.

Concrete scenario: if a created artifact event exists between the rewind anchor and the rewind point on the old branch, it produces a phantom artifact after restore. A removed event silently deletes a legitimate artifact.

Fix: also check that the artifact record appears in the JSONL after the rewind point (by position/index), not just that its parent is in the active set.

— qwen3.7-max via Qwen Code /review

restoredCount < snapshot.artifacts.length &&
warnings.length > baselineWarnings.length
) {
this.restoreState(previousState);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] restore() performs all-or-nothing rollback when any single artifact fails normalization. If 1 of 100 artifacts has a stale workspacePath or invalid metadata, all 99 successfully restored artifacts are discarded via restoreState(previousState). On a fresh session load (where previousState is empty), the user sees zero restored artifacts.

Additionally, the warning message at line 615 says "restored N/M artifacts; kept existing live artifacts" — but the rollback already discarded those N restored artifacts, making the message misleading.

Consider a keep-successful strategy: retain the artifacts that passed normalization and only skip the ones that failed. A single corrupted artifact in the persisted snapshot should not permanently block all artifact restore for that session.

— qwen3.7-max via Qwen Code /review

if (validationStrict) {
throw error;
}
if (error instanceof SessionArtifactAuthorizationError) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] When denyCrossClientMutation throws SessionArtifactAuthorizationError in non-strict mode, the error is caught and silently swallowed via continue. The caller receives a successful mutation result with zero changes and no warning indicating their request was denied.

A client that attempts to modify another client's artifact gets HTTP 200 with an empty changes list — they have no way to know the operation was rejected. Add a warning to the result so the caller can observe the denial:

Suggested change
if (error instanceof SessionArtifactAuthorizationError) {
if (error instanceof SessionArtifactAuthorizationError) {
warnings.push(`cross-client mutation denied: ${error.message}`);
continue;
}

— qwen3.7-max via Qwen Code /review

}
let normalized = await this.normalizeInput(
input,
++this.receivedSeq,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Published artifacts with file: URLs are silently dropped during session restore. The trustedPublisher flag is computed as artifact.storage === 'published' && !isFileArtifactUrl(artifact.url) (lines 551-553), which evaluates to false for file: URLs. The subsequent normalizeArtifactUrl(input.url, false) call rejects file: protocol, causing the artifact to be skipped with a generic warning.

This is a creation/restore asymmetry: tools acting as trusted publishers can create artifacts with file: URLs during live sessions, but those same artifacts are lost after daemon restart or session resume. Fix by preserving the original trust decision for restored published artifacts:

Suggested change
++this.receivedSeq,
let normalized = await this.normalizeInput(
input,
++this.receivedSeq,
artifact.storage === 'published',

— qwen3.7-max via Qwen Code /review

insertSeq: ++this.insertSeq,
});
}
const evicted = await this.evictOverflow(new Set(), []);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] selectEvictionCandidate (called from evictOverflow) does not consider retention when ranking candidates. Ephemeral artifacts compete equally with restorable ones for eviction slots, prioritized only by oldest createdAt/insertSeq.

After restore() re-adds preserved ephemeral artifacts (lines 620-629), an old restorable artifact is more likely to be evicted than a newer ephemeral. This inverts the intended durability guarantee: in a session at capacity, a rewind that triggers restore followed by ephemeral re-insertion can immediately evict durable artifacts that were just restored from persistence.

Add retention-aware eviction priority: evict ephemeral artifacts before restorable ones.

— qwen3.7-max via Qwen Code /review

createdAt: string;
}

export interface PersistedSessionArtifact {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] PersistedSessionArtifact does not include a clientId field. Both toPersistedArtifact and persistedArtifactToInput in sessionArtifacts.ts omit clientId entirely. After session restore, all client-owned artifacts lose their ownership information.

This has two impacts:

  1. Authorization bypass: denyCrossClientMutation checks existing.clientId === undefined and returns early (allowing the mutation). After restore, any client can modify or delete artifacts originally owned by a different client.
  2. Tombstone logic breakage: shouldSuppressTombstonedUpsert requires artifact.clientId === tombstonedClientId to allow re-creation of tombstoned artifacts. After restore, tombstonedClientIds entries are undefined, so legitimate client re-creations are silently suppressed.

Add clientId?: string to PersistedSessionArtifact, serialize it in toPersistedArtifact, pass it through in persistedArtifactToInput, and validate it in normalizePersistedArtifact.

— qwen3.7-max via Qwen Code /review

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Eviction does not propagate durableTombstoneRequired — artifact resurrection on restart

When an artifact was persisted (durableTombstoneRequired = true) then downgraded to ephemeral by a transient persistence failure (downgradeDurableChanges sets retention = 'ephemeral' but keeps durableTombstoneRequired = true at line 879), and later evicted by evictOverflow, the eviction change object omits durableTombstoneRequired. The eviction push at line ~1284 creates:

removed.push({ action: 'removed', artifactId: artifact.id, artifact: toPublicArtifact(artifact), reason: 'eviction' });

isDurablePersistenceChange returns false, so no tombstone is written. On restart, the original created event replays with no tombstone — the evicted artifact reappears.

The remove() method correctly copies durableTombstoneRequired to its change object (line 492). Eviction should do the same.

Note: durableTombstoneRequired is currently an undeclared field bolted onto SessionArtifactChange via an inline intersection type (line 486) and read via a runtime cast (line 1831). Declaring it on the interface directly would make this kind of omission impossible — the compiler would flag the missing field.

const updated = mergeArtifact(existing, artifact);
if (updated.changed) {
if (updated.artifact.id !== existing.id) {
this.artifacts.delete(existing.id);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Published-upgrade changes the artifact ID but doesn't tombstone the old identity, producing duplicate artifacts on restore.

When a trusted publisher publishes an artifact whose URL matches an existing external_url artifact, findPublishedUpgradeTarget finds the old artifact (by url-based id), and mergeArtifact sets next.id = incoming.id (managed-based id) — a completely different string. The old ID is deleted from the map but only a single 'updated' change is pushed for the new ID. No 'removed' change or tombstone is generated for the old ID.

On restore, rebuildSessionArtifactSnapshot replays both the old created event (url-based id) and the new updated event (managed-based id), producing two artifacts where there should be one. The duplicate only disappears after a compaction snapshot (requires 50 subsequent durable events).

Suggested change
this.artifacts.delete(existing.id);
if (updated.artifact.id !== existing.id) {
changes.push({
action: 'removed',
artifactId: existing.id,
artifact: toPublicArtifact(existing),
reason: 'explicit',
});
this.artifacts.delete(existing.id);
}

— glm-5.2 via Qwen Code /review

if (change.action === 'removed') {
removalNotPersisted = true;
if (change.reason === 'explicit') {
this.rememberTombstone(change);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Deleted artifacts resurrect after daemon restart when the tombstone write fails.

remove() deletes the artifact from live memory first (line 482), then calls persistChanges(changes, false). If the JSONL write fails (ENOSPC, EACCES, EROFS), strict=false routes to downgradeDurableChanges, which calls rememberTombstone() — an in-memory Set that dies with the process. The removal event is never written to the journal.

On daemon restart, rebuildSessionArtifactSnapshot replays the journal: the original created event IS in the journal, but the removal event is NOT, and the in-memory tombstone is gone. The artifact reappears in the user's list with no warning.

The client does receive 'artifact removal not persisted; live removal kept', but the underlying reason (ENOSPC, EACCES) is stripped — only stderr gets it. Consider: (1) keeping a durable pending-tombstone sidecar that re-emits on the next successful persistChanges, and (2) appending the error reason to the client-facing warning so it's diagnosable without stderr access.

— glm-5.2 via Qwen Code /review

@chiga0

chiga0 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Processed the latest suggestion summary and pushed e5d2e8756 (fix(daemon): harden artifact persistence validation).

Fixed in this commit:

  • URL validation now scans query parameter values and fragments for token-like secrets (Bearer, sk-, GitHub-style tokens, long hex/base64), not only query keys.
  • Inbound artifact event normalization now allows up to 800 changes, covering the current 400 input batch limit plus up to 400 eviction/removal changes without rejecting the whole persistence payload.

Left for maintainer/design decision rather than automatic change:

  • Post-restore overflow eviction persistence failure: changing this from live-prune-with-warning to restore rollback affects recovery semantics.
  • Artifact-only sessions with no conversation records: loadSession() currently returns before artifact rebuild when reconstructed conversation is empty; supporting this would change resume/load contract beyond a local side-record filter tweak.

Validation:

  • npx prettier --check packages/acp-bridge/src/sessionArtifacts.ts packages/acp-bridge/src/sessionArtifacts.test.ts packages/core/src/services/session-artifact-persistence.ts packages/core/src/services/session-artifact-persistence.test.ts
  • cd packages/acp-bridge && npx vitest run src/sessionArtifacts.test.ts
  • cd packages/core && npx vitest run src/services/session-artifact-persistence.test.ts
  • git diff --check
  • cd packages/acp-bridge && npm run typecheck
  • cd packages/core && npm run typecheck

@chiga0

chiga0 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up for the latest review pass: pushed commit c6229aea0 with the metadata/recovery fixes that stay within #6259's current V2 boundary.

Addressed here:

  • Re-ingest replayed seed artifacts when persisted artifact restore reports a restore failure warning.
  • Surface non-strict cross-client artifact ownership denials as warnings instead of silently swallowing them.
  • Emit a removal for the previous local identity when a trusted published artifact upgrades to a managed/published identity, while keeping durable tombstones conditional on durable/restored state.
  • Declare the durableTombstoneRequired sideband on SessionArtifactChange.
  • Avoid marking restored ephemeral artifacts as sticky unless the persisted sticky marker actually applies.
  • Preserve durable markers after a non-strict persistence failure downgrade.
  • Filter unsafe persisted metadata keys/string values during restore normalization.

Left unresolved for design/maintainer decision rather than auto-changing in this PR:

  • Whether DELETE/tombstone persistence failures should become strict request failures instead of 200 + warning.
  • Whether non-strict durable persistence failures should avoid live-state downgrades entirely.
  • Whether ACP artifact batches with normalization warnings should partially accept valid records.
  • Whether artifact restore should become all-or-nothing instead of preserving successfully restored entries.
  • Published file URL/content restore behavior, which appears to cross the current metadata-only/content-retention boundary.
  • Retention-priority eviction semantics after restore.
  • Persisting/enforcing clientId for restored records; the current design intentionally avoids using persisted clientId as an authorization boundary.
  • Filtering abandoned-branch artifact records after rewind/fork-remap, which changes recovery semantics.
  • Fork remap status normalization, which changes fork recovery semantics.

Validation:

  • npx prettier --check packages/acp-bridge/src/bridge.ts packages/acp-bridge/src/sessionArtifacts.ts packages/acp-bridge/src/sessionArtifacts.test.ts packages/core/src/services/session-artifact-persistence.ts packages/core/src/services/session-artifact-persistence.test.ts
  • cd packages/acp-bridge && npx vitest run src/sessionArtifacts.test.ts
  • cd packages/core && npx vitest run src/services/session-artifact-persistence.test.ts
  • cd packages/acp-bridge && npx vitest run src/bridge.test.ts -t "restores and persists artifact snapshots returned by rewind"
  • cd packages/acp-bridge && npx vitest run src/bridge.test.ts -t "restores artifact snapshots that omit marker arrays after fork remap"
  • git diff --check
  • cd packages/acp-bridge && npm run typecheck
  • cd packages/core && npm run typecheck

@chiga0

chiga0 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up: after pushing c6229aea0, origin/main advanced and #6259 is now merge-dirty.

I prechecked the merge and found a content conflict in packages/core/src/services/sessionService.ts. This is not a mechanical formatting conflict: upstream changed reconstructHistory to use buildOrderedUuidChain and surface historyGaps, while #6259 changes the same recovery path to rebuild artifact snapshots and remap artifact records for forks.

Because this touches restart recovery / fork recovery semantics and the ResumedSessionData contract, I am not auto-resolving it. It needs a maintainer/design decision on how artifact snapshot reconstruction should compose with the new history-gap detection behavior.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.

@chiga0

chiga0 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Processed the latest suggestion summary for c6229aea0 and pushed 9dcee96a9 (fix(acp): address artifact restore suggestions).

Handled:

  • Use the resolved workspace cwd consistently for workspace artifact path normalization/status checks, covering symlinked workspace roots.
  • Return an explicit restore warning when persisted pinned artifacts are downgraded to restorable because runtime metadata currently does not support pinned retention.

Validation:

  • npx prettier --check packages/acp-bridge/src/sessionArtifacts.ts packages/acp-bridge/src/sessionArtifacts.test.ts
  • cd packages/acp-bridge && npx vitest run src/sessionArtifacts.test.ts
  • git diff --check
  • cd packages/acp-bridge && npm run typecheck

Remaining non-automated blocker: #6259 is still merge-dirty against origin/main in packages/core/src/services/sessionService.ts; the conflict composes upstream history-gap recovery with this PR's artifact snapshot restore/fork remap behavior, so it still needs a recovery-semantics decision before I can safely sync main.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two correctness gaps in persistence error paths (pre-existing code, not in this diff):

  1. persistChanges no-persistence path (line 771) omits applyDurableMarkers — the error path at line 822 correctly calls both downgradeDurableChanges + applyDurableMarkers, but the no-persistence early-return at line 771 only calls downgradeDurableChanges. Eviction removals skip sticky ephemeral cleanup when persistence is unavailable.

  2. Eviction removals don't propagate durableTombstoneRequiredevictOverflow (line ~1322) creates action: 'removed', reason: 'eviction' without durableTombstoneRequired. After a durable artifact is downgraded to ephemeral (persistence unavailable) then evicted, isDurablePersistenceChange returns false, no tombstone is written, and the artifact reappears on restore.

Suggestion-level recommendations are in the Suggestion summary comment below.

— qwen3.7-max via Qwen Code /review

@chiga0

chiga0 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Processed the latest persistence error-path review in a1c04cb3c (fix(acp): persist downgraded eviction tombstones).

What changed:

  • The no-persistence durable-change fallback now applies the same durable marker bookkeeping as the persistence-failure fallback.
  • Eviction records now carry durableTombstoneRequired when the evicted artifact was already downgraded from durable persistence, so the eviction tombstone is written once persistence is available again.
  • Added targeted coverage for the downgraded-durable eviction tombstone path.

Validation:

  • npx prettier --check packages/acp-bridge/src/sessionArtifacts.ts packages/acp-bridge/src/sessionArtifacts.test.ts
  • cd packages/acp-bridge && npx vitest run src/sessionArtifacts.test.ts
  • git diff --check
  • cd packages/acp-bridge && npm run typecheck

@wenshao

wenshao commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

…stence

Integrated main's workspace-aware routing (withOwnerReadSession) into
the GET /session/:id/artifacts endpoint while preserving clientId
pass-through from the PR branch. Kept both artifactSnapshot (PR) and
historyGaps (main) fields on ResumedSessionData. Adapted forkSession
to main's extracted buildOrderedUuidChain utility while retaining
includeActiveSideArtifactRecords for artifact-aware history reconstruction.

Co-authored-by: Qwen-Coder <[email protected]>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge Conflict Resolution Summary

PR: #6259feat(daemon): persist session artifacts across restarts
Base: origin/mainfeat/session-artifacts-persistence-v2-design

Conflicted Files

  1. packages/cli/src/serve/routes/session.ts (1 conflict)
  2. packages/core/src/services/sessionService.ts (4 conflicts)

Resolutions

1. session.ts — GET /session/:id/artifacts route

  • PR side: Simple app.get(...) handler calling bridge.getSessionArtifacts(sessionId, { clientId }) with manual requireSessionId / parseClientIdHeader.
  • Main side: Refactored to withOwnerReadSession wrapper (workspace-aware routing), but dropped clientId pass-through.
  • Resolution: Used main's withOwnerReadSession wrapper pattern while restoring the PR's clientId extraction and pass-through to runtime.bridge.getSessionArtifacts(sessionId, { clientId }).

2. sessionService.tsResumedSessionData interface

  • PR side: Added artifactSnapshot?: RebuiltSessionArtifactSnapshot.
  • Main side: Added historyGaps?: HistoryGap[].
  • Resolution: Kept both fields — they are independent additive features.

3. sessionService.tsreconstructHistory internals

  • PR side: Inline UUID chain walk with currentUuid, uuidChain, visited (pre-refactoring code).
  • Main side: Extracted to buildOrderedUuidChain(records, opts) returning { uuids, gaps }.
  • Resolution: Took main's extracted utility. The PR's inline code was the older implementation before main's refactoring.

4. sessionService.tsloadSession return value

  • PR side: Added ...(artifactSnapshot ? { artifactSnapshot } : {}).
  • Main side: Added historyGaps: gaps.length > 0 ? gaps : undefined.
  • Resolution: Kept both — independent additive features. gaps comes from reconstructHistory({ detectGaps: true }), artifactSnapshot from rebuildSessionArtifactSnapshot(...).

5. sessionService.ts — `forkSessio

@chiga0

chiga0 commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 42c19ccfa (fix(daemon): keep artifact side records out of resume leaf) to address the Ubuntu CI build failure after the latest merge-resolution commit.

The fix keeps session artifact side records out of the reconstructed conversation leaf by passing the last non-artifact record as leafUuid, preserving the existing trailing-artifact resume behavior.

Validation:

  • npx prettier --check packages/core/src/services/sessionService.ts
  • git diff --check
  • npm run build --workspace=packages/core
  • cd packages/core && npm run typecheck
  • cd packages/core && npx vitest run src/services/sessionService.test.ts src/services/sessionService.corruption.test.ts src/services/session-artifact-persistence.test.ts

@wenshao

wenshao commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Re-review — session artifact persistence (V2)

Reviewed at a1c04cb3c; re-verified against the current head 42c19ccf (the newer commit is only a merge of main — the artifact-specific files are materially identical, and every point below still applies). This round focused on (a) whether the two data‑loss Criticals from the previous round are actually fixed, and (b) new issues in the reworked restore/rewind/eviction and workspace‑validation paths. The store internals (evictOverflow, tombstone LRU, operationQueue, restore()) and workspace‑path validation were each audited end‑to‑end.

✅ The two prior Criticals are resolved (and tested)

1. Restore no longer misses off‑chain artifact records. Artifact events/snapshots are written with updateActiveTail:false (parented to the active leaf but never advancing it), so a later turn used to make reconstructHistory() walk right past them and rebuildSessionArtifactSnapshot() return undefined. loadSession() now merges those side records back via includeActiveSideArtifactRecords(records, messages) (sessionService.ts) before rebuilding — correct, because the write side guarantees the record's parentUuid is an active‑chain node. Covered by sessionService.test.ts "loads artifact side records attached to the active branch" (a turn following the artifact) and "does not treat trailing artifact side records as the conversation leaf".

2. Rewind no longer wipes live restorable/pinned artifacts. Three independent guards now prevent the wipe: restore() early‑returns on an undefined snapshot (never clears the store); the rewind path skips restore() entirely when the child reports artifactSnapshotUnavailable and instead re‑records the live state (bridge.ts rewindSession); and a partial‑restore failure rolls back to previousState keeping existing live artifacts. Combined with fix #1, rewind now restores the correct rewound artifact state.

Also resolved from earlier rounds: the persistence capability uses conditional omission (session_artifacts_persistence advertised only when sessionArtifactsPersistenceAvailable === true), matching the "tag presence = behavior is on" convention.

packages/core artifact suites pass locally (152 tests, incl. the regression coverage above).


Findings this round

A. The PR description overstates what actually ships (please correct before merge). The body describes "managed content … stored under content‑addressed IDs with manifest validation, sha256 verification, quota enforcement, atomic manifest writes, best‑effort GC … cleanup on session close," and the test plan runs prettier over packages/acp-bridge/src/sessionArtifactContentStore.ts. None of that is present at this head — there is no sessionArtifactContentStore.ts, and sessionArtifacts.ts performs no content writes/manifests/GC (grep for writeFile|mkdir|manifest is empty). The content‑store split appears to have been rebased back out. What actually ships is metadata persistence plus a workspace‑file sha256 hash (for staleness detection, not content storage), and pin/unpin remain unreachable (normalizeRetention rejects pinned; restore downgrades pinned → restorable). Not a code bug, but a reviewer approving on the strength of "manifest validation / quota / GC / verified content" would be approving properties the code doesn't implement. The description (and the stale sessionArtifactContentStore.ts path in the test commands) should be brought in line with the metadata‑only reality.

B. Low — workspace hashing follows hardlinks (no nlink guard). getWorkspaceStatus() in sessionArtifacts.ts validates containment correctly for strings and symlinks — realpath + path.relative/isOutsidePath + O_RDONLY|O_NOFOLLOW + a dev/ino re‑check — and empirically rejects ../…, absolute paths, and symlinks to outside the workspace. But a hardlink inside the workspace pointing at an out‑of‑workspace inode has a genuinely in‑workspace realpath, so it passes, and the daemon hashes the outside file: a planted ws/hardlink.txt → /outside/secret records that secret's exact sha256/sizeBytes/mtimeMs into artifact metadata (reproduced). Impact is bounded at this head (only a hash/size/mtime leaks — no content is stored, and it needs local FS write into the workspace), hence Low, but it diverges from the repo's established "reject nlink > 1" pattern for file‑read→off‑machine features (cf. #5817 / #5780). A one‑line stat.nlink > 1 → escaped guard closes it.

C. Compatibility — POST/DELETE /session/:id/artifacts now hard‑require a client id. Both routes gained requireSessionArtifactClientId(...)403 client_id_required when x-qwen-client-id is absent (previously the id was optional). This is reasonable ownership hardening, but it contradicts the body's "V1 live‑session artifact behavior remains compatible" — any existing client that added/removed artifacts without the header now breaks. Please confirm it's intended and call it out as a breaking change in the migration notes.

D. Minor.

  • capabilities.ts: session_artifacts_persistence is registered { since: 'v1' } though it's new here. Harmless because it's presence‑gated, but the since is inaccurate.
  • restore() seeds this.tombstonedIds from snapshot.tombstonedIds without the .slice(-MAX_TOMBSTONED_IDS) bound that stickyEphemeralIds gets, so the set can transiently exceed the cap until the next rememberTombstone() trims it. Harmless (over‑cap tombstones map to already‑deleted ids), just inconsistent with the sibling handling.

Note on CI

The latest completed CI run is red on sessionService.ts … error TS6133: 'lastConversationRecordUuid' is declared but its value is never read — but that ran on an earlier merge commit. At this head the method is used (reconstructHistory calls this.lastConversationRecordUuid(records), added in 812c0634e) and tsc --build --force passes clean locally. The red ✗ is stale; a re‑trigger on the current head should go green.

Overall

No new Critical/High correctness issues — the reworked restore/rewind/eviction/tombstone paths and the workspace‑path validation all held up under a targeted adversarial audit, and the two prior data‑loss Criticals are genuinely fixed and covered by tests. The main actionable item is (A): align the PR description with the metadata‑only scope so the review is being done against what actually ships. (B) and (C) are worth a quick follow‑up.

中文版本

复审 — session artifact persistence (V2)

a1c04cb3c 上复审;并对当前 head 42c19ccf 重新校验(较新的提交只是 merge 了 main——artifact 相关文件实质相同,下述每一点仍然适用)。本轮聚焦两件事:(a) 上一轮两个数据丢失级 Critical 是否真的修复;(b) 重写后的 restore/rewind/eviction 与 workspace 校验路径是否引入新问题。store 内部(evictOverflow、tombstone LRU、operationQueuerestore())以及 workspace 路径校验都做了端到端审计。

✅ 上一轮两个 Critical 已修复(且有测试覆盖)

1. restore 不再漏掉 off‑chain artifact 记录。 artifact event/snapshot 以 updateActiveTail:false 写入(parent 指向 active leaf 但不推进它),因此之前一旦后面再来一个 turn,reconstructHistory() 会跳过它们、rebuildSessionArtifactSnapshot() 返回 undefined。现在 loadSession() 在 rebuild 前通过 includeActiveSideArtifactRecords(records, messages)sessionService.ts)把这些 side record 合并回来——这是正确的,因为写入侧保证了该记录的 parentUuid 一定是 active 链上的节点。测试覆盖:sessionService.test.ts"loads artifact side records attached to the active branch"(artifact 后面还跟着一个 turn,正是我上一轮报的回归场景)与 "does not treat trailing artifact side records as the conversation leaf"

2. rewind 不再抹掉存活的 restorable/pinned artifact。 现在有三重独立保护:restore() 在 snapshot 为 undefined 时提前返回(绝不清空 store);当 child 报告 artifactSnapshotUnavailable 时,rewind 路径完全跳过 restore(),改为重新记录当前存活状态(bridge.tsrewindSession);部分 restore 失败时回滚到 previousState、保留现有存活 artifact。结合修复 #1,rewind 现在能恢复到正确的 rewound artifact 状态。

前几轮的其他问题也已解决:persistence capability 采用条件省略(只有 sessionArtifactsPersistenceAvailable === true 时才广告 session_artifacts_persistence),符合"有 tag 即功能开启"的约定。

packages/core 的 artifact 测试本地全绿(152 个,含上述回归覆盖)。


本轮发现

A. PR 描述夸大了实际交付内容(合并前请修正)。 描述里写了"managed content … stored under content‑addressed IDs with manifest validation, sha256 verification, quota enforcement, atomic manifest writes, best‑effort GC … cleanup on session close",测试计划还对 packages/acp-bridge/src/sessionArtifactContentStore.ts 跑了 prettier。这些在当前 head 都不存在——没有 sessionArtifactContentStore.tssessionArtifacts.ts没有任何 content 写入/manifest/GC(grep writeFile|mkdir|manifest 为空)。content‑store 拆分似乎又被 rebase 掉了。实际交付的是 metadata 持久化 + 一个 workspace 文件的 sha256 hash(用于判断内容是否过期,而非存储内容),且 pin/unpin 仍不可达(normalizeRetention 拒绝 pinned;restore 会把 pinned → restorable 降级)。这不是代码 bug,但如果 reviewer 基于"manifest validation / quota / GC / verified content"去批准,等于批准了代码并未实现的属性。请把描述(以及测试命令里那个已不存在的 sessionArtifactContentStore.ts 路径)改成与"只持久化 metadata"的实际情况一致。

B. Low — workspace hashing 会跟随 hardlink(缺少 nlink 防护)。 sessionArtifacts.ts 里的 getWorkspaceStatus() 对字符串与 symlink 的越界校验是正确的——realpath + path.relative/isOutsidePath + O_RDONLY|O_NOFOLLOW + dev/ino 复检——实测能拒绝 ../…、绝对路径、以及指向 workspace 外的 symlink。但 workspace 内一个指向 workspace 外 inode 的 hardlink,其 realpath 确实落在 workspace 内,因此能通过,daemon 于是会 hash 这个外部文件:植入的 ws/hardlink.txt → /outside/secret 会把该 secret 的 sha256/sizeBytes/mtimeMs 记进 artifact metadata(已复现)。当前 head 影响有限(只泄露 hash/size/mtime,不存储内容,且需要本地对 workspace 有写权限),故为 Low,但它偏离了本仓库对"文件读→离机"类功能"拒绝 nlink > 1"的既有约定(参见 #5817 / #5780)。加一行 stat.nlink > 1 → escaped 即可堵住。

C. 兼容性 — POST/DELETE /session/:id/artifacts 现在强制要求 client id。 两个路由都加了 requireSessionArtifactClientId(...) → 缺少 x-qwen-client-id 时返回 403 client_id_required(此前该 id 是可选的)。作为 ownership 加固是合理的,但它与描述里的"V1 live‑session artifact behavior remains compatible"矛盾——任何此前不带该 header 就增删 artifact 的客户端现在会失败。请确认是否有意为之,并在 migration notes 里注明这是 breaking change。

D. 次要。

  • capabilities.tssession_artifacts_persistence 注册为 { since: 'v1' },但它其实是本次新增。因为按"是否出现"判定所以无害,只是 since 不准确。
  • restore()snapshot.tombstonedIds 填充 this.tombstonedIds没有stickyEphemeralIds 那样加 .slice(-MAX_TOMBSTONED_IDS) 上界,因此该集合可能短暂超过上限,直到下一次 rememberTombstone() 修剪。无害(超限的 tombstone 对应的都是已删除的 id),只是与同类处理不一致。

关于 CI

最新一次已完成的 CI 是红的,错误为 sessionService.ts … error TS6133: 'lastConversationRecordUuid' is declared but its value is never read——但那是跑在一个更早的 merge commit 上。当前 head 里该方法是被使用的(reconstructHistory 调用了 this.lastConversationRecordUuid(records),在 812c0634e 加入),本地 tsc --build --force 干净通过。那个红叉是过期的;对当前 head 重新触发 CI 应会变绿。

总体

没有新的 Critical/High 级正确性问题——重写后的 restore/rewind/eviction/tombstone 路径以及 workspace 路径校验在针对性的对抗审计下都站得住,上一轮两个数据丢失级 Critical 也确实修复并有测试覆盖。最需要处理的是 (A):让 PR 描述与"只持久化 metadata"的实际范围一致,以免复审是针对并未交付的内容进行的。(B)(C) 值得快速跟进。

@chiga0

chiga0 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Processed item A from the re-review by updating the PR body to match the current #6259 scope:

  • removed the managed content store / manifest / quota / GC claims
  • removed the stale sessionArtifactContentStore.ts validation path
  • clarified that this PR is metadata-only and content retention remains out of scope
  • added the current x-qwen-client-id requirement to the migration notes

For B/C: the hardlink nlink > 1 guard changes the workspace-file security boundary, and keeping vs relaxing the x-qwen-client-id requirement is an API compatibility decision. I am not changing those automatically in this monitoring pass; they need maintainer/product confirmation before implementation.

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Found one blocker in artifact persistence restore behavior and one focused transport coverage gap.

Comment thread packages/acp-bridge/src/sessionArtifacts.ts Outdated
Comment thread packages/cli/src/serve/acp-http/dispatch.ts
'GET /session/:id/artifacts',
async (_req, res, sessionId, runtime) => {
async (req, res, sessionId, runtime) => {
const clientId = parseClientIdHeader(req, res);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The GET handler accepts clientId: undefined and passes it unfiltered to getSessionArtifacts, while POST and DELETE both enforce requireSessionArtifactClientId (403 without it). In a multi-client session, any session reader can drop the X-Qwen-Client-Id header and enumerate every artifact — including file paths, content hashes, and metadata — from other clients.

Suggested change
const clientId = parseClientIdHeader(req, res);
async (req, res, sessionId, runtime) => {
const clientId = parseClientIdHeader(req, res);
if (clientId === null) return;
if (!requireSessionArtifactClientId(clientId, res)) return;
res

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Triage: needs design/API/security decision. This is a valid privacy concern, but requiring X-Qwen-Client-Id on GET /session/:id/artifacts changes the REST read contract and the visibility model for mixed tool/client artifacts. I am not changing that automatically in #6259 without owner confirmation on whether artifact listing is session-scoped or client-scoped. Leaving unresolved.

.map(cloneStoredArtifact)
: [];
let restoredCount = 0;
this.artifacts.clear();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] restore() unconditionally clears all artifacts before iterating the snapshot. If the snapshot is empty (corrupted/truncated JSONL), all durable artifacts are permanently lost. The rollback guard (snapshot.artifacts.length > 0) is false for empty snapshots, so rollback never fires. There is no log distinguishing "intentional empty restore" from "corrupted journal caused empty restore."

Consider logging when snapshot.artifacts.length === 0 && this.artifacts.size > 0 before clearing, or refusing to clear when the store has artifacts but the snapshot is empty.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Triage: needs recovery-semantics decision. An empty restore snapshot can mean either an intentional empty artifact state after rewind or a corrupt/truncated rebuild result. Refusing to clear live artifacts would preserve stale artifacts for intentional empty snapshots; clearing can lose data for corrupt empty snapshots. This needs an explicit policy before implementation, so I am leaving it unresolved.

: [];
let restoredCount = 0;
this.artifacts.clear();
this.tombstonedIds.clear();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] this.tombstonedIds.clear() followed by repopulation from snapshot.tombstonedIds is a full replacement, not a union. Tombstones from in-memory state that are absent from the snapshot (due to persistence failure, truncated JSONL, or non-strict remove) are silently dropped — allowing deleted artifacts to reappear on next session load.

Consider taking the union of the snapshot's tombstones with the existing set instead of clearing and replacing:

for (const id of snapshot.tombstonedIds) {
  this.tombstonedIds.add(id);
}

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Triage: needs recovery-semantics decision. Unioning live tombstones across restore may prevent intentional rewind/restore from reintroducing artifacts that the selected snapshot legitimately contains, while replacement can drop tombstones after partial/corrupt rebuilds. That tradeoff needs an explicit restore contract, so I am leaving this unresolved.

const changes: SessionArtifactChange[] = [removeChange];
const warnings = await this.persistChanges(changes, false);
delete removeChange.durableTombstoneRequired;
return {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] persistChanges(changes, false) with strict=false means persistence failures are silently downgraded to warnings. The in-memory deletion happens before persistence, so if persistence fails the artifact is gone from memory but the removal event was never written to disk. On next session load, the artifact reappears — silently undoing the user's explicit deletion.

Consider using strict=true for explicit user-initiated removals, or persisting a local tombstone before acknowledging the removal.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Triage: needs persistence-failure semantics decision. The concern is valid, but changing explicit remove from live-first-with-warning to durable-first/rollback changes user-visible deletion and API behavior under disk failure. I am not changing that boundary automatically in this PR; leaving unresolved for owner decision.

Comment thread packages/core/src/services/sessionService.ts
if (!isSessionArtifactRecord(record)) {
continue;
}
if (record.parentUuid !== null && activeUuids.has(record.parentUuid)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] This condition includes artifact records whose parentUuid matches any UUID in the active chain. After a rewind, artifact records from abandoned branches that share a parentUuid with a surviving ancestor will be re-included. A removed event from the abandoned branch could delete an artifact that is still valid in the new branch.

Consider filtering artifact records to exclude those that are ancestors of non-artifact records outside the active chain, or restricting inclusion to artifact records written after the last rewind point.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Triage: needs active-branch/recovery semantics decision. The parentUuid check is indeed insufficient to distinguish side records from abandoned timelines when they share a surviving ancestor, but the fix needs a defined active-timeline rule for artifact side records after rewind/fork, not just a local predicate tweak. Leaving unresolved for maintainer/design decision.

@chiga0

chiga0 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #6557, which carries the same net change as a single squashed commit for easier review.

@chiga0 chiga0 closed this Jul 9, 2026
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.

5 participants