Repository navigation
fix(task-persistence): delete under the canonical lock key (U9, #1375) #1917
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
Open
easonLiangWorldedtech
wants to merge
106
commits into
Zoo-Code-Org:main
Choose a base branch
from
easonLiangWorldedtech:fws/u9-task-history-delete
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
106 commits
Select commit
Hold shift + click to select a range
a0878e4
split unit U1 of PR 1833 (issue 1375)
404b8df
fix(file-safety): close the pre-merge findings on the publish primiti…
c1f7fc7
fix(file-safety): propagate a non-ENOENT lstat failure in resolvePubl…
644581d
fix(file-safety): keep the rollback pair typed and the mock stand-ins…
c0b563b
rebuild unit u2 on the fixed chain
6ee9aa3
chore(lint): prune the safeWriteJson suppression this unit earns
c03b9f4
rebuild unit u3 on the fixed chain
6409f44
fix(task): declare the observation registry on Task in this unit
da5acb3
rebuild unit u4 on the fixed chain
c685a51
rebuild unit u5 on the fixed chain
7476cc0
rebuild unit U8 on U5 (corrected merge order)
c8e69f2
rebuild unit u6 in the corrected order
646bb37
fix(apply-patch): a full hunk read is a complete observation
87887e1
rebuild unit u7 on the corrected u6
cfac653
rebuild unit U7 on the corrected U6
261f6fa
fix(task): keep main's abort re-check in ask
ebcb518
rebuild unit U9 on the corrected U7
a59746f
test(file-safety): check the paths the cleanup assertions actually name
1a31785
fix(file-safety): inherit unit 1 committed guard and unit 3 test wording
5365369
fix(file-safety): keep the publish error message in RollbackFailureError
d4d81ae
test(file-safety): assert the exact staging directory removed after a…
5cf3b3e
test: re-trigger required checks - the queued runs were cancelled by …
1639167
fix(file-safety): give the Windows DACL dump a per-write name
40b2126
test(tools): assert the completeness argument the tool passes, not th…
9a5c6bb
fix(file-safety): durable-copy publish and confined writes for the ta…
c78807f
fix(file-safety): do not read a failed target lstat as a missing target
d3de289
fix(file-safety): compare staging and target identity with bigint stats
1cd6a66
test(file-safety): pin the bigint options in the staging-identity tests
d5f9e25
fix(file-safety): report a Windows replacement whose DACL was not pre…
96f4840
fix(task-history): keep the item when a deletion did not happen
61d28b8
test(task-history): give the provider spec the fs exports the delete …
a2748f1
docs(file-safety): describe confineTo where it actually runs
bb8ef2b
fix(file-safety): keep DACL warning delivery from failing the save
f4ee7a0
fix(utils): check confinement before taking the advisory lock
09a6026
fix(integrations): the preview observation must not authorize an appr…
161ca86
fix(file-safety): handle async warning sinks and confine before mkdir
4042595
fix(tools): ApplyDiffTool must observe the version its diff was built on
9b74ee5
test(tools): type the apply_diff observation doubles for check-types
8e9b591
fix(integrations): never adopt an autosaved match for an unauthorized…
44e3673
fix(tools): never refresh a model observation the tool read on a diff…
a1ccf47
fix(webview): clean up the tasks a partial batch delete actually removed
e09b7db
test(webview): reach the private artifact cleanup through bracket not…
43fef0c
chore(ci): re-run the unit lane
ac90265
fix(webview): keep artifact cleanup reachable from partial receivers
6664b45
fix(webview): clean partial-delete artifacts before posting state
e0f655b
refactor(task): stop re-adding the isPaused field upstream deleted
ccb9ece
fix(file-safety): inherit unit 1 committed guard
5af828f
fix(file-safety): keep the publish error message in RollbackFailureError
d30b54a
test(file-safety): assert the exact staging directory removed after a…
f99bfd4
test: re-run mocked e2e and the ubuntu lane - no source change since …
c01245a
fix(file-safety): give the Windows DACL dump a per-write name
1a2320a
fix(tools): carry U1's hunk-read completeness rule into this unit
3887bcf
fix(tools): bring U7 onto the current file-safety core
d15f73b
chore: trigger a fresh review pass at this head
634cbe1
fix(integrations): preview observation must not authorize a save; asy…
316dc7f
fix(tools): ApplyDiffTool must observe the version its diff was built on
231b7cf
fix(safety): confine before the lock, keep the post-commit backup
67a4225
fix(safety): confine before mkdir, report the retained backup, stop p…
33802ef
fix(tools): honor an approval for a write outside the task root
5e645b1
fix(integrations): a denied preview must not stay authorized for the …
fe197a6
fix(u9): one lock key whether the parent exists, and no cleanup a wai…
f9360e9
fix(u7-port): no containment decision without a canonical workspace, …
901e54f
fix(diff-view): put the save's post-publish cleanup inside the same t…
0d5eb30
fix(diff-view): let only the teardown that started a pass finalize th…
6e5b61c
fix(diff-view): restore preview tabs for the pass that owns a rejecte…
f10bc21
fix(tools): pass the outside-workspace approval flag in its own position
d26d6c3
fix(guardedWrite): one unresolvable workspace folder must not veto ev…
a3a88be
test(task-history): make the failed-batch assertions say what they mean
411eea9
fix(guardedWrite): bind an approved outside-workspace write to the id…
3198543
fix(diff-view): keep the teardown guard held through the finalization…
1b2ac89
fix(write-to-file): resolve the approved identity inside the tool's e…
e0b458d
test(tools): keep the outside-workspace flag reset on the failure path
cd7de9d
test(observations): cover forget() and the per-task registry ownership
2dc326f
fix(tools): stop the apply_patch hunk read from refreshing a stale ob…
2a92bd9
fix(safe-write-json): release the backup a post-commit directory fsyn…
3b15a63
fix(diff-view): finalize the session when a cancellation lands during…
2226de8
fix(task-history): a failed delete is reported by the stage that fail…
ea2614d
fix(test): clear the merge artifacts on the U7 branch
caba972
refactor(safety,apply-patch): close six review threads on duplicated …
cad7265
style: format the seventeen files the formatting gate flags on this PR
aa19b7c
test(task-history): cover both answers the post-lock-failure check ca…
a38b594
fix(u9): align the unit with main's lint re-enable and format gate
easonLiangWorldedtech 486e074
fix(integrations): drop the Stryker directives from the guarded save …
easonLiangWorldedtech 1d54a75
fix(guarded-write): bind the publish to the authorized canonical target
easonLiangWorldedtech 3d8681d
test(tools): pass toolCallId in guarded-write spec callbacks
easonLiangWorldedtech 3a5ebbb
fix(guarded-write): refuse a dangling leaf link before publishing
easonLiangWorldedtech 6c37918
Merge upstream/main into fws/u9-task-history-delete
3a952e0
chore(lint): prune the stale no-explicit-any suppressions after the m…
easonLiangWorldedtech dfc4aa8
merge upstream/main (a4f774b17) into fws/u9-task-history-delete
easonLiangWorldedtech 8a23831
fix(file-safety): type the safeWriteJson payload as unknown
easonLiangWorldedtech ff2de55
test(core): double the guarded-read seams the merged upstream specs miss
easonLiangWorldedtech 0d93684
Merge remote-tracking branch 'upstream/main' into HEAD
easonLiangWorldedtech 620b755
fix(tools): state omitted lines for offset reads and hold the lock ke…
easonLiangWorldedtech 2b5ccf7
Merge upstream main 73931fe8be25 into fws/u9-task-history-delete
easonLiangWorldedtech fb14da0
Task-history delete: name the stage that actually failed (#1917 revie…
easonLiangWorldedtech c4bd9a2
Task-history delete: certify absence only under the lock (#1917 revie…
easonLiangWorldedtech c6962e3
fix(task-history): report a batch by id, so a duplicate id is judged …
ae4cfcb
fix(task-history): certify an absence against the spelling the write …
ed185e2
fix(task-history): ask the named path when the sweep's directory is gone
adb8187
test(task-history): seed the mtime map through the path that actually…
25e4fb8
fix(u9): name the lock-key re-derivation when it fails under a held lock
bc352ea
fix(u9): re-derive the lock key inside the certification and scan bot…
2281ac0
fix(u9): keep a duplicate id's entry when its later attempt fails aft…
0fae602
test(u9): put the delete-semantics injections where they land on ever…
60f9c8c
fix(task-history): keep a duplicate id's served entry through a secon…
6093d2a
test(task-history): assert the values a duplicate-id restore puts back
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
1,385 changes: 1,348 additions & 37 deletions
1,385
src/core/task-persistence/__tests__/TaskHistoryStore.deleteSemantics.spec.ts
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import { describe, it, expect, vi } from "vitest" | ||
|
|
||
| import { ObservationRegistry } from "../observationRegistry" | ||
|
|
||
| describe("ObservationRegistry", () => { | ||
| it("observe → get returns the recorded version and observedAt", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "1:2:300:4000000000:5000000000") | ||
|
|
||
| const obs = reg.get("/a/b/c.ts") | ||
| expect(obs).toBeDefined() | ||
| expect(obs!.version).toBe("1:2:300:4000000000:5000000000") | ||
| expect(typeof obs!.observedAt).toBe("number") | ||
| }) | ||
|
|
||
| it("re-observe replaces the entry with a fresh observedAt", () => { | ||
| vi.useFakeTimers() | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1") | ||
| const first = reg.get("/a/b/c.ts")! | ||
| expect(first.version).toBe("v1") | ||
|
|
||
| vi.advanceTimersByTime(50) | ||
| reg.observe("/a/b/c.ts", "v2") | ||
| const second = reg.get("/a/b/c.ts")! | ||
| expect(second.version).toBe("v2") | ||
| expect(second.observedAt).toBeGreaterThan(first.observedAt) | ||
|
|
||
| vi.useRealTimers() | ||
| }) | ||
|
|
||
| it("has returns true for observed paths, false otherwise", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/x.ts", "t1") | ||
| expect(reg.has("/x.ts")).toBe(true) | ||
| expect(reg.has("/y.ts")).toBe(false) | ||
| }) | ||
|
|
||
| it("size reflects the number of observed entries", () => { | ||
| const reg = new ObservationRegistry() | ||
| expect(reg.size).toBe(0) | ||
| reg.observe("/a.ts", "t1") | ||
| reg.observe("/b.ts", "t2") | ||
| expect(reg.size).toBe(2) | ||
| }) | ||
|
|
||
| it("clear removes all entries and resets size to 0", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a.ts", "t1") | ||
| reg.observe("/b.ts", "t2") | ||
| reg.clear() | ||
| expect(reg.size).toBe(0) | ||
| expect(reg.get("/a.ts")).toBeUndefined() | ||
| expect(reg.has("/b.ts")).toBe(false) | ||
| }) | ||
|
|
||
| it("get on empty registry returns undefined", () => { | ||
| const reg = new ObservationRegistry() | ||
| expect(reg.get("/any.ts")).toBeUndefined() | ||
| }) | ||
|
|
||
| it("separate instances are independent — observing in one does not appear in the other", () => { | ||
| const regA = new ObservationRegistry() | ||
| const regB = new ObservationRegistry() | ||
| regA.observe("/shared.ts", "v1") | ||
| expect(regA.get("/shared.ts")).toBeDefined() | ||
| expect(regB.get("/shared.ts")).toBeUndefined() | ||
| regB.observe("/shared.ts", "v2") | ||
| expect(regA.get("/shared.ts")!.version).toBe("v1") | ||
| expect(regB.get("/shared.ts")!.version).toBe("v2") | ||
| }) | ||
|
|
||
| describe("forget", () => { | ||
| it("removes the entry and reports that it removed one", () => { | ||
| const registry = new ObservationRegistry() | ||
| registry.observe("/a.ts", "1:1:1:1:1", true) | ||
|
|
||
| expect(registry.forget("/a.ts")).toBe(true) | ||
| expect(registry.has("/a.ts")).toBe(false) | ||
| expect(registry.get("/a.ts")).toBeUndefined() | ||
| expect(registry.size).toBe(0) | ||
| }) | ||
|
|
||
| it("reports false for a path it never observed, and changes nothing", () => { | ||
| const registry = new ObservationRegistry() | ||
| registry.observe("/a.ts", "1:1:1:1:1", true) | ||
|
|
||
| expect(registry.forget("/missing.ts")).toBe(false) | ||
| expect(registry.size).toBe(1) | ||
| expect(registry.get("/a.ts")?.version).toBe("1:1:1:1:1") | ||
| }) | ||
|
|
||
| it("leaves the other entries alone - that is why a caller uses forget and not clear", () => { | ||
| // A caller revoking an authorization it did not earn must not discard the | ||
| // observations other reads of the same task still rely on. | ||
| const registry = new ObservationRegistry() | ||
| registry.observe("/a.ts", "1:1:1:1:1", true) | ||
| registry.observe("/b.ts", "2:2:2:2:2", false) | ||
|
|
||
| expect(registry.forget("/a.ts")).toBe(true) | ||
| expect(registry.get("/b.ts")).toEqual({ | ||
| version: "2:2:2:2:2", | ||
| observedAt: expect.any(Number), | ||
| complete: false, | ||
| }) | ||
| expect(registry.size).toBe(1) | ||
| }) | ||
| }) | ||
|
|
||
| describe("completeness scope (S4b follow-up #46)", () => { | ||
| it("defaults to a complete observation when the read scope is not given", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1") | ||
|
|
||
| expect(reg.get("/a/b/c.ts")!.complete).toBe(true) | ||
| }) | ||
|
|
||
| it("records a partial observation when the read only returned a view of the file", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1", false) | ||
|
|
||
| expect(reg.get("/a/b/c.ts")!.complete).toBe(false) | ||
| }) | ||
|
|
||
| it("re-observing replaces the entry's completeness with the new read's scope", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1", false) | ||
| reg.observe("/a/b/c.ts", "v2") | ||
|
|
||
| const obs = reg.get("/a/b/c.ts")! | ||
| expect(obs.version).toBe("v2") | ||
| expect(obs.complete).toBe(true) | ||
| }) | ||
|
|
||
| it("re-observing with a partial scope downgrades a previously complete entry", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1") | ||
| reg.observe("/a/b/c.ts", "v2", false) | ||
|
|
||
| const obs = reg.get("/a/b/c.ts")! | ||
| expect(obs.version).toBe("v2") | ||
| expect(obs.complete).toBe(false) | ||
| }) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /** | ||
| * Per-task file observation registry (upstream epic #1375, phase A2). | ||
| * | ||
| * Each Task owns its own instance so parent and subtask observations are | ||
| * independent. The S4 guarded-write will compare these versions against the | ||
| * token recomputed pre-write to detect stale reads or file replacement. | ||
| * | ||
| * Pure in-memory — zero I/O, no dependencies. The S4 guarded-write consults | ||
| * these observations for the version check and for the completeness check that | ||
| * gates a full-file replacement. | ||
| */ | ||
|
|
||
| export interface FileObservation { | ||
| /** Version token derived from on-disk fs.stat (bigint mode). */ | ||
| version: string | ||
| /** Millisecond timestamp when the observation was recorded. */ | ||
| observedAt: number | ||
| /** | ||
| * Whether the read that produced this observation returned the complete | ||
| * file. A slice, line-range, truncated, or indentation-block read returns | ||
| * only a view of the file; such an observation authorizes targeted edits | ||
| * on the view the model saw, but never a full-file replacement. | ||
| */ | ||
| complete: boolean | ||
| } | ||
|
|
||
| export class ObservationRegistry { | ||
| private readonly entries = new Map<string, FileObservation>() | ||
|
|
||
| /** | ||
| * Record an observation for a file at its absolute path. | ||
| * | ||
| * Re-observing replaces the entry with a fresh observedAt timestamp, the | ||
| * new version token, and the read's completeness. `complete` defaults to | ||
| * true for callers that read the whole file themselves (spec doubles, | ||
| * WriteToFileTool). A caller whose read is internal to a targeted edit must | ||
| * carry the model's prior completeness instead, so the tool's own read cannot | ||
| * upgrade a partial read into authority for a full-file replacement. | ||
| */ | ||
| observe(absolutePath: string, version: string, complete: boolean = true): void { | ||
| this.entries.set(absolutePath, { version, observedAt: Date.now(), complete }) | ||
| } | ||
|
|
||
| get(absolutePath: string): FileObservation | undefined { | ||
| return this.entries.get(absolutePath) | ||
| } | ||
|
|
||
| has(absolutePath: string): boolean { | ||
| return this.entries.has(absolutePath) | ||
| } | ||
|
|
||
| /** | ||
| * Drop the observation for one path. A caller that must revoke an | ||
| * authorization it did not earn - a preview that observed a version the model | ||
| * never read - needs this instead of clear(), which would also discard the | ||
| * observations other reads of the same task still rely on. | ||
| */ | ||
| forget(absolutePath: string): boolean { | ||
| return this.entries.delete(absolutePath) | ||
| } | ||
|
|
||
| clear(): void { | ||
| this.entries.clear() | ||
| } | ||
|
|
||
| get size(): number { | ||
| return this.entries.size | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.