Skip to content

Commit f58f6bd

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21388-withheld-only-activity-row
2 parents f784d21 + bdd3654 commit f58f6bd

35 files changed

Lines changed: 1775 additions & 201 deletions
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
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`, enforced by the cloud AI runtime (#20274)
7+
8+
**BREAKING** — `agent.memory` narrows to what the cloud AI runtime, the one runtime
9+
that executes agents, actually does with it. That runtime recalls the newest
10+
`maxEntries` distilled notes for the user before the first round, writes one note
11+
every `reflectionInterval` delivered interactions, evicts notes beyond `maxEntries`,
12+
and keeps them in its own database store. Before an agent's first turn it refused
13+
exactly the declarations this spec still accepted, so authoring now refuses them,
14+
by name, with a prescription (ADR-0049 enforce-or-remove):
15+
16+
- **`longTerm.maxEntries` and `reflectionInterval` are required when
17+
`longTerm.enabled` is true.** No default is declared for either: none has a
18+
measured basis, and the runtime adds none.
19+
- **`reflectionInterval` is refused without an enabled `longTerm`** — a reflection
20+
writes a long-term note, so with none enabled it would do nothing.
21+
- **`longTerm.store` is retired as a whole key.** The memory store is platform
22+
infrastructure, not agent metadata: the runtime keeps the notes in its own
23+
database store, and refused `vector` (the key's default, so what an omitted
24+
`store` parsed to) and `redis`. Its old spellings `backend`, `storage` and
25+
`provider` under `longTerm` are answered with the same prescription instead of
26+
being steered onto `store`.
27+
28+
`longTerm.enabled` is unchanged.
29+
30+
### FROM → TO
31+
32+
| before | what to write instead |
33+
| --- | --- |
34+
| `memory.longTerm.store` — any value, `database` included | delete the key; where the notes are kept is the platform's choice. |
35+
| `longTerm: { enabled: true, … }` without `maxEntries` | add `maxEntries`: how many distilled notes are kept for each user (an integer of at least 1). |
36+
| `longTerm: { enabled: true, … }` without `memory.reflectionInterval` | add `reflectionInterval`: how many delivered interactions pass between the reflections that write a note (an integer of at least 1). |
37+
| `memory.reflectionInterval` without `longTerm.enabled: true` | enable long-term memory with both numbers, or delete `reflectionInterval`. |
38+
39+
**The one-line fix: declare `maxEntries` and `reflectionInterval` when `longTerm.enabled`; delete `store`.**
40+
`os migrate meta --from 17` lists the mechanical edits for existing sources (the
41+
`store` deletion); the two numbers are the author's to choose.
42+
43+
Each refusal is a parse error at the key's own path, naming the key and the fix, and
44+
`store` also fails `tsc` (its input type is `never`).
45+
46+
### The retirement kit
47+
48+
- **Tombstone.** `longTerm.store` is a `retiredKey()` carrying the prescription; the
49+
three old alias spellings moved from `aliases` to `guidance`, because an alias may
50+
not steer an author onto a tombstone.
51+
- **The contract check** is a refinement on `memory` (`reflectionInterval` is
52+
`longTerm`'s sibling), one `custom` issue per missing or misplaced key. A JSON
53+
Schema cannot state a value-conditioned requirement in the closed projection list,
54+
so the published `ai/Agent` schema (and the four installed-package schemas that
55+
embed agents) names the site in `x-dropped-refinements`, recorded in
56+
`dropped-refinements.baseline.json`.
57+
- **D2 conversion `agent-memory-long-term-store-removed`** (step 18, retired from the
58+
load path): it deletes `store` from `memory.longTerm`, whatever it holds — the
59+
delete is lossless, because no value of it ever chose a backend. Stored
60+
`sys_metadata` agent rows and built artifacts replay it; one notice per agent. It
61+
supplies neither number.
62+
- **D3 entry `agent-memory-store-retired-and-limits-required`** carries the judgement
63+
the conversion cannot make: the two numbers an enabled `longTerm` now requires.
64+
- **`RETIRED_KEYS_BY_MAJOR[18]`** registers `ai/Agent:memory.longTerm.store`.
65+
- **No deprecation window**, per the project's startup-stage posture.
66+
67+
### Describes and the liveness ledger
68+
69+
- `agent.memory` drops `[EXPERIMENTAL — not enforced]`: it states that the cloud AI
70+
runtime enforces it and that the open framework edition does not run agents.
71+
`longTerm`, `enabled`, `maxEntries` and `reflectionInterval` each state what the
72+
runtime does with them.
73+
- The ledger row moves `experimental` → `live`, citing the cloud reader
74+
`agent-runtime.ts#compileAgentMemory` (via `AgentRuntime.resolveTurnGuardrails`),
75+
the enforcement in `ai-service.ts` and the store `agent-memory.ts#AgentMemoryStore`,
76+
as attested by the cloud seat's reading at cloud `ef5a4344`, `verifiedAt`
77+
2026-10-02. `os lint` / `os validate` no longer warn
78+
`liveness-experimental-property` on an agent that sets `memory`.
79+
- ⚠️ **The window, stated.** At `ef5a4344` the cloud reader still reads `store`: it
80+
honours `database` only and refuses `vector` and `redis`. Cloud drops `store` in
81+
that one reader once this release reaches its pin, and no earlier.
82+
83+
### The agent form's help texts
84+
85+
- The `memory` row's help text on the agent metadata form named short-term memory,
86+
a key the schema refuses. It now states what memory does and that `maxEntries`
87+
and `reflectionInterval` are required once long-term memory is enabled.
88+
- The neighbouring `planning` row named a strategy and a replan switch the schema
89+
does not declare; it now states the one key it has, the iteration cap.
90+
- The `platform-objects` metadata-form catalogs follow: the English leaves are
91+
regenerated, and the `zh-CN`, `ja-JP` and `es-ES` leaves are authored, not copied.
92+
93+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is
94+
published, and tenant-authored agents were not measured. This repo authors no
95+
`longTerm` outside `packages/spec`, and no cloud built-in agent declares one.
96+
97+
Clause-②: yes (narrowing)
98+
99+
<!-- adr-0087: registered agent-memory-long-term-store-removed, agent-memory-store-retired-and-limits-required -->
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
The list-view field-reference rule no longer walks a list view's own `tabs[].filter`
6+
7+
Clause-②: no
8+
9+
The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape, and this rule judges the parsed stack, so the key could never reach the walk: the parse refuses it first, with its prescription. The dead branch is deleted. The rule still judges `filter` and `userFilters.tabs[].filter` exactly as before.
10+
11+
No finding changes for any stack that `os validate`, `os lint` or `os build` accepts.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
`computeViewReferenceDiagnostics` no longer walks a list view's own `tabs[].filter`
6+
7+
Clause-②: no
8+
9+
The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape. The write door refuses it, and a stored or artifact-shipped body has it stripped by the conversion replay before it is served, so the read could never see it. A served body that still carries it is already badged by the spec diagnostics (`computeMetadataDiagnostics`), with the tombstone's prescription. The `userFilters.tabs[].filter`, `filterableFields` and `kanban` checks are unchanged.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Liveness ledger: the view container's body `name` row stays `dead`, and its note now states what the platform actually does with the key
6+
7+
Clause-②: no
8+
9+
- The old note said the body copy was "a copy nobody reads". Measured, the metadata door stamps the save name into every saved view body that has none, containers included (`normalizeViewMetadata` in `@objectstack/metadata-protocol`). Its overlay paths key on that stamped copy: `hydrateOverlayIntoRegistry` registers no body without a `name`, and `mergePackageAwareOverlay` slots an overlay row by it.
10+
- The verdict is unchanged, because the ledger's `live` means that authoring the key changes runtime behaviour. An authored container `name` only restates the key the container already registers under, or contradicts it. `os validate` and `os lint` keep warning `liveness-dead-property` ("drop it").
11+
- The note records why the key is kept rather than tombstoned: the door's own saves stamp it, so a tombstone would refuse the platform's own writes. A maintainer ruling also refused a spec-level forbid of a container's `name`.
12+
- It corrects the old attribution too. Artifact-shipped containers and the metadata-validation sweep author no `name`; what was read as theirs is the door's stamp.
13+
- The ledger README's `view` cell says the same. The `view.list.tabs` row's note now records that the two author-time walks that still read a list view's own `tabs` are deleted.
14+
- A comment in `system/i18n-resolver.ts` that still called the list view's own `tabs` a live carrier now says the key is a tombstone and `UserFiltersSchema.tabs` is the one carrier.
15+
- ⛔ No schema, parse, export, status or accept-set change.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
fix(lint)!: `os validate`, `os build` and `os lint` refuse an `analyticsCubes` dimension over a JSON-stored column, and a cube `count_distinct` measure over one, which the analytics door already refuses at query time
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a refusal at authoring of two cube member TARGETS the analytics door already refuses with 400 INVALID_FIELD at query time: an analyticsCubes dimension whose sql column is declared structured-JSON (json, composite, repeater, record, location, address, vector) or multi-value (multiselect, checkboxes, tags, or a select, radio, lookup, user, file or image declared multiple: true), and a count_distinct measure whose sql column is either. It is the cube face of the dataset dimension refusal, which declared this category for the same door. No authorable key, spelling, export or stored shape moves: CubeSchema keeps parsing every member, no stored row is read or rewritten, and which scalar part of a document, or which member of a list, an author meant to group on or count is not something a ledger entry can rewrite. The other categories are closed on facts: the package publishes (not unpublished); dataset-measure-aggregate-field-type-refused scopes itself to DatasetMeasureSchema rows and no ADR-0087 id covers a cube member (not already-registered); and the change is a rule verdict, not a declaration (not runtime-interface-only or type-surface-only). -->
10+
11+
**BREAKING**: metadata that passed `os validate`, `os build` and `os lint` can now fail. An authored analytics cube (`defineStack({ analyticsCubes })`) is queried through the same analytics door as a compiled dataset, and that door refuses a query that groups by a JSON-stored column, or counts its distinct values, with `400 INVALID_FIELD` before any SQL is built. So such a member could be declared but never served, and until now no authoring rule read `analyticsCubes` at all. The dataset rule's two ids now judge cube members as well: `dimension-json-stored-field-refused` and `measure-aggregate-field-type-refused` (gating, `error`). It ships as `minor` under the launch-window convention for accept-set narrowings. No export is added or removed.
12+
13+
**What is refused.** On a cube whose `sql` names an object the stack defines: a `dimensions` entry whose `sql` column is declared with a structured-JSON type (`json`, `composite`, `repeater`, `record`, `location`, `address`, `vector`) or a multi-value declaration (`multiselect`, `checkboxes`, `tags`, or a `select`, `radio`, `lookup`, `user`, `file` or `image` declared `multiple: true`); and a `measures` entry of `type: 'count_distinct'` whose `sql` column is either. The column is the member's `sql`: a column of the cube's object, or a relationship path read on the object its last hop reaches (the join the cube declares for that hop, else the lookup field's `reference`). The classes are `@objectstack/spec/data`'s `STRUCTURED_JSON_TYPES`, `isMultiValueField` and the `count_distinct` row of `AGGREGATE_FIELD_TYPE_COMPATIBILITY`, the predicates the door reads.
14+
15+
**What an author sees now.** The finding names the cube, the member, the column, the object that declares it and its declaration, and says the analytics door refuses it with `400 INVALID_FIELD`. It names the route: group by, or count the distinct values of, a field that stores one scalar value; for a multi-value field, filter by one member with `$contains` in a record query. It is located at `analyticsCubes[N].dimensions.KEY.sql` or `analyticsCubes[N].measures.KEY.type`, where `KEY` is the member's key.
16+
17+
**Unchanged.** Every dataset finding, word for word. A cube member over any other column, a single-value `select` or `lookup` included; a `count` measure, and a `sum`, `avg`, `min` or `max` measure, which this check does not judge; the row wildcard `'*'`; a member whose column does not resolve or declares no type; a cube whose `sql` names no object this stack defines. The runtime metadata write door: no authoring rule is dispatched for an `analytics_cube` save, and a `dataset` save's snapshot carries no cubes.

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,18 @@ above declared `multiple: true`), is refused
232232
Group by a field that stores one value instead. Like the measure check, it
233233
stays silent when the field's type cannot be resolved.
234234

235+
Both checks also judge the members of an analytics cube (`analyticsCubes`),
236+
because an authored cube is queried through the same analytics door. A cube
237+
dimension whose `sql` column is declared with a structured-JSON type or as a
238+
multi-value field is refused (`dimension-json-stored-field-refused`) at
239+
`analyticsCubes[N].dimensions.<key>.sql`, and so is a `count_distinct` measure
240+
over such a column (`measure-aggregate-field-type-refused`, at
241+
`analyticsCubes[N].measures.<key>.type`). The column is read where the door
242+
reads it: on the object the cube's `sql` names, or, for a relationship path,
243+
on the object the last hop reaches — the join the cube declares for that hop,
244+
else the lookup's `reference`. A cube's other measure types and the row
245+
wildcard `'*'` are not judged by this check.
246+
235247
### 7. Navigation exposing objects nobody can read
236248

237249
Navigation and permissions are separate metadata, each valid on its own — so an

‎content/docs/references/ai/agent.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const result = AIModelConfigSchema.parse(data);
5858
| **access** | `string[]` | optional | Who can chat with this agent |
5959
| **permissions** | `string[]` | optional | Required permission-set capabilities |
6060
| **planning** | `{ maxIterations: integer }` | optional | Autonomous reasoning and planning configuration |
61-
| **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | [EXPERIMENTAL — not enforced] Agent memory management. Parsed but no runtime consumer yet. |
61+
| **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | Agent memory (long-term notes recalled before each conversation and written by periodic reflection), enforced by the cloud AI runtime; the open framework edition does not run agents. |
6262
| **guardrails** | `{ maxTokensPerInvocation?: integer; maxExecutionTimeSec?: integer; blockedTopics?: string[] }` | optional | Safety guardrails for the agent (token budget, time limit, blocked topics), enforced per user turn by the cloud AI runtime; the open framework edition does not run agents. |
6363
| **structuredOutput** | `{ format: Enum<'json_object' \| 'json_schema'>; schema?: Record<string, any>; strict: boolean; retryOnValidationFailure: boolean; … }` | optional | Structured output contract for the agent's final answer (JSON format, schema, retries, fallback format, transform steps), enforced on every final answer by the cloud AI runtime; the open framework edition does not run agents. |
6464
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this agent. |
@@ -101,8 +101,8 @@ const result = AIModelConfigSchema.parse(data);
101101

102102
| Property | Type | Required | Description |
103103
| :--- | :--- | :--- | :--- |
104-
| **longTerm** | `{ enabled: boolean; store: Enum<'vector' \| 'database' \| 'redis'>; maxEntries?: integer }` | optional | Long-term / persistent memory |
105-
| **reflectionInterval** | `integer` | optional | Reflect every N interactions to improve behavior |
104+
| **longTerm** | `{ enabled: boolean; maxEntries?: integer }` | optional | Long-term memory: distilled notes kept per user and agent and recalled before each conversation |
105+
| **reflectionInterval** | `integer` | optional | Reflect every N delivered interactions: each reflection writes one distilled note to long-term memory. Required when longTerm.enabled is true, and refused without it |
106106

107107
### Nested Shape: `Agent.guardrails`
108108

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,12 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
718718
// over the shipped dataset corpus before crossing, at the door's own
719719
// snapshot shape: 11 datasets (platform-objects 5, showcase 4, crm 1,
720720
// todo 1) — 0 findings, with a lit synthetic probe refused.
721+
//
722+
// [#21082] The rule also walks `analyticsCubes` (the cube leg, same two
723+
// ids). That leg is CLI-only by construction, not by a narrowing here: a
724+
// `dataset` write's snapshot carries no `analyticsCubes` (the context
725+
// collections are `RuntimeStackContext`'s), so it reads nothing at this
726+
// door, and an `analytics_cube` write has no `TYPE_TO_STACK_KEY` row.
721727
surfaces: CLI_AND_RUNTIME,
722728
runtimeTypes: ['dataset'],
723729
run: (stack) => validateDatasetMeasureAggregates(stack),

0 commit comments

Comments
 (0)