Bug Pattern Scan

This guide lists the bug families found in fix commits on main over four weeks, with commands that find siblings of each bug in the current tree. Use it to audit an area, or to review a diff for a known failure shape.

The source set was every non-merge commit on origin/main whose message matched fix or bug (461 commits). About 240 changed behavior. The rest were checkpoints, docs, refactors, or CI tweaks. Regenerate the list with:

git log origin/main --since="4 weeks ago" --no-merges -i -E --grep='fix|bug' --format='%h %s'

How to Use This Guide

  1. Pick a family and run its search commands from the repository root.
  2. Treat every hit as a hypothesis. Read the code and apply the manual check.
  3. Confirm a suspect with a failing test before fixing it, as the Harness-First rules require.
  4. Scope any conclusion to the hits you read.

Families are ordered by how often they recurred.

P1. Races and Lost Updates Across await

A writer reads state, awaits, then overwrites without re-checking. A user edit, an agent write, a running generation, or a recovery worker clobbers the other. Merge flows replace an accumulated list with the latest result.

Evidence: 0ef7ee26, a7797dd4, aea51e12, 2b9d993c, 992d54ad, fb6a70bf, 3be3dafd, 799354ed, 5d7714f4, b0123af1, 72c0696b.

// before: a concurrent render marked the doc dirty during the fetch
const doc = await trpcClient.timeline.get.query({ id });
adopt(doc);
// after: re-check after the await and route through the merge path
const doc = await trpcClient.timeline.get.query({ id });
if (isDirty()) { mergeRemote(doc); } else { adopt(doc); }
rg -n "await trpcClient\..*\.query" web/src/hooks web/src/stores -A4
rg -n "setConflicts\(|lastSyncedRef\.current\s*=" web/src
rg -n "db\.select" packages/models/src -A6 | rg -n "insert|update"
rg -n "reserve\(|commit\(|release\(" packages/runtime/src

Manual check: after each await, does the code write state it captured before the await without comparing a version, dirty flag, or timestamp? Is a check-then-write pair (project exists, then insert) inside one transaction? Does a shared budget or limiter hand each caller its own reservation handle?

P2. Pricing and Cost Recording Errors

A per-second or per-megapixel rate is returned as the run total. A flat scalar is used where a resolution or duration grid exists. Some node types or the streaming path never record cost.

Evidence: 7 to 8 commits in the pricing cluster, plus 76355344, e86d6089, d88ea441.

rg -n "resolvePrice|priceScalarUnit|priceGenspendEntry" packages/model-pricing packages/node-sdk
rg -n "recordPrediction|recordCost|nodetool_predictions" packages

Manual check: does every per-unit rate multiply by its quantity? Are resolution and duration passed to a grid lookup? List every node and tool that calls a paid provider and confirm each one records usage on every path, including streaming and errors.

P3. Scope Leaks and Wrong Default Scope

State, queries, or tabs from one project appear in another. An omitted project_id falls into a default bucket. An undefined argument means “inherit the current workflow” when the caller meant “none”. A key unique only within one scope is reused across scopes.

Evidence: a7ae40b8, 20c652a7, 21104cdc, cf473afb, f13e4261, f5ae0da8, 5eef8ad7, d62fad2c, plus the three cross-scope key commits in batch 1.

rg -n "project_id\s*(\?\?|\|\|)\s*[\"']default" packages web/src
rg -n "openTab\(" web/src | rg -v projectId
rg -n "createNewThread\([^,]*,\s*undefined" web/src
rg -n "projectId ===" web/src/stores web/src/components
rg -n "new Map<string," packages/agents/src/capabilities

Manual check: for each callee, does undefined mean “use the current X” or “no X”? For each map keyed by an entity ID, is that ID unique across the outer loop’s scope?

P4. Lookup by a Non-Unique Key or Unstable Identity

.find(x => x.id === value) ignores a second dimension such as provider. A preview matches “any video output” instead of its own node and output handle.

Evidence: 25cbf492, 03781867.

rg -n "\.find\(\(\w+\)\s*=>\s*\w+\.id\s*===" web/src packages
rg -n "outputHandle|output_handle" web/src/components/node -A3

Manual check: can the collection hold two entries with the same ID? Does the selector filter by node ID and output handle together?

P5. Duplicated Logic That Drifts

The same value, parser, type, or renderer exists twice. One copy is edited and the other is not. Examples: a staleness hash over clipPrompt while the render sends directClipPrompt, GPU and Canvas2D renderers, a hand-written request interface beside its Zod schema, a second frontmatter parser, a hand-kept build order list that covered 21 of 49 dependencies.

Evidence: c5436b22, 2d81b96a, cc1ad8fc, a7790498, e942bc39, 3d48196c, c05efcf1, e84c4de2, plus the frontmatter parser fix in batch 4.

rg -n "^export interface \w+(Request|Response)" packages/protocol/src
rg -n "sha256Hex|prompt_hash|isVersionStale" packages
rg -l "canvas2d|gpu" packages/timeline/src/render
rg -n "workspaces = \[" scripts
rg -n "frontmatter" packages --type ts -l

Manual check: for each interface, is there a schema with the same name? If so, derive the type with z.infer. Is the value compared in a staleness check produced by the same function that the work site calls?

P6. Guards Missing on Some Sibling Operations

A containment, ownership, or permission check covers most methods in a family but not all. deleteAll and mkdir skipped assertContained, which allowed a symlink escape. Approvers defaulted to allow. Capability mounts bypassed the shared gate.

Evidence: 3be1f1c4, 224981b8, f6fc6466, d1be6585, 9721099e.

rg -n "async (write|delete|move|copy|mkdir|deleteAll|rename)\(" packages/runtime/src
rg -n "approver:\s*\(\)\s*=>\s*[\"']allow" packages
rg -n "ungatedCapabilityRun" packages
rg -n "requireUserId\(\)" packages/websocket/src

Manual check: count guard calls per method in each storage or workspace class. A method without the guard its siblings use is a suspect. Every capability mount must go through the one shared gate.

P7. Regex ReDoS and Single-Pass Sanitizing

Anchored repeats like /^\/+|\/+$/g, or a repeated group around overlapping alternatives, backtrack polynomially on long input. CodeQL flagged most of them. A single .replace() strips nested tags incompletely.

Evidence: 04f8d69b, 64f80c39, 0c690863, 67cd8808, 0e61f4d4, 1a538402, 3564ef7c, d8ad0317.

rg -n "\.replace\(/\^?\\\\/\+" packages web/src
rg -n "\+\$/" packages web/src --type ts
rg -n "\(\?:[^)]*\|[^)]*\)[*+]" packages web/src --type ts
rg -n "\.replace\(/<\[\^>\]\*>/" packages web/src

Manual check: time each candidate on 50,000 repeated characters. Replace trailing-trim regexes with a loop, and strip markup with a parser or a loop that runs until the output stops changing.

P8. Schemas Too Narrow, or Fields Accepted and Ignored

MsgPack sends null where the schema allows only an optional string. A closed enum rejects a valid spelling. A stripping schema drops unknown fields during a save and reports success. A validated field is never read downstream.

Evidence: d124efcf, e942bc39, 5f85fef0, 8c8a1fbc, da92dcbc, 1d4b765e, aff17ed1, d88ea441, c05efcf1.

rg -n "z\.string\(\)\.optional\(\)" packages/protocol/src
rg -n "\.parse\(|\.safeParse\(" packages/websocket/src packages/execution/src

Manual check: can the field arrive as null over MsgPack? Does a save handler persist parsed output without reporting stripped keys? For each schema field, search its name in the consuming process or render code. Zero references outside the schema means a silent no-op.

P9. Media and Asset Handling

A node resolves media through a helper that handles inline bytes and the temporary store but not asset:// URIs. A chat attachment is inlined as a base64 data URI instead of uploaded. Video goes to a provider without a capability check, and the error names the wrong provider. iOS Safari never fires canplay before play().

Evidence: 465e46b6, 6c234230, 74f0cdb2, the video-input capability fix and the two iOS fixes in batch 4.

rg -n "decodeImage\(|decodeAudio\(" packages --type ts
rg -n "readAsDataURL|toDataURL\(" web/src
rg -n "not supported by \"?openai" packages/runtime/src/providers
rg -n "onCanPlay|canplay" web/src marketing

Manual check: does each media input reach loadMediaRefBytes or the asset store? Is every attachment uploaded and referenced by ID? Does each gated path call the matching providerSupports* check?

P10. Provider Contract Mismatches

A generic spec field is never mapped to a vendor API. Numeric fields pass to an API that declares int within a range, and the API returns 422. An enum tier is written into a free-string field. context.signal is not forwarded, so a cancelled run keeps polling a paid API.

Evidence: b244198d, 1a7eec1f, a59bfd06, 296476b6, the AtlasCloud size fix in batch 4.

rg -n "async process\(context" -A12 packages/*-nodes/src | rg -n "fetch\(|Execute\(|poll"
rg -n 'propType === "(int|float)"' packages/runtime/src/providers
rg -n "spec\.\w+" packages/compute/src/providers
rg -n "this\.serialize\(\)" packages/*-nodes/src

Manual check: does every paid call receive context.signal? Is every WorkerSpec field read by each provider? Are numeric fields clamped and rounded like enums are coerced? Code that reads dynamic properties must read the instance, since serialize() returns only declared properties.

P11. Identifier Collisions

IDs built from Date.now() collide within one millisecond. Migration version strings were reused across branches. A duplicate-ID pass disqualified the rightful owner of an ID.

Evidence: 9c4b24a3, plus the migration and glTF ID fixes in batch 4.

rg -n 'Date\.now\(\)\.toString\(36\)|\$\{Date\.now\(\)\}' packages web/src
rg -n 'version:\s*"[0-9]{8}_' packages/models/src/migrations
rg -n "used\.has\(" packages

Manual check: IDs need a counter or random component. Resource IDs follow the Resource ID Principles.

P12. Compact Resource IDs Not Resolved at a Boundary

An agent-facing tool accepts only the full 32-character ID, although agents receive 12-character prefixes.

Evidence: 670a215f, cf473afb.

rg -n "shortResourceId|resolveResourceId" packages/agents/src packages/protocol/src
rg -ln "_id" packages/agents/src/capabilities packages/agents/src/tools

Manual check: every tool input that takes an agent-visible ID must resolve an exact 12-character prefix uniquely and return an ambiguity error otherwise.

P13. Failure Handling Errors

One failing sub-read fails a whole aggregate through Promise.all. A cache stores a rejected or null result, so one transient error disables a resource for the rest of the run. An inner timeout equals its outer timeout, so the outer one fires with a generic message.

Evidence: 5917e4fa, dbcef8c2, 91719bdc, f59c1cc1.

rg -n "Promise\.all\(" packages/models/src web/src/stores
rg -n "\.catch\(\(\) => null\)" packages --type ts
rg -n "_TIMEOUT_MS|TIMEOUT_MS\b" packages/agents/src packages/cli/src

Manual check: should independent parts degrade separately with Promise.allSettled? Does a memo cache evict failures? Is each nested timeout strictly smaller than the one enclosing it?

P14. Falsy Defaults and Wholesale Replacement

x ?? "image.png" keeps an empty string, and path.resolve(folder, "") returns the folder. A setX(partial) API replaced the whole default object.

Evidence: 443a9187, b56d5516.

rg -n '\?\?\s*"' packages --type ts
rg -n "path\.resolve\(\w+,\s*(this\.)?\w+\)" packages
rg -n "set[A-Z]\w*Interfaces?\(" packages/runtime/src

Manual check: can the string be "" from a text field? Does the setter merge keys over the defaults?

P15. UI Stacking, Focus, and Keyboard Scope

A dialog opened from inside a popover renders beneath it. Opening a picker leaves a sibling menu open. Background tabs kept mounted as inert still consume key combos. A :hover reveal on a list wrapper shows every row’s button. A flex child without min-width: 0 overflows on phones.

Evidence: 8af16e41, 5b09d431, e4d347d1, 98891619, 103cb769, e7d6a80f, fac29e8d, 576448dd, 814e9e3c, plus the hover fix in batch 4.

rg -ln "Popover" web/src/components | xargs rg -ln "Dialog|Modal"
rg -n "useCombo\(" web/src/components
rg -n "\.[\w-]+:hover &" web/src/components
rg -n "align-items:\s*flex-start|alignItems:\s*\"flex-start\"" web/src

Manual check: use Z_INDEX tokens for nested surfaces. The combo dispatcher must check reachability when a key is pressed. Scope hover reveals to the row.

P16. Unregistered Tools and Unthreaded Parameters

A tool was implemented but missing from BUILTIN_TOOL_NAMES. A new callback parameter reached one loop call site but not the others.

Evidence: 11d76701, e297798a.

rg -n "export const \w+Tool\b" packages/agents/src/tools
rg -n "BUILTIN_TOOL_NAMES" packages/agents/src/tools/builtin-tools.ts

Manual check: every exported tool appears in a registry. When you add a parameter, list every call site of the function.

P17. Shell Scripts and Deploy Ordering

Under set -euo pipefail, a grep with no match kills the script before its empty-result branch runs. A deploy step proceeds when a request is accepted rather than when the machine is healthy.

Evidence: 2f22cabe, 8177ad65, c652fe6d, dd0dd7d2, d8c7be77.

rg -l "pipefail" scripts | xargs rg -n "\| *grep"
rg -n "fly machine (run|update)" scripts

Manual check: add || true where “no match” is valid. Wait on the health state the next step depends on.

P18. Geometry and Aggregation Math

An overlap test uses a value from before a clamp. An append boundary folds over kept and added but not foreign clips on the same track. A quaternion to Euler conversion had a sign error at gimbal lock.

Evidence: 66f1a969, d19e962e, 60336f71.

rg -n "Math\.(max|min)\(" packages/timeline/src/placement
rg -n "\.reduce\(.*Math\.max" packages/timeline/src
rg -n "atan2" packages/model3d/src packages/timeline/src

Manual check: after a clamp, no later code should use the unclamped input. Boundary math must include every group that stays in the result. Test degenerate math on a full grid of inputs.