Skip to content

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

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-20274-agent-memory-contract
Oct 2, 2026

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20274

Clause-②: yes (narrowing)

What this does

Ruling of record 5950198150 (director batch #268 item 4, letter A′, maintainer 「同意」): the agent.memory contract states exactly what the runtime honours. One PR, as the ruling's execution parameters say: the refinement, the store retirement, the row flip, the describes. The claim amendment 5951618837 adds the two agent-form help texts, made after PR #21398 landed on main.

  • The contract check. checkAgentMemoryContract in packages/spec/src/ai/agent.zod.ts is a refinement on memory, because reflectionInterval is longTerm's sibling. It raises one custom issue at the key's own path in each of three cases, and each message names the key, its path and the remedy:
    • longTerm.enabled: true without longTerm.maxEntries is refused at memory.longTerm.maxEntries.
    • longTerm.enabled: true without reflectionInterval is refused at memory.reflectionInterval. This is the most natural declaration, { enabled: true, maxEntries: 5 }, which cloud refused at turn time.
    • reflectionInterval while longTerm is absent or not enabled is refused at memory.reflectionInterval.
    • No default is declared for either number. The check runs only on a body the shape already accepted, so an unknown key, a wrong type or the store tombstone gets its own complaint and nothing on top of it.
  • longTerm.store retires as a whole key, by the spec-property-retirement playbook:
    • Tombstone. retiredKey() carries the prescription: "the memory store is platform infrastructure, not agent metadata … Delete the key", closing with the house os migrate meta --from 17 sentence. tsc refuses the key as well.
    • Aliases. The backend / storage / provider aliases pointed at store. They moved to guidance and carry the same answer, because an alias may not target a tombstone (alias-integrity.test.ts).
    • D2 conversion agent-memory-long-term-store-removed. Step 18, retiredFromLoadPath, retiredAfter 17.6.0, order 56. It deletes the key from agents[].memory.longTerm, losslessly, with one notice per agent. It supplies neither number. The fixture covers database, the materialized vector default and redis, for 3 notices, plus two untouched controls.
    • Registration. RETIRED_KEYS_BY_MAJOR[18] gains the nested ai/Agent:memory.longTerm.store. The entry file is 18.ai__Agent__memory.longTerm.store.ts, and the generated region was written by gen:migration-registry.
    • D3 entry agent-memory-store-retired-and-limits-required. Its conversionIds names the D2. It carries the judgement no conversion can make: the two numbers an enabled longTerm now requires. Its STEP18_RATIONALE fragment is at order 61.
    • acceptRetiredDefaultResidue is deliberately not adopted for the old vector default. 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.
  • Ledger. agent.memory moves experimental → live, with evidenceScope: cross-repo and verifiedAt 2026-10-02.
    • The evidence is quoted from the repo:cloud#1 seat's reading 5946891697 of cloud ef5a4344: the reader agent-runtime.ts#compileAgentMemory (via AgentRuntime.resolveTurnGuardrails), the enforcement in ai-service.ts, and the store agent-memory.ts#AgentMemoryStore.
    • The note attributes all of it to that reading. No seat in this session read cloud.
    • The window, stated in the note. At ef5a4344 the reader still reads store: it honours database only and refuses vector and redis. Per the ruling, cloud drops store in that one reader once this release reaches its pin, and no earlier.
    • state-counts/agent.md was regenerated by gen:liveness-counts: live 22 → 23, experimental 2 → 1.
  • Describes.
    • memory drops [EXPERIMENTAL — not enforced], in the guardrails / structuredOutput wording: enforced by the cloud AI runtime, and the open framework edition does not run agents.
    • longTerm, enabled, maxEntries and reflectionInterval each 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.
    • The NOTE comment's "forward-looking" is replaced by what is enforced.
  • The agent form.
    • The memory row'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.
    • The planning row's help text names only maxIterations.
    • pnpm i18n:extract regenerated the en leaves, run against a spec rebuilt from this tree. The zh-CN, ja-JP and es-ES leaves are authored, not copied.
  • The published JSON Schema. A value-conditioned requirement is not in the finite projection list: dependent-required is presence-only, and refinement-projection.ts says a value rule stays dropped. So the new site is annotated as dropped. dropped-refinements.baseline.json gains memory under ai/Agent and under the four installed-package schemas that embed agents, which is 5 sites (635 → 640). This is the corrected entry the build printed.
  • Regenerated: content/docs/references/ai/agent.mdx.
  • Changeset: .changeset/20274-agent-memory-contract.md.
    • The levels are @objectstack/spec minor and @objectstack/platform-objects patch (for the catalog leaves).
    • It carries the BREAKING header, the FROM → TO table and the one-line remedy: "declare maxEntries and reflectionInterval when longTerm.enabled; delete store".
    • It declares Clause-②: yes (narrowing) and the ADR-0087 marker registered 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 no authorable-surface/ line of its own. spec-changes.json and the upgrade guide are unchanged, because step 18 is not yet projected; PROTOCOL_MAJOR is 17, the same state as the structured-output precedent.

Verification (at 39412feb9e, after merging origin/main at ca0dfb658a)

  • @objectstack/spec build + check:generated: "All 15 generated artifacts are up to date".
  • @objectstack/spec vitest --project local: 599 files, 17583 passed, 1 todo.
  • @objectstack/spec vitest --project repo: 43 of 49 files, 705 passed. The other 6 are NOT MEASURED; see below.
  • @objectstack/spec typecheck (tsc --noEmit, the scripts program and the test layer): OK. "52 file(s) / 246 error(s)" is exactly the pinned debt. tsc --listFiles over tsconfig.test.json lists both src/ai/agent.test.ts and src/ai/agent-memory-store-retirement.test.ts, and none of the 246 errors is in either file. So the @ts-expect-error on store is live.
  • Consumers:
    • @objectstack/lint reads liveness/agent.json: test 119 files, 5575 passed; typecheck OK.
    • @objectstack/platform-objects holds the catalogs: test 59 files, 949 passed; typecheck OK.
  • 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 no major bump";
    • check:empty-changeset, check:liveness, check:doc-authoring, check:nul-bytes, check:migration-registry, check:cross-package-test-inputs, check:i18n, check:i18n-stale-fill and check:type-check-debt, which re-measured in 116.6 s.
    • On first runs, before the build closures existed, a few gates exited 3 (PREREQUISITE NOT MET). All were re-run green after building the lint, client and CLI closures.
    • One check:dts-closure red was a race with this worktree's own concurrent spec rebuild. It then named three unrelated packages (organizations, plugin-approvals, verify) whose dist had no .d.ts. They were rebuilt, and it is green.
    • check:i18n-coverage and check:i18n-walk-parity also ran green.
  • ESLint, as a proven narrowing (the repo-wide pnpm lint belongs to CI):
    • The run covered the 13 touched .ts files with --no-inline-config --format json: 13 files linted, 0 errors, 0 warnings.
    • All 13 are inside the config's population: ESLint.isPathIgnored is false for each.
    • The narrowing cannot have excluded anything. eslint.config.mjs enables no type-aware linting (no parserOptions.project; its own comment at :328 says so), so this diff cannot move the verdict on any untouched file.
  • Ablation (committed tree 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 empty git diff HEAD). Both test files import ./agent.zod relatively, so no dist rebuild is involved.
    • The contract check. The anchor 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.
    • A first attempt, whose replacement contained its own anchor, was a no-op. The tool refused it (anchor count 1 → 1), so no test ran.
    • The tombstone. 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, the defineStack envelope pin and the artifact boot-door pin.
    • The direction is red in both runs, as expected.

NOT MEASURED

  • Six @objectstack/spec repo-project files: publish-smoke-boot-failure, publish-smoke-port-collision, dist-freshness, dist-freshness-adoption, build-schemas-check-mode and schema-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/cli integration tier. Reason: declared to CI; no CLI file is touched.

Fixture and producer census

  • Producers. Measured at merge base 6d487d2094, outside packages/spec, in examples/** and packages/**:
    • longTerm: 0 non-test files and 0 test files. reflectionInterval: 0 and 0.
    • The lit control guardrails hits 4 non-test files, the four metadata-form catalogs.
    • So no producer exists in this repo.
    • Cloud's built-in agents also have none, per the ruling record's own census. Tenant-authored agents are NOT MEASURED.
  • objectui, at the dispatch pin 31971ff1e28f and at the current pin 89cad75d55 (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 reads memory.longTerm.store through an as any. No build break follows. See Acceptance notes.
    • reflectionInterval: 0. The control AgentSchema hits 3 files.
  • Fixtures that pinned the old acceptance, flipped:
    • packages/spec/src/ai/agent.test.ts had two. "should accept agent with memory configuration" wrote store: 'vector' and expected it back; it now writes both numbers and asserts that no store materializes. "should accept all memory store backends" is replaced by acceptance of memory without enabled long-term memory.
    • packages/lint/src/lint-liveness-properties.test.ts had three cases using agent.memory as the live experimental ledger witness. They were repointed to tool.outputSchema: memory is no longer experimental, and tool.outputSchema is the remaining experimental row with no retirement queued. agent.lifecycle has one, the spec half tracked separately.
  • New pins.
    • agent.test.ts gains 8 contract cases.
    • The new src/ai/agent-memory-store-retirement.test.ts, registered in vitest.repo-tests.json, holds 14 cases: the tombstone at every door, the D2 conversion, the registration, and the tree-scoped absence walk.
    • The walk covers the five roots @objectstack/spec#test:repo already declares in scripts/cross-package-test-inputs.mjs. Its matcher judges a longTerm block that writes store / backend / storage / provider, has an anti-vacuity case, and excludes the conversions fixture, release notes and gitignored output.
  • Other docs. content/docs/** other than the regenerated reference page: 0 hits.

Acceptance notes

  • Published skill text that is now false (skills/**, Tier H, not edited here). skills/objectstack-ai/SKILL.md:311-313 says "memory is declared only — no runtime reads it". No skills/** text names longTerm or store. Carrier: the skills seat that took the guardrails half of the same sentence.
  • objectui AgentPreview.tsx:205-216 renders short.maxMessages (retired by ADR-0013 D3) and long.store (retired here). Both now render nothing. It is display-only and read through as any, so the Console Pin Gate is unaffected. No carrier is named.
  • The release number in the prescriptions. They say "removed in @objectstack/spec 17.7.0", which assumes the next release is a minor (the label is 17.6.0, published). That is the same assumption the structured-output prescriptions make.
  • dropped-refinements.baseline.json measured.refinementSitesThatDidProject reads 369, while this tree's build prints 436. That counter was stale before this PR, and only droppedRefinementSites was moved (+5). No carrier is named.
  • The liveness README's agent row still says "autonomy tier experimental". After this flip, lifecycle is 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

claude added 11 commits October 2, 2026 10:49
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>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:ai tests tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 25 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json, packages/spec/liveness/agent.json, packages/spec/liveness/state-counts/agent.md, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/ai/agents.mdx (via AgentSchema (symbol, a top-level const))
  • content/docs/concepts/metadata-lifecycle.mdx (via maxEntries (literal, a string literal in AgentSchema; a string literal in checkAgentMemoryContract))
  • content/docs/getting-started/quick-reference.mdx (via AgentSchema (symbol, a top-level const))
  • content/docs/upgrading.mdx (via AgentSchema (symbol, a top-level const))

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

  • content/docs/releases/v13.mdx (via AgentSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object agentMemoryLongTermStoreRemoved), retiredFromLoadPath (symbol, a field of const object agentMemoryLongTermStoreRemoved))
  • content/docs/releases/v17/17-6.mdx (via RETIRED_KEYS_BY_MAJOR (symbol, a top-level const object))

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
  • 5 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json, packages/spec/liveness/agent.json, packages/spec/liveness/state-counts/agent.md, …) — pages documenting those are invisible to this run
  • 11 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 138 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 5e58193338ac4c09591b8dfb3bb7a95a8fea2ae8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e05a381f4f4bcfdb72e93cd349d1c2789c2b79b1 — the merge of head 39412feb9e0ee9e899b1c6b27ae2c82b2ff58674 into base 5e58193338ac4c09591b8dfb3bb7a95a8fea2ae8, 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 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

⚠️ 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 5e58193338ac4c09591b8dfb3bb7a95a8fea2ae8 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 39412feb9e0ee9e899b1c6b27ae2c82b2ff58674
Local-runs: none

PR #21413 @ 39412feb9e (card #20274, ruling of record 5950198150 letter A′, claim 5950482165, amendment 5951618837, dev report 5953769101). Read: the card body and all 28 comments, the PR body and its one bot comment, the 19-file list, the net diff against main (+1066 / −56), the check-runs on the head, and the tree at the head and at the objectui pin (git show / git grep, nothing built or run). One read outside the listed inputs, disclosed: the state of issue #21288, the carrier the dev named for the skills finding (③ below).

Check-runs on 39412feb9e, read at 2026-10-02T13:59Z (the latest reading before posting): 34 runs — 28 success, 2 skipped by design (Console Pin Gate is path-filtered to console changes; Packed-tarball smoke is opt-in), 4 in_progress. Green: Build Core, Check Changeset, Lint & Repo Gates, Spec property liveness, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace, Temporal Conformance (live PG + MySQL), Dogfood Regression Gate (3/3 plus the rollup), Dogfood Verify CLI, Governed Surface Queue Guard, the three claim/card guards, Flag docs affected by code changes, Build Docs, Check Documentation Links, Check PR Size, Test Core 2 and 4 of 6. ⚠ Still in_progress at that reading: Test Core 1, 3, 5 and 6 of 6. An in-progress gate is a reading, not a pass: this record's PASS is rendered on ①②③ below and does not stand in for those four; the queue rule still wants every check green before landing, and a red among them re-opens the head.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged against ruling A′ and the spec-property-retirement playbook.

  1. Accept set narrowed exactly to the ruling — right. checkAgentMemoryContract (a superRefine on memory, the sibling-correct level since reflectionInterval is longTerm's sibling) refuses, each as one custom issue at the key's own path: longTerm.enabled: true without maxEntries; the same without reflectionInterval (the { enabled: true, maxEntries: 5 } declaration cloud refused at turn time); reflectionInterval with longTerm absent, empty or disabled. No default is declared for either number. longTerm.enabled keeps .default(false) (the ruling: "unchanged"). { enabled: false, maxEntries: 5 } stays accepted — the ruling is silent and cloud's reader treats it as no policy, so this is not a narrowing beyond the ruling. The check runs only on a body the shape accepted, so a tombstone or type fault yields one complaint, not two (pinned). .superRefine() on the ZodObject keeps .shape, so AgentSchema.shape.memory, the handle cloud's reader re-parses with, carries the contract.
  2. longTerm.store retired as a WHOLE key, not a one-value enum — right (the ruling's explicit "not taken: A"). retiredKey() inside the live longTerm block: tsc input never and the parse prescription. The three old aliases backend / storage / provider moved to guidance with the same answer — required, since an alias may not target a tombstone (alias-integrity). Prescription conventions (playbook §2, five points): fully-qualified key first in backticks ✓; "was removed in @objectstack/spec 17.7.0 (ADR-0049 enforce-or-remove)" — 17.7.0 is the next minor from the published 17.6.0 and is the same spelling the structuredOutput-members tombstone in the same file already uses ✓; the why-clause ✓; "Delete the key" plus the mechanism that now governs ✓; the standard os migrate meta --from 17 sentence ✓. The two refinement messages carry no migrate sentence — correct, because the D2 supplies neither number.
  3. ADR-0087 kit — right on all three obligations. D2 agent-memory-long-term-store-removed: toMajor 18, retiredFromLoadPath, retiredAfter: '17.6.0' = packages/spec/package.json at the head (the playbook's rule; the structuredOutput precedent uses the same value), stripKeys copy-on-write (idempotent by construction), expectedNotices: 3 = three agents carrying the key, fixture covers the honoured database, the materialized vector default and the refused redis plus two untouched controls. Placed in MAJOR_18_CONVERSIONS at order 56 (unique in the array) at its alphabetical slot. D3 agent-memory-store-retired-and-limits-required: one entry per family, non-empty reason and acceptanceCriteria, conversionIds names the D2, entry file plus the generated semantic:18 region, STEP18_RATIONALE fragment order 61 (unique) at its id's sort position. Registration ai/Agent:memory.longTerm.store under 18 follows five existing nested registrations (api/RestApiConfig:documentation.enabled et al.) and the entry file name follows their 18.category__Def__a.b.c.ts form.
  4. Four ratchets byte-identical — right, not a missed regeneration. authorable-surface/ai.json at the head carries only top-level rows (ai/Agent:memory, ai/Agent:tools [RETIRED]); a nested key of an inline block has no row, so no [RETIRED] line can appear. The published json-schema.manifest lists defs, not shapes. spec-changes.json and the upgrade guide stay unchanged because step 18 is not yet projected (PROTOCOL_MAJOR 17), the same state as the structuredOutput precedent.
  5. Liveness ledger — right. memory experimental → live, evidenceScope: cross-repo, verifiedAt: 2026-10-02 = the date of the cloud seat's reading 5946891697, every pointer quoted from that reading and attributed in the note, the store window stated rather than smoothed. No producer field is owed: the runtime reads the authored numbers directly (README table: "the author IS the producer"). The tombstoned nested key needs no row of its own — ledger rows are per top-level key, and Spec property liveness is green (no UNCLASSIFIED, no ORPHAN). state-counts/agent.md regenerated 22/2 → 23/1. One discipline note, not a defect: the ai-service.ts pointer carries no #symbol anchor because the attested reading named none; the next re-attestation should pin one.
  6. Lint witnesses repointed to tool.outputSchema — right. liveness/tool.json at the head still has outputSchema experimental (per 5943158888), and it is the only agent-family experimental row without a queued retirement (agent.lifecycle has one).
  7. Describes and form help texts — right. memory drops [EXPERIMENTAL — not enforced] in the guardrails / structuredOutput wording; each child states what the runtime does. agent.form.ts planning "(1–100, default 10)" matches maxIterations: int().min(1).max(100).default(10) at the head; memory names only declared keys. The four catalog leaves agree with the source; the zh-CN / ja-JP / es-ES leaves are faithful translations, not copies.
  8. Published JSON Schema cannot carry the rule — recorded, right. A value-conditioned requirement is outside the closed projection list (build-schemas.ts "stays dropped and annotated", [finding] the published JSON Schema is WIDER than the zod schema it is generated from wherever a .refine() carries the rule — an author validating against packages/spec/json-schema/** gets a green for metadata the runtime refuses #18670; dependent-required is presence-only), so the memory site is annotated dropped and dropped-refinements.baseline.json gains the 5 sites (635 → 640). The changeset states the consequence. The regenerated reference page omits the never-typed store from longTerm's rendered type, which is the generator's rendering (check:generated and Build Core green).
  9. Public surface and the pinned sibling — right. Agent input type: memory.longTerm.store becomes never (a tsc break for any author who wrote it) — declared BREAKING. Output: store no longer materializes. At the pin .objectui-sha = 89cad75d55, the only reader is packages/app-shell/src/views/metadata-admin/previews/AgentPreview.tsx:211, (memory.longTerm as any)?.store, display-only through as any: no build break, so the path-filtered skip of Console Pin Gate loses nothing (AGENTS.md checklist item 4 satisfied).
  10. Residual authorings — none. At the head, outside packages/spec, longTerm / reflectionInterval appear only in the regenerated reference page and the four catalog leaves. The drift bot's metadata-lifecycle.mdx hit is MetadataCache.maxEntries, unrelated; content/docs/ai/agents.mdx authors no memory block. The tree-scoped absence walk (agent-memory-store-retirement.test.ts, repo project, declared radius = the five roots cross-package-test-inputs.mjs already declares for spec) judges the authoring shape, has an anti-vacuity case, and excludes only the kit with a reason each.
  11. Claim and amendment honoured. Branch = claude/issue-20274-agent-memory-contract; Part of #20274, not Fixes (guard green). agent.form.ts was edited only after feat(spec): offer agent.structuredOutput on the agent form and drop its stale not-enforced-yet ledger row #21398 landed: ca0dfb658a (feat(spec): offer agent.structuredOutput on the agent form and drop its stale not-enforced-yet ledger row #21398) is an ancestor of the head. No skills/**, no cloud, no governed path in the 19 files (Governed Surface Queue Guard green). The diff reviewed is GitHub's net diff against the merge base; the branch is behind main at 5e58193338.

② Semver level

  • @objectstack/spec: minor with feat(spec)!:, a BREAKING banner, the FROM → TO table and the one-line fix — correct. AGENTS.md: Clause-②: yes takes at least minor and (narrowing) is BREAKING; the playbook and check-changeset-no-major forbid major in the launch window, so BREAKING rides the banner. The retirement kit paragraph is present as the playbook asks.
  • @objectstack/platform-objects: patch — right for the four regenerated/authored catalog leaves added by amendment 5951618837.
  • @objectstack/lint: test-only change, no changeset owed. content/docs regenerated page: none owed.
  • Clause-②: line: Clause-②: yes (narrowing) sits at a line start in the PR body and in the changeset body — matches what the diff publishes (an accept-set narrowing plus a key removal; nothing widens).
  • ADR-0087 disposition: exactly one marker comment in the changeset, registered naming agent-memory-long-term-store-removed and agent-memory-store-retired-and-limits-required, both ids introduced by this diff (R4-clean) and both resolving in the registries. Check Changeset and Lint & Repo Gates (the CI authority for check:adr-0087-registration and check-changeset-no-major) are both green on the head; the dev's local run read the same disposition, [BREAKING+bang+clause-②-narrowing] registered ….

③ Boundary flags

Dev report 5953769101: open_questions: []; five deviations; four out_of_scope_findings. PR body: five Acceptance notes. Each answered or escalated:

  • Surface widened (new repo-project test + vitest.repo-tests.json, dropped-refinements.baseline.json, the lint test): accepted. Each is mechanical and gate-driven (the walk reads outside the package so it must be repo-project; the build refuses an unrecorded refinement site; the ledger flip invalidates the lint witnesses). The claim's hard ⛔s held.
  • acceptRetiredDefaultResidue deliberately not adopted for the old vector default: accepted, with the condition stated. The helper is a ruled exception (feat(spec): retired-defaulted-key tolerance — the retired default parses as inert residue and strips; non-default values keep the loud refusal (#12497 class rule) #12840) for a MEASURED residue population reaching a door that replays no D2 — the connector's in-code registerConnector def, the 75-occurrence permission artifact. Here the measured population is zero (in-repo producers 0 with the guardrails lit control at 4; cloud built-ins 0 per the ruling record), tenants are unmeasured and ruling A′ ② names the prescription as their backstop, and the data-at-rest seams replay the D2 (stored row and boot door both pinned). Adopting the stage without a ruling would have been the deviation. Condition: if a residue population is ever measured, the remedy is this helper under its own ruling, as feat(spec): retired-defaulted-key tolerance — the retired default parses as inert residue and strips; non-default values keep the loud refusal (#12497 class rule) #12840 was — never a silent adoption.
  • platform-objects patch bump: accepted (② above).
  • Commit-trailer form / one --workspace-concurrency ordering slip: process notes with no bearing on the contract; the slip touched nothing in the diff.
  • ESCALATED — skills/objectstack-ai/SKILL.md:311-313 ("memory is declared only — no runtime reads it") is false once this lands. The dev's named carrier is "the skills seat that took the guardrails half of the same sentence", i.e. finding(skills, docs): published AI-author text still describes guardrails as unenforced and outputSchema as never validated; the cloud AI runtime now enforces both (objectstack-ai SKILL.md, actions.mdx) #21288. Read: finding(skills, docs): published AI-author text still describes guardrails as unenforced and outputSchema as never validated; the cloud AI runtime now enforces both (objectstack-ai SKILL.md, actions.mdx) #21288 is closed, and its brief kept "memory is still unenforced" and "memory and lifecycle stay described as unenforced" — so no open card holds the memory half. The PR correctly cannot carry it (⛔ skills/** in the claim, Tier H). The adopting seat owes a card to domain:skills (or a rider on an open skills card) naming that line, before or at landing. This is a disposition gap in the report, not a defect in the diff; it does not move the verdict.
  • objectui AgentPreview.tsx:205-216 renders long.store and short.maxMessages, both retired, so both cells are now always empty: escalated as a one-line objectui follow-up candidate; display-only, no pin break, non-blocking.
  • refinementSitesThatDidProject 369 vs this tree's 436: pre-existing and not ratcheted by the build (check:generated green); non-blocking, noted for whoever next touches the baseline.
  • liveness/README.md agent row "autonomy tier experimental": now true of lifecycle alone; the two precedent flips left it too. Non-blocking; the lifecycle retirement should rewrite the clause.
  • "removed in @objectstack/spec 17.7.0" assumes the next release is a minor: matches the structuredOutput-members tombstone in the same file and the no-major rule; accepted.
  • check:dts-closure race and the six NOT-MEASURED build-driving repo files: declared to CI; Build Core, Lint & Repo Gates and all three Type Check gates are green on the head; Test Core shards 1, 3, 5 and 6 were still running at the reading above.

Implemented-by: claude/issue-20274-agent-memory-contract
Reviewed-by: session_01YDt3PzwfrkuFzUBF89WPmM

VERDICT: PASS


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
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:ai size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants