Skip to content

feat(objectql): the lifecycle sweep keeps a last-swept watermark in sys_lifecycle_state, so a reboot skips a sweep the interval has not come due for - #30751

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

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

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#1 · os-elon-musk · session_016WV4cxAY2io7s5aVJa6YL4

Fixes #29918
Fixes #30641

Clause-②: yes (widening) — PLATFORM_OBJECTS_BY_PACKAGE['platform-objects'] (and with it PLATFORM_PROVIDED_OBJECT_NAMES / isPlatformProvidedObjectName) lists one more real platform object, sys_lifecycle_state, and the exported LifecycleEngineLike gains three optional members (findOne, insert, update); nothing that was accepted is refused.
Metadata-protocol: added
Skills-docs: none — skills/** and content/docs/** enumerate no platform-object roster and teach nothing about the lifecycle sweep's schedule; the object is internal clock state with no data API, so there is nothing for an author to write against it.

Claim: 6672243354 (amended 6672382820) · Branch: claude/project-thread-ie3sda · Ruling-ref: 6663872259 (letter B), confirmed 6670551310 · Claim correction: 6670586478 · Spec consent: 6669304514, 6670346930 · Resumes: release 6671108523 (claude/project-thread-0jjbbb@4a9c5296)

维护者速读

  • 改了什么:objectql 生命周期清扫记住「上次扫完的时间」,存在新的系统状态对象 sys_lifecycle_state(引擎自有、无租户列、每个数据库一行)。内核启动 60 秒时读这一行:距上次扫完不到一小时就不扫,只花这一条读;超过一小时、没有这一行、读失败,照旧扫。全新建库那一轮仍按 fix(objectql): defer the first lifecycle sweep one interval on a datastore this boot created #30214 延后一个间隔,并顺手写下这一行。spec 名册加一行 sys_lifecycle_state,与对象同一提交。
  • 为什么改:每次内核重启都会在第一分钟跑满一遍清扫(cloud 托管内核上 130 条语句),cloud#3413 要求第一分钟不超过约 10 条。裁决 B(6663872259,确认 6670551310):水位线是引擎状态,不是设置。
  • 风险与回滚:小。最坏情况是水位线写不进去,下次启动多扫一遍(等于今天的行为),写失败只记一次 warn。新增一个系统表(无数据 API、无设置键、无环境变量、不进 Setup)。回滚即撤 PR,多出的空表无害。
  • 先看:packages/objectql/src/lifecycle/lifecycle-service.ts(runFirstScheduledSweep、watermarkIsYoung、writeWatermark,以及 sweep() 末尾只在完整成功时推进水位线),packages/platform-objects/src/system/sys-lifecycle-state.object.ts,packages/spec/src/system/constants/platform-object-names.ts 那一行。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/lifecycle/lifecycle-service.test.ts -t "sweep watermark",预期 9 条全绿,其中「younger than the interval」断言初始延迟时只发出一次读。
  • 需要你做的:无,席位落地。

What changes

@objectstack/objectql — LifecycleService (lifecycle/lifecycle-service.ts)

  • New exported constants LIFECYCLE_STATE_OBJECT = 'sys_lifecycle_state' and LIFECYCLE_STATE_ROW_ID = 'lifecycle'.
  • LifecycleEngineLike gains optional findOne / insert / update. An engine missing any of the three, or whose registry does not register sys_lifecycle_state, keeps no watermark and sweeps exactly as before; the object is never read when unregistered.
  • The scheduler's first tick (runFirstScheduledSweep):
    1. a store this boot created from empty (fix(objectql): defer the first lifecycle sweep one interval on a datastore this boot created #30214's wasDatastoreCreatedFromEmpty) still defers one interval, and now seeds the row at now without reading it;
    2. otherwise one primary-key findOne of the row: a last_swept_at younger than the interval ⇒ no sweep, the read is the only statement; the interval is armed either way;
    3. an older, missing, unparsable or future watermark, or a read that throws (logged once at warn) ⇒ sweep at the initial delay, as before.
  • sweep() advances the row (insert the first time, update after; the row's presence is remembered so no second read) only for a COMPLETED sweep: no policy error, the reference-audit leg ran (or was never meant to), and no teardown cut it short. Stamped with the sweep's start, which the cutoffs were taken from. A failed write is a functional degradation (the next boot sweeps again), logged at warn once and debug after, never thrown.
  • A direct sweep() call always sweeps. OS_LIFECYCLE_DISABLED=1 still arms nothing.

@objectstack/platform-objects — new sys_lifecycle_state (system/sys-lifecycle-state.object.ts): isSystem, managedBy: 'engine-owned', systemFields: { tenant: false } (ADR-0131 D7 deployment-level state), requiredPermissions: ['manage_platform_settings'], apiEnabled: false, no lifecycle block (never swept), fields id, last_swept_at, created_at, updated_at. Registered by PlatformObjectsPlugin beside sys_migration and exported from system/index.ts.

@objectstack/spec — the one roster line 'sys_lifecycle_state' in PLATFORM_OBJECTS_BY_PACKAGE['platform-objects'] (sorted between sys_jwks and sys_member), in the same commit as the object, per domain:spec#3's consent (6669304514, 6670346930): the roster test holds the roster to the declared *.object.ts set both ways, so #30641 cannot land alone. Nothing else under packages/spec moves.

@objectstack/cli — ORGANIZATION_OWNERSHIP_INVENTORY gains the sys_lifecycle_state row (deployment-level, column-drop).

scripts/platform-object-tenancy-census.json — the new row (reach: out, systemFields.tenant: false) and the totals (registered 94, out of reach 47).

Changeset — minor for spec / objectql / platform-objects / cli / plugin-audit, carrying both declaration lines and the Skills-docs: line.

Tests

  • packages/objectql/src/lifecycle/lifecycle-service.test.ts, new describe "LifecycleService sweep watermark (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)", 9 pins: a younger watermark issues only the read; an interval-old watermark sweeps and advances; no row sweeps and writes it; a created-from-empty store defers, seeds without reading, and sweeps one interval later; a failed policy or a throwing audit leaves the row unchanged; a failing read sweeps and warns; an engine without the object is never asked; a direct sweep() sweeps and advances; OS_LIFECYCLE_DISABLED reads and writes nothing.
  • packages/platform-objects: plugin.test.ts (registration order, six infra objects, no routing triple), deployment-level-no-tenant-column.test.ts (no tenant column, platform-only reads).
  • Roster (platform-object-names.test.ts) and CLI inventory (organization-ownership-inventory.test.ts) unchanged and green with the new rows.

Acceptance notes

  • The carried head is one commit. The released branch held the object (28a633a3) and the roster line (2de76a73) in two commits; the dispatch order requires them in one because the roster test holds both directions, so this branch squashes the released work onto current main (6ef7f583). The released branch is untouched.
  • Docblock reattached. On the released head, runFirstScheduledSweep's docblock had been inserted between firstSweepDueAtInitialDelay's docblock and its method, leaving that method undocumented and two docblocks stacked; this branch moves the new methods above it. No code moved.
  • Skills-docs: spelling. The release and the dispatch order spelled the value no move; the declared grammar (pm-dispatch SKILL.md) is updated here | follow-up owed — FILES | follow-up #N | none — REASON, so the changeset and this body say none — REASON with the same reason.
  • A fresh store now issues one write at the initial delay (the seed insert), not zero. The card's done-when allows "at most the watermark read"; the seed is the price of making a reboot inside the first interval skip too (card Expected item 2). One statement against the 130 measured.
  • plugin-audit hunk (amendment 6673909090). The first head's watermark writes were audited like any business write, so every completed sweep added a sys_audit_log and a sys_activity row about its own clock, which the next sweep archived (CI: the audit-key-material / audit-mail-bodies archive integration tests counted 2 archived rows where 1 is right). Ruling 6663872259 forbids an audit row per sweep and ADR-0057 section 3.3 forbids cleanup re-feeding the tables it drains. No write option bypasses the code-registered audit hooks and there is no object-level opt-out, so sys_lifecycle_state joins SKIP_OBJECTS beside sys_job_queue; audit-writers.test.ts pins zero rows for its insert and update, and the changeset gains '@objectstack/plugin-audit': minor.

Verification

Rework head 29c59f4d: the rework report 6674667474 on #29918 records 96/96 derived gates and pnpm lint exit 0 (0 NOT-MEASURED), full dogfood 421 files / 3513 tests green, both cli archive integration tests 16/16, and plugin-audit 1146/1146. The block below is the first head's.

All on head f9f4c1b6 (base main 6ef7f583), after a whole-tree turbo run build (119/119, lock VERDICT command-exit 0).

Targeted suites and typecheck — all exit 0
  • pnpm --filter @objectstack/objectql test: Test Files 556 passed (556), Tests 11078 passed (11078)
  • pnpm --filter @objectstack/objectql typecheck: exit 0
  • pnpm --filter @objectstack/platform-objects test: Test Files 116 passed (116), Tests 1581 passed (1581); typecheck exit 0
  • pnpm --filter @objectstack/spec exec vitest run src/system/constants/platform-object-names.test.ts: 14 passed (14)
  • pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date
  • pnpm --filter @objectstack/cli exec vitest run --project unit src/utils/organization-ownership-inventory.test.ts: 26 passed (26); cli typecheck exit 0
  • Lock VERDICT command-exit 0.
Ablations — both gates turn their pin red; restore proven against HEAD

Through scripts/ablation-replace.mjs, vitest run src/lifecycle/lifecycle-service.test.ts -t "sweep watermark" (the suite imports the service from source, so no build sits between mutation and run):

  • Read gate: the line in watermarkIsYoung that returns false for an unparsable, future or interval-old watermark (the Number.isFinite / intervalMs check) replaced by return Boolean(0); (anchor 1 to 0, blob ba3f2b02 to 2f1db1a3): 1 failed, 8 passed, the failure being "a reboot whose watermark is younger than the interval issues only the watermark read at the initial delay". A first attempt with the replacement return false; was refused by the tool as a no-op (the replacement is a substring of the anchor, count 7 to 7) and measured nothing.
  • Write gate: if (audited && report.errors.length === 0 && !this.stopped) { replaced by if (true) { (blob ba3f2b02 to 7bd5ebd0): 1 failed, 8 passed, the failure being "a sweep that fails a policy, or whose audit throws, leaves the watermark unchanged".
  • Each restore: blob equals HEAD ba3f2b02, git diff HEAD empty.
Derived gates — 94 of 94 run, all exit 0, plus pnpm lint

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 94 families on f9f4c1b6; each ran sequentially, exit code captured before any pipe, plus pnpm lint (exit 0). --ran: "94 derived famil(ies) accounted for — 94 run, 0 NOT-MEASURED"; pnpm lint listed as outside the derivation.

Full dogfood — green

pnpm --filter @objectstack/dogfood test under the verify lock on f9f4c1b6: Test Files 421 passed, 3 skipped (424); Tests 3513 passed, 36 skipped (3549); VERDICT command-exit 0.


Generated by Claude Code

…ys_lifecycle_state

A kernel that reboots onto an existing datastore reads one row at the
initial delay and skips a first sweep the interval has not come due for.
The watermark lives in a new engine-owned, tenant-less object,
sys_lifecycle_state, declared and registered by platform-objects, with
its roster line, census row and ownership-inventory row in this same
commit because the roster test holds both directions.

Carries the released branch claude/project-thread-0jjbbb@4a9c5296
(session_01Utp6CaGhTo68T8rcfBh8Zv) onto current main as one commit, and
reattaches the docblock of firstSweepDueAtInitialDelay to its method.

Claude-Session: https://claude.ai/code/session_016WV4cxAY2io7s5aVJa6YL4
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:system tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/objectql, @objectstack/platform-objects, @objectstack/plugin-audit, @objectstack/spec, touching 29 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/platform-objects/src/system/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

80 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json b3098661cd3ad78634dd77a31ae68530ab9a3a83.

⛔ 18 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/platform-objects/src/system/index.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: isSystem (5 routes)
  • 13 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 221 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 — 182 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 b3098661cd3ad78634dd77a31ae68530ab9a3a83 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b59a30c2874734fc9b1c4e536b0a1348a3d70e10 — the merge of head 29c59f4d04e02b2953856d14c1bb46668715f371 into base b3098661cd3ad78634dd77a31ae68530ab9a3a83, 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 b59a30c2874734fc9b1c4e536b0a1348a3d70e10 && git checkout b59a30c2874734fc9b1c4e536b0a1348a3d70e10
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b3098661cd3ad78634dd77a31ae68530ab9a3a83 29c59f4d04e02b2953856d14c1bb46668715f371 && git checkout -B drift-repro b3098661cd3ad78634dd77a31ae68530ab9a3a83 && git merge --no-ff 29c59f4d04e02b2953856d14c1bb46668715f371

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

⚠️ 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 b3098661cd3ad78634dd77a31ae68530ab9a3a83 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract-review-claim: session_016WV4cytLmJxfAmgPVjUbbW · head f9f4c1b · 2026-11-06T00:41Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f9f4c1b60342b2da2ab079f88d55b42f76943507
Local-runs: none
Scope: packages/spec/src/system/constants/platform-object-names.ts + packages/spec/src/system/constants/platform-object-names.test.ts, packages/platform-objects/src/system/sys-lifecycle-state.object.ts, packages/platform-objects/src/plugin.ts, packages/platform-objects/src/system/index.ts, packages/cli/src/utils/organization-ownership-inventory.ts, scripts/platform-object-tenancy-census.json, packages/objectql/src/lifecycle/lifecycle-service.ts, packages/objectql/src/lifecycle/lifecycle-service.test.ts (widened to the whole non-prose diff; the cited engine code on main — engine.ts findOne/insert/update, engine-validate.ts system-context strip bypass, engine-migration-flags.ts wasDatastoreCreatedFromEmpty, migration-flag.ts write precedent — was read, not reviewed)

Governing text: ruling B 6663872259 (confirmed 6670551310); spec consent 6669304514 / 6670346930; claim 6672243354 + amendment 6672382820; dev report 6673429255. Read-only shape held: diff at the head, both cards and every comment, the head's check-runs. PR user.login is objectstack-fleet[bot].

① Derived judgments

  1. Roster line (the schema face). PLATFORM_OBJECTS_BY_PACKAGE['platform-objects'] gains 'sys_lifecycle_state' in sorted position between 'sys_jwks' and 'sys_member'; with it PLATFORM_PROVIDED_OBJECT_NAMES and isPlatformProvidedObjectName answer true for one more real object. A reference to that name now resolves instead of drawing the unknown-platform-name advisory; nothing accepted before is refused. Right: widening, as declared. Nothing else under packages/spec moves (file list and diff confirm: one hunk, one line). The roster test on main holds the roster to the declared *.object.ts set both ways, and the declaration is in this diff, so the pair is consistent.
  2. Same commit as the object. Verified on the commit API, not the body: 04cab785 holds all 11 files (object, registration, index export, roster line, inventory row, census row, service, tests, changeset); f9f4c1b6 touches only the changeset's Skills-docs: word. Right, per the consent's "same commit" term.
  3. The new object is what ruling B says. sys-lifecycle-state.object.ts: isSystem: true, managedBy: 'engine-owned', systemFields: { tenant: false } (ADR-0131 D7), requiredPermissions: ['manage_platform_settings'], apiEnabled: false, no lifecycle block, fields id / last_swept_at / created_at / updated_at; declared in packages/platform-objects/src/system/ beside sys-migration.object.ts and registered in the same PlatformObjectsPlugin manifest ([SysMigration, SysMigrationJournal, SysSecret, SysExchangeRate, SysTrustedKey, SysLifecycleState], no routing triple). Compared field-for-field with sys_migration on main: same posture. No settings key, no env override, no audit row, no Setup nav. Right: engine-owned, tenant-less, beside sys_migration.
  4. @objectstack/platform-objects public surface. SysLifecycleState is exported from system/index.ts, which the package root re-exports with export *. One new named export; nothing removed. Widening, covered by the minor.
  5. @objectstack/objectql public surface. LifecycleEngineLike (exported from the package root on main) gains three OPTIONAL members findOne / insert / update; an engine missing any keeps the old schedule. Widening, as declared. LIFECYCLE_STATE_OBJECT / LIFECYCLE_STATE_ROW_ID are module-level exports of lifecycle-service.ts and are NOT re-exported from src/index.ts, so they are not in the published accept set (the body's "new exported constants" overstates them; harmless).
  6. Sweep scheduling honours the watermark. Read against the live engine on main:
    • runFirstScheduledSweep: fresh store (wasDatastoreCreatedFromEmpty, which on main requires zero existing tables and at least one created, so an UPGRADE boot onto an existing store that merely gains the new table is NOT fresh) ⇒ defer one interval and seed the row at now, no read. Otherwise one findOne by primary key under SYSTEM_CTX; young ⇒ no sweep, the read is the only statement; old, absent, unparsable, negative age (clock moved back), unregistered object, missing engine verbs, or a throwing read (warn once) ⇒ sweep at the initial delay, exactly as before. The interval is armed in start() either way. Right.
    • First boot / cold start on an existing store with no row: sweep, then insert. Right.
    • Multi-instance: two kernels on one store both read; a kernel that boots after another's completed sweep skips inside the interval; two that both find it old both sweep (idempotent reaps) and both write. A concurrent first boot of two kernels onto one empty store makes the second seed insert fail on the primary key: logged once at warn, presence reset to unknown, the next write re-reads. Degradation only. Acceptable.
    • Clock skew across instances: a future stamp reads as negative age ⇒ sweep; an ahead-clock instance reading a correct stamp sees it older ⇒ sweep. Both directions fail toward sweeping. Right.
    • Failed watermark write: never thrown, warn once then debug, presence reset; the next boot sweeps again (today's behaviour). Right, and the ruling's "the row can only ever save work, never withhold it" holds.
    • The write advances only a COMPLETED sweep: audited && report.errors.length === 0 && !this.stopped, stamped with report.at (the sweep's start, which the cutoffs were taken from). Early returns in sweep() (re-entrant, disabled, torn down, no engine, governance snapshot failed, governance disabled, leg-boundary stop) all precede the write. auditReferences now returns true when it ran or was never meant to, false on throw or teardown. Right.
    • Engine contract: findOne(object, { where: { id }, context }), update(object, { id, last_swept_at }, { context }) (the engine extracts the id from data), insert(object, row, { context }) — the same shapes migration-flag.ts uses on main for sys_migration; isSystem: true bypasses the static readonly strip (engine-validate.ts), so the readonly: true, required: true last_swept_at is written as sys_migration.last_run_at is. Right.
    • Direct sweep() always sweeps (one presence read on first write, documented); OS_LIFECYCLE_DISABLED=1 arms nothing. Right.
  7. Nothing narrows. No accepted input is refused; no default moves; the fresh-store deferral of fix(objectql): defer the first lifecycle sweep one interval on a datastore this boot created #30214 is kept; a kernel without the object or the verbs behaves as on main.
  8. Census / inventory treatment of a tenant-less object. Census row reach: out, tenantField: null, reason systemFields.tenant: false; totals 93→94, outOfReach 46→47, the reason bucket 20→21, inReach unchanged. Inventory row census: PMAIN, fate: 'column-drop', citing CENSUS_MAIN and D7 — the same shape as sys_trusted_key's row directly above it. deployment-level-no-tenant-column.test.ts adds the object to the D7 table (no injected or declared organization_id, opt-out via systemFields.tenant, platform-only capability). check:platform-object-tenancy-census ran in the green "Lint · repo & tooling gates". Right.

② Semver level

  • Changeset .changeset/29918-lifecycle-sweep-watermark.md: minor for @objectstack/spec, @objectstack/objectql, @objectstack/platform-objects, @objectstack/cli. No major (pre-GA rule of check-changeset-no-major.mjs). Level axis: Metadata-protocol: added requires at least one package whose published source moves at minor or above — spec and platform-objects both do; added sits beside Clause-②: yes (widening), no contradiction. The Check Changeset run on this head concluded success.
  • Declaration lines are identical in the PR body and the changeset: Clause-②: yes (widening) — …, Metadata-protocol: added, Skills-docs: none — REASON. Fixes #29918 and Fixes #30641 are both in the body. Every consent term of 6669304514 / 6670346930 is met.
  • Skills-docs spelling. The declared grammar (pm-dispatch SKILL.md, the Metadata-protocol row) is updated here | follow-up owed — FILES | follow-up #N | none — REASON. no move (the order's and the release's word) is not in it. No script reads the line (the only Skills-docs hit under scripts/ and .github/ is a comment in check-skills-token-ratchet.mjs), so the gates are indifferent; the dev's none — REASON is the grammar the seat's ACCEPT checks against, and the reason given (no roster enumerated in skills/** or content/docs/**; no data API) matches the diff. Right.

③ Boundary flags

open_questions: []. The dev's four deviations and one acceptance note, each answered:

  1. Squash of the released branch into one commit — verified (①.2); the released branch claude/project-thread-0jjbbb@4a9c5296 compares to the same 11 files against 6ef7f583, so the squash dropped nothing. The release note's "regenerated translation bundles" (6671108523) described files that were never on that branch; none are needed (see ④.4). Accepted.
  2. Skills-docs: none — REASON instead of no move — the right grammar (②). Accepted.
  3. firstSweepDueAtInitialDelay docblock reattached, no code moved — confirmed in the diff (the new methods sit above it). Accepted.
  4. Assignee / reviewer not set (no MCP write) — the seat's stroke at hand-over. Noted.
  5. Acceptance note: a fresh store now issues ONE write (the seed insert) at the initial delay, where the card's done-when says "at most the watermark read". The seed is card Expected item 2 ("seed the watermark when the tables are created") placed at the initial delay, one statement against the 130 measured, and it is what makes a reboot inside the first interval skip too. Ruling B's own done-when (a re-booted kernel with a young watermark issues only the read; an older one still sweeps) is met exactly. Accepted as a declared deviation; the seat names it at ACCEPT and cloud re-measures under cloud#3413.

Author note: PR user.login is objectstack-fleet[bot]; no outside author.

④ Design judgment

  1. Roster line against its first consumer. platform-object-names.test.ts on main scans every *.object.ts and asserts equality both ways per package; the declaration and the line land together, so the test is green only with both — the consent's reason for riding one PR holds in the tree. The inventory test (organization-ownership-inventory.test.ts, "a platform object without a row reds here, by name") is satisfied by the new inventory row. No generated spec artifact derives from the roster (nothing under packages/spec/scripts, packages/spec/generated or scripts/ reads PLATFORM_OBJECTS_BY_PACKAGE), so check:generated has nothing to drift. Holds.
  2. Object against the ruling and its first consumer. The sole reader and writer is LifecycleService, which asks registry.getObject before any statement, so the object is never read where it is not registered. Single-row shape per the ruling of record (the keyed id column leaves room for a second clock without a schema change). No lifecycle block, so the row that records the sweep is never reaped by it. Holds.
  3. Pins are real. Read against the gates they pin: the "younger watermark issues only the read" pin asserts statements() === 1 and deletes === 0, which the read-gate ablation (watermarkIsYoung forced false) turns red by sweeping; the "failed policy or throwing audit leaves the row unchanged" pin asserts zero inserts/updates on both paths, which the write-gate ablation (if (true)) turns red. The dev reports both ablations 1 failed / 8 passed with restore proven against HEAD (ba3f2b02), and the first read-gate attempt refused as a no-op is recorded honestly. The remaining pins (no row ⇒ sweep + insert; fresh ⇒ seed without read, sweep one interval later; failing read ⇒ sweep + one warn; unregistered ⇒ never asked; direct sweep ⇒ sweeps and advances; disabled ⇒ zero statements) each name a distinct branch of runFirstScheduledSweep / writeWatermark. Holds.
  4. Dogfood cell and derived gates. "Dogfood Regression Gate", "Governed Surface Queue Guard", "Check Changeset", "PR-body guards", "Lint · repo & tooling gates" (census check), "Lint · PM dispatch-gates", "Lint · ESLint", "Spec property liveness", "Type Check · source gates" all success on this head. Still in_progress at review time: Build Core, Test Core 1–8, Type Check · workspace / debt ledger / consumer gates (the last runs check:i18n and the coverage ratchet), Lint · CI release & engine gates, Lint · scoped slot lookup, the UI jobs. On check:i18n: i18n-extract.config.ts imports an explicit object list that already omits sys_trusted_key and sys_migration_journal, so the new object produces no bundle drift and is outside the ratchet's population; it has no UI (apiEnabled: false, no nav), so untranslated labels reach no user. The dev reports full dogfood 421 files / 3513 tests green under the verify lock and 94/94 derived gates exit 0. Landing precondition ② (every check green) is the dispatching seat's to confirm once the in-progress runs conclude; nothing read here predicts a red.
  5. Tried to break it. Watermark older than the interval but a sweep aborted by stop() mid-leg: no write, next boot sweeps — correct. Row present with last_swept_at as a driver Date object: String(date) parses; as an epoch number it parses to NaN ⇒ sweep (conservative). sweepIntervalMs shortened between boots: compared against the new interval — correct. Governance enabled: false via settings: sweep() returns before the write, so the watermark does not advance while sweeping is switched off — correct (a later re-enable sweeps at once). A kernel that boots inside the interval repeatedly (cloud's 30-minute residency against the hourly interval): each boot reads one row and skips until the last completed sweep is an interval old, then sweeps at the initial delay — the "brief, frequent tenant is never swept" failure the card warned about does not occur. No must-fix found.

Should-fix (non-blocking, for the seat's ACCEPT note or a follow-up): (a) system/index.ts's header docblock still lists what the module hosts without the new object — prose only; (b) the body's "new exported constants" are module exports, not package exports — wording only.

Implemented-by: claude/project-thread-ie3sda
Reviewed-by: session_016WV4cytLmJxfAmgPVjUbbW

VERDICT: PASS

… audit ledger

Every completed sweep wrote its sys_lifecycle_state row through the
generic audit writers, so each sweep fed sys_audit_log and sys_activity a
row the next sweep archives. The archive integration tests of
os migrate audit-key-material / audit-mail-bodies counted it as a second
archived row.

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

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 29c59f4d04e02b2953856d14c1bb46668715f371
Local-runs: none
Scope: packages/spec/src/system/constants/platform-object-names.ts + packages/plugins/plugin-audit/src/audit-writers.ts, packages/plugins/plugin-audit/src/audit-writers.test.ts, packages/platform-objects/src/system/sys-lifecycle-state.object.ts, packages/platform-objects/src/plugin.ts, packages/platform-objects/src/system/index.ts, packages/platform-objects/src/plugin.test.ts, packages/platform-objects/src/deployment-level-no-tenant-column.test.ts, packages/cli/src/utils/organization-ownership-inventory.ts, scripts/platform-object-tenancy-census.json, packages/objectql/src/lifecycle/lifecycle-service.ts, packages/objectql/src/lifecycle/lifecycle-service.test.ts (widened to the whole non-prose diff; the cited code on main — plugin-audit read-audit.ts, parent-record-read-gate.ts, comment-access-hooks.ts, activity-changed-fields.ts, audit-hook-object-scope.test.ts, docs/adr/0057-system-data-lifecycle-and-retention.md §3.3, scripts/check-changeset-no-major.mjs, scripts/pm/clause2-line.mjs, .changeset/config.json — was read, not reviewed)

Fresh record for the new head. The prior PASS 6673587300 named f9f4c1b60342b2da2ab079f88d55b42f76943507 and does not carry; it is the baseline for everything the delta leaves alone. Governing text for the delta: file-surface amendment 6673909090, dev rework report 6674667474, ruling 6663872259 ("no audit row per sweep (not A)"), ADR-0057 §3.3 hard rule ("the LifecycleService's own deletes/rotations are not audited or activity-logged, otherwise cleanup re-feeds the tables it is draining"). Read-only shape held: git diff f9f4c1b6 29c59f4d, files via git show, both cards and the head's check-runs through the API. PR user.login is objectstack-fleet[bot].

The delta. One commit on top of f9f4c1b6, no merge of main (merge-base with origin/main is still 6ef7f583, no merge commits on the branch). Three files, +33/−1: audit-writers.ts (one SKIP_OBJECTS entry with its comment), audit-writers.test.ts (one pin), the existing changeset (one frontmatter line, one sentence). Every other path of the PR is byte-identical to the reviewed head (--stat lists only those three), so baseline ① items 1, 2, 4, 5, 6, 7 and 8 hold at this head unchanged; item 3 is corrected below.

① Derived judgments

  1. SKIP_OBJECTS is the right seam, and the only one. The CRUD ledger has ONE handler, writeAudit, registered on afterInsert, afterUpdate and afterDelete with excludeObjects: AUDIT_EXCLUDED_OBJECTS — a spread of the same set — and whose first line is if (SKIP_OBJECTS.has(ctx.object)) return;. One entry therefore unhooks all three verbs on both faces, registration and handler. There is no object-metadata opt-out: enable.activities: false drops only the sys_activity mirror and still writes the ledger row (test at line 815), and there is no "system-context writes are not audited" rule (the describe's own comment; the test at line 219 records a machine-originated write). The entry sits in group (2) beside sys_job_queue and sys_upload_session, whose posture it shares exactly: engine-owned, a single writer under SYSTEM_CTX, one or more writes per mechanical tick, no act anyone performed. The comment's claims were checked against the tree: managedBy: 'engine-owned' and isSystem: true on the object file, LifecycleService the sole reader and writer (baseline ①.6), one write per completed sweep. Right.
  2. Nothing else audits the row. The other sys_audit_log writers in the tree are event-specific, not object hooks: config-change-audit.ts (settings keys), admin-import-users.ts, action-invocation-audit.ts, auth-event-audit.ts. The only other sys_activity hook, installActivityStampHook (activity-changed-fields.ts), is registered with object: 'sys_activity'. The wildcard afterInsert hooks outside plugin-audit (auth-plugin.ts, plugin-sharing rule-hooks.ts / primary-bu-projection.ts) write no ledger. So the ruling's "no audit row per sweep" now holds end-to-end, not only by declaration. Right.
  3. Correction to baseline ①.3. At f9f4c1b6 the object's "no audit row" was the declaration's intent, not the tree's behaviour: the generic writer audited the watermark insert, the aging sweep archived that ledger row, and Test Core 1/8 (the cli audit-key-material / audit-mail-bodies archive integration tests, case 4, archived 2 vs 1) caught it. The prior record read the declaration and not the writer; this head makes the declaration true. The archive tests' expected count is unchanged — the fix removes the spurious row rather than widening the expectation, which is the right direction.
  4. Derived consequences of the list entry, each intended. AUDIT_EXCLUDED_OBJECTS has three other consumers on main: (a) read-audit.ts refuses a read-audit opt-in naming the object, with a warn, so the sweeper's one boot-time findOne is never read-audited either — consistent with the ruling and with apiEnabled: false; (b) parent-record-read-gate.ts drops it from the enumerated parents, and commentThreadGate() names every excluded object as a parent, so comment visibility is unchanged (and the object has no data API to be commented on); (c) engine.ts reads the registration face only. No business object changes posture: the control pins ("still audits ordinary business writes", sys_file stays audited) are untouched. Nothing is unaudited by accident.
  5. The pin is real. The fake engine's registerHook ignores excludeObjects, so the handler fires for the object; past the skip line its only early return before the two create calls is !api?.sudo, which the fake supplies, and a session: { isSystem: true } write of a business object reaches the ledger (line 219). Deleting the entry writes a sys_audit_log and a sys_activity row for the insert and again for the update, created is non-empty, toEqual([]) reds. The dev's ablation record agrees (delete of the line: 1 failed; restore blob equals HEAD, git diff HEAD empty). The pin fires the two verbs the sweeper issues (insert, then update); delete is the same handler and is covered by the sys_job_queue lifecycle case beside it. Holds.
  6. Roster line (the schema face) unchanged. platform-object-names.ts is identical to the reviewed head: one line, sorted between sys_jwks and sys_member, the roster test on main holds it to the declared *.object.ts set both ways. Widening as declared. Holds.

② Semver level

  • Changeset .changeset/29918-lifecycle-sweep-watermark.md: minor for spec, objectql, platform-objects, cli and now plugin-audit; no major. Level axis (check-changeset-no-major.mjs, ruling A as the clause2-line.mjs docblock states it): a declared added must grade AT LEAST ONE package whose published source moves minor or above — spec and platform-objects do, as before; only a declared none forces every entry to patch, so a minor on plugin-audit under added is admissible, and it is consistent with the changeset's own cli entry (a non-protocol inventory row, also minor). Every one of those packages is in the one fixed group, so the per-package level affects the CHANGELOG heading and nothing of the version. Check Changeset concluded success on this head.
  • Declaration lines are unchanged byte-for-byte (Clause-②: yes (widening), Metadata-protocol: added, Skills-docs: none — REASON) and identical between body and changeset. The new changeset sentence ("@objectstack/plugin-audit leaves sys_lifecycle_state out of sys_audit_log and sys_activity, so a sweep writes no ledger row for its own clock") describes the tree. Right.

③ Boundary flags

open_questions: []. No new deviation. Two prose items are the seat's stroke, not the dev's (os-dev rule 1 bars a dev PATCH of the body): the PR body still reads "Changeset — minor for spec / objectql / platform-objects / cli" and "Verification: All on head f9f4c1b", and the plugin-audit paragraph the dev handed over in 6674667474 is not yet under Acceptance notes. Should-fix at ACCEPT, prose only. Baseline ③ items 1–5 stand. Author note: PR user.login is objectstack-fleet[bot]; no outside author.

④ Design judgment

  1. Roster line against its first consumer — unchanged from the baseline and re-read: the roster test and the declaration land together, the inventory test is satisfied by the row, no generated artifact derives from the roster. Holds.
  2. The skip against ADR-0057 §3.3 and ruling 6663872259. §3.3's hard rule is that the sweeper's own activity is never audited or activity-logged because cleanup would re-feed the tables it drains; the watermark write is that activity's own record, so excluding it is the rule applied, not an exception to it. The ruling's "no audit row per sweep (not A)" is now a property of the tree. A second clock row under another id would be skipped too — object-level by design, and the object comment says one row per clock. Holds.
  3. Tried to break it. A read-audit policy that lists the object: warned and dropped, no row. A manage_platform_settings holder writing the row by hand: there is no data API (apiEnabled: false), so the sweeper under system context remains the only writer and no human act goes unrecorded. A deployment composing plugin-audit without platform-objects: the entry names an object that is never registered, the excludeObjects subtraction is a no-op, nothing else changes. The archive integration contract: with the entry, the sweep writes no ledger row about itself, so the archived count is exactly the business rows the tests seed. No must-fix found.

Check-runs on this head (API, at review time): 64 runs, 21 success, 11 skipped, 32 still running, 0 red. Green so far: Check Changeset, Governed Surface Queue Guard, PR-body guards, Dogfood Regression Gate, Lint · ESLint, Lint · PM dispatch-gates, Lint · repo & tooling gates (the census checks), Lint · CI release & engine gates, Lint · scoped tooling self-tests and query-options erasure, Spec property liveness, Type Check · source gates, Plan Test Core shards, Auto Label, Check PR Size, Flag docs. Still running: Build Core, Test Core 1–8 (1/8 is the shard that was red at f9f4c1b6), Type Check · workspace / debt ledger / consumer gates, Lint · scoped slot lookup, Type Check UI, Build & E2E UI, Live E2E UI, Test UI 1–16. The dev reports at this head the two archive integration files 16/16 (were 2 failed / 14 passed at f9f4c1b6, reproduced), plugin-audit 1146/1146, 96/96 derived gates exit 0, full dogfood 3513 passed. Landing precondition ② (every check green) stays the dispatching seat's to confirm once the running jobs conclude; nothing read here predicts a red, and the one shard that was red is the one this commit fixes.

Should-fix (non-blocking): (a) the PR-body prose named in ③; (b) baseline should-fixes (a) and (b) from 6673587300 remain open, prose only.

Implemented-by: claude/project-thread-ie3sda
Reviewed-by: session_016WV4cytLmJxfAmgPVjUbbW

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW

席位核对 · ACCEPT · 2026-11-06T02:18Z · #29918 (and #30641)

Head-sha: 29c59f4

  • Shape: the lifecycle sweep reads a watermark row (sys_lifecycle_state, engine-owned, tenant-less, beside sys_migration) and skips the boot sweep while the last completed sweep is younger than the interval, per ruling B 6663872259. The object, its registration, roster line, census and inventory rows ride one commit; the service, tests and changeset sit beside them. The rework commit adds sys_lifecycle_state to plugin-audit's SKIP_OBJECTS with a pin. Inside claim 6672243354 and amendments 6672382820 / 6673909090. Spec hunks under domain:spec consent 6669304514 / 6670346930.
  • Review of record: CONTRACT_REVIEW_TIER PASS 6674767458 at this head, which re-records the PASS 6673587300 of the first head f9f4c1b6 for the three-file delta.
  • Gates: every check-run on this head has concluded: 59 success, 13 skipped, 0 failure. The first head's red (Test Core 1/8, the two cli archive integration tests, archived 2 vs 1) is fixed by the rework, not re-run around. Locally, the rework report 6674667474 shows 96/96 derived gates and pnpm lint exit 0, full dogfood 421 files / 3513 tests, both archive tests 16/16, plugin-audit 1146/1146.
  • Accepted deviations:
    • A fresh store now issues one write at the initial delay (the seed insert), where the card's done-when says "at most the watermark read". It is card Expected item 2 placed at the initial delay, and it makes a reboot inside the first interval skip too; ruling B's own done-when is met exactly.
    • The plugin-audit hunk (amendment 6673909090): without it every completed sweep wrote an audit and an activity row about its own clock, which ruling 6663872259 and ADR-0057 §3.3 forbid. SKIP_OBJECTS is the only seam, and @objectstack/plugin-audit moves at minor with the other packages.
    • The released branch was squashed into one commit so the roster line lands with the object; Skills-docs: none — REASON replaces the order's no move, which is outside the grammar; a misplaced docblock was reattached with no code moved.
  • Should-fix, not taken here (prose only): the system/index.ts header docblock does not list the new object, and the body calls LIFECYCLE_STATE_OBJECT / LIFECYCLE_STATE_ROW_ID exported constants although the package root does not re-export them.
  • Contract: Clause-②: yes (widening), Metadata-protocol: added, confirmed by the record.
  • Landing: ready, then auto-merge, through the relay. Fixes #29918 and Fixes #30641; the spec seat on duty posts the spec(platform-object-names): reserve sys_lifecycle_state in the platform-objects roster (blocks #29918 item 1) #30641 landing note (6674573954).

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 protocol:system size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant