Skip to content

Never invent objective or action ids (finish what #61 started for claims) - #66

Merged
lucca65 merged 1 commit into
masterfrom
fix/create-path-serial-fallback
Aug 8, 2026
Merged

Never invent objective or action ids (finish what #61 started for claims)#66
lucca65 merged 1 commit into
masterfrom
fix/create-path-serial-fallback

Conversation

@lucca65

@lucca65 lucca65 commented Aug 8, 2026

Copy link
Copy Markdown
Member

#61 removed the serial fallback for claims. The objective and action create paths still had it, and it is the same defect: when the chain read fails, the resolver substitutes a DB serial that silently names a different row.

For actions it is worse than for claims — claimaction carries action_id, so later claims attach to the wrong action, and the action becomes un-editable. That is the shape of the phantom action ids 399–406 the audit found.

Removing the fallback alone would not have been safe

The reads underneath it were still truncatable:

  • actionsForObjective used the byobjective SECONDARY index. Secondary reads cannot be resumed — nodeos returns the secondary key as next_key (a query bounded to 389 returns next_key: 389) — so a short read just looks like this objective has fewer actions than it does, and the caller then hands the create an id that is already taken. It now pages the whole action table through the primary index and filters client-side: 400 rows / 5 calls on prod, on the create path only.
  • objectivesForCommunity took a single call at limit: 2000 and trusted it. It now pages its scope until more: false.

Both go through one pager, and assertComplete is gone with them — a lone call is never proof of a complete set at any table size, so there is nothing left for a throw-on-more guard to protect.

Retries

Chain reads are retried (3 attempts, backing off). Without a fallback a failed read costs a skipped create until someone reindexes, and about 2 in 100 calls came back without a rows array while paging the action table on prod — the acceptance check below failed 2 of ~105 calls before the retry and passes cleanly after. The node's own message is carried into the error, since Sentry is not a paid account and the log line is the only diagnostic. The claim reader shares the retry for the same reason.

Create-path inserts now log and rethrow instead of swallowing. A swallowed insert drops the row while ledgered still records the action as processed, so no reindex revisits it — the likeliest explanation for the two claims found missing on prod.

Verification

scripts/verify-create-resolvers.js against the prod chain replays each parent's creation history (first k ids known, ask for the next) and requires the resolver to name id k+1 every time:

communities with objectives checked: 14   (95 steps, all pass)
actions: 400 across 89 objectives
  PASS actions of objective 5: 21/21 steps, exhausted-probe threw as required
  ... 8 objectives sampled, all pass
ALL CHECKS PASS

An exhausted parent throws instead of inventing an id. verify-claim-resolver.js still 636/636 after the reader refactor.

#61 removed the serial fallback for claims. The same fallback was still in the
objective and action create paths, and it is the same defect: when the chain
read failed, the resolver substituted a DB serial that silently names a
DIFFERENT row. For an action that is worse than for a claim — claimaction
carries action_id, so later claims attach to the wrong action, and the action
becomes un-editable. That is the shape of the phantom action ids 399-406 the
audit found.

Removing the fallback alone would not have been safe, because the reads
underneath it were still truncatable:

  * actionsForObjective used the `byobjective` SECONDARY index. Secondary reads
    cannot be resumed — nodeos returns the secondary key as next_key — so a
    short read just looks like "this objective has fewer actions than it does",
    and the caller then hands the create an id that is already taken. It now
    pages the whole `action` table through the primary index and filters
    client-side: 400 rows / 5 calls on prod, on the create path only.
  * objectivesForCommunity took a single call at limit 2000 and trusted it. It
    now pages its scope until more=false.

Both go through one pager, and assertComplete is gone with them: a lone call is
never proof of a complete set at any table size, so there is nothing left for a
throw-on-`more` guard to protect.

Chain reads are now retried (3 attempts, backing off) before giving up. Without
a fallback a failed read costs a skipped create until someone reindexes, and
about 2 in 100 calls came back without a rows array while paging the action
table on prod — the acceptance check below failed 2 of 105 calls before the
retry and passes cleanly after. The node's own message is carried into the
error, since Sentry is not running and the log line is the only diagnostic.
The claim reader shares the retry for the same reason.

Create-path inserts log AND rethrow instead of swallowing. A swallowed insert
drops the row while ledgered still records the action as processed, so no
reindex revisits it — the likeliest explanation for the two claims found
missing on prod.

Verified with scripts/verify-create-resolvers.js against the prod chain: replay
each parent's creation history (first k ids known, ask for the next) and require
the resolver to name id k+1 every time. 14 communities / 95 objective steps and
77 action steps across the busiest objectives all pass, and an exhausted parent
throws instead of inventing an id. verify-claim-resolver.js still 636/636.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lucca65
lucca65 merged commit 0b78a14 into master Aug 8, 2026
2 checks passed
@lucca65
lucca65 deleted the fix/create-path-serial-fallback branch August 8, 2026 13:33
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.

1 participant