Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7cfb207
perf(loop): drop trimmed turn groups in one forward scan
jkyberneees Oct 9, 2026
287d386
perf(loop): memoize the redacted effect-evidence body
jkyberneees Oct 9, 2026
600296d
perf(loop): stop copying the pending-dropped queue per digest refresh
jkyberneees Oct 9, 2026
5991987
perf(loop): merge checkpoints into the durable transcript in place
jkyberneees Oct 9, 2026
d14a860
perf(danger): gate injection patterns with required-literal prefilter
jkyberneees Oct 9, 2026
1f59be5
perf(danger): memoize resource-token stat per analysis, hoist regexps
jkyberneees Oct 9, 2026
3f5307b
perf(redact): skip the rewrite for patterns that do not match
jkyberneees Oct 9, 2026
88145d1
perf(skills): hoist matcher tables, index descriptions, memoize local…
jkyberneees Oct 9, 2026
bd7018f
perf(session): build conversation text with strings.Builder
jkyberneees Oct 9, 2026
9502a49
perf(session): skip re-redacting unchanged principal prompts per save
jkyberneees Oct 9, 2026
3d53bef
perf(session): verify the revision with an lstat instead of a full Load
jkyberneees Oct 9, 2026
d205302
perf(session): step checkpoints skip index.json rewrites within a 2s …
jkyberneees Oct 9, 2026
dbd01bb
perf(session): List stats only the entries that fill the page
jkyberneees Oct 9, 2026
a758b8d
perf(memory): reuse parsed atoms.json across List calls
jkyberneees Oct 9, 2026
e611936
perf(session): Cleanup removes vectors in one write, outside the locks
jkyberneees Oct 9, 2026
9990a5e
perf(session): tie the principal-prompt redaction memo to the secret …
jkyberneees Oct 9, 2026
4601a37
perf(runtimelog): skip rewriting the log when nothing expired
jkyberneees Oct 9, 2026
31a3dc8
perf(runtimelog): write queued records in one batch per wake-up
jkyberneees Oct 9, 2026
81302e6
perf(bgproc): reuse one backing array for the output ring
jkyberneees Oct 9, 2026
18f8405
perf(telegram): share one keep-alive client across fallback requests
jkyberneees Oct 9, 2026
0409c2c
perf(telegram): apply progress edits off the agent loop and honour re…
jkyberneees Oct 9, 2026
866e3a0
perf(telegram): probe chat idleness with TryLock instead of a parked …
jkyberneees Oct 9, 2026
b29a218
perf(repl): redraw a bracketed paste once and append history lines
jkyberneees Oct 9, 2026
a41831d
perf(agent): remember accepted AGENTS.md scan verdicts per process
jkyberneees Oct 9, 2026
5e22e1a
perf(resource): bound @-completion walk, honour ctx, no per-file Rel
jkyberneees Oct 9, 2026
f711ebc
perf(resource): probe session ids by streaming instead of full decode
jkyberneees Oct 9, 2026
f3f7c92
perf(events): redact events before taking the emitter lock
jkyberneees Oct 9, 2026
fc020cd
perf: truncate by byte offset instead of []rune conversion
jkyberneees Oct 9, 2026
13ae56e
perf(artifact): bound artifact fields before mapping them
jkyberneees Oct 9, 2026
ae25b6b
perf(mcpclient): decode each stdio response line once
jkyberneees Oct 9, 2026
a0bad27
perf(serve): per-connection WS writer queue, coalesced deltas
jkyberneees Oct 9, 2026
dd9cc8d
perf(serve): prune rate-limiter windows in place, amortise gc
jkyberneees Oct 9, 2026
b189472
perf(serve): cache static asset ETags per handler
jkyberneees Oct 9, 2026
5c8bf98
perf(tools): batch untrusted wrapping and make audit appends O(1)
jkyberneees Oct 9, 2026
3b806c2
perf(tools): build the diff LCS table only over the changed region
jkyberneees Oct 9, 2026
e3e4567
perf(tools): stat once per file when sorting search results, share sc…
jkyberneees Oct 9, 2026
3f28b43
perf(tools): skip hashing for windowed read_file reads
jkyberneees Oct 9, 2026
8c351c7
perf(tools): stop session_search deep scan once the limit is outranked
jkyberneees Oct 9, 2026
fc4d92f
perf(tools): classify the shell script gate once before approval
jkyberneees Oct 9, 2026
619027a
perf(loop): key the effect-evidence memo on the redaction generation
jkyberneees Oct 9, 2026
678b745
perf(session): read the registry generation before redacting, distrus…
jkyberneees Oct 9, 2026
79d0665
perf(serve): initialise the WS writer space channel at construction
jkyberneees Oct 9, 2026
ca70d9d
perf(agent): key the AGENTS.md scan cache on values and cache local v…
jkyberneees Oct 9, 2026
08a558b
perf(telegram): parse retry_after tolerantly and bound the progress f…
jkyberneees Oct 9, 2026
da3d6ab
perf(repl): read the bracketed-paste terminator in full
jkyberneees Oct 9, 2026
bc23e55
perf(tools): keep per-element scanning for a sidecar guard; document …
jkyberneees Oct 9, 2026
0c28776
build: ignore the odek binary built inside cmd/odek
jkyberneees Oct 9, 2026
54b7e1e
perf(bgproc): saturate the ring growth arithmetic
jkyberneees Oct 9, 2026
b2d80c2
perf(bgproc,danger): bound ring sizes by a constant; make the scan sp…
jkyberneees Oct 9, 2026
2eaf6f2
perf(guard,telegram): decide batching and memo eligibility by the gua…
jkyberneees Oct 9, 2026
9c17818
perf(bgproc): clamp the ring growth operands where the size is computed
jkyberneees Oct 9, 2026
567dd8f
perf(tools,serve,telegram): code-review follow-ups
jkyberneees Oct 9, 2026
d119642
docs(security): describe chunked audit ingests, sidecar per-element s…
jkyberneees Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ docs/INTELLIGENCE.md
go.work
go.work.sum

.docs-review/
.docs-review/
# build artifact from `go build ./cmd/odek` run inside the package dir
/cmd/odek/odek
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ ReAct cycle: observe → think → act → repeat.
- **Interaction modes** — engaging (narrated), enhance (persistent), verbose (raw), off.
- Max 90 iterations by default. On iteration-budget exhaustion the engine makes one final tool-less LLM call for a partial-progress summary (30s bound), returned marked `[Iteration budget reached — partial summary]`.
- **Post-response async processing** — episode extraction and extended-memory extraction run in background goroutines; `Agent.Close` drains them (~15s bound). Pending memory episodes can be promoted or discarded from the WebUI and REST API.
- **Per-turn session persistence** — `SetMessagesPersistCallback` fires after each completed step with a fresh snapshot; CLI/REPL/serve/Telegram wire it to `Store.SaveNoIndex` (atomic, skips remote vector indexing). Interrupted runs resume via `odek continue` from the last completed step; error paths persist partial history minus dangling tool calls. The final save per turn still updates the semantic index once.
- **Per-turn session persistence** — `SetMessagesPersistCallback` fires after each completed step with a fresh snapshot; CLI/REPL/serve/Telegram wire it to `Store.SaveNoIndex` (atomic, skips remote vector indexing; leaves `index.json` alone while only UpdatedAt/tokens moved by <2s — the end-of-turn save always refreshes it). Interrupted runs resume via `odek continue` from the last completed step; error paths persist partial history minus dangling tool calls. The final save per turn still updates the semantic index once.
- **Storage maintenance janitor** — `maintenance.Start` sweeps `~/.odek` (retention, log rotation, media sweep) inside `odek telegram`/`serve`/`schedule daemon`; `odek cleanup [--dry-run]` runs it on demand. Session files are trimmed at write time when they would exceed `MaxSessionFileBytes`. Operator-only config. See docs/MAINTENANCE.md.
- **Artifact-aware file search** — `search_files` skip `node_modules`, `vendor`, `.git`, `__pycache__`, `.venv`, etc.
- **Semantic session search** — `session_search` tool: go-vector RandomProjections + k-NN, two-tier (vector index → exhaustive fallback).
Expand Down
60 changes: 60 additions & 0 deletions cmd/odek/bg_telegram_idle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package main

import (
"testing"
"time"
)

func chatWaiters(chatID int64) int {
chatMetaMu.Lock()
defer chatMetaMu.Unlock()
if s := chatSlots[chatID]; s != nil {
return s.waiters
}
return 0
}

// A busy chat must not leave a goroutine parked on its mutex after the
// probe times out: the only waiter left is the turn that holds the slot.
func TestRED_Telegram_ChatIsIdleLeavesNoParkedWaiter(t *testing.T) {
resetChatMutexes()
const chat = int64(4242)
slot := pinChat(chat)
slot.mu.Lock()
if chatIsIdle(chat, 30*time.Millisecond) {
t.Fatal("busy chat reported idle")
}
time.Sleep(20 * time.Millisecond)
if w := chatWaiters(chat); w != 1 {
t.Fatalf("waiters=%d after timed-out probe, want 1 (holder only)", w)
}
unpinChat(chat, slot)
if chatMutexCount() != 0 {
t.Fatal("slot leaked")
}
}

func TestChatIsIdleWhenFreeAndReleasesSlot(t *testing.T) {
resetChatMutexes()
const chat = int64(4243)
if !chatIsIdle(chat, 50*time.Millisecond) {
t.Fatal("free chat reported busy")
}
if chatMutexCount() != 0 {
t.Fatal("probe leaked the slot")
}
}

func TestChatIsIdleBecomesTrueWhenTurnEndsWithinWait(t *testing.T) {
resetChatMutexes()
const chat = int64(4244)
slot := pinChat(chat)
slot.mu.Lock()
go func() {
time.Sleep(40 * time.Millisecond)
unpinChat(chat, slot)
}()
if !chatIsIdle(chat, 2*time.Second) {
t.Fatal("probe missed a turn that ended within the wait")
}
}
24 changes: 12 additions & 12 deletions cmd/odek/bg_telegram_wake.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,17 +237,17 @@ func (c *tgWakeController) wakeSpend() int {
// chatIsIdle reports whether the chat's turn slot can be taken within wait
// (i.e. no agent turn is running). The probe releases the slot immediately.
func chatIsIdle(chatID int64, wait time.Duration) bool {
acquired := make(chan struct{}, 1)
go func() {
slot := pinChat(chatID)
slot.mu.Lock()
acquired <- struct{}{}
unpinChat(chatID, slot)
}()
select {
case <-acquired:
return true
case <-time.After(wait):
return false
slot := pinChat(chatID)
deadline := time.Now().Add(wait)
for {
if slot.mu.TryLock() {
unpinChat(chatID, slot)
return true
}
if !time.Now().Before(deadline) {
dropChatPin(chatID, slot)
return false
}
time.Sleep(5 * time.Millisecond)
}
}
15 changes: 12 additions & 3 deletions cmd/odek/browser_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,13 +492,22 @@ func parseHTML(ctx context.Context, html, pageURL string, status int) browserSna

// Title, element text, and link URLs come from the page — wrap them as
// untrusted content so a hostile `href` cannot inject instructions.
snap.Title = wrapUntrusted(ctx, pageURL, snap.Title)
// The title, element texts and link URLs are scanned and audit-recorded
// as one batch; each still gets its own wrapper.
fields := []*string{&snap.Title}
for i := range snap.Elements {
snap.Elements[i].Text = wrapUntrusted(ctx, pageURL, snap.Elements[i].Text)
fields = append(fields, &snap.Elements[i].Text)
if snap.Elements[i].Type == "link" && snap.Elements[i].URL != "" {
snap.Elements[i].URL = wrapUntrusted(ctx, pageURL, snap.Elements[i].URL)
fields = append(fields, &snap.Elements[i].URL)
}
}
contents := make([]string, len(fields))
for i, f := range fields {
contents[i] = *f
}
for i, w := range wrapUntrustedBatch(ctx, pageURL, nil, contents) {
*fields[i] = w
}
// Keep the raw page URL itself unwrapped for internal navigation; it is
// wrapped at the result-output boundary in doNavigate/doSnapshot.

Expand Down
157 changes: 157 additions & 0 deletions cmd/odek/diff_compute_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package main

import (
"fmt"
"math/rand"
"reflect"
"runtime"
"testing"
)

// computeDiffReference is the original dense-LCS implementation, kept so the
// optimised computeDiff can be checked for identical output.
func computeDiffReference(a, b []string) []diffHunk {
m, n := len(a), len(b)
lcs := make([][]int, m+1)
for i := range lcs {
lcs[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if a[i-1] == b[j-1] {
lcs[i][j] = lcs[i-1][j-1] + 1
} else if lcs[i-1][j] >= lcs[i][j-1] {
lcs[i][j] = lcs[i-1][j]
} else {
lcs[i][j] = lcs[i][j-1]
}
}
}

var hunks []diffHunk
i, j := m, n

var equalLines, addedLines, removedLines []diffLine

flushHunk := func() {
if len(equalLines) > 0 {
hunks = append(hunks, diffHunk{Type: "equal", Lines: equalLines})
equalLines = nil
}
if len(removedLines) > 0 {
hunks = append(hunks, diffHunk{Type: "removed", Lines: removedLines})
removedLines = nil
}
if len(addedLines) > 0 {
hunks = append(hunks, diffHunk{Type: "added", Lines: addedLines})
addedLines = nil
}
}

for i > 0 || j > 0 {
if i > 0 && j > 0 && a[i-1] == b[j-1] {
flushHunk()
equalLines = append([]diffLine{{OldLine: i, NewLine: j, Content: a[i-1]}}, equalLines...)
i--
j--
} else if j > 0 && (i == 0 || lcs[i][j-1] >= lcs[i-1][j]) {
addedLines = append([]diffLine{{NewLine: j, Content: b[j-1]}}, addedLines...)
j--
} else if i > 0 {
removedLines = append([]diffLine{{OldLine: i, Content: a[i-1]}}, removedLines...)
i--
}
}
flushHunk()

for i, k := 0, len(hunks)-1; i < k; i, k = i+1, k-1 {
hunks[i], hunks[k] = hunks[k], hunks[i]
}
return hunks
}

func TestComputeDiff_MatchesReference(t *testing.T) {
rng := rand.New(rand.NewSource(7))
check := func(a, b []string) {
t.Helper()
got, want := computeDiff(a, b), computeDiffReference(a, b)
if !reflect.DeepEqual(got, want) {
t.Fatalf("diff mismatch\na=%q\nb=%q\ngot =%+v\nwant=%+v", a, b, got, want)
}
}
// Hand-picked tie cases: repeated lines make the LCS ambiguous.
check(nil, nil)
check([]string{"x"}, nil)
check(nil, []string{"x"})
check([]string{"x", "x"}, []string{"x"})
check([]string{"x"}, []string{"x", "x"})
check([]string{"a", "x", "x"}, []string{"a", "x"})
check([]string{"x", "y", "x"}, []string{"x", "x"})
check([]string{"a", "b", "c"}, []string{"a", "b", "c"})
check([]string{"a", "b", "c"}, []string{"c", "b", "a"})
// Random corpus over tiny alphabets (many ties, shared prefixes/suffixes).
for iter := 0; iter < 4000; iter++ {
alpha := 1 + rng.Intn(4)
gen := func() []string {
l := rng.Intn(14)
s := make([]string, l)
for i := range s {
s[i] = string(rune('a' + rng.Intn(alpha)))
}
return s
}
a := gen()
var b []string
if rng.Intn(2) == 0 {
// Mutate a: keeps long common prefix/suffix.
b = append([]string(nil), a...)
for k := rng.Intn(3); k >= 0 && len(b) > 0; k-- {
pos := rng.Intn(len(b))
switch rng.Intn(3) {
case 0:
b = append(b[:pos], b[pos+1:]...)
case 1:
b = append(b[:pos], append([]string{string(rune('a' + rng.Intn(alpha)))}, b[pos:]...)...)
default:
b[pos] = string(rune('a' + rng.Intn(alpha)))
}
}
} else {
b = gen()
}
check(a, b)
}
}

func TestRED_ComputeDiff_NearIdenticalFilesStayCheap(t *testing.T) {
const n = 3000
a := make([]string, n)
b := make([]string, n)
for i := range a {
a[i] = fmt.Sprintf("line %d", i)
b[i] = a[i]
}
b[n/2] = "changed"
var before, after runtime.MemStats
runtime.ReadMemStats(&before)
hunks := computeDiff(a, b)
runtime.ReadMemStats(&after)
if alloc := after.TotalAlloc - before.TotalAlloc; alloc > 8<<20 {
t.Fatalf("diff of near-identical %d-line files allocated %d MiB; want under 8", n, alloc>>20)
}
if !reflect.DeepEqual(hunks, computeDiffReference(a, b)) {
t.Fatal("output differs from reference")
}
}

func TestComputeDiff_LongRunOfAdditionsIsLinear(t *testing.T) {
a := []string{"head", "tail"}
b := []string{"head"}
for i := 0; i < 2000; i++ {
b = append(b, fmt.Sprintf("new %d", i))
}
b = append(b, "tail")
if !reflect.DeepEqual(computeDiff(a, b), computeDiffReference(a, b)) {
t.Fatal("output differs from reference")
}
}
Loading
Loading