Files
openworker/tests/test_wake_resume.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

63 lines
1.8 KiB
Python

"""Phase 3 wiring — self-wake tools registered, scheduler resume hook, wake messages."""
from __future__ import annotations
import asyncio
from coworker.agent import build_engine
from coworker.agents.code import code_agent
from coworker.agents.cowork import cowork_agent
from coworker.automation.scheduler import Scheduler
from coworker.selfwake import Wake, WakeStore
from coworker.server.manager import SessionManager
class _FakeStore:
def due(self):
return []
def test_scheduler_runs_extra_tick():
async def run():
hits = {"n": 0}
async def extra():
hits["n"] += 1
sched = Scheduler(_FakeStore(), runner=None, extra_tick=extra)
await sched._tick(trigger="schedule")
assert hits["n"] == 1
asyncio.run(run())
def test_wake_messages_by_kind():
timer = Wake("1", "s1", "timer", note="poll")
completion = Wake("2", "s1", "completion", job_id="job-9")
event = Wake("3", "s1", "event", event_key="pr-opened")
assert "timer" in SessionManager._wake_message(timer)
assert "poll" in SessionManager._wake_message(timer)
assert "job-9" in SessionManager._wake_message(completion)
assert "pr-opened" in SessionManager._wake_message(event)
def test_selfwake_tools_registered_for_knowledge(tmp_path):
engine = build_engine(
agent=cowork_agent(),
workspace=tmp_path,
wake_store=WakeStore(tmp_path / "wakes.json"),
session_id="s1",
)
names = set(engine.registry.names())
assert {"sleep_for", "sleep_until", "wake_on", "wake_on_event"} <= names
def test_selfwake_tools_absent_for_code(tmp_path):
engine = build_engine(
agent=code_agent(),
workspace=tmp_path,
wake_store=WakeStore(tmp_path / "wakes.json"),
session_id="s1",
)
assert "sleep_until" not in set(engine.registry.names())