diff --git a/docs/specs/agent-environments/state-writes/spec.md b/docs/specs/agent-environments/state-writes/spec.md index 08ed9d4939..6a0642e0b0 100644 --- a/docs/specs/agent-environments/state-writes/spec.md +++ b/docs/specs/agent-environments/state-writes/spec.md @@ -270,7 +270,8 @@ A create in a collection held under more than one spelling (`acme1` and `%61cme1`) goes to the one the call spells verbatim, as item calls choose their entity; a collection keeps its spelling after its last entity is deleted. A reset or load knows the spellings of the Evidence's state and of the state it -installs, never those of a state it replaced. `data/add` refuses a document that `data/get` would write back past the +installs, never those of a state it replaced; when two spellings fit a call +equally, the one holding entities wins. `data/add` refuses a document that `data/get` would write back past the 32 MiB intake limit once it adds the members a document may omit, such as `types` and the state Evidence id. diff --git a/docs/specs/agent-environments/state-writes/verification.md b/docs/specs/agent-environments/state-writes/verification.md index 871fbff47a..9b5f687c9d 100644 --- a/docs/specs/agent-environments/state-writes/verification.md +++ b/docs/specs/agent-environments/state-writes/verification.md @@ -253,3 +253,8 @@ collection spellings, so a create in a collection the loaded state does not hold keeps its recorded spelling. Each new test fails on the previous head. The verdicts over the 50 captures are unchanged plan by plan, the smoke applies all 31 served write tools, and the gates passed again. + +A ninth round made a create prefer a collection the state holds over an equally +fitting spelling known only from the Evidence, so a create after a load lands in +the loaded collection and reads back. Its test fails on the previous head; the +verdicts and the smoke are unchanged and the gates passed again. diff --git a/src/application/environment/environmentStateStore.ts b/src/application/environment/environmentStateStore.ts index 10f4c0b80b..9807f256ab 100644 --- a/src/application/environment/environmentStateStore.ts +++ b/src/application/environment/environmentStateStore.ts @@ -288,9 +288,14 @@ export class EnvironmentStateStore { return this.#typeCollections.get(type) ?? []; } - /** Every collection spelling a type's entities have been held under since the last reset or load, including ones that have since emptied. */ + /** Every collection spelling a type's entities have been held under since the last reset or load, or the Evidence's state holds: those holding entities first. */ knownCollectionsOf(type: string): readonly string[] { - return this.#knownCollections.get(type) ?? []; + return [ + ...new Set([ + ...this.collectionsOf(type), + ...(this.#knownCollections.get(type) ?? []), + ]), + ]; } document(tool: string): HeldDocument | undefined { diff --git a/src/application/environment/environmentStateWriteAnswer.test.ts b/src/application/environment/environmentStateWriteAnswer.test.ts index 382c375d06..c331ad43b5 100644 --- a/src/application/environment/environmentStateWriteAnswer.test.ts +++ b/src/application/environment/environmentStateWriteAnswer.test.ts @@ -567,6 +567,30 @@ describe("values the state started with", () => { expect(create("my%20org1", "Epsilon")).toBe("/api/orgs/my%20org1/books"); }); + it("creates in the loaded state's collection when the Evidence spells the same one differently", () => { + const { books, create } = twoOrgs("%61cme1", "acme1"); + const { rea_state: held } = books.store.snapshot(); + expect( + books.store.replace({ + rea_state: { + version: held.version, + entities: held.entities.map((entity) => + entity.collection.includes("%61cme1") + ? { + ...entity, + collection: entity.collection.replace("%61cme1", "a%63me1"), + record_ordinals: [], + } + : entity, + ), + documents: held.documents, + }, + }).ok, + ).toBe(true); + // Both spellings decode to the argument; the one holding books wins over the Evidence's emptied one. + expect(create("acme1", "Epsilon")).toBe("/api/orgs/a%63me1/books"); + }); + it("forgets the spellings of a loaded state once reset", () => { const { books, create } = twoOrgs("%61cme1", "acme1"); const { rea_state: held } = books.store.snapshot(); diff --git a/src/application/environment/environmentStateWriteAnswer.ts b/src/application/environment/environmentStateWriteAnswer.ts index 1bea1b2cfe..fa4d74acd0 100644 --- a/src/application/environment/environmentStateWriteAnswer.ts +++ b/src/application/environment/environmentStateWriteAnswer.ts @@ -246,7 +246,7 @@ const createdCollection = ( ): string | undefined => { const segments = callSegments(request.tool, request.args); if (segments === undefined) return undefined; - // The spelling the call supplies verbatim wins, as item calls choose their entity; an emptied collection keeps its spelling. + // The spelling the call supplies verbatim wins, as item calls choose their entity, then the first known: held ones lead. let held: string | undefined; let verbatim = -1; for (const collection of serving.store.knownCollectionsOf(