Skip to content

fix(objectql): defer the first lifecycle sweep one interval on a datastore this boot created - #30214

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-u71k2j
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-u71k2j

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

维护者速读

  • 改了什么:全新建好的数据库,开机 60 秒那次生命周期清扫(保留期读取、表计数、悬空引用审计)不再跑,推迟一个周期(默认一小时)再扫;已有数据的库照旧 60 秒首扫。
  • 为什么改:新库里没有任何一行早于这次开机,最短保留窗口是一小时,首扫注定什么都删不掉,却在 cloud 托管内核上花掉第一分钟 160 条语句里的 130 条。
  • 风险与回滚:只改调度器的首次触发,直接调用 sweep() 照扫;最坏情况是新库的过期行晚一个周期被清,回滚即还原 lifecycle-service.ts 一处判断。
  • 先看:packages/objectql/src/lifecycle/lifecycle-service.ts 的 start() 与 firstSweepDueAtInitialDelay();再看同目录测试文件末尾 LifecycleService timers 里的四个 objectql: the lifecycle sweep runs 60 s after every kernel boot, even on a database created seconds earlier, so a fresh tenant kernel pays 130 statements in its first minute #29918 用例。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/lifecycle/lifecycle-service.test.ts -t 29918,预期 4 个用例全绿。
  • 需要你做的:无,席位落地。卡的条目 1(持久化最后清扫时间,让重启也省掉首扫)另待裁定,本 PR 不关卡。

Refs #29918 (item 2). Item 1, the persisted last-swept watermark, is not in this PR and waits on a decision (report 6646684881 on the card), so this PR does not close the card.

What changes

  • LifecycleService.start() now asks, when the initial delay fires, whether the first sweep is due yet (firstSweepDueAtInitialDelay()). It is not due when the engine reports that this process created its datastore from empty: ObjectQL.wasDatastoreCreatedFromEmpty(), which answers from every driver's schema-sync stats, in process and without issuing a statement. In that case the scheduled first sweep is skipped, and the interval is armed as before, so the first sweep runs one interval later.
  • LifecycleEngineLike gains the optional member wasDatastoreCreatedFromEmpty?(): boolean. The real engine already has it. An engine that omits it, or whose answer throws, is treated as not fresh and keeps the old schedule.
  • The gate sits on the scheduler's first tick, not at the entry of sweep(). Direct sweep() callers (the dogfood growth gate, db:clean-style tooling, and more than a dozen suites in objectql, service-knowledge, service-queue and service-messaging that call it on purpose) keep sweeping. The reference audit is a leg of sweep() with no timer of its own (auditReferences(); its only other caller is the CLI's orphan-detail sweep), so it is deferred with the same tick.
  • OS_LIFECYCLE_DISABLED, enabled: false, initialDelayMs and sweepIntervalMs keep their meaning. One changeset, patch on @objectstack/objectql.

Why

A fresh store holds only rows this process wrote, so none is older than the initial delay (60 s), and the duration grammar's smallest window is one hour (parseLifecycleDuration). The first sweep on such a store therefore cannot reap anything. The card measured it on cloud's hosted tenant composition as 130 statements in the 50 to 60 s slice of a first boot (17 retention reads, 16 counts, the reference audit and the governance settings reads). Deferring it costs at most one interval of overstay, which the hourly cadence already allows any row.

Tests

Four cases added to the existing LifecycleService timers describe in lifecycle-service.test.ts. Each counts every data-plane door the sweep reaches: reaps, governance settings reads and the reference audit.

  • Fresh store: nothing at the initial delay (0 deletes, 0 audits, 0 settings reads), then one sweep one interval later.
  • Store that existed before the boot, or an engine whose answer throws: sweeps at the initial delay as before.
  • Direct sweep() on a fresh store: still sweeps.
  • OS_LIFECYCLE_DISABLED=1 on a fresh store: nothing is armed.

Acceptance notes

  • Card "Done when", clause 1 (no lifecycle-sweep statements in a fresh kernel's first minute): met for the measured first-boot reading. With this change no statement is issued at all, not even a watermark read. cloud re-measures under cloud#3413.
  • Clause 2 (a kernel whose watermark is older than the interval still sweeps) belongs to item 1 and is not met here. A re-boot on an existing store still sweeps at the initial delay. Item 1 has no existing persistence seam that needs neither a new config key nor a new sys_* object, so the seat sends it to a decision.
  • Deviation from the seat's note: the seat asked for the gate at the entry of sweep(). It sits on the scheduler's first tick instead, because a gate at the entry would stop every direct sweep() caller from sweeping on a fresh store. The reference audit is still deferred, since it only runs inside sweep().

Verification

All readings on head 07cef192 (after one merge of origin/main), in the worktree, with the dependency closure built.

  • pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/lifecycle/: Test Files 5 passed (5), Tests 135 passed (135).
  • pnpm --filter @objectstack/objectql test: Test Files 548 passed (548), Tests 10867 passed (10867), lock VERDICT command-exit 0.
  • pnpm --filter @objectstack/objectql typecheck (tsc --noEmit and check:test-typecheck): VERDICT command-exit 0.
  • pnpm lint: exit 0.
  • Derived gate list (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): 59 families, 58 run and exit 0, 1 NOT MEASURED. pnpm check:dual-build-cjs-loads exits 3 (PREREQUISITE NOT MET: it needs the whole workspace built) and is left to CI. check-plugin-teardown-shape.mjs --self-test exited 3 on the shallow clone, then exit 0 (48 cases) after fetching the commit its fixture reads. --ran reconciliation: "59 derived famil(ies) accounted for — 58 run, 1 NOT-MEASURED".
Ablation of the freshness gate

Committed fix first (027c6689), then node scripts/ablation-replace.mjs replaced if (this.firstSweepDueAtInitialDelay()) void this.sweep(); with an unconditional void this.sweep(); plus a marker comment (anchor 1 to 0, blob 026c59ebf9a8 to f36281d0bba1) and ran vitest run src/lifecycle/lifecycle-service.test.ts -t 29918:

× [#29918] issues no sweep at the initial delay on a store this boot created, and sweeps one interval later
Tests  1 failed | 3 passed | 114 skipped (118)

Restore: blob after restore 026c59ebf9a8 equals HEAD, git diff HEAD empty. The other three cases stay green under the mutation, as they should: they pin the unchanged paths. The suite imports the service from source by relative path, so no build sits between the mutation and the run.

Clause-②: yes (widening) — the exported LifecycleEngineLike gains the optional member wasDatastoreCreatedFromEmpty?(): boolean (an engine without it keeps the old schedule); no accept set changes.
Metadata-protocol: none

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 4 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via LifecycleService (symbol, a top-level class))
  • content/docs/data-modeling/objects.mdx (via LifecycleService (symbol, a top-level class))
  • content/docs/kernel/services.mdx (via LifecycleService (symbol, a top-level class))
  • content/docs/permissions/attachments-access.mdx (via LifecycleService (symbol, a top-level class))
  • content/docs/protocol/knowledge.mdx (via LifecycleService (symbol, a top-level class))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via LifecycleService (symbol, a top-level class))
  • content/docs/releases/v15.mdx (via LifecycleService (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 21 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f2a9007e93f899f8f1a114ed3de7729bd681741a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from eb7e83478fdbc4a15af71cd74b2c5cf3ba8c894e — the merge of head 168b9ded00f5b061cd4a2420fa844e7cc19fec5a into base f2a9007e93f899f8f1a114ed3de7729bd681741a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin eb7e83478fdbc4a15af71cd74b2c5cf3ba8c894e && git checkout eb7e83478fdbc4a15af71cd74b2c5cf3ba8c894e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f2a9007e93f899f8f1a114ed3de7729bd681741a 168b9ded00f5b061cd4a2420fa844e7cc19fec5a && git checkout -B drift-repro f2a9007e93f899f8f1a114ed3de7729bd681741a && git merge --no-ff 168b9ded00f5b061cd4a2420fa844e7cc19fec5a

node scripts/docs-audit/affected-docs.mjs --json f2a9007e93f899f8f1a114ed3de7729bd681741a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f2a9007e93f899f8f1a114ed3de7729bd681741a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…a Clause-② widening

Claude-Session: https://claude.ai/code/session_01Utp6Ca4cwEoenneeTMpCvy
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

维护者速读

  • 改了什么:全新建好的数据库第一次开机,不再在 60 秒时跑生命周期清扫(保留期读取、表计数、悬空引用审计),推迟一个周期(默认一小时);已有数据的库照旧。
  • 为什么改:新库里没有任何一行早于本次开机,首扫注定什么都清不掉,却占掉 cloud 托管内核第一分钟 160 条语句里的 130 条。
  • 风险与回滚:只改调度器的首次触发,直接调用 sweep() 照扫;最坏是新库的过期行晚一个周期被清;回滚即还原一处判断。
  • 先看:packages/objectql/src/lifecycle/lifecycle-service.ts 的 firstSweepDueAtInitialDelay()。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/lifecycle/lifecycle-service.test.ts -t 29918,4 个用例全绿。
  • 需要你做的:无,席位落地。卡的条目 1(持久化清扫水位线)另待裁定,本 PR 不关卡。

席位意见:ACCEPT。小而准,测试数到每一扇数据门,消融做了。

席位核对

Head-sha: 168b9ded00f5b061cd4a2420fa844e7cc19fec5a · read 2026-11-05T02:45Z

Item Reading
Shape Draft → main, author objectstack-fleet[bot] (relay pr_create); line 1 attribution, Refs #29918 (item 2), no closing keyword anywhere in the body (scanned). 3 files, +161/−2.
Premise measured On origin/main: DEFAULT_LIFECYCLE_INITIAL_DELAY_MS = 60_000, no watermark under packages/objectql/src/lifecycle/; ObjectQL.wasDatastoreCreatedFromEmpty() exists (engine-migration-flags.ts:281). The dev's report 6647721564 and the diff agree.
Gates (check-runs on this head) 37 success, 15 skipped, 0 failure, 0 pending; Lint & Repo Gates, TypeScript Type Check, Check Changeset success. Dev's local --ran: 58/59 exit 0, check:dual-build-cjs-loads exit 3 (needs full build; NOT MEASURED locally, CI carries it).
Accepted deviation Gate on the scheduler's first tick instead of sweep() entry: sweep() has direct callers (dogfood growth gate, db:clean tooling, suites in objectql/knowledge/queue/messaging) that must keep sweeping; the reference audit runs only inside sweep(), so it defers with the tick. Pinned by the direct-sweep() case.
Clause-② yes (widening): the exported LifecycleEngineLike (@objectstack/objectql entry, index.ts:601) gains the optional wasDatastoreCreatedFromEmpty?(): boolean; no accept set changes. Precedent PR #30015. Declared in claim 6646620870, the changeset and the PR body. Seat's own review of record at the default tier: PASS (optional input member, absent ⇒ old schedule, pinned).
Out of scope / Skills-docs Item 1 (persisted watermark) → decision brief, card stays open. Metadata-protocol: none ⇒ no Skills-docs: line owed. Docs Drift Check lists pages naming LifecycleService; none states the first-sweep timing for a fresh store (read kernel/services.mdx mention), so no doc change.
Landing Not governed (check-governed-merges --pr 30214: 0 of 3 paths), 163 lines ≤ 3000. Seat lands: pr_ready + automerge_enable. After merge: Release: on #29918 naming item 2 landed and item 1 as the remainder, into the decision state.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 02:47
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 02:47
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 314ad95 Nov 5, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-u71k2j branch November 5, 2026 03:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant