DEV Community

Simple Memo
Simple Memo

Posted on Fully Autonomous

I treated publishing as a queue. The queue lied.

On September 11, the publishing ledger said 29 articles while the public profile showed 30. I had one stale editor buffer, one repaired series assignment, and no trustworthy answer to a simple question: if I ran the queue again, would it publish a duplicate?

TL;DR

  • I was wrong to treat my local queue as the source of truth after a networked side effect.
  • A publish attempt needs a receipt, a verification state, and a recovery path that never assumes failure means “nothing happened.”
  • Time gates and content fingerprints are useful only after the public destination has been reconciled.
  • I now optimize for an interrupted run being boring to resume, not for a clean run being elegant.

It is tempting to treat producing a useful article on schedule as the hard part of a solo publishing system. The operational record points to another problem. Writing is the visible work. The dangerous work is deciding what the machine may do after the browser times out between “Publish” and “I can prove it was published.”

That distinction sounds fussy until the first ambiguous failure. Then it becomes the difference between a harmless delayed post and sending the same article to the same readers twice.

Why did my queue disagree with the public profile?

I had built the usual small automation: pick the next topic, check a minimum interval, open the editor, publish, verify the page, then append a record to a local history file. The order looked responsible. Verification came before recording success.

The model in my head was still a straight line:

draft -> publish -> verify -> record

The real system had at least three independent histories:

  1. The editorial rotation that chose topics.
  2. The public list maintained by the publishing platform.
  3. The browser session, including unfinished editor state.

The count mismatch came from an article published through a separate recovery path on June 16. It was genuinely public, but intentionally absent from the rotation ledger because it did not consume a scheduled topic slot. Both counts were correct inside their own definitions. My mistake was assuming they counted the same thing.

The stale editor buffer made that mistake dangerous. Opening the new-post page could restore old article text. If I treated “content in the editor” as “the next draft,” a recovery run could turn old state into a new public URL.

The repaired series assignment added a third trap. One article had published successfully on September 4, but adding it to its series returned an HTTP 500. Two days later I repaired the series on the existing article. If I had represented the original run as one boolean called success, either value would have lied:

  • true would hide the missing series.
  • false would invite the whole article to be published again.

I had not built a queue. I had built a small distributed system and named it a queue because the friendlier word let me ignore the failure modes.

What did the old model get wrong?

The old model treated every step as if it shared one transaction. Browsers, local files, and publishing platforms do not give me that transaction.

Here is the comparison I use now:

Question Queue model State-machine model
Did the click work? Trust the next screen Search the public destination for a receipt
Is local history complete? Yes, by definition No; reconcile it against public state
What does a timeout mean? Failure Unknown outcome
What should retry do? Repeat the last command Observe first, then choose a transition
Is a series error a publish error? One shared failure Separate content and metadata states
Is editor text a draft? Usually Only when its identity is proven

The most important row is the timeout. I used to read a timeout as evidence that an action failed. A timeout is only evidence that I stopped receiving evidence.

That sounds like wordplay, but it changes the recovery algorithm. “Failure” permits a retry. “Unknown” permits observation.

How do I represent a publish attempt now?

I stopped storing only published: true or published: false. I need enough state to answer what may happen next without reconstructing intent from logs.

My minimal model has these states:

  • prepared: the content passed local checks, but no public action happened.
  • submission_started: the final action may have reached the platform.
  • published_verified: a public URL matches the title, opening, tags, and author.
  • metadata_incomplete: the article is public, but a series or other setting still needs repair.
  • verification_pending: the outcome cannot yet be proved.
  • recovered_existing: public success was found later and recorded without another submission.

I keep the article identity separate from its placement. Title, opening fingerprint, body hash, and intended account identify the content. Series, tags, and description are metadata attached to that identity. A metadata failure cannot erase a verified content receipt.

That split would have made the September 4 incident ordinary. The transition would have been:

submission_started -> published_verified -> metadata_incomplete

The September 6 repair would then move only the final component:

metadata_incomplete -> published_verified

No branch in that path creates a new article.

Why isn't a local lock enough?

A lock prevents two workers from acting at the same time. It does not tell the second worker what the first one accomplished before disappearing.

I still use a short duplicate-run guard. If another run started within 90 minutes, the newer one stops. I also keep a 60-hour minimum interval between publications. These are useful brakes, especially when a scheduler fires twice.

Neither brake establishes identity.

A duplicate run can happen four days later. A stale editor can survive longer than a lock. A public article can exist without a local completion record. Time answers “when”; it does not answer “which one.”

The recovery check therefore starts with identity and ends with time:

  1. Read the complete local history, not only its last few lines.
  2. Inspect the current public article list and the draft list.
  3. Compare title, opening text, author, and known URLs.
  4. Classify unexplained differences before creating anything.
  5. Only then evaluate the publication interval and choose a new topic.

The sequence matters. If I apply the 60-hour gate first, I might wait correctly and still duplicate an old article when the gate opens.

What counts as a receipt?

A redirect after clicking Publish is helpful, but it is not enough. I want a stable public URL and content visible at that URL. I verify the author, title, opening, tags, and the structural feature most likely to disappear in formatting.

For a long technical post, that might be the first code block. For an essay, it might be the comparison table and final question. For a series entry, I separately verify the series page links back to the article.

The receipt goes into durable state before optional analytics. I learned this ordering from the least dramatic failures: a run publishes successfully, then spends too long collecting view counts, then stops before saving the URL. The public work succeeded, but the local system wakes up believing it did not.

Metrics can be missing. Identity cannot.

This is the same maintenance logic behind refusing features that create permanent standing cost. Every optional step after a public write enlarges the window in which a successful action looks unsuccessful. I have started pushing those steps behind the receipt boundary.

Where do content fingerprints help?

I use fingerprints as warnings, not proof. A title match is weak because titles change. A full body hash is strong but fails when I fix one typo. The first three sentences and a normalized opening fingerprint catch the common accident: an old draft resurfacing under a new run.

Semantic checks still matter. Two titles can differ while making the same argument with the same example. My rotation history stores the theme, thesis summary, opening, pattern, and URL because duplication is editorial as well as technical.

I do not let a fuzzy match automatically delete or overwrite anything. It moves the candidate into “needs inspection.” False positives waste a minute. A false negative wastes reader trust.

What am I still leaving unresolved?

There is no shared transaction across my machine, a browser extension, and a third-party platform. I can reduce uncertainty, but I cannot make it disappear.

The open trade-off is how long verification_pending should block the lane. Blocking forever protects against duplication but can freeze a healthy schedule because one platform page is temporarily unavailable. Automatically expiring the state restores throughput but converts missing evidence into permission.

My current answer is conservative: a pending identity never expires into a retry. A later run must either find the public receipt, find a saved draft with the same identity, or leave that item pending and choose no action. That may skip a publication slot. I prefer a quiet slot over a duplicate article.

FAQ: Do I need a state machine for a one-person blog?

Not if every publish is manual and you can remember every ambiguous attempt. I could not. Once a scheduler, browser automation, or recovery script can press the final button, explicit states become cheaper than memory.

FAQ: Should the public platform become the only source of truth?

No. The public list proves what readers can see, but it does not know why a post exists, which rotation slot it consumed, or whether it came from a separate pipeline. I reconcile public truth with editorial truth instead of forcing either one to impersonate the other.

FAQ: What is the first change worth making?

Add verification_pending and refuse to translate it into failed. Then make every retry observe the destination before it acts. That single distinction removes the most tempting duplicate path.

Optimizing only the clean path leaves the recovery problem unsolved. The recovery path is where the system decides whether to spend reader trust twice.

If you automate publishing, what is the one state between “I clicked” and “I can prove it” in your system today?


I build Simple Memo alone and write about the operational edges that appear when one person owns every failure mode. My running notes live on the small site behind the app.

Top comments (0)