Skip to content

Release: merge development into beta - #865

Open
github-actions[bot] wants to merge 8 commits into
betafrom
development
Open

github-actions[bot] wants to merge 8 commits into
betafrom
development

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

github-actions Bot and others added 3 commits September 13, 2026 18:43
…260913184323

chore(sync): carry beta back into development
…API and frontend (#870)

* feat(session): declare the session schema with every property it has to carry

First of the five session specs. Inert by design: nothing reads these schemas yet, so
this is safe to merge on its own.

Session and SessionTurn take over every property of Conversation and Message, copied
verbatim from the LIVE schemas rather than transcribed, because the migration compares
field for field and a property on one side and not the other drops data silently:

  conversation -> title, userId, agentId, metadata, talkRoomToken, participants,
                  talkRoomOrigin
  message      -> role, content, sources, context, authorId, authorDisplayName

Session also gains `triggerOrigin` (human|cron|event|flow, default human). The Chat page
splits human sessions from automated ones on it, so without it every scheduled run turns
up in somebody's chat list. Every property now carries a title AND a description, because
the Skills page sources its header tooltips from descriptions and a schema shipped
without them cannot grow that later.

THE SLUG STAYS `agentsession`, AND THE GATE IS WHY. Task 1.2 says to stop if slug
`session` resolves to another app's schema. Measured: it returns scholiq's id 336, "a
scheduled occurrence of a Cohort meeting", and `hermiq/session` is not found at all. But
hermiq's session schema already exists as `agentsession`, and this register already
prefixes for exactly this reason (agentskill, agentbudget, agentwebhook, agentaifeature).
Declaring the bare slug would walk into the collision; the prefix avoids it, and the
chain's goal is one vocabulary in the UI, the API and the code, none of which a storage
slug is.

Conversation and Message stay declared and untouched. Nothing is removed until the
migration is verified in production.

VERIFIED BY IMPORT, not by absence of an error. A schema that fails to import VANISHES
and the failure is only logged, so this was confirmed to EXIST afterwards: forced the
re-import locally, then read the schemas back from the instance.

  agentsession     v0.2.0, 10 properties, triggerOrigin default human, enum present
  agentsessionturn v0.2.0, 9 properties

Demo data gains the split the spec asks for: two human sessions and one cron, with real
titles instead of the "Voorbeeld Title 1/2/3" placeholders, so a fresh install has
something in each group to render.

Recorded in design.md for the migration spec to start from: the machine-derived property
lists, and two measurements it needs. The archived-read path does NOT work
(`_includeDeleted=true` moves the total 1 -> 10 and still returns 1 row, the flag reaches
the count query only), so the migration must read through
`ObjectEntityMapper::findDeletedAcrossAllMagicTables()` or silently leave 9 of 10
conversations behind. And the real baseline on this instance is 10 conversations (1 live,
9 archived) and 7 messages, not the 282 the spec assumes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(session): migrate conversations onto the session schema, and prove it

Second of the five session specs, and the only one that can lose data. Copy semantics:
sources are never modified or deleted, so rollback is deleting the copies. Nothing reads
sessions yet, so this is still safe to merge alone.

Measured on this instance, 10 conversations and 7 messages, not the 282 the spec assumes:

  run 1  sessions: 10 copied, 0 already present, 0 failed
         turns:     7 copied, 0 already present, 0 failed
  run 2  sessions:  0 copied, 10 already present, 0 failed
         turns:     0 copied,  7 already present, 0 failed

  conversation 10 (9 archived) -> agentsession     10 (9 archived)
  message       7 (6 archived) -> agentsessionturn  7 (6 archived)

Verifier clean, 0 mismatches. Sources compared field for field against a pre-migration
snapshot: 0 modified. Rollback exercised four times.

FOUR DEFECTS IN THIS STEP, ALL SILENT, ALL FOUND BY VERIFYING RATHER THAN BY THE COUNTS.

1. `ObjectService::find()` returned the SOURCE conversation when asked for a session with
   the same uuid. The one live conversation was reported "already present" and never
   migrated, while nine archived ones were. The step now checks the ANSWER's schema, and
   reads through the mapper with register and schema given explicitly.

2. `saveObject()` ignores `@self.deleted`. All nine archived conversations arrived LIVE,
   with no error: the chat list would have filled with threads the user had archived and
   the Archive tab would have been empty. That is the exact symptom the spec's task 3.4
   names.

3. It also stamps `_owner` from the acting identity and re-dates `_created`/`_updated` to
   now, so a March thread claimed to be written today and every row landed as
   `__system__` rather than `admin`. Impersonating the owner around the call was tried and
   did not take.

   (2) and (3) are why `carryProvenance()` copies owner, organisation, created, updated
   and the archive marker as columns afterwards. That is the one place this step leaves
   the object API, and it is deliberate: the spec requires those to survive EXACTLY, and
   soft-deleting the copy instead would rewrite `deletedAt`/`deletedBy` to today and to
   this migration, losing when and by whom something was really archived.

4. `ObjectService::find()` excludes soft-deleted rows, so on a re-run every archived
   session read as absent and was rewritten. The second run reported "8 copied, 2 already
   present" when all ten existed, which is the overwrite the idempotency guarantee
   forbids, because it would undo an edit made through the UI afterwards. The existence
   check now passes `includeDeleted: true`.

Two more traps worth naming. `RegisterMapper` has no `findBySlug()` — its `find()` takes
`string|int` and resolves either — and calling the method that does not exist threw, was
caught, and became a silent "could not resolve" that read as "no filter needed". And the
entities `findDeletedAcrossAllMagicTables()` returns do not carry `deleted` hydrated even
though the query selects `*`, so the marker is read from the column rather than the
entity.

The archived read is the whole reason the step is shaped this way: `_includeDeleted=true`
moves the reported total from 1 to 10 and still returns 1 row, because the flag reaches
the count query only. Reading through `ObjectService` alone migrates 1 of 10.

`verify.sql` ships with the change. Its positive control was run first, as the spec
requires: un-archiving one migrated object and changing its title made it report exactly
that uuid and nothing else. A verifier that has never failed is not evidence.

`RegisterScopedSchema` is extracted rather than inlined: resolving a slug inside one
register is the thing this whole chain works around, and `session` and `agent` both
resolve to another app's schema globally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(session): move the API and every reader onto the session schema

Third of the five session specs, and the breaking one. `/api/sessions/*` is the surface
now; `/api/conversations/*` stays as a deprecated alias so no existing integration 404s
on deploy.

THE SPEC READS AS THOUGH THE CONTROLLER IS THE READER. IT IS NOT. 14 files reference the
`conversation` schema: the Talk bridge (TalkRoomBinding, TalkSessionRoom, TalkTurnService,
TalkBotInvokeListener), the engine (Engine, ConversationManagementHandler,
ConversationTitleWriter, MessageHistoryHandler), AssistantService,
ContextAgentInteractionService, ScheduleService and three controllers. Moving only the
controller would have split the app in half: the UI writing sessions while Talk, the
engine and the scheduler kept reading conversations, with neither half erroring. They all
move together.

15 schema constants and 10 field sites, each checked in context rather than swept:

  - `CONVERSATION_SCHEMA`/`MESSAGE_SCHEMA` values -> agentsession/agentsessionturn (15)
  - filters and object writes keyed `conversationId` -> `sessionId` (9)
  - one LITERAL `setSchema('message')` that the constants would have missed
  - `deleteRelatedObjects()` now picks the key per schema: a turn points at its session
    through `sessionId`, Feedback still uses `conversationId` because feedback objects are
    not part of this rename. Filtering both on one key matches nothing for one of them,
    and here that means deleting a session and orphaning its turns.

Deliberately NOT changed: request params, response keys and log context that happen to be
spelled `conversation`/`message`. Those are the API contract the frontend reads, and the
frontend does not move until the next spec. `serializeMessage()` bridges it — reads the
stored `sessionId`, emits `sessionId` AND `conversationId` — so the Chat page keeps
working today and has something to move onto tomorrow.

TWO RUNTIME FAILURES THAT ONLY CALLING THE ROUTES COULD FIND.

Renaming the class left the aliases pointing at `conversation#index`. That is not a
startup failure: the app loaded, `/api/sessions` answered 200, and every
`/api/conversations/*` path threw `Could not resolve ConversationController` as a runtime
500. This is exactly what task 4.1 warns about, and it happened anyway.

Then, once repointed, POST returned 405: a Nextcloud route name is
`app.controller.action`, so both families registering `session#create` collided silently.
The aliases carry `'postfix' => 'legacy'`, which is the supported way to point two paths
at one method — and they ARE one method, not copies, so the two families cannot drift.

All 16 routes across both families verified at 200, the whole archive lifecycle on each.
The deprecation log fires, so retirement can be decided on traffic rather than optimism.

TALK WAS ENABLED FOR THIS, AND IT MATTERED. `occ app:enable spreed` failed on a missing
`vendor/`, so the four Talk room-ownership e2e tests had been skipping wherever spreed is
absent, which is CI and was this instance. They run now, and all four pass — the first
time they have ever executed in this repo. Moving the Talk bridge to a new schema with
those tests still skipping would have been unverifiable.

1904 unit tests pass. 14 fixtures encoded the old vocabulary and were updated; each was
read in context first, because a fixture named `'message'` may be a schema slug or a
request parameter and only one of those moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(session): finish the frontend rename, and split human from automated

The API and the schema moved to "session" in the previous two commits; this
moves what a user reads and fixes the six defects the rename was blocked on.

## The split needed a backend change first

`triggerOrigin` was never serialised. Grouping the list on a field the API does
not return renders identically whether the split works or matches nothing, which
is the failure the spec warns about, so the serialiser now returns it. A session
stored before the property existed reports `human` rather than nothing: every one
of them was started by a person, and a session in neither group has disappeared.

## The six defects

- The new-session control was a no-op with no thread open, so it read as broken.
  It now moves focus to the start surface, which is a change the user can see.
- The start surface centred its children ON its own scroll container. Once the
  content is taller than the column that pushes the first row above the scroll
  origin, where scrolling cannot reach it. Measured at a 420px window: the
  heading sat at -85.5px and the first agent card at -1px, and `scrollTop` will
  not go below 0. Centring now lives on an inner block whose auto margins
  collapse to 0 when the content overflows.
- Rows show an origin icon, the agent's name and the time, not a bare date.
- The single archive button is now a menu: Continue, Archive, Delete. The
  permanent-delete endpoint never required an archived session, so Delete works
  from both tabs; the modal's docblock said "archived" and now does not.
- Active splits into "Started by you" and "Started automatically". An
  unrecognised origin falls to the human group on purpose: treating anything
  that is not `human` as automated would hide a real chat behind a heading
  nobody thinks to look under the first time a new origin ships.

## The stored title was still the old word

The stream endpoint wrote the literal title "New conversation", which the list
renders verbatim. It now writes "New session", and the placeholder detector
accepts both: recognising only the new spelling would leave every existing row
permanently unnamed, and nothing about such a row looks different when it does.

## Catalogues

Dutch gained 36 entries and lost the 31 the rename left dead. Three chip labels
that had shipped untranslated are translated. Four user-facing strings lost their
em-dashes, per the writing skill's rule 8.

## Tests

`chat.spec.ts` asserts the split in both directions, because two presence
assertions alone pass a filter that lets everything through. Three other e2e
specs asserted strings or a schema path this rename moved; one of them
(`chat-conversation-row`) had already been dead since the testid was renamed.
The migration's `@spec` tags pointed at a spec file that did not exist, and its
container lookups declared their optionality only through the shape of each
catch; both are fixed.

* chore(session): tag the renamed chat methods with their spec, and satisfy phpcs on touched lines

Gate 16 found 22 changed frontend methods without @SPEC; each now names the
session-surface requirement it implements. The three legacy route aliases that
ran past 150 characters are split like their neighbours (the routes array is
unchanged), and the touched test lines use named arguments.

* chore(session): tag the delete modal watcher and name the logger mock argument

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… kept (#872)

* docs(openspec): the hermiq umbrella for competitor parity wave 3

Records where the five wave-3 changes come from: cluster 46 of the round 4 competitor
sweep plus C-intake-30, and decisions D13, D6, D21 and D17. Also records what hermiq
already ships against the cluster, and why the command language stays with
openregister. States no requirement of its own, so it carries skip_specs.

* docs(openspec): a provider and a place per AI feature

Cluster 46, C-integrations-38 (must) and C-configuration-75. Joins tenant-model-policy
to ai-feature-governance: a feature binds its own provider inside the policy ceiling, a
provider declares its residency, a run outside a required residency is refused before
the call, and the run records which model saw what and where.

* docs(openspec): what the model reads and what is kept

Cluster 46, C-access-and-privacy-3 and -53, both must. A feature may require filinq's
redaction and fails closed without it; a detection never counts as a redaction. Every
run carries a retention, a job enforces it and reports that it ran, and deletion
removes the payload while leaving the audit chain whole.

* docs(openspec): the declared tool surface and the prompt library

Cluster 46, C-integrations-19 and C-configuration-44. Adds the outbound direction
beside agent-tool-governance: an outside agent calls declared tools under its own
principal's rights, with a per-registration grant and an output allowlist. Makes the
assistant prompts administered objects with a one-act kill switch.

* docs(openspec): a conversational intake that files for the citizen

Cluster 46, C-intake-9, C-communication-64 and C-integrations-1, none with a driven
passer. A surface separate from the tool-free case assistant, with a create-only grant,
a classification that can abstain, no dead ends, one conversation across channels, and
a deterministic escalation signal preferred over a model one.

* docs(openspec): identical reports collapse into one

C-intake-30, a must the sweep puts in dossiq's cluster 35. hermiq answers whether two
reports describe one event, in three bands, additively and reversibly, with readable
reasons. dossiq decides what a group means. Grouping never reduces the confirmations of
receipt owed under Awb 4:3a.
…stry (#875)

* docs(openspec): adopt the connection registry

* feat(connections): declare six outside connections for integriq's registry

* feat(connections): an Integrations page over integriq's app_connection rows

* feat(connections): report the AI provider, web search, GitHub store and webhook delivery to integriq

* test(e2e): the Integrations page against integriq's registry, written and not run
… must keep (#877)

The port of feat/retire-bespoke-github-sync onto OpenRegister's
FederatedConfigService was redone against development and stopped: every
store operation would lose behaviour that 22 later commits added, and most
of it can only be restored inside OpenRegister.

This adds wire-level tests over a recording broker and a fake GitHub for
that behaviour, each watched failing with its guard broken, and an OpenSpec
change recording the eight gaps. No production code changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant