Backend
Host Sleep Is Not Job Cancel: Dual-Residency Resume for Local Agents
Emery Li DEV Community
1 views
A backend engineer closed a laptop lid on a crowded commuter train and expected a local coding agent to keep working. The agent had been halfway through summarizing a public GitHub issue while a .env file sat two directories away. Local inference stopped the moment the kernel suspended the GPU, and a naive webhook to a hosted model would have uploaded the entire workspace. That gap between host sleep and unfinished public work is the actual design problem, not another routing slogan.
Local-first agents treat the laptop as both the secret boundary and the compute pool, which works until the machine disappears. Hosted agents invert that arrangement by assuming every unfinished token is allowed to travel across the network. A dual-residency design keeps secrets on the host that already holds them, while public remainder work may finish remotely after the lid closes. The sections below describe a proposed sleep-boundary contract, an illustrative Python snapshot, and the cases where a free server is the honest winner.
Two residencies, one unfinished job
Secret residency answers an ownership question about bytes that must never leave the kernel that loaded them. Compute residency answers a different question about where tokens may be spent after the local GPU has been powered down. Mixing those questions produces either leaked environment files or agents that die at the first kernel suspend. Splitting them produces a resume path that stays inspectable, boring to operate, and limited to public corpora.
The job in the opening story had two layers that looked similar in a chat log and were not similar on disk. Issue titles, README excerpts, and open pull-request comments were already public on the network before the agent started. API tokens, internal hostnames, and the adjacent environment file were private and had no business leaving the laptop. A sleep-boundary agent must freeze that distinction before remote completion starts, then merge the remote tail without rewriting secret-bearing files.
This article treats the workflow as a proposal with labeled example code rather than a production benchmark from a private lab. No latency numbers, model names, or quota claims are implied by the snippets that follow in later sections. Teams should measure suspend timing, redaction recall, and merge conflicts against their own repositories and secret scanners.
What the contract must freeze
A useful snapshot is smaller than a workspace archive and stricter than a raw chat export from the agent window. It should record the public task, the shareable files, a hash of the last local edit, and a deny-list of secret paths. It should also record whether the host is about to sleep, because a live GPU and a sleeping GPU are different compute residencies. Unchanged repositories still change residency when the kernel freezes devices, which is why host_state belongs in the snapshot.
The deny-list is not optional flavor text and must include environment files, key material, and private packaging credentials. Any path the operator marked as internal belongs there as well, even when the filename looks ordinary to a classifier. The shareable set should default to empty and grow only through explicit allow rules, never through model guesses about publicity. Silent assumption is how unfinished agents leak, which is the failure mode this contract is designed to stop.
A numbered failover workflow
The following steps are meant to run on the local host before suspend, then again after the machine wakes. Remote completion is allowed only after the packing step returns a clean public bundle with an inspectable digest. If any step fails closed, the job waits for the laptop instead of guessing about bytes it cannot prove are public.
Detect a sleep boundary. Subscribe to systemd-logind, pmset, or an editor idle hook, and treat lid-close as a first-class event rather than a crashed child process.
Classify every dirty path. Mark each file secret, public, or unknown, and treat unknown as secret until a human allow-lists that path.
Freeze a snapshot. Write a JSON document that names the public goal, allow-listed excerpts, content hashes, and the deny-list remote workers must never request.
Strip and pack. Copy only public excerpts into a bundle whose bytes can be inspected with sha256sum before any upload starts.
Hand the public tail to a remote lane. Send only that bundle to free model access on a free server, never the live working tree.
Park secret work. Leave secret-classified edits on local disk, unsent, with a blocked_until_wake flag stored in the snapshot.
Merge on wake. Verify hashes, apply public remote patches, and refuse any remote hunk that touches a deny-listed path.
Write a resume ledger. Store the remote request id, the bundle hash, and the merge decision so the next sleep does not replay blindly.
These steps look ceremonial until someone pastes a whole monorepo into a hosted prompt because the laptop went dark. Ceremony is cheaper than rotating every token that lived in the adjacent environment file on that commute. The ledger also makes the remote lane auditable, which local-only logs cannot provide after a kernel suspend has already dropped in-memory traces.
Artifact: a sleep-boundary snapshot
The code below is illustrative and unexecuted in this article, so it should be treated as a contract sketch rather than vendor glue. It shows the data model and the merge guard, not a tuned client for any particular hosted endpoint. Readers should add their own secret scanners before packing a bundle, because suffix rules miss comments that embed credentials.
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass, field, asdict
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional
class Residency(str, Enum):
SECRET = "secret"
PUBLIC = "public"
UNKNOWN = "unknown"
DENY_SUFFIXES = {".env", ".pem", ".key", ".p12"}
DENY_NAMES = {".env", "id_rsa", "credentials.json", "secrets.yaml"}
def classify_path(path: Path) -> Residency:
name = path.name.lower()
if name in DENY_NAMES or path.suffix.lower() in DENY_SUFFIXES:
return Residency.SECRET
if "secret" in name or "internal" in name:
return Residency.SECRET
return Residency.UNKNOWN
def sha256_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
@dataclass
class FileSlice:
relative_path: str
residency: Residency
digest: str
excerpt: Optional[str] = None
@dataclass
class SleepSnapshot:
goal: str
host_state: str
deny_list: List[str]
slices: List[FileSlice] = field(default_factory=list)
blocked_until_wake: List[str] = field(default_factory=list)
remote_request_id: Optional[str] = None
def public_bundle(self) -> Dict:
public = [asdict(s) for s in self.slices if s.residency == Residency.PUBLIC]
return {
"goal": self.goal,
"files": public,
"deny_list": self.deny_list,
}
def freeze_workspace(root: Path, goal: str, allow: List[str], host_state: str) -> SleepSnapshot:
deny: List[str] = []
slices: List[FileSlice] = []
blocked: List[str] = []
allow_set = {str(Path(item)) for item in allow}
for path in root.rglob("*"):
if not path.is_file():
continue
rel = str(path.relative_to(root))
residency = classify_path(path)
data = path.read_bytes()
digest = sha256_bytes(data)
if residency is Residency.SECRET:
deny.append(rel)
blocked.append(rel)
slices.append(FileSlice(rel, residency, digest, excerpt=None))
continue
if rel in allow_set:
text = data.decode("utf-8", errors="replace")
slices.append(FileSlice(rel, Residency.PUBLIC, digest, excerpt=text[:4000]))
else:
blocked.append(rel)
slices.append(FileSlice(rel, Residency.UNKNOWN, digest, excerpt=None))
return SleepSnapshot(
goal=goal,
host_state=host_state,
deny_list=sorted(set(deny)),
slices=slices,
blocked_until_wake=sorted(set(blocked)),
)
def assert_remote_patch_is_safe(snapshot: SleepSnapshot, patch_paths: List[str]) -> None:
denied = set(snapshot.deny_list)
blocked = set(snapshot.blocked_until_wake)
for item in patch_paths:
if item in denied or item in blocked:
raise PermissionError(f"remote hunk touched parked path: {item}")
def merge_on_wake(snapshot: SleepSnapshot, remote_files: Dict[str, str], root: Path) -> List[str]:
applied: List[str] = []
assert_remote_patch_is_safe(snapshot, list(remote_files))
public = {s.relative_path: s for s in snapshot.slices if s.residency == Residency.PUBLIC}
for rel, body in remote_files.items():
if rel not in public:
raise PermissionError(f"remote produced a path never allow-listed: {rel}")
target = root / rel
before = sha256_bytes(target.read_bytes()) if target.exists() else None
if before and before != public[rel].digest:
raise RuntimeError(f"local file changed under the snapshot: {rel}")
target.write_text(body, encoding="utf-8")
applied.append(rel)
return applied
if __name__ == "__main__":
snap = freeze_workspace(
root=Path("./demo-repo"),
goal="Summarize the public issue and draft a README note.",
allow=["README.md", "docs/issue-142.md"],
host_state="sleeping",
)
bundle = snap.public_bundle()
Path("sleep-snapshot.json").write_text(json.dumps(asdict(snap), indent=2))
Path("public-bundle.json").write_text(json.dumps(bundle, indent=2))
print(
f"deny={len(snap.deny_list)} public={len(bundle['files'])} parked={len(snap.blocked_until_wake)}"
)
A companion shell check keeps the bundle honest before it ever leaves the machine that still holds the deny-listed files.
python freeze_sleep.py
jq '.files[].relative_path' public-bundle.json
sha256sum public-bundle.json
# Reject the upload if jq prints anything that matches the deny list.
Unknown paths stay parked on purpose so the agent cannot widen the bundle because a README summary felt incomplete. Completeness is a wake-time problem for secret-bearing trees, not a reason to weaken residency during host sleep. Operators can extend classify_path with repository-specific scanners without changing the merge guard or the snapshot shape.
When a free server is the honest winner
A free remote lane wins when three conditions are true at the same time for the unfinished job. The local host is sleeping or otherwise unavailable, and the remaining work sits entirely inside the public bundle. The merge guard must prove that remote hunks cannot touch deny-listed paths before any patch is written back. If any condition is false, waiting for wake is the correct engineering choice, even when that wait feels unproductive.
Latency arguments cut both ways and should not be replaced with round numbers that were never measured on the target host. A sleeping laptop has unbounded local latency for new tokens, which is a different failure than a warm but sluggish local model. Offline secret work still belongs on the host that holds the keys, regardless of how idle a remote queue appears. Public summarization, draft README edits, and issue triage can finish elsewhere without pretending the GPU is still awake.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode is an open-source coding assistant that offers free model access and a free server option for work like this public lane. The laptop can stay closed while that server completes only the stripped bundle, not the live working tree beside the environment file. The snippets do not depend on that product, because any inspectable remote endpoint with an explicit request log can play the same role. Readers who already run local-first agents can point the public bundle at that free server and compare the merge ledger after wake.
The decision table below is a method for choosing residency, not a performance benchmark or a ranking of vendors. Rows are mutually exclusive on purpose so operators cannot mark a job both secret-bearing and remote-eligible. Unknown classification inherits the secret row, which keeps the public lane from growing by accident.
Host state
Job bytes
Preferred residency
Remote allowed
Awake GPU
Secret-bearing
Local only
No
Awake GPU
Public excerpts
Local first
Optional
Sleeping
Secret-bearing
Park until wake
No
Sleeping
Public excerpts
Free server lane
Yes, after strip
Offline, no network
Any
Local or wait
No
Unknown classification
Any
Treat as secret
No
Limitations and who should skip this
Redaction by suffix and filename is incomplete against secrets that developers embed in ordinary source comments. Teams that paste production connection strings into comments will export those strings inside an allow-listed implementation file. Those teams should add content scanners before the packing step rather than trusting the filename classifier alone. Hash checks also fail if a user edits the same public file during sleep from another clone of the repository.
The merge function refuses mismatched digests instead of rebasing silently, which is noisy and safer than an automatic overlay. This approach is a poor fit for regulated datasets, medical records, unreleased product source, or repositories that are public only in name. It is also a poor fit for agents that must call internal tools whose outputs are secret by construction and cannot be stripped. Those jobs should stay parked, even if a free server is idle and the README looks harmless from a distance.
Suspend hooks differ across operating systems, and a missed lid-close event means the agent still dies with no snapshot. The contract does not replace disk encryption, operating-system keychains, or a dedicated secret manager already in the fleet. It only stops a sleeping host from being treated as a blanket permission to upload whatever the agent had open.
Local-first remains the right default for secret-bearing work, offline edits, and anything whose classification is still unknown. A free server earns its place only as a public-lane overflow when the machine that owns the secrets is dark. Treating those two residencies as one permission set is how commute-time agents turn into incident tickets after the train ride.
Read original: https://dev.to/codepro_9661/host-sleep-is-not-job-cancel-dual-residency-resume-for-local-agents-g74
← Previous
Question for web hacking developers: data breach and account hijacks
Next →
I Turned My Personal Portfolio Into an SEO Experiment
Related
AI Coding Isn't About Writing Less Code 🤖
Backend
0
Dev.to (EN Zone)
Wabe Labs Is Born, and Building With Blueprints
Backend
2
DEV Community
5 Verbal Corrections Didn't Stick, So a 46-Line Stop Hook Made the Mistake Physically Impossible
Backend
2
DEV Community
Open-PR: một AI agent review PR nói chuyện như đồng nghiệp, không như một con bot
Backend
2
DEV Community
Comments0
No comments yet — be the first