Repository navigation
fix(cron)!: treat blank thread_id/id as unset; document thread_id/id behavior - #1564
Conversation
- Thread Behavior: 'no thread_id writeback' (enabled=false can still be written back), qualify the pinned-thread row on successful writeback and link the protected-file caveat, mark '""' invalid on every platform - drop the sentence duplicating the 'omitted/omitted' row - config-reference: thread_id default is unset, not ""; baseline rejects only disable_on_success, other usercron-only fields are ignored - note id is ignored in baseline [[cron.jobs]] - use 18-digit example IDs consistently
- field table: omitting thread_id with a usercron id pins the job to its first thread; id must be non-empty and unique (writeback updates the first match), empty id counts as unset - replace the commented thread_id = "" in config.toml.example - add a Troubleshooting row for the empty thread_id symptom - use a thread_id example distinct from the channel example
Describe thread_id = "" as a known defect rather than a rule (only the Discord failure is verified; a future release may treat it as unset), and note that a written-back thread_id takes effect after the next usercron reload (about a minute).
- matrix: id column 'any' (id still drives enabled=false writeback); qualify pinning on a successful thread creation that records a real thread ID; replace the anchor that does not exist on this base - config-reference: omitting thread_id pins with a usercron id; id must be non-empty and unique - config.toml.example: 18-digit channel example
thread_id = "" deserialized to Some(""), skipped thread creation and
failed the send. Normalize thread_id and id with non_empty_opt in
parse_job_list (shared by baseline and usercron), and trim the file-side
id in update_usercron_job so a padded id still matches on writeback.
…e cases Blank thread_id/id now behave like omitted fields, so remove the "" matrix row, Troubleshooting row and "never \"\"" comments. Shorten the id row, move writeback edge cases (reload delay, failed persist, Telegram chat-ID writeback) below the matrix, add matching Troubleshooting rows, and fix the contradictory usercron-only fields intro.
This comment has been minimized.
This comment has been minimized.
… ids, tighten id docs F1: document and test non-blank thread_id trimming (normalize_job_config doc comment, Thread Behavior note, padded case in the thread_id test table). F2: reduce both id field rows to one sentence linking to Thread Behavior, and keep Thread Behavior as the single canonical copy of the writeback edge cases (collapse the duplicated 'writeback fails' prose). F3: skip usercron entries whose ids collide after trimming (warn + skip at load, consistent with other load_usercron_file validation), so a padded duplicate id can no longer overwrite another job's writeback entry. Adds load_usercron_file_skips_ids_that_collide_after_trimming.
|
Thanks for the round-1 review. Addressed all three 🟡 findings in 1ad66c6. CI F1 — Document and test
F2 — Concise
F3 — Whitespace-only The non-blocking suggestions (writeback gate / Telegram root cause tracking issue, |
This comment has been minimized.
This comment has been minimized.
… target Round-2 F1: move the duplicate-id reservation to the top of the load_usercron_file filter, before parse_cron_expr/timezone/platform checks, so the first file-order entry owns its trimmed id even when it is invalid or disabled. Previously a cron-invalid first entry failed to claim its id, letting a later same-id entry load and run — its writeback would then land on the first raw entry (the cross-job write the guard was meant to prevent). Add a comment that the early reservation is intentional. Round-2 F2: document the full duplicate-id policy — all duplicates (compared after trimming, including exact matches) keep only the first entry, and an invalid/disabled first entry still blocks later same-id entries. Reword the warn to 'duplicate id (compared after trimming)'. Add load_usercron_file_skips_exact_duplicate_ids and load_usercron_file_first_entry_owns_id_even_when_invalid (invalid cron, invalid timezone, disabled first entry).
Round-2 F2: the writeback edge-case bullet now covers all duplicate ids (exact matches and whitespace-only differences) sharing one writeback target, keeping only the first file-order entry, and that an invalid or disabled first entry still blocks later same-id entries. Match the warn text updated in code (duplicate id (compared after trimming)).
|
Thanks for the round-2 review. Both 🟡 findings addressed. F1 — Reserve the id before any validation New test F2 — Document the full duplicate-id policy
Verification on this head:
The tracking issue for the writeback gate / Telegram chat-ID fallback is still noted as a follow-up; I'll open it and link it from the Telegram edge-case text. |
This comment has been minimized.
This comment has been minimized.
Fixes clippy::bool_assert_comparison introduced in b84a7f3.
This comment has been minimized.
This comment has been minimized.
…-resolution and Telegram limitation Address round-4 review (chaodu-obk): docs/metadata only, no code change. - F1: Breaking Change callout + migration step + Troubleshooting row for the duplicate-id skip (duplicate ids that ran on main now stop after upgrade). - F2: writeback re-resolves first trimmed-id match at write time; external edits while a job is in flight can redirect thread_id/enabled to another entry. - F3: mark the Telegram chat-ID fallback as a known limitation; tracking issue pending.
|
Round-4 review addressed. All three blocking findings were docs/metadata only, as noted — no code change. Commit: thoththeagent@8bfa926 F1 — Treat the duplicate-id skip as a breaking change. Done.
F2 — Document the writeback re-resolution window. Done. Added a Writeback edge-case bullet: writeback re-resolves the first trimmed-id match at write time (not the loaded snapshot); F3 — Mark the Telegram fallback as a known limitation. Done. The bullet is now prefixed Known limitation, states that chat IDs and topic IDs are both numeric so a written-back value can't be distinguished from a correct topic ID, and marks it as a documented workaround rather than final behavior. Tracking issue not opened yet, so it reads "tracking issue pending" per your allowance; I'll link it once it exists. F4–F8 (green) require no action. Non-blocking follow-ups (writeback-target hardening, Telegram root cause, release note, readability) are left as follow-ups. |
|
On the F3 tracking issue: I can't open one on Could a maintainer open the tracking issue for the writeback-gate / Telegram chat-ID fallback root cause? Once it exists I'll swap the docs' "tracking issue pending" for the link. The docs already mark the bullet as a known limitation per F3, so this is the only remaining piece. |
|
Heads-up: This commit only changes |
This comment has been minimized.
This comment has been minimized.
…ent tracking issue ref - Move the [!CAUTION] Breaking Change callout out of the duplicate-id list item to a top-level blockquote under Thread Behavior, so GitHub renders it as an alert instead of a plain blockquote with a literal [!CAUTION] marker (verified via the GitHub GFM markdown API). Addresses round-5 F1. - Drop the "(tracking issue pending)" parenthetical that pointed to an issue that does not exist and cannot be created from this account; keep the known-limitation marker and recovery step. Addresses round-5 F2.
|
Round-5 review addressed. Both blocking findings were docs-only, as noted. Commit: thoththeagent@77c5fea F1 — Breaking Change callout did not render as a GitHub alert (nested in a list item). Fixed.
F2 — Docs referenced a tracking issue that does not exist and cannot be created from this account. Fixed.
F3-F7 were acknowledgements of resolved items; no action needed. The non-blocking suggestions (writeback gate, observability summary, test-comment wording, redundant trims, placeholders) remain noted for the follow-up and are out of scope for this docs round. This increment changes only |
This comment has been minimized.
This comment has been minimized.
The callout now sits above the duplicate-id bullet, so 'see the Breaking Change note below' pointed the wrong way. Drop the direction word so the reference survives future moves (round-6 F1).
|
Thanks for the round-6 review. Addressed the one blocking finding. F1 🟡 — cross-reference direction ( F2–F6 🟢 — Agreed, no action needed; these confirm the round-5 fixes and the load/writeback invariant. Non-blocking suggestions — Noted. The tracking-issue creation, the duplicate-id ownership wording tweak ("skipped or disabled"), the shared canonical-id helper, the version string in the new callout, and the bullet-splitting readability pass are all reasonable follow-ups. The tracking issue in particular is a maintainer action, as you note, since I can't open issues on this repo. Leaving these out of this PR to keep the scope to the |
|
Note LGTM ✅ - The round-6 finding is fixed: the duplicate-id bullet now points to the Breaking Change note without a direction word. A full re-check of What This PR Does
How It Works
Findings
Finding Details🟢 F1: Cross-reference fixedRound 6 asked to drop or flip the direction word. The author dropped it, which is the preferred option. The nearest Breaking Change callout is the one at 🟢 F2-F6The code has not changed since round 4. Reviewers re-read the full diff from the architecture, performance, readability, docs, security, and simplicity angles and found no new correctness, safety, or security issues. Non-blocking suggestions and follow-ups (not required for this PR)
Addressing Previous Rounds and Author FeedbackRound 6 (
|
| Prior finding | Status on cc5303cf |
|---|---|
| F1 Duplicate-id bullet pointed to the callout "below" while it sits above | ✅ Addressed in cc5303cf |
@thoththeagent (reply to round 6)
Dropped the direction word entirely so the reference survives future moves:
(see the Breaking Change note).
✅ Verified. This is the only change in the increment.
The tracking issue in particular is a maintainer action, as you note, since I can't open issues on this repo.
ℹ️ Accepted. This is not counted against the PR (see follow-ups).
No other reviewer feedback exists on this PR.
Baseline Check
- PR opened: 2026-10-09 (continuation of docs(cron): document thread_id/id interaction, fix empty thread_id #1561)
- Head:
cc5303cf3974e2fa3a39759629c34f434e5e55a0; basemain; merge-base6431a1b6; not stacked - Full diff: 4 files, +294/-16 (
config.toml.example,crates/openab-core/src/cron.rs,docs/config-reference.md,docs/cronjob.md); round-7 increment77c5feac..cc5303cf:docs/cronjob.md+1/-1 - Main already has:
non_empty_opt,should_create_cron_thread, first-matchupdate_usercron_job(untrimmed),usercron_write_lock, the gateway chat-ID fallback, the sticky-bit sentence, and no duplicate-id handling - Net-new value: parse-time canonicalization, trim-aware writeback, the load-time duplicate-id ownership guard, and Thread Behavior docs
- CI:
check,Review Contract, and all smoke tests are green
What's Good (🟢)
- The round-6 fix was precise and picked the wording that will not go stale
- The breaking change is disclosed in the title, the PR body, a top-level alert, and Troubleshooting
- The core invariant (load-time id ownership, first-match writeback, preserved file order) is sound, commented, and tested
- The author responded to every round quickly and kept follow-ups explicit
Three Reasons We Might Not Need This PR
- The core bug needs two lines. Normalizing
thread_idalone fixesthread_id = ""; id trimming, dedup, and the breaking-change docs grew over seven rounds. Counter: exact duplicate ids already cross-wrote onmain, so the dedup fixes a real pre-existing bug, and trim plus dedup form one invariant. - Rejecting could be simpler than normalizing. Failing the whole usercron file on padded or duplicate ids would give a stricter, more visible invariant. Counter: that would turn one bad entry into a total outage for every usercron job, while warn-and-skip matches how the loader already handles invalid entries.
- The config file is also a state store. Re-resolving by id at write time and the Telegram chat-ID fallback are both consequences of writing state into a user-edited file. Counter: both are pre-existing on
main; this PR documents them with recovery steps instead of widening them.
…ndow (#1573) GitHub delivers the hourly schedule every ~4-5h, so the 'commit in the last ~70min' gate skipped most merges. The last full pre-beta build was 99c11ec (2026-10-03); #1562, #1564, #1568 and #1569 never reached pre-beta-*, which also kept downstream openab-pty images on Node 22.23.3. Build when main HEAD differs from the head SHA of the last successful run that tagged every agent. Co-authored-by: chaodu-agent <274062505+chaodu-agent@users.noreply.github.com>
What problem does this solve?
Continuation of #1561, which was auto-closed while waiting on the author. My apologies for the silence; the round-2 review went unnoticed on my side. This PR carries over all #1561 commits, including @chaodu-agent's four follow-up commits, rebased onto current
main(after #1562). It then addresses every round-2 finding.The underlying problems are unchanged:
thread_id = "". That value deserialized toSome(""), skipped thread creation, and failed the send. Nothing was posted; the error only appeared in the logs.idwriteback, which pins a job to its first thread, was undocumented.idwas missing from the field table.Supersedes #1561.
Discord Discussion URL: https://discord.com/channels/1491295327620169908/1491365158868619404/1556505265183137842
Review Contract
Goal
Make blank
thread_id/idbehave like omitted fields, and document howthread_idandiddecide where a cron job posts, including writeback edge cases.Non-goals
Accepted Residual Risks
create_topicstill persists the chat ID asthread_id. This is now documented with a Troubleshooting row and a recovery step (remove the line). The code fix is a follow-up.Acceptance Criteria
thread_id = ""/ whitespace-only behaves exactly like an omittedthread_idon both the baseline and usercron paths (tested).idis treated as unset; a paddedidstill matches on writeback (tested).""as a defect or say "never""".idmatrix row is one sentence; the edge cases are listed separately with an anchor link to Choosing the right scope.failed to persist usercron thread_idand the Telegram chat-ID writeback.Follow-ups
thread_idwriteback on a thread actually being created (ch.thread_id.is_some() || ch.parent_id.is_some()), so the gateway fallback no longer persists the chat ID (review F4).channelplaceholders, de-duplicating theiddescription, staleplatformrows inconfig-reference.md, andtimezonecomment alignment.At a Glance
Prior Art & Industry Research
Not applicable — small input-normalization bug fix plus docs for existing behavior.
Proposed Solution
Code (
crates/openab-core/src/cron.rs):normalize_job_config()inparse_job_listcanonicalizesthread_idandidwith the existingnon_empty_opthelper.parse_job_listis shared by baseline and usercron, andfire_cronjobonly readsParsedJob.config. The value is therefore normalized beforesend_message()runs, not only insideshould_create_cron_thread(per F1).update_usercron_jobtrims the file-sideidbefore comparing, soid = " x "matches the trimmed lookup key (the pre-existing edge case noted in F1).parse_job_list_normalizes_thread_id_on_baseline_and_usercron: omitted,"", whitespace-only, and a real ID on both paths. It also assertsshould_create_cron_thread.parse_job_list_trims_and_drops_blank_idupdate_usercron_job_matches_padded_id_in_fileDocs:
""matrix row, the""Troubleshooting row, and every "never""" comment (cronjob.md,config-reference.md,config.toml.example). Added one line stating that blank equals omitted.idrow is now one sentence. Reload delay, failed persist, and Telegram moved to a short "Writeback edge cases" list with an anchor link to#choosing-the-right-scope. Added a Troubleshooting row keyed onfailed to persist usercron thread_id.thread_id, with a Troubleshooting row (create_topic failed/create_topic timeout) and the fix (remove the line).Why this approach?
F1 option (a): the fix is two lines with no compatibility risk, since
""could never succeed on any platform. Fixing the code is cheaper than keeping, and later deleting, docs about the defect.Alternatives Considered
.or(...)fallback), so it deserves its own review.Validation
cargo test -p openab-core --lib cron::: 78 passed, 0 failed, including the 3 new tests.cargo clippy -p openab-core -- -D warnings(same scope as CI): clean. With--all-targetsthere are 6 pre-existing test-only lints (cron.rs:1272,1294and others informat.rs,discord.rs), none in the changed code.#thread-behavior,#choosing-the-right-scope,#platform-prerequisites.0.10.0-beta.1(see docs(cron): document thread_id/id interaction, fix empty thread_id #1561):thread_id = ""loggedfailed to send cron message ... cannot parse integer from empty string;idwithoutthread_idpinned the job to its first thread.@chaodu-obk round-2 findings, point by point:
parse_job_list, covering both paths beforesend_message, plus tests. Padded-idwriteback fixed. All""defect docs removed.failed to persist usercron thread_id.mainafter #1562; the scope reference is now an anchor link.Behavior change from
main(duplicate usercron ids)On
main, two usercron entries with the sameidboth load and run. As of this PR, duplicate ids (compared after trimming, including exact matches) share one writeback target, so the loader keeps only the first entry in file order and skips the rest (usercron: duplicate id (compared after trimming), skipping). The first entry owns the id even when it is itself invalid or disabled, so a later same-id entry still does not run. This prevents a later job'sthread_id/enabledwriteback from landing on a different entry.Breaking change / migration: this is a user-facing behavior change (a duplicate-id job that ran on
mainsilently stops after upgrading). Before upgrading, checkcronjob.tomlfor ids that are equal after trimming — including values that differ only by surrounding whitespace — and make them unique. The docs carry a> [!CAUTION] Breaking Changecallout and a Troubleshooting row for this.