Skip to content

fix(topic-memory): stop head conflicts from spending window attempts - #2597

Open
Fengzdadi wants to merge 3 commits into
oceanbase:masterfrom
Fengzdadi:fix/topic-head-conflict-attempts
Open

Fengzdadi wants to merge 3 commits into
oceanbase:masterfrom
Fengzdadi:fix/topic-head-conflict-attempts

Conversation

@Fengzdadi

@Fengzdadi Fengzdadi commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Which issue or RFC does this PR close?

No tracking issue. This implements the Worker side of RFC 1614 §5.3: if a manual replacement wins the head CAS, the stale Worker commit is rejected and "its existing reread/recompute behavior" recovers it.

Rationale for this change

When a concurrent replace or Dream moves a Topic head between a Topic window's read and its publication, the publisher raises RevisionConflictError and the Worker returns head_conflict. The supervisor defers the retry for 0.1 s and does not count it as a failure. However, TopicMemoryWorkBudget.begin() had already charged the attempt, and no cause was recorded. Reproduced on 923e3d4ce through TopicMemoryProcessor.process with a concurrent revision published during the global stage:

attempt 1: outcome=head_conflict attempts=1 last_error_code=None
attempt 2: outcome=head_conflict attempts=2 last_error_code=None
attempt 3: outcome=head_conflict attempts=3 last_error_code=None
attempt 4: raised TopicMemoryGenerationError: window_attempt_limit

After three concurrent edits, the Scope stays blocked until an operator retry, and nothing records why. The new e2e reproduces the same block with real HTTP PUT replacements and a spawned Topic Worker.

What changes are included in this PR?

  • Worker (topic_memory_processing.py): when the publisher's head CAS fails, the attempt records head_conflict as its last_error_code on its own budget row, then returns HEAD_CONFLICT. The publish transaction has already rolled back, so nothing is written. Only the publish call is covered. A RevisionConflictError from any other step keeps the previous mapping: HEAD_CONFLICT without a recorded cause, so that attempt still counts.
  • Budget (topic_memory_budget.py): an attempt whose last error is head_conflict is not spent. exhausted_reason and the next begin() both use the spent count, so the existing conflict deferral starts a fresh attempt that rereads the head and recomputes. exhausted_reason is also used by block observation, explicit retry, the selector guard and the status reader in feat(processing): add read-only artifact processing status inspection (part of #2130) #2568, so they all stay consistent.
  • Cost bound: provider reservations are still committed before each call and are never refunded. Only a generated proposal can target an existing head, and every stage reserves before its call. So each recomputation spends from the frontier's existing 512-request and 64M-token ceilings. A head that never settles ends in window_provider_budget_exceeded. The ceiling is normally crossed partway through an attempt, so that code is also the block's last_error_code. A low attempts count, typically 1, shows that recomputations after head conflicts used the budget rather than failed attempts. Each conflict also appears in the supervisor log, described below. I chose this over a dedicated conflict counter because the frontier's model cost stays capped by the ceiling it already had, and a counter would need a migration on every backend. The trade-off: before this change, conflicts could cost at most three attempts' worth of model calls. Now they can use up the full frontier ceiling, at the rate other writers move the head.
  • Supervisor log: artifact_processing.attempt.completed for a control conflict now sets error_code to cursor_conflict or head_conflict. Before, both appeared only as outcome=conflict. This uses an existing field, so no allowlist change.
  • Docs (en/zh): Topic budget configuration, the window_attempt_limit description, the attempt-completed event, the Topic Memory workflow's processing section, and the processing-failure matrix.
  • Persistence review: release/upgrade-support.json has the new digest and a rationale sentence. No table, column, constraint, migration resource, checksum or row encoding changes. Existing rows never contain head_conflict, so their counters and blocks keep their meaning.

Self-review (code-review skill at high effort, plus REVIEW.md) and an independent review found no blocking defects. Changes that came out of them:

  • The processor comment now states the invariant that bounds recomputation.
  • The handback is limited to the publish call.
  • The docs and the ceiling test now describe the usual block for a head that never settles.

Notes I left unchanged:

  • Block and status attempts show the raw column, so a handed-back attempt is still counted there.
  • A Worker killed after recording head_conflict but before returning is treated as a conflict by the budget and as a failure (with backoff) by the in-memory supervisor state.
  • The new error_code makes the logging filter derive error_category=conflict on those events. The shipped dashboards count only artifact_processing.failed, so no panel changes.
  • If recording head_conflict itself fails, for example on a transient database error, the conflict becomes an ordinary counted Worker failure with backoff. This is conservative and still bounded.

Are there any user-facing changes?

Yes, in behavior:

  • Concurrent manual replacements or Dream no longer exhaust a Topic frontier's three attempts.
  • last_error_code can now be head_conflict.
  • Conflict attempt logs carry error_code.

No API, schema or persisted-format changes. A binary without this change would count an already recorded head-conflict attempt as spent, which can only stop that frontier sooner.

How was this change tested?

  • New tests/builtin/runtime/test_topic_memory_head_conflict.py:
    • Five repeated head moves leave attempts=1 and last_error_code=head_conflict, and the window then publishes on the moved head with the right lineage.
    • Three real failures still exhaust the frontier with conflicts between them.
    • A conflict on the last remaining attempt hands it back.
    • A head that never settles stops at the provider ceiling, crossed partway through an attempt. The block shows window_provider_budget_exceeded with attempts=1.
    • An in-process supervisor run publishes with no block and logs head_conflict.
  • New tests/e2e/test_topic_memory_head_conflict.py:
    • Setup: real Server app over ASGI, spawned Topic Worker, local stub model. The stub replaces the Topic through HTTP PUT while answering the global stage, four times.
    • Result: four head_conflict attempts, then the Worker revision is published, with no block.
  • Both new files fail on 923e3d4ce and pass with this change.
  • make check: pass.
  • make unit-test (on the first commit): 8733 passed. 12 failed, all already failing on unmodified master: 8 in test_jev_rerank_comparison, 4 in test_ci_installation_roles.
  • After the follow-up commit, 350 focused unit and e2e Topic tests passed.
  • Related suites passed on the first commit:
    • Topic Memory processing, work budget, scope, recovery and terminal frontier
    • Processing scheduler, observations and diagnostics
    • Recovery CLI
    • e2e: artifact processing persistence, Topic Memory budget integrity, content admission, generic API, privacy, processing, product chain, recovery access, recovery migration, terminal frontier, worker logging
  • Backends: SQLite only. The change does not touch SQL or backend-specific code, but OceanBase and seekDB were not run locally. Real model providers were not exercised; the e2e uses a local stub.

AI usage statement

Investigation, implementation, tests and this description were produced with Claude Code (Claude Opus 5.5), then reviewed and run locally by the author.

A Topic window whose publication lost the head CAS to a concurrent replace
or Dream returned HEAD_CONFLICT without recording a cause, yet still kept
the attempt it had begun. Three such conflicts left attempts at 3 with no
last error, and the next attempt blocked the Scope with
window_attempt_limit.

The Worker now records head_conflict as the attempt's last error. The
attempt ceiling and the next attempt's counter treat that attempt as not
spent, so the existing 0.1 s conflict deferral rereads the head and
recomputes (RFC 1614 section 5.3). Provider reservations are never
refunded: each recomputation spends from the frontier's 512-request and
64M-token ceilings, which stop a head that keeps moving with
window_provider_budget_exceeded. The supervisor's attempt.completed log
now names cursor_conflict or head_conflict in error_code.

No schema, migration or row encoding changes; the persistence review is
updated for the reinterpreted budget column.
Copilot AI balanced review requested due to automatic review settings November 11, 2026 22:17

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

Only the publisher's head CAS hands an attempt back. A RevisionConflictError
from any other step keeps the previous mapping: HEAD_CONFLICT without a
recorded cause, so that attempt still counts. The catch sits outside the
commit tracing stage, so the stage still observes the conflict.

A head that never settles normally crosses the provider ceiling inside an
attempt, so the block shows window_provider_budget_exceeded as both reason
and last error, with attempts=1. Test that case instead of one where the
charges align exactly with the ceiling, and correct the Topic Memory docs.
Copilot AI balanced review requested due to automatic review settings November 11, 2026 22:46

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@AlexStocks AlexStocks added the 1.6.0 v1.6.0 release tracking label Nov 12, 2026

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

Reviewed commit 9626a4c. No blocking correctness findings. Returning an attempt after a rolled-back publication head conflict matches RFC 1614: the next attempt rereads the head, real failures still consume attempts, and provider reservations remain charged. No new database tables or interfaces; the existing last_error_code field can now contain head_conflict.

Validation covered SQLite, HTTP replacements racing a spawned Worker, and reopening the database after a conflict on the last available attempt. A child-startup timeout in the broader suite passed on both the base and this head using the same explicit source-path invocation. Native database backends and real model providers were not exercised.

Before merging, resolve the release/upgrade-support.json conflict with current master and clear the macOS Intel Desktop test timeout. That CI failure is in unchanged frontend code; I found no evidence connecting it to this patch. This approval applies to the reviewed commit, not to an unreviewed conflict resolution.

Resolve the release/upgrade-support.json conflict: keep master's schema
review rationale, append this branch's head-conflict sentence, and
recompute persistence_sha256 with scripts/upgrade_release_gate.py.
Copilot AI balanced review requested due to automatic review settings November 12, 2026 19:39

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@Fengzdadi

Copy link
Copy Markdown
Contributor Author

@hidb4ai Thanks for the review. I merged current master into this branch (merge commit 3b2dd04, no rebase). The only conflict was the persistence digest in release/upgrade-support.json. I kept master's rationale, appended this PR's head-conflict sentence, and recomputed persistence_sha256 with scripts/upgrade_release_gate.py's managed_digest (now 3d98977e…). Apart from those two lines, this PR's diff against master is identical to the one you approved on 9626a4c.

Checks run locally after the merge, all on SQLite:

The earlier red check was desktop-validation on macos-15-intel: desktop/ui/tests/memory.test.tsx:369 hit its 5 s timeout. This PR doesn't touch that code; the new push triggers a fresh run.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.6.0 v1.6.0 release tracking

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants