# Blackboard and Fork

Solution Write-up: AI Mathematical Olympiad Progress Prize 3
Competition:
[AIMO Progress Prize 3](https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/overview)
Submission basis:
Private leaderboard (private leaderboard score 41.5)
Kaggle notebook:
[For final submission (verbose log output not reduced)](https://www.kaggle.com/code/ippeiogawa/fork-v10timecforshort110-norej-critique-a8i4e4s2?scriptVersionId=311356082)
,
[Scored public score 46 (version 13); edited v17 for readability but could not debug after H100 access ended](https://www.kaggle.com/code/ippeiogawa/critique-rlm-like-cyicha-33-120b-a4e4?scriptVersionId=313459182)
Added Links:
[Google Colab Notebook for final submission code](https://colab.research.google.com/drive/1XxkyvJ8r2WxeXDwvVPA1lWmmvOXlpgfY?usp=sharing)
[model in HuggingFace, which is MXFP4 of GPT-OSS120B](https://huggingface.co/unsloth/gpt-oss-120b)
[Source of Figure 2: Generated text](https://drive.google.com/file/d/1T_d6Ljiom2yJSEnjVl2bfy4xEGEF7ZiM/view?usp=sharing)
[Google Colab Notebook to create Figure 3,4,5 and A1](https://colab.research.google.com/drive/1_JMEGmA_-_xEWWS9cQ8V4DGlCra5kXmg?usp=sharing)
[Kaggle Notebook to create Figure 6](https://www.kaggle.com/code/ippeiogawa/aimo3publicscoregraph)
[Source of Appendix B: Generated text](https://drive.google.com/file/d/1QOGRTTLJwJetDjcy8hxxY80eJh9GvLUO/view?usp=sharing)
Rewritten on May 17, 2026 after rebuttal phase:
Private leaderboard score 41.5 was updated on Submission basis.
§3.1.2.1 was added.
In §4.3, the line started with
bb_read()
(the pull system) was changed to express twin-failure-mode framing explicitly.
Acknowledgements
Thanks to Simon Frieder, Ryan Holbrook, and the Kaggle staff for organising; to XTX Markets for hosting; to Fields Model Initiative and Tinker for computational resources; and to many competitors for discussions.
I learned a great deal from:
[andreasbis](https://www.kaggle.com/andreasbis)
—
[starter notebook using GPT-OSS 120B with tool use](https://www.kaggle.com/code/andreasbis/aimo-3-gpt-oss-120b-with-tools)
[huikang](https://www.kaggle.com/huikang)
—
[streaming inference notebook](https://www.kaggle.com/code/huikang/streaming-inference/notebook)
, dataset and its viewer, leaderboard monitoring system
[nurikw3](https://www.kaggle.com/nurikw3)
—
[Recursive Language Models (RLM) implementation](https://www.kaggle.com/code/nurikw3/aimo3-rlm)
for long chain-of-thought inference, which suggested the idea of extracting part of a long reasoning trace
Additional reference datasets:
[threerabbits](https://www.kaggle.com/threerabbits)
—
[CrystalMath dataset](https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/672592#3407319)
[nguyennguyen599](https://www.kaggle.com/nguyennguyen599)
—
[Astral Math dataset](https://www.kaggle.com/datasets/nguyennguyen599/astral-math-v1)
[wenliangtlh](https://www.kaggle.com/wenliangtlh)
—
[AIMO3-High-Difficulty-Tool-Calling-Dataset](https://www.kaggle.com/datasets/wenliangtlh/aimo3-high-difficulty-tool-calling-dataset)
Abstract
We present an inference-time coordination system for mathematical olympiad problem-solving, built on top of OpenAI GPT-OSS 120B, served without fine-tuning. The core contribution is a shared-memory mechanism ("Blackboard") that propagates intermediate computational discoveries between parallel inference threads. We show empirically that what is shared matters substantially: sharing reasoning text—which often includes predicted answers—leads to blind copying of answers, whereas sharing only recent tool inputs and outputs produces better outcomes. We further introduce a per-thread forking mechanism for answer verification, in which each thread that produces a candidate answer spawns an independent copy; both branches continue reasoning to confirm or revise. Analysis of over 3,000 answer-producing thread instances across 328 problems shows that double-checking recovers incorrect answers approximately four times more often than it corrupts correct ones. The system achieved the highest public leaderboard score of 46 at its peak, with an average of approximately 40–42 and 41 for the final submission.
1. Introduction
Automated mathematical reasoning at the olympiad level remains one of the most challenging benchmarks for large language models. The AIMO Progress Prize competition series benchmarks progress on olympiad-level problems, where solutions often require chained deductions, creative problem decompositions, and exact integer answers.
The central question we address is: given an open-source reasoning model capable of solving most of the problems and a fixed time budget, how should multiple parallel inference attempts coordinate to maximise the probability of producing a correct answer? We explore this through three mechanisms: (1) a shared-memory Blackboard for propagating computational discoveries between threads, (2) a per-thread forking and double-checking protocol for answer verification, and (3) adaptive resource management for sandboxed Python tool use. The combination achieves a notable improvement.
A secondary challenge throughout this work was evaluation under high score variance: with only 50 problems, which makes it difficult to attribute observed changes to specific interventions. We discuss this limitation explicitly and qualify all comparisons accordingly.
2. System Overview
Figure 1 illustrates the full inference pipeline for a single problem.
![Strategy diagram for final submission](assets/7b76f21b67612afa50aee8d6.svg)
Figure 1. Inference pipeline for final submission. Threads share a Blackboard; each forks independently on first answer for double-checking. Individual and global stop conditions govern termination.
2.1 Model and Serving Environment
Model:
OpenAI GPT-OSS 120B (MXFP4 quantized). Kaggle model card:
[danielhanchen/gpt-oss-120b](https://www.kaggle.com/models/danielhanchen/gpt-oss-120b/Transformers/default)
. Original model on HuggingFace:
[openai/gpt-oss-120b](https://huggingface.co/openai/gpt-oss-120b)
.
Serving:
vLLM [version: 0.11.2], running on Kaggle's GPU environment (1× NVIDIA H100). Key vLLM parameters:
kv_cache_dtype='fp8_e4m3'
,
gpu_memory_utilization=0.96
. No fine-tuning was applied.
Sampling:
Temperature 1.0,
min_p
0.02, with streaming.
Prompt format:
The model uses the openai_harmony chat template. A system prompt instructs the model to use Python for computation; some threads are permitted extended time limits.
Global system prompt:
"You are a world-class International Mathematical Olympiad (IMO) competitor. The final answer must be a non-negative integer between 0 and 99999. You must place the final integer answer inside \boxed{}."
Tool usage instruction:
Shorter version:
"Use this tool to execute Python code. The environment is a stateful Jupyter notebook. You must use print() to output results. Default time limit is 6s. You have access to
math
,
numpy
and
sympy
to solve the problem."
Longer version:
"Use this tool to execute Python code. The environment is a stateful Jupyter notebook. You must use print() to output results. Default time limit is 6s. To change the time limit, insert the desired number after each tool call
analysis to=python code
, for example for 20s,
analysis to=python code20
, then your code. Set this in every tool call if you want, since it is automatically reset after each cell execution. Time limit should not exceed 60s. Do not set it too high because time is limited. You have access to
math
,
numpy
,
sympy
,
pulp
,
ortools
,
pysat
,
mip
and
z3
to solve the problem. Suppress log output. When you use pulp, set msg=False. When you use cp_model.CpSolver() from ortools.sat.python, set cp_model.CpSolver().parameters.log_to_stdout = False. When you use m=mip.Model(), set m.verbose=0. Manage total run time not to exceed the tool environment time limit."
Half of the threads use the shorter prompt version and the other half use the longer version. Tool execution is implemented with adaptive time limit control (see notebook for details). This adaptive time limit was motivated by the observation that, for some problems, tool calls frequently time out within 6 seconds. However, always allowing longer limits results in unnecessary delays on irrelevant tool executions.
2.2 Thread Structure
Each problem is allocated to 4 parallel inference threads. Threads are implemented with
concurrent.futures.ThreadPoolExecutor
sharing a single vLLM engine.
Initialisation:
16 Python sandboxes are pre-initialised and held in a queue at the start of each problem — 2 per thread (one active, one reserved for a potential fork).
Forking:
When a thread produces its first candidate answer, it forks independently. The original thread and the forked copy both continue reasoning, treating the candidate answer as a hypothesis to verify or revise. This happens on a per-thread basis: other threads that have not yet produced an answer continue unaffected.
Token trimming on fork:
If the forked thread's context exceeds 20,000 tokens, the oldest tokens (pre-dating the most recent 20,000) are dropped to maintain inference speed, since longer contexts increase per-step latency substantially.
Maximum threads:
8 (in the extreme case where all 4 threads fork).
2.3 Stop Conditions
Stop conditions operate at two levels:
Individual (per-thread):
A thread halts when it produces 3 consecutive identical answers. This does not affect other threads.
Global (all threads):
All threads halt when either (a) the weighted vote count for a single answer reaches 4, where a double-checked answer within a thread counts as 1.5 votes, or (b) the per-problem time limit is reached.
2.4 Answer Selection
At termination, the final answer is selected by majority vote across all threads' last reported answers. On a tie, the answer from the thread with lower prediction entropy (computed from the token log-probabilities of the answer-producing generation step) is preferred.
2.5 Sandbox Management
Each thread's Python sandbox is an isolated subprocess. The sub-sandbox (reserved for forking) runs only import statements and function definitions to avoid heavy CPU load. When a thread forks, the fork inherits the parent's variable state via
globals()
serialisation and takes the reserved sandbox.
Sandboxes are health-checked on every tool call: a probe is sent to the subprocess, and if no response arrives within a timeout, the sandbox is replaced from the queue. All sandboxes are destroyed and re-initialised between problems to prevent state leakage and accumulation of crashed processes.
2.6 Tool Use
Pre-installed Python libraries available to the model:
pulp
,
ortools
,
pysat-solver
,
z3-solver
, plus the standard scientific Python stack (
numpy
,
sympy
,
scipy
). The GPT-OSS model naturally attempts to use constraint solvers on combinatorial problems; pre-installation avoids failed import attempts.
Tool execution time is adaptively capped between 6 and 60 seconds depending on an explicit time directive in the prompt (see "Longer version" tool instruction in Section 2.1) or extraction of
time_limit=**
from the code script. The lower bound prevents trivially fast tools from hogging the response loop; the upper bound prevents constraint-solver runs from consuming the entire problem time budget. When a tool call exceeds the cap, it is interrupted and a timeout message is returned to the model with intermediate outputs.
2.7 Time Management
Total time per problem varies between 300 and 950 seconds, allocated dynamically based on the remaining total budget. The first 8 problems each receive at least 800 seconds to handle the possibility of difficulty front-loading in the test set. When the per-problem deadline is within approximately 60 seconds, a prompt is injected instructing the model to commit to its best current answer. Without this, since GPT-OSS is well trained to verify answers thoroughly before committing, threads tend to continue indefinitely. The system targets completion well under the 5-hour scoring limit; in practice, wall-clock times varied between approximately 4h 15m and 5h 10m across runs.
3. Key Components
3.1 Blackboard: Inter-Thread Information Sharing
Motivation.
When a problem is difficult, the probability that any single thread finds the correct reasoning path is low. However, if one thread discovers a useful intermediate result — a correct bound, a working code approach, a key insight — sharing it with other threads can efficiently guide those threads without requiring them to rediscover the same result independently. The Blackboard system is designed to enable this sharing.
Implementation.
The Blackboard is a shared
sqlite3
database, chosen because it is thread-safe and allows concurrent reads and writes without blocking. All threads read from and write to the same database file on disk.
Two sharing modes were explored: push (automatic injection) and pull (model-driven retrieval).
3.1.1 Push Mode (Final Design)
After each tool call completes, a portion of another thread's recent tool input and output — the last 500 characters in the final submission (ranging from 400–800 characters across earlier notebook versions — is automatically inserted into the current thread's context immediately following the tool response. The model sees this as a system message formatted as:
### Concurrent discoveries from other agents:

Attempt{k} discoveries:
{recent tool code and output from thread k}
Figure 2 shows a successful example: one thread's z3-solver output (finding that m=243 is the threshold) is injected into a peer thread, which immediately redirects its reasoning toward targeted verification rather than independent search.
Injected:
<|start|>system
<|message|>### Concurrent discoveries from other agents:

Attempt3 discoveries:
vars = {n: Bool(f"x{n}") for n in range(3, m+1)}
s = Solver()
for a in range(3, m+1):
    for b in range(3, m+1):
        c = a*b
        if c > m: continue
        s.add(Not(And(vars[a], vars[b], vars[c])))
        s.add(Not(And(Not(vars[a]), Not(vars[b]), Not(vars[c]))))
return s.check() == sat

for m in range(240, 247):
    print(m, is_colorable(m))

240 True  241 True  242 True  243 False  244 False  245 False  246 False
(Elapsed 1.7s / Limit 6s)

<|end|>
Response:
<|start|>assistant
<|message|>Great! The other concurrent agent found threshold around 243.
So minimal m where unsatisfiable appears is 243.
Let's verify ourselves using CP-SAT for m=242 and 243.
Figure 2. Successful push: a peer thread's z3-solver result is inserted, leading the receiving thread to redirect from independent search to verification. Correct answer: 243 (problem pacore-00181 from the
[CrystalMath dataset](https://www.kaggle.com/competitions/ai-mathematical-olympiad-progress-prize-3/discussion/672592#3407319)
).
Based on manual observation, when injected information is not relevant to the current thread's reasoning path, it is typically ignored. The model occasionally shows brief distraction ("let me note what the other agent found…") before continuing its own approach.
3.1.2 Sharing Topology
The sharing topology for the final submission follows a ring structure: each thread receives pushes from exactly 2 peers, offset by +1 and +2 modulo 4, with a slight difference for forked threads. Concretely:
Thread
Receives from
0
1, 2
1
2, 3
2
3, 0
3
0, 1
4 (fork from 0)
1, 5
5 (fork from 1)
2, 6
6 (fork from 2)
3, 7
7 (fork from 3)
0, 4
After many experiments varying which threads and how many to receive from — including random selection and receiving from only one thread — no clearly superior topology was found. Receiving from many threads offers the advantage of diverse information, but also risks interrupting a thread's own reasoning. Receiving from one or two peers appeared to perform better, though further research is needed. The ring topology ensures no thread is isolated while preventing any single thread's output from reaching all others simultaneously.
Sharing reasoning text — which often includes predicted answers — caused threads to simply copy the first answer produced, regardless of correctness. This is further discussed in 3.1.2.1 "Why tool I/O, not reasoning text".
Shared content
Observed behaviour
Reasoning text (often including predicted answer)
Blind copying of answer; errors propagate
Recent tool I/O, ring topology
Better results; errors less likely to cascade
3.1.2.1  Why tool I/O, not reasoning text
Several approaches share
reasoning text
between threads — either raw chain-of-thought, a model-generated summary, or the conversation history preceding a branch point. We chose to share only tool I/O events. This subsection states the empirical evidence and the mechanistic reasons.
Empirical evidence.
In an experiment, we shared each tool I/O event together with the few hundred characters of reasoning text immediately preceding it, hypothesizing that the context would help recipients interpret the result. Public score dropped from an average around 41 (I/O-only) to 37 (I/O + preceding context) — lower than the lowest of several I/O-only trials, albeit on a single run. We take this as an indication that the text component was actively harmful rather than merely redundant.
Mechanism 1: non-directionality of code.
Shared reasoning text routinely contains assertive closers — "so the answer is X", "therefore…", "this confirms…" — and the model's natural continuation tendencies favour agreement with such assertions when they appear as quasi-demonstrations in context. Code carries no such closers: a snippet that returns
242
reports a value, not a verdict. In our traces, recipients of shared tool I/O reliably
verify
before adopting an implication of the result (see the very last line of Fig. 2, where the recipient model responds to the I/O insertion with "Let's verify ourselves using CP-SAT for m=242 and 243"); recipients of shared text typically continue from the text's conclusion without re-verification. This is, we believe, the reason text sharing produces a wrong-answer collapse and tool-I/O sharing does not.
Mechanism 2: information density and context economy.
A short Python snippet plus its output can encode an algorithmic attempt, a verification, or a pattern probe in a few hundred characters; the natural-language reasoning needed to convey the same intent is typically several times longer. Long insertions also distract the recipient from its own reasoning thread — even if the recipient ultimately judges the insertion irrelevant, recovering its prior context is non-trivial. Short code minimises both costs, and
gpt-oss-120b
reads the intent of short code fluently.
Mechanism 3: built-in salience.
Every tool I/O event marks a discrete reasoning step the producing thread judged worth executing — a calculation, a verification, a hypothesis test. The set of "events worth broadcasting" is therefore self-defining, requiring neither a learned filter nor an auxiliary summarization pass (the latter would also be expensive under our inference budget).
Taken together, tool I/O changes both the
signal
shared between threads (no embedded verdict) and the
behaviour
it induces in recipients (verification rather than continuation). Reasoning-text sharing modifies neither, which leads to wrong-answer collapse.
3.1.3 Pull Mode (Discarded)
An alternative mode allowed threads to actively retrieve peer discoveries on demand by calling
bb_read()
("Blackboard_read") from within the Python sandbox:
# Injected into each sandbox's initialisation
import sqlite3, json

def bb_read(qid, key):
    conn = sqlite3.connect("aimo_blackboard.db")
    r = conn.execute(
        "SELECT value FROM bb WHERE qid=? AND key=?",
        (qid, key)
    ).fetchone()
    conn.close()
    return json.loads(r[0]) if r else None
When
bb_read()
was mentioned once in the initial system prompt, it was called very rarely — once or twice across all threads over dozens of problems. To encourage more frequent use, a call snippet was inserted alongside every push message. This produced the opposite failure: threads repeatedly called
bb_read()
for all peer threads in sequence, spending most of their token budget reading others' conversations rather than reasoning (Appendix B). The pull system was therefore discarded entirely.
3.2 Double-Check via Forking
Motivation.
During early trials on a difficult reference problem (target answer: 8687), we observed that even after a thread produced an incorrect answer, prompting it with "The answer you provided was wrong. Try again." sometimes led to the correct answer. From this observation, several prompt variants were tested.
Mechanism.
When a thread first produces an answer, it forks independently: the original thread and a copy both continue reasoning, each treating the candidate answer as a hypothesis to confirm or revise. Each follows its own chain of thought, runs its own tool calls, and stops under the same individual stop condition (3 consecutive identical answers). Other threads that have not yet produced an answer continue unaffected.
The double-check prompt is selected from the following options:
Main prompts:
"The answer you provided was wrong. Try again."
(aggressive) — higher recovery rate, but higher risk of destabilising a correct answer.
"Please double check your answer."
(mild)
Optional sub-prompts
(appended to the mild prompt):
"Can you construct a counterexample? What assumption could be wrong?"
"Before introducing symbolic parameters or primitive vectors, check whether the problem has a finite set of integer or geometric configurations that can be enumerated directly. Avoid premature parameter reduction."
— excluded from the final submission as too problem-specific.
Empirical analysis.
We analysed 3,497 answer-producing thread instances across 328 problems. Figure 3 shows the breakdown of outcome categories.
![Figure 3: distribution of outcome categories](assets/a97e8fb67ac353e3fdcd93bb.png)
Figure 3. Distribution of outcome categories: Correct_single (no fork), Wrong_single (no fork), Correct→Correct, Wrong→Wrong, Wrong→Correct (WC), Correct→Wrong (CW), and rare multi-change sequences.
Correct-single and Wrong-single instances were not double-checked due to the time limit or a global majority stop. The key finding is that wrong-to-correct (WC) transitions occur approximately 4× more often than correct-to-wrong (CW) transitions, suggesting that well-reasoned correct answers are more robust to verification pressure than poorly-reasoned wrong answers. Multi-change sequences (C→W→C or W→C→W) were rare enough to be negligible.
WC transitions occur in approximately 4% of threads on a moderately difficult problem set. Extrapolated to 50 test problems with up to 8 threads each (400 thread instances total), this is a small absolute count, but on hard problems — where perhaps 10 or more problems in the test set may be at the difficulty level where WC transitions occur at higher rates — one or two problems out of 50 may be rescued per run. The prediction is rough, and one or two problems is within the base score variance, but the direction is positive.
Prompt comparison.
Figure 4 compares the two main prompts. The aggressive prompt achieves a higher WC rate (13.9% vs 4.9%) but also a higher CW rate (4.0% vs 1.8%). Figure 5 compares sub-prompt variants within the mild-prompt family; the counterexample sub-prompt improves the WC rate without a proportionate increase in the CW rate.
![Figure 4: grouped bar chart comparing WC% and CW% for aggressive vs mild prompt](assets/6350e41c63588b6c25d0633d.png)
Figure 4. Grouped bar chart comparing WC% and CW% for aggressive vs. mild prompt. N labelled per condition.
![Figure 5](assets/48014ff614a838c6f4a56156.png)
Figure 5. Grouped bar chart comparing WC% and CW% for mild, mild+counterexample, and mild+structure-check. N labelled per condition.
The prompt for each double-check call in the final submission is selected from aggressive, mild, or mild+counterexample based on a simple heuristic rule.
This analysis was conducted on data from recent runs, not a designed experiment: problem distributions and prompt assignments were not balanced across conditions. A problem-weighted normalisation (each problem's CC, CW, WC and WW rates are computed per-problem and weighted by frequency) leaves the results qualitatively unchanged; see Appendix A.
3.3 Constraint Solver Support
For combinatorial and discrete optimisation problems, GPT-OSS 120B naturally attempts to use constraint-solving libraries. Pre-installing
pulp
,
ortools
,
pysat-solver
, and
z3-solver
avoids import-time failures and latency. Threads are selectively permitted to use these libraries via their system prompts. Because these libraries can run for many seconds or cause sandbox instability, tool calls were given flexible time limits and sandbox health was monitored continuously.
4. Ablation Studies and Score History
Note on variance:
With only 50 public test problems, score differences of 1–3 points are within noise. All comparisons below are directional, not statistically definitive.
The best observed public score from this work was 46, which was the highest on the public leaderboard at the time. The final submitted notebook, which incorporated the fixes described in Section 4.3, scored 41 on the public leaderboard with an average of approximately 40–42 across repeated runs of similar strategies.
4.1 Score History and Strategy Comparison
Figure 6 shows our public leaderboard score history across all submissions, grouped by identical notebook versions.
![Figure 6](assets/d63d0d4e7998611f8c2de6ca.png)
Figure 6. Public score history. Identical versions of the same notebook are grouped with the same markers (e.g., Notebook #73 was submitted five times and received public scores [45, 46, 39, 39, 42], shown with green circles). The notable jump corresponds to the introduction of the Blackboard push system, which was preliminarily tested in Notebook #73 and fully applied in Notebook #76 (alongside the simultaneous RLM-like pull system).
A clear score increase was observed when the Blackboard push and pull systems were introduced simultaneously, making individual attribution difficult. Since the pull system was later discarded without a clear drop in average score, the push system is believed to be the main contributor — though this could not be confirmed statistically.
4.2 Thread Count Variants
Two thread-count variants were compared:
Variant
Description
Average score
Peak score
Notebook # in Figure 6
Final (4-thread + fork)
4 threads, fork on first answer
~40–42
46
#76, #93, #105, final submission, and others
Alternative (6-thread, no fork)
6 threads, no fork, early stop at 3 identical
~41–42
43
#78, #101, and others
The alternative showed a slightly higher average score but a lower peak, and was not selected.
4.3 Issues Fixed or Changed Between Best Public Score and Final Submission
The following issues were identified in the notebook that achieved the best public score and were partially or fully fixed before the final submission:
Tools continued executing for up to 60 seconds even after a global majority-vote stop signal was issued.
Forked threads started with a fresh sandbox, losing all parent variable definitions and causing frequent
NameError
failures on tool calls. Variable inheritance via
globals()
serialisation was added.
bb_read()
(the pull system) was called very rarely without explicit per-step prompting, and called excessively when such prompting was added — the model began querying siblings in place of its own reasoning. We could not find a useful middle ground, and it was discarded entirely.
Sandboxes using heavy constraint libraries (
ortools
, etc.) sometimes became unresponsive with no error returned; health-check replacement was made more aggressive.
The number of Blackboard-sharing peers per thread was limited to 1 in the best-scoring public notebook (e.g., T0→T1, T1→T2, …) and increased to 2 in the final submission (e.g., T1 & T2→T0, T2 & T3→T1, …). Sharing with more threads was intended to improve solution quality, but this effect was not confirmed.
5. Known Limitations
Score variance:
50 test problems is too few for reliable ablation at the resolution of 1–2 score points. All reported score differences should be interpreted with caution.
Prompt sensitivity:
The double-check prompt study is based on unbalanced observational data, not a controlled experiment. Problem difficulty varies across conditions.
Time management:
Wall-clock completion times varied, with some runs approaching or slightly exceeding the 5-hour limit. More robust time management is needed.
6. Conclusion
We presented an inference-time coordination system for mathematical olympiad reasoning that combines a shared Blackboard for propagating intermediate computational discoveries, a per-thread forking protocol for answer verification, and adaptive resource management for Python tool use. The most impactful contribution is believed to be the Blackboard push system — automatically inserting a portion of another thread's recent tool activity into the current thread's context in a ring topology — combined with the per-thread forking system for answer double-checking. Analysis of over 3,000 answer-change events shows that double-checking recovers incorrect answers approximately 4× more often than it corrupts correct ones, providing quantitative support for the verification approach. Adaptive sandbox management and constraint-solver support contributed to robustness.
Time management is a central constraint: double-checking and heavy solver libraries both consume significant time, and starting with 8 threads was not affordable. Starting with 4 threads (forking to at most 8) was the selected balance.
Improving thread coordination, reducing evaluation variance through larger reference problem sets, and more robust sandbox management under heavy solver load remain open directions.
References
Recursive Language Models. Zhang et al.
[https://arxiv.org/html/2512.24601v1](https://arxiv.org/html/2512.24601v1)
Appendix A: Problem-Weighted Double-Check Analysis
The double-check prompt comparison in Section 3.2 uses unbalanced observational data (different problems appear at different rates across prompt conditions). To control for problem difficulty, per-problem WC and CW rates are computed for each problem and weighted by frequency. The weighted results are qualitatively consistent with the unweighted analysis: the rank ordering of prompts by CC (Correct→Correct), CW (Correct→Wrong), WC (Wrong→Correct), and WW (Wrong→Wrong) rate is unchanged, and approximate magnitudes are similar.
![Figure A1](assets/a436d98224b6d94a1f0b91bd.png)
Figure A1. Weighted CC%, CW%, WC%, and WW% per prompt type. Data count and problem count for each prompt are shown as n=*|**problems. Prompt sets not shown in Figure 5 due to low data count are included here.
Appendix B: Example of Excess Use of bb_read()
As described in Section 3.1.3, encouraging frequent use of
bb_read()
by inserting a call snippet alongside every push message produced an adverse effect: threads repeatedly called
bb_read()
for all peer threads in sequence, spending most of their token budget reading others' conversations rather than reasoning independently. An example is shown below.
When Attempt 1 (Thread 1) discoveries were shared using
bb_read("attempt1")
, extracting the "content" and "text" fields as shown below — where the block beginning with "from functools …" represents real Attempt 1 tool I/O —
<|start|>system
<|message|>###Concurrent discoveries from other attempts:

Attempt1 discoveries by running
msgs = bb_read("attempt1")
for m in msgs[-2:]:
    t = m.get("content")
    if t:
        print(t[0].get("text","")[-800:])
:
from functools import lru_cache

# board 5x5, index cells 0..24
size = 5
cell_coords = [(i, j) for i in range(size) for j in range(size)]

# compute subgrids (top-left corners)

..... [Attempt1 tool input and output shared].....

(Elapsed0.0s/Limit6s)
<|end|>
The model then attempted to retrieve more from Attempt 1 via
bb_read
, first checking the message count:
<|start|>assistant
<|channel|>analysis
<|message|>Search more.
<|end|>
<|start|>assistant to=python
<|channel|>analysis code
<|message|>msgs = bb_read('attempt1')
len(msgs)

<|call|>
<|start|>python to=assistant
<|channel|>analysis
<|message|>5
(Elapsed0.0s/Limit6s)
<|end|>
The model then read a message and received output:
<|start|>assistant
<|channel|>analysis
<|message|>Let's view the later messages.

<|end|>
<|start|>assistant to=python
<|channel|>analysis code 0-5 -0.5s 
<|message|>for i,m in enumerate(msgs):
    if m.get('role')!='assistant':
        continue
    print(i, m.get('content')[:500])

<|call|>
<|start|>python to=assistant
<|channel|>analysis
<|message|>2 [{'type': 'text', 'text': 'We have a combinatorial game: two players .........
(Elapsed0.0s/Limit6s)
<|end|>
The model continued reading further messages from Attempt 1:
<|start|>assistant
<|channel|>analysis
<|message|>Let's see further.

<|end|>

......continuing reading attempt1's message......
The model determined that Attempt 1 had not yet found a solution and proceeded to read from Attempt 2, then Attempt 3, then back to Attempt 1:
<|start|>assistant
<|channel|>analysis
<|message|>So attempt1 hasn't solved.

Now attempt2, attempt3? Let's list all attempts.
<|end|>
<|start|>assistant to=python
<|channel|>analysis code 0-8 -0.4s 
<|message|>msgs = bb_read('attempt2')
len(msgs)

<|call|>
<|start|>python to=assistant
<|channel|>analysis
<|message|>29
(Elapsed0.0s/Limit6s)
<|end|>
<|start|>assistant
<|channel|>analysis
<|message|>Let's view attempt2 maybe later messages include final answer.

<|end|>
<|start|>assistant to=python
<|channel|>analysis code 0-9 -0.2s 
<|message|>for i,m in enumerate(msgs):
    if m.get('role')=='assistant':
        print(i, m.get('content')[:200])

<|call|>
<|start|>python to=assistant
<|channel|>analysis
<|message|>2 [{'type': 'text', 'text': 'We have a set S size 35. Let F be a ...........
(Elapsed0.0s/Limit6s)
<|end|>
<|start|>assistant
<|channel|>analysis
<|message|>So attempt2 not relevant.

Attempt3 maybe.

<|end|>
<|start|>assistant to=python
<|channel|>analysis code 0-10 -0.4s 
<|message|>msgs = bb_read('attempt3')
len(msgs)

<|call|>

.........continuing to get Attempt3 and then Attempt1 message again......
The model then attempted to read from Attempt 5. In this way, Thread 4 spent most of its token budget gathering information from other threads rather than reasoning independently. When this behaviour was observed repeatedly in the Blackboard pull system with encouraged
bb_read()
use, further trials with this approach were abandoned.
<|start|>assistant
<|channel|>analysis
<|message|>Let's see further after index 9? Already we looked.

Now attempt1 has some analysis but not final answer.

We also have attempt5? Let's see.

............continuing read Attempt5 message