Repository navigation
feat(spec)!: an agent's memory contract states exactly what the runtime honours — maxEntries and reflectionInterval are required once long-term memory is enabled, longTerm.store is retired, and the block is live - #21413
Conversation
…, ledger flip Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…hangeset Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…ent-memory-contract
…ent-memory-contract
…clared keys Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
…ning help texts Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e05a381f4f4bcfdb72e93cd349d1c2789c2b79b1 && git checkout e05a381f4f4bcfdb72e93cd349d1c2789c2b79b1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5e58193338ac4c09591b8dfb3bb7a95a8fea2ae8 39412feb9e0ee9e899b1c6b27ae2c82b2ff58674 && git checkout -B drift-repro 5e58193338ac4c09591b8dfb3bb7a95a8fea2ae8 && git merge --no-ff 39412feb9e0ee9e899b1c6b27ae2c82b2ff58674
node scripts/docs-audit/affected-docs.mjs --json 5e58193338ac4c09591b8dfb3bb7a95a8fea2ae8
|
Contract reviewServed-tier: PR #21413 @ Check-runs on ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged against ruling A′ and the
② Semver level
③ Boundary flagsDev report
Implemented-by: VERDICT: PASS Generated by Claude Code |
…count consumes it (objectstack-ai#21409) (objectstack-ai#21431) Fixes objectstack-ai#21409 Clause-②: no (narrowing) Dispatched by the PM claim `5953981594` (PM loop round 1, `domain:spec` seat 1), on the triage direction in the card body (the B answer `5952826307` to `5952467081`). Session `session_01UtnxvdiN376GF3sgXwAw4d`. The row wildcard `'*'` is what a `count` aggregates (`COUNT(*)`). It is now admitted in exactly one place, a measure that counts. Everywhere else it is refused at the authoring parse, naming the slot and prescribing a `count` or a column. The measured `500 DATABASE_ERROR` at `POST /api/v1/analytics/dataset/query` is now a `400 VALIDATION_FAILED`. ## What changes (`@objectstack/spec`) | position | slot | refusal | spelled as | |---|---|---|---| | 1 | cube measure `MetricSchema.sql`, under any `type` but `count` | `custom` at `sql` | refinement asking the ONE predicate | | 2 | cube dimension `DimensionSchema.sql` | `invalid_format` at `sql` | pattern `ANALYTICS_COLUMN_PATH` (the dataset dimension's own) | | 3 | dataset measure `DatasetMeasureSchema.field`, under any `aggregate` but `count` (or none: a `derived` measure) | `custom` at `field` | refinement asking the ONE predicate | | control | dataset dimension `DatasetDimensionSchema.field` | `invalid_format` (since PR objectstack-ai#21240) | unchanged | - **One predicate.** `rowWildcardOutsideCount(reference, aggregate)` and its refusal `rowWildcardOutsideCountRefusal(slot, aggregateKey, aggregate)` live in `packages/spec/src/data/analytics-column-reference.ts`, beside the column-reference grammar, outside the `data` barrel (not published API). Both measure refinements call them. Neither restates the rule. The pin asserts each issue IS the builder's output for its slot. - **Dropped refinements.** The two measure refinements are cross-field, so they cannot be a JSON-Schema `pattern`. They are declared in `dropped-refinements.baseline.json`: the roots `data/Metric` and `ui/DatasetMeasure`, plus the embedded sites the build printed (`data/Cube`, `ui/Dataset`, and the four installed-package API schemas). The measured counts move to 217 schemas / 652 sites. Position 2 is a `pattern`, so the published JSON Schema states it. - **ADR-0087.** One D3 entry: `migrations/entries/semantic/18.analytics-row-wildcard-outside-count-refused.ts`. `registry.ts` was regenerated by `gen:migration-registry`, never edited between markers. There is no D2 conversion: rewriting to `count` changes the figure the author asked for, and only the author can name a column. There is no `RETIRED_KEYS_BY_MAJOR` row, and no `STEP18_RATIONALE` fragment. That fragment is optional, and adding it would be a hand edit to `registry.ts` outside the claimed generated region. `spec-changes.json` and the upgrade guide stay at protocol 17, as for every major-18 entry, and both checks are green. - **Why an entry.** I judged this against `cube-member-sql-expression-retired` and `dataset-member-field-expression-refused`, the two accept-set narrowings of the same slots. Both register a D3 entry because a stored document needs a prescription and has no mechanical rewrite. The same holds here. - **Liveness.** `analytics_cube` `measures.sql` and `dimensions.sql` were the notes that pointed the count-only boundary at objectstack-ai#21000. They are re-pointed here. `dataset` `measures.field` states the narrowing. All three stay `live`, re-verified 2026-10-02. - **Docs.** `content/docs/references/ui/dataset.mdx` is regenerated: the measure `field` describe now says `"*"` is for a count. - **Changeset.** `@objectstack/spec` `minor`, with the BREAKING banner, the `(narrowing)` arm, FROM → TO and one ADR-0087 marker (`registered analytics-row-wildcard-outside-count-refused`). - **Runtime.** Unchanged. No strategy code in `service-analytics` was touched. ## Zone 1, read as written — one point flagged, not silently chosen The direction says "one cross-field rule per position, sharing one predicate". Position 2, a cube dimension, has no aggregate, so no rule there can be cross-field. Zone 2 item 2 says to find how the control (the dataset dimension, PR objectstack-ai#21240) spells its `'*'` refusal and follow it. The control spells it as a `pattern`, `ANALYTICS_COLUMN_PATH`, not as a refinement. So position 2 takes that same pattern, and the two dimension slots now publish one identical pattern. The cross-field predicate covers the two measure slots, where an aggregate exists. This is strictly stronger than a third refinement would be. The published JSON Schema carries this half, and no dropped-refinement row is needed for it. There is still one rule source (`COLUMN_PATH`, read twice) and one cross-field predicate (read twice). Nothing has a second spelling. ## The PM's mechanism assumptions, measured 1. **Confirmed.** On `ceb4a939b4`, `analytics-column-reference.ts` declared the shared grammar, and `ANALYTICS_COLUMN_REFERENCE` admitted `'*'` for every member. `cube-member-sql-column-reference.test.ts` pinned `'*'` on a cube dimension. That pin is now replaced by the refusal, because it pinned exactly the branch removed. 2. **Partly falsified.** The control is a pattern, not a refinement (see above). The measure positions are refinements (`superRefine` chained on the strict objects, the `DatasetSchema` precedent), because only they are cross-field. 3. **Measured. What a stored document meets now:** - **At `/meta` reads.** It is served as stored, with the refusal on `_diagnostics`. Probe through `computeMetadataDiagnostics` on the built spec: a stored dataset with `{ aggregate: 'sum', field: '*' }` reads back as `valid: false` with `measures.1.field` / `custom`. A stored cube reads back as `measures.total.sql` / `custom` and `dimensions.everything.sql` / `invalid_format`. A re-save through the write door is refused at the slot. - **At the dataset query door.** The route parses every dataset it is handed, inline or saved. A stored dataset carrying such a measure is refused `400 VALIDATION_FAILED` on every query. That includes a query that selects only its healthy `count`, which answered `200` before. It fails closed, never a stand-down, and the blast radius is the dataset. The door test pins both selections. - **Stored `analytics_cube` rows.** Read from code: these never reach the analytics registry. `serve.ts` feeds it from the stack definition's `analyticsCubes` only, and that parse (`defineStack`) refuses such a cube. ## Census: no producer (triage's "no producer is known", measured) - **This repo at `ceb4a939b4`.** `git grep` of every `field` / `sql` value spelled `'*'` over `examples`, `packages` (fixtures included), `content`, `skills`, `apps`, `scripts` and `docs` found 173 hits. Each was read in its enclosing object literal: 154 under a `count`. The other 19 are QueryAST aggregations (`function: 'sum', field: '*'` in objectql conformance tests, which is not one of the three positions), comments, and strategy-level `method: 'count'` literals. Zero sit at a non-count cube measure, a cube dimension or a non-count dataset measure. - One more author was found through a loop variable: the cube-dimension accept pin above. - **objectui at the `.objectui-sha` pin `89cad75d55`.** Read-only `git grep` at the pin found zero `field` / `sql` values spelled `'*'`. Lit controls: 51 `aggregate: 'sum'`, 438 `field: 'amount'`. A `'*'` scan of the 302 files mentioning `aggregate` found only `objectName: '*'` bus events, query-builder `'*'` and i18n required marks. None is a dataset or cube slot. - **Deployed metadata.** NOT MEASURED. ## The door cell: 500 → 400 `packages/rest/src/analytics-dataset-row-wildcard-door.test.ts` drives the real route over a real `ObjectQL` engine with a better-sqlite3 `SqlDriver`. It uses `AnalyticsServicePlugin`'s own composition, once per strategy, with read counters proving which strategy answered. - **Before.** I ran this test against the BASE spec build (`ceb4a939b4`, dist verified free of the new predicate): `Tests 20 failed | 4 passed (24)`. - Every inline cell answered `{"error":"Internal server error","code":"DATABASE_ERROR"}: expected 500 to be 400`. That held for `sum`, `avg`, `min`, `max` and `count_distinct` over `'*'`, on both strategies. - The saved dataset, querying its healthy count, answered `200`. - The four count controls were green. - **After.** On the fixed spec build: `Tests 42 passed (42)` (this file's 34 plus the neighbouring `analytics-16019-driver-declared-fault.test.ts`'s 8). - Every refused cell answers 400 `VALIDATION_FAILED` with the issue at `measures.2.field` (`custom`). - Raw-SQL and engine-aggregate counters stay at 0. - The `count`-over-`'*'` controls answer the row counts, `[{a,2,2},{b,1,1}]`, on native SQL (raw-SQL counter ≥ 1) and on ObjectQL (aggregate counter ≥ 1). - The cells for a saved dataset selecting the wildcard measure itself were added after the base run, so their base answer is NOT MEASURED. They compile the same measure the inline cells do. ## Tests (final union at `60644d73d9`, after the `main` merge) - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` gave `Test Files 601 passed (601)`, `Tests 17647 passed | 1 todo`. - It includes the new `src/data/analytics-row-wildcard-count-only.test.ts` (31 cases: every non-count `AggregationMetricType` and `AggregationFunction` option, every `DimensionType`, the `derived` case, the controls, `CubeSchema` and the `analytics_cube` door, `defineCube`, `DatasetSchema` and the `dataset` door, `defineStack` with STACK_SCHEMA_INVALID / 422, the JSON-Schema halves with the ledger rows, and the D3 entry). - `pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/analytics-dataset-row-wildcard-door.test.ts src/analytics-16019-driver-declared-fault.test.ts` gave `Tests 42 passed (42)`. - Also run before the merge (`934b70a2db`; the incoming `main` commits touch neither package): - `rest --project local`: `256 passed (256)` files, 4848 tests. - `service-analytics`, as the main consumer of both shapes: `168 passed (168)` files, 3794 tests. - spec repo-project subset (`cube-member-inner-name-retirement`, `cube-refresh-key-retirement`, `step18-rationale-merge`, `liveness/evidence`, `liveness/proof-registry`): `131 passed`. - `pnpm --filter @objectstack/spec typecheck` and `pnpm --filter @objectstack/rest typecheck`: exit 0. - The whole spec repo project (48 files) is NOT MEASURED locally. One run exceeded the foreground cap, so it is declared to CI. ## Ablation (one-shot, not kept) From the committed fix, through `scripts/ablation-replace.mjs`, `rowWildcardOutsideCount` was made to answer `false` (anchor 1 → 0, marker 0 → 1, blob `2bb692602dc8` → `4bdfba658269`). The new spec file went `19 failed | 12 passed (31)`. Red: the predicate table, every position-1 and position-3 cell, and the four door cases. Green: position 2 (a pattern, untouched by the predicate), every control, the JSON-Schema halves and the D3 pin. That is the predicted direction. Restored with `git checkout HEAD --`: blob equals the HEAD blob and `git diff HEAD` is empty, under a `trap` on EXIT/INT/TERM. The spec suite imports the source by relative path, so no `dist/` sits on its resolution path. ## Gates - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths, merge base `39a912ea7`) derived 115 families. I ran all 115, and `--ran` reconciles **113 run green, 2 NOT MEASURED (exit 3, prerequisite), 0 UNRUN**: - `pnpm check:dual-build-cjs-loads`: needs every package's `dist`, which means a whole-repo build. - `pnpm check:type-check-debt`: its `--re-measure` builds the ledgered packages' closure itself, and that build passed the 300 s per-gate cap. - Both are whole-tree, and CI's `Lint & Repo Gates` runs them. - `check:skill-examples` first exited 3 (client-react unbuilt). After building `client` and `client-react` it exited 0 (`259 prose examples type-check`). - `pnpm --filter @objectstack/spec check:generated` passes all 15 artifacts after the merge. The only stale artifact before was `content/docs/references/**`, regenerated with `gen:docs`. - **Clause-② measured.** `node scripts/pm/check-widening-tells.mjs --declaration no --diff` (merge-base diff) exited 0 with no widening tell. It stated two silences: the `rowWildcardOutsideCountRefusal(` lines name an imported factory it does not resolve. The predicate is not exported from any published entry (`check:api-surface` green, artifacts byte-identical), so the arm is `no (narrowing)`, as triage wrote. - **ESLint (narrowed, a measurement).** - Population: `eslint.config.mjs` lints `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 8 changed `.ts` files fall inside it. - Count: `--format json` read back 8 files, 0 errors, 0 warnings. - Invariance: the config never enables type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move a verdict on an untouched file. - The repo-wide `pnpm lint` is CI's. ## Serial - PR objectstack-ai#21413 landed while this branch was in flight and touched `dropped-refinements.baseline.json` and `registry.ts`. I merged `main` through `scripts/pm/os-regen-merge.sh`. - The baseline conflicted on its `measured` counts only. It was not text-merged: I took `main`'s file and re-declared this branch's 12 sites, and the counts were recomputed from the entries. `gen:schema` then validated the ledger against the tree. - `gen:migration-registry` reproduced the auto-merged region byte-identically. objectstack-ai#21413's `agent-memory-store-retired-and-limits-required` entry is present at HEAD. - objectstack-ai#21365 had not landed at merge time. Whichever lands later merges `main`. ## Acceptance notes (not filed) - **Runtime inference mints the same shape, unreached by this parse.** `service-analytics` `inferMeasure` turns a caller-named measure with an empty prefix (`_sum`, `_avg`, `_min`, `_max`, `_count_distinct`) into `{ type: 'sum' …, sql: '*' }` (`key.slice(0, -suffix.length) || '*'`). The caller-measure gate admits `inferredSql === '*'`. Read from code only, NOT MEASURED at a door, and outside this card's no-strategy-edit surface. Carrier: the `domain:services` lane; no carrier named. - **A `derived` dataset measure's `field` is read by nothing.** The compiler skips it. This card now refuses `'*'` there, but any column value still parses inert. Observed while reading the compiler; no producer found. Carrier: none named. - **objectstack-ai#21000's enum retirement is untouched.** `AggregationMetricType` `number` / `string` / `boolean` are still covered by the predicate's "anything but count" for as long as they exist. --- _Generated by [Claude Code](https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…runtime, not declared only (objectstack-ai#21652) Fixes objectstack-ai#21415 Clause-②: no ## What changed `skills/objectstack-ai/SKILL.md`, Common Pitfalls 1, told an AI author that `memory` is "declared only — no runtime reads it". Since the agent memory contract landed on `main` (spec commit 22c2d6f, ruling A′ on objectstack-ai#20274), that sentence is false in the published catalog, and the open Version Packages PR (objectstack-ai#21352) ships that contract in the next release — the skill must not go out contradicting it. One clause is rewritten; nothing else in the pitfall changes. The new text states the three facts the contract makes true and keeps the pitfall's point that none of this is a gate: - `memory` is enforced by the AI runtime ☁️ — long-term notes are recalled and reflected on; - `maxEntries` and `reflectionInterval` are required once `longTerm.enabled` is true; - there is no storage backend to choose. The sentence that follows ("For a gate that is **enforced**, use …") is untouched. The three other `memory` mentions in the file (:218–247) are the knowledge adapter id `'memory'`, a different thing, and are left alone. ## Evidence read from the spec (at `origin/main` 15fe567, `packages/spec/src/ai/agent.zod.ts`) - :185–188, header comment: "The one runtime that executes agents, cloud's AI service, enforces long-term memory from `enabled`, `maxEntries` and `reflectionInterval`". - :196 `LONG_TERM_STORE_RETIRED`: "`agent.memory.longTerm.store` was removed in @objectstack/spec 17.7.0 (ADR-0049 enforce-or-remove) — the memory store is platform infrastructure, not agent metadata". - :210 `MAX_ENTRIES_REQUIRED`: "`agent.memory.longTerm.maxEntries` is required when `agent.memory.longTerm.enabled` is true". - :217 `REFLECTION_INTERVAL_REQUIRED`: "`agent.memory.reflectionInterval` is required when `agent.memory.longTerm.enabled` is true". - :224 `REFLECTION_INTERVAL_WITHOUT_LONG_TERM`: "`agent.memory.reflectionInterval` requires `agent.memory.longTerm.enabled: true`". - :242–259 `checkAgentMemoryContract`, the refinement on `memory` that issues those three. - PR objectstack-ai#21413's commit 22c2d6f is an ancestor of `origin/main` (`git merge-base --is-ancestor`, exit 0) and of the Version PR's base 36ad321 (REST compare: ahead 112, behind 0). ## Readings - Branch cut at `origin/main` 15fe567 (the dispatch read 0c50b5d; the one commit between touches `packages/spec/src` only, and the skill file is byte-identical across the two). - Changed file, whole file: 418 → 421 lines (net +3; budget ≤ +3). - Whole package (every `skills/**/SKILL.md` summed): 4397 → 4400 lines. - Token ratchet `node scripts/check-skills-token-ratchet.mjs`: `skills/objectstack-ai/SKILL.md` 5486 → 5539 tokens, ceiling 6806 unchanged, headroom 1320 → 1267. Self-test: 65 cases pass. ## Verification (all at f85f008, the only commit on the branch) Gate list derived in the worktree with `node scripts/pm/dispatch-gates.mjs --commands` (no paths; change set off merge base 15fe567): 24 commands, identical to the dispatch's list. Every one exited 0; `--ran` reconciliation with exit codes recorded: "24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN … a DERIVED zero — all 24 recorded an exit code and none of them is 3". - `pnpm --filter @objectstack/spec build` under `scripts/pm/os-verify-lock.sh` — "VERDICT command-exit 0" (held 127s, waited 134s), before `check:skill-docs` — "✅ Skill docs in sync". - `check:doc-formula-expressions` first answered exit 3 (PREREQUISITE NOT MET: `@objectstack/formula` / `@objectstack/lint` unbuilt — nothing measured); after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under the lock (VERDICT command-exit 0) it reads "✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 460 files / 1381 TS blocks judged clean". - `check:skill-identifier-liveness` — "OK — Leg 1: 457 citation(s) over 53 published file(s) checked against 118371 implementation word tokens (0 ledgered exemption(s)); Leg 2: 8 registered exhaustive section(s), 0 ledgered gap(s)". - `check:doc-authoring` — "17369 customer-facing string(s) across 1256 spec sources clean — no internal issue-id references". - `check:nul-bytes` — "OK (scanned 10022 text file(s) … no raw ASCII control bytes)"; control-character grep over the edited file: 0 hits. - `check:skill-compatibility` — "10 SKILL.md file(s) reconciled against 80 workspace packages"; `check:corpus-claim-drift`, `check:role-word`, `check:closing-keyword-parity` (+ self-test, 40 assertions), `check-doc-route-spelling --advisory` (+ self-test), `check:comment-mask-corpus` (8119 files, 0 disagree), `check:ci-filter-parity`, `check:agent-test-spelling`, `check:cross-package-test-inputs`, `check:driver-memory-census`, `check:gitlink-declared`, `check:pm-governed-merges`, `check:refd-timer-probe`, `check:skill-frame-sync`, `check:watch-hint-literal` — all green. Changeset: none — `skills/**` is not shipped by any workspace package's `files[]`; `skip-changeset` is requested on this PR. ## 维护者速读(草稿) **改了什么。** 只改 `skills/objectstack-ai/SKILL.md` 的 Common Pitfalls 第 1 条里关于 `memory` 的那半句:原文说「`memory` 只是声明、没有运行时读它」,现在改成「`memory` 由 AI 运行时 ☁️ 强制执行 —— 长期记忆会被回忆与反思;`longTerm.enabled` 为 true 时 `maxEntries` 与 `reflectionInterval` 必填;存储后端不可选」,并保留「这些都不是 gate」的要点。其余一字未动。净增 3 行(预算 ≤ 3),token 读数 5486 → 5539(上限 6806 未动)。 **为什么改。** 这份 skill 随 `npx skills add` 发到客户项目,AI 作者照它写元数据。spec 的 `agent.memory` 契约已经在 `main` 上落地(PR objectstack-ai#21413),开着的 Version Packages PR 会把它随下一版一起发出去;若 skill 仍说「没有运行时读它」,AI 作者会漏写必填的 `maxEntries` / `reflectionInterval`,或继续写已退役的 `longTerm.store`,发布时被拒。写给 AI 的文档说错一句等于产品缺陷,且必须与契约同版发运,这是 p1 的由来。 **风险与代价(含回滚)。** 纯文档改动,不发运任何包,无 changeset;本地 24 个派生门禁全绿。风险仅在措辞:三件事实均逐条对照 `packages/spec/src/ai/agent.zod.ts` 的拒收文案核过。回滚 = revert 这一个 commit(f85f008),无其它依赖。 **席位意见。** (留空,由席位定稿) **你要做的。** 这是 Tier H 受管面(`skills/**`),需要你一次授权的 APPROVED review;之后由席位落地。不需要你操作 Version Packages PR。 ## Acceptance notes - Out-of-scope findings: none. The `knowledge` adapter id `'memory'` at :218–247 of the same file is unrelated to `agent.memory` and was not touched. - The Version Packages PR (objectstack-ai#21352) head moved since the dispatch's reading (f872ccf → 015a5e3); its base 36ad321 still carries 22c2d6f (REST compare: ahead 112, behind 0). The Version PR and all changeset files are untouched by this PR. - Governed surface (Tier H): this PR stays draft; landing waits for an authorized approval and then the owning seat. Not flipped to ready, no auto-merge armed. - Writes spent by this run: `git push` (2: the empty branch probe, then f85f008), one `pr_create` through the fleet-write relay, one `label-write` (`skip-changeset` + assignee), one report comment. No MCP calls. --- _Generated by [Claude Code](https://claude.ai/code/session_01CB6W87z22K2yjUCDyVrJRk)_ Co-authored-by: objectstack-fleet[bot] <objectstack-fleet[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
Part of #20274
Clause-②: yes (narrowing)
What this does
Ruling of record 5950198150 (director batch #268 item 4, letter A′, maintainer 「同意」): the
agent.memorycontract states exactly what the runtime honours. One PR, as the ruling's execution parameters say: the refinement, thestoreretirement, the row flip, the describes. The claim amendment 5951618837 adds the two agent-form help texts, made after PR #21398 landed onmain.checkAgentMemoryContractinpackages/spec/src/ai/agent.zod.tsis a refinement onmemory, becausereflectionIntervalislongTerm's sibling. It raises onecustomissue at the key's own path in each of three cases, and each message names the key, its path and the remedy:longTerm.enabled: truewithoutlongTerm.maxEntriesis refused atmemory.longTerm.maxEntries.longTerm.enabled: truewithoutreflectionIntervalis refused atmemory.reflectionInterval. This is the most natural declaration,{ enabled: true, maxEntries: 5 }, which cloud refused at turn time.reflectionIntervalwhilelongTermis absent or not enabled is refused atmemory.reflectionInterval.storetombstone gets its own complaint and nothing on top of it.longTerm.storeretires as a whole key, by thespec-property-retirementplaybook:retiredKey()carries the prescription: "the memory store is platform infrastructure, not agent metadata … Delete the key", closing with the houseos migrate meta --from 17sentence.tscrefuses the key as well.backend/storage/provideraliases pointed atstore. They moved toguidanceand carry the same answer, because an alias may not target a tombstone (alias-integrity.test.ts).agent-memory-long-term-store-removed. Step 18,retiredFromLoadPath,retiredAfter17.6.0, order 56. It deletes the key fromagents[].memory.longTerm, losslessly, with one notice per agent. It supplies neither number. The fixture coversdatabase, the materializedvectordefault andredis, for 3 notices, plus two untouched controls.RETIRED_KEYS_BY_MAJOR[18]gains the nestedai/Agent:memory.longTerm.store. The entry file is18.ai__Agent__memory.longTerm.store.ts, and the generated region was written bygen:migration-registry.agent-memory-store-retired-and-limits-required. ItsconversionIdsnames the D2. It carries the judgement no conversion can make: the two numbers an enabledlongTermnow requires. ItsSTEP18_RATIONALEfragment is at order 61.acceptRetiredDefaultResidueis deliberately not adopted for the oldvectordefault. The measured producer census is zero, and the ruling's ② names the prescription as the backstop for the unmeasured tenant population. The replayed D2 heals stored rows and built artifacts. The reasoning is in the retired-key entry file.agent.memorymovesexperimental→live, withevidenceScope: cross-repoandverifiedAt2026-10-02.repo:cloud#1seat's reading 5946891697 of cloudef5a4344: the readeragent-runtime.ts#compileAgentMemory(viaAgentRuntime.resolveTurnGuardrails), the enforcement inai-service.ts, and the storeagent-memory.ts#AgentMemoryStore.ef5a4344the reader still readsstore: it honoursdatabaseonly and refusesvectorandredis. Per the ruling, cloud dropsstorein that one reader once this release reaches its pin, and no earlier.state-counts/agent.mdwas regenerated bygen:liveness-counts: live 22 → 23, experimental 2 → 1.memorydrops[EXPERIMENTAL — not enforced], in theguardrails/structuredOutputwording: enforced by the cloud AI runtime, and the open framework edition does not run agents.longTerm,enabled,maxEntriesandreflectionIntervaleach state what the runtime does with them: the newest N notes are recalled, a note is written every N delivered interactions, and notes beyond N are evicted.memoryrow's help text no longer names short-term memory, which is refused. It now states what memory does and that both numbers are required once long-term memory is enabled.planningrow's help text names onlymaxIterations.pnpm i18n:extractregenerated theenleaves, run against a spec rebuilt from this tree. Thezh-CN,ja-JPandes-ESleaves are authored, not copied.dependent-requiredis presence-only, andrefinement-projection.tssays a value rule stays dropped. So the new site is annotated as dropped.dropped-refinements.baseline.jsongainsmemoryunderai/Agentand under the four installed-package schemas that embed agents, which is 5 sites (635 → 640). This is the corrected entry the build printed.content/docs/references/ai/agent.mdx..changeset/20274-agent-memory-contract.md.@objectstack/specminor and@objectstack/platform-objectspatch (for the catalog leaves).maxEntriesandreflectionIntervalwhenlongTerm.enabled; deletestore".Clause-②: yes (narrowing)and the ADR-0087 markerregistered agent-memory-long-term-store-removed, agent-memory-store-retired-and-limits-required.The four surface ratchets (
api-surface,authorable-surface,json-schema.manifest,api-surface-signatures) are byte-identical, as expected for a NESTED key of an inline block: it has noauthorable-surface/line of its own.spec-changes.jsonand the upgrade guide are unchanged, because step 18 is not yet projected;PROTOCOL_MAJORis 17, the same state as the structured-output precedent.Verification (at
39412feb9e, after mergingorigin/mainatca0dfb658a)@objectstack/specbuild +check:generated: "All 15 generated artifacts are up to date".@objectstack/specvitest --project local: 599 files, 17583 passed, 1 todo.@objectstack/specvitest --project repo: 43 of 49 files, 705 passed. The other 6 are NOT MEASURED; see below.@objectstack/spectypecheck(tsc --noEmit, the scripts program and the test layer): OK. "52 file(s) / 246 error(s)" is exactly the pinned debt.tsc --listFilesovertsconfig.test.jsonlists bothsrc/ai/agent.test.tsandsrc/ai/agent-memory-store-retirement.test.ts, and none of the 246 errors is in either file. So the@ts-expect-erroronstoreis live.@objectstack/lintreadsliveness/agent.json:test119 files, 5575 passed;typecheckOK.@objectstack/platform-objectsholds the catalogs:test59 files, 949 passed;typecheckOK.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran: "117 derived famil(ies) accounted for — 117 run, 0 NOT-MEASURED (a DERIVED zero — all 117 recorded an exit code and none of them is 3)". Each exit code was written to disk before any pipe. The list includes:check:adr-0087-registration: "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+bang+clause-②-narrowing] registered agent-memory-long-term-store-removed, agent-memory-store-retired-and-limits-required";check-changeset-no-major: "This diff introduces nomajorbump";check:empty-changeset,check:liveness,check:doc-authoring,check:nul-bytes,check:migration-registry,check:cross-package-test-inputs,check:i18n,check:i18n-stale-fillandcheck:type-check-debt, which re-measured in 116.6 s.check:dts-closurered was a race with this worktree's own concurrent spec rebuild. It then named three unrelated packages (organizations,plugin-approvals,verify) whosedisthad no.d.ts. They were rebuilt, and it is green.check:i18n-coverageandcheck:i18n-walk-parityalso ran green.pnpm lintbelongs to CI):.tsfiles with--no-inline-config --format json: 13 files linted, 0 errors, 0 warnings.ESLint.isPathIgnoredis false for each.eslint.config.mjsenables no type-aware linting (noparserOptions.project; its own comment at:328says so), so this diff cannot move the verdict on any untouched file.8965af164d;scripts/ablation-replace.mjs; each mutation proven on disk by anchor count and blob hash, and each restore proven by blob equal to HEAD and an emptygit diff HEAD). Both test files import./agent.zodrelatively, so nodistrebuild is involved.const enabled = memory.longTerm?.enabled === true;was replaced by an early return: 6 failed, 67 passed. The six are the five refusal pins and the D2 lit control. The two acceptance pins stayed green.store: retiredKey(…)was replaced by the old three-value enum: 5 failed, 68 passed. The five are the every-value refusal, the did-you-mean pin, the tsc/parse pin, thedefineStackenvelope pin and the artifact boot-door pin.NOT MEASURED
@objectstack/specrepo-project files:publish-smoke-boot-failure,publish-smoke-port-collision,dist-freshness,dist-freshness-adoption,build-schemas-check-modeandschema-tree-freshness. Reason: they drive whole builds and exercise build tooling this diff does not touch; the precedent PR saw them cap-killed at the foreground ceiling. They are declared to CI.@objectstack/cliintegration tier. Reason: declared to CI; no CLI file is touched.Fixture and producer census
6d487d2094, outsidepackages/spec, inexamples/**andpackages/**:longTerm: 0 non-test files and 0 test files.reflectionInterval: 0 and 0.guardrailshits 4 non-test files, the four metadata-form catalogs.31971ff1e28fand at the current pin89cad75d55(after PR chore(objectui): bump the console pin to 89cad75d5570 (carries objectui 0858267e, 8001068b, 3ae91930 and d0fba91aa) #21380):longTerm: 1 file,packages/app-shell/src/views/metadata-admin/previews/AgentPreview.tsx, a display-only preview that readsmemory.longTerm.storethrough anas any. No build break follows. See Acceptance notes.reflectionInterval: 0. The controlAgentSchemahits 3 files.packages/spec/src/ai/agent.test.tshad two. "should accept agent with memory configuration" wrotestore: 'vector'and expected it back; it now writes both numbers and asserts that nostorematerializes. "should accept all memory store backends" is replaced by acceptance of memory without enabled long-term memory.packages/lint/src/lint-liveness-properties.test.tshad three cases usingagent.memoryas the liveexperimentalledger witness. They were repointed totool.outputSchema:memoryis no longer experimental, andtool.outputSchemais the remaining experimental row with no retirement queued.agent.lifecyclehas one, the spec half tracked separately.agent.test.tsgains 8 contract cases.src/ai/agent-memory-store-retirement.test.ts, registered invitest.repo-tests.json, holds 14 cases: the tombstone at every door, the D2 conversion, the registration, and the tree-scoped absence walk.@objectstack/spec#test:repoalready declares inscripts/cross-package-test-inputs.mjs. Its matcher judges alongTermblock that writesstore/backend/storage/provider, has an anti-vacuity case, and excludes the conversions fixture, release notes and gitignored output.content/docs/**other than the regenerated reference page: 0 hits.Acceptance notes
skills/**, Tier H, not edited here).skills/objectstack-ai/SKILL.md:311-313says "memoryis declared only — no runtime reads it". Noskills/**text nameslongTermorstore. Carrier: the skills seat that took theguardrailshalf of the same sentence.AgentPreview.tsx:205-216rendersshort.maxMessages(retired by ADR-0013 D3) andlong.store(retired here). Both now render nothing. It is display-only and read throughas any, so the Console Pin Gate is unaffected. No carrier is named.dropped-refinements.baseline.jsonmeasured.refinementSitesThatDidProjectreads 369, while this tree's build prints 436. That counter was stale before this PR, and onlydroppedRefinementSiteswas moved (+5). No carrier is named.agentrow still says "autonomy tier experimental". After this flip,lifecycleis the only experimental agent row, and its retirement is queued. The precedent flips left the row as written, and so does this one.Generated by Claude Code