remove(ai): delete the composer-race post and its generator entries (user decision)
Deploy / build (push) Successful in 21s

- 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
This commit is contained in:
hoelee committed 2026-10-16 13:19:42 +08:00
1 parent 27d3dca3c3
commit 3e354cc5db
4 files changed
-289

No files matched your search

-17
View File
@@ -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: [
-5
View File
@@ -382,11 +382,6 @@ TERMINALS['docker-reported-exit-0-the-oom-killer-disagreed'] = `
<div class="line"><span class="prompt">&nbsp;</span><span class="err">ExitCode 0 - OOMKilled false - RestartCount 6</span></div>
<div class="line"><span class="prompt">$</span><span class="cmd">dmesg | grep -i 'killed process'</span><span class="fix">-> 6 x cgroup OOM at ~480 MB / 512 MiB</span></div>`;
TERMINALS['the-dialog-opened-in-7-seconds-my-script-waited-5'] = `
<div class="line"><span class="prompt">$</span><span class="cmd">linkedin-daily-post</span></div>
<div class="line"><span class="prompt">&nbsp;</span><span class="err">FAILED: the composer dialog did not open</span></div>
<div class="line"><span class="prompt">$</span><span class="cmd">dialog +1.8s, buttons +2.8s, editor +7.2s</span><span class="fix">-> poll, never sleep</span></div>`;
TERMINALS['authentik-css-greater-than-bug'] = `
<div class="line"><span class="prompt">$</span><span class="cmd">grep 'data-id="brand-css"' page.html -> rule is there</span></div>
<div class="line"><span class="prompt">&nbsp;</span><span class="err">selector matches, computed display: list-item</span></div>
@@ -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: [[email protected]](mailto:[email protected]?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.
@@ -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)** · **邮箱:[[email protected]](mailto:[email protected]?subject=Automation%20that%20tells%20the%20truth)** · **[hoelee.com](https://hoelee.com)**
我经营 Hoelee Enterprise,一家面向马来西亚中小企业的网站设计与开发公司——上面这些自动化工作,跑的就是我给客户搭建的那套基础设施。如果你正在招聘平台、DevOps 或 AI 基础设施方向的岗位,同样的渠道也能找到我。