Skip to content

Commit 6ca092a

Browse files
committed
fix(extended-memory): align SourceInferred trust boundary and wrap ReturnAfterBreak
- Remove SourceInferred from TrustBoost (treated as tainted/quarantined until promotion), matching IsTaintedSourceClass. - Wrap the return-after-break summary with wrapUntrusted before injecting it as a system message in odek continue; attach the audit recorder early so the ingestion is recorded. - Update docs to mark inferred as tainted/quarantined and document the first-pronoun, score-gated anaphora behavior.
1 parent bf2a46c commit 6ca092a

3 files changed

Lines changed: 26 additions & 26 deletions

File tree

cmd/odek/main.go

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2121,10 +2121,26 @@ func continueCmd(args []string) error {
21212121
// The system message is already in the session
21222122
messages := sess.GetMessages()
21232123

2124+
// Create the run context early so that the return-after-break summary can
2125+
// be recorded in the audit log before the turn starts.
2126+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
2127+
defer cancel()
2128+
2129+
// Audit: record every untrusted-content ingestion that fires during
2130+
// this turn. The recorder is scoped to the run context so a later turn
2131+
// (or background goroutine) cannot accidentally write to the wrong
2132+
// session's audit log.
2133+
auditStore := session.NewAuditStore(store.Dir())
2134+
currentTurn := sess.Turns + 1
2135+
sessIDCapture := sess.ID
2136+
ctx = loop.WithIngestRecorder(ctx, func(source, content string) {
2137+
_ = auditStore.RecordIngest(sessIDCapture, currentTurn, source, content)
2138+
})
2139+
21242140
// Return-after-break: on session resume, load a concise summary of where
21252141
// the user left off and the next likely step.
21262142
if mm := agent.Memory(); mm != nil {
2127-
rbCtx, rbCancel := context.WithTimeout(context.Background(), 5*time.Second)
2143+
rbCtx, rbCancel := context.WithTimeout(ctx, 5*time.Second)
21282144
if rb := mm.FormatReturnAfterBreak(rbCtx); rb != "" {
21292145
insertIdx := -1
21302146
for i := len(messages) - 1; i >= 0; i-- {
@@ -2133,7 +2149,8 @@ func continueCmd(args []string) error {
21332149
break
21342150
}
21352151
}
2136-
rbMsg := llm.Message{Role: "system", Content: rb}
2152+
wrapped := wrapUntrusted(rbCtx, "return_after_break", rb)
2153+
rbMsg := llm.Message{Role: "system", Content: wrapped}
21372154
if insertIdx >= 0 {
21382155
messages = append(messages[:insertIdx+1], append([]llm.Message{rbMsg}, messages[insertIdx+1:]...)...)
21392156
} else {
@@ -2150,20 +2167,6 @@ func continueCmd(args []string) error {
21502167
mm.AppendBuffer("user", task)
21512168
}
21522169

2153-
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
2154-
defer cancel()
2155-
2156-
// Audit: record every untrusted-content ingestion that fires during
2157-
// this turn. The recorder is scoped to the run context so a later turn
2158-
// (or background goroutine) cannot accidentally write to the wrong
2159-
// session's audit log.
2160-
auditStore := session.NewAuditStore(store.Dir())
2161-
currentTurn := sess.Turns + 1
2162-
sessIDCapture := sess.ID
2163-
ctx = loop.WithIngestRecorder(ctx, func(source, content string) {
2164-
_ = auditStore.RecordIngest(sessIDCapture, currentTurn, source, content)
2165-
})
2166-
21672170
rend.Start(task)
21682171
result, allMessages, err := agent.RunWithMessages(ctx, messages)
21692172
if err != nil {

docs/EXTENDED_MEMORY.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ The single most important design decision is the trust boundary.
2929
|---|---|---|
3030
| `user_said` | Trusted | Yes |
3131
| `user_approved` | Trusted | Yes |
32-
| `inferred` | Trusted, but flagged `inferred` | Yes, with lower trust boost |
32+
| `inferred` | Tainted / quarantined | No |
3333
| `tool_output` | Tainted | No |
3434
| `file_read` | Tainted | No |
3535
| `web` | Tainted | No |
3636
| `mcp` | Tainted | No |
3737
| `subagent` | Tainted | No |
3838
| `agent_generated` | Tainted | No |
3939

40-
User inputs are trusted because they come from the operator. Indirect content may be prompt-injected, malicious, or simply noisy, so it is never auto-recalled. Planned promotion paths include inline commands such as:
40+
User inputs and explicitly user-approved atoms are trusted because they come from the operator. Indirect content may be prompt-injected, malicious, or simply noisy, so it is never auto-recalled. `inferred` atoms are treated as tainted: they are quarantined until a user explicitly promotes them, matching the code's `IsTaintedSourceClass` classification. Planned promotion paths include inline commands such as:
4141

4242
- `odek, remember that`
4343
- `odek, remember the browser output`
@@ -183,8 +183,7 @@ retention_score = confidence
183183
184184
decay_factor = 0.5 ^ (age_days / decay_half_life_days)
185185
trust_boost = 1.0 for user_said and user_approved
186-
= 0.8 for inferred
187-
= 0.0 for tainted
186+
= 0.0 for inferred and all tainted source classes
188187
189188
composite_score = 0.6 * cosine_similarity(query_vector, atom_vector)
190189
+ 0.4 * retention_score
@@ -291,8 +290,7 @@ pin_boost = infinity for pinned atoms, 1.0 otherwise
291290
decay_factor = 0.5 ^ (age_days / decay_half_life_days)
292291
293292
trust_boost = 1.0 for user_said and user_approved
294-
= 0.8 for inferred
295-
= 0.0 for tainted
293+
= 0.0 for inferred and all tainted source classes
296294
```
297295

298296
Eviction order:
@@ -422,7 +420,7 @@ The `promote` and `pin` actions for quarantined atoms are reserved for phase P4
422420
Once Extended Memory is enabled, the following proactive behaviors are planned:
423421

424422
- **Return after break**: on session resume, summarize where the user left off and what the next likely step is.
425-
- **Anaphora resolution**: pronouns like "that" or "it" are resolved against recent atoms, not just the last buffer line.
423+
- **Anaphora resolution**: the first pronoun in a user message (e.g. "that" or "it") is resolved against recent trusted atoms only when the top atom's semantic similarity is above `semantic_search_min_score`; otherwise the message is passed through unchanged. Only the first pronoun occurrence is replaced.
426424
- **Follow-up anticipation**: the agent pre-loads related conventions, test patterns, and file references based on predicted intent.
427425
- **Style mirroring**: tone, verbosity, and explanation depth adapt automatically to the user model.
428426

internal/memory/extended/atom.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,12 @@ const (
6464

6565
// TrustBoost returns a multiplicative boost for high-trust source classes.
6666
// External / generated sources receive a zero boost so they cannot be
67-
// recalled without promotion.
67+
// recalled without promotion. Inferred atoms are also untrusted because
68+
// they are not directly user-sourced and are quarantined until promotion.
6869
func TrustBoost(sourceClass string) float32 {
6970
switch sourceClass {
7071
case SourceUserSaid, SourceUserApproved:
7172
return 1.0
72-
case SourceInferred:
73-
return 0.8
7473
default:
7574
return 0.0
7675
}

0 commit comments

Comments
 (0)