Skip to content

Commit d0ff650

Browse files
Build reusable knowledge guides from saved history (#1975)
Separate saved evidence, concept organization, reviewed guides, and human changes. Publish maintained pages atomically for Memory and agent recall. Document module contracts for contributors.
1 parent 4326eb3 commit d0ff650

48 files changed

Lines changed: 5321 additions & 369 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/ARCHITECTURE.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ user's project knowledge vault is not the repository's developer documentation.
5151
A project-knowledge feature needs both developer guidance here when structure
5252
changes and product guidance in the manual when behavior changes.
5353

54+
## Change profile knowledge through its owner
55+
56+
The knowledge vault owns source admission, background compilation, publication,
57+
search, and human overrides. The session history reader decides which saved records
58+
can enter the vault. Memory renders published pages and does not run compilation
59+
on the UI loop. Agent recall reads the same effective publication.
60+
61+
Start with the [knowledge architecture](knowledge.md) for entry points and module
62+
contracts. Preserve immutable sources, stable page identities, cancellation,
63+
revision-checked edits, and deletion intent when changing this path. Keep provider
64+
requests outside database write transactions and publish pages and search entries
65+
as one revision.
66+
5467
## Contracts and design rationale
5568

5669
Current interface references include [headless behavior](HEADLESS.md),
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
kind: changed
3+
title: Build and maintain concept guides from saved chat and task history
4+
pr: 1975
5+
surface: [chat, engine, docs]
6+
invalidates:
7+
- "Saved task journals were excluded from knowledge history; substantive saved task reports now participate while injected briefs and control traffic remain excluded."
8+
- "Knowledge discovery saw only fresh evidence notes; it now considers existing concepts and reviews the initial tree before writing guides."
9+
- "Independent subject writing was serial; initial drafts and editorial refreshes now run with at most two provider jobs while shared decisions and publication remain ordered."
10+
- "Knowledge progress lacked stage percentages; Memory now distinguishes history import, source processing and remaining page organization."
11+
---
12+
13+
Memory uses clear heading, text and numeric colors, hover feedback on switches
14+
and controls, and progress that wraps within the article sidebar. Stage counts
15+
are measured items rather than elapsed-time or overall-completion estimates.
16+
17+
The first build drains a fixed admitted history boundary before organization.
18+
Catalog-aware routing reuses existing page identities. The initial concept tree
19+
receives a complete review within each source scope before independent guides are
20+
written. Separate collections cannot be merged by that planning call.
21+
Short titles and two-level folders follow the actual subjects rather than fixed
22+
project categories. Topic maps stay with their pages.
23+
24+
Claims retain original citations and assistant reports stay attributed. Changed
25+
and retired evidence refreshes affected guides; exact quote spans prevent
26+
unrelated guidance from being invalidated. Human prose and deleted concepts
27+
remain protected. New background evidence flags edited pages for review.
28+
29+
Incomplete provider results and invalid organization references preserve pending
30+
work. Failed plans are not cached as valid results. Generated prose must pass
31+
both evidence support and lasting-usefulness checks. Existing published page
32+
identities are retained; this does not automatically merge older published
33+
concepts or rewrite human edits. Large concepts stay on one canonical page while sequential evidence parts keep
34+
each provider request bounded. New evidence continues refreshing these guides.
35+
A final selection pass keeps the overview concise without rewriting reviewed
36+
paragraphs or dropping its underlying evidence. Reuse can supply only an
37+
existing page identity and evidence; its saved heading and scope remain authoritative.

‎docs/knowledge.md‎

Lines changed: 93 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,100 @@
11
# Native knowledge architecture
22

3-
This reference describes the chat application's native knowledge module. The
4-
user workflow lives in the [Memory manual](../internal/manual/chat/places.md#knowledge-from-saved-chats-inside-memory).
3+
This contributor reference describes the chat application's profile knowledge
4+
module. Product behavior belongs in the [Memory manual](../internal/manual/chat/places.md#knowledge-from-saved-chats-inside-memory).
55

6-
## One profile database
6+
## Ownership and entry points
77

8-
`internal/knowledge.Vault` owns the database at `knowledge/knowledge.sqlite`
9-
beneath the resolved profile. `Open(profile)` returns a lightweight handle;
10-
operations open bounded SQLite connections. WAL permits readers while the
11-
compiler makes short transactions. Write transactions begin immediately so
12-
concurrent source admission cannot fail while upgrading a read lock.
8+
| Responsibility | Implementation owner |
9+
| --- | --- |
10+
| Start, resume, and stop background work | `cmd/codeaf/chatv3_knowledge.go`, `internal/knowledge/build.go` |
11+
| Discover and read saved history | `internal/session/knowledge_history.go` |
12+
| Admit immutable sources and publish readable pages | `internal/knowledge/store.go` |
13+
| Compile evidence into maintained concept guides | `internal/knowledge/compiler.go` |
14+
| Preserve edits and deletion intent | `internal/knowledge/edit.go` |
15+
| Agent knowledge tools and local recall | `internal/session/knowledge_tools.go`, `internal/knowledge/compiler_recall.go` |
16+
| Memory rendering, navigation, and editor | `internal/tui3/knowledge*.go` |
17+
18+
`knowledge.Open(profile)` returns a lightweight `Vault` handle. The vault owns
19+
`knowledge/knowledge.sqlite` beneath the resolved profile. Callers use vault
20+
operations rather than reading compiler state or writing database tables directly.
21+
The chat and resident surfaces have separate lifecycles.
22+
23+
## Data flow
24+
25+
```text
26+
saved history and new conversation events
27+
→ immutable source admission
28+
→ evidence extraction and concept organization
29+
→ reviewed guides
30+
→ atomic publication
31+
→ Memory, indexed search, and agent recall
32+
```
33+
34+
The initial build organizes a finite admitted history snapshot. Later evidence
35+
updates affected guides or creates distinct concepts. Source eligibility and
36+
concept organization are separate responsibilities. Saved task reports can supply
37+
evidence; internal control traffic and deleted history remain excluded.
38+
39+
## Storage authority
1340

1441
| Stored data | Authority |
1542
| --- | --- |
16-
| Immutable source events and journal cursors | Original admitted text, provenance, pending work, and exactly where import resumes |
17-
| Compiler publications and checkpoints | Extraction, learning, topic registry, validated model receipts, and recovery after interrupted composition |
18-
| Effective pages and FTS | Reader, indexed search, citations, links, and one committed publication revision |
19-
| Human overrides | Revision-checked Markdown edits and durable deletion tombstones, separate from generated prose |
20-
| Build state and history errors | First-build intent, background progress, partial imports, retries, and recorded cost |
21-
22-
Compiler documents retain optional claim and learning metadata inside SQLite.
23-
Per-page records and full-text indexes serve readers; a page lookup does not
24-
load the compiler document. JSON files and experimental exports are not read
25-
paths. Publication overlays the latest human changes, replaces effective pages
26-
and search entries, and commits the visible revision in one transaction.
27-
28-
## Background ownership
29-
30-
`v3Process` starts and joins `Vault.Run` under its process context. A worker lock
31-
admits one process per profile; another running process waits and takes ownership
32-
when the OS releases that lock. A separate compiler lock prevents overlapping
33-
manual flushes. These OS locks retain worker ownership during provider requests;
34-
SQLite transactions end before network work begins.
35-
36-
A successful empty inventory arms future capture and shows the waiting state.
37-
Existing history waits for **Build knowledge base**. The action records durable
38-
intent and returns; it does not run models on the UI loop. Initial history drains
39-
in bounded batches. Future turns use the quiet/max-wait policy. Cancellation
40-
leaves admitted sources and completed stages available for the next process.
41-
42-
`internal/session.KnowledgeHistory` owns saved-journal discovery and parsing.
43-
It visits all project buckets without a recent-chat limit, includes archives and
44-
legacy flat journals, resolves sealed layouts through `Place.Transcript`, and
45-
excludes deleted conversations and task journals. Only complete records advance
46-
cursors. Original messages use stable session/byte-offset IDs. Admission and
47-
cursor advancement commit together; retries compare immutable source content.
48-
49-
## Compiler and evidence
50-
51-
The compiler preserves the original extraction, exact-quote validation,
52-
claim deduplication, qualified assistant reports, human-only newer corrections,
53-
contradictions, stable note identity, subject discovery, reviewed explanations,
54-
question resolution, topic reconciliation, and canonical mention links.
55-
56-
Provider results enter through typed role-response checks. Claims and paragraphs
57-
must reference admitted sources or known claims; quotes must occur in the supplied
58-
context. Separate review gates generated explanations. Model receipts record the
59-
actual returned model ID and reject substitutions. The configured knowledge model
60-
applies to every role; credentials never enter receipts or visible errors.
61-
62-
The reader and editor share the same effective pages used by agent recall.
63-
Edited prose is explicitly human-authored, with original source history kept
64-
separate. Tombstones suppress generated pages across rebuilds. Stale editor
65-
revisions cannot overwrite later edits. Search entries change in the same
66-
transaction as visible page content.
67-
68-
## Responsiveness and bounds
69-
70-
Extraction admits at most 64 spans and 24,000 source characters per call. A
71-
journal read handles at most 256 records or 64 eligible sources, with a 16 MiB
72-
record bound. Discovery processes at most 256 notes per group, and oversized
73-
subjects retain a partition-needed state rather than truncating claims.
74-
75-
The UI performs database work through asynchronous commands. Search uses FTS,
76-
late filter results are discarded, and unchanged publication revisions reuse
77-
cached reader content. Rendering and navigation do not call the provider.
78-
Provider responses and timeouts are bounded; retries use the same selected model.
79-
80-
## Verification
81-
82-
Tests cross saved-journal-to-readable/searchable-page, human-edit/rebuild, and
83-
Knowledge-screen seams. They cover history from multiple projects, first-chat
84-
population, notes/compaction exclusion, source support, correction authority,
85-
review rejection, duplicate replay, and navigation while a build is requested.
86-
87-
`TestKnowledgeLiveNativeCompiler` is opt-in through `CODEAF_KNOWLEDGE_LIVE=1`.
88-
It uses the existing OpenRouter credential, removes Python from the executable
89-
search path, and checks the actual model recorded for every call. Ordinary tests
90-
use a provider fixture and require no credential.
43+
| Source events and import cursors | Admitted text, provenance, and resumable intake |
44+
| Compiler checkpoints and validated receipts | Recoverable background compilation |
45+
| Effective pages and full-text index | One published view for readers and search |
46+
| Human overrides | Revision-checked edits and durable deletion intent |
47+
| Build state | Background intent, stage progress, and recorded usage |
48+
49+
Generated prose does not replace source evidence. Page identity is independent of
50+
its current title. Concept guides, detailed evidence notes, and topic maps have
51+
different reader roles. A visible guide count and the total stored page count need
52+
not be equal.
53+
54+
## Public module contracts
55+
56+
`Capture` admits local source text without a model call. `StartBuild` records
57+
background intent. The process owns and joins `Run` through its context. `Flush`
58+
runs compilation outside the UI loop. `Snapshot`, `Read`, and `Search` expose
59+
published content. `EditPage` and `DeletePage` require the expected page revision.
60+
`Revisit` operates on explicitly named immutable sources. Use the owning APIs for
61+
optional intake rather than bypassing their concurrency checks.
62+
63+
Memory and agent recall consume the same effective pages. An edit must remain
64+
human-authored across future builds. A deletion must remain suppressed. An older
65+
editor revision must not overwrite a newer change. Generated additions to an
66+
edited guide remain reviewable through its source evidence.
67+
68+
## Concurrency and failure boundaries
69+
70+
The worker and compiler have separate per-profile ownership locks. Provider work
71+
runs outside SQLite write transactions. Independent guide work can run
72+
concurrently; shared extraction decisions and visible publication remain ordered.
73+
A publication transaction replaces pages, search entries, and the visible revision
74+
together, after overlaying the latest human changes.
75+
76+
Interrupted work retains admitted sources and completed checkpoints. Invalid
77+
provider output cannot become a valid cached result or a partial visible
78+
publication. Every role uses the selected knowledge model; a fallback is refused.
79+
Keep credentials and source content out of visible errors.
80+
81+
## Reader responsiveness
82+
83+
The UI performs database work through asynchronous commands. Search uses the
84+
published full-text index. Late filter results are discarded. Unchanged publication
85+
revisions reuse reader content. Rendering, browsing, and local search do not call a
86+
model. Import and source-processing progress describe their own completed items,
87+
not an overall completion estimate.
88+
89+
## Change and verify the owning boundary
90+
91+
Keep database representation, compiler checkpoints, and prompt composition inside
92+
`internal/knowledge`. History eligibility changes belong with the session history
93+
reader. User-facing changes also update the shipped Memory manual. Test through
94+
capture, compilation, published pages, edit conflicts, and the actual terminal UI.
95+
96+
Ordinary tests use a provider fixture and require no credential. The opt-in
97+
`TestKnowledgeLiveNativeCompiler` checks the actual recorded model through the live
98+
provider. Run the repository's required checks before merging. Recheck cancellation,
99+
source citations, human edits, deletion intent, and search consistency when changing
100+
publication or concurrency.

‎internal/knowledge/algorithms_test.go‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,23 @@ import (
77
"io"
88
"net/http"
99
"strings"
10+
"sync"
1011
"testing"
1112

1213
"github.com/Agent-Field/codeaf/internal/knowledge"
1314
)
1415

1516
func withKnowledgeModel(t *testing.T, respond func(map[string]any) map[string]any) {
17+
t.Helper()
18+
var mu sync.Mutex
19+
withConcurrentKnowledgeModel(t, func(payload map[string]any) map[string]any {
20+
mu.Lock()
21+
defer mu.Unlock()
22+
return respond(payload)
23+
})
24+
}
25+
26+
func withConcurrentKnowledgeModel(t *testing.T, respond func(map[string]any) map[string]any, consolidate ...func(map[string]any) map[string]any) {
1627
t.Helper()
1728
original := http.DefaultTransport
1829
t.Cleanup(func() { http.DefaultTransport = original })
@@ -28,7 +39,21 @@ func withKnowledgeModel(t *testing.T, respond func(map[string]any) map[string]an
2839
if err := json.Unmarshal([]byte(body.Messages[1].Content), &payload); err != nil {
2940
return nil, err
3041
}
31-
content, _ := json.Marshal(respond(payload))
42+
var result map[string]any
43+
if catalog, ok := payload["consolidation_catalog"].([]any); ok {
44+
groups := []any{}
45+
for _, raw := range catalog {
46+
draft := raw.(map[string]any)
47+
groups = append(groups, map[string]any{"members": []any{draft["id"]}, "title": draft["title"], "scope": draft["scope"], "purpose": draft["purpose"], "folder": draft["folder"]})
48+
}
49+
result = map[string]any{"groups": groups}
50+
if len(consolidate) > 0 {
51+
result = consolidate[0](payload)
52+
}
53+
} else {
54+
result = respond(payload)
55+
}
56+
content, _ := json.Marshal(result)
3257
raw, _ := json.Marshal(map[string]any{"model": body.Model, "choices": []any{map[string]any{"finish_reason": "stop", "message": map[string]any{"content": string(content)}}}})
3358
return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(raw)), Header: make(http.Header)}, nil
3459
})
@@ -139,7 +164,7 @@ func TestKnowledgeWithholdsExplanationRejectedByReview(t *testing.T) {
139164
claim := payload["claims"].([]any)[0].(map[string]any)
140165
return map[string]any{"sections": []any{map[string]any{"heading": "Status", "paragraphs": []any{map[string]any{"text": "Cedar is deployed.", "claims": []any{claim["id"]}}}}}, "questions": []any{}, "conflicts": []any{}}
141166
}
142-
return map[string]any{"checks": []any{map[string]any{"id": "P0", "supported": false, "reason": "A proposal is not deployment."}}}
167+
return map[string]any{"checks": []any{map[string]any{"id": "P0", "supported": false, "useful": true, "reason": "A proposal is not deployment."}}}
143168
})
144169
if err := v.Capture(knowledge.Source{ID: "s", Session: "chat", Role: "user", TS: "2026-10-25T12:00:00Z", Text: "Cedar is proposed."}); err != nil {
145170
t.Fatal(err)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package knowledge_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/Agent-Field/codeaf/internal/knowledge"
7+
"testing"
8+
)
9+
10+
func TestKnowledgeTopicMapUsesItsPagesCollection(t *testing.T) {
11+
v := knowledge.Open(t.TempDir())
12+
if err := v.StartBuild(); err != nil {
13+
t.Fatal(err)
14+
}
15+
withKnowledgeModel(t, func(p map[string]any) map[string]any {
16+
if messages, ok := p["messages"].([]any); ok {
17+
claims := []any{}
18+
for _, raw := range messages {
19+
s := raw.(map[string]any)
20+
claims = append(claims, map[string]any{"local_id": s["id"], "text": s["text"], "scope": "Cedar", "topic": s["id"], "kind": "fact", "evidence": []any{map[string]any{"source": s["id"], "quote": s["text"]}}})
21+
}
22+
return map[string]any{"claims": claims}
23+
}
24+
if proposal, ok := p["proposed_subjects"]; ok {
25+
return map[string]any{"subjects": proposal}
26+
}
27+
if catalog, ok := p["catalog"].([]any); ok {
28+
subjects := []any{}
29+
for _, raw := range catalog {
30+
n := raw.(map[string]any)
31+
subjects = append(subjects, map[string]any{"title": n["title"], "scope": "Cedar", "folder": []any{"Library", "Cedar"}, "notes": []any{n["id"]}})
32+
}
33+
return map[string]any{"subjects": subjects}
34+
}
35+
return map[string]any{"sections": []any{}, "questions": []any{}, "conflicts": []any{}}
36+
})
37+
for i, text := range []string{"Cedar queues durable work.", "Cedar reports usage spending."} {
38+
if err := v.Capture(knowledge.Source{ID: fmt.Sprintf("concept-%d", i), Session: "chat", Role: "user", TS: "2026-10-25T12:00:00Z", Text: text}); err != nil {
39+
t.Fatal(err)
40+
}
41+
}
42+
if err := v.Flush(context.Background(), "fixture", true); err != nil {
43+
t.Fatal(err)
44+
}
45+
state, err := v.Snapshot()
46+
if err != nil {
47+
t.Fatal(err)
48+
}
49+
maps := 0
50+
for _, p := range state.Pages {
51+
if p.Kind == "map" {
52+
maps++
53+
if len(p.Folder) != 2 || p.Folder[0] != "Library" || p.Folder[1] != "Cedar" {
54+
t.Fatalf("topic map created a disconnected scope folder: %v", p.Folder)
55+
}
56+
}
57+
}
58+
if maps != 1 {
59+
t.Fatalf("expected one connected topic map, got %d", maps)
60+
}
61+
}

0 commit comments

Comments
 (0)