Skip to content
This repository was archived by the owner on Sep 28, 2026. It is now read-only.

fix: thread-list matcher robustness + worktree preflight gate - #17

Merged
jnyross merged 3 commits into
mainfrom
feat/matcher-keys-preflight
Aug 9, 2026
Merged

jnyross merged 3 commits into
mainfrom
feat/matcher-keys-preflight

Conversation

@jnyross

@jnyross jnyross commented Aug 9, 2026 •

Copy link
Copy Markdown
Owner

Implements three ranked findings from the pending-clientThreadId research memo (/tmp/cg-researcher-pending-v4.md, sections 2, 3, 6) plus finding 4 of the frozen Lisbon v5 static review (/tmp/orch/lean-v5-review.md).

Changes

P0 — matcher reads name and preview (memo §2.3, §6.1)

Open-source thread list rows carry name? and preview, not title/summary (openai/codex codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs: id, sessionId, preview, name?, cwd, status, gitInfo…). The documented findExactThread read only title/summary, so a materialized, ready thread could go unmatched forever. The matcher now reads the explicit key list name/title/summary/preview. The key list stays explicit; there is deliberately still no bare id fallback (memo §6.7).

P1 — git-toplevel preflight for worktree targets (memo §3, §6.2)

openai/codex#28204 diagnosis: worktree provisioning runs git rev-parse --show-toplevel on the saved project root; on failure it aborts before git worktree add, writes no thread row, and the pending id never appears in list_threads — the orchestrating agent gets no error signal at all. Generated scripts already fetch isGitRepository in resolveProject and then ignore it. New contract rule: environment: { type: "worktree" } REQUIRES isGitRepository === true; otherwise degrade a single repository writer to a root-orchestrator write on the real checkout, or fail fast with a named unresolved_risk before any create. This closes the silent #28204 class entirely; openai/codex#26861 confirms there is no resolve API to recover after the fact.

P2 — matcher false-positive and cross-binding guards (memo §2.6, §6.5; lean-v5-review finding 4)

  • Run-tag matching requires the bracketed exact form [<runTag>], never a bare substring.
  • The per-node unique form is required: the bracketed tag hit plus the handle's node id in the same field. Finding 4 of the frozen Lisbon v5 review showed the fallback matched the SHARED graph tag alone, so two concurrent pending workers could resolve to the same or each other's thread.
  • name/title/summary are preferred over preview; a preview-only hit is a second-pass fallback (a parent thread's preview can embed worker titles).
  • The orchestrator's own/parent thread id is excluded from candidates, and every claimed thread id is excluded from later resolutions (no cross-binding, no double-claim).

Enforcement surfaces

  • references/task-lifecycle.md: contract prose, findExactThread reference pattern, resolution loop with shared CLAIMED_THREAD_IDS, §8 lifecycle checklist cases.
  • references/code-mode-script-patterns.md + SKILL.md: mirrored rules.
  • scripts/validate_skill.py: phrase checks for all three surfaces (name/title/summary/preview key list, bracketed exact form, per-node unique, claimed thread id, isGitRepository === true, named unresolved_risk, no bare id fallback).
  • scripts/task_collection_harness.{js,test.js}: findExactThread and preflightWorktreeTarget with 9 new fixtures — OSS name/preview rows resolve, clientThreadId correlation regression, preview-only second-pass fallback, bracketed-form guard, parent-preview false positive rejected, two concurrent shared-tag workers resolve to their own threads (no cross-binding, no double-claim), worktree-without-git degrade/fail-closed/proceed.

Verification

sh scripts/run_regressions.sh green: validator PASS, graph_coherence selfcheck PASS, harness 23/23 (14 existing + 9 new), auto-release tests 4/4.

No version bump; testcases/ untouched (PR #16 owns it).

Evidence: memo §2/§3/§6 · openai/codex#26861 · openai/codex#28204 · thread_data.rs row shape · frozen-lisbon-v4 terminal (workflow.js:133-181,188) · lean-v5-review finding 4.


Note

Low Risk
Documentation, reference patterns, and test harness only—no production runtime path changes; risk is low aside from stricter generated-script contracts that may surface previously silent orchestration failures.

Overview
Tightens pending clientThreadId resolution and worktree targeting for generated Code Mode orchestration scripts.

Thread-list matching now scans name/title/summary/preview (not only Desktop title/summary), requires the bracketed tag [<runTag>] plus the node id in the same field, prefers strong keys over preview-only (second pass), excludes the orchestrator/parent and already-claimed thread ids, and keeps per-node resolve bounds with a shared CLAIMED_THREAD_IDS set.

Worktree preflight requires isGitRepository === true before worktree creates; non-git roots either degrade a single writer to a root-orchestrator write or fail fast with a named unresolved_risk instead of silent provisioning failure.

Docs (SKILL.md, task-lifecycle.md, code-mode-script-patterns.md) mirror the contract; validate_skill.py enforces the new phrases; task_collection_harness.js exports findExactThread and preflightWorktreeTarget with nine new tests. Changelog Unreleased line added.

Reviewed by Cursor Bugbot for commit 6b4f2d1. Bugbot is set up for automated code reviews on this repo. Configure here.


Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Improved task matching using exact run tags, node identifiers, previews, and client thread IDs.
    • Added safeguards against duplicate or cross-node task assignments.
    • Added Git repository checks for worktree tasks, with safe fallback or fail-closed handling.
  • Documentation

    • Updated task lifecycle and scripting guidance for repository validation, task ownership, and unresolved risks.
  • Tests

    • Expanded coverage for task matching, exclusions, concurrent workers, and non-Git worktree scenarios.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds Git-aware worktree preflight, exact per-node task-thread matching, claimed-thread exclusion, expanded lifecycle guidance, and validation tests.

Changes

Task setup and resolution

Layer / File(s) Summary
Worktree repository preflight
skills/codex-graph/SKILL.md, skills/codex-graph/references/code-mode-script-patterns.md, skills/codex-graph/references/task-lifecycle.md, skills/codex-graph/scripts/task_collection_harness.js, skills/codex-graph/scripts/task_collection_harness.test.js
Worktree tasks now require confirmed Git repository status. Single-writer non-Git targets fall back to root writes. Other non-Git targets return a named unresolved_risk.
Exact task-thread resolution
skills/codex-graph/SKILL.md, skills/codex-graph/references/code-mode-script-patterns.md, skills/codex-graph/references/task-lifecycle.md, skills/codex-graph/scripts/task_collection_harness.js, skills/codex-graph/scripts/task_collection_harness.test.js
Pending tasks now use bracketed run tags and node IDs across supported metadata fields. Matching excludes orchestrator and claimed threads, supports nested responses, and records resolved thread IDs.
Guidance and validation alignment
skills/codex-graph/scripts/validate_skill.py, CHANGELOG.md
Validation now checks repository detection, exact tag formatting, metadata fields, node uniqueness, ownership, and claimed-thread handling. The changelog records the changes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TaskSetupWorkflow as Task setup workflow
  participant preflightWorktreeTarget
  participant findExactThread
  participant TaskRows as Task rows
  TaskSetupWorkflow->>preflightWorktreeTarget: validate environment and Git status
  preflightWorktreeTarget-->>TaskSetupWorkflow: allow worktree, root fallback, or unresolved_risk
  TaskSetupWorkflow->>findExactThread: resolve run tag, node ID, client thread, and exclusions
  findExactThread->>TaskRows: inspect name, title, summary, and preview
  TaskRows-->>findExactThread: return matching metadata
  findExactThread-->>TaskSetupWorkflow: return resolved thread ID
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the two main changes: thread-list matcher robustness and the worktree preflight gate.
Description check ✅ Passed The description explains the changes, rationale, validation results, and regression coverage, although it does not follow every template heading exactly.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/matcher-keys-preflight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

cursor[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +85 to +96
Match the run tag only in the bracketed exact form `[<runTag>]`, against the
explicit key list `name`/`title`/`summary`/`preview`. Open-source thread rows
carry `name` (optional user-facing title) and `preview` (first user message),
not `title`/`summary` (`thread_data.rs`); a matcher that reads only
`title`/`summary` misses a materialized, ready thread. Desktop rows have
shown `title`, and while a project task is loading, Codex can temporarily put the
requested title in `summary` — keep both keys too. Prefer `name`/`title`
(and `summary`) over `preview`: a parent thread's preview can embed worker
titles, so accept a preview-only hit just as a second-pass fallback, and
never match a row whose thread id is the orchestrator's own (or parent)
thread id, when that id is known. Keep the key list explicit;
never add a bare `id` fallback to list matching.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Recommended task naming makes newly created worker tasks unfindable under the new stricter matching rules

The reference spawn pattern names a new task with only the lower-cased node id (args.task_name = node.id.toLowerCase() at skills/codex-graph/references/code-mode-script-patterns.md:125), which no longer satisfies the new matching rule that requires the bracketed run tag plus the exact-case node id in the same field, so a freshly created worker task can never be recognised in the task list.
Impact: Workflows generated from this guidance can stall waiting for a worker task that in fact exists but is never matched.

How the naming pattern and the new matcher contradict each other

findExactThread (skills/codex-graph/scripts/task_collection_harness.js:48-104, mirrored at skills/codex-graph/references/task-lifecycle.md:126-182) now requires, for every candidate field (name/title/summary/preview), both s.includes("[" + runTag + "]") and a case-sensitive token match on the node id.

The list row's name is populated by the create call's task_name. The reference spawn helper still sets task_name = node.id.toLowerCase() — no brackets, no run tag, and lower-cased so even \bN2A\b would not match n2a. A script that follows both references therefore never gets a name hit and depends entirely on the preview second-pass fallback (which the same docs say is only a fallback and can false-positive on the parent thread's preview).

skills/codex-graph/references/task-lifecycle.md:58-61 says every node's title must be [<runTag>] <label>; the spawn-args example should be updated to build task_name/title from that exact form.

Prompt for agents
The new matcher in findExactThread (skills/codex-graph/scripts/task_collection_harness.js and the mirrored snippet in references/task-lifecycle.md) requires the bracketed run tag `[<runTag>]` plus a case-sensitive node-id token in the same list-row field (name/title/summary/preview). However, the spawn-argument example in references/code-mode-script-patterns.md still sets `args.task_name = node.id.toLowerCase()`, which produces a row `name` with neither the bracketed tag nor an exact-case node id. Scripts following both references would never match on `name` and would rely solely on the preview fallback. Update the spawn-args pattern (and any related guidance) so the created task's name/title uses the documented `[<runTag>] <label>` form with the per-node run tag `<graphTag>-<nodeId>`, or relax the matcher to be case-insensitive on the node-id token.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/codex-graph/scripts/task_collection_harness.js`:
- Around line 59-82: Require a resolved thread identifier in both resolver
patterns: in skills/codex-graph/scripts/task_collection_harness.js lines 59-82,
return false immediately when rowThreadId is absent, before clientThreadId or
metadata matching; in skills/codex-graph/references/task-lifecycle.md lines
137-160, document the same guard and add a fixture where an ID-less matching
metadata object precedes a valid thread row.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0af44466-6e88-4dd6-859b-4ff57178692d

📥 Commits

Reviewing files that changed from the base of the PR and between a3fce5d and 23a9ca0.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • skills/codex-graph/SKILL.md
  • skills/codex-graph/references/code-mode-script-patterns.md
  • skills/codex-graph/references/task-lifecycle.md
  • skills/codex-graph/scripts/task_collection_harness.js
  • skills/codex-graph/scripts/task_collection_harness.test.js
  • skills/codex-graph/scripts/validate_skill.py

Comment on lines +59 to +82
const rowThreadId = row.threadId ?? row.thread_id ?? row.id ?? null;
if (rowThreadId != null && excluded.has(String(rowThreadId))) return false;
const listedClient = row.clientThreadId ?? row.client_thread_id ?? null;
if (
clientThreadId &&
listedClient &&
String(listedClient) === String(clientThreadId)
) {
return true;
}
// Explicit key list: OSS rows carry name/preview, Desktop rows have
// shown title/summary. Never add a bare `id` fallback here.
const strongHit =
fieldHit(text(row, "name")) ||
fieldHit(text(row, "title")) ||
fieldHit(text(row, "summary"));
const previewHit = allowPreview && fieldHit(text(row, "preview"));
if (!strongHit && !previewHit) return false;
// Projectless handles have no projectId; match the unique run tag alone.
// When a project is bound, prefer projectId match but do not require it
// while setup is still loading (projectId may be absent on early rows).
const projectRequired = projectId != null && projectId !== "";
const listedProject = row.projectId ?? row.project_id;
return !projectRequired || listedProject == null || listedProject === projectId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require a thread identifier for every resolved match. Both resolvers can return an ID-less nested metadata object before they reach the actual thread row. The pending handle then remains unresolved.

  • skills/codex-graph/scripts/task_collection_harness.js#L59-L82: return false when rowThreadId is absent before checking clientThreadId or metadata fields.
  • skills/codex-graph/references/task-lifecycle.md#L137-L160: apply the same guard to the documented resolver pattern and add a fixture with an ID-less matching object before a valid thread row.
📍 Affects 2 files
  • skills/codex-graph/scripts/task_collection_harness.js#L59-L82 (this comment)
  • skills/codex-graph/references/task-lifecycle.md#L137-L160
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/codex-graph/scripts/task_collection_harness.js` around lines 59 - 82,
Require a resolved thread identifier in both resolver patterns: in
skills/codex-graph/scripts/task_collection_harness.js lines 59-82, return false
immediately when rowThreadId is absent, before clientThreadId or metadata
matching; in skills/codex-graph/references/task-lifecycle.md lines 137-160,
document the same guard and add a fixture where an ID-less matching metadata
object precedes a valid thread row.

jnyross added a commit that referenced this pull request Aug 9, 2026
- resume-path handle re-validation now mirrors the matcher contract:
  name/title/summary/preview + bracketed [<runTag>] + node id (finding 1)
- bracket fixture keys the row on title so it discriminates the bracket
  rule against the pre-PR17 matcher (finding 4)
- document the extraction-vs-matching id distinction instead of dropping
  id: open-source rows carry the thread id under id, so a matched row
  must stay extractable; matching still never falls back to bare id
  (finding 2, alternative branch per review)
- adopt per-node resolveBounds(handle) over module-level constants;
  CLAIMED_THREAD_IDS stays one shared set — bounds per node, claims
  global (finding 3)
@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_42cde175-6058-456d-a9c0-4f2c25ebf01b)

jnyross and others added 3 commits August 9, 2026 21:45
- findExactThread reads name AND preview besides title/summary (OSS thread
  rows carry name?/preview, not title/summary — thread_data.rs); key list
  stays explicit, no bare id fallback
- matcher requires the bracketed exact form [<runTag>] plus the handle's
  node id in the same field (per-node unique form), prefers name/title/
  summary over preview, excludes the orchestrator's own/parent thread id,
  and excludes already-claimed thread ids from later resolutions
- worktree targets are gated on isGitRepository === true from the project
  lookup; single writer degrades to a root-orchestrator write, otherwise
  fail fast with a named unresolved_risk (openai/codex#28204)
- mirrored in task-lifecycle.md, code-mode-script-patterns.md, SKILL.md;
  enforced by validate_skill.py phrases and 9 new harness fixtures
Plain includes() let N1 bind N10 and N2 bind N2A when titles share a
run tag. Use a word-boundary regex so concurrent pending workers stay
distinct, and cover the prefix-collision cases in tests.
- resume-path handle re-validation now mirrors the matcher contract:
  name/title/summary/preview + bracketed [<runTag>] + node id (finding 1)
- bracket fixture keys the row on title so it discriminates the bracket
  rule against the pre-PR17 matcher (finding 4)
- document the extraction-vs-matching id distinction instead of dropping
  id: open-source rows carry the thread id under id, so a matched row
  must stay extractable; matching still never falls back to bare id
  (finding 2, alternative branch per review)
- adopt per-node resolveBounds(handle) over module-level constants;
  CLAIMED_THREAD_IDS stays one shared set — bounds per node, claims
  global (finding 3)
@jnyross
jnyross force-pushed the feat/matcher-keys-preflight branch from 8004d93 to 6b4f2d1 Compare August 9, 2026 20:46
@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_af05cd39-423e-4de9-8244-8c2e474a7d9d)

@jnyross
jnyross merged commit 3fd58c1 into main Aug 9, 2026
3 of 4 checks passed
@jnyross
jnyross deleted the feat/matcher-keys-preflight branch August 9, 2026 20:49
jnyross added a commit that referenced this pull request Aug 9, 2026
PRs #14, #16, #17, #18, and #20 added manual Unreleased bullets; the
release workflow prepended [0.4.5] through [0.5.4] for the same changes
without folding them in. The changelog is generated from commit subjects
at release time, so the manual section is pure duplication. Also
document the convention in the README versioning section so it stops
recurring.
@coderabbitai coderabbitai Bot mentioned this pull request Aug 10, 2026
2 of 3 tasks
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants