# Glue Task Manager

> Glue TaskManager Scheduling

- Skill: `vchelaru/glue-task-manager` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/glue-task-manager`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/glue-task-manager/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: vchelaru (https://skillmd.com/u/vchelaru)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vchelaru/glue-task-manager

---


# Glue TaskManager Scheduling

`FRBDK/Glue/Glue/Tasks/TaskManager.cs` runs almost all project-mutating work through one dedicated
thread and a priority queue (`taskQueue`), not a FIFO queue. `glue-unit-test-bootstrap`'s "Sibling seams"
section covers the `SynchronousMode` test seam that collapses this to inline sequential execution —
which is why ordering bugs here are invisible under that mode and need a real (non-synchronous) queue
to pin.

## Tiers beat enqueue order

`TaskExecutionPreference` (`Asap` / `Fifo` / `AddOrMoveToEnd`) are fixed numeric tiers far apart
(`0`, `ulong.MaxValue/3`, `2*ulong.MaxValue/3`); `taskoffset` only breaks ties *within* a tier. A `Fifo`
task queued after an `AddOrMoveToEnd` task still runs first, unconditionally. `GenerateElementCode`
(`GenerateCodeCommands.cs`) deliberately queues its file-write + `.csproj`-registration work at
`AddOrMoveToEnd` to keep the UI responsive — so any operation that depends on that codegen having
already landed (moving or deleting the files it wrote) must also be `AddOrMoveToEnd`, or a default-`Fifo`
call queued later can still run first.

## Coalescing keys off DisplayInfo, which can be stale

`AddOrMoveToEnd`'s de-dup cancels an older pending task sharing the same `EffectiveId`
(`CustomId ?? DisplayInfo`). A `DisplayInfo` built from mutable state (e.g. `element.ToString()` →
`Name`) gives two logically-identical requests different ids across a mutation, so they never coalesce.
Also: `AddOrRunIfTasked`'s "already in a task, run inline" fast path explicitly excludes
`AddOrMoveToEnd` — it always goes through the real queue.

## A same-tier task queued early can still surface last

`GenerateElementCode` on a screen with N derived screens synchronously enqueues one `AddOrMoveToEnd`
task per derived screen the moment it runs - so any other `AddOrMoveToEnd` call made around the same
time (e.g. a plugin's own error-refresh reacting to the same property change) ends up dequeued after
all N of them, even though it was queued just as early. This is "tiers beat enqueue order" above,
just easy to miss because the delay looks like *your* call ran late, not like a same-tier burst beat
it. `RefreshCommands.RefreshErrorsFor` (`RefreshCommands.cs`) takes an optional `executionPreference`
for exactly this: pass `Fifo` (or anything but `AddOrMoveToEnd`) from a reaction that must show up
promptly, and - since `AddOrRunIfTasked`'s inline fast path excludes only `AddOrMoveToEnd` - if you're
already inside a task, it runs immediately instead of queueing at all. Conversely, a reaction that
must wait until codegen has actually landed still wants `AddOrMoveToEnd` (see above).

## The nested-task fast path checks `TaskManager.Self`, not the calling instance

`AddOrRunIfTasked`'s `IsInTask()` gate is hardcoded to `TaskManager.Self.SyncTaskThreadId`, not
`this.SyncTaskThreadId` — so a `TaskManager.Self.AddAsync(...)` call made from inside an already-running
task's callback (any preference except `AddOrMoveToEnd`) runs inline via `RunTask(...).Wait()` instead of
re-entering the priority queue, meaning its own `TaskExecutionPreference` argument is inert. In production
this is safe because `StartDoTaskManagerLoop` wraps the dedicated task thread in Nito.AsyncEx's
`AsyncContext.Run`, which pins every `await` continuation started within it back to that same thread — so
`IsInTask()` still sees the same thread ID after any number of nested `await`s. This can't be observed
under `GlueUnitTests`: `GlueTestBootstrap` forces `SynchronousMode = true` before `TaskManager.Self` is
ever constructed, which permanently fixes that one singleton instance's `SyncTaskThreadId` to whatever
thread first touched it — no later toggle of `TaskManager.SynchronousMode` (e.g. in
`TaskManagerSynchronousModeTests`) un-fixes it, so nested calls always look "in task" there regardless of
what the real threaded model would do.

## An awaited task holds the whole queue

`DoTaskManagerLoop` awaits `RunTask(...)` before its next `taskQueue.TryTake`, so a task keeps its slot
across every `await` inside it and nothing else is dequeued until it completes. I/O awaited from inside
a task — `GameCommunicationPlugin`'s socket round trips, each with a 10-second ceiling — stalls all
queued project work for its full duration.

