mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-15 15:50:35 +00:00
fix(workspace): treat hidden output paths as working state (#8254)
* fix(workspace): treat hidden output paths as working state get_changed_output_paths counted every regular file under outputs as a produced artifact, so a finished run that left a cursor/state JSON or a .work/ scratch tree there was recorded as delivery incomplete. Skip paths with a hidden segment below outputs; visible files still count. Part of #8239. * fix(workspace): share the outputs prefix and document the hidden-path rule The delivery filter partitioned on a hardcoded /outputs/ while the scanner built the same prefix in recorder.py, so a future prefix change would silently stop filtering working state. Export one OUTPUTS_VIRTUAL_PREFIX used by both, and record the hidden-path exclusion in the runtime guidance contract.
This commit is contained in:
parent
a1a6866ebc
commit
778f53a456
@ -161,7 +161,10 @@ and after scans. Its budget is shared and consumed root by root (workspace
|
||||
before outputs), so a workspace at the limit starves outputs.
|
||||
`get_changed_output_paths()` inherits this uncertainty: newly observed outputs
|
||||
are suppressed when the before snapshot was truncated, while modifications
|
||||
observed in both snapshots remain eligible. The worker passes this list through
|
||||
observed in both snapshots remain eligible. Hidden paths are working state, not
|
||||
deliverables: any dot-prefixed segment below the outputs root (a cursor/state
|
||||
JSON, a `.work/` scratch tree) is excluded, so it never enters `produced_paths`.
|
||||
The worker passes this list through
|
||||
`_produced_output_paths()` to its delivery check; an empty list leaves
|
||||
`produced_paths` and `verification` absent from the delivery receipt, and
|
||||
`_delivery_error()` returns no error. When that empty list accompanies a truncated
|
||||
|
||||
@ -109,18 +109,37 @@ def get_changed_paths(before: WorkspaceSnapshot, after: WorkspaceSnapshot) -> se
|
||||
return changed
|
||||
|
||||
|
||||
#: Virtual prefix of the outputs workspace root. Shared with ``recorder.py`` so
|
||||
#: the delivery filter below cannot silently drift from the scanner's paths.
|
||||
OUTPUTS_VIRTUAL_PREFIX = "/mnt/user-data/outputs"
|
||||
|
||||
|
||||
def _is_hidden_output_path(path: str) -> bool:
|
||||
"""Whether any segment below the outputs root starts with a dot.
|
||||
|
||||
Scheduled tasks and agents keep working state under outputs (a cursor or
|
||||
state JSON, a ``.work/`` scratch tree). Those files are not deliverables, so
|
||||
they must not make an otherwise finished run fail delivery verification.
|
||||
"""
|
||||
prefix = f"{OUTPUTS_VIRTUAL_PREFIX}/"
|
||||
if not path.startswith(prefix):
|
||||
return False
|
||||
return any(segment.startswith(".") for segment in path[len(prefix) :].split("/") if segment)
|
||||
|
||||
|
||||
def get_changed_output_paths(before: WorkspaceSnapshot, after: WorkspaceSnapshot) -> list[str]:
|
||||
"""Return regular output changes established by the bounded snapshots.
|
||||
|
||||
Paths absent from a truncated before snapshot are omitted, including
|
||||
genuinely new files; modifications observed in both snapshots are retained.
|
||||
The worker's delivery check relies on this list and is skipped when it is
|
||||
empty, so an empty result does not prove that the run produced no outputs.
|
||||
Hidden paths are working state, not deliverables. The worker's delivery
|
||||
check relies on this list and is skipped when it is empty, so an empty
|
||||
result does not prove that the run produced no outputs.
|
||||
"""
|
||||
paths: list[str] = []
|
||||
for path in sorted(get_changed_paths(before, after)):
|
||||
snapshot = after.files.get(path)
|
||||
if snapshot is not None and snapshot.root == "outputs" and not snapshot.symlink:
|
||||
if snapshot is not None and snapshot.root == "outputs" and not snapshot.symlink and not _is_hidden_output_path(path):
|
||||
paths.append(path)
|
||||
return paths
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ from typing import Any
|
||||
|
||||
from deerflow.config import get_paths
|
||||
|
||||
from .diff import compare_snapshots, get_changed_paths
|
||||
from .diff import OUTPUTS_VIRTUAL_PREFIX, compare_snapshots, get_changed_paths
|
||||
from .scanner import scan_workspace_roots
|
||||
from .types import (
|
||||
WORKSPACE_CHANGES_EVENT_CATEGORY,
|
||||
@ -35,7 +35,7 @@ def build_thread_workspace_roots(thread_id: str, *, user_id: str | None = None)
|
||||
WorkspaceRoot(
|
||||
name="outputs",
|
||||
host_path=paths.sandbox_outputs_dir(thread_id, user_id=user_id),
|
||||
virtual_prefix="/mnt/user-data/outputs",
|
||||
virtual_prefix=OUTPUTS_VIRTUAL_PREFIX,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@ -478,6 +478,23 @@ def test_get_changed_output_paths_ignores_externalized_tool_results(tmp_path):
|
||||
assert get_changed_output_paths(before, after) == ["/mnt/user-data/outputs/report.md"]
|
||||
|
||||
|
||||
def test_get_changed_output_paths_ignores_hidden_working_state(tmp_path):
|
||||
"""Hidden files and directories under outputs are working state, not deliverables."""
|
||||
roots = _roots(tmp_path)
|
||||
outputs = roots[1].host_path
|
||||
before = scan_workspace_roots(roots)
|
||||
|
||||
(outputs / ".digest_cursor.json").write_text("{}", encoding="utf-8")
|
||||
(outputs / ".work").mkdir()
|
||||
(outputs / ".work" / "chart.png").write_bytes(b"png")
|
||||
(outputs / "sub").mkdir()
|
||||
(outputs / "sub" / ".state.json").write_text("{}", encoding="utf-8")
|
||||
(outputs / "sub" / "report.md").write_text("deliverable", encoding="utf-8")
|
||||
after = scan_workspace_roots(roots)
|
||||
|
||||
assert get_changed_output_paths(before, after) == ["/mnt/user-data/outputs/sub/report.md"]
|
||||
|
||||
|
||||
def test_scan_workspace_roots_can_skip_text_loading(tmp_path):
|
||||
roots = _roots(tmp_path)
|
||||
workspace = roots[0].host_path
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user