Skip to content

Commit 4435664

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21293-single-series-measure-arity
2 parents b3b3c51 + 39a912e commit 4435664

108 files changed

Lines changed: 8149 additions & 470 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@objectstack/metadata-protocol': minor
5+
'@objectstack/trigger-api': minor
6+
'@objectstack/runtime': minor
7+
---
8+
9+
feat(automation): a flow's credentials live in a write-only channel, not in its stored definition (#20790)
10+
11+
Clause-②: yes (widening)
12+
13+
A flow's two credentials, an inbound hook's `secret` on its start node and an `http` node's `signingSecret`, are no longer stored in the flow definition. The metadata save door moves each explicit value into a new platform object, `sys_flow_credential`, owned by `@objectstack/service-automation`. Its one field is `type: 'secret'`, so the engine encrypts it through the host crypto provider, masks it on every read, and dereferences it only through `resolveSecretField`. This is the same seam the webhook signing secret uses. The stored row, every new version-history row and the row's content hash carry no credential. The engine reads the value only when it verifies an inbound post or signs an outbound request. Authoring does not change: you still write the literal, a save that leaves the key out (the form every read serves) keeps the stored secret, `''` clears it, and only an explicit new value rotates it.
14+
15+
**⚠️ Rotate every inbound and outbound flow secret that existed before this release.** On the first boot with a crypto provider, or when a provider registers after a boot without one, each stored flow that still carries a credential is moved into the channel once, and the log prints one notice per flow: `[Automation] flow '<name>' (<state>): … was stored in cleartext … ROTATE: …`. The move guarantees no new copy, but the version-history rows and audit snapshots written before it stay as they were (both are append-only), so an administrator could have read those values. To rotate, save the flow with a new `config.secret` / `config.signingSecret`, then give the new value to whoever signs posts to the hook or verifies its deliveries. The run is recorded in `sys_migration` as `flow-credential-channel` (flow names only, never values). Packaged flows are not moved: a packaged flow's literal stays its source of truth, and where the channel holds a row for it, the row wins at verification.
16+
17+
What else changes:
18+
19+
- **`@objectstack/spec`**: `PLATFORM_OBJECTS_BY_PACKAGE['service-automation']` lists `sys_flow_credential`.
20+
- **`@objectstack/metadata-protocol`**: `registerCredentialChannel(type, channel)` registers a type's write-only credential channel (exported type `MetadataCredentialChannel`). `saveMetaItem` stores the body the channel returns, after the carry-forward and before the put. The runtime authoring gate reads the channel's held positions as present, on an active save and when a draft is published. `SysMetadataRepository.restoreVersion` takes `deriveRestoredBody`, shaped like `promoteDraft`'s `deriveActiveBody`. Rollback and revert pass the channel's strip, so restoring a version written before the move never puts its credential back at rest, and the channel keeps its current credential.
21+
- **`@objectstack/service-automation`**: exports `SysFlowCredential`, `FlowCredentialChannel` and `migrateFlowCredentialsIntoChannel`. `AutomationEngine` gains `setFlowCredentialSource`, `holdsFlowCredential`, `resolveFlowCredential` and `flowCredentialHoldings`. An `api` binding carries `resolveSecret()`, which reads the secret at verification time, so a rotation applies to the next post. A draft save never rotates the live secret; publishing the draft promotes it. Deleting a flow's stored row drops its credentials.
22+
- **`@objectstack/trigger-api`**: `FlowTriggerBinding.resolveSecret` arms a hook without a literal. A post whose secret cannot be read is answered `503 SERVICE_UNAVAILABLE` and is never verified against nothing.
23+
- **Refused now, loudly**:
24+
- With no crypto provider, a save that carries a flow credential is refused with `503 SERVICE_UNAVAILABLE` before anything is written. Register a provider (`setCryptoProvider`) and save again.
25+
- The clone door (`POST /api/v1/automation/:name/clone`) refuses a source that holds a credential, as a literal or in the channel, with `409 RESOURCE_CONFLICT`, because a copy would share it. ⚠️ Accepted cost: a packaged inbound flow can no longer be cloned in one step. Author the copy as a new flow under a new name, with its own secret.
26+
27+
<!-- adr-0087: not-required (no-migration-prescription) the one-time move rewrites stored flow rows through the metadata save door itself, at boot; no authorable key, spelling, export or stored shape is retired, so an author or an upgrading agent has nothing to rewrite. The operator's action is the rotation stated above, which is not a FROM to TO mapping. The gate reads this changeset as non-breaking; the disposition is stated for the migration the ruling named. -->
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/service-analytics': patch
4+
'@objectstack/formula': patch
5+
'@objectstack/objectql': patch
6+
---
7+
8+
Published comments that named `driver-memory`'s retired reference matcher as a live filter backend now name what replaced it
9+
10+
Clause-②: no
11+
12+
`driver-memory`'s reference matcher (`memory-matcher.ts`) was retired in commit `8fec76a2b`. Four published packages still described it as a live surface in text that ships:
13+
14+
- `@objectstack/spec`:
15+
- The backend table in the filter-logic conformance docblock, which ships in `data/index.d.ts` and `data/index.d.mts`, now lists the in-memory backend as `driver-memory`'s query path (`normalizeFilterCondition`, then mingo) where it listed `memory-matcher`, and says the matcher held that row until commit `8fec76a2b` retired it.
16+
- `src/data/filter.zod.ts` ships as source. In it, the `$icontains` implementation table lists `driver-memory`'s query path and analytics face, both on `asciiCaseInsensitiveRegexSource`. The `$like` / `$ilike` and `$empty` tables keep the matcher only in a note that commit `8fec76a2b` retired it. The `foldAsciiCase` docblock counts five JS evaluation faces where it counted six. The `asciiCaseInsensitiveContains` docblock names objectql's `having` and `formula` as its callers. The string-ordering note says `driver-memory`'s query path hands the comparison to mingo. Of these, the `foldAsciiCase`, `asciiCaseInsensitiveContains` and `FILTER_OPERATORS` docblocks also ship in the filter declaration chunk (`filter.zod-*.d.ts` / `.d.mts`).
17+
- `src/ui/view.zod.ts` ships as source. It now says that `driver-memory`'s query path runs `assertFilterConditionShape` through `convertToMongoQuery`, where it said `match()` did.
18+
- A comment inside `FILTER_TEXT_CASES` ships in `data/index.js` / `.mjs` and `browser/data/index.js` / `.mjs`. It now says the reference matcher measured case-exact until commit `8fec76a2b` retired it.
19+
- `@objectstack/service-analytics`: two comments in `ObjectQLStrategy`, which ship in the JavaScript output (the first also in `index.d.ts` / `index.d.cts`), changed. The first names `driver-memory`'s query path, not its matcher, as a face that pins `{$not: {}}` as the zero-row filter. The second says in the past tense that `memory-matcher.ts` read `$regex` as a real regex, until `$regex` was retired and commit `8fec76a2b` retired the matcher too.
20+
- `@objectstack/formula`: the comment over the `$icontains` arm in `matches-filter.ts` ships in `index.js` / `index.mjs`. It now names objectql's `having` as the other caller of `asciiCaseInsensitiveContains`. It says `driver-memory`'s reference matcher called it until commit `8fec76a2b` retired it, and that `driver-memory`'s query path folds through `asciiCaseInsensitiveRegexSource`.
21+
- `@objectstack/objectql`: the comment over the `having` walker's `$notContains` arm in `having-filter.ts` ships in `index.js` / `index.mjs` and `core.js` / `core.mjs`. It now says the record-at-a-time faces (`formula` and this walker) answer the predicate on a stored value that is not a string, as `driver-memory`'s reference matcher did until commit `8fec76a2b` retired it.
22+
23+
Comment only: no export, type, error code, status, message text or runtime behaviour changes.
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.

0 commit comments

Comments
 (0)