Skip to content

fix(crew): key the task registry by task, not by name - #117

Closed
CaptainAni187 wants to merge 1 commit into
smallest-inc:mainfrom
CaptainAni187:fix_task_registry_name_collision
Closed

CaptainAni187 wants to merge 1 commit into
smallest-inc:mainfrom
CaptainAni187:fix_task_registry_name_collision

Conversation

@CaptainAni187

@CaptainAni187 CaptainAni187 commented Sep 12, 2026

Copy link
Copy Markdown

TaskManager keys its registry by task name, and task names are not unique.

CrewSession builds a handler task's name from the session, the event and the handler function:

task_name = f"{self.name}::{event_name}::{handler.__name__}::handler"

That is fully deterministic, so the same event firing twice while the first handler is still running produces two live tasks with identical names. _add_task then overwrites:

name = task_data.task.get_name()
self._tasks[name] = task_data

and _task_done_handler deletes by that same shared name:

if name in self._tasks:
    del self._tasks[name]

So the second task evicts the first from the registry, and when the first finishes it deletes the entry belonging to the second, which is still running. The live task is then invisible to current_tasks().

Reproducing

import asyncio
from smallestai.atoms.crew.task_manager import TaskManager, TaskManagerParams

async def main():
    tm = TaskManager()
    tm.setup(TaskManagerParams(loop=asyncio.get_running_loop()))

    first_done = asyncio.Event()
    async def quick():
        await asyncio.sleep(0.05)
        first_done.set()
    async def slow():
        await asyncio.sleep(10)

    name = "sess::on_message::handle::handler"
    t1 = tm.create_task(quick(), name=name)
    t2 = tm.create_task(slow(), name=name)
    print("registry size:", len(tm.current_tasks()), "| t1 running:", not t1.done())

    await first_done.wait()
    await asyncio.sleep(0.05)
    print("registry size:", len(tm.current_tasks()), "| t2 running:", not t2.done(),
          "| t2 tracked:", t2 in tm.current_tasks())

asyncio.run(main())

Before:

registry size: 1 | t1 running: True
registry size: 0 | t2 running: True | t2 tracked: False

After:

registry size: 2 | t1 running: True
registry size: 1 | t2 running: True | t2 tracked: True

The registry reports zero tasks while one is still running.

Scope, honestly

CrewSession._cleanup keeps its own _event_tasks set holding the task objects, so handler tasks are still cancelled on teardown and I am not claiming they leak. What is wrong is the registry itself: current_tasks() is documented as "the list of currently created/registered tasks" and it under-reports, down to zero while work is in flight. Anything that trusts it as the source of truth, including the Waiting for N tasks to complete line in cleanup, is working from a short count.

The change

Key _tasks by the task object, which is unique and hashable, instead of by its name. _add_task and _task_done_handler follow. Nothing else about the manager changes: names are still set on the task and still used for logging.

One line in session.py read self.task_manager._tasks.keys() to log task names, reaching into the registry's internals. It now takes the names from current_tasks(), which is the public accessor and stays correct whatever the keys are.

Tests

tests/custom/test_crew_task_registry.py, five cases. Two fail on main: both tasks sharing a name are registered, and one finishing does not unregister its namesake. The other three pin the behaviour the fix must not break, a finished task still being pruned, cancel_task still unregistering, and distinct names being unaffected.

src/smallestai/atoms/crew/** is .fernignored, so a regen keeps this.

Task names are not unique. CrewSession derives a handler task's name from the
session, the event and the handler function, so the same event firing twice while
the first handler still runs produces two live tasks with identical names.

Keyed by name, the second evicted the first from the registry, and then the first
task's done handler deleted the second entry by that shared name, so a running
task disappeared from current_tasks() entirely. Key by the task object instead,
and have the session log the names it gets from current_tasks() rather than
reaching into the registry's keys.
@crim-app

crim-app Bot commented Sep 12, 2026

Copy link
Copy Markdown

crim doesn't review pull requests automatically here.

Comment crim review on this pull request whenever you want a review.

@CaptainAni187

Copy link
Copy Markdown
Author

crim review

@crim-app

crim-app Bot commented Sep 12, 2026

Copy link
Copy Markdown

crim is reviewing this pull request. Findings will be posted shortly.

@crim-app

crim-app Bot commented Sep 12, 2026

Copy link
Copy Markdown

Good to merge

The registry finally stopped mistaking two tasks for one just because they wore the same name badge — and it brought receipts in the form of five tests. Clean fix, ship it.

@crim-app crim-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

One-sentence assessment: A small, correct bug fix that keys the task registry by the task object rather than its non-unique name, backed by solid regression tests.

What this PR does

Task names in CrewSession are not unique (they are derived from session/event/handler), so two concurrent handlers could share a name. Keying TaskManager._tasks by name let the second task evict the first, and the first's done-callback then deleted the still-running second task's entry — losing it from current_tasks(). This PR changes the registry to Dict[asyncio.Task, TaskData], updates _add_task/_task_done_handler accordingly, and fixes session.cleanup to log names from the live task list. New tests cover the collision, pruning, and cancellation paths.

Findings

No issues found.

@abhishekmishragithub

Copy link
Copy Markdown
Collaborator

Superseded by #120 (re-homed onto an upstream branch so CI could run; your commit is included with authorship preserved, and the test type-check gaps for #112/#118 were fixed there). Shipping in 5.12.1.

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.

2 participants