Files
openworker/tests/test_clock_tool.py
Devika Verma c1e8b8cdf3 Make the time a tool instead of a line in every turn (OPE-192, final fix)
The per-turn `<system-context>` block opened with `Now: YYYY-MM-DD HH:MM` (2c9fb74,
2026-08-20) so that absolute scheduling had a clock. The block is glued onto the last
user message — the task prompt in a tool loop — and a provider's prompt cache is
reusable only up to the first byte that differs, so every minute crossing rewrote
message one and re-processed the whole conversation. 9d6a716 worked around it with a
trailing-message shape, an Anthropic-only relocation of that message, and a
`live_clock` switch that headless runs set off. This commit removes the cause instead
and deletes the workarounds; the shape is the same on every provider again.

- The block carries no clock, on any provider. What is left (folders, skill menu,
  mode notices, memory-off notice, disable countermand) changes only when the user
  changes something, so the outbound history is byte-identical turn to turn.
- `current_time` tool (coworker/clock.py), registered for every persona: local time
  with offset and zone, the same instant in UTC, weekday. The environment block's
  "Today's date" now says it is a session-start snapshot and points at the tool.
- `sleep_for(seconds)` is back beside `sleep_until` (a relative wait needs no clock
  arithmetic; the four lead personas' manifests still told the model to call
  sleep_for, which 2c9fb74 had removed). `sleep_until`'s description points at
  current_time for the date/timezone. Timer wake messages carry their fire time.
- Deleted: `live_clock` config + COWORKER_LIVE_CLOCK, the trailing-message shape in
  `_outbound_messages`, `_relocate_ephemeral_note` in the Anthropic provider. The
  cache-diagnostics instrument (off by default) stays.

Validation runs (one long tool-heavy task, one attempt each on Fable 5.1 and Kimi K3,
compared with the 9d6a716 runs of the same task) are recorded on OPE-192.

Tests: tests/test_clock_tool.py (new), tests/test_context_block_placement.py and
tests/test_cache_breakpoint_placement.py rewritten for the single shape,
tests/test_live_clock.py removed; self-wake, attachments, message-source and
multiroot tests updated. Full suite unchanged against the Windows baseline.
2026-09-17 18:25:47 +05:30

64 lines
2.6 KiB
Python

"""OPE-192: the time is a tool (`current_time`), not a line in the per-turn context block.
The block must be byte-identical turn to turn so the provider's prompt cache keeps
working; a value that changes by itself has no place in it."""
from __future__ import annotations
from datetime import datetime, timezone
from coworker.clock import clock_tools, current_time
def test_current_time_reports_local_and_utc_for_the_same_instant():
before = datetime.now(timezone.utc)
out = current_time()
after = datetime.now(timezone.utc)
local = datetime.fromisoformat(out["local"])
utc = datetime.fromisoformat(out["utc"].replace("Z", "+00:00"))
assert local.tzinfo is not None and utc.tzinfo is not None
assert local == utc # same instant, two renderings
assert before.replace(microsecond=0) <= utc <= after
assert out["weekday"] == local.strftime("%A")
assert isinstance(out["timezone"], str)
def test_clock_tools_exposes_exactly_current_time():
assert [t.__name__ for t in clock_tools()] == ["current_time"]
def test_current_time_is_registered_for_every_persona(tmp_path, monkeypatch):
from coworker.agent import build_engine
from coworker.agents.registry import get_agent
from coworker.permissions import Mode
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setenv("COWORKER_SCRATCH_BASE", str(tmp_path / "scratch"))
ws = tmp_path / "ws"
ws.mkdir()
for persona in ("cowork", "code"):
engine = build_engine(agent=get_agent(persona), workspace=ws, model="gpt-5.5", mode=Mode("bypass-approvals"))
assert "current_time" in set(engine.registry.names()), persona
def test_context_block_carries_no_clock_and_is_byte_stable(tmp_path, monkeypatch):
from coworker.agent import build_engine
from coworker.agents.registry import get_agent
from coworker.permissions import Mode
monkeypatch.setenv("COWORKER_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setenv("COWORKER_SCRATCH_BASE", str(tmp_path / "scratch"))
ws = tmp_path / "ws"
ws.mkdir()
engine = build_engine(agent=get_agent("cowork"), workspace=ws, model="gpt-5.5", mode=Mode("bypass-approvals"))
first = engine.context_provider()
assert "Now:" not in first
assert str(ws) in first # the folders are still there
assert engine.context_provider() == first # byte-identical turn to turn
def test_environment_block_points_at_the_tool(tmp_path):
from coworker.environment import environment_context
block = environment_context(tmp_path)
assert "Today's date:" in block and "current_time" in block