diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0fe272b..c281906 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,13 @@ Phase 0/1 of the QQQ Assistant: read-only Q&A and navigation. Requires QQQ 4.1 w
- the AI tool kernel (`qqq-backend-core` `actions.ai`, QRun-IO/qqq#1078)
- the developer-panel contract (QRun-IO/qqq#1076).
+### Changed (0.1.0-RC.2)
+- **Snake_case names** (QRun-IO naming rule, 2026-10-12). Metadata only; the physical tables were already `assistant_*`:
+ - tables `assistantConversation` ... `assistantUsageDaily` are now `assistant_conversation`, `assistant_message`, `assistant_turn`, `assistant_turn_event`, `assistant_tool_call`, `assistant_feedback`, `assistant_policy` and `assistant_usage_daily`; the conversation possible-value source follows the table name
+ - permissions are unchanged: `assistant.hasAccess` keeps QQQ's standard `hasAccess` suffix, and `assistant.admin` stays as is
+ - security key type `assistantUserId` is now `assistant_user_id`, with all-access key `assistant_user_id_all_access`
+ - the old names remain as `@Deprecated` `LEGACY_*` constants until 0.2.0. See [UPGRADE.md](UPGRADE.md) for the stored references to update (permissions, saved views and reports, `audit_table` names).
+
### Added
- **`qbit-assistant`**
- `AssistantQBitProducer` and `AssistantQBitConfig`:
@@ -17,7 +24,7 @@ Phase 0/1 of the QQQ Assistant: read-only Q&A and navigation. Requires QQQ 4.1 w
- budgets, rate limits
- retention of 90 / 30 / 365 days
- owner key type and pricing.
- - Standard, visible tables: `assistantConversation`, `assistantMessage`, `assistantTurn`, `assistantTurnEvent`, `assistantToolCall`, `assistantFeedback`, `assistantPolicy` and `assistantUsageDaily`. User-owned tables are locked to their owner (`assistantUserId`), and admins read everything through `assistantUserIdAllAccess`.
+ - Standard, visible tables: `assistant_conversation`, `assistant_message`, `assistant_turn`, `assistant_turn_event`, `assistant_tool_call`, `assistant_feedback`, `assistant_policy` and `assistant_usage_daily`. User-owned tables are locked to their owner (`assistant_user_id`), and admins read everything through `assistant_user_id_all_access`.
- The Assistant Admin app, gated by `assistant.admin`.
- The `assistantRetention` process.
- `/qqq/v1/assistant` routes: config probe, conversations CRUD, turns as Server-Sent Events, replay through `/events?after=`, cancel, and feedback. The panel requires `assistant.hasAccess`. The kill switch and budgets are checked when a turn starts, and the kill switch again before every step.
diff --git a/CLAUDE.md b/CLAUDE.md
index 79d3b6b..9b9f991 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -32,7 +32,7 @@ Always use the wrapper: Maven 3.10 cannot read the parent POM. The committed def
- `qbit-assistant` (`com.kingsrook.qbits.assistant`):
- `AssistantQBitProducer`, `AssistantQBitConfig` (with `AssistantModelRouting` and `AssistantBudgets`), `AssistantRuntime`, `AssistantSecurity`.
- `metadata/`: tables, owner key type, the Assistant Admin app, the retention process, and the `assistant.admin` custom permission checker.
- - `model/`: entities for the `assistant*` tables.
+ - `model/`: entities for the `assistant_*` tables.
- `persistence/AssistantStore`: the **only** code that calls QQQ data actions, and only on the QBit's own tables, under the user's session.
- `engine/`: `TurnEngine` (the loop), `ContextBuilder` (cache-prefix order), `SystemPrompt`, `PolicyService` (policy row, budgets), `RateLimiter`, the `TurnEvent` / `TurnEventLog` stream, and `MessageParts`.
- `api/`: `AssistantApi` (transport-free) and `AssistantRouteProvider` (Javalin, SSE).
diff --git a/README.md b/README.md
index 6681503..c0a7168 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ The data access itself is done by the AI tool kernel in QQQ core (`com.kingsrook
```java
AssistantQBitConfig config = new AssistantQBitConfig()
- .withBackendName("rdbms") // where the assistant* tables live
+ .withBackendName("rdbms") // where the assistant_* tables live
.withTableMetaDataCustomizer(snakeCaseCustomizer) // optional: backend names for your schema
.withModelProvider(AnthropicModelProvider.fromEnvironment()); // key from ANTHROPIC_API_KEY
@@ -44,7 +44,7 @@ new AssistantQBitProducer().withQBitConfig(config).produce(qInstance);
`produce` does the following:
- Registers the QBit.
-- Adds the tables, the `assistantUserId` security key type, the `assistantRetention` process and the **Assistant Admin** app.
+- Adds the tables, the `assistant_user_id` security key type, the `assistantRetention` process and the **Assistant Admin** app.
- Builds the runtime.
- Registers the `/qqq/v1/assistant/*` routes when Javalin is on the classpath.
- Registers the record Developer-screen panel when QQQ core has the developer-panel contract.
@@ -64,7 +64,7 @@ The provider's `toString` never contains the key. Its errors carry only the HTTP
| Setting | Default | Meaning |
|---|---|---|
| `enabled` | `true` | Per-app switch. The environment variable `QQQ_ASSISTANT_ENABLED=false` also turns it off. |
-| `backendName` | (required) | Backend of the `assistant*` tables. |
+| `backendName` | (required) | Backend of the `assistant_*` tables. |
| `providerName` | `anthropic` | Which registered provider to use. |
| `models` | Sonnet 5.5 at effort `medium` | `mainModel`, `mainEffort`, `mainMaxOutputTokens` (16000). The escalation (Opus 5.5, `high`) and summary (Haiku 5.5) slots are reserved for phase 2. Model ids are the exact strings `claude-sonnet-5-5`, `claude-opus-5-5` and `claude-haiku-5-5`. |
| `defaultExposure` | `VALUES` | The kernel's default exposure. Use `METADATA_ONLY` for high-sensitivity apps. |
@@ -76,14 +76,14 @@ The provider's `toString` never contains the key. Its errors carry only the HTTP
| `maxTurnsPerUserPerMinute` / `maxConcurrentTurnsPerUser` | 10 / 1 | Rate limits. |
| `conversationRetentionDays` / `toolResultRetentionDays` / `approvalRetentionDays` | 90 / 30 / 365 | Used by `assistantRetention`. |
| `retentionSchedulerName` | none | Runs `assistantRetention` daily on that scheduler. |
-| `ownerSecurityKeyType` | `assistantUserId` | Key type of the owner record locks. |
+| `ownerSecurityKeyType` | `assistant_user_id` | Key type of the owner record locks. |
| `pricing` | Anthropic list prices, 2026-10 | USD per million tokens per model, used for cost and budgets. |
| `allowedWidgetNames` | none | Dashboard widgets `render_widget` may render. The kernel denies every widget not on this list. This config list is the **only** way to allow a widget: QQQ widget meta-data has no supplemental hook, so there is no per-widget "assistant-safe" flag. Allow only widgets that render the same data for every user who can see them. |
| `hiddenWidgetNames` | none | Widgets that are never rendered, even when allowed. |
| `customRedactionPatterns` | none | Extra regular expressions the kernel redacts from tool results. |
| `killSwitchPollMillis` | 2000 | How often a running turn re-checks the kill switch while it waits on the model. |
-At runtime, an admin can override the kill switch, the main model and effort, and the budgets in the single **`assistantPolicy`** row.
+At runtime, an admin can override the kill switch, the main model and effort, and the budgets in the single **`assistant_policy`** row.
A running turn notices the kill switch in two ways:
- It re-reads the switch before every model call and tool step.
@@ -104,9 +104,9 @@ When the QBit is enabled, it registers the supplemental instance meta-data `assi
### Record locks on the assistant's own tables
-Conversation data belongs to its user. Every user-owned table has a record lock on `userId` with key type `assistantUserId`:
+Conversation data belongs to its user. Every user-owned table has a record lock on `userId` with key type `assistant_user_id`:
- The panel routes add the user's own id to their session, so users only ever see their own rows.
-- Admins see everyone's rows in the Assistant Admin app through the key type's all-access key, `assistantUserIdAllAccess`. Grant that key to `assistant.admin` holders from your authentication customizer:
+- Admins see everyone's rows in the Assistant Admin app through the key type's all-access key, `assistant_user_id_all_access`. Grant that key to `assistant.admin` holders from your authentication customizer:
```java
// in your QAuthenticationModuleCustomizerInterface.customizeSession(...)
@@ -128,7 +128,7 @@ The kernel applies these rules to tool results:
- Exposure `METADATA_ONLY` and `MASKED` fields are filtered or masked.
- Rows are capped at 50 and cells at 500 characters.
- Secrets such as keys, JWTs and card numbers are redacted.
-- The `assistant*` tables are never visible to the model.
+- The `assistant_*` tables are never visible to the model.
Old tool results are cleared from the context of later turns; only one-line summaries remain.
@@ -178,16 +178,16 @@ All of these are standard, visible QQQ tables in the Assistant Admin app:
| Table | Holds |
|---|---|
-| `assistantConversation` | Conversations. |
-| `assistantMessage` | Messages (`partsJson`). |
-| `assistantTurn` | Turn accounting. |
-| `assistantTurnEvent` | Stream events, for replay. |
-| `assistantToolCall` | The audit of every tool call. |
-| `assistantFeedback` | Feedback on answers. |
-| `assistantPolicy` | The kill switch and overrides. |
-| `assistantUsageDaily` | Usage rollups: per user per day, plus an instance row whose `userId` is null. |
+| `assistant_conversation` | Conversations. |
+| `assistant_message` | Messages (`partsJson`). |
+| `assistant_turn` | Turn accounting. |
+| `assistant_turn_event` | Stream events, for replay. |
+| `assistant_tool_call` | The audit of every tool call. |
+| `assistant_feedback` | Feedback on answers. |
+| `assistant_policy` | The kill switch and overrides. |
+| `assistant_usage_daily` | Usage rollups: per user per day, plus an instance row whose `userId` is null. |
-Field names are camelCase. Map them to your schema with a table customizer. `docs/schema-postgres.sql` has a reference DDL.
+Table names follow the QRun-IO naming rule: lowercase snake_case with the `assistant_` prefix, the same as the physical tables in `docs/schema-postgres.sql` (the reference DDL). Field names are camelCase. Map them to your columns with a table customizer. Upgrading from 0.1.0-RC.1, which used camelCase table names and the `assistantUserId` key type: see [UPGRADE.md](UPGRADE.md).
## Build
diff --git a/UPGRADE.md b/UPGRADE.md
new file mode 100644
index 0000000..3e99f81
--- /dev/null
+++ b/UPGRADE.md
@@ -0,0 +1,93 @@
+# Upgrading qbit-assistant
+
+## 0.1.0-RC.1 to 0.1.0-RC.2: snake_case names
+
+RC.2 applies the QRun-IO naming rule (owner decision, 2026-10-12): *"table names should always be lowercase and use _, and qbits should always be prefixed with their name"*. The QQQ table names, the owner security key type and the possible-value source change. Permissions do not. **This is a metadata-only change**: no table, column or index in your database changes, and no Liquibase or DDL step is needed.
+
+### What changed
+
+| Kind | RC.1 name | RC.2 name |
+|---|---|---|
+| Table | `assistantConversation` | `assistant_conversation` |
+| Table | `assistantMessage` | `assistant_message` |
+| Table | `assistantTurn` | `assistant_turn` |
+| Table | `assistantTurnEvent` | `assistant_turn_event` |
+| Table | `assistantToolCall` | `assistant_tool_call` |
+| Table | `assistantFeedback` | `assistant_feedback` |
+| Table | `assistantPolicy` | `assistant_policy` |
+| Table | `assistantUsageDaily` | `assistant_usage_daily` |
+| Possible-value source (conversations) | `assistantConversation` | `assistant_conversation` |
+| Permissions | `assistant.hasAccess`, `assistant.admin` | unchanged (QQQ's standard `.hasAccess` suffix; the `assistant` base already follows the rule) |
+| Security key type | `assistantUserId` | `assistant_user_id` |
+| All-access key | `assistantUserIdAllAccess` | `assistant_user_id_all_access` |
+
+Unchanged: Java class names, field names (`userId`, `conversationId`, ...), the `assistantAdmin` app, the `assistantRetention` process, the `/qqq/v1/assistant` routes and the `assistant` supplemental instance meta-data.
+
+The old names stay available as `@Deprecated` constants for one release, so migration code can refer to them: `.LEGACY_TABLE_NAME`, `AssistantQBitConfig.LEGACY_DEFAULT_OWNER_KEY_TYPE` and `AssistantMetaDataBuilder.LEGACY_ALL_ACCESS_SUFFIX`. They will be removed in 0.2.0.
+
+### Physical tables
+
+`docs/schema-postgres.sql` already creates `assistant_*` tables. If your RDBMS backend mapped the old QQQ names to those tables (for example with `inferBackendName`), the new QQQ names now equal the physical names and nothing else is needed; the mapping becomes a no-op. If you created tables literally named `assistantConversation` and so on, either rename them:
+
+```sql
+ALTER TABLE "assistantConversation" RENAME TO assistant_conversation;
+ALTER TABLE "assistantMessage" RENAME TO assistant_message;
+ALTER TABLE "assistantTurn" RENAME TO assistant_turn;
+ALTER TABLE "assistantTurnEvent" RENAME TO assistant_turn_event;
+ALTER TABLE "assistantToolCall" RENAME TO assistant_tool_call;
+ALTER TABLE "assistantFeedback" RENAME TO assistant_feedback;
+ALTER TABLE "assistantPolicy" RENAME TO assistant_policy;
+ALTER TABLE "assistantUsageDaily" RENAME TO assistant_usage_daily;
+```
+
+or keep them and set `RDBMSTableBackendDetails.tableName` to the old name in your table customizer.
+
+### Stored references to update
+
+QQQ stores table names, permission names and key names outside this qbit. Update them in the same deployment. The examples are for Postgres with QQQ's default snake_case backend names; adjust the table and column names to your schema.
+
+1. **Security keys.** Permission grants (`assistant.hasAccess`, `assistant.admin`) need no change. If your authentication customizer grants the all-access key itself rather than calling `AssistantSecurity.applySessionKeys`, grant `assistant_user_id_all_access`. If you set `ownerSecurityKeyType` explicitly, the all-access key is now `_all_access`.
+
+2. **Saved views and saved reports** on the assistant tables:
+
+ ```sql
+ UPDATE saved_view SET table_name = CASE table_name
+ WHEN 'assistantConversation' THEN 'assistant_conversation'
+ WHEN 'assistantMessage' THEN 'assistant_message'
+ WHEN 'assistantTurn' THEN 'assistant_turn'
+ WHEN 'assistantTurnEvent' THEN 'assistant_turn_event'
+ WHEN 'assistantToolCall' THEN 'assistant_tool_call'
+ WHEN 'assistantFeedback' THEN 'assistant_feedback'
+ WHEN 'assistantPolicy' THEN 'assistant_policy'
+ WHEN 'assistantUsageDaily' THEN 'assistant_usage_daily'
+ ELSE table_name END
+ WHERE table_name IN ('assistantConversation', 'assistantMessage', 'assistantTurn', 'assistantTurnEvent',
+ 'assistantToolCall', 'assistantFeedback', 'assistantPolicy', 'assistantUsageDaily');
+ ```
+
+ Run the same statement against `saved_report` (and `quick_saved_view` / `shared_saved_view` only if they store a table name in your schema).
+
+3. **Audit table names.** QQQ audits reference tables through the `audit_table` lookup. Rename the rows so old and new audits stay under one table:
+
+ ```sql
+ UPDATE audit_table SET name = CASE name
+ WHEN 'assistantConversation' THEN 'assistant_conversation'
+ WHEN 'assistantMessage' THEN 'assistant_message'
+ WHEN 'assistantTurn' THEN 'assistant_turn'
+ WHEN 'assistantTurnEvent' THEN 'assistant_turn_event'
+ WHEN 'assistantToolCall' THEN 'assistant_tool_call'
+ WHEN 'assistantFeedback' THEN 'assistant_feedback'
+ WHEN 'assistantPolicy' THEN 'assistant_policy'
+ WHEN 'assistantUsageDaily' THEN 'assistant_usage_daily'
+ ELSE name END
+ WHERE name IN ('assistantConversation', 'assistantMessage', 'assistantTurn', 'assistantTurnEvent',
+ 'assistantToolCall', 'assistantFeedback', 'assistantPolicy', 'assistantUsageDaily');
+ ```
+
+ If a new-name row was already created (the app ran RC.2 before this update), repoint `audit.audit_table_id` from the old row to the new one first, then delete the old row.
+
+4. **Anything else that stores a table name**: frontend links or bookmarks to `/app/.../assistantConversation`, Quick Search index rows (`quick_search_index.table_name`; reindex instead of editing), ESB topics or reports you configured by table name.
+
+### Order
+
+Deploy RC.2 and run the statements above in the same maintenance window. Until the update runs, saved views and reports on the assistant tables do not open, and admins who get the all-access key from their own customizer only see their own rows.
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/AssistantQBitConfig.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/AssistantQBitConfig.java
index 4f1bb32..e6abe75 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/AssistantQBitConfig.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/AssistantQBitConfig.java
@@ -38,7 +38,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Configuration of the Assistant QBit (spec 2.3).
**
- ** Required: backendName (where the assistant* tables live) and at least one
+ ** Required: backendName (where the assistant_* tables live) and at least one
** model provider (withModelProvider), named by providerName. API keys never
** go here: a provider reads its key from the environment or from a value the
** host passes in from its secret store.
@@ -50,11 +50,17 @@ public class AssistantQBitConfig implements QBitConfig
{
public static final String ACCESS_PERMISSION = "assistant.hasAccess";
public static final String ADMIN_PERMISSION = "assistant.admin";
- public static final String DEFAULT_OWNER_KEY_TYPE = "assistantUserId";
+ public static final String DEFAULT_OWNER_KEY_TYPE = "assistant_user_id";
public static final String ENABLED_ENV_VAR = "QQQ_ASSISTANT_ENABLED";
public static final String DEFAULT_SHORTCUT = "mod+j";
public static final String DEFAULT_PROVIDER_NAME = "anthropic";
+ /*******************************************************************************
+ ** The owner key type used before 0.1.0-RC.2 (camelCase). See UPGRADE.md.
+ *******************************************************************************/
+ @Deprecated(since = "0.1.0-RC.2")
+ public static final String LEGACY_DEFAULT_OWNER_KEY_TYPE = "assistantUserId";
+
private Boolean enabled = true;
private String backendName;
private String providerName = DEFAULT_PROVIDER_NAME;
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/AssistantSecurity.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/AssistantSecurity.java
index b20a627..7a96b13 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/AssistantSecurity.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/AssistantSecurity.java
@@ -24,7 +24,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
- ** Session helpers for the owner record locks on the assistant* tables.
+ ** Session helpers for the owner record locks on the assistant_* tables.
**
** The assistant never creates sessions. These methods only add security key
** values to the user's OWN session:
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/metadata/AssistantMetaDataBuilder.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/metadata/AssistantMetaDataBuilder.java
index d1943a3..bdf037b 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/metadata/AssistantMetaDataBuilder.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/metadata/AssistantMetaDataBuilder.java
@@ -59,22 +59,30 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
**
** Security:
** - Every user-owned table carries userId and a record security lock on it
- ** (key type ownerSecurityKeyType, default assistantUserId). The panel
+ ** (key type ownerSecurityKeyType, default assistant_user_id). The panel
** routes put the user's own id in their session, so reads and writes of
** these tables through QQQ actions only ever touch the user's own rows.
- ** - The key type's all-access key (assistantUserIdAllAccess) lets admins see
+ ** - The key type's all-access key (assistant_user_id_all_access) lets admins see
** everyone's rows; hosts grant it to users holding assistant.admin with
** AssistantSecurity.applySessionKeys in their authentication customizer.
** - Tables, app and process need assistant.admin in the generic UI / API.
- ** - assistantUsageDaily rows with a null userId are the INSTANCE rollup and
+ ** - assistant_usage_daily rows with a null userId are the INSTANCE rollup and
** are readable by any session (the lock allows null).
- ** - The assistant's own tool kernel never exposes assistant* tables to the
+ ** - The assistant's own tool kernel never exposes assistant_* tables to the
** model (exposure NONE by name).
*******************************************************************************/
public class AssistantMetaDataBuilder
{
public static final String APP_NAME = "assistantAdmin";
public static final String RETENTION_PROCESS_NAME = "assistantRetention";
+ public static final String ALL_ACCESS_SUFFIX = "_all_access";
+
+ /*******************************************************************************
+ ** The all-access key suffix before 0.1.0-RC.2 (assistant_user_id_all_access).
+ ** See UPGRADE.md.
+ *******************************************************************************/
+ @Deprecated(since = "0.1.0-RC.2")
+ public static final String LEGACY_ALL_ACCESS_SUFFIX = "AllAccess";
private final AssistantQBitConfig config;
@@ -95,7 +103,7 @@ public class AssistantMetaDataBuilder
*******************************************************************************/
public static String allAccessKeyName(String ownerKeyType)
{
- return (ownerKeyType + "AllAccess");
+ return (ownerKeyType + ALL_ACCESS_SUFFIX);
}
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantConversation.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantConversation.java
index d7d69ee..22c2e7d 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantConversation.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantConversation.java
@@ -30,7 +30,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior
*******************************************************************************/
public class AssistantConversation extends QRecordEntity
{
- public static final String TABLE_NAME = "assistantConversation";
+ public static final String TABLE_NAME = "assistant_conversation";
+
+ /*******************************************************************************
+ ** The table name before 0.1.0-RC.2 (camelCase). See UPGRADE.md.
+ *******************************************************************************/
+ @Deprecated(since = "0.1.0-RC.2")
+ public static final String LEGACY_TABLE_NAME = "assistantConversation";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantFeedback.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantFeedback.java
index 3ead7e7..ab8cf5a 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantFeedback.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantFeedback.java
@@ -30,7 +30,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior
*******************************************************************************/
public class AssistantFeedback extends QRecordEntity
{
- public static final String TABLE_NAME = "assistantFeedback";
+ public static final String TABLE_NAME = "assistant_feedback";
+
+ /*******************************************************************************
+ ** The table name before 0.1.0-RC.2 (camelCase). See UPGRADE.md.
+ *******************************************************************************/
+ @Deprecated(since = "0.1.0-RC.2")
+ public static final String LEGACY_TABLE_NAME = "assistantFeedback";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantMessage.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantMessage.java
index 8f76d15..e5f5d58 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantMessage.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantMessage.java
@@ -31,7 +31,13 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
*******************************************************************************/
public class AssistantMessage extends QRecordEntity
{
- public static final String TABLE_NAME = "assistantMessage";
+ public static final String TABLE_NAME = "assistant_message";
+
+ /*******************************************************************************
+ ** The table name before 0.1.0-RC.2 (camelCase). See UPGRADE.md.
+ *******************************************************************************/
+ @Deprecated(since = "0.1.0-RC.2")
+ public static final String LEGACY_TABLE_NAME = "assistantMessage";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantPolicy.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantPolicy.java
index c838fd5..43d1c2f 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantPolicy.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantPolicy.java
@@ -32,7 +32,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior
*******************************************************************************/
public class AssistantPolicy extends QRecordEntity
{
- public static final String TABLE_NAME = "assistantPolicy";
+ public static final String TABLE_NAME = "assistant_policy";
+
+ /*******************************************************************************
+ ** The table name before 0.1.0-RC.2 (camelCase). See UPGRADE.md.
+ *******************************************************************************/
+ @Deprecated(since = "0.1.0-RC.2")
+ public static final String LEGACY_TABLE_NAME = "assistantPolicy";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantToolCall.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantToolCall.java
index 63f9267..ed665b0 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantToolCall.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantToolCall.java
@@ -32,7 +32,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior
*******************************************************************************/
public class AssistantToolCall extends QRecordEntity
{
- public static final String TABLE_NAME = "assistantToolCall";
+ public static final String TABLE_NAME = "assistant_tool_call";
+
+ /*******************************************************************************
+ ** The table name before 0.1.0-RC.2 (camelCase). See UPGRADE.md.
+ *******************************************************************************/
+ @Deprecated(since = "0.1.0-RC.2")
+ public static final String LEGACY_TABLE_NAME = "assistantToolCall";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantTurn.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantTurn.java
index d6aece4..57ac68b 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantTurn.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantTurn.java
@@ -30,7 +30,13 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
*******************************************************************************/
public class AssistantTurn extends QRecordEntity
{
- public static final String TABLE_NAME = "assistantTurn";
+ public static final String TABLE_NAME = "assistant_turn";
+
+ /*******************************************************************************
+ ** The table name before 0.1.0-RC.2 (camelCase). See UPGRADE.md.
+ *******************************************************************************/
+ @Deprecated(since = "0.1.0-RC.2")
+ public static final String LEGACY_TABLE_NAME = "assistantTurn";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantTurnEvent.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantTurnEvent.java
index 32b84e3..786b8f4 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantTurnEvent.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantTurnEvent.java
@@ -30,7 +30,13 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
*******************************************************************************/
public class AssistantTurnEvent extends QRecordEntity
{
- public static final String TABLE_NAME = "assistantTurnEvent";
+ public static final String TABLE_NAME = "assistant_turn_event";
+
+ /*******************************************************************************
+ ** The table name before 0.1.0-RC.2 (camelCase). See UPGRADE.md.
+ *******************************************************************************/
+ @Deprecated(since = "0.1.0-RC.2")
+ public static final String LEGACY_TABLE_NAME = "assistantTurnEvent";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantUsageDaily.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantUsageDaily.java
index e9999a7..88cbd82 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantUsageDaily.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/model/AssistantUsageDaily.java
@@ -31,7 +31,13 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
*******************************************************************************/
public class AssistantUsageDaily extends QRecordEntity
{
- public static final String TABLE_NAME = "assistantUsageDaily";
+ public static final String TABLE_NAME = "assistant_usage_daily";
+
+ /*******************************************************************************
+ ** The table name before 0.1.0-RC.2 (camelCase). See UPGRADE.md.
+ *******************************************************************************/
+ @Deprecated(since = "0.1.0-RC.2")
+ public static final String LEGACY_TABLE_NAME = "assistantUsageDaily";
@QField(isEditable = false, isPrimaryKey = true)
private Integer id;
diff --git a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/persistence/AssistantStore.java b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/persistence/AssistantStore.java
index a5cebed..3014809 100644
--- a/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/persistence/AssistantStore.java
+++ b/qbit-assistant/src/main/java/com/kingsrook/qbits/assistant/persistence/AssistantStore.java
@@ -54,7 +54,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
- ** Reads and writes the QBit's own assistant* tables through QQQ actions,
+ ** Reads and writes the QBit's own assistant_* tables through QQQ actions,
** under whatever session is in QContext - always the requesting user's
** session (the record locks on userId then confine every call to the user's
** own rows). Queries also filter on userId explicitly, so an admin's
diff --git a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/ArchitectureTest.java b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/ArchitectureTest.java
index 5f6f59e..03b4a76 100644
--- a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/ArchitectureTest.java
+++ b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/ArchitectureTest.java
@@ -44,7 +44,7 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
** - no QSystemUserSession, no QSession construction (the assistant only
** ever uses the requesting user's own session);
** - QQQ data actions only from the persistence package (the QBit's own
- ** assistant* tables); everything else reaches data through the
+ ** assistant_* tables); everything else reaches data through the
** AiToolKernel.
*******************************************************************************/
class ArchitectureTest
diff --git a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/AssistantQBitProducerTest.java b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/AssistantQBitProducerTest.java
index a7650d0..d017696 100644
--- a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/AssistantQBitProducerTest.java
+++ b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/AssistantQBitProducerTest.java
@@ -62,7 +62,7 @@ class AssistantQBitProducerTest extends BaseAssistantTest
@Test
void testProducedMetaData()
{
- List owned = List.of("assistantConversation", "assistantMessage", "assistantTurn", "assistantTurnEvent", "assistantToolCall", "assistantFeedback", "assistantUsageDaily");
+ List owned = List.of("assistant_conversation", "assistant_message", "assistant_turn", "assistant_turn_event", "assistant_tool_call", "assistant_feedback", "assistant_usage_daily");
for(String name : owned)
{
QTableMetaData table = qInstance.getTable(name);
@@ -79,7 +79,7 @@ class AssistantQBitProducerTest extends BaseAssistantTest
assertThat(qInstance.getTable(AssistantPolicy.TABLE_NAME).getRecordSecurityLocks()).isNullOrEmpty();
QSecurityKeyType keyType = qInstance.getSecurityKeyType(AssistantQBitConfig.DEFAULT_OWNER_KEY_TYPE);
- assertThat(keyType.getAllAccessKeyName()).isEqualTo("assistantUserIdAllAccess");
+ assertThat(keyType.getAllAccessKeyName()).isEqualTo("assistant_user_id_all_access");
assertThat(qInstance.getApp(AssistantMetaDataBuilder.APP_NAME).getChildren()).hasSize(9);
assertThat(qInstance.getProcess(AssistantMetaDataBuilder.RETENTION_PROCESS_NAME)).isNotNull();
@@ -159,12 +159,12 @@ class AssistantQBitProducerTest extends BaseAssistantTest
AssistantSecurity.applySessionKeys(editor, config);
AssistantSecurity.applySessionKeys(editor, config);
assertThat(editor.getSecurityKeyValues(AssistantQBitConfig.DEFAULT_OWNER_KEY_TYPE)).containsExactly(AssistantTestFixture.EDITOR);
- assertThat(editor.getSecurityKeyValues("assistantUserIdAllAccess")).isNullOrEmpty();
+ assertThat(editor.getSecurityKeyValues("assistant_user_id_all_access")).isNullOrEmpty();
QSession admin = new QSession().withUser(new com.kingsrook.qqq.backend.core.model.session.QUser().withIdReference("boss"));
admin.withPermission(AssistantQBitConfig.ADMIN_PERMISSION);
AssistantSecurity.applySessionKeys(admin, config);
- assertThat(admin.getSecurityKeyValues("assistantUserIdAllAccess")).containsExactly(true);
+ assertThat(admin.getSecurityKeyValues("assistant_user_id_all_access")).containsExactly(true);
AssistantSecurity.applySessionKeys(null, config);
assertThat(AssistantSecurity.userId(new QSession())).isNull();
diff --git a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/NamingTest.java b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/NamingTest.java
new file mode 100644
index 0000000..8eb764b
--- /dev/null
+++ b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/NamingTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2026 Kingsrook, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kingsrook.qbits.assistant;
+
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import com.kingsrook.qbits.assistant.metadata.AssistantMetaDataBuilder;
+import com.kingsrook.qbits.assistant.model.AssistantConversation;
+import com.kingsrook.qbits.assistant.model.AssistantFeedback;
+import com.kingsrook.qbits.assistant.model.AssistantMessage;
+import com.kingsrook.qbits.assistant.model.AssistantPolicy;
+import com.kingsrook.qbits.assistant.model.AssistantToolCall;
+import com.kingsrook.qbits.assistant.model.AssistantTurn;
+import com.kingsrook.qbits.assistant.model.AssistantTurnEvent;
+import com.kingsrook.qbits.assistant.model.AssistantUsageDaily;
+import com.kingsrook.qbits.assistant.testing.BaseAssistantTest;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+/*******************************************************************************
+ ** Owner naming rule (2026-10-12): table names are lowercase snake_case and
+ ** prefixed with the qbit's name (assistant_). Pins the new names and the
+ ** deprecated pre-RC.2 names that UPGRADE.md tells hosts to migrate from.
+ *******************************************************************************/
+@SuppressWarnings("deprecation")
+class NamingTest extends BaseAssistantTest
+{
+ private static final String SNAKE_WITH_PREFIX = "^assistant_[a-z0-9]+(_[a-z0-9]+)*$";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static Map tableNames()
+ {
+ Map names = new LinkedHashMap<>();
+ names.put(AssistantConversation.TABLE_NAME, AssistantConversation.LEGACY_TABLE_NAME);
+ names.put(AssistantMessage.TABLE_NAME, AssistantMessage.LEGACY_TABLE_NAME);
+ names.put(AssistantTurn.TABLE_NAME, AssistantTurn.LEGACY_TABLE_NAME);
+ names.put(AssistantTurnEvent.TABLE_NAME, AssistantTurnEvent.LEGACY_TABLE_NAME);
+ names.put(AssistantToolCall.TABLE_NAME, AssistantToolCall.LEGACY_TABLE_NAME);
+ names.put(AssistantFeedback.TABLE_NAME, AssistantFeedback.LEGACY_TABLE_NAME);
+ names.put(AssistantPolicy.TABLE_NAME, AssistantPolicy.LEGACY_TABLE_NAME);
+ names.put(AssistantUsageDaily.TABLE_NAME, AssistantUsageDaily.LEGACY_TABLE_NAME);
+ return (names);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testTableNamesAreSnakeCaseWithPrefix()
+ {
+ assertThat(tableNames()).hasSize(8);
+ for(Map.Entry entry : tableNames().entrySet())
+ {
+ String name = entry.getKey();
+ assertThat(name).matches(SNAKE_WITH_PREFIX);
+ QTableMetaData table = qInstance.getTable(name);
+ assertThat(table).as(name).isNotNull();
+
+ /////////////////////////////////////////////////////////////
+ // the old camelCase names are no longer registered at all //
+ /////////////////////////////////////////////////////////////
+ assertThat(qInstance.getTable(entry.getValue())).as(entry.getValue()).isNull();
+ }
+
+ ////////////////////////////////////////////////////////////////
+ // every produced table follows the rule (none left behind) //
+ ////////////////////////////////////////////////////////////////
+ for(QTableMetaData table : qInstance.getTables().values())
+ {
+ if(table.getName().toLowerCase().startsWith("assistant"))
+ {
+ assertThat(table.getName()).matches(SNAKE_WITH_PREFIX);
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testLegacyNames()
+ {
+ assertThat(tableNames()).containsEntry("assistant_conversation", "assistantConversation")
+ .containsEntry("assistant_message", "assistantMessage")
+ .containsEntry("assistant_turn", "assistantTurn")
+ .containsEntry("assistant_turn_event", "assistantTurnEvent")
+ .containsEntry("assistant_tool_call", "assistantToolCall")
+ .containsEntry("assistant_feedback", "assistantFeedback")
+ .containsEntry("assistant_policy", "assistantPolicy")
+ .containsEntry("assistant_usage_daily", "assistantUsageDaily");
+
+ ///////////////////////////////////////////////////////////////////////
+ // permissions keep QQQ's standard .hasAccess suffix (unchanged) //
+ ///////////////////////////////////////////////////////////////////////
+ assertThat(AssistantQBitConfig.ACCESS_PERMISSION).isEqualTo("assistant.hasAccess");
+ assertThat(AssistantQBitConfig.ADMIN_PERMISSION).isEqualTo("assistant.admin");
+ assertThat(AssistantQBitConfig.DEFAULT_OWNER_KEY_TYPE).isEqualTo("assistant_user_id");
+ assertThat(AssistantQBitConfig.LEGACY_DEFAULT_OWNER_KEY_TYPE).isEqualTo("assistantUserId");
+ assertThat(AssistantMetaDataBuilder.allAccessKeyName(AssistantQBitConfig.DEFAULT_OWNER_KEY_TYPE)).isEqualTo("assistant_user_id_all_access");
+ assertThat(AssistantQBitConfig.LEGACY_DEFAULT_OWNER_KEY_TYPE + AssistantMetaDataBuilder.LEGACY_ALL_ACCESS_SUFFIX).isEqualTo("assistantUserIdAllAccess");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testPossibleValueSourceUsesTableName()
+ {
+ assertThat(qInstance.getPossibleValueSource("assistant_conversation")).isNotNull();
+ assertThat(qInstance.getPossibleValueSource("assistantConversation")).isNull();
+ assertThat(qInstance.getTable(AssistantMessage.TABLE_NAME).getField("conversationId").getPossibleValueSourceName()).isEqualTo("assistant_conversation");
+ }
+
+}
diff --git a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/evals/RedTeamTest.java b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/evals/RedTeamTest.java
index d26728e..714a38f 100644
--- a/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/evals/RedTeamTest.java
+++ b/qbit-assistant/src/test/java/com/kingsrook/qbits/assistant/evals/RedTeamTest.java
@@ -275,9 +275,9 @@ class RedTeamTest extends BaseAssistantTest
calls.add(new Object[] { "describe_table", map("table", "blog_post") });
calls.add(new Object[] { "search_records", map("term", "Proxmox") });
calls.add(new Object[] { "count_records", map("table", "blog_post", "filter", filter("AND", criterion("internalNotes", "EQUALS", HIDDEN_SENTINEL))) });
- calls.add(new Object[] { "query_records", map("table", "assistantConversation", "filter", filter("AND")) });
- calls.add(new Object[] { "query_records", map("table", "assistantToolCall", "filter", filter("AND")) });
- calls.add(new Object[] { "describe_table", map("table", "assistantMessage") });
+ calls.add(new Object[] { "query_records", map("table", "assistant_conversation", "filter", filter("AND")) });
+ calls.add(new Object[] { "query_records", map("table", "assistant_tool_call", "filter", filter("AND")) });
+ calls.add(new Object[] { "describe_table", map("table", "assistant_message") });
calls.add(new Object[] { "get_record_history", map("table", "blog_post", "id", "1") });
provider.thenToolUses(null, calls).thenText("done");
@@ -295,7 +295,7 @@ class RedTeamTest extends BaseAssistantTest
////////////////////////////////////////////////////////////////////
for(ModelRequest request : provider.getRequests())
{
- assertThat(JsonUtils.toJson(request.getSystem())).doesNotContain("assistantConversation").doesNotContain("assistantToolCall").doesNotContain("assistantMessage");
+ assertThat(JsonUtils.toJson(request.getSystem())).doesNotContain("assistant_conversation").doesNotContain("assistant_tool_call").doesNotContain("assistant_message");
for(ModelMessage message : request.getMessages())
{
for(ModelPart part : message.getParts())
@@ -308,9 +308,9 @@ class RedTeamTest extends BaseAssistantTest
}
}
List results = provider.getRequests().get(1).getMessages().get(2).getParts();
- assertThat(results.get(6).getIsError()).as("assistantConversation query").isTrue();
- assertThat(results.get(7).getIsError()).as("assistantToolCall query").isTrue();
- assertThat(results.get(8).getIsError()).as("assistantMessage describe").isTrue();
+ assertThat(results.get(6).getIsError()).as("assistant_conversation query").isTrue();
+ assertThat(results.get(7).getIsError()).as("assistant_tool_call query").isTrue();
+ assertThat(results.get(8).getIsError()).as("assistant_message describe").isTrue();
}