Skip to content

feat(ego-browser): reclaim idle agent task spaces to release renderers - #271

Open
Vitus213 wants to merge 1 commit into
citrolabs:mainfrom
Vitus213:feat/idle-taskspace-reclaim
Open

feat(ego-browser): reclaim idle agent task spaces to release renderers#271
Vitus213 wants to merge 1 commit into
citrolabs:mainfrom
Vitus213:feat/idle-taskspace-reclaim

Conversation

@Vitus213

Copy link
Copy Markdown

What

Add lazy idle task-space reclamation so agent task spaces left by earlier sessions — and their renderer processes — get released automatically instead of piling up.

Closes #270.

Why

Today completeTaskSpace is the only release path and it is 100% agent-driven; abandoned sessions leak renderer processes and swap climbs monotonically (see #270 for 26h measurements: 20→58 helper processes, 10→35 GB swap). The harness kept no cross-process activity state and listTaskSpaces carries no timestamp, so there was nothing to time out against.

How to verify

cd package/ego-browser
npm ci
npm test    # 312 pass / 0 fail

Behavior covered by the new taskspace-reclaim.test.mjs:

  • a space idle past EGO_RECLAIM_IDLE_S (default 7200s) is closed; a fresh one is kept
  • ownership === "user" is never touched
  • hard cap (EGO_RECLAIM_MAX_SPACES) reclaims oldest-first when over the cap
  • EGO_RECLAIM_DISABLE=1 is a no-op
  • first-sight seeding: spaces with no activity record are seeded with now and skipped (deploy safety — never mass-closes pre-existing / in-use spaces); they only become reclaimable after the idle window
  • a seeded space is reclaimed on a later run once idle
  • no ego runtime → empty result, no throw

Manual (against a live browser):

ego-browser nodejs <<'EOF'
const t = await taskSpaces.useOrCreate('reclaim-verify');   // triggers the once-per-process reap
console.log(JSON.stringify({ id: t.id, spaces: (await listTaskSpaces()).length }));
await taskSpaces.complete('reclaim-verify', { keep: false });
EOF

Impact

  • Helper surface: no new public helper. Reclamation is an internal mechanism triggered from run.ts execute() (CLI mode) and helpers.useOrCreateTaskSpace() (embedded app-SDK mode), collapsed to once-per-process via state.reclaimDone.
  • Agent side: no change required. Agents that already call completeTaskSpace behave identically; agents that forget it now have a safety net.
  • Existing tests: taskspace-e2e.test.mjs and helpers.test.mjs set EGO_RECLAIM_DISABLE=1 in their harness so call-sequence assertions stay deterministic. New suites: taskspace-activity.test.mjs, taskspace-reclaim.test.mjs.
  • Config: env-gated, default on (idle 2h, cap 8). EGO_RECLAIM_DISABLE=1 opts out.

Files

  • src/taskspace-activity.ts (new) — activity map + atomic persistence + config
  • src/taskspace-reclaim.ts (new) — reclaim logic; talks to globalThis.ego directly (no helpers import → no cycle); once-per-process guard
  • src/helpers.tsselectTaskSpace touches activity (gated by disabled); useOrCreateTaskSpace triggers reclaim (CLI+app-SDK coverage)
  • src/run.tsexecute() triggers reclaim (CLI coverage)
  • src/state.tsreclaimDone once-guard
  • AGENTS.md — Task Spaces section documents the mechanism + env

Open questions for reviewers

  1. Are the default thresholds (2h idle / 8 cap) right for the project, or should reclamation be opt-in by default?
  2. useOrCreateTaskSpace as the app-SDK trigger point — acceptable, or prefer a different hook?

Agent task spaces left by earlier sessions were never auto-closed; their tabs and renderer processes piled up across heredoc/omp rounds (observed ~58 ego helper processes and ~35GB swap over 26h), exhausting memory on long-running setups.

Add a lazy idle reaper:

- taskspace-activity.ts: persist {spaceId->lastTouchedAt} atomically (tmp+rename) to EGO_RECLAIM_STATE_FILE; helpers.selectTaskSpace touches it per use (gated by EGO_RECLAIM_DISABLE).
- taskspace-reclaim.ts: reclaimIdleTaskSpaces() closes agent-owned spaces idle past EGO_RECLAIM_IDLE_S (default 7200s) or over EGO_RECLAIM_MAX_SPACES (default 8). First-sight seeding skips spaces with no record (created before this feature shipped, or in use by a concurrent session) so a fresh deploy never mass-closes them; only ownership==agent is ever reclaimed.
- run.ts execute() and helpers.useOrCreateTaskSpace() each trigger it once per process (state.reclaimDone guard), covering both CLI and embedded app SDK modes.

Env: EGO_RECLAIM_IDLE_S / EGO_RECLAIM_MAX_SPACES / EGO_RECLAIM_DISABLE / EGO_RECLAIM_STATE_FILE. e2e/helpers tests set EGO_RECLAIM_DISABLE=1 to keep call sequences deterministic; dedicated suite in taskspace-{activity,reclaim}.test.mjs.

Verified: npm test 312 pass / 0 fail.
@Vitus213

Copy link
Copy Markdown
Author

@section9-lab @WUXM5 Hi — this PR has been open for a few days with no review. It reclaims idle agent task spaces so their renderer processes are released, cutting memory footprint when agents park between rounds. Happy to adjust scope or add tests if needed — just let me know what would help move this forward. Thanks!

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.

Idle agent task spaces are never reclaimed — renderer processes and swap pile up across sessions

1 participant