Stop after two notes, not after something goes wrong
You organize a workshop and want a small facts list from three planning notes. An assistant might eventually read longer documents and propose summaries. Before giving it that job, you need a way to see what finished, stop further work and continue later.
Input: schedule: Doors open at 09:00. Finished output: the same attributed fact, stored beside two other facts in work.sqlite, a local database file. The first pass processes two notes. A pause blocks the third. Resume finishes it, and another run adds nothing.
This is the reliable-deployment step in our practical assistant learning path. It builds a local worker, meaning a program that processes queued work. It is not a deployed service or an AI model. The extraction rule deliberately copies the note, so you can check the controls without judging generated prose.
For three notes, reading them yourself is simpler. Use this exercise to prepare for a recurring job that needs controlled progress.
The question and the boundary
A January 30, 2026 public report of repeated tool calls describes work continuing until a framework step limit. The report was closed February 20 with a maintainer pointing to available call-limit middleware. We did not reproduce that application, and it is not evidence of a current framework defect. It does explain why “ask the assistant to stop” and an enforced work limit are different things.
Research question: Can a beginner preserve completed local work across separate commands while enforcing both a pause and a lifetime work allowance?
A checkpoint is saved progress you can load later. Here, each result row is a checkpoint. A transaction groups database changes so they commit together or roll back together. The result and its progress count share one database; there is no separate text file whose write might disagree with the saved count.
There are two limits: at most two notes per work command, and three completed notes over the database's lifetime. Restarting the program does not reset that second count. Neither limit is a dollar budget. There are no provider calls, tokens or measured charges in this lesson.
1. Make the smallest complete worker
You need Python 3.10 or newer, a terminal and a text editor. Check python --version, using python3 instead if that is your installed command. Allow 25 minutes. No packages, credentials or earlier lessons are needed.
Create an empty folder and save the entire following file as lab.py. The three fictional notes are all the sample data. Run python lab.py. The default demonstration creates work.sqlite; it refuses to replace an existing file.
import argparse
from contextlib import closing
from pathlib import Path
import sqlite3
DB = Path("work.sqlite")
# Original fictional notes. Extraction is fixed code, not a model.
NOTES = [
(1, "schedule", "Doors open at 09:00."),
(2, "access", "The north entrance has a ramp."),
(3, "supplies", "Bring a notebook; pencils are provided."),
]
def initialize(budget):
if type(budget) is not int or not 1 <= budget <= 20:
raise ValueError("budget must be an integer from 1 to 20")
# Exclusive creation protects an existing exercise database.
with DB.open("xb"):
pass
with closing(sqlite3.connect(DB)) as con, con:
con.execute("CREATE TABLE control (paused INTEGER, budget INTEGER)")
con.execute("INSERT INTO control VALUES (0, ?)", (budget,))
con.execute("CREATE TABLE notes (id INTEGER PRIMARY KEY, topic TEXT, text TEXT)")
con.executemany("INSERT INTO notes VALUES (?, ?, ?)", NOTES)
con.execute("CREATE TABLE results (id INTEGER PRIMARY KEY, fact TEXT)")
def operate(command, batch=2):
if type(batch) is not int or not 1 <= batch <= 20:
raise ValueError("batch must be an integer from 1 to 20")
if not DB.is_file():
raise ValueError("initialize in a clean folder first")
with closing(sqlite3.connect(DB, isolation_level=None)) as con:
# One short transaction per item, never around a network call.
for _ in range(batch if command == "work" else 1):
con.execute("BEGIN IMMEDIATE")
try:
if command in {"pause", "resume"}:
con.execute("UPDATE control SET paused=?", (command == "pause",))
paused, budget = con.execute("SELECT paused, budget FROM control").fetchone()
done = con.execute("SELECT count(*) FROM results").fetchone()[0]
row = con.execute("SELECT id, topic, text FROM notes WHERE id NOT IN "
"(SELECT id FROM results) ORDER BY id LIMIT 1").fetchone()
state = ("DONE" if row is None else "PAUSED" if paused else
"BUDGET" if done >= budget else "READY")
if command != "work" or state != "READY":
con.commit()
break
key, topic, text = row
fact = f"{topic}: {text}"
con.execute("INSERT INTO results VALUES (?, ?)", (key, fact))
con.commit()
except BaseException:
con.rollback()
raise
# Snapshot the status and results together for a consistent display.
con.execute("BEGIN")
paused, budget = con.execute("SELECT paused, budget FROM control").fetchone()
facts = con.execute("SELECT id, fact FROM results ORDER BY id").fetchall()
total = con.execute("SELECT count(*) FROM notes").fetchone()[0]
done = len(facts)
state = ("DONE" if done == total else "PAUSED" if paused else
"BUDGET" if done >= budget else "READY")
con.commit()
print(f"{command}: {state}; completed={done}/{total}; budget={done}/{budget}")
if command == "status":
for key, fact in facts:
print(f"{key}. {fact}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("command", nargs="?", default="demo",
choices=["demo", "init", "work", "pause", "resume", "status"])
parser.add_argument("--budget", type=int, default=3)
parser.add_argument("--batch", type=int, default=2)
args = parser.parse_args()
try:
if args.command in {"init", "demo"}:
initialize(args.budget)
if args.command == "demo":
print("LOCAL WORKER: fictional notes; no model or external action")
for command in ["work", "pause", "work", "resume", "work", "work", "status"]:
operate(command, args.batch)
elif args.command != "init":
operate(args.command, args.batch)
except (ValueError, OSError, sqlite3.Error) as error:
parser.error(str(error))
if __name__ == "__main__":
main()
Complete expected stdout:
LOCAL WORKER: fictional notes; no model or external action
work: READY; completed=2/3; budget=2/3
pause: PAUSED; completed=2/3; budget=2/3
work: PAUSED; completed=2/3; budget=2/3
resume: READY; completed=2/3; budget=2/3
work: DONE; completed=3/3; budget=3/3
work: DONE; completed=3/3; budget=3/3
status: DONE; completed=3/3; budget=3/3
1. schedule: Doors open at 09:00.
2. access: The north entrance has a ramp.
3. supplies: Bring a notebook; pencils are provided.
READY means unfinished work is eligible for a later command. No background process is still running. PAUSED means another work command must leave the saved facts unchanged. DONE means all three notes have results, not that anyone has reviewed their real-world accuracy.
2. Prove that progress survives a restart
In another empty folder, copy only lab.py. Run these commands individually, waiting for each to finish:
python lab.py init
python lab.py work
python lab.py status
python lab.py pause
python lab.py work
python lab.py resume
python lab.py work
python lab.py status
The first status must say READY; completed=2/3; budget=2/3 and list only schedule and access. The final status must say DONE; completed=3/3; budget=3/3 and list all three facts exactly as above. Each command starts a fresh process. If those results persist, they are not merely variables left in memory.
The worker selects the lowest unfinished note ID. The results table permits one row per ID, and completed IDs are excluded from selection. A later run therefore processes the third note rather than repeating the first two. The short database transaction checks the pause and allowance before inserting each result.
pause also needs the database write lock. It takes effect when that command succeeds, not at the instant you type it. An item already being committed can finish first. This is a boundary pause, not an emergency interruption of arbitrary code.
3. Test an allowance that cannot finish
In a third fresh folder, run python lab.py init --budget 2, then python lab.py work. Expect work: BUDGET; completed=2/3; budget=2/2. Run work again, then resume, then work once more. Each must still report BUDGET with two results. Resume clears a pause; it does not grant more allowance.
--budget is used only when initializing a new database. Passing a different value to a later command does not change the saved allowance. --batch 1 on work permits only one item in that invocation. Passing --batch 0 must fail rather than silently mean unlimited work.
Inspect the two retained facts with status. Do not delete the database to make the report look complete. An unfinished job stopped by policy is an honest outcome. To study another allowance, preserve this folder and create a separate experiment.
4. Know what the transaction does not protect
Python's SQLite documentation describes explicit transactions and connection handling. This worker explicitly begins, commits or rolls back a transaction and closes its connection. It stores only original fictional text.
A crash before the item's commit leaves that item unfinished; a later invocation can process it. A crash after commit leaves a saved result. Test this boundary using a copy of the exercise, not a live workflow.
Do not put an email send, payment, model call or slow network request inside this transaction and assume it becomes reversible. SQLite cannot roll back a remote action. An interrupted provider request may still have consumed money, even when no result was saved. A real integration needs separate attempt records, timeouts, usage settlement and a policy for uncertain outcomes.
LangGraph's functional API guide discusses persisted task results and warns that unfinished tasks can run again. Its pause-and-resume guide explains durable state and resume identities. Our program uses neither API and does not reproduce their full behavior. Those are next implementation references, not requirements for this lab.
Apply it without overstating it
To adapt the exercise, replace NOTES before initialization with your own fictional planning notes, keeping distinct integer IDs. initialize snapshots them into the database. Editing the Python list afterward does not update an existing job. That prevents a resumed run from silently reading a changed input list, but it is not a versioned document-management system.
The fixed extraction rule is sufficient for copying these facts. A model becomes useful only when the task needs interpretation that rules cannot handle well. Before adding one, define what an acceptable summary contains and compare it with the original text. Keep a person responsible for the resulting brief.
For deployment, you would also need protected storage, authenticated pause controls, an independent supervisor, disk and time limits, redacted incident records and backups. This script has none of those. Someone who can edit the program or database can bypass its policy. A count of completed notes is neither an attempt limit nor a guarantee against expensive or stuck individual operations.
Troubleshooting: “File exists” protects previous work; use a fresh folder. “Database is locked” means another command holds the write lock; let it finish and inspect status. A missing or partially initialized database needs investigation, not automatic reset. Keep the original file for diagnosis and reproduce in another folder. Never use this exercise with sensitive notes.
Completion check: match every demonstration line, verify persistence using separate commands, observe unchanged results while paused, and prove that resume cannot bypass the two-note lifetime allowance. Next task: add one deliberately failing fictional note in a separate copy. Decide what evidence distinguishes “not attempted,” “failed” and “finished” before adding any retry or model integration.
References checked October 20, 2026. The notes and worker are original educational material. No production change or model performance result is claimed.