Files
hoelee-blog/src/content/posts/the-dialog-opened-in-7-seconds-my-script-waited-5.md
T
hoelee 18f2ed3c74
Deploy / build (push) Successful in 22s
draft(ai): park the composer-race post (EN+ZH) — not to be published
Written 2026-10-16 from the linkedin-daily-post session, then dropped by the user.
Kept as draft:true rather than deleted so the text is version-controlled and a
flip of one flag can publish it later. draft:true excludes it from the build
(153 pages vs 155), the listings and the sitemap — verified. Its OG/banner PNGs
were deleted (drafts in this repo ship without images); BANNERS/TERMINALS entries
for the slug are already committed, so regeneration is one command.
2026-10-16 13:13:08 +08:00

7.9 KiB

title, description, pubDate, category, tags, ogImage, banner, draft
title description pubDate category tags ogImage banner draft
The Dialog Opened in 7.2 Seconds. My Script Waited 5. A cron job kept reporting 'the composer dialog did not open'. The three DOM stages mounted at 1.8 s, 2.8 s and 7.2 s — and the script only looked once, at 5 s. 2026-10-15 ai
automation
linkedin
cron
browser
agents
/og/the-dialog-opened-in-7-seconds-my-script-waited-5.png /banners/the-dialog-opened-in-7-seconds-my-script-waited-5.png true

The failure message that was technically true and completely wrong

My scheduled posting job reports to my phone when it can't do its job, and that morning it said:

⚠️ Cron 'linkedin-daily-post' failed: Script exited with code 1
   LinkedIn daily post — FAILED
   FAILED: the composer dialog did not open

Confident, specific, and useless. Nothing had been published, the site was up, the browser profile was logged in, and the message told me which component to blame: a dialog that never appeared.

The dialog had appeared. It appeared 3.2 seconds before my script decided it hadn't.

Where I looked first, and why that was the wrong place

An automation that fails against a website has three usual suspects: the session expired, the site changed its markup, or the account got rate-limited. I checked the session first — it was healthy. And in my own project notes, the two earlier failures, four and five days before, carried the same message, with the working theory written down as "the session may be dead".

That sentence is the tell. Two identical failures, one theory, and the theory was never confirmed. A repeated failure with an unconfirmed cause is a measurement problem, not a diagnosis problem.

Measure the mount instead of the message

So I opened the composer with the browser in the state the script leaves it, and timed when each piece of the DOM actually arrives:

+1.8 s   the composer dialog element exists
+2.8 s   the Post / Proofread buttons exist
+7.2 s   the text editor exists          <- the node the script actually types into

The script clicked "start a post", slept five seconds, and looked once. At five seconds the dialog was there, the buttons were there, and the editor was not — and the code's single failure message covered all three cases with the sentence "the dialog did not open".

Nothing was broken. The script was asking a question at the wrong time and reporting the answer as a fact about LinkedIn.

The fix: poll for the node you need, not for the clock

  • Poll for the specific node, with a 40-second deadline, instead of sleeping a fixed amount and checking once.
  • Name the missing precondition. A timeout now says whether the dialog, the buttons or the editor failed to appear. That single change would have saved two sessions.
  • Retry the click once if no dialog appears at all. A swallowed click is a real second failure mode, and it deserves a different response than a slow mount.
  • Leave a screenshot on failure. The next person debugging this gets pixels, not a sentence.
  • Remove the same single-shot look from the edit-and-verify modal, which had the identical shape on a different screen — the same bug in a second place is not a coincidence, it is a pattern in how the code waits.
  • Tests: 13 new browser-free tests pin the staged mount and the swallowed click, so a future refactor that reintroduces sleep(5) fails in CI instead of on my phone.

The second defect, which only publishing could reveal

With the race fixed, the post went out — but not on the first attempt, and the second failure is the more interesting one. The first click on "Post" was ignored for 75 seconds. Then an identical click — same coordinates, same DOM, and I verified the pointer sequence did reach the button's span — was accepted in 2 seconds.

A click that is delivered and does nothing looks exactly like a click that was never delivered. The only way to tell them apart is to stop treating the click as the proof and make the result the proof:

  • the composer closing is now the only accepted confirmation of submission;
  • a bounded three-attempt re-click, with a refusal to re-click a disabled button;
  • and no tearing the tab down mid-submission, which is what "cleaning up" did while the request was still in flight.

The queue problem hiding behind the bug

The bug was loud. The next failure would have been silent.

My drafts are approved in batches with a 72-hour time-to-live, and the queue drains at one post per day. A batch that was approved for a 12-day runway therefore expired — all seven remaining rows — at 10:36 that same morning. The next morning's tick would have found nothing usable and simply not posted. Nobody gets an error message for work that no longer exists; a scheduler that runs out of work goes quiet, and quiet looks like "nothing to say today".

I fixed only the expiration on the seven unused, hash-matching rows, leaving status, hash and payload untouched, because whether a draft gets re-approved is a human decision and the automation should not manufacture consent. The durable fix — sizing the approval window to the queue's drain rate instead of the batch's import date — is a decision I deliberately left open.

What I would do differently

  1. Never sleep(n) and look once at anything that mounts asynchronously. Poll for the artifact you need, with a deadline and a named failure.
  2. Treat a repeated false failure as evidence the first diagnosis was wrong. "The session may be dead" was a story that fit the symptom and was never tested.
  3. Make the error message name the missing precondition. Vague messages do not just fail to help — they actively misdirect, and this one sent me looking at sessions and markup for two days.
  4. Prove submission by the result, not by the click. An input event is not a side effect; the disappearance of the composer is.
  5. Monitor the queue separately from the job. A job that runs perfectly and posts nothing is indistinguishable from a working one unless you count what came out.

Verification

Check Result
Post live published, with the permalink and activity URN read back from the page
Content authenticated read-back confirms the body text and the bold phrase rendered correctly
Cron path re-ran the job: ok, failure_streak back to 0
Double-post guard correctly refused a second post — "post cap reached: 1 done today"
Ledger approval consumed and a POST_PUBLISHED row written with the permalink
Tests 404 pass, including the 13 new ones for the staged mount and the swallowed click
Cleanup the 11 tabs my probes opened were closed; the browser's original 4 were left as found

The post that went out, by the way, was the one about running speech-to-text on my own hardware — the two-of-everything setup I described here. It has nothing to do with this bug. That is rather the point: the job's reporting was wrong about what had happened, and wrong reporting is worse than no reporting.

Want this for your business?

If you have a repetitive publishing, reporting or data-entry job that a person is doing by hand — or an automation that "sometimes fails" and nobody knows why — I build and debug browser and API automations on infrastructure I run myself. Most of the work in a post like this is not the happy path; it is making the failure honest enough to fix.

WhatsApp: +60 12-797 2969 · Email: [email protected] · hoelee.com

I run Hoelee Enterprise, a website design and development business for Malaysian SMEs — and the automation work above runs on the same infrastructure I build for clients. If you are hiring for a platform, DevOps or AI-infrastructure role, the same channels reach me.