feat(version-chain): auto-link sessions into version chains#83
Conversation
每次 sth send 现在自动按 (project, entry_file basename, owner) 归入版本链。
agent / skill 零改动:同产物的多次迭代自动串成 v1→v2→v3,浏览器预览页
注入版本时间线侧栏,相邻版本展示元数据 diff (tags 增删、category/project
变更)。HTML 内容 diff 留作下一版独立 capability。
数据层
- document_chains 表:UNIQUE(project, entry_file, user_id) 标识链
- sessions 加 chain_id / version_no 列 (nullable, 向后兼容) + 复合索引
- LinkToChain / GetChainOfSession / DiffChainMetadata (单事务, 单写者安全)
服务端
- GET /api/sessions/{id}/chain 返回 chain + versions + metadataDiff
- handleCreateUploadedSession 在 setSessionMetadata 后自动链化, 失败仅 log
到 stderr 不阻塞上传 (降级为孤立 session)
预览页
- 复用 internal/live.InjectMiddleware 注入 versionDrawer (CSS/HTML/JS)
- 徽标默认隐藏, probe chain 接口仅当 versions.length > 1 时显示版本号
- 时间线纵向展示, 当前版本高亮, 相邻版本下方渲染元数据 diff
- 选择器 #sth-version-* 命名空间 + !important 抵抗用户样式冲突
CLI
- runSend 解析 chainId/versionNo, stdout 仍只打 URL (管道友好), stderr
打印 version: vN of chain <id>
- list/search JSON 自动带 chainId/versionNo 字段
软删兼容
- SoftDelete 现有行为不变, 时间线跳过被软删版本, 版本号不重排
OpenSpec
- openspec/changes/version-chain-sessions/ 全套 (proposal/design/tasks/spec)
- 决策 D14-D18 续接 support-password-login 的 D13
- openspec validate version-chain-sessions 通过
测试: go test ./... 全绿 (新增 14 个), go vet 无警告
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (5)
📝 WalkthroughWalkthroughAdds automatic session version chains keyed by project, entry-file basename, and owner. It introduces chain storage and APIs, metadata diffs, preview timeline UI, CLI chain output, document-list fields, tests, and OpenSpec documentation. ChangesSession version chains
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant Server
participant Store
participant Preview
Client->>Server: Upload session
Server->>Store: Link session to project/file/owner chain
Store-->>Server: chainId and versionNo
Server-->>Client: Session URL and chain metadata
Preview->>Server: GET /api/sessions/{id}/chain
Server->>Store: Load versions and metadata diffs
Store-->>Server: Chain response data
Server-->>Preview: Ordered version timeline
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
openspec/changes/version-chain-sessions/tasks.md (1)
34-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUpdate the checklist to reflect the completed tests and validations.
The PR objectives confirm that
go test,go vet, andopenspec validateare all passing successfully. Consider checking these boxes to keep the specification checklist fully up to date before it is archived.📝 Proposed update
- - [ ] 6.2 `go test ./...` 全绿。 - - [ ] 6.3 `go vet ./...` 无警告。 - - [ ] 6.4 `openspec validate version-chain-sessions` 通过。 + - [x] 6.2 `go test ./...` 全绿。 + - [x] 6.3 `go vet ./...` 无警告。 + - [x] 6.4 `openspec validate version-chain-sessions` 通过。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@openspec/changes/version-chain-sessions/tasks.md` around lines 34 - 36, Update checklist items 6.2, 6.3, and 6.4 in the validation checklist to checked state, reflecting that go test ./..., go vet ./..., and openspec validate version-chain-sessions have completed successfully.internal/session/version.go (1)
198-211: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant fallback loop.
Lines 198-202 already set
Current = trueon the matching session unconditionally, so thecurrentVersion == 0fallback loop at Lines 203-211 can never mark anything the first loop missed. It can be removed.♻️ Proposed cleanup
for i := range versions { if versions[i].SessionID == sessionID { versions[i].Current = true } } - if currentVersion == 0 && len(versions) > 0 { - // Fallback when the column hasn't been populated yet. - for i := range versions { - if versions[i].SessionID == sessionID { - versions[i].Current = true - break - } - } - }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/session/version.go` around lines 198 - 211, Remove the redundant currentVersion == 0 fallback loop after the initial versions iteration. Keep the existing loop that marks the matching session’s Current field true, and leave the surrounding version-selection logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/server/server.go`:
- Around line 1889-1909: The handleGetChain logic must not synthesize a current
version for a soft-deleted requested session. Before the fallback that assigns
versions[len(versions)-1], detect when the requested session is deleted and
absent from the live versions, and return the existing not-found response;
otherwise preserve the fallback only for valid non-deleted requests.
In `@internal/session/version.go`:
- Around line 87-119: Make LinkToChain’s NULL-owner chain creation atomic around
the SELECT-then-INSERT flow, using the transaction’s locking or upsert
mechanism, and re-check version assignment under concurrent anonymous uploads so
one chain is used per project and entry file; update
internal/session/version.go:87-119. In internal/session/metadata.go:61-70,
document that UNIQUE(project, entry_file, user_id) does not enforce NULL-owner
uniqueness and remove the redundant idx_document_chains_lookup index, unless the
revised atomic creation mechanism replaces that constraint’s role.
---
Nitpick comments:
In `@internal/session/version.go`:
- Around line 198-211: Remove the redundant currentVersion == 0 fallback loop
after the initial versions iteration. Keep the existing loop that marks the
matching session’s Current field true, and leave the surrounding
version-selection logic unchanged.
In `@openspec/changes/version-chain-sessions/tasks.md`:
- Around line 34-36: Update checklist items 6.2, 6.3, and 6.4 in the validation
checklist to checked state, reflecting that go test ./..., go vet ./..., and
openspec validate version-chain-sessions have completed successfully.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 288eb228-7800-40e6-b30a-caee121b6a1e
📒 Files selected for processing (13)
internal/cli/cli.gointernal/live/inject.gointernal/server/server.gointernal/server/server_test.gointernal/session/metadata.gointernal/session/store.gointernal/session/version.gointernal/session/version_test.goopenspec/changes/version-chain-sessions/.openspec.yamlopenspec/changes/version-chain-sessions/design.mdopenspec/changes/version-chain-sessions/proposal.mdopenspec/changes/version-chain-sessions/specs/version-chain/spec.mdopenspec/changes/version-chain-sessions/tasks.md
#1 软删 session 的 chain 请求错误兜底 (Major) - GetChainOfSession 读 deleted_at, 软删直接返 ErrSessionNotFound - handleGetChain 去掉 "fabricate current" 兜底, current.SessionID=="" 即 404 - 新增 store 层 + HTTP 层回归测试 #2 LinkToChain anonymous 竞态 (Major) - SQLite UNIQUE(project, entry_file, user_id) 对 NULL owner 不生效 (SQL 标准 NULL ≠ NULL), 原 SELECT-then-INSERT 在并发 anonymous send 下 会拆链 - 改用 BEGIN IMMEDIATE 立即拿 RESERVED 写锁, 串行化整个读-改-写 - 链行创建改 upsert (INSERT ON CONFLICT DO NOTHING) + 再 SELECT, 认证 owner 路径额外有 UNIQUE 兜底 - design.md D17 + Risks 同步更新事务安全说明 #3 冗余 fallback 循环 (Trivial) - 删除 version.go 里 currentVersion==0 的 dead-code 分支, 上一循环已无条件 标记 Current #4 冗余索引 (Trivial) - 删除 idx_document_chains_lookup, UNIQUE 约束已自带 (project, entry_file, user_id) 左前缀索引 - metadata.go 文档化 UNIQUE 对 NULL owner 不生效的限制 #5 tasks 勾选 - 6.2/6.3/6.4 打勾, 新增第 7 节记录本轮 review 修复 测试: go test ./... 全绿 (新增 2 个回归测试), go vet 无警告, openspec validate 通过
What
每次
sth send自动按(project, entry_file basename, owner)归入版本链。同产物的多次迭代自动串成 v1→v2→v3,浏览器预览页注入版本时间线侧栏,相邻版本展示元数据 diff(tags 增删、category/project 变更)。agent / skill 零改动——现有
sth send工作流逐字段兼容。OpenSpec change:
version-chain-sessions(openspec validate通过,合并后执行openspec archive version-chain-sessions)。Why
sth的核心场景是 agent 反复迭代同一份 HTML 产物。但当前每次 send 都是孤立 session,首页平铺一堆同名条目,无法回答"上次这个 dashboard 改到哪了"、"这一版相比上一版改了什么"。agent 自己也不会管理版本关系,需要在零改动前提下把隐式的"同产物迭代"显式化。详细动机与决策见
openspec/changes/version-chain-sessions/{proposal,design}.md。How it works
document_chains表按(project, entry_file, user_id)唯一标识链;sessions加chain_id/version_no列(nullable,老数据向后兼容)。handleCreateUploadedSession在写元数据后调用LinkToChain,单事务内SELECT MAX(version_no)+1或新建链。失败仅 log 到 stderr,不阻塞上传。GET /api/sessions/{id}/chain返回 chain 元信息、版本列表(v1→vN 升序、当前高亮)、相邻版本元数据 diff。internal/live.InjectMiddleware注入版本时间线 drawer。徽标默认隐藏,probe chain 接口仅当链长 > 1 时显示版本号;点击懒加载、缓存复用。sth sendstderr 打印version: vN of chain <id>;list/searchJSON 自动带chainId/versionNo。Backward compatibility
--auth关闭行为不变;老 sessionchain_id/version_no为 NULL,不迁移不阻塞users/api_keys/user_credentials/login_sessionsschemaTest
go test ./...全绿(新增 14 个测试:internal/session/version_test.go覆盖链化/续链/owner 隔离/软删跳过/diff;internal/server/server_test.go增测 chain endpoint happy path / 单版本 / 404)go vet ./...无警告sth-version-btn/sth-version-panelNon-Goals(留作后续)
--parent <id>显式覆盖自动链化OpenSpec tasks progress
所有任务在 `openspec/changes/version-chain-sessions/tasks.md` 中跟踪,合并后 archive。
Summary by CodeRabbit