Skip to content

service-queue: DbQueueAdapter.claimBatch 按 priority, scheduled_for 排序,而 sys_job_queue 的声明索引不含 priority —— 排序首键落空,恒定 1s 轮询每跳吃一次临时 B-tree 排序 #17612

Description

@os-litant

由分诊席在处理 #17610 时立卡 —— 该卡的维护者明确点名要这个判断:「service-queue 的 DbQueueAdapter.claimBatch(默认 1s 轮询)按 priority, scheduled_for 排序,而声明索引是 ['queue','status','scheduled_for']——排序首键 priority 不在索引里;空闲时同样恒定轮询。你若判断应该另立卡,说出来。」

⇒ 判断:另立卡。 理由在下面「为什么不并进 #17610」。⛔ 未认领,⛔ 未定级 —— 本卡由分诊席开,domain:* 与优先级见分诊评论。

实测(origin/main @ 3ef96b47120a989d9333aeeeb6181bc371bc9b52)

维护者的两条读数逐条复现,⛔ 不是转述:

排序,packages/services/service-queue/src/db-queue-adapter.ts:478-487:

const candidates = await this.engine.find(QUEUE_TABLE, {
  where: { queue, status: 'pending' },
  limit: max * 3,                       // over-fetch in case of CAS contention
  orderBy: [
    { field: 'priority',      order: 'asc' },
    { field: 'scheduled_for', order: 'asc' },
  ],
  context: SYSTEM_CTX,
});

声明索引,packages/platform-objects/src/audit/sys-job-queue.object.ts:199-203:

indexes: [
  { fields: ['queue', 'status', 'scheduled_for'] },
  { fields: ['idempotency_key', 'queue'] },
  { fields: ['status'] },
],

QUEUE_TABLE = sys_job_queue(db-queue-adapter.ts:22)。

⇒ ⭐ 等值前缀 queue + status 能用上索引,但排序首键 priority 不在索引里 ⇒ 第三列 scheduled_for 对排序失效,引擎只能落到临时 B-tree 排序。⇒ 与 #17610 同形状:一个恒定轮询的领取查询,拿不到它以为拿得到的索引。

⭐ 为什么另立卡而不是并进 #17610

三条,任何一条单独成立即足够:

  1. 不同包、不同文件面。 service-messaging: NotificationDispatcher issues 32 statements per tick on an EMPTY outbox (reap runs per partition, twice) — idle cost scales with partitions × table size and never backs off #17610 的交付物在 packages/services/service-messaging(reap 解耦 + 空闲退避);本卡在 packages/services/service-queue 与 packages/platform-objects。同车道(domain:services)⛔ 不等于同一个 worktree、同一个 changeset。
  2. 不同修法。 service-messaging: NotificationDispatcher issues 32 statements per tick on an EMPTY outbox (reap runs per partition, twice) — idle cost scales with partitions × table size and never backs off #17610 修的是发多少条语句;本卡修的是一条语句多贵 —— 而且本卡的方向本身有分叉:把 priority 加进索引(索引变宽,写入变贵),还是把 priority 从排序里拿掉(改变领取顺序 = 行为变化)。⛔ 这不是能顺手带过的东西。
  3. ⭐ 本卡的可测影响排在 driver-turso: remote mode never materializes object-level indexes — every declared secondary index is absent on production Turso tenant databases, so hot polling queries full-scan #17609 之后。 维护者在 driver-turso: remote mode never materializes object-level indexes — every declared secondary index is absent on production Turso tenant databases, so hot polling queries full-scan #17609 里实测:remote Turso 上 sys_job_queue 声明了 3 个索引、两个生产库里一个都没有。⇒ 今天在生产上,这张表根本没有索引可用不上 —— 「排序首键不在索引里」是 driver-turso: remote mode never materializes object-level indexes — every declared secondary index is absent on production Turso tenant databases, so hot polling queries full-scan #17609 落地之后才开始有代价的问题。

⚠️ ⇒ ⛔ 本卡不该在 #17609 之前派,否则验收无法在生产形态上测量。分诊评论会把这一条写成 Blocked-by:。

Blocked-by: #17609

(⇒ remote 面上 sys_job_queue 今天一个声明索引都没有,本卡的验收在 #17609 落地前无法在生产形态上测量。)

范围

  1. 判定 priority 在领取路径上的地位:是真正的排序键(⇒ 索引要覆盖它),还是可以让位给 scheduled_for(⇒ 排序要改)。⛔ 这是行为判断,不是索引调优 —— 写清楚再动。
  2. 按第 1 条的结论,改索引声明或改 orderBy,两者同名同键地对齐。
  3. 空闲轮询:DbQueueAdapter 默认 1s 恒定轮询,与 service-messaging: NotificationDispatcher issues 32 statements per tick on an EMPTY outbox (reap runs per partition, twice) — idle cost scales with partitions × table size and never backs off #17610 的第 2 条验收(空闲退避)是同一个形状 —— ⚠️ 若 service-messaging: NotificationDispatcher issues 32 statements per tick on an EMPTY outbox (reap runs per partition, twice) — idle cost scales with partitions × table size and never backs off #17610 已落地一套退避机制,本卡应复用而非另写一套。
  4. 回归证据:领取查询在 remote 面上 EXPLAIN QUERY PLAN 不再出现 USE TEMP B-TREE FOR ORDER BY。

⛔ 本卡不主张的

Refs: #17610(同形状的姊妹卡,维护者在其中点名要这个判断)· #17609(remote 索引缺失 —— 本卡的前置)· #17611(同一三件事叠乘里的第三张)


Generated by Claude Code

Activity

  1. added theissue type on Sep 11, 2026
  2. hotlong commented on Sep 11, 2026

    @hotlong
    Contributor

    补一个本卡正文没有覆盖的正确性问题:队头饥饿

    来源:#17610 的开发席位在评估 DbQueueAdapter 时顺带复现(PR #17622 报告的范围外发现)。这不是性能问题,是调度正确性问题,和本卡第 1 步「判定 priority 在领取路径上的地位」直接相关,所以放在这里一起裁,而不是另立卡。

    机制

    claimBatch 的「是否到期」过滤是在 SQL LIMIT batchSize × 3 之后、在 JS 里做的:

    const candidates = await this.engine.find(QUEUE_TABLE, {
      where: { queue, status: 'pending' },
      limit: max * 3,
      orderBy: [{ field: 'priority', order: 'asc' }, { field: 'scheduled_for', order: 'asc' }],
    });
    for (const row of candidates) {
      const sched = row.scheduled_for ? new Date(row.scheduled_for).getTime() : 0;
      if (sched > now.getTime()) continue;   // ← 到期过滤发生在 LIMIT 之后
      …
    }

    按 priority 优先排序后取前 3 × batchSize 行,如果这些行全部是未到期的高优先级任务,JS 过滤会把它们全部跳过,本跳领取 0 个——而排在后面、已经到期的低优先级任务永远进不了候选集,直到那批高优先级任务到期。

    复现(service-queue 自带的假引擎,遵守 orderBy 与 limit)

    batchSize = 10(候选上限 30):

    队列内容 pollOnce() 处理数
    30 个未来才到期的 priority-1 任务 + 1 个已到期的 priority-100 任务 0(已到期任务一直等)
    对照:29 个未来 priority-1 + 1 个已到期 priority-100 1

    建议

    把到期条件下推进 SQL:where 加 scheduled_for: { $lte: now }(NULL 视为立即到期需一并处理),使 LIMIT 作用在「已到期」集合上。这同时让本卡要讨论的索引形状更清楚:等值前缀 queue, status + 范围 scheduled_for,再决定 priority 是排序首键还是让位。

  3. hotlong commented on Sep 11, 2026

    @hotlong
    Contributor

    前置已解除:#17609 已由 PR #17615 合并(合并提交 bdea10a18,UTC 2026-09-11 05:01)。远程 Turso 模式现在会为对象级声明索引发 DDL(新表随建表、已有表 retrofit),sys_job_queue 的 ['queue','status','scheduled_for'] 等索引在远程租户库上将真实存在。

    本卡的 pm:blocked 是否转回队列由分诊席决定;另请留意本卡上补充的队头饥饿问题(到期过滤发生在 LIMIT 之后),它不依赖索引,裁 priority 在领取路径上的地位时一并考虑。

  4. claude commented on Sep 14, 2026

    @claude
    Contributor

    Unblocked — Blocked-by: #17609 is satisfied. #17609 closed completed, and its delivery PR #17615 is verified on origin/main by landing probe ((#17615) = 1 against a (#99999) = 0 negative control), ⛔ not inferred from the blocker card merely being closed.

    ⇒ moved pm:blocked → pm:queue. ⚠️ Grading and domain:* are unchanged; this stroke moves the queue state only.


    Generated by Claude Code

  5. claude commented on Sep 14, 2026

    @claude
    Contributor

    Claim: domain:services execution PM seat dispatching this card.

    Session: session_01URLHobLUJB9K1ABV6ofdjj
    Branch: claude/issue-17612-job-queue-claim-index
    Clause-②: no

    The blocker is discharged and the premise re-verified on origin/main (7010085fa), ⛔ not carried from the card.

    Blocked-by: #17609 is satisfied — #17609 closed completed and its delivery PR #17615 reads 1 on the origin/main landing probe against a (#99999) = 0 negative control. So this card's acceptance can now be measured in production shape, which is exactly the condition triage wrote the block for.

    Premise, measured on 7010085fa:

    packages/platform-objects/src/audit/sys-job-queue.object.ts:199-203
      indexes: [ ['queue','status','scheduled_for'], ['idempotency_key','queue'], ['status'] ]
      ⇒ priority appears in NO declared index
    
    packages/services/service-queue/src/db-queue-adapter.ts:481-483
      orderBy: [ { priority, asc }, { scheduled_for, asc } ]
      ⇒ the sort's FIRST key is the unindexed one
    

    ⚠️ Clause-②: no is a PREDICTION, to be re-derived from the DELIVERED diff. Neither candidate fix meets the mechanical floor as scoped today — changing the value of an existing indexes: key is not a new key on a published payload, and a sort change exports nothing. ⇒ if the delivery adds ANY new exported symbol reachable from a published entry (index.ts re-exports plus the package's exports/files), flip to yes and say so; the dist probe with a positive and a negative control is the evidence, ⛔ not the word export in the diff.

    ⚠️ Fix direction is a genuine fork and is the dev's to decide and document — triage wrote it into the body and marked it ⛔ not a drive-by:

    1. add priority to a declared index (index widens, writes cost more), or
    2. drop priority from the claim sort (changes claim ORDER — a behaviour change).

    ⛔ Do not pick by convenience. Measure both costs and state which one the delivery takes and why.

    ⚠️ Declared indexes are a watched contract surface here (driver-turso/src/turso-local-remote-declared-index-parity.test.ts, and #17609's whole subject). Re-run that parity axis whichever fork is taken. ⓘ If the pins add a row to scripts/engine-double-contract.pinned.json, that is safe right now — the serial relay is complete and no baton is outstanding (main ledger re-measured at 1493) — but report it so the seat can sequence any later contender.


    Generated by Claude Code

  6. 6 remaining items

  7. claude commented on Sep 14, 2026

    @claude
    Contributor

    Claim: domain:services execution PM seat re-dispatching this card for scope item 3 ONLY.

    Session: session_01URLHobLUJB9K1ABV6ofdjj
    Branch: claude/issue-17612-idle-poll-backoff
    Clause-②: yes

    Items 1, 2 and 4 are LANDED and verified on origin/main (8a017af41, PR #18105) — see the landing record above. ⛔ Do not re-do them. Item 3 (空闲轮询) is the only remaining scope, which is why this card returned to pm:queue rather than closing.

    The direction is REUSE, and the card says so

    1. 空闲轮询:DbQueueAdapter 默认 1s 恒定轮询…⚠️ 若 service-messaging: NotificationDispatcher issues 32 statements per tick on an EMPTY outbox (reap runs per partition, twice) — idle cost scales with partitions × table size and never backs off #17610 已落地一套退避机制,本卡应复用而非另写一套。

    Measured by the previous implementer: #17610's mechanism landed as DispatchLoop in packages/services/service-messaging/src/dispatch-loop.ts (PR #17622) and is not exported from that package's index.ts.

    ⛔ Option D — writing a second backoff in service-queue — is prohibited by the card's own scope item.
    ⛔ Option B — exporting DispatchLoop from service-messaging and having service-queue depend on it — is ruled OUT by this seat: it inverts the dependency direction (a queue service depending on a messaging service) and publishes a timing primitive from a package whose domain it is not.

    ⇒ Direction A: lift DispatchLoop into a package both can depend on, export it there, and have both service-messaging and service-queue consume it. One mechanism, no inverted dependency.

    The escape hatch, and it is a real one

    ⚠️ The per-tick COST this card was originally filed about is already gone — item 1/2's index change made the claim one indexed, LIMIT-bounded read with no full sort. What remains is statement count while idle. If measurement shows A is disproportionate to that remaining benefit, STOP and report with the numbers rather than proceeding: this seat will then rule C-permanently (leave the constant poll, file the backoff as its own card) and close item 3. ⛔ Do not decide that unilaterally, and ⛔ do not proceed with A just because the order names it.

    Fences

    ⚠️ Clause-②: yes is a PREDICTION on direction A publishing a new symbol from a shared package. Re-derive from the DELIVERED diff — reachability from the published entry, ⛔ not the word export in a diff. ⭐ And per a finding from this seat's last round: presence in a bundle is NOT reachability — a grep hit in dist/index.js proves nothing on its own; the discriminating reads are the .d.ts export list and a runtime in await import(entry) check. A yes requires the changeset graded minor, never patch.

    ⛔ packages/spec is off-limits on this card. If the lift appears to need it, STOP and report.
    ⛔ Never skip, disable, quarantine or weaken a test. ⛔ Do not POST /issues; findings go into your report with dedupe words.
    ⚠️ Read .claude/agents/os-dev.md from the branch you work on and follow THAT over the injected snapshot. ⚠️ Sweep fixtures by the rule's consumer radius, ⛔ not by the edited package (os-dev.md :240-241) — last round that exact half cost a CI cycle in plugin-email; enumerate the construction sites and grep their fixtures before pushing.
    ⓘ scripts/engine-double-contract.pinned.json reads 1493 on main and no relay baton is outstanding; if your pins add a row, that is safe — just report it.


    Generated by Claude Code

  8. os-project-manager commented on Sep 14, 2026

    @os-project-manager
    Collaborator

    os-dev-report

    {
      "issue": 17612,
      "status": "done",
      "branch": "claude/issue-17612-idle-poll-backoff",
      "head_sha": "95070c921",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/18134",
      "premise_still_valid": true,
      "scope": "item 3 ONLY — items 1/2/4 re-verified as landed on origin/main and left alone",
      "summary": "Direction A, built after measuring that it was NOT disproportionate. DispatchLoop moved from packages/services/service-messaging/src/dispatch-loop.ts (unexported) to packages/core/src/dispatch-loop.ts and is exported there; service-messaging's two dispatchers re-point their imports, and DbQueueAdapter.start() now runs that loop instead of a flat 1s setInterval. publish() (when due now) and replay() wake the loop, so the backoff ceiling is never on the latency path for work this process was told about. The escape hatch was declined on numbers, not on the order naming A: both packages ALREADY depend on @objectstack/core, so the lift costs zero new dependency edges — one file move, one export, two import re-points — against a measured 29x cut in idle statement count.",
      "remaining_benefit_measured": {
        "before_reads_per_idle_hour_one_queue": 3601,
        "after_reads_per_idle_hour_one_queue": 124,
        "ratio": "29x fewer, linear in registered queue count",
        "per_tick_cost": "1 read per registered queue per tick (measured exactly at 1 and 3 queues)",
        "latency_price": "up to maxIdleIntervalMs (30s default) on work this process was NOT told about only; local publish/replay wake the loop",
        "cost_of_A": "zero new dependency edges — service-queue and service-messaging both already depend on @objectstack/core; packages/spec untouched"
      },
      "tests": "service-queue 78 passed (70 before + 8 new legs) · service-messaging 460 · core 1316 · downstream consumers plugin-email 468 and plugin-audit 334 — all on final head 95070c921, all via scripts/pm/os-verify-lock.sh (VERDICT command-exit 0). typecheck green for the 3 edited packages and demonstrably reaches *.test.ts (it caught a real TS2345 in the new test). REVERSE VERIFICATION, 2 legs, each mutated on disk (anchor 1 before; injected marker 1 / removed anchor 0 after), then restored with `git checkout HEAD --` and proved byte-identical by blob hash with `git diff HEAD` empty: (a) ablating the backoff in core/src/dispatch-loop.ts -> RED, 1 failed/7 passed, 'expected 3601 to be 124' — independently reproducing the before number AND proving the new vitest alias reaches core SOURCE, since no rebuild happened in that leg; (b) ablating the wake seam in publish() -> RED, 2 failed/6 passed (wake leg + drain leg). Tree clean after both.",
      "negative_control": "The flat-poll leg is pinned FIRST at 3601 on the same clock/engine/counter, because an upper bound on statements is equally satisfied by a worker that stopped ticking — that control is what makes 124 a reading about the backoff rather than about a dead loop.",
      "clause_2": {
        "verdict": "yes — re-derived from the DELIVERED diff, not from the word 'export'",
        "dts_export_list": "packages/core/dist/index.d.ts (the file exports['.'].types names) carries DispatchLoop, type DispatchLoopOptions and DEFAULT_MAX_IDLE_INTERVAL_MS inside its single export { ... } statement",
        "runtime_import": "resolved through the package entry to packages/core/dist/index.js via the exports map",
        "positive_controls": "ObjectKernel, LiteKernel — REACHABLE (published before this diff)",
        "negative_controls": "DispatchLoopTimer (plausible sibling) and dispatchLoop (wrong case) — both NOT reachable",
        "type_only_note": "DispatchLoopOptions absent at runtime by design, present in the .d.ts export list",
        "changeset": "@objectstack/core minor, @objectstack/service-queue minor, @objectstack/service-messaging patch — minor as Clause-② requires, never patch",
        "label": "needs:contract-review applied via the additive endpoint (HTTP 200), comparative read-back shows no stripping",
        "pair_exit_code": 0
      },
      "gate_denominator": "73 derived / 70 run (exit 0) / 3 NOT MEASURED / 0 UNRUN — reconciled with `dispatch-gates.mjs --ran` on final head 95070c921, exit codes recorded per family. The 3 NOT MEASURED are check:dual-build-cjs-loads, check:i18n and check:type-check-debt, each exiting 3, the code those gates use for 'prerequisite not met, nothing was measured' (all three want a whole-repo pnpm build); declared NOT MEASURED, never read as green. Re-derived AFTER merging origin/main because the merge changed dispatch-gates.mjs itself: the family count went 65 -> 73 and the 8 new ones were run.",
      "gates_i_turned_red_and_fixed": "check:engine-double-contract (new double's update() now opens with assertEngineUpdateDispatch), check:objectql-double-limit (double applies the caller's limit by PRESENCE, so limit:0 is not silently widened to the whole table), check:test-source-alias (package now aliases @objectstack/core to source). All three fixed, none routed around.",
      "pinned_ledger_delta": "scripts/engine-double-contract.pinned.json +2 rows (one delete, one update, both for the new test file): main 800 entries / 875 pinned-sum -> 802 / 877. NOTE: the dispatch's ⓘ figure of 1493 matches NONE of the readings I can take from this file — entries (800), pinned-sum (875), or the --census member count (2659 across 3687 scanned test files). Reporting the discrepancy rather than claiming the number matched.",
      "consumer_radius_sweep": "Swept by CONSTRUCTION SITE repo-wide, not by edited package. Every `new DbQueueAdapter` enumerated: all test sites (plugin-email x4, service-queue x8) pass autoStart:false and drive pollOnce() by hand, so the timer change reaches none of them; the one production construction is QueueServicePlugin, whose destroy() already awaits stop(). plugin-email initially failed with 5 file-level load errors — that was 'Failed to resolve entry for @objectstack/service-queue', a missing dist prerequisite and NOT a finding; after building the closure it is 468/468. Also checked: no name collision for the 3 new symbols in core's barrel, and the two `export * from '@objectstack/core'` re-exporters (plugin-hono-server, runtime).",
      "mcp_calls": "2 — create_pull_request, add_issue_comment. Everything else went through git, the zero-quota issue-page payload, and repo-scoped REST (probed green first).",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: the sibling engine doubles in packages/services/service-queue/src/db-queue-adapter.test.ts and job-queue-retention.test.ts apply `limit` by truthiness (`if (opts.limit)`) — the same shape check:objectql-double-limit rejected in my new file. They sit inside that gate's existing baseline so it already knows about them; a fixture asking for limit:0 would read the whole table. Carrier: the next PR to touch either file. Dedupe words: objectql-double-limit truthiness limit 0 db-queue-adapter.test.ts job-queue-retention.test.ts engine double bound.",
        "noted, not filed: DbQueueAdapter.start() now runs its first tick immediately (DispatchLoop.start() semantics) where the old setInterval waited one full interval. Strictly lower latency, no test depended on the delay. Recorded in the PR's acceptance notes. Carrier: none needed."
      ],
      "contract_deviations": "none — the repo copy of .claude/agents/os-dev.md was read from the branch first and matches the injected snapshot substantively; no action taken that it forbids."
    }

    Generated by Claude Code

  9. claude commented on Sep 14, 2026

    @claude
    Contributor

    Landed — scope item 3, and with it the whole card. PR #18134 merged 2026-09-14T06:55:15Z as 920f887dcc43cb6409d5f45c81e74a2626d29b54, enqueued 06:25:45Z. All four ## 范围 items are now delivered (1, 2 and 4 in PR #18105 8a017af41; 3 here).

    Verified on origin/main with controls, ⛔ not inferred from the merge:

    (#18134) = 1     ← the delivery
    (#18109) = 1     ← positive control, known merged
    (#18105) = 1     ← positive control, known merged
    (#99999) = 0     ← negative control
    content : packages/core/src/dispatch-loop.ts PRESENT; core/src/index.ts re-exports it
    content : db-queue-adapter.ts  maxIdleIntervalMs — 4 occurrences
    ledger  : len(entries) = 802 (was 800), $comment unchanged at 693 chars
    

    What item 3 delivered

    Direction A, and the escape hatch was declined on numbers rather than on the order naming A. DispatchLoop moved from service-messaging (where it was unexported) into @objectstack/core, is exported there, and both service-messaging's dispatchers and DbQueueAdapter.start() now run that one loop. The card's 「复用而非另写一套」 is honoured by a git rename, ⛔ not a copy.

    The cost measurement that decided it: both packages already depend on @objectstack/core, so the lift costs zero new dependency edges — one file move, one export, two import re-points. Against idle reads/hour 3601 → 124 (29×), linear in registered queue count. Latency is unchanged for work this process is told about: publish() (when due now) and replay() wake the loop; the ceiling applies only to rows another node wrote.

    ⭐ The measurement carries its own negative control, which is why 124 means what it says: the flat-poll leg is pinned FIRST at 3601 on the same clock, engine and counter — an upper bound on statement count is equally satisfied by a worker that stopped ticking. The at-tier review then ablated the loop dead and confirmed both legs fail (the control reading expected 1 to be 3601), ruling out the vacuous reading.

    Clause-② review of record

    Clause-②: yes. At-tier record 5659878791 on PR #18134, head 95070c921484eabf579456da80a0d6f7aef84446, verdict PASS, Served-tier: CONTRACT_REVIEW_TIER (the constant's NAME). Dual carrier cleared from this card and the PR in one stroke (provenance 5659888702); --pair 18134 read 0 after the strip, governed-surface 0 of 11.

    Published surface: DispatchLoop and DEFAULT_MAX_IDLE_INTERVAL_MS reachable from @objectstack/core's entry on both runtimes, DispatchLoopOptions type-only by design, plus DbQueueAdapterOptions.maxIdleIntervalMs — a new key on an already-published payload. ⚠️ The review recorded one evidence gap, ⛔ not a wrong claim: DbQueueAdapter.wake() is also a new public method on a published class and the write-up did not name it; the minor grade already covers it and @objectstack/spec is untouched.

    ⓘ The docs-drift flags on automation/jobs.mdx, automation/webhooks.mdx and protocol/kernel/lifecycle.mdx were homonyms — none names DispatchLoop; they are the job-schedule trigger field, PluginHealthMonitor, and HttpDispatcher (which still lives in service-messaging). ⛔ No page was inaccurate and no doc edit was owed.

    ⓘ Three gates the implementer turned red were fixed, not routed around: check:engine-double-contract, check:objectql-double-limit, check:test-source-alias — all baselines byte-identical to main, confirmed independently by the review.

    ⓘ Carried forward, non-blocking: subscribe() does not wake(), so a queue registered after the loop has backed off sees already-persisted rows up to maxIdleIntervalMs late rather than ≤ 1 s. That sits inside the documented latency class. Also noted by the implementer: the sibling engine doubles in db-queue-adapter.test.ts and job-queue-retention.test.ts apply limit by truthiness, so a fixture asking for limit: 0 would read the whole table — inside check:objectql-double-limit's existing baseline; carrier is the next PR to touch either file.

    ⚠️ This seat is going off shift on the maintainer's ruling 「当前任务处理完就下班」. This was its last item.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions