{"record":{"id":"1c1bf116447eab3e","repo":"JuliusBrussee/caveman","slug":"forget-begin-w","errorCode":null,"errorMessage":"forget begin: %w","messagePattern":"forget begin: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mem/store.go","lineNumber":327,"sourceCode":"\t\t\treturn nil, fmt.Errorf(\"broken supersession history at %s: %w\", cursor.ID, err)\n\t\t}\n\t\tif seen[next.ID] {\n\t\t\treturn nil, fmt.Errorf(\"cyclic supersession history at %s\", next.ID)\n\t\t}\n\t\tseen[next.ID] = true\n\t\thistory = append(history, next)\n\t\tcursor = next\n\t}\n\treturn history, nil\n}\n\n// Forget deletes a memory by id and atomically repairs neighboring lineage\n// pointers. Expired predecessors stay expired: deleting a current version means\n// forgetting that fact, never silently resurrecting an older one.\nfunc (s *Store) Forget(id string) (bool, error) {\n\ttx, err := s.db.Begin()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"forget begin: %w\", err)\n\t}\n\tdefer tx.Rollback()\n\n\tvar supersedes, supersededBy sql.NullString\n\tif err := tx.QueryRow(\n\t\t`SELECT supersedes, superseded_by FROM memories WHERE id = ?`, id,\n\t).Scan(&supersedes, &supersededBy); err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, fmt.Errorf(\"read forgotten memory: %w\", err)\n\t}\n\tif supersedes.Valid {\n\t\tif _, err := tx.Exec(\n\t\t\t`UPDATE memories SET superseded_by = ? WHERE id = ?`,\n\t\t\tnullableLineageID(supersededBy), supersedes.String,\n\t\t); err != nil {\n\t\t\treturn false, fmt.Errorf(\"repair forgotten predecessor: %w\", err)","sourceCodeStart":309,"sourceCodeEnd":345,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/mem/store.go#L309-L345","documentation":"Forget() opens a SQLite transaction before reading and repairing lineage pointers. If db.Begin() fails, the deletion never starts and this wrapped error is returned. Begin failures almost always mean the underlying SQLite connection or database file is unusable (locked, closed, or I/O error).","triggerScenarios":"Calling Forget(id) (API, cavemem_forget MCP tool, or `cavemem forget` CLI) while the database file is locked by another long-running writer, the store was already Closed(), or the DB file/directory is unreadable or on a full disk.","commonSituations":"Two cavemem processes (CLI + MCP server) contending on the same database, a hung external sqlite3 shell holding a write lock, read-only filesystem or full disk, or calling Forget after Close.","solutions":["Check for other processes holding the DB: `fuser <db-file>` or `lsof <db-file>`; close or kill stale cavemem/sqlite3 writers and retry.","Verify disk space (`df -h`) and that the file/directory is writable; free space or fix permissions, then retry.","If the store was closed in-process, reopen it with the store constructor before calling Forget.","Enable a busy timeout in the SQLite DSN (e.g. `?_busy_timeout=5000`) so short-lived lock contention resolves instead of failing."],"exampleFix":"// before: default DSN, concurrent writers cause immediate 'database is locked'\ns, err := mem.Open(\"cavemem.db\")\n// after: give SQLite a busy timeout\ns, err := mem.Open(\"file:cavemem.db?_busy_timeout=5000\")","handlingStrategy":"retry","validationCode":"// Go: check the store is usable before calling Forget\nfunc storeWritable(path string) error {\n\tif fi, err := os.Stat(path); err != nil {\n\t\treturn err\n\t} else if fi.Mode()&0200 == 0 {\n\t\treturn fmt.Errorf(\"db file not writable: %s\", path)\n\t}\n\treturn nil\n}","typeGuard":"null","tryCatchPattern":"var ok bool\nvar err error\nfor i := 0; i < 3; i++ {\n\tok, err = store.Forget(id)\n\tif err == nil || !strings.Contains(err.Error(), \"forget begin\") {\n\t\tbreak\n\t}\n\ttime.Sleep(100 * time.Millisecond << i) // wait out transient lock\n}","preventionTips":["Configure a busy timeout in the SQLite DSN (e.g. `?_busy_timeout=5000`) to absorb short lock contention.","Run a single cavemem process (MCP server or CLI, not both writing) per database at a time.","Monitor disk space on the volume hosting the DB.","Never call Forget after Close(); keep store lifetime ownership explicit."],"tags":["sqlite","transaction","database-locked","concurrency"],"backgroundTag":"database-query-failed","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}