From 3e354cc5db6dda2d0b3ac6f65bdf7d768de91f32 Mon Sep 17 00:00:00 2001 From: hoelee Date: Fri, 16 Oct 2026 13:19:42 +0800 Subject: [PATCH] remove(ai): delete the composer-race post and its generator entries (user decision) - git rm src/content/posts/the-dialog-opened-in-7-seconds-my-script-waited-5.md + the zh twin - drop its TERMINALS/BANNERS entries from both generators (pure deletions: 0 added, 17 + 5 removed, node --check OK, 0 remaining references to the slug, the other three slugs intact) - build still 153 pages; no route, no listing row, no sitemap entry --- scripts/banner-gen/generate.mjs | 17 -- scripts/og-gen/generate.mjs | 5 - ...-opened-in-7-seconds-my-script-waited-5.md | 166 ------------------ ...-opened-in-7-seconds-my-script-waited-5.md | 101 ----------- 4 files changed, 289 deletions(-) delete mode 100644 src/content/posts/the-dialog-opened-in-7-seconds-my-script-waited-5.md delete mode 100644 src/content/posts/zh/the-dialog-opened-in-7-seconds-my-script-waited-5.md diff --git a/scripts/banner-gen/generate.mjs b/scripts/banner-gen/generate.mjs index 1ca03f0..c69efe0 100644 --- a/scripts/banner-gen/generate.mjs +++ b/scripts/banner-gen/generate.mjs @@ -1254,23 +1254,6 @@ BANNERS['docker-reported-exit-0-the-oom-killer-disagreed'] = { ], }; -BANNERS['the-dialog-opened-in-7-seconds-my-script-waited-5'] = { - titlebar: "root@win11 - the dialog was open", - lines: [ - { t: 'prompt', text: "$" }, { t: 'cmd', text: "cron linkedin-daily-post" }, - { t: 'dim', text: "dialog +1.8s - buttons +2.8s - editor +7.2s" }, { t: 'err', text: "script slept 5s, looked once: 'dialog did not open'" }, - { t: 'prompt', text: "$" }, { t: 'cmd', text: "poll(node=editor, timeout=40s)" }, - { t: 'ok', text: "submitted - the composer closing is the proof" }, { t: 'hl', text: "click ignored 75s, identical click accepted in 2s" }, - { t: 'dim', text: "13 tests pin the staged mount + the swallowed click" }, { t: 'dim', text: "72h approval TTL vs a 12-day queue: silent ticks" }, - ], - flow: [ - { n: '1', label: "click start a post" }, - { n: '2', label: "poll for the editor" }, - { n: '3', label: "submit once" }, - { n: '4', label: "confirm it closed" }, - ], -}; - BANNERS['authentik-css-greater-than-bug'] = { titlebar: "auth.hoelee.com - brand css", lines: [ diff --git a/scripts/og-gen/generate.mjs b/scripts/og-gen/generate.mjs index 4316d9b..f26e3da 100644 --- a/scripts/og-gen/generate.mjs +++ b/scripts/og-gen/generate.mjs @@ -382,11 +382,6 @@ TERMINALS['docker-reported-exit-0-the-oom-killer-disagreed'] = `
 ExitCode 0 - OOMKilled false - RestartCount 6
$dmesg | grep -i 'killed process'-> 6 x cgroup OOM at ~480 MB / 512 MiB
`; -TERMINALS['the-dialog-opened-in-7-seconds-my-script-waited-5'] = ` -
$linkedin-daily-post
-
 FAILED: the composer dialog did not open
-
$dialog +1.8s, buttons +2.8s, editor +7.2s-> poll, never sleep
`; - TERMINALS['authentik-css-greater-than-bug'] = `
$grep 'data-id="brand-css"' page.html -> rule is there
 selector matches, computed display: list-item
diff --git a/src/content/posts/the-dialog-opened-in-7-seconds-my-script-waited-5.md b/src/content/posts/the-dialog-opened-in-7-seconds-my-script-waited-5.md deleted file mode 100644 index 06207db..0000000 --- a/src/content/posts/the-dialog-opened-in-7-seconds-my-script-waited-5.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: "The Dialog Opened in 7.2 Seconds. My Script Waited 5." -description: "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." -pubDate: 2026-10-15 -category: ai -tags: ["automation", "linkedin", "cron", "browser", "agents"] -ogImage: /og/the-dialog-opened-in-7-seconds-my-script-waited-5.png -banner: /banners/the-dialog-opened-in-7-seconds-my-script-waited-5.png -draft: 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: - -```text -⚠️ 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: - -```text -+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](/posts/self-hosted-speech-to-text-api/). -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](https://wa.me/60127972969)** · **Email: [me@hoelee.com](mailto:me@hoelee.com?subject=Automation%20that%20tells%20the%20truth)** · **[hoelee.com](https://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. diff --git a/src/content/posts/zh/the-dialog-opened-in-7-seconds-my-script-waited-5.md b/src/content/posts/zh/the-dialog-opened-in-7-seconds-my-script-waited-5.md deleted file mode 100644 index b70ac9d..0000000 --- a/src/content/posts/zh/the-dialog-opened-in-7-seconds-my-script-waited-5.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: "对话框 7.2 秒就打开了,而我的脚本只等了 5 秒" -description: "定时任务一直报「composer 对话框没有打开」。其实三个阶段分别挂在 1.8 秒、2.8 秒和 7.2 秒——而脚本只在第 5 秒看了一次。" -pubDate: 2026-10-15 -category: ai -tags: ["automation", "linkedin", "cron", "browser", "agents"] -ogImage: /og/the-dialog-opened-in-7-seconds-my-script-waited-5.png -banner: /banners/the-dialog-opened-in-7-seconds-my-script-waited-5.png -draft: true ---- - -## 一条技术上属实、方向上全错的失败消息 - -我那个定时发帖任务干不成活的时候会往我手机上发消息,那天早上它说: - -```text -⚠️ Cron 'linkedin-daily-post' failed: Script exited with code 1 - LinkedIn daily post — FAILED - FAILED: the composer dialog did not open -``` - -语气笃定、指认具体、毫无用处。没有任何内容被发出去,站点是好的,浏览器配置也还登着,而这条消息告诉我该怪谁:一个根本没出现过的对话框。 - -对话框出现了。它在我脚本判定它没出现之前 3.2 秒就已经出现了。 - -## 我先看的地方,以及为什么那是错的地方 - -一个对着网站上跑的自动化失败,通常有三个嫌疑对象:会话过期、网站改了结构、账号被限流。我先查了会话——健康。而在我自己的项目笔记里,四五天前的两次失败带着**一模一样**的消息,当时写下的工作假设是「会话可能已经死了」。 - -那句话就是线索。两次完全相同的失败、一个假设,而那个假设从未被验证过。一个反复出现、原因却没被确认的失败,是**量测问题**,不是诊断问题。 - -## 去量挂载时间,别去猜消息 - -于是我把浏览器保持在脚本留下的状态里打开 composer,给每一块 DOM 到达的时间记账: - -```text -+1.8 秒 composer 对话框元素存在 -+2.8 秒 Post / Proofread 按钮存在 -+7.2 秒 文本编辑器存在 <- 脚本真正要输入的那个节点 -``` - -脚本点了「开始发帖」,睡了五秒,然后看了**一次**。在第 5 秒,对话框在、按钮在、编辑器不在——而代码只有一条失败消息,用「对话框没有打开」这一句话覆盖了全部三种情况。 - -没有任何东西坏掉。是脚本在错误的时刻问了一个问题,然后把答案当成了一桩关于 LinkedIn 的事实。 - -## 修法:为你要的节点轮询,而不是为时钟 - -- **为具体节点轮询**,给 40 秒的期限,而不是睡一个固定时长再看一次。 -- **把缺失的前置条件说出来。** 现在超时会说明是对话框、按钮还是编辑器没出现。仅这一处改动就能省掉两个会话。 -- **如果对话框根本没出现,把点击重试一次。** 被吞掉的点击是另一个真实的失败模式,它值得一个与「挂载慢」不同的应对。 -- **失败时留下截图。** 下一个查这个 bug 的人应该拿到像素,而不是一句话。 -- **把编辑与校验弹窗里同一种「看一次」的写法也删掉**——那是另一个屏幕上完全相同的形状。同一个 bug 出现在第二处不是巧合,而是这段代码等待方式的模式。 -- **测试:** 新增 13 条不依赖浏览器的测试,钉住分阶段挂载与被吞掉的点击,于是将来任何重新引入 `sleep(5)` 的重构,会在 CI 里失败,而不是在我手机上失败。 - -## 第二个缺陷,只有真的发出去才看得见 - -竞态修好之后,帖子发出去了——但不是一次成功,而第二次失败更值得写。第一次点「Post」被**无视了整整 75 秒**。然后一次完全相同的点击(同样的坐标、同样的 DOM,我核对过指针事件序列确实到达了按钮的 span)在 **2 秒**内被接受。 - -一次「被送达但毫无作用」的点击,和一次「从未被送达」的点击,看起来完全一样。唯一能分辨两者的办法,是别再拿点击当证据,改拿**结果**当证据: - -- 现在只有 composer 自己关闭,才算提交被确认; -- 有上限的三次重试点击,并且拒绝对一个 disabled 按钮再点; -- 提交在途时不再把标签页拆掉——所谓「清理」在请求还在飞的时候就干过这件事。 - -## bug 背后藏着的是队列问题 - -这个 bug 很吵。而下一个失败会是**无声**的。 - -我的草稿是分批审批的,每批带 72 小时有效期,而队列每天只消耗一篇。于是一批按「12 天余量」审批的草稿,在当天上午 10:36 全部过期——剩下七行全部作废。第二天早上的定时任务会发现没有可用内容,然后**什么都不发**。「再也没有工作可做」不会产生任何错误消息;一个跑空了活的调度器只会安静下来,而安静看起来就像「今天没什么好发的」。 - -我只修了那七行未使用、哈希匹配记录的过期时间,状态、哈希、载荷一个字没动——因为草稿要不要重新批准是人的决定,自动化不该自己制造同意。至于那个更彻底的修法(让审批窗口按队列的消耗速率来定,而不是按批次导入的日期),我故意留着没做。 - -## 如果重来一次 - -1. **对任何异步挂载的东西,永远不要 `sleep(n)` 之后只看一次。** 为你要的产物轮询,带期限、带具名失败。 -2. **把一个反复出现的假失败,当成「第一次诊断错了」的证据。** 「会话可能已经死了」是一个贴合症状、却从未被检验的故事。 -3. **让报错消息说出缺失的前置条件。** 含糊的消息不只是没帮助——它主动把你带偏,这一条让我去查了两天的会话和页面结构。 -4. **用结果证明提交,而不是用点击。** 输入事件不是副作用;composer 消失才是。 -5. **把队列的健康和任务的健康分开监控。** 一个运行完美却没发出任何东西的任务,跟一个正常工作的任务长得一模一样——除非你去数它产出了什么。 - -## 验证 - -| 检查项 | 结果 | -|---|---| -| 帖子已上线 | 已发布,从页面回读了永久链接与 activity URN | -| 内容 | 认证态回读确认正文文字与加粗短语渲染正确 | -| 定时任务路径 | 重跑该任务:`ok`,`failure_streak` 回到 0 | -| 重复发布防护 | 正确拒绝第二次发布——「post cap reached: 1 done today」 | -| 台账 | 审批被消耗,并写入一条带永久链接的 `POST_PUBLISHED` 记录 | -| 测试 | 404 条通过,包含新增的 13 条(分阶段挂载、被吞掉的点击) | -| 收尾 | 我的探针打开过的 11 个标签页已关闭;浏览器原有的 4 个保持原样 | - -顺便说一句,那天真正发出去的那篇,是讲我把语音转文字跑在自己硬件上的那篇——[就是这里描述的那套「什么都是两份」的配置](/posts/self-hosted-speech-to-text-api/)。它跟这个 bug 毫无关系。而这恰恰是重点:这个任务的汇报对「发生了什么」说错了话,而错误的汇报比没有汇报更糟。 - -## 想要给你的业务做一套? - -如果你手上有一件重复性的发布、报表或数据录入工作正由人手工在做——或者有一个「偶尔会失败」但没人知道为什么的自动化——我在自己运维的基础设施上做浏览器与 API 自动化。像这样一篇文里的工作量,大部分不在顺风路径上,而在于**把失败弄得足够诚实,以至于能被修**。 - -**WhatsApp:[+60 12-797 2969](https://wa.me/60127972969)** · **邮箱:[me@hoelee.com](mailto:me@hoelee.com?subject=Automation%20that%20tells%20the%20truth)** · **[hoelee.com](https://hoelee.com)** - -我经营 Hoelee Enterprise,一家面向马来西亚中小企业的网站设计与开发公司——上面这些自动化工作,跑的就是我给客户搭建的那套基础设施。如果你正在招聘平台、DevOps 或 AI 基础设施方向的岗位,同样的渠道也能找到我。