Skip to content

Conversation

@sauerdaniel
Copy link
Contributor

@sauerdaniel sauerdaniel commented Jan 13, 2026

Summary

Fix memory leak in State.dispose() where disposed instance keys remain in the parent Map forever.

Fixes #4315
Relates to #3013

Problem

The State namespace uses a two-level Map structure:

  • recordsByKey: Map<string, Map<any, Entry>> - outer Map keyed by instance
  • Inner Map stores state entries for that instance

When State.dispose(key) is called:

  1. It gets the entries Map for the key
  2. Calls dispose on each entry
  3. Calls entries.clear() to empty the inner Map
  4. Bug: Never deletes the key from recordsByKey

This means every disposed instance leaves an empty Map<any, Entry> in recordsByKey, causing unbounded memory growth over the application lifetime.

Solution

Add recordsByKey.delete(key) after entries.clear() to remove the outer Map entry entirely.

entries.clear()
recordsByKey.delete(key)  // <-- Added
await Promise.all(tasks)

Testing

  • ✅ Build passes for all platforms
  • ✅ Full test suite passes (652 tests, 0 failures)
  • Code-review verified to follow correct cleanup pattern

The State.dispose() function clears the entries Map but never deletes
the key from recordsByKey. This causes unbounded memory growth as each
disposed instance leaves an empty Map entry in recordsByKey forever.

Add recordsByKey.delete(key) after entries.clear() to properly clean
up the parent Map entry.
@github-actions
Copy link
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions
Copy link
Contributor

The following comment was made by an LLM, it may be inaccurate:

Based on my search, I found related PRs addressing memory leak issues in disposal patterns, but they appear to focus on different areas:

Potentially Related PRs:

  1. fix(core): add dispose functions to prevent subscription memory leaks #7032 - fix(core): add dispose functions to prevent subscription memory leaks

  2. fix(core): add dispose functions to prevent subscription memory leaks #7914 - fix(core): add dispose functions to prevent subscription memory leaks

  3. fix: clean up pending permissions when session is deleted #7154 - fix: clean up pending permissions when session is deleted

These PRs address similar memory leak patterns in disposal/cleanup scenarios, but none appear to be direct duplicates of PR #8252. PR #8252 is specifically fixing the State.dispose() memory leak by deleting keys from recordsByKey, while the related PRs focus on subscription cleanup and session cleanup in different areas of the codebase.

@sauerdaniel
Copy link
Contributor Author

Duplicate Analysis

The bot mentioned PRs #7032, #7914, and #7154 as potentially related. After investigation:

PR Files Modified Focus
#7032 agent.ts, bus/index.ts, format/index.ts, plugin/index.ts, share-next.ts, share.ts Bus subscription cleanup via dispose() functions
#7914 Same as #7032 (rebased version) Same - Bus subscription cleanup
#7154 permission/next.ts Pending permission cleanup on session delete
#8252 state.ts Map entry cleanup in State.dispose()

Verdict: Not a duplicate.

This PR fixes a different memory leak pattern in a completely different file. The State namespace maintains a recordsByKey Map that tracks state entries per project root. The existing dispose() function calls entries.clear() but never removes the root Map entry itself, causing unbounded growth over time.

The other PRs focus on Bus subscription cleanup and permission cleanup - none touch state.ts or address the Map entry retention issue fixed here.

@rekram1-node
Copy link
Collaborator

/review

@github-actions
Copy link
Contributor

lgtm

@rekram1-node rekram1-node merged commit b68a4a8 into anomalyco:dev Jan 13, 2026
5 checks passed
@rekram1-node
Copy link
Collaborator

@sauerdaniel thank you for finding these tiny details, they are hard to catch and great finds!!!

very very much appreciated and I bet youll find more.

please ping me if I miss ur prs because ur prs so far are better than most relating to mem/resource issues

@sauerdaniel
Copy link
Contributor Author

@rekram1-node thank you very much, AI helps a lot :)

shuv1337 added a commit to Latitudes-Dev/shuvcode that referenced this pull request Jan 15, 2026
* sync

* sync

* Update Nix flake.lock and x86_64-linux hash

* Update aarch64-darwin hash

* fix(TUI): make tui work when OPENCODE_SERVER_PASSWORD is set (anomalyco#8179)

* chore: generate

* wip: black

* docs: Update plan mode restrictions (anomalyco#8290)

* docs: add 302ai provider (anomalyco#8142)

* fix: add missing metadata() and ask() defintions to ToolContext type (anomalyco#8269)

* fix(tui): track all timeouts in Footer to prevent memory leak (anomalyco#8255)

* fix: deduplicate file refs in sent prompts (anomalyco#8303)

* chore: generate

* wip: black

* wip: black

* release: v1.1.17

* add fullscreen view to permission prompt

* chore: generate

* fix(desktop): correct health check endpoint URL to /global/health (anomalyco#8231)

* feat(desktop): Adding Provider Icons (anomalyco#8215)

* chore: generate

* console: reduce desktop download cache ttl to 5 minutes

* fix(github): add persist-credentials: false to workflow templates (anomalyco#8202)

* release: v1.1.18

* fix(desktop): Revert provider icon on select model dialog (anomalyco#8245)

* feat: add Undertale and Deltarune built-in themes (anomalyco#8240)

* chore: generate

* feat(desktop): Ask Question Tool Support (anomalyco#8232)

* fix(mcp): close existing client before reassignment to prevent leaks (anomalyco#8253)

* fix(state): delete key from recordsByKey on instance disposal (anomalyco#8252)

* docs: document ~/.claude/CLAUDE.md compatibility behavior (anomalyco#8268)

* feat: Add GitLab Duo Agentic Chat Provider Support (anomalyco#7333)

Co-authored-by: Aiden Cline <[email protected]>
Co-authored-by: Aiden Cline <[email protected]>

* fix(cli): mcp auth duplicate radio button icon (anomalyco#8273)

* Update Nix flake.lock and x86_64-linux hash

* Update aarch64-darwin hash

* tweak: ensure external dir and bash tool invocations render workdir details

* tweak: external dir permission rendering in tui

* add family to gpt 5.2 codex in codex plugin

* fix(prompt-input): handle Shift+Enter before IME check to prevent stuck state (anomalyco#8275)

* feat: add plan mode with enter/exit tools (anomalyco#8281)

* chore: generate

* test: fix plan agent test path from .opencode/plan/* to .opencode/plans/*

* remove plan

* fix: update User-Agent string to latest Chrome version in webfetch (anomalyco#8284)

* fix: Add Plugin Mocks to Provider Tests (anomalyco#8276)

* chore: generate

* tweak: prompt for explore agent better

* do not allow agent to ask custom-less questions

* release: v1.1.19

* fix(TUI): make tui work when OPENCODE_SERVER_PASSWORD is set (anomalyco#8179)

* chore: generate

* docs: Update plan mode restrictions (anomalyco#8290)

* docs: add 302ai provider (anomalyco#8142)

* fix: add missing metadata() and ask() defintions to ToolContext type (anomalyco#8269)

* fix(tui): track all timeouts in Footer to prevent memory leak (anomalyco#8255)

* fix: deduplicate file refs in sent prompts (anomalyco#8303)

* chore: generate

* chore: generate

* Update Nix flake.lock and x86_64-linux hash

* Update aarch64-darwin hash

* fix(session): skip duplicate system prompt for Codex OAuth sessions (anomalyco#8357)

Co-authored-by: Claude <[email protected]>

* feat: show connected providers in /connect dialog (anomalyco#8351)

* fix(opencode): fix docker image after sst rename in tips (anomalyco#8376)

* chore: generate

* ignore: update download stats 2026-01-14

* docs(prd): mark codex token refresh PR task as complete

- PR created at #298
- Fix includes: write token refresh to openai provider ID, fallback to legacy codex entry
- Tests pass (744 pass, 1 skip, 0 fail)

* test(auth): verify backwards compatibility for OAuth metadata fields

- Add test confirming minimal OAuth results (without optional email, name,
  plan, orgName fields) persist correctly in auth storage
- All new metadata fields are optional in plugin types and auth schema
- Provider auth handler uses conditional checks before setting optional fields
- Verify 745 pass, 1 skip, 0 fail

* chore(prd): mark extractUserInfo tasks as complete

The extractUserInfo function is already implemented in codex.ts:92-105.
It parses id_token, extracts email, name, and accountId from claims,
and returns a UserInfo object. All tests pass.

* chore(prd): mark OAuth callback integration tasks as complete

OAuth callback already calls extractUserInfo(tokens) after token exchange and includes email, name, accountId in the success payload.

* fix(app): file listing  (anomalyco#8309)

* chore(prd): mark normalizePlanType tasks as complete

normalizePlanType function maps plan values to standardized tiers (free, plus, pro, team, enterprise, unknown) and handles case variations.

* feat(server): add GET /auth/info/:providerID endpoint for account metadata

- Added endpoint in server.ts that returns auth metadata (authenticated, type, email, plan, accountId)
- Returns 404 with authenticated: false if provider not found
- Generated openapi.json with new endpoint definition
- Regenerated SDK with auth.info method and types (AuthInfoData, AuthInfoResponses)
- Updated prd.json to mark SDK generation tasks as complete
- Tests pass: 745 pass, 1 skip, 0 fail

* chore(prd): mark PR merge and SDK tasks as complete

- Marked "Merge PR before starting Phase 1" as complete (PR #298 already merged)
- Updated verification steps for auth.info endpoint implementation

* chore(prd): mark legacy codex cleanup task as complete

- Verified migration copies codex to openai, Auth.remove('codex') available for manual cleanup
- Removal is intentionally manual to prevent data loss if migration fails

* chore: mark session complete

* chore(prd): mark background task implementation as complete

- Verified fetchChatGPTUserInfo background task at codex.ts:606-632
- Task spawns via 'void updatePlan().catch(() => {})' - non-blocking
- Updates auth metadata via auth.set when plan/orgName retrieved
- OAuth success returns immediately regardless of background task result

* chore(prd): mark manifest.ts creation as complete

- Verified manifest.ts exists with InstalledSkill and SkillManifest interfaces
- Implements loadManifest, saveManifest, getInstalled, addInstalled, removeInstalled
- Used by skill installer to track installed skills and their metadata

* chore(prd): mark skill index and installer files as complete

- Verified skill/index.ts exists with SkillIndex interface and caching logic
- Implements loadIndex (cache with TTL), buildIndex (fetch and write), search (fuzzysort)
- Verified skill/installer.ts exists with installSkill and uninstallSkill functions
- Copies skill files to SKILLS_DIR and updates manifest with installed metadata

* fix plan mode when not in git worktree

* chore(prd): mark github fetcher as complete

- Verified skill/fetcher/github.ts exists with 200 lines implementing git and tarball fetching
- fetchRegistry() entry point with hasGit() check and tarball fallback
- scanSkills() walks directories matching SKILL.md globs with minimatch
- parseSkillFile() extracts YAML frontmatter for skill metadata

* chore(prd): mark clawdhub and url fetchers as complete

- Verified skill/fetcher/clawdhub.ts exists with stub fetchRegistry returning []
- Verified skill/fetcher/url.ts exists with stub fetchRegistry returning []
- Both are TODO stubs awaiting API contract confirmation and implementation

* fix(ui): layout-bottom icons (anomalyco#8330)

* fix(desktop): "load more" button behavior in desktop sidebar (anomalyco#8430)

* chore: generate

* release: v1.1.20

* sync: record last synced tag v1.1.20

* fix: suppress TS2589 type recursion error in server route chain

---------

Co-authored-by: Frank <[email protected]>
Co-authored-by: Github Action <[email protected]>
Co-authored-by: Leonidas <[email protected]>
Co-authored-by: Eduard Voiculescu <[email protected]>
Co-authored-by: ⌞L⌝ <[email protected]>
Co-authored-by: Daniel M Brasil <[email protected]>
Co-authored-by: Daniel Sauer <[email protected]>
Co-authored-by: Felix Sanchez <[email protected]>
Co-authored-by: opencode <[email protected]>
Co-authored-by: Dax Raad <[email protected]>
Co-authored-by: usvimal <[email protected]>
Co-authored-by: Daniel Polito <[email protected]>
Co-authored-by: Brendan Allan <[email protected]>
Co-authored-by: cmdr-chara <[email protected]>
Co-authored-by: Zeke Sikelianos <[email protected]>
Co-authored-by: Vladimir Glafirov <[email protected]>
Co-authored-by: Aiden Cline <[email protected]>
Co-authored-by: Aiden Cline <[email protected]>
Co-authored-by: Dillon Mulroy <[email protected]>
Co-authored-by: Joe Harrison <[email protected]>
Co-authored-by: Dax <[email protected]>
Co-authored-by: Alan <[email protected]>
Co-authored-by: zerone0x <[email protected]>
Co-authored-by: Claude <[email protected]>
Co-authored-by: Akshar Patel <[email protected]>
Co-authored-by: Goni Zahavy <[email protected]>
Co-authored-by: Filip <[email protected]>
Co-authored-by: Andrew Jazbec <[email protected]>
Co-authored-by: Shane Bishop <[email protected]>
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.

Memory stays unbounded: ACP session map + compacted tool outputs

2 participants