Merge branch 'main' into fix/canvas-test-and-design-fixes
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 12s
CI / Detect changes (pull_request) Successful in 17s
Harness Replays / detect-changes (pull_request) Failing after 8s
E2E API Smoke Test / detect-changes (pull_request) Successful in 19s
Harness Replays / Harness Replays (pull_request) Has been skipped
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 11s
sop-tier-check / tier-check (pull_request) Successful in 11s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 17s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 18s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 19s
CI / Platform (Go) (pull_request) Successful in 13s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6s
CI / Python Lint & Test (pull_request) Successful in 11s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 12s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 12s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 10s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m58s
CI / Canvas (Next.js) (pull_request) Failing after 10m46s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 12s
CI / Detect changes (pull_request) Successful in 17s
Harness Replays / detect-changes (pull_request) Failing after 8s
E2E API Smoke Test / detect-changes (pull_request) Successful in 19s
Harness Replays / Harness Replays (pull_request) Has been skipped
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 11s
sop-tier-check / tier-check (pull_request) Successful in 11s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 17s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 18s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 19s
CI / Platform (Go) (pull_request) Successful in 13s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6s
CI / Python Lint & Test (pull_request) Successful in 11s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 12s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 12s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 10s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m58s
CI / Canvas (Next.js) (pull_request) Failing after 10m46s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
This commit is contained in:
commit
c8cbebb2a8
@ -668,6 +668,31 @@ async def main(): # pragma: no cover
|
|||||||
if heartbeat.active_tasks > 0:
|
if heartbeat.active_tasks > 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Issue #381 fix: skip the idle prompt if there are unconsumed
|
||||||
|
# delegation results waiting. The heartbeat sends a self-message
|
||||||
|
# for every new result batch, so sending the idle prompt here would
|
||||||
|
# race: the agent would compose a stale tick BEFORE processing the
|
||||||
|
# results notification, producing repeated identical asks (peer sends
|
||||||
|
# correction, we respond with stale state, peer asks again).
|
||||||
|
# By skipping the idle prompt when results are pending, we let the
|
||||||
|
# heartbeat's own self-message wake the agent after results are
|
||||||
|
# written. The agent then sees the results in _prepare_prompt()
|
||||||
|
# and processes them before composing.
|
||||||
|
from heartbeat import DELEGATION_RESULTS_FILE as _DRF
|
||||||
|
try:
|
||||||
|
with open(_DRF) as _rf:
|
||||||
|
_rf.seek(0)
|
||||||
|
_content = _rf.read().strip()
|
||||||
|
if _content:
|
||||||
|
print(
|
||||||
|
f"Idle loop: skipping — {len(_content)} bytes of unconsumed "
|
||||||
|
f"delegation results pending (heartbeat will notify agent)",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass # No results file — normal, proceed with idle prompt
|
||||||
|
|
||||||
# Self-post the idle prompt via the platform A2A proxy (same
|
# Self-post the idle prompt via the platform A2A proxy (same
|
||||||
# path as initial_prompt). The agent's own concurrency control
|
# path as initial_prompt). The agent's own concurrency control
|
||||||
# rejects if the workspace becomes busy between this check and
|
# rejects if the workspace becomes busy between this check and
|
||||||
|
|||||||
80
workspace/tests/test_idle_loop_pending_check.py
Normal file
80
workspace/tests/test_idle_loop_pending_check.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"""Tests for issue #381: idle loop must not fire when delegation results are pending.
|
||||||
|
|
||||||
|
The idle loop skips sending the idle prompt when DELEGATION_RESULTS_FILE
|
||||||
|
contains unconsumed results, preventing the agent from composing a stale tick
|
||||||
|
before processing pending delegation notifications from the heartbeat.
|
||||||
|
|
||||||
|
Source: workspace/main.py:_run_idle_loop() pending-results guard.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def check_results_pending(file_path: str) -> bool:
|
||||||
|
"""Mirror the guard logic from workspace/main.py:_run_idle_loop().
|
||||||
|
|
||||||
|
Returns True if the results file exists and is non-empty,
|
||||||
|
meaning the idle loop should skip this tick.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(file_path) as rf:
|
||||||
|
rf.seek(0)
|
||||||
|
content = rf.read().strip()
|
||||||
|
return bool(content)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class TestIdleLoopPendingCheck:
|
||||||
|
"""Tests for the idle-loop pending-delegation-results guard."""
|
||||||
|
|
||||||
|
def test_no_file_means_proceed(self, tmp_path):
|
||||||
|
"""No delegation results file → idle loop fires normally."""
|
||||||
|
results_file = tmp_path / "delegation_results.jsonl"
|
||||||
|
assert not check_results_pending(str(results_file))
|
||||||
|
|
||||||
|
def test_empty_file_means_proceed(self, tmp_path):
|
||||||
|
"""Empty file → no pending results → idle loop fires."""
|
||||||
|
results_file = tmp_path / "delegation_results.jsonl"
|
||||||
|
results_file.write_text("", encoding="utf-8")
|
||||||
|
assert not check_results_pending(str(results_file))
|
||||||
|
|
||||||
|
def test_whitespace_only_file_means_proceed(self, tmp_path):
|
||||||
|
"""File with only whitespace → treated as empty → idle loop fires."""
|
||||||
|
results_file = tmp_path / "delegation_results.jsonl"
|
||||||
|
results_file.write_text(" \n ", encoding="utf-8")
|
||||||
|
assert not check_results_pending(str(results_file))
|
||||||
|
|
||||||
|
def test_single_result_means_skip(self, tmp_path):
|
||||||
|
"""File with one delegation result → skip idle tick."""
|
||||||
|
results_file = tmp_path / "delegation_results.jsonl"
|
||||||
|
results_file.write_text(
|
||||||
|
json.dumps({
|
||||||
|
"status": "completed",
|
||||||
|
"delegation_id": "del-abc",
|
||||||
|
"summary": "Done",
|
||||||
|
}) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
assert check_results_pending(str(results_file))
|
||||||
|
|
||||||
|
def test_multiple_results_means_skip(self, tmp_path):
|
||||||
|
"""File with multiple delegation results → skip idle tick."""
|
||||||
|
results_file = tmp_path / "delegation_results.jsonl"
|
||||||
|
results_file.write_text(
|
||||||
|
json.dumps({"status": "completed", "delegation_id": "del-1", "summary": "A"})
|
||||||
|
+ "\n"
|
||||||
|
+ json.dumps({"status": "failed", "delegation_id": "del-2", "summary": "B"})
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
assert check_results_pending(str(results_file))
|
||||||
|
|
||||||
|
def test_file_with_only_newline_means_proceed(self, tmp_path):
|
||||||
|
"""File with only a newline character → stripped to empty → fires."""
|
||||||
|
results_file = tmp_path / "delegation_results.jsonl"
|
||||||
|
results_file.write_text("\n", encoding="utf-8")
|
||||||
|
assert not check_results_pending(str(results_file))
|
||||||
Loading…
Reference in New Issue
Block a user