Skip to content

datamanager should not directly access the S3 DB with creating a session , change it to use wt-data-gateway instead - #39

Merged
BinHuangPJLAB merged 3 commits into
AI45Lab:v2from
BinHuangPJLAB:gateway-bug-fix-1
Aug 2, 2026
Merged

datamanager should not directly access the S3 DB with creating a session , change it to use wt-data-gateway instead#39
BinHuangPJLAB merged 3 commits into
AI45Lab:v2from
BinHuangPJLAB:gateway-bug-fix-1

Conversation

@BinHuangPJLAB

@BinHuangPJLAB BinHuangPJLAB commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Evaluation results can now be saved when no trainable trajectory step exists.
    • Session-step views can retrieve the latest available state.
    • Cloud records are more reliably associated with their processing jobs.
  • Bug Fixes

    • Improved cloud reward commits, completion updates, and session queries.
    • Environment state data is handled consistently during reward recording.
    • Evaluation summaries now preserve reward, metadata, and completion details.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c18e045-7836-44f1-835c-e2d3ef49ac52

📥 Commits

Reviewing files that changed from the base of the PR and between 5bbe94d and 994c7a1.

📒 Files selected for processing (1)
  • core/data_manager/strategy/cloud_strategy_impl.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/data_manager/strategy/cloud_strategy_impl.py

📝 Walkthrough

Walkthrough

The PR adds evaluation-summary persistence, latest-checkout session reads, and job-partitioned cloud record operations. Cloud reward commits handle sessions without trainable trajectory rows.

Changes

Cloud evaluation storage

Layer / File(s) Summary
Data-manager API contracts
core/data_manager/manager.py, core/data_manager/strategy/base_strategy.py
The APIs support checkout_latest and expose record_evaluation_summary.
Partitioned cloud record access
core/data_manager/strategy/cloud_strategy_impl.py, evaluator/trajectory_reader.py
Cloud writes and completion updates track job IDs. Session and metadata queries use job partitions and latest-checkout options.
Evaluation-summary commit flow
core/data_manager/strategy/cloud_strategy_impl.py, evaluator/reward_committer.py
Cloud reward commits create or update terminal, non-trainable evaluation steps when no trainable step exists. Environment-state loading accepts dictionary values directly.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RewardCommitter
  participant DataManager
  participant CloudStrategy
  participant LandingTable
  RewardCommitter->>DataManager: record_evaluation_summary(session, step, reward, env_state)
  DataManager->>CloudStrategy: record_evaluation_summary(...)
  CloudStrategy->>LandingTable: persist terminal evaluation summary
  LandingTable-->>CloudStrategy: persistence result
  CloudStrategy-->>DataManager: record ID
  DataManager-->>RewardCommitter: record ID
Loading

Possibly related PRs

  • AI45Lab/SAfactory#18: Shares the DataManager.list_session_steps and cloud reward integration paths.
  • AI45Lab/SAfactory#31: Introduces related list_session_steps changes that this PR extends with checkout_latest.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes replacing direct S3 database access with wt-data-gateway, which matches the main changes in the pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@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: 4

🧹 Nitpick comments (2)
core/data_manager/strategy/cloud_strategy_impl.py (2)

593-597: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add an explicit strict= to zip().

Ruff flags B905 on this call. records and record_ids are built in the same loop, so strict=True documents the invariant and satisfies the rule.

♻️ Proposed change
-        for record, record_id in zip(records, record_ids):
+        for record, record_id in zip(records, record_ids, strict=True):
🤖 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 `@core/data_manager/strategy/cloud_strategy_impl.py` around lines 593 - 597,
Update the zip call in the record-processing loop to pass strict=True,
documenting that records and record_ids must have equal lengths while satisfying
Ruff B905. Keep the existing job_id mapping and return behavior unchanged.

Source: Linters/SAST tools


847-849: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trace label filter_landing no longer matches the SDK method. Both call sites now invoke self.client.query_landing but still report sdk_operation="filter_landing". Traces and the DLDB metrics log therefore name a method that is no longer called. _timed_db_call also classifies the operation with sdk_operation.startswith("filter"), so renaming the label alone would flip these reads to db_write.

  • core/data_manager/strategy/cloud_strategy_impl.py#L847-L849: rename the label to query_landing in list_session_steps.
  • core/data_manager/strategy/cloud_strategy_impl.py#L958-L960: rename the label to query_landing in mark_latest_session_completed.

Update the read/write classification in _timed_db_call in the same change, for example by matching ("filter", "query") prefixes.

🤖 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 `@core/data_manager/strategy/cloud_strategy_impl.py` around lines 847 - 849,
Rename the _timed_db_call operation label from filter_landing to query_landing
in list_session_steps at
core/data_manager/strategy/cloud_strategy_impl.py#L847-L849 and
mark_latest_session_completed at
core/data_manager/strategy/cloud_strategy_impl.py#L958-L960. Update
_timed_db_call’s read classification to recognize both filter and query prefixes
so query_landing remains classified as a database read rather than db_write.
🤖 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 `@core/data_manager/strategy/cloud_strategy_impl.py`:
- Around line 1139-1159: Make _load_existing_meta_json asynchronous and replace
its direct query_landing call with an awaited _timed_db_call so the database
operation runs off the event loop and is traced. Propagate the async change
through _normalize_session_step_updates_for_cloud, and await the normalizer from
update_session_step.
- Around line 942-950: Update the read that selects the latest persisted step
before sealing it, in the surrounding method containing _job_id_for_session and
the fallback warning, to pass checkout_latest=True. Ensure the same option is
applied to the corresponding reads at the additionally noted locations so the
method observes rows written by other processes and seals the actual newest
step.
- Around line 886-929: Update record_evaluation_summary to create and pass a
detached SessionContext copy to record_step instead of the cached session,
preserving the live session’s total_reward and message_history. Populate the
copy with the same session metadata and current job context, then ensure the
persisted summary row uses the evaluation score for both step_reward and reward,
matching the sqlite path.
- Around line 599-666: Update record_session_close handling around
mark_records_completed so the ValueError raised when job_id is unavailable is
treated as a no-op and still emits the completed status. Preserve existing
behavior for other exceptions, including their current reporting or propagation.

---

Nitpick comments:
In `@core/data_manager/strategy/cloud_strategy_impl.py`:
- Around line 593-597: Update the zip call in the record-processing loop to pass
strict=True, documenting that records and record_ids must have equal lengths
while satisfying Ruff B905. Keep the existing job_id mapping and return behavior
unchanged.
- Around line 847-849: Rename the _timed_db_call operation label from
filter_landing to query_landing in list_session_steps at
core/data_manager/strategy/cloud_strategy_impl.py#L847-L849 and
mark_latest_session_completed at
core/data_manager/strategy/cloud_strategy_impl.py#L958-L960. Update
_timed_db_call’s read classification to recognize both filter and query prefixes
so query_landing remains classified as a database read rather than db_write.
🪄 Autofix (Beta)

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: 968276cf-7f2b-4080-8adb-8cafaaeea94f

📥 Commits

Reviewing files that changed from the base of the PR and between 17ceb21 and 33ae7bf.

📒 Files selected for processing (5)
  • core/data_manager/manager.py
  • core/data_manager/strategy/base_strategy.py
  • core/data_manager/strategy/cloud_strategy_impl.py
  • evaluator/reward_committer.py
  • evaluator/trajectory_reader.py

Comment on lines 599 to 666
async def mark_records_completed(self, record_ids: List[str]) -> int:
"""Mark known landing record IDs completed without scanning session rows."""
"""Mark known landing record IDs completed in their associated HASH buckets."""
await self.init()
unique_ids = list(dict.fromkeys(str(record_id) for record_id in record_ids if record_id))
if not unique_ids:
return 0
if self._enable_buffer:
await self._flush_records()

quoted_ids = ", ".join(f"'{_escape_sql_literal(record_id)}'" for record_id in unique_ids)
await self._timed_db_call(
"update_landing",
self.client.update_landing,
f"id IN ({quoted_ids})",
{
"is_session_completed": True,
"is_terminal": True,
},
trace_context={"record_count": len(unique_ids)},
)
ids_by_job: Dict[str, List[str]] = {}
inferred_ids: List[str] = []
missing_job_ids: List[str] = []
for record_id in unique_ids:
job_id = self._record_job_ids.get(record_id)
if not job_id and self.job_id:
job_id = str(self.job_id)
inferred_ids.append(record_id)
if not job_id:
missing_job_ids.append(record_id)
continue
ids_by_job.setdefault(job_id, []).append(record_id)

if inferred_ids:
log.warning(
"Record-to-job association unavailable for %d landing records; "
"falling back to configured job_id=%s",
len(inferred_ids),
self.job_id,
)
if missing_job_ids:
log.error(
"Cannot mark %d landing records completed without job_id; "
"refusing an all-bucket HASH update",
len(missing_job_ids),
)
raise ValueError(
"job_id is required to mark landing records completed without "
"scanning all HASH buckets"
)

for job_id, job_record_ids in ids_by_job.items():
quoted_ids = ", ".join(
f"'{_escape_sql_literal(record_id)}'"
for record_id in job_record_ids
)
filter_query = (
f"job_id = '{_escape_sql_literal(job_id)}' "
f"AND id IN ({quoted_ids})"
)
await self._timed_db_call(
"update_landing",
self.client.update_landing,
filter_query,
{
"is_session_completed": True,
"is_terminal": True,
},
partition=job_id,
trace_context={
"job_id": job_id,
"record_count": len(job_record_ids),
},
)
for record_id in job_record_ids:
self._record_job_ids.pop(record_id, None)

log.debug("Marked %d known cloud records completed", len(unique_ids))
return len(unique_ids)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find callers of mark_records_completed and their error handling.
rg -nP -C 8 '\bmark_records_completed\s*\(' --type=py

Repository: AI45Lab/SAfactory

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -e

echo "Repository files matching cloud_strategy_impl.py:"
fd -a 'cloud_strategy_impl\.py$' . || true

echo
echo "Git status/stat:"
git diff --stat || true
git status --short || true

echo
echo "Search mark_records_completed in tracked files:"
git ls-files | xargs grep -n "mark_records_completed\|ValueError" | sed -n '1,200p'

echo
echo "Inspect target file outline/section if present:"
target=$(fd 'cloud_strategy_impl\.py$' . | head -n1)
if [ -n "$target" ]; then
  echo "target=$target"
  wc -l "$target"
  sed -n '560,680p' "$target" | nl -ba -v 560
fi

Repository: AI45Lab/SAfactory

Length of output: 20020


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
target = Path("core/data_manager/strategy/cloud_strategy_impl.py")
print("mark_records_completed occurrences in cloud_strategy_impl.py:")
lines = target.read_text().splitlines()
for i,l in enumerate(lines, 1):
    if "mark_records_completed" in l or "raise ValueError" in l or "'job_id is required' in" not in l:
        if i == 634:
            # do not print unrelated lines
            pass
print("  (occurrences already listed above; no direct caller in cloud_strategy_impl.py except no-op context)")

for name, path in (
    ("data_manager_manager", Path("core/data_manager/manager.py")),
    ("gateway_storage", Path("gateway/storage.py")),
):
    p = Path(path)
    if p.exists():
        print(f"\n--- {path} ---")
        for i,l in enumerate(p.read_text().splitlines(), 1):
            if "mark_records_completed" in l:
                start=max(1,i-12); end=min(p.read_text().count("\n") + 1, i+20)
                for j in range(start,end+1):
                    prefix=f"{j:5d} "
                    print(prefix + lines[j-1] if name=="cloud_strategy_impl" else p.read_text().splitlines()[j-1])
PY

echo
echo "All direct calls to mark_records_completed in tracked Python paths:"
python3 - <<'PY'
import sys, ast
from pathlib import Path
matches=[]
for p in Path(".").rglob("*.py"):
    if p.parts[0] in {".git","env","evaluator"}:
        continue
    try:
        s=p.read_text()
    except Exception:
        continue
    try:
        t=ast.parse(s)
    except Exception:
        continue
    for node in ast.walk(t):
        if isinstance(node, ast.Await) and isinstance(node.value, ast.Call):
            call=node.value
        elif isinstance(node, ast.Call):
            call=node
        else:
            continue
        func=call.func
        if isinstance(func, ast.Attribute) and func.attr == "mark_records_completed":
            line_no=node.lineno
            context=max(0,line_no-10); context2=min(len(s.splitlines()),line_no+12)
            matches.append((str(p), line_no, s.splitlines()[line_no-1], context, context2))
for m in matches:
    file, line, stmt, context, context2 = m
    print(f"\nFile: {file}:{line}")
    print("  call:", stmt)
    for j in range(context+1, context2+1):
        print(f"  {j:5d}: {s[j-1]}" if (s:=(Path(file).read_text().splitlines()) if False else None) is not None else)
PY

echo
echo "Inspect gateway/storage.py around direct call:"
sed -n '410,450p' gateway/storage.py

Repository: AI45Lab/SAfactory

Length of output: 4632


🏁 Script executed:

#!/bin/bash
set -eu

echo "Direct callers with surrounding control-flow:"
python3 - <<'PY'
from pathlib import Path
import ast
targets = []
for p in Path(".").rglob("*.py"):
    if str(p).startswith((".git/", "env/", "evaluator/")):
        continue
    try:
        s = p.read_text()
        t = ast.parse(s)
    except Exception:
        continue
    for i, node in enumerate(ast.iter_child_nodes(t)):
        if isinstance(node, ast.Await) and isinstance(node.value, ast.Call):
            if isinstance(node.value.func, ast.Attribute) and node.value.func.attr == "mark_records_completed":
                targets.append((p, node.value.func.value.lineno, node.value.func.value.col_offset, node.value.func, node))
        elif isinstance(node, ast.Call):
            if isinstance(node.func, ast.Attribute) and node.func.attr == "mark_records_completed":
                targets.append((p, node.func.value.lineno, node.func.value.col_offset, node.func, node))
lines_cache = {}
for p, outer_line, col, func, node in targets:
    print(f"\nFile {p}:{outer_line}")
    src = lines_cache.get(p)
    if src is None:
        src = p.read_text().splitlines()
        lines_cache[p] = src
    print(f"  inner={func.lineno}:{func.col_offset} col={col}")
    print(f"  expr={src[func.lineno-1]}")
    print(f"  wait={src[node.lineno-1]}")
PY

echo
echo "Inspect storage.py session_close path:"
sed -n '380,470p' gateway/storage.py

echo
echo "Inspect manager.py callers around mark_records_completed:"
grep -n "mark_records_completed" core/data_manager/manager.py gateway/storage.py

Repository: AI45Lab/SAfactory

Length of output: 5078


🏁 Script executed:

#!/bin/bash
set -eu

echo "gateway/storage.py record_session_close completion path:"
sed -n '470,540p' gateway/storage.py

echo
echo "gateway/storage.py broader error handling for record_session_close callers:"
rg -n -C 6 "record_session_close|except|ValueError" gateway/app.py || true

echo
echo "Inspect cloud strategy init/job_id relevant section:"
sed -n '1,180p' core/data_manager/strategy/cloud_strategy_impl.py
sed -n '620,645p' core/data_manager/strategy/cloud_strategy_impl.py
sed -n '160,230p' core/data_manager/strategy/base_strategy.py
sed -n '130,150p' core/data_manager/manager.py

Repository: AI45Lab/SAfactory

Length of output: 21758


Handle ValueError from direct session-close callers.

gateway/storage.py calls self.data_manager.mark_records_completed(record_ids) under a generic except Exception block, but it immediately re-raises before reporting the completed status. If cloud storage uses this new ValueError, record_session_close currently propagates as a failure for the whole session-close call. Keep the existing path behavior or add a handling path that treats this as a no-op and emits completed status instead of raising.

🤖 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 `@core/data_manager/strategy/cloud_strategy_impl.py` around lines 599 - 666,
Update record_session_close handling around mark_records_completed so the
ValueError raised when job_id is unavailable is treated as a no-op and still
emits the completed status. Preserve existing behavior for other exceptions,
including their current reporting or propagation.

Comment thread core/data_manager/strategy/cloud_strategy_impl.py
Comment thread core/data_manager/strategy/cloud_strategy_impl.py
Comment on lines +1139 to 1159
def _load_existing_meta_json(
self,
filter_query: str,
*,
job_id: Optional[str] = None,
) -> Dict[str, Any]:
meta_json: Dict[str, Any] = {"source": "AIEvoBox"}
if not job_id:
log.warning(
"Loading landing meta_json without job_id; "
"falling back to an all-bucket HASH query"
)
try:
df = self.client.session.filter(
self.client.config.tables.landing_table,
query=filter_query,
df = self.client.query_landing(
filter_query=filter_query,
limit=1,
columns=["meta_json"],
partition_cond=None,
partition=job_id or None,
checkout_latest=True,
as_dataframe=True,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

This blocking DB query runs on the event loop.

_load_existing_meta_json is synchronous and calls self.client.query_landing directly. The call chain is update_session_step (async, line 777) → _normalize_session_step_updates_for_cloud → this method. Every other DB call in this class goes through _timed_db_call, which offloads to a thread with asyncio.to_thread. This one does not, so it stalls the event loop for the query duration and produces no latency trace.

Make this helper async, await _timed_db_call, and await the normalizer from update_session_step.

🤖 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 `@core/data_manager/strategy/cloud_strategy_impl.py` around lines 1139 - 1159,
Make _load_existing_meta_json asynchronous and replace its direct query_landing
call with an awaited _timed_db_call so the database operation runs off the event
loop and is traced. Propagate the async change through
_normalize_session_step_updates_for_cloud, and await the normalizer from
update_session_step.

@BinHuangPJLAB BinHuangPJLAB changed the title Gateway bug fix 1 datamanager should not directly access the S3 DB with creating a session , change it to use wt-data-gateway instead Aug 1, 2026
columns=["step_id", "is_session_completed", "meta_json", "agent_model"],
partition_cond=None,
partition=job_id or None,
checkout_latest=False,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这个checkout_latest传false是否正确,需要确认一下。safactory其他地方都传的是true。
checkout_latest是lancedb提供的字段,他的含义可以简单理解为:
False:允许使用当前 dldb session 已经看到的表版本,开销较小。
True:查询前确保看到最新表版本,适合其他 worker/process 刚写完、当前进程马上要读的场景。

Comment thread core/data_manager/strategy/base_strategy.py
Comment thread core/data_manager/strategy/cloud_strategy_impl.py
self.client.session.filter,
self.client.config.tables.landing_table,
query=query,
self.client.query_landing,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

query_landing这个方法在下午的讨论里改掉了(因为在这个pr之前没有人用这个方法T_T),现在统一用query_data(),用法一样。调整一下

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
core/data_manager/strategy/cloud_strategy_impl.py (1)

853-880: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

list_session_steps drops the request field on read; verify the query_landing method name.

Two concerns in this block:

  1. meta_fields now includes "request" (Line 1100) and _build_step_record writes it into meta_json (Line 727). fetch_done_steps_with_context reads it back at Line 1479. list_session_steps, however, only extracts env_state, group_id, dataset, and is_trainable from meta (Lines 871-878); it never sets row["request"]. Any consumer of list_session_steps rows that expects request (mirroring the other read path) gets nothing.
  2. A previous human reviewer noted that query_landing was replaced with query_data() during offline discussion, with an equivalent call signature. This call site (Line 853) still uses query_landing, along with the other two call sites at Lines 964 and 1156.
🐛 Proposed fix for issue 1
             meta = _json_object(row.pop("meta_json", None))
             row["llm_model"] = row.pop("agent_model", None)
             row["env_state"] = _json_object(meta.get("env_state"))
             row["group_id"] = meta.get("group_id")
+            row["request"] = meta.get("request")
             if "dataset" in meta:
                 row["dataset"] = meta["dataset"]
🤖 Verification script for the `query_landing`/`query_data` question
#!/bin/bash
# Description: Confirm the current wt-data-gateway client method name for landing queries.
rg -n 'def query_landing|def query_data' --type=py -g '!core/data_manager/strategy/cloud_strategy_impl.py'
rg -n 'client\.query_landing|client\.query_data' --type=py
🤖 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 `@core/data_manager/strategy/cloud_strategy_impl.py` around lines 853 - 880,
Update list_session_steps so it restores row["request"] from meta, matching
fetch_done_steps_with_context and the metadata written by _build_step_record.
Verify the cloud client’s supported landing-query method, then replace
query_landing with query_data in this and the other relevant call sites if
query_data is the current API.
♻️ Duplicate comments (4)
core/data_manager/strategy/cloud_strategy_impl.py (4)

946-971: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

checkout_latest=False correctness for latest-session reads is still unconfirmed.

This method reads the latest step with checkout_latest=False (Line 969) before sealing it as completed. Elsewhere in this PR, reads that may race with another writer pass checkout_latest=True (for example _load_existing_meta_json, and the list_session_steps callers in evaluator/reward_committer.py and evaluator/trajectory_reader.py). A human reviewer also asked to confirm this choice, noting that other parts of the codebase consistently pass True. With False, this read can observe a stale table version and seal the wrong (older) step, or find nothing to complete. If the stale-read risk applies here too, pass checkout_latest=True.

🤖 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 `@core/data_manager/strategy/cloud_strategy_impl.py` around lines 946 - 971,
The latest-session read in the surrounding method uses checkout_latest=False,
which can select stale data. Update the self.client.query_landing call in the
latest-session completion flow to pass checkout_latest=True, matching the
race-safe reads used by _load_existing_meta_json and related session-step
callers.

1134-1163: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

_load_existing_meta_json still runs a blocking DB call on the event loop.

This helper is synchronous and calls self.client.query_landing directly (Line 1156), while every other DB call in this class is routed through _timed_db_call, which offloads to a thread via asyncio.to_thread. The call chain is update_session_step (async) → _normalize_session_step_updates_for_cloud → this method, so the query stalls the event loop for its duration and produces no latency trace. This was raised previously; make the helper async, await _timed_db_call, and propagate the await through _normalize_session_step_updates_for_cloud and its caller.

🤖 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 `@core/data_manager/strategy/cloud_strategy_impl.py` around lines 1134 - 1163,
Make _load_existing_meta_json asynchronous and route query_landing through the
existing _timed_db_call helper, awaiting its result to preserve latency tracing
and thread offloading. Propagate await through
_normalize_session_step_updates_for_cloud and its async caller
update_session_step, while preserving the existing query arguments and metadata
behavior.

601-668: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

All-or-nothing failure blocks legitimate completions; downstream ValueError handling still unconfirmed.

Two issues remain in mark_records_completed:

  1. The loop at Lines 613-621 builds ids_by_job for every record_id that resolves a job, and separately collects missing_job_ids. Line 630 checks if missing_job_ids: and raises before Line 641 processes ids_by_job. If even one record_id lacks a resolvable job_id, none of the other, already-resolved records get marked completed. The caller loses valid work due to one unrelated record.
  2. This was flagged before: gateway/storage.py calls mark_records_completed and re-raises under a generic exception handler. That caller still needs to treat this ValueError as a no-op that still reports completed status, not a hard failure.
🐛 Proposed fix for issue 1: process resolvable buckets before raising
-        if missing_job_ids:
-            log.error(
-                "Cannot mark %d landing records completed without job_id; "
-                "refusing an all-bucket HASH update",
-                len(missing_job_ids),
-            )
-            raise ValueError(
-                "job_id is required to mark landing records completed without "
-                "scanning all HASH buckets"
-            )
-
         for job_id, job_record_ids in ids_by_job.items():
             quoted_ids = ", ".join(
                 f"'{_escape_sql_literal(record_id)}'"
                 for record_id in job_record_ids
             )
             filter_query = (
                 f"job_id = '{_escape_sql_literal(job_id)}' "
                 f"AND id IN ({quoted_ids})"
             )
             await self._timed_db_call(
                 "update_landing",
                 self.client.update_landing,
                 filter_query,
                 {
                     "is_session_completed": True,
                     "is_terminal": True,
                 },
                 partition=job_id,
                 trace_context={
                     "job_id": job_id,
                     "record_count": len(job_record_ids),
                 },
             )
             for record_id in job_record_ids:
                 self._record_job_ids.pop(record_id, None)
+
+        if missing_job_ids:
+            log.error(
+                "Cannot mark %d landing records completed without job_id; "
+                "refusing an all-bucket HASH update",
+                len(missing_job_ids),
+            )
+            raise ValueError(
+                "job_id is required to mark landing records completed without "
+                "scanning all HASH buckets"
+            )
🤖 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 `@core/data_manager/strategy/cloud_strategy_impl.py` around lines 601 - 668,
Update mark_records_completed to process all ids_by_job buckets before handling
missing_job_ids, so resolvable records are completed even when unrelated records
lack job associations; then raise ValueError only after those updates finish,
preserving the existing refusal to perform an all-bucket update. Also update the
caller handling mark_records_completed in the storage gateway so this ValueError
is treated as a no-op that still reports completion rather than being re-raised
by the generic exception path.

890-933: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Reused session still inflates the persisted reward for evaluation summaries.

record_evaluation_summary still reuses the cached SessionContext from self._sessions and passes it to record_step. _build_step_record performs session.total_reward += step_reward and writes reward=session.total_reward. If the session already has trajectory steps, the persisted reward becomes the accumulated trajectory reward plus the evaluation score, not the evaluation score alone. This also overwrites session.message_history with [], discarding the S3-URL prefix cache for a session that could still be active. This was raised previously with a detailed fix using a detached SessionContext copy; that fix is not yet applied here.

🤖 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 `@core/data_manager/strategy/cloud_strategy_impl.py` around lines 890 - 933,
Update record_evaluation_summary to create and pass a detached SessionContext
copy to record_step, preserving the cached session’s accumulated reward and
message_history. Copy the required session metadata, including session_id,
env_id, env_name, llm_model, and job_id, while using empty messages only on the
detached context so the persisted evaluation reward remains evaluation-only and
the active session remains unchanged.
🤖 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.

Outside diff comments:
In `@core/data_manager/strategy/cloud_strategy_impl.py`:
- Around line 853-880: Update list_session_steps so it restores row["request"]
from meta, matching fetch_done_steps_with_context and the metadata written by
_build_step_record. Verify the cloud client’s supported landing-query method,
then replace query_landing with query_data in this and the other relevant call
sites if query_data is the current API.

---

Duplicate comments:
In `@core/data_manager/strategy/cloud_strategy_impl.py`:
- Around line 946-971: The latest-session read in the surrounding method uses
checkout_latest=False, which can select stale data. Update the
self.client.query_landing call in the latest-session completion flow to pass
checkout_latest=True, matching the race-safe reads used by
_load_existing_meta_json and related session-step callers.
- Around line 1134-1163: Make _load_existing_meta_json asynchronous and route
query_landing through the existing _timed_db_call helper, awaiting its result to
preserve latency tracing and thread offloading. Propagate await through
_normalize_session_step_updates_for_cloud and its async caller
update_session_step, while preserving the existing query arguments and metadata
behavior.
- Around line 601-668: Update mark_records_completed to process all ids_by_job
buckets before handling missing_job_ids, so resolvable records are completed
even when unrelated records lack job associations; then raise ValueError only
after those updates finish, preserving the existing refusal to perform an
all-bucket update. Also update the caller handling mark_records_completed in the
storage gateway so this ValueError is treated as a no-op that still reports
completion rather than being re-raised by the generic exception path.
- Around line 890-933: Update record_evaluation_summary to create and pass a
detached SessionContext copy to record_step, preserving the cached session’s
accumulated reward and message_history. Copy the required session metadata,
including session_id, env_id, env_name, llm_model, and job_id, while using empty
messages only on the detached context so the persisted evaluation reward remains
evaluation-only and the active session remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e8e3cc17-36d8-4c0b-acb4-d4f77b07507d

📥 Commits

Reviewing files that changed from the base of the PR and between 33ae7bf and 5bbe94d.

📒 Files selected for processing (5)
  • core/data_manager/manager.py
  • core/data_manager/strategy/base_strategy.py
  • core/data_manager/strategy/cloud_strategy_impl.py
  • evaluator/reward_committer.py
  • evaluator/trajectory_reader.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • evaluator/trajectory_reader.py
  • core/data_manager/manager.py
  • evaluator/reward_committer.py
  • core/data_manager/strategy/base_strategy.py

@BinHuangPJLAB
BinHuangPJLAB merged commit d3e5c7a into AI45Lab:v2 Aug 2, 2026
1 check passed
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.

2 participants