fix(docrevision): keep PG snapshot IDs ahead of in-memory counters

Opening a template stored database IDs without advancing nextSnapID, so the next temporary snapshot could overwrite and then delete the real baseline. Adopt helpers now bump every counter and only drop a map entry when it still points at the same object.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-13 23:43:56 +00:00
co-authored by Cursor
parent fa6a033c95
commit 2d0fcb94bc
6 changed files with 394 additions and 86 deletions
+103
View File
@@ -0,0 +1,103 @@
package docrevision
// advanceID 把进程内分配计数推进到不少于已落库 ID,避免下一临时 ID 撞上真实对象。
func advanceID(counter *int64, id int64) {
if counter == nil || id <= 0 {
return
}
if id > *counter {
*counter = id
}
}
// nextFreeID 分配一个当前 map 中未被占用的临时 ID。
func nextFreeID(counter *int64, occupied func(int64) bool) int64 {
if counter == nil {
return 0
}
for {
*counter++
id := *counter
if occupied == nil || !occupied(id) {
return id
}
}
}
func (s *Service) adoptSnapshotID(snap *Snapshot, dbID int64) {
if s == nil || snap == nil || dbID <= 0 {
return
}
oldID := snap.ID
if oldID != 0 && oldID != dbID {
if existing := s.snapshots[oldID]; existing == snap {
delete(s.snapshots, oldID)
}
}
snap.ID = dbID
s.snapshots[dbID] = snap
advanceID(&s.nextSnapID, dbID)
}
func (s *Service) adoptVersionID(ver *Version, dbID int64) {
if s == nil || ver == nil || dbID <= 0 {
return
}
oldID := ver.ID
if oldID != 0 && oldID != dbID {
if existing := s.versions[oldID]; existing == ver {
delete(s.versions, oldID)
}
}
ver.ID = dbID
s.versions[dbID] = ver
advanceID(&s.nextVerID, dbID)
}
func (s *Service) adoptEventID(ev *ChangeEvent, dbID int64) {
if s == nil || ev == nil || dbID <= 0 {
return
}
oldID := ev.ID
if oldID != 0 && oldID != dbID {
if existing := s.events[oldID]; existing == ev {
delete(s.events, oldID)
}
}
ev.ID = dbID
s.events[dbID] = ev
if ev.IdempotencyKey != "" {
s.idemp[ev.IdempotencyKey] = dbID
}
advanceID(&s.nextEvtID, dbID)
}
func (s *Service) adoptReportID(rep *ChangeReport, dbID int64) {
if s == nil || rep == nil || dbID <= 0 {
return
}
oldID := rep.ID
if oldID != 0 && oldID != dbID {
if existing := s.reports[oldID]; existing == rep {
delete(s.reports, oldID)
}
}
rep.ID = dbID
s.reports[dbID] = rep
advanceID(&s.nextRepID, dbID)
}
func (s *Service) adoptCandidateID(c *AgentCandidate, dbID int64) {
if s == nil || c == nil || dbID <= 0 {
return
}
oldID := c.ID
if oldID != 0 && oldID != dbID {
if existing := s.candidates[oldID]; existing == c {
delete(s.candidates, oldID)
}
}
c.ID = dbID
s.candidates[dbID] = c
advanceID(&s.nextCandID, dbID)
}
+70
View File
@@ -0,0 +1,70 @@
package docrevision
import (
"testing"
"time"
)
func TestNextFreeID_SkipsOccupied(t *testing.T) {
var counter int64 = 248
occupied := map[int64]bool{249: true}
got := nextFreeID(&counter, func(id int64) bool { return occupied[id] })
if got != 250 {
t.Fatalf("next free id=%d want 250", got)
}
}
func TestAdoptSnapshotID_DoesNotDeleteOtherObject(t *testing.T) {
s := NewService(nil)
baseline := &Snapshot{ID: 249, DocID: "doc_a", ContentSHA256: "aa", CreatedAt: time.Now().UTC()}
temp := &Snapshot{ID: 249, DocID: "doc_b", ContentSHA256: "bb", CreatedAt: time.Now().UTC()}
s.snapshots[249] = baseline
s.nextSnapID = 248
s.adoptSnapshotID(temp, 250)
if s.snapshots[249] != baseline {
t.Fatal("adopting a different object must not delete the real baseline")
}
if s.snapshots[250] != temp || temp.ID != 250 {
t.Fatalf("temp must live at 250, id=%d", temp.ID)
}
if s.nextSnapID != 250 {
t.Fatalf("counter=%d want 250", s.nextSnapID)
}
}
func TestAdoptSnapshotID_RemovesOnlySameObject(t *testing.T) {
s := NewService(nil)
snap := &Snapshot{ID: 1, DocID: "doc_a", ContentSHA256: "aa"}
s.snapshots[1] = snap
s.adoptSnapshotID(snap, 249)
if _, ok := s.snapshots[1]; ok {
t.Fatal("same-object remap must drop the old temp key")
}
if s.snapshots[249] != snap || snap.ID != 249 {
t.Fatal("same object must occupy the database id")
}
if s.nextSnapID < 249 {
t.Fatalf("counter must advance to db id, got %d", s.nextSnapID)
}
}
func TestPutSnapshotLocked_DoesNotClobberExistingBaseline(t *testing.T) {
s := NewService(nil)
s.mu.Lock()
defer s.mu.Unlock()
baseline := &Snapshot{ID: 249, DocID: "doc_seed", ContentSHA256: "base"}
s.snapshots[249] = baseline
s.nextSnapID = 248
s.blobs["out"] = &ContentBlob{ContentSHA256: "out", ContentMarkdown: "# out\n"}
got := s.putSnapshotLocked("doc_new", "# out\n", "out", nil, SourceAgent, nil, "run", ActorSnapshot{}, "run", nil)
if got.ID == 249 {
t.Fatal("temporary snapshot must not reuse the occupied baseline id")
}
if s.snapshots[249] != baseline {
t.Fatal("baseline snapshot must remain")
}
if s.snapshots[got.ID] != got {
t.Fatal("new snapshot must be stored under its own id")
}
}
+198
View File
@@ -0,0 +1,198 @@
package docrevision_test
import (
"context"
"fmt"
"testing"
"time"
"chat-agent-system/internal/db"
"chat-agent-system/internal/docrevision"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestMaterializeAgentOutput_HighSequenceFirstGenKeepsBaseline_PG(t *testing.T) {
if testing.Short() {
t.Skip("skipping PostgreSQL id-remap integration in -short")
}
dsn := db.SkipIfNoTestDatabaseURL(t)
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Skipf("postgres unavailable: %v", err)
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
t.Skipf("postgres ping failed: %v", err)
}
if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil {
t.Fatalf("migration failed: %v", err)
}
var n int
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM information_schema.tables WHERE table_name='document_snapshots'`).Scan(&n); err != nil || n == 0 {
t.Skip("document_snapshots table missing")
}
suffix := time.Now().UnixNano()
seedDoc := fmt.Sprintf("doc_idremap_seed_%d", suffix)
genDoc := fmt.Sprintf("doc_idremap_gen_%d", suffix)
t.Cleanup(func() {
cleanupRevisionDocs(context.Background(), pool, seedDoc, genDoc)
})
cleanupRevisionDocs(ctx, pool, seedDoc, genDoc)
seedSnap := nextExplicitID(t, ctx, pool, "document_snapshots")
seedVer := nextExplicitID(t, ctx, pool, "document_versions")
seedEvt := nextExplicitID(t, ctx, pool, "document_change_events")
seedHighIDs(t, ctx, pool, seedDoc, seedSnap, seedVer, seedEvt)
svc := docrevision.NewService(pool)
seedState, err := svc.GetRevisionState(ctx, seedDoc)
if err != nil {
t.Fatalf("hydrate seed: %v", err)
}
if seedState.BaselineSnapshotID != seedSnap {
t.Fatalf("seed baseline=%d want %d", seedState.BaselineSnapshotID, seedSnap)
}
outMD := "# 首轮交付\n\n## 范围\n\nPG 序列碰撞回归正文\n"
res, err := svc.MaterializeAgentOutput(ctx, docrevision.AgentMaterializeRequest{
DocID: genDoc, RunID: fmt.Sprintf("run-idremap-%d", suffix), OutputMarkdown: outMD,
ProjectID: 1, WorkDir: "ws/idremap",
ActorKind: docrevision.ActorAgentSquad,
ActorSnapshot: docrevision.ActorSnapshot{SquadName: "bizdev-squad", Model: "test"},
AutoCommitUserID: 42,
})
if err != nil {
t.Fatalf("materialize first gen: %v", err)
}
if res.State == nil || res.State.CurrentVersionNo != 2 {
t.Fatalf("want V2 after auto-commit, got %+v", res.State)
}
if res.Candidate == nil || res.Candidate.Status != docrevision.CandidateCommitted {
t.Fatalf("candidate must be committed, got %+v", res.Candidate)
}
if res.State.BaselineSnapshotID == 0 {
t.Fatal("baseline snapshot id must remain")
}
versions, err := svc.ListVersions(ctx, genDoc)
if err != nil {
t.Fatal(err)
}
var v1, v2 *docrevision.Version
for i := range versions {
switch versions[i].VersionNo {
case 1:
v1 = &versions[i]
case 2:
v2 = &versions[i]
}
}
if v1 == nil || v2 == nil {
t.Fatalf("want V1+V2, got %+v", versions)
}
if v1.BaselineSnapshotID != seedSnap+1 {
t.Fatalf("first-gen template snapshot want %d (next after seed %d), got %d", seedSnap+1, seedSnap, v1.BaselineSnapshotID)
}
if v2.BaselineSnapshotID == v1.BaselineSnapshotID || v2.HeadSnapshotID == v1.BaselineSnapshotID {
t.Fatalf("V2 must not reuse/drop V1 baseline: v1=%+v v2=%+v", v1, v2)
}
if res.Candidate.InputSnapshotID != v1.BaselineSnapshotID {
t.Fatalf("candidate input=%d want V1 baseline %d", res.Candidate.InputSnapshotID, v1.BaselineSnapshotID)
}
seedAfter, err := svc.GetRevisionState(ctx, seedDoc)
if err != nil {
t.Fatal(err)
}
if seedAfter.BaselineSnapshotID != seedSnap || seedAfter.HeadSnapshotID != seedSnap {
t.Fatalf("seed doc must keep snapshot %d, got %+v", seedSnap, seedAfter)
}
restart := docrevision.NewService(pool)
st2, err := restart.GetRevisionState(ctx, genDoc)
if err != nil {
t.Fatalf("hydrate gen after new Service: %v", err)
}
if st2.CurrentVersionNo != 2 || st2.HeadSnapshotID == 0 {
t.Fatalf("restart hydrate want V2/head, got %+v", st2)
}
cand, err := restart.GetCandidate(ctx, genDoc, res.Candidate.ID)
if err != nil || cand == nil || cand.Status != docrevision.CandidateCommitted {
t.Fatalf("restart candidate: %+v err=%v", cand, err)
}
if st2.BaselineSnapshotID == 0 {
t.Fatal("restart must keep baseline")
}
seedRestart, err := restart.GetRevisionState(ctx, seedDoc)
if err != nil {
t.Fatal(err)
}
if seedRestart.BaselineSnapshotID != seedSnap {
t.Fatalf("restart seed baseline=%d", seedRestart.BaselineSnapshotID)
}
}
func nextExplicitID(t *testing.T, ctx context.Context, pool *pgxpool.Pool, table string) int64 {
t.Helper()
var maxID int64
q := fmt.Sprintf(`SELECT COALESCE(MAX(id), 0) FROM %s`, table)
if err := pool.QueryRow(ctx, q).Scan(&maxID); err != nil {
t.Fatalf("max id %s: %v", table, err)
}
return maxID + 50
}
func seedHighIDs(t *testing.T, ctx context.Context, pool *pgxpool.Pool, docID string, snapID, verID, evtID int64) {
t.Helper()
sha := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
if _, err := pool.Exec(ctx, `
INSERT INTO document_content_blobs (content_sha256, content_markdown, byte_size)
VALUES ($1, $2, 8)
ON CONFLICT (content_sha256) DO NOTHING
`, sha, "# seed\n"); err != nil {
t.Fatalf("seed blob: %v", err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO document_snapshots (id, doc_id, content_sha256, source_kind)
VALUES ($1, $2, $3, 'template')
`, snapID, docID, sha); err != nil {
t.Fatalf("seed snapshot: %v", err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO document_versions (id, doc_id, version_no, status, origin_kind, baseline_snapshot_id, head_snapshot_id)
VALUES ($1, $2, 1, 'open', 'template', $3, $3)
`, verID, docID, snapID); err != nil {
t.Fatalf("seed version: %v", err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO document_change_events (
id, doc_id, version_id, event_no, to_snapshot_id, change_kind, actor_kind, idempotency_key
) VALUES ($1, $2, $3, 1, $4, 'import', 'template', $5)
`, evtID, docID, verID, snapID, "seed:"+docID); err != nil {
t.Fatalf("seed event: %v", err)
}
if _, err := pool.Exec(ctx, `SELECT setval('document_snapshots_id_seq', $1, true)`, snapID); err != nil {
t.Fatalf("setval snapshots: %v", err)
}
if _, err := pool.Exec(ctx, `SELECT setval('document_versions_id_seq', $1, true)`, verID); err != nil {
t.Fatalf("setval versions: %v", err)
}
if _, err := pool.Exec(ctx, `SELECT setval('document_change_events_id_seq', $1, true)`, evtID); err != nil {
t.Fatalf("setval events: %v", err)
}
}
func cleanupRevisionDocs(ctx context.Context, pool *pgxpool.Pool, docIDs ...string) {
for _, docID := range docIDs {
_, _ = pool.Exec(ctx, `DELETE FROM document_agent_candidates WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `UPDATE document_versions SET change_report_id=NULL WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_change_reports WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_change_events WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_versions WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_snapshots WHERE doc_id=$1`, docID)
}
}
+14 -61
View File
@@ -26,8 +26,7 @@ func (s *Service) openTemplatePGLocked(ctx context.Context, docID, canon, sha st
if err != nil {
return err
}
sn.ID = id
s.snapshots[id] = sn
s.adoptSnapshotID(sn, id)
ver := &Version{
DocID: docID, VersionNo: 1, Status: VersionOpen, OriginKind: OriginTemplate,
@@ -37,8 +36,7 @@ func (s *Service) openTemplatePGLocked(ctx context.Context, docID, canon, sha st
if err != nil {
return err
}
ver.ID = vid
s.versions[vid] = ver
s.adoptVersionID(ver, vid)
ev := &ChangeEvent{
DocID: docID, VersionID: vid, EventNo: 1, ToSnapshotID: id,
@@ -50,9 +48,7 @@ func (s *Service) openTemplatePGLocked(ctx context.Context, docID, canon, sha st
if err != nil {
return err
}
ev.ID = eid
s.events[eid] = ev
s.idemp[ev.IdempotencyKey] = eid
s.adoptEventID(ev, eid)
root := &DocRootPointers{
DocID: docID, Kind: "controlled", ProjectID: projectID, WorkDir: workDir,
@@ -84,30 +80,20 @@ func (s *Service) persistHumanRevisionLocked(ctx context.Context, root *DocRootP
if err := s.ensureBlobTX(ctx, tx, blob); err != nil {
return err
}
oldSnapID := snap.ID
id, err := s.insertSnapshotTX(ctx, tx, snap)
if err != nil {
return err
}
if oldSnapID != 0 && oldSnapID != id {
delete(s.snapshots, oldSnapID)
}
snap.ID = id
s.snapshots[id] = snap
s.adoptSnapshotID(snap, id)
if isBaseline {
ver.BaselineSnapshotID = id
ver.HeadSnapshotID = id
oldVerID := ver.ID
vid, err := s.insertVersionTX(ctx, tx, ver)
if err != nil {
return err
}
if oldVerID != 0 && oldVerID != vid {
delete(s.versions, oldVerID)
}
ver.ID = vid
s.versions[vid] = ver
s.adoptVersionID(ver, vid)
root.LedgerVersionID = &vid
root.HeadSnapshotID = &id
ev.VersionID = vid
@@ -122,19 +108,11 @@ func (s *Service) persistHumanRevisionLocked(ctx context.Context, root *DocRootP
ev.VersionID = ver.ID
}
oldEv := ev.ID
eid, err := s.insertEventTX(ctx, tx, ev)
if err != nil {
return err
}
if oldEv != 0 && oldEv != eid {
delete(s.events, oldEv)
}
ev.ID = eid
s.events[eid] = ev
if ev.IdempotencyKey != "" {
s.idemp[ev.IdempotencyKey] = eid
}
s.adoptEventID(ev, eid)
return s.projectRootPointersTX(ctx, tx, root)
})
}
@@ -151,41 +129,26 @@ func (s *Service) persistAgentCandidateLocked(ctx context.Context, outSnap *Snap
if err := s.ensureBlobTX(ctx, tx, blob); err != nil {
return err
}
oldSnap := outSnap.ID
sid, err := s.insertSnapshotTX(ctx, tx, outSnap)
if err != nil {
return err
}
if oldSnap != 0 && oldSnap != sid {
delete(s.snapshots, oldSnap)
}
outSnap.ID = sid
s.snapshots[sid] = outSnap
c.OutputSnapshotID = &sid
s.adoptSnapshotID(outSnap, sid)
c.OutputSnapshotID = &outSnap.ID
rep.ToSnapshotID = sid
oldRep := rep.ID
rid, err := s.insertReportTX(ctx, tx, rep)
if err != nil {
return err
}
if oldRep != 0 && oldRep != rid {
delete(s.reports, oldRep)
}
rep.ID = rid
s.reports[rid] = rep
c.ChangeReportID = &rid
s.adoptReportID(rep, rid)
c.ChangeReportID = &rep.ID
oldC := c.ID
cid, err := s.insertCandidateTX(ctx, tx, c)
if err != nil {
return err
}
if oldC != 0 && oldC != cid {
delete(s.candidates, oldC)
}
c.ID = cid
s.candidates[cid] = c
s.adoptCandidateID(c, cid)
return nil
})
}
@@ -201,32 +164,22 @@ func (s *Service) persistAgentCommitLocked(ctx context.Context, root *DocRootPoi
if err := s.updateVersionTX(ctx, tx, oldVer); err != nil {
return err
}
oldVerID := newVer.ID
vid, err := s.insertVersionTX(ctx, tx, newVer)
if err != nil {
return err
}
if oldVerID != 0 && oldVerID != vid {
delete(s.versions, oldVerID)
}
newVer.ID = vid
s.versions[vid] = newVer
root.LedgerVersionID = &vid
s.adoptVersionID(newVer, vid)
root.LedgerVersionID = &newVer.ID
hid := outSnap.ID
root.HeadSnapshotID = &hid
ev.VersionID = vid
ev.ToSnapshotID = outSnap.ID
oldEv := ev.ID
eid, err := s.insertEventTX(ctx, tx, ev)
if err != nil {
return err
}
if oldEv != 0 {
delete(s.events, oldEv)
}
ev.ID = eid
s.events[eid] = ev
s.adoptEventID(ev, eid)
if err := s.updateCandidateTX(ctx, tx, c); err != nil {
return err
-12
View File
@@ -457,15 +457,3 @@ func (s *Service) withTx(ctx context.Context, fn func(pgx.Tx) error) error {
// Pool exposes the underlying pool for integration tests.
func (s *Service) Pool() *pgxpool.Pool { return s.pool }
// remapSnapshotID updates parent pointers after a snapshot receives its real PG id.
func (s *Service) remapSnapshotID(oldID, newID int64) {
if oldID == 0 || oldID == newID {
return
}
if sn := s.snapshots[oldID]; sn != nil {
delete(s.snapshots, oldID)
sn.ID = newID
s.snapshots[newID] = sn
}
}
+9 -13
View File
@@ -499,17 +499,15 @@ func (s *Service) CreateAgentCandidate(ctx context.Context, req AgentCandidateRe
if *root.HeadSnapshotID != req.InputSnapshotID {
status = CandidateStale
}
s.nextRepID++
rep.ID = s.nextRepID
rep.ID = nextFreeID(&s.nextRepID, func(id int64) bool { return s.reports[id] != nil })
rep.DocID = req.DocID
rep.FromSnapshotID = req.InputSnapshotID
rep.ToSnapshotID = outSnap.ID
rep.CreatedAt = time.Now().UTC()
s.reports[rep.ID] = rep
s.nextCandID++
c := &AgentCandidate{
ID: s.nextCandID, DocID: req.DocID, InputSnapshotID: req.InputSnapshotID,
ID: nextFreeID(&s.nextCandID, func(id int64) bool { return s.candidates[id] != nil }), DocID: req.DocID, InputSnapshotID: req.InputSnapshotID,
OutputSnapshotID: &outSnap.ID, RunID: req.RunID, ProposalID: req.ProposalID,
Status: status, AuthorizedScope: req.AuthorizedScope, ChangeReportID: &rep.ID,
ActorKind: req.ActorKind, ActorSnapshot: req.ActorSnapshot, CreatedAt: time.Now().UTC(),
@@ -621,9 +619,11 @@ func (s *Service) CommitAgentVersion(ctx context.Context, docID string, candidat
c.DecidedAt = &now
if s.usingPG() {
if err := s.withTx(ctx, func(tx pgx.Tx) error {
if _, err := s.insertEventTX(ctx, tx, ev); err != nil {
eid, err := s.insertEventTX(ctx, tx, ev)
if err != nil {
return err
}
s.adoptEventID(ev, eid)
return s.updateCandidateTX(ctx, tx, c)
}); err != nil {
s.forgetDocLocked(docID)
@@ -860,8 +860,7 @@ func (s *Service) ImportFreeformLegacy(ctx context.Context, docID string, ownerI
ID: 0, DocID: docID, VersionNo: startNo, Status: VersionOpen, OriginKind: OriginMigration,
BaselineSnapshotID: snap.ID, HeadSnapshotID: snap.ID, OpenedAt: time.Now().UTC(),
}
s.nextVerID++
ver.ID = s.nextVerID
ver.ID = nextFreeID(&s.nextVerID, func(id int64) bool { return s.versions[id] != nil })
s.versions[ver.ID] = ver
root := &DocRootPointers{
DocID: docID, Kind: "freeform", ProjectID: projectID,
@@ -939,9 +938,8 @@ func (s *Service) putSnapshotLocked(docID, canon, sha string, parent *int64, sou
} else {
blocks = BuildBlockMap(canon, clientBlocks)
}
s.nextSnapID++
snap := &Snapshot{
ID: s.nextSnapID, DocID: docID, ContentSHA256: sha, ParentSnapshotID: parent,
ID: nextFreeID(&s.nextSnapID, func(id int64) bool { return s.snapshots[id] != nil }), DocID: docID, ContentSHA256: sha, ParentSnapshotID: parent,
BlockMap: blocks, SourceKind: source, ActorUserID: userID, ActorRef: actorRef,
ActorSnapshot: actor, SourceRunID: runID, CreatedAt: time.Now().UTC(),
}
@@ -961,9 +959,8 @@ func (s *Service) openVersionLocked(docID, origin string, baselineID, headID int
v.SealedAt = &now
}
}
s.nextVerID++
ver := &Version{
ID: s.nextVerID, DocID: docID, VersionNo: maxNo + 1, Status: VersionOpen,
ID: nextFreeID(&s.nextVerID, func(id int64) bool { return s.versions[id] != nil }), DocID: docID, VersionNo: maxNo + 1, Status: VersionOpen,
OriginKind: origin, BaselineSnapshotID: baselineID, HeadSnapshotID: headID,
SourceRunID: runID, OpenedAt: time.Now().UTC(),
}
@@ -972,9 +969,8 @@ func (s *Service) openVersionLocked(docID, origin string, baselineID, headID int
}
func (s *Service) putEventLocked(docID string, versionID int64, eventNo int, from *int64, to int64, kind, actorKind string, userID *int64, actorRef string, actor ActorSnapshot, hunks []DiffHunk, editSession, requestID, idemp string) *ChangeEvent {
s.nextEvtID++
ev := &ChangeEvent{
ID: s.nextEvtID, DocID: docID, VersionID: versionID, EventNo: eventNo,
ID: nextFreeID(&s.nextEvtID, func(id int64) bool { return s.events[id] != nil }), DocID: docID, VersionID: versionID, EventNo: eventNo,
FromSnapshotID: from, ToSnapshotID: to, ChangeKind: kind, ActorKind: actorKind,
ActorUserID: userID, ActorRef: actorRef, ActorSnapshot: actor, ChangeSet: hunks,
EditSessionID: editSession, RequestID: requestID, IdempotencyKey: idemp, CreatedAt: time.Now().UTC(),