Skip to content

refactor: delegate StateManager knowledge loading to KnowledgeTracker (plan 010)#84

Open
DavertMik wants to merge 6 commits into
mainfrom
refactor/knowledge-single-owner-standalone
Open

refactor: delegate StateManager knowledge loading to KnowledgeTracker (plan 010)#84
DavertMik wants to merge 6 commits into
mainfrom
refactor/knowledge-single-owner-standalone

Conversation

@DavertMik

@DavertMik DavertMik commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Implements plan 010 (plans/010-*.md) — make KnowledgeTracker the single owner of ./knowledge, so StateManager stops running its own duplicate scanner.

The problem

StateManager duplicated the entire knowledge-directory scan (readdir + gray-matter + URL match) that KnowledgeTracker already owns. Worse, KnowledgeTracker itself was new'd in ~10 places (Explorer, ExperienceTracker, Navigator, historians, AddKnowledge, CLI…), each with its own cache — since it derives everything from ConfigParser (app-global) and reads the same persistent files, there is never a reason for more than one.

The fix: singleton

KnowledgeTracker becomes a singleton mirroring ConfigParserprivate constructor + static getInstance() + static resetForTesting(). Every call site uses getInstance(); StateManager drops its constructor param entirely and just calls KnowledgeTracker.getInstance(), delegating getRelevantKnowledge() to it.

Result: one instance, one cache, app-wide. addKnowledge() invalidation is now global, and the injected-param footgun (a missing arg silently spawning a divergent instance) is gone.

Also: StateManager deletes scanKnowledgeFiles/knowledgeCache/lastKnowledgeScan/knowledgeDir; KnowledgeTracker gains a recursive .md scan (parity with the removed scanner) and ${env.*}/${config.*} interpolation. No refresh TTL — knowledge is persistent; load-once + addKnowledge invalidation is sufficient.

Verification

bun run lint clean; bunx tsc --noEmit 724 (= baseline, no new errors in touched files); unit 764/0; integration 63/0. Tests reset the singleton in beforeEach.

🤖 Generated with Claude Code

… (plan 010)

StateManager duplicated the entire knowledge directory scan (readdir + gray-matter
parse + URL match) that KnowledgeTracker already owns — a second scanner with its
own 30s cache, drifting independently. Per the code-ownership map, KnowledgeTracker
is the single owner of reading ./knowledge.

- StateManager takes an injected KnowledgeTracker and delegates getRelevantKnowledge
  to it; deletes scanKnowledgeFiles, knowledgeCache, lastKnowledgeScan, knowledgeDir
  and the config-relative dir resolution. Explorer now constructs the KnowledgeTracker
  and passes the same instance in (one owner, one cache).
- KnowledgeTracker: recursive .md scan (matches the removed scanner), a 30s refresh
  TTL so long sessions see edits, and addKnowledge() invalidates the cache. Knowledge
  interface is exported from KnowledgeTracker and re-exported from StateManager for
  back-compat.

The constructor accepts (but does not act on) `incognito` so Explorer keeps passing
it; the actual incognito wiring is plan 009's ExperienceTracker({ disabled }) change.
This keeps the two PRs independent off main and regression-free in either merge order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DavertMik DavertMik force-pushed the refactor/knowledge-single-owner-standalone branch from 4f92c12 to be2662b Compare July 12, 2026 09:47
@github-actions

github-actions Bot commented Jul 12, 2026

Copy link
Copy Markdown

Explorbot Self-Regression

Commit 3a8dde9 · run

Scenario Result Attempts Duration
basic (native) PASS 2/3 13m
experience: control OK — failed as expected 1/1 2m
experience: seeded PASS 1/3 5m

Attempt details

  • basic (native) attempt 1 — FAIL: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=8, wellFormed=true, keywords=7/3); scenarios: PASS (tests=6/5, features=4/3); tests passed: FAIL (3 passed, 2 failed (reporter: 3 passed, 2 failed))
  • basic (native) attempt 2 — PASS: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=6, wellFormed=true, keywords=7/3); scenarios: PASS (tests=7/5, features=4/3); tests passed: PASS (5 passed, 0 failed (reporter: 5 passed, 0 failed))
  • experience: control attempt 1 — PASS: control: OK — failed as expected (explorbot ran but no test passed)
  • experience: seeded attempt 1 — PASS: seeded: PASS (1 passed, 0 failed)

Session analysis — basic (native):

Test Session Summary

Session Results: 5/5 tests passed (100% success rate)

Test Details:

Test Scenario Result
ET-1 Create new issue via toolbar button ✅ PASSED
ET-2 Search for issue by keyword ✅ PASSED
ET-3 Filter issues by status ✅ PASSED
ET-4 Filter issues by label ✅ PASSED
ET-5 Open issue detail from list ✅ PASSED

Key Achievements:

  1. Issue Creation: Successfully created "Test Issue from Automation" - appeared as clickable link at position tester.ts required fields for open ai models #6
  2. Search Functionality: Search for "dark" correctly filtered to 1 issue
  3. Status Filter: "Open" status filter properly excluded In Progress and Closed issues
  4. Label Filter: Bug label filter worked despite UI challenges
  5. Issue Detail Navigation: Successfully opened issue detail page showing title and description

All pilot verifications confirmed expected behaviors. No remaining expectations or failures to address.

…ngle-owner-standalone

# Conflicts:
#	src/state-manager.ts
Knowledge is persistent. The 30s TTL mirrored the old StateManager scanner's
throttle, but that throttle only existed because the scanner naively re-read the
directory on every getRelevantKnowledge call. KnowledgeTracker loads once
(isLoaded) and addKnowledge() invalidates the single shared instance's cache on
writes, so there is nothing to poll for — the TTL just re-read disk every 30s
for data that doesn't change mid-session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KnowledgeTracker derives everything from ConfigParser (app-global) and reads the
same persistent ./knowledge dir, so there is never a reason for more than one.
The app had ~10 `new KnowledgeTracker()` sites, each with its own cache — the
exact duplication plan 010 set out to remove, and the injected-param on
StateManager was a footgun (a missing arg silently spawned a divergent instance).

Mirror ConfigParser: private constructor + static getInstance() + resetForTesting().
StateManager drops the constructor param and just calls KnowledgeTracker.getInstance();
every other call site (Explorer, ExperienceTracker, Navigator, historians,
AddKnowledge, explorbot, CLI) does the same. One instance, one cache app-wide;
addKnowledge() invalidation is now global. Tests reset the singleton in beforeEach.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DavertMik DavertMik deployed to regression July 12, 2026 10:10 — with GitHub Actions Active
… singleton)

Replaces the KnowledgeTracker.getInstance() singleton with ownership in the DI
container. ExplorBot exposes experienceTracker() / knowledgeTracker() (lazy
||=-cached, matching the agentXxx() factory pattern) and injects both DOWN through
Explorer into new StateManager(experienceTracker, knowledgeTracker) — lower layers
never import the container, so no explorbot<->state-manager import cycle and no
process-global static state.

- KnowledgeTracker reverts to a plain class (no static instance/getInstance/reset).
- ExperienceTracker takes the shared KnowledgeTracker as a constructor arg.
- StateManager takes both trackers injected; keeps its get*Tracker() getters so the
  ~25 consumers reading stateManager.getExperienceTracker() are unchanged. Adds
  getKnowledgeTracker().
- Consumers that used to new-up or getInstance() a tracker now route through the
  shared instance: Navigator/Historian mixins via explorer/stateManager, Captain via
  explorBot.experienceTracker(). Standalone one-shot contexts with no ExplorBot
  (CLI `learn`, standalone AddKnowledge which now takes a knowledgeTracker prop)
  construct their own.
- ExplorBot get{Experience,Knowledge}Tracker() renamed to {experience,knowledge}Tracker().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Navigator already holds `explorer`, and its only caller passed exactly
`explorer.getStateManager().getExperienceTracker()` — so the optional param plus
`|| fallback` was pure redundancy (and the same divergent-instance footgun). Get it
from explorer directly, matching the knowledgeTracker line right above it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant