Skip to content

BACK-627 - Prevent forced allocation refresh from joining an in-flight stale fetch - #925

Open
jafigueroam94 wants to merge 12 commits into
MrLesk:mainfrom
jafigueroam94:tasks/back-627-force-allocation-refresh
Open

BACK-627 - Prevent forced allocation refresh from joining an in-flight stale fetch#925
jafigueroam94 wants to merge 12 commits into
MrLesk:mainfrom
jafigueroam94:tasks/back-627-force-allocation-refresh

Conversation

@jafigueroam94

Copy link
Copy Markdown

Summary

Follow-up from the PR #899 (BACK-624) verification round. The task-ID allocation path forces a remote-ref refresh past the normal 60s lease, but a forced refresh arriving while a non-forced fetch is already in flight simply joined that fetch and returned — a push landing during the remaining fetch duration was invisible to the allocation, allowing a duplicate numeric ID (window typically under 2s, capped at 10s).

  • Core.refreshRemoteRefsForTaskRead (src/core/backlog.ts) now tracks when the in-flight refresh it joins actually started (remoteRefRefreshStartedAt).
  • On a forced call, if the joined refresh started before the request, it loops to trigger one more fetch after that refresh resolves, instead of returning immediately.
  • No extra fetch is issued when no refresh is in flight — the existing single-fetch behavior is unchanged.

Closes BACK-627.

Test plan

  • New regression test in src/test/core-task-corpus-regressions.test.ts reproduces the push-during-in-flight-fetch race with a gated git.fetch mock (real fetch data capture happens before the push, resolution withheld until released) driving a concurrent forced generateNextId() call
  • Confirmed the new test fails on pre-fix code (allocates a colliding TASK-2) and passes post-fix (allocates TASK-3 via exactly 2 fetches)
  • Existing "allocates past a remote task pushed inside the read refresh window" test still asserts exactly 1 fetch when nothing is in flight
  • bunx tsc --noEmit clean
  • bun run check . clean (391 files)
  • bun run test — 2395 pass / 6 pre-existing skips / 0 fail across 250 files

@MrLesk

MrLesk commented Aug 24, 2026

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e58b5635e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/web/components/TaskDetailsModal.tsx Outdated
...(isCreateMode && assignee.length === 0 && createModeAssignee.length === 0 ? {} : { assignee }),
labels,
priority: priority === "" ? undefined : priority,
project: project === "" ? undefined : project,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Pass project through the web task mutation handlers

When a user selects a project in the browser, this value is sent to the real POST/PUT endpoints, but BacklogServer.handleCreateTask does not include payload.project in createTaskFromInput, and handleUpdateTask never copies updates.project into updateInput. Consequently neither creating nor editing a task through the shipped web UI persists the selection; the component tests miss this because they mock apiClient. Add project handling to both server mutation paths.

Useful? React with 👍 / 👎.

Comment thread src/cli.ts
)
.option("--priority <priority>", "set task priority (configured priorities)")
.option("--type <type>", "set task type (configured task types; pass an empty value to clear)")
.option("--project <project>", "set task project (configured projects; pass an empty value to clear)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat --project as a direct CLI field flag

In an interactive TTY, backlog task edit TASK-1 --project Web still satisfies !hasEditFieldFlags(options) because that detector was not updated for the new option, so the command opens the full wizard and ignores the requested flag instead of applying it directly. hasCreateFieldFlags has the same omission, causing a title-less create with --project to enter the wizard rather than follow the other explicit-field behavior; add options.project !== undefined to both detectors.

AGENTS.md reference: AGENTS.md:L67-L72

Useful? React with 👍 / 👎.

Comment thread src/core/backlog.ts Outdated
const joinedRefreshStartedAt = this.remoteRefRefreshStartedAt;
await this.remoteRefRefreshPromise;

if (options?.force !== true || joinedRefreshStartedAt >= requestedAt) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use an ordered refresh generation instead of Date.now

If a forced allocation request arrives after the stale fetch starts but within the same Date.now() millisecond, joinedRefreshStartedAt equals requestedAt, so this condition treats the older fetch as sufficiently fresh and returns without the follow-up fetch. A push occurring during that fetch can therefore remain invisible and still produce the duplicate ID this change is intended to prevent; track refresh/request ordering with a monotonically incremented generation or promise identity rather than millisecond timestamp comparison.

Useful? React with 👍 / 👎.

Comment thread src/web/components/ProjectBadge.tsx Outdated
Comment on lines +36 to +38
const ProjectBadge: React.FC<ProjectBadgeProps> = ({ project, availableProjects, className = '' }) => {
const label = project?.trim();
if (!label) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Hide project badges when projects are unconfigured

When availableProjects is empty but a task still has persisted project frontmatter—for example after removing the projects list from config—this component continues rendering the badge because it only checks the task value. The rest of the new web project controls disappear in that state, leaving visible project UI that cannot be edited or filtered; return null when no projects are configured, as the select and filter already do.

Useful? React with 👍 / 👎.

Comment thread src/cli.ts
cleanup();
return;
}
filters.project = canonicalProjects;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Scope project-filtered search in every output mode

When --project is supplied without an explicit result --type, types has already been calculated at lines 2058-2070 and remains task/document/decision because only modified-file and task-type filters trigger task-only scoping. SearchService applies project filtering only to tasks, so plain and JSON output append every document and decision; the interactive path additionally replaces the filtered task set with allTasks and omits project from the runUnifiedView filter. Parse projects before result-type selection, treat them like --task-type, and pass the project filter into the interactive view.

AGENTS.md reference: AGENTS.md:L67-L72

Useful? React with 👍 / 👎.

Comment thread src/cli.ts
Comment on lines +2689 to +2691
if (baseFilters.project) {
const projects = Array.isArray(baseFilters.project) ? baseFilters.project : [baseFilters.project];
activeFilters.push(`Project: ${projects.join(", ")}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Seed the interactive task list with the project filter

In a TTY, backlog task list --project Web records this project only in the displayed filter banner, while initialUnifiedFilter at lines 2702-2733 has no project property and the interactive loader deliberately loads the unfiltered corpus. The resulting TUI therefore shows every task under a title claiming Project: Web; include baseFilters.project in the unified filter just as the type filter is included.

AGENTS.md reference: AGENTS.md:L67-L72

Useful? React with 👍 / 👎.

- `backlog task list --search "desktop app" --labels frontend,bug --limit 20 --plain`

Avoid broad unfiltered listing when the project may have many tasks. Use `--status`, `--exclude-status`, `--type`, `--assignee`, `--unassigned`, `--parent`, `--priority`, `--labels`, `--search`, or `--limit` where applicable. Repeat `--exclude-status` or pass comma-separated configured statuses to exclude multiple states. Repeat `--type` or pass comma-separated configured task types to include multiple types.
Avoid broad unfiltered listing when the project may have many tasks. Use `--status`, `--exclude-status`, `--type`, `--project`, `--assignee`, `--unassigned`, `--parent`, `--priority`, `--labels`, `--search`, or `--limit` where applicable. Repeat `--exclude-status` or pass comma-separated configured statuses to exclude multiple states. Repeat `--type` or pass comma-separated configured task types to include multiple types. `--project` filters by configured project in monorepo-style backlogs; it has no effect and is not shown in `--help` when no `projects:` list is configured.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Document the unconfigured --project failure accurately

This shipped agent instruction says --project has no effect and is absent from help when projects are unconfigured, but normalizeCliProjects exits nonzero and the Commander option remains visible with a no projects configured schema description. Agents following this text can incorrectly treat the flag as a safe no-op; describe the fail-closed error and continued help visibility instead.

AGENTS.md reference: AGENTS.md:L73-L75

Useful? React with 👍 / 👎.

Comment thread src/ui/task-viewer-with-search.ts Outdated
Comment on lines +1425 to +1427
screen.key(["g", "G"], () => {
if (modalOpen || filterPopupOpen) return;
void openFilterPicker("project");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid reusing the detail pane's g/G navigation keys

When the task detail pane is focused, lowercase g and uppercase G are already bound at lines 1095-1103 to scroll to the top and bottom. This new screen-level project binding claims the same keys, so key propagation can both scroll and open the picker, or the global handler can make the established detail navigation unavailable whenever projects are configured; choose an unused shortcut or suppress the project handler while the detail pane owns the key.

Useful? React with 👍 / 👎.

@jafigueroam94
jafigueroam94 force-pushed the tasks/back-627-force-allocation-refresh branch from 8e58b56 to dbd8fc1 Compare August 24, 2026 22:01
Adds a validated, single-valued `project` task attribute for monorepo
backlogs, following the same pattern as `priority` and `type` but
fail-closed: with no `projects:` configured, the field is invalid to
set and invisible in every surface (no badge, no filter, no MCP enum).

Covers subtasks BACK-637.1 through BACK-637.4:
- Core: Task.project, frontmatter serialization, Core.normalizeProject,
  config `projects:` list (src/utils/project-config.ts)
- CLI: --project on task create/edit, config get/list, --help, task
  wizard, shell completions, plain/JSON output
- MCP: project on task_create/task_edit with schema field omitted when
  unconfigured
- Filtering: --project on task list/search (OR semantics, matching
  --type), Core/ContentStore/FileSystem/SearchService/task-search all
  wired consistently, MCP task_list/task_search, GET /api/search

TUI (BACK-637.5) and Web UI (BACK-637.6) are tracked as follow-up
subtasks under the BACK-637 parent.
… views

Adds the project attribute to the terminal UI: badges on board cards
and list rows, a Project: line in task detail view, a filter-header
control mirroring --type's multi-value OR semantics through the shared
unified-view filter state, a G keyboard shortcut, and a task-composer
field. Every project UI element is conditionally hidden when no
projects are configured, matching the fail-closed design from the
core/CLI/MCP work.

The task composer renders project as its own full-width row below
type/priority rather than recalculating the existing 3-column
compact/expanded layout math, keeping the change isolated to a single
extra row in detailsHeight when configured.
Adds the project attribute to the web UI, scoped to where the type
attribute already lives (Board/BoardPage/TaskCard/TaskDetailsModal),
after confirming via source that TaskList.tsx and DraftsList.tsx have
no type support to mirror. New ProjectBadge.tsx component;
availableProjects threaded from App.tsx through BoardPage's
URL-param-driven filter down to TaskCard's badge; TaskDetailsModal
gained a project select using priority's simple immediate-save
pattern. All project UI is absent when config.projects is empty.
The compact task-list JSON envelope test asserted an exact object
shape via toEqual, which broke once BACK-637.2 added project: null to
TaskSummaryJson. Add the field to the expected fixture; document/
decision search results are unaffected since project is a task-only
field.
… to end

- search/task list: --project now scopes result types like --task-type,
  is forwarded into the interactive view's filter and filter description,
  and seeds the TUI's tasksLoader query (previously loaded unfiltered)
- hasCreateFieldFlags/hasEditFieldFlags now recognize --project, so it no
  longer forces the interactive wizard open on a TTY
- "No projects are configured" now interpolates the actual resolved
  config path via a shared noProjectsConfiguredMessage() helper instead
  of a hardcoded backlog/config.yml, at all 3 call sites
- Web UI can now clear an existing project on edit (project: null flows
  through TaskDetailsModal -> api.ts -> server handlers -> Core, mirroring
  the existing dueDate/milestone clear pattern)
- server/index.ts handleCreateTask/handleUpdateTask now forward payload
  project into Core, closing the gap where the web UI's project field
  was a no-op end to end (new server-task-project-endpoint.test.ts covers
  this, verified to fail without the fix)
- Project badges (web ProjectBadge, TUI formatProjectBadge and its board
  list/detail-pane/popup callers) now stay hidden when no projects are
  configured, even if a task still carries stale project frontmatter
- Moved the project filter picker off g/G in both board and task-list TUI
  views (now v/V) to stop colliding with the pre-existing detail-pane
  scroll-to-top/bottom shortcuts; updated the help overlay to match
- Corrected the shipped task-creation.md guidance, which claimed --project
  is hidden from --help and a no-op when unconfigured (it's always shown
  and fails closed with a nonzero exit)
@jafigueroam94
jafigueroam94 force-pushed the tasks/back-627-force-allocation-refresh branch from 112bded to 74b1d58 Compare August 24, 2026 22:35
…t stale fetch

A forced allocation refresh that joined an already-in-flight non-forced
fetch returned without observing a push that landed during that fetch's
remaining duration, risking a duplicate task ID. The force path now
tracks when the joined refresh started and issues one more fetch if it
started before the force request.
…tion counter

Date.now()-based comparison could misjudge freshness when a forced
request and the in-flight fetch's start both land in the same
millisecond, treating a stale fetch as sufficiently fresh. Replace
remoteRefRefreshStartedAt/requestedAt timestamps with a monotonically
incremented remoteRefRefreshGeneration counter for the loop's
join-vs-refetch decision. disposeContentStore no longer resets the
counter (resetting a monotonic counter would make a caller holding a
stale high generation loop through extra real fetches before exiting).
@jafigueroam94
jafigueroam94 force-pushed the tasks/back-627-force-allocation-refresh branch from 32fbbbd to 220bc80 Compare August 24, 2026 22:36
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