-
Notifications
You must be signed in to change notification settings - Fork 173
feat(history): tombstones, deletion, and privacy semantics (slice 5/6) #1393
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
7cc79b2
ea7e33b
041973d
3cc57c7
9fb1bd5
1f91268
c4f20d5
89ac348
146db16
e302337
779bdb7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -193,8 +193,8 @@ export function parseStoreLine(raw: string): StoreEntry | null { | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // =========================================================================== | ||||||||||||||||||||||||||
| // Instance writer (formerly multi-store.ts; scope deletes and GC arrive | ||||||||||||||||||||||||||
| // in later slices) | ||||||||||||||||||||||||||
| // Instance writer (formerly multi-store.ts; GC/compaction arrives in a | ||||||||||||||||||||||||||
| // later slice) | ||||||||||||||||||||||||||
| // =========================================================================== | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** Mutable state of ONE pi instance's exclusive capture file. */ | ||||||||||||||||||||||||||
|
|
@@ -411,6 +411,87 @@ export function drainGlobal( | |||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||
| // Scope delete (design v2) | ||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| interface SweepResult { | ||||||||||||||||||||||||||
| filesAffected: number; | ||||||||||||||||||||||||||
| removed: number; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Remove every line whose prompt identity matches `text` from each file in | ||||||||||||||||||||||||||
| * `files`, one atomic rewrite (tmp + rename) per affected file. Files whose | ||||||||||||||||||||||||||
| * every line matched are kept as empty files (never removed — the instance | ||||||||||||||||||||||||||
| * owning a session file may still append to it). | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| function sweepFiles(files: string[], text: string): SweepResult { | ||||||||||||||||||||||||||
| const key = promptKey(text); | ||||||||||||||||||||||||||
| let filesAffected = 0; | ||||||||||||||||||||||||||
| let removed = 0; | ||||||||||||||||||||||||||
| for (const file of files) { | ||||||||||||||||||||||||||
| let raw = ""; | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| raw = fs.readFileSync(file, "utf8"); | ||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| const kept: string[] = []; | ||||||||||||||||||||||||||
| let fileRemoved = 0; | ||||||||||||||||||||||||||
| for (const lineText of raw.split("\n")) { | ||||||||||||||||||||||||||
| const parsed = parseStoreLine(lineText); | ||||||||||||||||||||||||||
| if (!parsed) continue; | ||||||||||||||||||||||||||
| if (promptKey(parsed.text) === key) { | ||||||||||||||||||||||||||
| fileRemoved += 1; | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| kept.push(JSON.stringify(parsed)); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if (fileRemoved === 0) continue; | ||||||||||||||||||||||||||
| const tmp = `${file}.tmp-${process.pid}-${Date.now()}`; | ||||||||||||||||||||||||||
| fs.writeFileSync(tmp, kept.length > 0 ? kept.join("\n") + "\n" : "", "utf8"); | ||||||||||||||||||||||||||
| fs.renameSync(tmp, file); | ||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Handle rewrite failures in
The failure has three more effects:
Use 🛠️ Proposed fix- const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
- fs.writeFileSync(tmp, kept.length > 0 ? kept.join("\n") + "\n" : "", "utf8");
- fs.renameSync(tmp, file);
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
+ try {
+ fs.writeFileSync(tmp, kept.length > 0 ? kept.join("\n") + "\n" : "", "utf8");
+ fs.renameSync(tmp, file);
+ } catch {
+ try { fs.unlinkSync(tmp); } catch { /* not created */ }
+ failed += 1;
+ continue;
+ }Add 📝 Committable suggestion
Suggested change
🧰 Tools🪛 ast-grep (0.45.3)[warning] 452-452: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use. (detect-non-literal-fs-filename-typescript) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| filesAffected += 1; | ||||||||||||||||||||||||||
| removed += fileRemoved; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return { filesAffected, removed }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** Delete every copy of a prompt from the CURRENT project's scope. */ | ||||||||||||||||||||||||||
| export function deleteFromProject( | ||||||||||||||||||||||||||
| root: string, | ||||||||||||||||||||||||||
| cwd: string, | ||||||||||||||||||||||||||
| text: string, | ||||||||||||||||||||||||||
| ): SweepResult { | ||||||||||||||||||||||||||
| return sweepFiles( | ||||||||||||||||||||||||||
| listProjectFiles(path.join(root, "projects", projectHash(cwd))), | ||||||||||||||||||||||||||
| text, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** Delete every copy of a prompt from the GLOBAL scope (all projects + seed). */ | ||||||||||||||||||||||||||
| export function deleteFromGlobal(root: string, text: string): SweepResult { | ||||||||||||||||||||||||||
| const files: string[] = []; | ||||||||||||||||||||||||||
| const globalSeed = globalSeedPath(root); | ||||||||||||||||||||||||||
| if (fs.existsSync(globalSeed)) files.push(globalSeed); | ||||||||||||||||||||||||||
| let projectDirs: fs.Dirent[]; | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| projectDirs = fs.readdirSync(path.join(root, "projects"), { | ||||||||||||||||||||||||||
| withFileTypes: true, | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||
| projectDirs = []; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| for (const dirEntry of projectDirs) { | ||||||||||||||||||||||||||
| if (!dirEntry.isDirectory()) continue; | ||||||||||||||||||||||||||
| files.push( | ||||||||||||||||||||||||||
| ...listProjectFiles(path.join(root, "projects", dirEntry.name)), | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return sweepFiles(files, text); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||
| // Legacy migration (design v2: one-time, gated) | ||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Prevent a global or project sweep from losing concurrent captures.
sweepFilesreads each<instance>.jsonland writes a filtered copy. It then renames the copy over the original. Other pi instances append to their own files withappendFileSyncat any time.If instance B appends a prompt after instance A reads B's file and before A renames the copy, the rename discards B's line. The test at
tests/history-scope-delete.test.tsLines 148-163 covers only a writer in the same process that appends after the sweep. It does not cover this window.The design comment says instance files have "zero shared writes". Scope delete breaks that invariant. Choose one of these fixes:
🧰 Tools
🪛 ast-grep (0.45.3)
[warning] 435-435: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(file, "utf8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
🤖 Prompt for AI Agents