From f5fae3a826ac8c1a5d21ad54cb805f40355bfca3 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:29:24 -0800 Subject: [PATCH 1/2] feat(memory): save project facts without an approval queue Migrate existing facts to a saved-or-forgotten lifecycle and update clients and agents together. Keep provenance, explicit supersession, feedback and TraceDecay-derived ranking. Co-Authored-By: Claude Opus 5.5 --- .../components/ProjectKnowledgeEntry.test.tsx | 6 +- .../src/components/ProjectKnowledgeEntry.tsx | 11 +- .../components/ProjectKnowledgeFactDetail.tsx | 30 +- .../components/ProjectKnowledgeFactList.tsx | 25 +- ...SettingsServerControlsRouteScreen.test.tsx | 16 +- .../SettingsServerControlsRouteScreen.tsx | 8 +- .../src/mcp/toolkits/knowledge/tools.ts | 6 +- .../ProviderRuntimeRecoveryService.test.ts | 2 +- .../RestartContinuation.test.ts | 11 +- .../RunExecutionService.test.ts | 1 + .../server/src/persistence/Migrations.test.ts | 220 ++++++++++++++ apps/server/src/persistence/Migrations.ts | 2 + .../091_ProjectKnowledgeSavedLifecycle.ts | 77 +++++ .../reconcileV2PreviewMigration.test.ts | 2 + .../knowledge/AssistedLearning.test.ts | 284 +++++++++++------- .../src/project/knowledge/AssistedLearning.ts | 63 ++-- .../knowledge/ProjectKnowledge.test.ts | 244 +++++++-------- .../src/project/knowledge/ProjectKnowledge.ts | 99 ++---- .../knowledge/assistedLearningSchedule.ts | 5 - .../src/project/knowledge/persistence.ts | 41 +-- .../src/provider/agentContextBudget.json | 6 +- .../FileCodeRelationships.browser.test.tsx | 2 +- .../files/FileCodeRelationships.tsx | 1 - .../AssistedLearningSection.tsx | 11 +- .../projectKnowledge/ProjectFactDetail.tsx | 3 +- .../ProjectMemoryFactList.tsx | 4 +- .../ProjectMemoryInspector.tsx | 22 +- .../ProjectMemoryPanel.browser.test.tsx | 94 ++---- .../projectKnowledge/ProjectMemoryPanel.tsx | 35 +-- .../projectKnowledge/projectMemoryLabels.ts | 7 +- .../src/components/settings/ZackSettings.tsx | 19 +- .../settings/scopedSettings.test.ts | 4 +- .../src/components/settings/scopedSettings.ts | 4 +- .../src/components/settings/settingsSearch.ts | 6 +- docs/user/composer.md | 45 +-- docs/user/outside-agents.md | 5 +- .../cases/project-intelligence.ndjson | 5 +- .../src/projectKnowledge/index.ts | 11 +- packages/contracts/src/environment.ts | 2 +- .../contracts/src/projectKnowledge.test.ts | 71 ++++- packages/contracts/src/projectKnowledge.ts | 37 +-- packages/contracts/src/settings.ts | 4 +- .../src/server/orchestrationInstructions.ts | 2 +- 43 files changed, 880 insertions(+), 673 deletions(-) create mode 100644 apps/server/src/persistence/Migrations/091_ProjectKnowledgeSavedLifecycle.ts diff --git a/apps/mobile/src/components/ProjectKnowledgeEntry.test.tsx b/apps/mobile/src/components/ProjectKnowledgeEntry.test.tsx index dc628aa11f445..fea867f7cd530 100644 --- a/apps/mobile/src/components/ProjectKnowledgeEntry.test.tsx +++ b/apps/mobile/src/components/ProjectKnowledgeEntry.test.tsx @@ -245,7 +245,7 @@ const fact: ProjectFact = { tags: [], kind: "note", sources: [], - status: "proposed", + status: "saved", revision: 1, trust: 0.5, retrievalCount: 0, @@ -486,7 +486,6 @@ it("shows user origin once without treating the assertion as a citation", async const asserted: ProjectFact = { ...fact, origin: { kind: "user" }, - status: "confirmed", sources: [{ kind: "user" }], }; state.list = { facts: [asserted], nextCursor: null }; @@ -681,7 +680,6 @@ it("marks a superseded fact read-only and lists superseded facts on request", as await screen.click(`Inspect fact: ${fact.content}`); const labels = [...screen.container.querySelectorAll("button")].map((item) => item.textContent); expect(labels).not.toContain("Edit"); - expect(labels).not.toContain("Confirm"); expect(labels).not.toContain("Supersede"); await screen.click("Back"); expect(state.listInputs.at(-1)).not.toHaveProperty("includeSuperseded"); @@ -841,7 +839,7 @@ it("drops a revised fact from the selection and the draft after saving", async ( }); screen.openMemory(); await screen.click(`Inspect fact: ${fact.content}`); - await screen.click("Confirm"); + await screen.click("Helpful"); expect(state.setText).toHaveBeenCalledWith(scope.draftKey, ""); expect(screen.container.textContent).toContain("0 selected"); expect(screen.container.textContent).toContain("removed from the draft"); diff --git a/apps/mobile/src/components/ProjectKnowledgeEntry.tsx b/apps/mobile/src/components/ProjectKnowledgeEntry.tsx index a664c574a46a3..1c66817f19235 100644 --- a/apps/mobile/src/components/ProjectKnowledgeEntry.tsx +++ b/apps/mobile/src/components/ProjectKnowledgeEntry.tsx @@ -502,18 +502,17 @@ function ProjectKnowledgeSheet( if (inspectedFact.status !== "forgotten") push({ kind: "editor", mode: { kind, fact: inspectedFact } }); }} - onRevise={(action) => { - const revise = () => void apply({ action, ...factMutation(inspectedFact) }); - if (action !== "forget") return revise(); + onForget={() => Dialog.confirm({ title: "Forget this fact permanently?", message: "Its content, history and every copy sent to agents are removed and cannot be restored. If the fact changed rather than became wrong, supersede it instead.", confirmText: "Forget", destructive: true, - onConfirm: revise, - }); - }} + onConfirm: () => + void apply({ action: "forget", ...factMutation(inspectedFact) }), + }) + } onFeedback={(helpful) => void apply({ action: "feedback", helpful, ...factMutation(inspectedFact) }) } diff --git a/apps/mobile/src/components/ProjectKnowledgeFactDetail.tsx b/apps/mobile/src/components/ProjectKnowledgeFactDetail.tsx index eb20cb7fd4448..76fb48a56374e 100644 --- a/apps/mobile/src/components/ProjectKnowledgeFactDetail.tsx +++ b/apps/mobile/src/components/ProjectKnowledgeFactDetail.tsx @@ -126,7 +126,7 @@ export function ProjectFactInspector(props: { readonly onReadSource: (ref: KnowledgeMessageRef) => void; readonly onOpenFact: OpenFact; readonly onEdit: (mode: "edit" | "supersede") => void; - readonly onRevise: (action: "confirm" | "forget") => void; + readonly onForget: () => void; readonly onFeedback: (helpful: boolean) => void; readonly onDismissHints: () => void; }) { @@ -162,13 +162,6 @@ export function ProjectFactInspector(props: { Trust signal {fact.trust.toFixed(2)} · revision {fact.revision} - {fact.status === "proposed" && - fact.origin.kind === "assisted" && - fact.supersedes != null ? ( - - Confirming replaces fact {fact.supersedes.slice(0, 8)}; forgetting leaves it as it is. - - ) : null} @@ -202,14 +195,6 @@ export function ProjectFactInspector(props: { {active ? ( - {fact.status === "proposed" && !superseded ? ( - - ) : null} {/* The server rejects revisions of a superseded fact; its replacement is the one to change. */} {!superseded ? (