This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
What I Built
The oldest piece of outdoor safety advice is also the most ignored: tell someone where you're going and when you'll be back. The problem is that "someone" has to remember, has to notice you're late, and has to know what to do about it.
TrailWatch is that someone. It's a hiking check-in timer that can't forget about you.
- Before you leave, you type one sentence: "Monk's Trail up Doi Suthep, solo, back by 5ish."
- Gemma, running locally, turns that into a plan: return time, turnaround time, how often to check in, and tips based on tonight's sunset and the forecast.
- You put your phone away. That's the whole point. You only need the phone again if a notification asks "Are you OK?", and you answer it from the lock screen with one tap.
- If you miss a check-in, TrailWatch nudges you, waits a grace period, then alerts your emergency contact. First it sends a plain template message instantly. Then it sends a situation brief written by Gemma from the trip log. When you get signal back and tap I'm OK, your contact gets an all-clear.
The screen time for a whole hike is about thirty seconds: one sentence before you go, and maybe one tap on the trail.
The interesting part isn't the AI. It's this: a safety timer that lives in a normal process is a safety timer that can silently die. A laptop sleeps, a server reboots, a phone's battery saver kills your app. The one moment the system must work is when you're out of signal and nobody's watching. So I built TrailWatch so the timers can't be lost, and then I tried hard to break it.
Demo
That's one real run, recorded in a single take. Local Gemma plans the trip, Temporal runs the workflow, and real pushes go through ntfy.sh. There are two tricks, and both are shown on screen:
- The clock is compressed. One "trip minute" is 2 seconds, so a 40-minute walk fits in under two minutes. It's the same code path as real time; only the timer lengths are scaled.
- The worker-is-dead stretch plays at 4Γ speed (labelled in the corner). Nothing interesting happens there, and that's the point.
The voice-over is Piper, an open-source text-to-speech model, running offline on the same laptop.
What happens:
- Gemma arms the trip: check in every 15 min, grace 15 min.
-
I hard-kill the worker (
TerminateProcess, no cleanup). Nothing is running TrailWatch code. - The phone view notices and says so: your timers live on the Temporal server and are still running.
- The check-in deadline passes. Then the grace period passes. The alert is now overdue, and there is still no worker.
- I restart the worker. Temporal replays the history, sees both timers already fired, nudges the hiker and alerts the contact. The alert went out about 10 seconds after the worker came back.
- Gemma's brief arrives, the "hiker" taps I'm OK, and the contact gets the all-clear.
And here's the event history for that run in the Temporal UI. Event 64 is the "I'm home" signal from the phone. Event 68 is the last durable timer being cancelled because of it. Every send_notice result says pushed to https://ntfy.sh/....
This is a real brief Gemma 3 4B wrote for the contact during an earlier kill-test run, unedited:
Ali is overdue from a solo hike up the ridge behind campus, which was scheduled to return by 23:54. Ali started the hike at 23:14 and was last heard from at 23:14. Their last known location is unknown. Sam, please attempt to contact Ali immediately. If you do not reach Ali within 15 minutes, contact local emergency services.
(Ali and Sam are demo personas.)
Code
syncaimain
/
trailwatch
A hiking check-in timer that can't forget about you: local Gemma + Temporal durable workflows + ntfy
TrailWatch
A hiking check-in timer that can't forget about you.
Tell it where you're going and when you'll be back, in your own words. A local Gemma model turns that into a plan: return time, turnaround time, how often to check in, and safety tips based on sunset and the forecast. Then you put your phone away.
Each trip is a Temporal workflow. It sleeps on durable timers until a check-in is due. If you don't tap I'm OK, it nudges you waits a grace period, then alerts your emergency contact: first with a template message (instantly, no AI on the critical path), then with a situation brief written by local Gemma. When you surface and tap OK, your contact gets an all-clear.
Kill the worker, restart the web app, reboot the Temporal server mid-hike: the timer still fires.
TrailWatch is a prototype. It is not a replacement forβ¦
The safety logic is in two files: trailwatch/workflows.py (the durable workflow) and trailwatch/rules.py (the deterministic rules the model can't override). pytest runs 12 tests. scripts/kill_test.py reproduces the video on your machine.
How I Built It
Stack: Gemma 3 4B via Ollama, on a laptop with a 6 GB RTX 3050 Β· Temporal (Python SDK, local dev server) Β· ntfy for push notifications with action buttons Β· Open-Meteo for sunset and weather Β· FastAPI and one HTML file.
phone (web page or ntfy lock-screen buttons)
β start trip Β· I'm OK (signal) Β· +30 min (update) Β· SOS Β· I'm home
βΌ
FastAPI (stateless) βββΊ Temporal server βββ worker
one HikeWorkflow per trip
ββ fetch_conditions Open-Meteo sunset + weather
ββ plan_trip local Gemma β rules clamp it
ββ durable timers check-in / grace / re-alert
ββ send_notice ntfy push, retried until it lands
ββ write_briefing Gemma situation brief
One workflow per trip
Every trip is a Temporal workflow, and every Temporal feature I used is doing a real job:
| Temporal feature | What it does in TrailWatch |
|---|---|
| Durable timers | Check-in deadlines, grace periods and re-alerts every 30 min. They live on the server, not in my process. |
| Signals | I'm OK, SOS, I'm home and Cancel, sent from the web page or straight from ntfy's lock-screen buttons. |
| Update + validator | "+30 min" returns the new return time to the phone. The validator rejects nonsense like +500 min, or more than 6 h total. |
| Query | The web page reads live trip state from the workflow itself. There is no database. |
| Activity retries | Gemma cold starts, out-of-memory errors and bad JSON retry with backoff, then fall back to a regex planner. Pushes retry for up to an hour. |
The heart of it is a timer racing an inbox:
async def _wait_until(self, trip_min: float) -> bool:
"""Durable timer racing the inbox. True if a check-in arrived first."""
if self.inbox:
return True
seconds = (trip_min - self._elapsed()) * self.req.minute_seconds
if seconds <= 0:
return False
try:
await workflow.wait_condition(lambda: bool(self.inbox), timeout=timedelta(seconds=seconds))
return True
except asyncio.TimeoutError:
return False
That wait_condition timeout is a durable timer. If the worker dies while it's waiting, nothing is lost. The timer fires on the server, and whichever worker comes back next picks up from exactly that line.
The AI is never on the alert's critical path
Gemma does two jobs: turning a casual sentence into a plan, and writing the brief for the contact. It does not decide whether to alert, and the first alert never waits for it:
- The alert itself is a template, sent instantly.
- Then, best effort, Gemma writes the context-rich brief. If Gemma is down, the contact already has what they need.
A cold Gemma load on my laptop takes about 70 seconds. That's fine for planning at home, and unacceptable on the alert path.
The model proposes, the code decides
Gemma's plan goes through rules.py before anything is armed. The model can suggest anything, but it can't:
- set check-ins longer than 60 min on a solo trip, or 45 min on a high-risk one
- lower the risk level that the sunset or wind implies
- pick a turnaround time that leaves no time to walk back
Every rule has a test, and most of them exist because Gemma broke something.
Three bugs worth telling you about
1. "Turn around at minute 1440." For a three-hour hike, Gemma 3 4B proposed a turnaround time of 1440 minutes, a full day later. Fix: a turnaround has to land in the first 75% of the trip, or it's replaced with the halfway point.
2. "Back in 40 minutes" β back at 12:39 tomorrow. At 23:00, Gemma turned "back in 40 minutes" into expected_return_local: "12:39" and, in the same reply, duration_min: 40. My parser trusted the clock time, so the hiker would have been reported missing 13 hours late. I only caught this because the kill-test run printed the plan. Fix: when the model's clock time and duration disagree, use the earlier one. A false alarm is better than a late one.
3. My own bug: downtime pushed alerts later. My first version started the grace period after the nudge was sent. If the worker was dead through the deadline, the nudge went out late when it restarted, the grace period started from there, and the alert came a full grace period late. Durable timers don't help if you measure from the wrong moment. Fix: grace is counted from the missed deadline, which is fixed in the workflow's history, so downtime can never delay an alert. There's now an integration test that keeps the worker dead through both deadlines and checks that the alert fires the moment it's back.
In one test, Gemma also called a 10% chance of rain "significant." That's why tips are just advice and warnings come from code.
Tests
- Time-skipping: Temporal's test server runs a 6-hour silent hike in seconds. It checks the contact gets exactly 10 alerts (one at 75 min, then every 30 min) and only one Gemma brief.
- Worker death: a real dev server with a compressed clock. The worker is shut down through the deadline and the grace period, and the test checks the alert fires on restart.
- Rules: real Gemma mistakes, kept as regression tests.
Why Does Open Innovation Matter?
Because this app knows when you're alone and where you'll be. A trip plan is "this person will be by themselves on this ridge until 5 pm, and nobody will be home." I don't want that in someone else's logs. With TrailWatch, the model runs on my laptop, Temporal is open source and self-hosted, ntfy can be self-hosted, and the weather comes from open data. Nothing in the safety path depends on a vendor's uptime, pricing or terms of service.
Because a safety tool shouldn't cost a subscription. Running TrailWatch costs nothing. A small open-weight model is enough because the architecture keeps it off the critical path: it plans and explains, while code and durable timers make the decisions.
Because I could see how it fails. Running Gemma locally, I could hammer it, watch it make the 1440-minute mistake and the 12:39 mistake, and turn each into a rule and a test. Swapping models is one environment variable: any Ollama model, or any OpenAI-compatible endpoint, including hosted Gemma.
Where a closed API would have been better: honestly, speed and judgment. A big hosted model would plan in about 2 seconds instead of 10β35, and would probably not suggest turning around tomorrow. But for this problem I'll take a model I can run, inspect and constrain over one that's merely smarter.
Honest limits
- This is a prototype, not a satellite messenger or a PLB. It needs your phone to get a push now and then. Losing signal is exactly the situation it alerts on, but it can't call for help from a valley by itself.
- The video uses a compressed clock. The code path is the same as real time, but it's still a demo.
- ntfy topics are like passwords on the public server. Use long random ones, or self-host ntfy. I added an access key before putting the app behind a tunnel, so a leaked URL can't cancel your trip.
- Push delivery is at-least-once. If the worker dies mid-push, a retry can send a duplicate. In my runs, the push that was in flight when I killed the worker arrived exactly once, but I don't promise that.
My Agent Session
I built TrailWatch pair-programming with an AI coding agent (Claude Code). That session is also where the bugs above were found: the agent wrote the kill test, read Gemma's real output, and flagged the grace-period bug in its own earlier code.
Prize Categories
- Best Use of Temporal: each trip is a durable workflow using timers, signals, an update with a validator, queries and retry policies, with a hard-kill test on video and the event history to back it up.
- Best Use of Gemma: Gemma 3 4B runs locally through Ollama to plan trips and write situation briefs, constrained by tested rules.


Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.