Inspired by Amp: cron self-context — context_from='self' gives recurring jobs run-to-run continuity

Amp's 'Right on Schedule' (Jul 21 2026) lets scheduled agents wake up with
their saved context and continue where they left off. Hermes cron jobs run
in isolated sessions with per-run amnesia; the existing context_from chain
mechanism only referenced OTHER jobs. This adds the special value 'self'
(and treats a job's own literal id the same way): the job's most recent
output is injected with continuity framing so recurring scouts/monitors
dedupe against what they already reported and continue where they left off.

- cron/scheduler.py: resolve 'self'/own-id in _build_job_prompt with
  continuity framing instead of upstream-job framing
- tools/cronjob_tools.py: allow 'self' through create/update validation
  (can't be validated against the store — the job doesn't exist yet at
  create time); schema description documents the value
- tests: 6 new tests incl. sabotage-verified failures without the fix
- docs: self-context section in cron.md
This commit is contained in:
Teknium
2026-08-06 21:13:21 -07:00
parent 35598d8e8e
commit 47d7661aa8
4 changed files with 158 additions and 8 deletions
+28 -7
View File
@@ -3550,6 +3550,16 @@ def _build_job_prompt(
if isinstance(context_from, str):
context_from = [context_from]
for source_job_id in context_from:
# "self" resolves to the job's own id: the job wakes up with its
# most recent output injected, giving recurring jobs continuity
# across runs (dedupe against what was already reported, continue
# where the last run left off) without touching session history.
is_self = False
if isinstance(source_job_id, str) and source_job_id.strip().lower() == "self":
source_job_id = str(job.get("id") or "")
is_self = True
elif source_job_id == job.get("id"):
is_self = True
# Guard against path traversal — valid job IDs are 12-char hex strings
if not source_job_id or not all(c in "0123456789abcdef" for c in source_job_id):
logger.warning(
@@ -3577,13 +3587,24 @@ def _build_job_prompt(
if len(latest_output) > _MAX_CONTEXT_CHARS:
latest_output = latest_output[:_MAX_CONTEXT_CHARS] + "\n\n[... output truncated ...]"
if latest_output:
prompt = (
f"## Output from job '{source_job_id}'\n"
"The following is the most recent output from a preceding "
"cron job. Use it as context for your analysis.\n\n"
f"```\n{latest_output}\n```\n\n"
f"{prompt}"
)
if is_self:
prompt = (
"## Your previous run's output\n"
"The following is this job's most recent output from its "
"previous run. Use it for continuity: avoid repeating what "
"was already reported, and continue where the last run "
"left off.\n\n"
f"```\n{latest_output}\n```\n\n"
f"{prompt}"
)
else:
prompt = (
f"## Output from job '{source_job_id}'\n"
"The following is the most recent output from a preceding "
"cron job. Use it as context for your analysis.\n\n"
f"```\n{latest_output}\n```\n\n"
f"{prompt}"
)
has_injected_data = True
else:
continue # silent skip — empty output
+100
View File
@@ -217,3 +217,103 @@ class TestUpdateContextFrom:
assert reloaded["context_from"] == [job_a["id"]]
class TestSelfContext:
"""The special 'self' value injects the job's OWN previous output.
Inspired by Amp's "Right on Schedule" (agents wake up with their saved
context and continue where they left off): recurring jobs get run-to-run
continuity without touching session history.
"""
def test_self_injects_own_previous_output(self, cron_env):
from cron.jobs import create_job, OUTPUT_DIR
from cron.scheduler import _build_job_prompt
job = create_job(
prompt="Scan for news", schedule="every 1h", context_from="self"
)
out_dir = OUTPUT_DIR / job["id"]
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "2026-08-01_10-00-00.md").write_text(
"Reported: story A, story B", encoding="utf-8"
)
prompt = _build_job_prompt(job)
assert "Reported: story A, story B" in prompt
assert "previous run" in prompt.lower()
# Self-context uses continuity framing, not the upstream-job framing.
assert f"Output from job '{job['id']}'" not in prompt
def test_self_case_insensitive(self, cron_env):
from cron.jobs import create_job, OUTPUT_DIR
from cron.scheduler import _build_job_prompt
job = create_job(
prompt="Scan", schedule="every 1h", context_from="SELF"
)
out_dir = OUTPUT_DIR / job["id"]
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "2026-08-01_10-00-00.md").write_text("prev", encoding="utf-8")
prompt = _build_job_prompt(job)
assert "prev" in prompt
def test_self_silent_skip_on_first_run(self, cron_env):
from cron.jobs import create_job
from cron.scheduler import _build_job_prompt
job = create_job(
prompt="Scan for news", schedule="every 1h", context_from="self"
)
# No output yet (first run) — base prompt intact, no placeholder.
prompt = _build_job_prompt(job)
assert "Scan for news" in prompt
assert "previous run" not in prompt.lower()
def test_own_id_treated_as_self(self, cron_env):
"""Passing the job's literal id gets the continuity framing too."""
from cron.jobs import create_job, update_job, get_job, OUTPUT_DIR
from cron.scheduler import _build_job_prompt
job = create_job(prompt="Scan", schedule="every 1h")
update_job(job["id"], {"context_from": [job["id"]]})
job = get_job(job["id"])
assert job is not None
out_dir = OUTPUT_DIR / job["id"]
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "2026-08-01_10-00-00.md").write_text("prev output", encoding="utf-8")
prompt = _build_job_prompt(job)
assert "prev output" in prompt
assert "previous run" in prompt.lower()
def test_tool_create_accepts_self(self, cron_env):
from tools.cronjob_tools import cronjob
from cron.jobs import get_job
import json
result = json.loads(cronjob(
action="create",
prompt="Scan for news",
schedule="every 1h",
context_from="self",
))
assert result["success"] is True
job_id = result["job_id"]
assert get_job(job_id)["context_from"] == ["self"]
def test_tool_update_accepts_self(self, cron_env):
from cron.jobs import create_job, get_job
from tools.cronjob_tools import cronjob
import json
job = create_job(prompt="Scan", schedule="every 1h")
result = json.loads(cronjob(
action="update",
job_id=job["id"],
context_from="self",
))
assert result["success"] is True
assert get_job(job["id"])["context_from"] == ["self"]
+12 -1
View File
@@ -1220,6 +1220,11 @@ def cronjob(
from cron.jobs import get_job as _get_job
refs = [context_from] if isinstance(context_from, str) else context_from
for ref_id in refs:
# "self" is resolved to the job's own id at run time —
# it can't be validated against the store (the job does
# not exist yet at create time).
if isinstance(ref_id, str) and ref_id.strip().lower() == "self":
continue
if not _get_job(ref_id):
return tool_error(
f"context_from job '{ref_id}' not found. "
@@ -1500,6 +1505,9 @@ def cronjob(
if refs:
from cron.jobs import get_job as _get_job
for ref_id in refs:
# "self" resolves to the job's own id at run time.
if ref_id.lower() == "self":
continue
if not _get_job(ref_id):
return tool_error(
f"context_from job '{ref_id}' not found. "
@@ -1649,7 +1657,10 @@ Scheduling from cron-run sessions is disabled by default and enabled via cron.al
"Optional job ID or list of job IDs whose most recent completed output is "
"injected into the prompt as context before each run. "
"Use this to chain cron jobs: job A collects data, job B processes it. "
"Each entry must be a valid job ID (from cronjob action='list'). "
"The special value 'self' injects this job's OWN previous output, giving "
"recurring jobs continuity across runs (dedupe against what was already "
"reported, continue where the last run left off). "
"Each other entry must be a valid job ID (from cronjob action='list'). "
"Note: injects the most recent completed output — does not wait for "
"upstream jobs running in the same tick. "
"On update, pass an empty array to clear."
+18
View File
@@ -629,14 +629,32 @@ cronjob(
|--------|---------|
| Single job ID (string) | `context_from="a1b2c3d4"` |
| Multiple job IDs (list) | `context_from=["job_a", "job_b"]` |
| The special value `self` | `context_from="self"` |
Outputs are concatenated in the order listed.
**Self-context: continuity across runs**
`context_from="self"` injects the job's *own* most recent output into each run. Recurring jobs normally start every run with amnesia — a news scout re-reports the same stories, a monitor re-alerts on the same condition. With self-context, the job wakes up seeing what it reported last time and can dedupe and continue where it left off:
```python
cronjob(
action="create",
prompt="Scan HN and arXiv for new agent-tooling papers. Report only items NOT already covered in your previous run's output.",
schedule="every 6h",
context_from="self",
name="Agent Tooling Scout",
)
```
The first run has no previous output, so the prompt runs as-is. On later runs the previous output is prepended with continuity framing ("avoid repeating what was already reported"). You can combine it with upstream jobs: `context_from=["self", "<other_job_id>"]`.
**When to use it:**
- Multi-stage pipelines (collect → filter → format → deliver)
- Dependent tasks where step N's work depends on step N−1's output
- Fan-out/fan-in patterns where one job aggregates results from several others
- Recurring scouts/monitors that should dedupe against their own previous report (`context_from="self"`)
## Provider recovery