Generated with '
+ f'{_escape_html(str(summary_intro.get("model_deployment") or "configured model"))} on '
+ f'{_escape_html(str(summary_intro.get("generated_at", "")))}
'
+ )
+ if message.get('timestamp'):
+ parts.append(
+ f'
{_escape_html(str(message.get("timestamp")))}
'
+ )
+ parts.append('')
+ for thought in message.get('thoughts', []):
+ thought_label = (thought.get('step_type') or 'step').replace('_', ' ').title()
+ parts.append(
+ f'
{_escape_html(thought_label)}: '
+ f'{_escape_html(str(thought.get("content") or "No content recorded."))}'
+ )
+ if thought.get('duration_ms') is not None:
+ parts.append(
+ f' Duration: {thought.get("duration_ms")} ms'
+ )
+ if thought.get('timestamp'):
+ parts.append(
+ f' Timestamp: '
+ f'{_escape_html(str(thought.get("timestamp")))}'
+ )
+ if thought.get('detail'):
+ parts.append(' Detail:')
+ _append_html_code_block(parts, thought.get('detail'))
+ parts.append('
'
+ )
+ if message.get('timestamp'):
+ parts.append(
+ f'
{_escape_html(str(message.get("timestamp")))}
'
+ )
+ content = message.get('content_text', '') or 'No content recorded.'
+ content_html = markdown2.markdown(
+ content,
+ extras=['fenced-code-blocks', 'tables', 'break-on-newline']
+ )
+ parts.append(content_html)
+
+ return '\n'.join(parts)
+
+
+def _render_pdf_bytes(body_html: str) -> bytes:
+ """Render HTML body content to PDF bytes using PyMuPDF Story API."""
+ MEDIABOX = fitz.paper_rect("letter")
+ WHERE = MEDIABOX + (36, 36, -36, -36)
+
+ story = fitz.Story(html=body_html, user_css=_PDF_CSS)
+
+ tmp_path = None
+ try:
+ with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
+ tmp_path = tmp.name
+
+ writer = fitz.DocumentWriter(tmp_path)
+ more = True
+ while more:
+ device = writer.begin_page(MEDIABOX)
+ more, _ = story.place(WHERE)
+ story.draw(device)
+ writer.end_page()
+ writer.close()
+ del story
+ del writer
+
+ with open(tmp_path, 'rb') as f:
+ return f.read()
+ finally:
+ if tmp_path:
+ try:
+ os.unlink(tmp_path)
+ except OSError:
+ pass
+
+
+def _conversation_to_pdf_bytes(entry: Dict[str, Any]) -> bytes:
+ """Convert a conversation export entry to PDF bytes."""
+ body_html = _build_pdf_html_body(entry)
+ return _render_pdf_bytes(body_html)
+
+
+def _html_body_to_pdf_bytes(body_html: str) -> bytes:
+ """Convert raw HTML body content to PDF bytes."""
+ return _render_pdf_bytes(body_html)
+
+
+def _append_html_table(parts: List[str], mapping: Dict[str, Any]):
+ """Append a key-value mapping as an HTML table."""
+ if not isinstance(mapping, dict) or not mapping:
+ parts.append('
No data available.
')
+ return
+
+ parts.append('
')
+ parts.append('
Property
Value
')
+ for key, value in mapping.items():
+ label = _format_markdown_key(key)
+ if isinstance(value, dict):
+ formatted = _format_nested_html_value(value)
+ elif isinstance(value, list):
+ formatted = (
+ ', '.join(_escape_html(str(item)) for item in value)
+ if value else 'None'
+ )
+ elif isinstance(value, bool):
+ formatted = 'Yes' if value else 'No'
+ else:
+ formatted = _escape_html(str(value))
+ parts.append(f'
{_escape_html(label)}
{formatted}
')
+ parts.append('
')
+
+
+def _format_nested_html_value(mapping: Dict[str, Any], depth: int = 0) -> str:
+ """Format a nested dict as an HTML string for table cells."""
+ if not mapping:
+ return 'None'
+
+ items = []
+ for key, value in mapping.items():
+ label = _format_markdown_key(key)
+ if isinstance(value, dict):
+ nested = _format_nested_html_value(value, depth + 1)
+ items.append(f'{_escape_html(label)}: {nested}')
+ elif isinstance(value, list):
+ list_str = (
+ ', '.join(_escape_html(str(v)) for v in value)
+ if value else 'None'
+ )
+ items.append(f'{_escape_html(label)}: {list_str}')
+ elif isinstance(value, bool):
+ items.append(f'{_escape_html(label)}: {"Yes" if value else "No"}')
+ else:
+ items.append(f'{_escape_html(label)}: {_escape_html(str(value))}')
+ return ' '.join(items)
+
+
+def _append_html_citations(parts: List[str], message: Dict[str, Any]):
+ """Append citation data as HTML."""
+ citations = message.get('citations', [])
+ if not citations:
+ parts.append('
No citations were recorded for this message.
')
+ return
+
+ doc_citations = [c for c in citations if c.get('citation_type') == 'document']
+ web_citations = [c for c in citations if c.get('citation_type') == 'web']
+ agent_citations = [c for c in citations if c.get('citation_type') == 'agent_tool']
+ legacy_citations = [c for c in citations if c.get('citation_type') == 'legacy']
+
+ if doc_citations:
+ parts.append('
Document Sources
')
+ parts.append('')
+ for citation in doc_citations:
+ parts.append(
+ f'
')
+ parts.append('')
+
+ if web_citations:
+ parts.append('
Web Sources
')
+ parts.append('')
+ for citation in web_citations:
+ title = _escape_html(
+ str(citation.get('title') or citation.get('label') or 'Web source')
+ )
+ url = citation.get('url')
+ if url:
+ parts.append(f'
';
+ });
+}
+
+/**
+ * Render a list of thought steps as HTML.
+ * @param {Array} thoughts
+ * @returns {string} HTML string
+ */
+function renderThoughtsList(thoughts) {
+ let html = '
+
+
+
+ Maximum blob size (in MB) allowed for tabular file previews (CSV, XLSX). Files larger than this will not be previewed.
+ Increase for larger files if your compute has sufficient memory, or decrease to protect smaller instances. Default: 200 MB.
+
+
return d.innerHTML;
}
+ function renderGroupTagBadges(tags, maxDisplay = 3) {
+ if (!Array.isArray(tags) || tags.length === 0) {
+ return 'No tags';
+ }
+
+ let html = '';
+ const displayTags = tags.slice(0, maxDisplay);
+
+ displayTags.forEach(tagName => {
+ const tag = groupWorkspaceTags.find(t => t.name === tagName);
+ const color = tag && tag.color ? tag.color : '#6c757d';
+ const textClass = isGroupColorLight(color) ? 'text-dark' : 'text-light';
+
+ html += `${escapeGroupHtml(tagName)}`;
+ });
+
+ if (tags.length > maxDisplay) {
+ html += `+${tags.length - maxDisplay}`;
+ }
+
+ return html;
+ }
+
// --- Tag Management Modal ---
function showGroupTagManagementModal() {
loadGroupWorkspaceTags().then(() => {
diff --git a/application/single_app/templates/index.html b/application/single_app/templates/index.html
index 7a146e0d..c3c2abc6 100644
--- a/application/single_app/templates/index.html
+++ b/application/single_app/templates/index.html
@@ -62,8 +62,7 @@
{% else %}
{% if session.get('user') %}
- You are logged in but do not have the required permissions to access this application.
- Please submit a ticket to request access.
+ {{ app_settings.access_denied_message | nl2br }}
Setting App Service Container Image ..."
+ # This deployer uses a container-based App Service.
+ # Gunicorn startup is handled by the Dockerfile ENTRYPOINT inside the image,
+ # so App Service native Python startup settings are not configured here.
# az webapp config container set `
# --name $appServiceName `
# --resource-group $resourceGroupName `
diff --git a/deployers/bicep/README.md b/deployers/bicep/README.md
index 5c50ed9f..8a3a1d0a 100644
--- a/deployers/bicep/README.md
+++ b/deployers/bicep/README.md
@@ -57,6 +57,17 @@ Ensure the following resource providers are registered in your subscription:
## Deployment Process
+## Runtime Startup Behavior
+
+- This deployer publishes a **container image** to Azure App Service.
+- Gunicorn is started by the container entrypoint in `application/single_app/Dockerfile`.
+- Do **not** add an App Service Stack Settings Startup command for this deployer unless you intentionally change the deployment model away from containers.
+- If you later switch to a native Python App Service deployment, deploy the `application/single_app` folder and use this startup command instead:
+
+```bash
+python -m gunicorn -c gunicorn.conf.py app:app
+```
+
The below steps cover the process to deploy the Simple Chat application to an Azure Subscription. It is assumed the user has administrative rights to the subscription for deployment. If the user does not also have permissions to create an Application Registration in Entra, a stand-alone script can be provided to an administrator with the correct permissions.
### Pre-Configuration:
@@ -358,7 +369,7 @@ A: Base infrastructure (without optional services) costs approximately:
### Upgrading
**Q: How do I upgrade to a new version?**
-A: Run `azd up` again from the updated codebase. Use `azd provision --preview` to review changes first.
+A: For **code-only** container updates, prefer `azd deploy`. Use `azd provision --preview` and then `azd up` only when the release also changes infrastructure. See [../../docs/how-to/upgrade_paths.md](../../docs/how-to/upgrade_paths.md) for the upgrade decision guide.
---
diff --git a/deployers/bicep/main.bicep b/deployers/bicep/main.bicep
index bcf19d31..6adc0d6a 100644
--- a/deployers/bicep/main.bicep
+++ b/deployers/bicep/main.bicep
@@ -6,13 +6,18 @@ targetScope = 'subscription'
param location string
@description('''The target Azure Cloud environment.
-- Accepted values are: AzureCloud, AzureUSGovernment
-- Default is AzureCloud''')
+- Accepted values are: AzureCloud, AzureUSGovernment, public, usgovernment, custom
+- Default is based on the ARM cloud name''')
@allowed([
- 'AzureCloud'
- 'AzureUSGovernment'
+ 'AzureCloud' // public, keep allowed values for backwards compatibility
+ 'AzureUSGovernment' // usgovernment
+ 'public'
+ 'usgovernment'
+ 'custom'
])
-param cloudEnvironment string
+param cloudEnvironment string = az.environment().name == 'AzureCloud' ? 'public' : (az.environment().name == 'AzureUSGovernment' ? 'usgovernment' : 'custom')
+// SimpleChat expects public, usgovernment or custom
+var scCloudEnvironment = cloudEnvironment == 'AzureCloud' ? 'public' : (cloudEnvironment == 'AzureUSGovernment' ? 'usgovernment' : cloudEnvironment)
@description('''The name of the application to be deployed.
- Name may only contain letters and numbers
@@ -80,6 +85,20 @@ param enableDiagLogging bool
- Default is false''')
param enablePrivateNetworking bool
+// --- Custom Azure Environment Parameters (for 'custom' azureEnvironment) ---
+@description('Custom blob storage URL suffix, e.g. blob.core.usgovcloudapi.net')
+param customBlobStorageSuffix string = 'blob.${az.environment().suffixes.storage}'
+@description('Custom Graph API URL, e.g. https://graph.microsoft.us')
+param customGraphUrl string? // az.environment().graph is legacy AD, do not use
+@description('Custom Identity URL, e.g. https://login.microsoftonline.us/')
+param customIdentityUrl string = az.environment().authentication.loginEndpoint
+@description('Custom Resource Manager URL, e.g. https://management.usgovcloudapi.net')
+param customResourceManagerUrl string = az.environment().resourceManager
+@description('Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default')
+param customCognitiveServicesScope string = 'https://cognitiveservices.azure.com/.default'
+@description('Custom search resource URL for token audience, e.g. https://search.azure.us')
+param customSearchResourceUrl string = 'https://search.azure.com'
+
@description('''Array of GPT model names to deploy to the OpenAI resource.''')
param gptModels array = [
{
@@ -424,7 +443,7 @@ module appService 'modules/appService.bicep' = {
logAnalyticsId: logAnalytics.outputs.logAnalyticsId
appServicePlanId: appServicePlan.outputs.appServicePlanId
containerImageName: containerImageName
- azurePlatform: cloudEnvironment
+ azurePlatform: scCloudEnvironment
cosmosDbName: cosmosDB.outputs.cosmosDbName
searchServiceName: searchService.outputs.searchServiceName
openAiServiceName: openAI.outputs.openAIName
@@ -439,6 +458,14 @@ module appService 'modules/appService.bicep' = {
enablePrivateNetworking: enablePrivateNetworking
#disable-next-line BCP318 // expect one value to be null if private networking is disabled
appServiceSubnetId: enablePrivateNetworking? virtualNetwork.outputs.appServiceSubnetId : ''
+
+ // --- Custom Azure Environment Parameters (for 'custom' azureEnvironment) ---
+ customBlobStorageSuffix: customBlobStorageSuffix
+ customGraphUrl: customGraphUrl
+ customIdentityUrl: customIdentityUrl
+ customResourceManagerUrl: customResourceManagerUrl
+ customCognitiveServicesScope: customCognitiveServicesScope
+ customSearchResourceUrl: customSearchResourceUrl
}
}
diff --git a/deployers/bicep/modules/appService.bicep b/deployers/bicep/modules/appService.bicep
index 5d9aa471..14f251b7 100644
--- a/deployers/bicep/modules/appService.bicep
+++ b/deployers/bicep/modules/appService.bicep
@@ -27,6 +27,23 @@ param keyVaultUri string
param enablePrivateNetworking bool
param appServiceSubnetId string = ''
+// --- Custom Azure Environment Parameters (for 'custom' azureEnvironment) ---
+@description('Custom blob storage URL suffix, e.g. blob.core.usgovcloudapi.net')
+param customBlobStorageSuffix string?
+@description('Custom Graph API URL, e.g. https://graph.microsoft.us')
+param customGraphUrl string?
+@description('Custom Identity URL, e.g. https://login.microsoftonline.us')
+param customIdentityUrl string?
+@description('Custom Resource Manager URL, e.g. https://management.usgovcloudapi.net')
+param customResourceManagerUrl string?
+@description('Custom Cognitive Services scope ex: https://cognitiveservices.azure.com/.default')
+param customCognitiveServicesScope string?
+@description('Custom search resource URL for token audience, e.g. https://search.azure.us')
+param customSearchResourceUrl string?
+
+var tenantId = tenant().tenantId
+var openIdMetadataUrl = '${az.environment().authentication.loginEndpoint}${tenantId}/v2.0/.well-known/openid-configuration'
+
// Import diagnostic settings configurations
module diagnosticConfigs 'diagnosticSettings.bicep' = if (enableDiagLogging) {
name: 'diagnosticConfigs'
@@ -55,12 +72,15 @@ resource appInsights 'Microsoft.Insights/components@2020-02-02' existing = {
name: appInsightsName
}
-var acrDomain = azurePlatform == 'AzureUSGovernment' ? '.azurecr.us' : '.azurecr.io'
+var acrDomain = az.environment().suffixes.acrLoginServer
// add web app
resource webApp 'Microsoft.Web/sites@2022-03-01' = {
name: toLower('${appName}-${environment}-app')
location: location
+ // This module deploys a Linux container App Service.
+ // Gunicorn startup comes from the container image entrypoint,
+ // so App Service native Python startup settings are not used here.
kind: 'app,linux,container'
properties: {
serverFarmId: appServicePlanId
@@ -77,7 +97,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = {
ftpsState: 'Disabled'
healthCheckPath: '/external/healthcheck'
appSettings: [
- { name: 'AZURE_ENDPOINT', value: azurePlatform == 'AzureUSGovernment' ? 'usgovernment' : 'public' }
+ { name: 'AZURE_ENVIRONMENT', value: azurePlatform }
{ name: 'SCM_DO_BUILD_DURING_DEPLOYMENT', value: 'false' }
{ name: 'AZURE_COSMOS_ENDPOINT', value: cosmosDb.properties.documentEndpoint }
{ name: 'AZURE_COSMOS_AUTHENTICATION_TYPE', value: toLower(authenticationType) }
@@ -150,8 +170,18 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = {
{ name: 'InstrumentationEngine_EXTENSION_VERSION', value: 'disabled' }
{ name: 'SnapshotDebugger_EXTENSION_VERSION', value: 'disabled' }
{ name: 'XDT_MicrosoftApplicationInsights_BaseExtensions', value: 'disabled' }
- { name: 'XDT_MicrosoftApplicationInsights_Mode', value: 'recommended' }
- { name: 'XDT_MicrosoftApplicationInsights_PreemptSdk', value: 'disabled' }
+ {name: 'XDT_MicrosoftApplicationInsights_Mode', value: 'recommended' }
+ {name: 'XDT_MicrosoftApplicationInsights_PreemptSdk', value: 'disabled' }
+ ...(azurePlatform == 'custom' ? [
+ {name: 'CUSTOM_GRAPH_URL_VALUE', value: customGraphUrl ?? ''}
+ {name: 'CUSTOM_IDENTITY_URL_VALUE', value: customIdentityUrl ?? ''}
+ {name: 'CUSTOM_RESOURCE_MANAGER_URL_VALUE', value: customResourceManagerUrl ?? ''}
+ {name: 'CUSTOM_BLOB_STORAGE_URL_VALUE', value: customBlobStorageSuffix ?? ''}
+ {name: 'CUSTOM_COGNITIVE_SERVICES_URL_VALUE', value: customCognitiveServicesScope ?? ''}
+ {name: 'CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE', value: customSearchResourceUrl ?? ''}
+ {name: 'KEY_VAULT_DOMAIN', value: az.environment().suffixes.keyvaultDns}
+ {name: 'CUSTOM_OIDC_METADATA_URL_VALUE', value: openIdMetadataUrl ?? ''}]
+ : [])
]
}
clientAffinityEnabled: false
@@ -205,7 +235,7 @@ resource authSettings 'Microsoft.Web/sites/config@2022-03-01' = {
azureActiveDirectory: {
enabled: true
registration: {
- openIdIssuer: azurePlatform == 'AzureUSGovernment' ? 'https://login.microsoftonline.us/${tenant().tenantId}/' : 'https://sts.windows.net/${tenant().tenantId}/'
+ openIdIssuer: '${az.environment().authentication.loginEndpoint}${tenant().tenantId}/'
clientId: enterpriseAppClientId
clientSecretSettingName: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET'
}
diff --git a/deployers/terraform/ReadMe.md b/deployers/terraform/ReadMe.md
index fb2dcf7d..acb2bc74 100644
--- a/deployers/terraform/ReadMe.md
+++ b/deployers/terraform/ReadMe.md
@@ -39,6 +39,12 @@ ACR_PASSWORD = "your_acr_password"
From Github > Actions > "SimpleChat Docker Image Publish" > Run workflow
+## Upgrading
+
+- For **code-only** container updates, publish a new image to ACR and follow the existing App Service container rollout process instead of rerunning Terraform for every release.
+- Use Terraform when you are intentionally changing infrastructure or configuration that belongs in Terraform state.
+- See [../../docs/how-to/upgrade_paths.md](../../docs/how-to/upgrade_paths.md) for the native-vs-container upgrade guide and the ACR/image-only rollout notes.
+
## Terraform deployment
Initialize: Run terraform init to download the necessary providers.
diff --git a/deployers/terraform/main.tf b/deployers/terraform/main.tf
index 77b486df..77eb1e75 100644
--- a/deployers/terraform/main.tf
+++ b/deployers/terraform/main.tf
@@ -360,6 +360,9 @@ resource "azurerm_linux_web_app" "app" {
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
service_plan_id = azurerm_service_plan.asp.id
+ # This Terraform deployer uses a container-based Linux Web App.
+ # Gunicorn startup comes from the container image entrypoint,
+ # so native Python app_command_line/startup settings are not used here.
ftp_publish_basic_authentication_enabled = false
webdeploy_publish_basic_authentication_enabled = false
diff --git a/custom-ca-certificates/.gitkeep b/docker-customization/custom-ca-certificates/.gitkeep
similarity index 100%
rename from custom-ca-certificates/.gitkeep
rename to docker-customization/custom-ca-certificates/.gitkeep
diff --git a/docker-customization/pip.conf b/docker-customization/pip.conf
new file mode 100644
index 00000000..3dc81272
--- /dev/null
+++ b/docker-customization/pip.conf
@@ -0,0 +1 @@
+# Add pip configuration here
\ No newline at end of file
diff --git a/docs/Gemfile b/docs/Gemfile
index 4ca5f8aa..478cf7f3 100644
--- a/docs/Gemfile
+++ b/docs/Gemfile
@@ -5,6 +5,7 @@ gem "github-pages", group: :jekyll_plugins
# Ruby 3+ compatibility
gem "webrick", "~> 1.8"
+gem "json", ">= 2.19.2"
# Jekyll plugins for enhanced functionality
group :jekyll_plugins do
diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock
index 9dede6d7..4af17774 100644
--- a/docs/Gemfile.lock
+++ b/docs/Gemfile.lock
@@ -220,7 +220,7 @@ GEM
gemoji (>= 3, < 5)
html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0)
- json (2.15.0)
+ json (2.19.2)
kramdown (2.4.0)
rexml
kramdown-parser-gfm (1.1.0)
@@ -287,6 +287,7 @@ DEPENDENCIES
jekyll-seo-tag
jekyll-sitemap
jekyll-titles-from-headings
+ json (>= 2.19.2)
webrick (~> 1.8)
BUNDLED WITH
diff --git a/docs/explanation/features/CHAT_COMPLETION_NOTIFICATIONS.md b/docs/explanation/features/CHAT_COMPLETION_NOTIFICATIONS.md
new file mode 100644
index 00000000..871ae2b0
--- /dev/null
+++ b/docs/explanation/features/CHAT_COMPLETION_NOTIFICATIONS.md
@@ -0,0 +1,103 @@
+# Chat Completion Notifications (v0.239.128)
+
+## Overview
+Chat completion notifications add background awareness for personal chat conversations. When a streamed assistant response finishes after the user has moved away from the chat, the app now creates a personal notification that deep-links back to the exact conversation and shows a green unread dot in both conversation lists until the conversation is opened.
+
+**Version Implemented:** 0.239.128
+
+## Dependencies
+- Flask chat and conversation routes
+- Cosmos DB conversations and notifications containers
+- Existing notification platform in `functions_notifications.py`
+- Chat SSE finalization in `static/js/chat/chat-streaming.js`
+- Main and sidebar conversation list modules
+
+## Implemented in version: **0.239.128**
+
+## Architecture Overview
+
+### Backend
+- **Stream completion hook:** `application/single_app/route_backend_chats.py`
+- **Unread-state helpers:** `application/single_app/functions_conversation_unread.py`
+- **Notification helpers:** `application/single_app/functions_notifications.py`
+- **Read/clear endpoint:** `POST /api/conversations//mark-read`
+
+When a personal chat stream completes, the backend now:
+- persists the assistant message as before
+- marks the conversation with unread assistant-response fields
+- creates a personal `chat_response_complete` notification
+- stores `conversation_id` and `message_id` in notification metadata
+- uses `/chats?conversationId=...` as the notification deep link
+
+### Frontend
+- **Main list module:** `application/single_app/static/js/chat/chat-conversations.js`
+- **Sidebar list module:** `application/single_app/static/js/chat/chat-sidebar-conversations.js`
+- **Stream finalization:** `application/single_app/static/js/chat/chat-streaming.js`
+- **Styling:** `application/single_app/static/css/chats.css`, `application/single_app/static/css/sidebar.css`
+
+The chat UI now:
+- renders a green unread dot for personal conversations with unread assistant responses
+- clears unread state when a conversation is opened
+- immediately clears the just-created unread state if the user is still watching that conversation when streaming finishes
+
+## Notification Behavior
+
+### Deep-Linking
+Notification clicks use the existing notification navigation flow and point directly to:
+
+`/chats?conversationId=`
+
+`chat-onload.js` already supports this URL shape, so the destination conversation is selected automatically after the chat page loads.
+
+### Approximate Active-View Suppression
+This implementation intentionally does not add heartbeat or presence tracking. Instead:
+- the backend always creates the completion notification for personal chats
+- the active chat page immediately calls the new mark-read endpoint after stream completion
+- this keeps the user-facing result aligned with the active-view scenario without adding presence infrastructure
+
+## Conversation Data Shape
+Personal conversation payloads now normalize these fields:
+- `has_unread_assistant_response`
+- `last_unread_assistant_message_id`
+- `last_unread_assistant_at`
+
+Older conversation documents that do not yet contain these fields are normalized to safe defaults in the conversation list and metadata APIs.
+
+## Files Updated
+- `application/single_app/functions_conversation_unread.py`
+- `application/single_app/functions_notifications.py`
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/route_backend_conversations.py`
+- `application/single_app/static/js/chat/chat-conversations.js`
+- `application/single_app/static/js/chat/chat-sidebar-conversations.js`
+- `application/single_app/static/js/chat/chat-streaming.js`
+- `application/single_app/static/css/chats.css`
+- `application/single_app/static/css/sidebar.css`
+- `application/single_app/config.py`
+- `functional_tests/test_chat_completion_notifications.py`
+
+## Usage Instructions
+- Start a personal chat and send a prompt that takes long enough for streaming to remain active.
+- Navigate away from the chat page before the response completes.
+- After completion, open Notifications and click the new AI response notification.
+- The app navigates back to the exact conversation and clears the unread state.
+
+## Testing and Validation
+- **Functional test:** `functional_tests/test_chat_completion_notifications.py`
+
+The regression test validates:
+- chat-response notification creation and deep-link shape
+- unread-field normalization for older conversation documents
+- mark-read endpoint clearing both conversation unread state and notification read state
+- frontend wiring for unread dots and mark-read calls
+
+## Performance Considerations
+- No polling was added beyond the existing notification badge polling
+- The mark-read endpoint is idempotent and lightweight
+- The unread indicator stores a single latest unread assistant response state, not an unread count
+
+## Known Limitations
+- First rollout is personal chats only
+- Group and public chat conversations do not yet participate in this notification flow
+- Presence detection is approximate rather than heartbeat-based
+- The green dot indicates unread assistant completion state, not a count of unread assistant messages
\ No newline at end of file
diff --git a/docs/explanation/features/CONVERSATION_EXPORT.md b/docs/explanation/features/CONVERSATION_EXPORT.md
index c56d261a..a674dca4 100644
--- a/docs/explanation/features/CONVERSATION_EXPORT.md
+++ b/docs/explanation/features/CONVERSATION_EXPORT.md
@@ -1,139 +1,186 @@
# Conversation Export
## Overview
-The Conversation Export feature allows users to export one or multiple conversations directly from the Chats experience. A multi-step wizard modal guides users through format selection, output packaging, and downloading the final file.
+The Conversation Export feature lets users export one or more chats from the Chats experience as JSON, Markdown, or PDF. The export now mirrors the live conversation view more closely by excluding deleted and inactive-thread messages, including processing thoughts, and preserving the modern citation buckets used by the chat UI.
-**Version Implemented:** 0.237.050
+**Version Implemented:** 0.239.022 (base), 0.239.023 (PDF export), 0.239.030 (persistent summaries)
## Dependencies
-- Flask (backend route)
-- Azure Cosmos DB (conversation and message storage)
-- Bootstrap 5 (modal, step indicators, cards)
-- ES modules (chat-export.js)
+- Flask backend route and file responses
+- Azure Cosmos DB conversation, message, and thought containers
+- Bootstrap 5 modal workflow
+- ES modules in `static/js/chat/chat-export.js`
+- Azure OpenAI / APIM-backed chat model access for optional intro summaries
+- PyMuPDF (fitz) for HTML-to-PDF rendering via the Story API
+
+## Implemented in version: **0.239.030**
## Architecture Overview
### Backend
-- **Route file:** `route_backend_conversation_export.py`
+- **Route file:** `application/single_app/route_backend_conversation_export.py`
- **Endpoint:** `POST /api/conversations/export`
-- **Registration:** Called via `register_route_backend_conversation_export(app)` in `app.py`
+- **Registration:** `register_route_backend_conversation_export(app)`
+
+The export route now:
+- verifies the current user owns each requested conversation
+- loads messages ordered by timestamp, then reapplies thread ordering to match the chat UI
+- removes soft-deleted messages and inactive-thread retries
+- joins processing thoughts from the thoughts container by `message_id`
+- builds both normalized citation summaries and raw citation buckets
+- optionally generates a per-conversation intro summary using the selected chat model
+
+### Frontend
+- **JS module:** `application/single_app/static/js/chat/chat-export.js`
+- **Modal host:** `application/single_app/templates/chats.html`
+- **Entry point:** `window.chatExport.openExportWizard(conversationIds, skipSelection)`
+
+The wizard now has up to 5 steps:
+1. **Selection** — Review selected conversations (multi-select exports only)
+2. **Format** — Choose JSON, Markdown, or PDF
+3. **Packaging** — Choose a single file or ZIP archive
+4. **Summary** — Optionally generate a per-conversation intro summary and choose the summary model from the same model list used in the chat composer
+5. **Download** — Review settings and download the export
+
+## Request Payload
-The endpoint accepts a JSON body with:
| Field | Type | Description |
|---|---|---|
-| `conversation_ids` | list[str] | IDs of conversations to export |
-| `format` | string | `"json"` or `"markdown"` |
+| `conversation_ids` | list[str] | Conversation IDs to export |
+| `format` | string | `"json"`, `"markdown"`, or `"pdf"` |
| `packaging` | string | `"single"` or `"zip"` |
-
-The server verifies user ownership of each conversation, fetches messages from Cosmos DB, filters for active thread messages, sanitizes internal fields, and returns either a single file or ZIP archive as a binary download.
-
-### Frontend
-- **JS module:** `static/js/chat/chat-export.js`
-- **Modal HTML:** Embedded in `templates/chats.html` (`#export-wizard-modal`)
-- **Global API:** `window.chatExport.openExportWizard(conversationIds, skipSelection)`
-
-The wizard has up to 4 steps:
-1. **Selection Review** — Shows selected conversations with titles (skipped for single-conversation export)
-2. **Format** — Choose between JSON and Markdown via action-type cards
-3. **Packaging** — Choose between single file and ZIP archive
-4. **Download** — Summary and download button
-
-## Entry Points
-
-### Single Conversation Export
-- **Sidebar ellipsis menu** → "Export" item (in `chat-sidebar-conversations.js`)
-- **Left-pane ellipsis menu** → "Export" item (in `chat-conversations.js`)
-- Both call `window.chatExport.openExportWizard([conversationId], true)` — skips the selection step
-
-### Multi-Conversation Export
-- Enter selection mode by clicking "Select" on any conversation
-- Select multiple conversations via checkboxes
-- Click the export button in:
- - **Left-pane header** — `#export-selected-btn` (btn-info, download icon)
- - **Sidebar actions bar** — `#sidebar-export-selected-btn`
-- These call `window.chatExport.openExportWizard(selectedIds, false)` — shows all 4 steps
+| `include_summary_intro` | boolean | Whether to generate a per-conversation intro summary |
+| `summary_model_deployment` | string | Optional selected model deployment for the summary intro |
## Export Formats
### JSON
-Produces a JSON array where each entry contains:
-```json
-{
- "conversation": {
- "id": "...",
- "title": "...",
- "last_updated": "...",
- "chat_type": "...",
- "tags": [],
- "is_pinned": false,
- "context": []
- },
- "messages": [
- {
- "role": "user",
- "content": "...",
- "timestamp": "...",
- "citations": []
- }
- ]
-}
-```
+Each exported conversation entry contains:
+- `conversation` metadata and aggregate counts
+- `summary_intro` status and content (when enabled)
+- `messages` with:
+ - raw `content`
+ - normalized `content_text`
+ - curated `details`
+ - normalized `citations`
+ - raw `legacy_citations`, `hybrid_citations`, `web_search_citations`, and `agent_citations`
+ - nested `thoughts`
+
+This makes the JSON export useful both for readable inspection and downstream processing.
### Markdown
-Produces a Markdown document with:
-- `# Title` heading
-- Metadata block (last updated, chat type, tags, message count)
-- `### Role` sections per message with timestamps
-- Citation lists where applicable
-- `---` separators between messages and conversations
+Markdown exports are now organized like a lightweight report:
+- conversation title and metadata header
+- optional **Abstract** section generated by the selected model
+- **Transcript** section with clean user/assistant turns only
+- appendix sections for:
+ - conversation metadata
+ - curated message details
+ - references/citations
+ - processing thoughts
+ - supplemental non-transcript messages such as file or system records
+
+This keeps the main body readable while moving verbose reference material to the end of the document.
+
+### PDF
+PDF exports render the conversation as a print-ready document with chat-bubble styling that visually resembles the live chat interface:
+- **User messages** are displayed in blue bubbles (#c8e0fa) aligned to the right
+- **Assistant messages** are displayed in gray bubbles (#f1f0f0) aligned to the left
+- **System and file messages** use distinct background colors
+- Message content is converted from Markdown to HTML for rich formatting
+- The same appendix structure as Markdown is included (metadata, details, references, thoughts, supplemental messages)
+- PDF rendering uses PyMuPDF's Story API on US Letter paper (612 × 792pt) with 0.5-inch margins
+- Multi-page content is handled automatically with page overflow
+
+## Citation Handling
+
+The export supports the same citation categories used in the live chat UI:
+- **Document citations** from `hybrid_citations`
+- **Web citations** from `web_search_citations`
+- **Agent/tool citations** from `agent_citations`
+- **Legacy citations** for backwards-compatible older messages
+
+Normalized citation summaries provide a single export-friendly list, while the raw buckets preserve the original structured data.
+
+## Thoughts Handling
+
+If processing thoughts are enabled and available, they are exported with the assistant message they belong to. Each thought includes:
+- `step_index`
+- `step_type`
+- `content`
+- `detail`
+- `duration_ms`
+- `timestamp`
+
+Markdown exports place these in the **Processing Thoughts** appendix.
## Output Packaging
### Single File
-- One file containing all selected conversations
-- JSON: `.json` file
-- Markdown: `.md` file with `---` separators between conversations
+- JSON exports combine all selected conversations into one `.json` file
+- Markdown exports combine all selected conversations into one `.md` file separated by `---`
+- PDF exports combine all selected conversations into one `.pdf` file with visual separators between conversations
### ZIP Archive
-- One file per conversation inside a `.zip`
-- Filenames: `{sanitized_title}_{id_prefix}.{ext}`
-- Titles are sanitized for filesystem safety (special chars replaced, truncated to 50 chars)
-
-## File Structure
-```
-application/single_app/
-├── route_backend_conversation_export.py # Backend API endpoint
-├── app.py # Route registration
-├── static/js/chat/
-│ ├── chat-export.js # Export wizard module
-│ ├── chat-conversations.js # Left-pane wiring
-│ └── chat-sidebar-conversations.js # Sidebar wiring
-├── templates/
-│ ├── chats.html # Modal HTML + button + script
-│ ├── _sidebar_nav.html # Sidebar export button
-│ └── _sidebar_short_nav.html # Short sidebar export button
-functional_tests/
-└── test_conversation_export.py # Functional tests
-```
-
-## Security
-- Endpoint requires `@login_required` and `@user_required` decorators
-- Each conversation is verified for user ownership before export
-- Internal Cosmos DB fields (`_rid`, `_self`, `_etag`, `user_id`, etc.) are stripped from output
-- No sensitive data is included in the export
+- Each conversation is written to its own file in the archive
+- Filenames use `{sanitized_title}_{conversation_id_prefix}.{ext}`
+- Summary intros remain per conversation in both JSON and Markdown archive entries
+
+## Persistent Conversation Summaries (v0.239.030)
+
+Summaries generated during export or on demand are now persisted to the conversation document in Cosmos DB and reused automatically.
+
+### How Caching Works
+- Each summary stores `message_time_start` and `message_time_end` from the messages it was built from.
+- On subsequent exports, `_build_summary_intro` compares the cached `message_time_end` against the latest message timestamp. If no new messages exist, the cached summary is returned instantly.
+- If new messages exist beyond the cached range, a fresh summary is generated, saved, and returned.
+
+### On-Demand Generation
+- The conversation details modal (opened from the sidebar) now includes a **Summary** card.
+- If no summary exists, a **Generate Summary** button with a model selector lets users create one without exporting.
+- If a summary exists, the content, generation date, and model are displayed with a **Regenerate** button.
+- New API endpoint: `POST /api/conversations//summary` with optional `{ "model_deployment": "..." }` body.
+
+### Reusable Helper
+- `generate_conversation_summary()` (in `route_backend_conversation_export.py`) is the shared LLM call function used by both the export pipeline and the API endpoint.
+- It handles transcript assembly, truncation, model role selection (developer vs system), and Cosmos persistence.
+
+## Security and Data-Shaping Rules
+- Route uses authenticated user checks before export
+- Only user-owned personal conversations are exported
+- Internal Cosmos metadata is not passed through
+- Deleted messages and inactive-thread retries are excluded
+- Exported message details are curated rather than raw metadata dumps
+- Raw settings are not exposed to the browser; the export wizard reuses the existing chat model selector already rendered from sanitized settings
+
+## Files Updated
+- `application/single_app/route_backend_conversation_export.py`
+- `application/single_app/route_backend_conversations.py`
+- `application/single_app/static/js/chat/chat-export.js`
+- `application/single_app/static/js/chat/chat-conversation-details.js`
+- `application/single_app/config.py`
+- `functional_tests/test_conversation_export.py`
+- `functional_tests/test_persistent_conversation_summary.py`
## Testing and Validation
-- **Functional test:** `functional_tests/test_conversation_export.py`
-- Tests cover:
- - Conversation sanitization (internal field stripping)
- - Message sanitization
- - Markdown generation (headings, metadata, citations)
- - JSON structure validation
- - ZIP packaging (correct entries, valid content)
- - Filename sanitization (special chars, truncation, empty input)
- - Active thread message filtering
+- **Functional tests:**
+ - `functional_tests/test_conversation_export.py` — export pipeline (8 tests)
+ - `functional_tests/test_persistent_conversation_summary.py` — summary caching (8 tests)
+
+Coverage includes:
+- deleted/inactive message filtering
+- normalized and raw citation export
+- thoughts attached to assistant messages
+- transcript-style Markdown appendices
+- summary-intro metadata shape
+- filename sanitization and ZIP naming
+- content normalization and citation-count helpers
+- PDF HTML body generation with chat-bubble classes
+- PDF byte output validation (%PDF- header)
## Known Limitations
-- Export is limited to conversations the authenticated user owns
-- Very large conversations (thousands of messages) may take longer to process
-- The wizard fetches conversation titles client-side; if a title lookup fails, it shows the conversation ID instead
+- Export remains limited to user-owned personal conversations
+- Optional intro summaries increase export time because each selected conversation is summarized individually
+- Very large conversations may be truncated for summary generation, though the full export content is still preserved in the output
+- Summary generation is best-effort: if model initialization or generation fails, the export still completes and records the summary error state
+- PDF rendering relies on PyMuPDF’s CSS subset which does not support flexbox, float, or full border-radius; bubble alignment is approximated using margin offsets
diff --git a/docs/explanation/features/MESSAGE_EXPORT.md b/docs/explanation/features/MESSAGE_EXPORT.md
new file mode 100644
index 00000000..4495b5a1
--- /dev/null
+++ b/docs/explanation/features/MESSAGE_EXPORT.md
@@ -0,0 +1,118 @@
+# Per-Message Export
+
+## Overview
+The Per-Message Export feature adds export and action options directly to the three-dots dropdown menu on individual chat messages. Users can export a single message to Markdown or Word, insert it into the chat prompt, or open it in their default email client — all without leaving the chat interface.
+
+**Version Implemented:** 0.239.005–0.239.007
+
+## Dependencies
+- Flask (backend route for Word export)
+- `python-docx` 1.1.2 (Word document generation)
+- Azure Cosmos DB (message retrieval for Word export)
+- Bootstrap 5 (dropdown menus, icons)
+- ES modules (`chat-message-export.js`)
+
+## Architecture Overview
+
+### Backend
+- **Route file:** `route_backend_conversation_export.py`
+- **Endpoint:** `POST /api/message/export-word`
+- **Registration:** Registered alongside the existing conversation export routes
+
+The Word export endpoint accepts a JSON body with:
+
+| Field | Type | Description |
+|---|---|---|
+| `message_id` | string | ID of the message to export |
+| `conversation_id` | string | ID of the conversation the message belongs to |
+
+The server verifies user ownership of the conversation, fetches the specific message from Cosmos DB, generates a Word document using `python-docx` with basic Markdown-to-DOCX conversion (headings, paragraphs, bold, italic, inline code, code blocks, lists, citations), and returns the `.docx` as a binary download.
+
+### Frontend
+- **JS module:** `static/js/chat/chat-message-export.js`
+- **Dropdown integration:** `static/js/chat/chat-messages.js` (AI and user message dropdowns)
+- **Dynamic import:** Module is loaded on-demand when any export action is clicked (same pattern as `chat-edit.js`)
+
+## Features
+
+### Export to Markdown
+- **Location:** Three-dots dropdown → "Export to Markdown"
+- **Icon:** `bi-markdown`
+- **Behavior:** Entirely client-side. Grabs the message content from the existing hidden textarea (AI messages) or `.message-text` element (user messages), wraps it with a role header, and triggers a `.md` file download via Blob URL.
+- **Filename pattern:** `message_export_YYYYMMDD_HHMMSS.md`
+
+### Export to Word
+- **Location:** Three-dots dropdown → "Export to Word"
+- **Icon:** `bi-file-earmark-word`
+- **Behavior:** POSTs to `/api/message/export-word`. The backend generates a styled `.docx` document with:
+ - Title heading ("Message Export")
+ - Role metadata
+ - Message content with Markdown formatting preserved (headings, bold, italic, code blocks, lists)
+ - Citations section (if present on the message)
+- **Filename pattern:** `message_export_YYYYMMDD_HHMMSS.docx`
+
+### Use as Prompt
+- **Location:** Three-dots dropdown → "Use as Prompt"
+- **Icon:** `bi-clipboard-plus`
+- **Behavior:** Entirely client-side. Inserts the raw message content directly into the chat input box (`#user-input`), focuses the input, and triggers the auto-resize/send-button update. The user can review, edit, and send it.
+
+### Open in Email
+- **Location:** Three-dots dropdown → "Open in Email"
+- **Icon:** `bi-envelope`
+- **Behavior:** Entirely client-side. Opens the user's default email client via a `mailto:` link with:
+ - **Subject:** "Chat message from [sender name]"
+ - **Body:** The full message content
+- Uses `encodeURIComponent` for safe URL encoding of the content.
+
+## Dropdown Menu Structure
+
+Both AI and user messages now have export options below a divider:
+
+**AI Message Dropdown:**
+1. Delete
+2. Retry
+3. Feedback (thumbs up/down)
+4. ─── divider ───
+5. Export to Markdown
+6. Export to Word
+7. Use as Prompt
+8. Open in Email
+
+**User Message Dropdown:**
+1. Edit
+2. Delete
+3. Retry
+4. ─── divider ───
+5. Export to Markdown
+6. Export to Word
+7. Use as Prompt
+8. Open in Email
+
+## File Structure
+
+| File | Purpose |
+|------|---------|
+| `static/js/chat/chat-message-export.js` | Client-side export functions (Markdown, Word fetch, Use as Prompt, Open in Email) |
+| `static/js/chat/chat-messages.js` | Dropdown menu HTML and event bindings for both AI and user messages |
+| `route_backend_conversation_export.py` | Backend `/api/message/export-word` endpoint and Markdown-to-DOCX conversion |
+
+## Testing and Validation
+
+### Test Scenarios
+1. AI message → Export to Markdown → `.md` file downloads with content and role header
+2. AI message → Export to Word → `.docx` file downloads with formatted content and citations
+3. User message → Export to Markdown → `.md` file downloads
+4. User message → Export to Word → `.docx` file downloads
+5. AI/User message → Use as Prompt → Content appears in chat input box
+6. AI/User message → Open in Email → Default email client opens with pre-filled subject and body
+7. Existing actions (Delete, Retry, Edit, Feedback) still function correctly
+
+### Known Limitations
+- Word export requires a round-trip to the backend; offline use is not supported for Word format
+- `mailto:` URL length is limited by the email client/OS; very long messages may be truncated
+- Markdown export for user messages uses `innerText` rather than original markdown source
+
+## Cross-References
+- Related feature: [Conversation Export](CONVERSATION_EXPORT.md) — exports entire conversations
+- Backend shares infrastructure with conversation export (`route_backend_conversation_export.py`)
+- Functional tests: `functional_tests/test_message_export.py` (when created)
diff --git a/docs/explanation/features/SIMPLECHAT_STARTUP.md b/docs/explanation/features/SIMPLECHAT_STARTUP.md
new file mode 100644
index 00000000..d5a35d23
--- /dev/null
+++ b/docs/explanation/features/SIMPLECHAT_STARTUP.md
@@ -0,0 +1,191 @@
+# SimpleChat Startup and Scheduler (v0.239.136)
+
+## Overview
+This document explains how SimpleChat should be started in local development, Azure App Service native Python deployments, and container-based runtimes. It also explains how background scheduler work is separated from the Gunicorn web process so administrators can use more than one web worker without duplicating scheduler threads.
+
+**Version Implemented:** 0.239.136
+
+## Dependencies
+- Flask application bootstrap in `application/single_app/app.py`
+- Gunicorn runtime config in `application/single_app/gunicorn.conf.py`
+- Shared scheduler loops in `application/single_app/background_tasks.py`
+- Dedicated scheduler entrypoint in `application/single_app/simplechat_scheduler.py`
+- Container startup in `application/single_app/Dockerfile`
+
+## Implemented in version: **0.239.136**
+
+## Technical Specifications
+
+### Web Process Modes
+- **Local debug mode:** `FLASK_DEBUG=1` and `python app.py`
+- **Direct Gunicorn mode:** Gunicorn launched by App Service or by an operator command
+- **Optional handoff mode:** `python app.py` with `SIMPLECHAT_USE_GUNICORN=1` on Linux-compatible runtimes
+
+The web process now supports two production-safe approaches:
+
+1. Launch Gunicorn directly.
+2. Launch `python app.py` and let the process exec into Gunicorn when `SIMPLECHAT_USE_GUNICORN=1` is set.
+
+If Gunicorn is already the startup command, `SIMPLECHAT_USE_GUNICORN` is not needed.
+
+Important platform note:
+
+- Windows local development should stay on `FLASK_DEBUG=1` with `python app.py`.
+- Gunicorn and the optional handoff path should be treated as Linux/container/App Service runtime options, not native Windows runtime options.
+- If a Windows developer needs Gunicorn-specific worker or thread validation, use Docker Desktop, WSL2, or another Linux environment.
+
+### Background Scheduler Separation
+Scheduler-style loops are defined in `background_tasks.py` and can be started either:
+
+- inside a single-process web runtime for local development or legacy deployments
+- in a separate dedicated scheduler process by running `simplechat_scheduler.py`
+
+Background loops now start unless `SIMPLECHAT_RUN_BACKGROUND_TASKS` is explicitly set to a false-like value such as `0`, `false`, `no`, or `off`.
+
+Approval expiry and retention policy execution also use Cosmos-backed distributed lease documents in the shared settings container so only one worker or instance should perform those jobs at a time.
+
+### Environment Variables
+
+#### Web Process
+- `FLASK_DEBUG=1`
+ Uses the Flask development server with HTTPS and local-friendly behavior.
+- `SIMPLECHAT_USE_GUNICORN=1`
+ Only matters when the process starts as `python app.py` in non-debug mode on a runtime that can execute Gunicorn.
+- `SIMPLECHAT_RUN_BACKGROUND_TASKS`
+ Background loops are enabled when this setting is unset. Set it to `0`, `false`, `no`, or `off` to disable background loops in the current process.
+
+#### Gunicorn Tuning
+- `GUNICORN_BIND`
+- `GUNICORN_WORKERS`
+- `GUNICORN_THREADS`
+- `GUNICORN_TIMEOUT`
+- `GUNICORN_GRACEFUL_TIMEOUT`
+- `GUNICORN_KEEPALIVE`
+- `GUNICORN_MAX_REQUESTS`
+- `GUNICORN_MAX_REQUESTS_JITTER`
+
+## Native Python App Service
+
+### Recommended Web Startup
+Use Gunicorn directly in the App Service Startup command when deploying the native Python runtime.
+
+Deploy and run the `application/single_app` folder in App Service.
+
+Use this Startup command:
+
+```bash
+python -m gunicorn -c gunicorn.conf.py app:app
+```
+
+An explicit full command is also valid:
+
+```bash
+gunicorn --bind=0.0.0.0:$PORT --worker-class gthread --workers 2 --threads 8 --timeout 900 --graceful-timeout 60 --keep-alive 75 --max-requests 500 --max-requests-jitter 50 app:app
+```
+
+### Recommended Scheduler Process
+Run scheduler work in a separate job/process instead of inside the web workers.
+
+Recommended command:
+
+```bash
+python simplechat_scheduler.py
+```
+
+Operational options include:
+- a separate App Service or worker instance dedicated to the scheduler command
+- a WebJob or automation step that runs the scheduler command
+- a scheduled container/job platform that launches the same codebase with the scheduler command
+
+### Admin Guidance
+- Keep Gunicorn as the web Startup command.
+- Leave `SIMPLECHAT_USE_GUNICORN` unset unless you intentionally want `python app.py` to hand off to Gunicorn.
+- Set `SIMPLECHAT_RUN_BACKGROUND_TASKS=0` in multi-worker Gunicorn web deployments if the scheduler runs elsewhere.
+- Use `workers=2` for the web process only after moving scheduler work out to the dedicated scheduler process.
+
+## Container Runtime
+
+### Default Web Container Behavior
+The container image now starts the web process with Gunicorn by default through the Docker entrypoint.
+
+Web container entrypoint:
+
+```text
+python3 -m gunicorn -c /app/gunicorn.conf.py app:app
+```
+
+### Dedicated Scheduler Container or Job
+Use the same image with an overridden command to run scheduler work separately.
+
+Scheduler command:
+
+```bash
+python3 /app/simplechat_scheduler.py
+```
+
+This allows a deployment topology such as:
+- one web container with `workers=2`
+- one scheduler container or job running `simplechat_scheduler.py`
+
+## Local Development
+
+### Default Local Workflow
+For everyday development, use:
+
+```bash
+FLASK_DEBUG=1
+python app.py
+```
+
+This keeps the normal Flask development flow and starts background loops in the local process.
+
+On Windows, this is the recommended local workflow. Keep `FLASK_DEBUG=1` enabled and do not rely on native Gunicorn execution.
+
+If multiple workers or instances are active, the approval expiry and retention policy jobs now coordinate through distributed locks. Logging timer work still runs per process.
+
+### Production-Like Local Workflow
+For concurrency, timeout, and streaming validation, run Gunicorn locally only in Linux-compatible environments such as Docker, WSL2, or a native Linux/macOS shell:
+
+```bash
+gunicorn --bind=0.0.0.0:5000 --worker-class gthread --workers 2 --threads 8 --timeout 900 --graceful-timeout 60 --keep-alive 75 --max-requests 500 --max-requests-jitter 50 app:app
+```
+
+Windows note:
+
+- Native Windows Python cannot run Gunicorn because Gunicorn depends on Unix-only modules.
+- On Windows, use `python app.py` for application development and switch to Docker or WSL2 when you need Gunicorn-specific validation.
+
+To test the scheduler separately at the same time:
+
+```bash
+python simplechat_scheduler.py
+```
+
+## Usage Instructions
+
+### Native Python App Service
+1. Set the App Service Startup command to Gunicorn.
+2. Set `SIMPLECHAT_RUN_BACKGROUND_TASKS=0` in the web app configuration when scheduler work is running in a separate process/job.
+3. Run scheduler work with `python simplechat_scheduler.py` in a separate process/job.
+
+### Container Deployments
+1. Keep the default Gunicorn web entrypoint.
+2. Launch a second container/job using the same image.
+3. Override its command to `python3 /app/simplechat_scheduler.py`.
+
+## Testing and Validation
+- Functional test: `functional_tests/test_gunicorn_startup_support.py`
+- Functional test: `functional_tests/test_startup_scheduler_support.py`
+
+These tests verify:
+- Gunicorn-aware startup helpers and config defaults
+- shared background task module extraction
+- dedicated scheduler entrypoint presence
+- deployment guidance documentation presence
+
+## Known Limitations
+- Native Windows Python is not a supported Gunicorn runtime.
+- Leaving `SIMPLECHAT_RUN_BACKGROUND_TASKS` unset enables the loops in every Gunicorn worker process.
+- Approval expiry and retention policy now coordinate with distributed locks, but logging timer work still runs in every enabled process.
+- Set `SIMPLECHAT_RUN_BACKGROUND_TASKS=0` in web workers if you want the separate scheduler process to be the only scheduler runtime.
+- The scheduler separation prepares the app for multi-worker web runtimes, but the actual Azure job/container orchestration still needs to be configured per environment.
\ No newline at end of file
diff --git a/docs/explanation/features/v0.229.001/OPENAPI_ACTION.md b/docs/explanation/features/v0.229.001/OPENAPI_ACTION.md
index d1b765cc..9a6b999b 100644
--- a/docs/explanation/features/v0.229.001/OPENAPI_ACTION.md
+++ b/docs/explanation/features/v0.229.001/OPENAPI_ACTION.md
@@ -12,7 +12,6 @@ This OpenAPI Semantic Kernel Action/Plugin allows you to expose any OpenAPI-comp
- **Better error handling**: Validates inputs and provides clear error messages
- **Multiple file formats**: Supports both YAML and JSON OpenAPI specifications
- **Secure file uploads**: Comprehensive security validation for uploaded OpenAPI specs
-- **Multiple source types**: Supports file upload and URL download
- **Web UI integration**: Full modal interface for configuration through the web application
## Installation
@@ -21,7 +20,6 @@ No additional dependencies beyond what's already in the project. The plugin uses
- `yaml` for YAML parsing
- `json` for JSON parsing
- `semantic_kernel` for plugin functionality
-- `requests` for URL-based spec downloads
- `re` for security pattern matching
## Configuration Methods
@@ -31,7 +29,6 @@ The OpenAPI plugin can be configured in three ways:
### 1. Through Web UI (Recommended)
Use the plugin configuration modal in the web application to:
- Upload OpenAPI specification files (with security validation)
-- Download specs from URLs (with security checks)
- Configure authentication settings
- Test and validate configurations before saving
@@ -49,8 +46,12 @@ from openapi_plugin_factory import OpenApiPluginFactory
# Create from configuration
plugin = OpenApiPluginFactory.create_from_config({
- 'openapi_source_type': 'file', # or 'url'
- 'openapi_file_id': 'uploaded_file_id',
+ 'openapi_spec_content': {
+ 'openapi': '3.0.0',
+ 'info': {'title': 'My API', 'version': '1.0.0'},
+ 'paths': {}
+ },
+ 'openapi_source_type': 'content',
'base_url': 'https://api.example.com',
'auth': {'type': 'bearer', 'token': 'your-token'}
})
@@ -162,14 +163,6 @@ The OpenAPI plugin includes comprehensive security validation to prevent malicio
- Code execution patterns (`eval()`, `exec()`, `import os`, etc.)
- SQL injection attempts (`DROP TABLE`, `UNION SELECT`, etc.)
-### URL Security
-- **HTTPS enforcement**: Recommends secure connections
-- **Private network blocking**: Prevents access to:
- - Localhost (`127.0.0.1`, `::1`)
- - Private IP ranges (`10.x.x.x`, `192.168.x.x`, `172.16-31.x.x`)
- - Link-local addresses (`169.254.x.x`)
-- **Content validation**: Downloaded content undergoes same security checks as uploads
-
### Structure Validation
- **Nesting depth limits**: Prevents DoS attacks via deeply nested objects
- **OpenAPI format validation**: Ensures valid OpenAPI 2.0/3.0 structure
@@ -209,9 +202,7 @@ The OpenAPI plugin includes comprehensive security validation to prevent malicio
### Web UI Configuration (Recommended)
1. Open the plugin configuration modal
2. Select "OpenAPI" as plugin type
-3. Choose your specification source:
- - **Upload File**: Drag & drop or select your OpenAPI file
- - **From URL**: Enter URL to your OpenAPI specification
+3. Upload your OpenAPI specification file
4. Configure authentication settings
5. Test and save configuration
@@ -221,8 +212,12 @@ The OpenAPI plugin includes comprehensive security validation to prevent malicio
from openapi_plugin_factory import OpenApiPluginFactory
config = {
- 'openapi_source_type': 'file',
- 'openapi_file_id': 'abc123', # From upload endpoint
+ 'openapi_source_type': 'content',
+ 'openapi_spec_content': {
+ 'openapi': '3.0.0',
+ 'info': {'title': 'My API', 'version': '1.0.0'},
+ 'paths': {}
+ },
'base_url': 'https://api.myservice.com',
'auth': {'type': 'bearer', 'token': 'your-token'}
}
@@ -257,7 +252,6 @@ The plugin includes comprehensive error handling:
- **ValueError**: If required parameters are missing or file format is invalid
- **YAMLError/JSONDecodeError**: If spec file is malformed
- **SecurityValidationError**: If uploaded file contains malicious content
-- **URLSecurityError**: If URL points to forbidden locations (localhost, private networks)
- **FileSizeError**: If uploaded file exceeds 5MB limit
## API Endpoints
@@ -272,36 +266,11 @@ Content-Type: multipart/form-data
Response: {
"success": true,
"file_id": "abc123",
- "api_info": {
+ "spec_info": {
"title": "My API",
- "version": "1.0.0",
- "endpoints_count": 25
- }
-}
-```
-
-### Validate OpenAPI URL
-```
-POST /api/openapi/validate-url
-Content-Type: application/json
-Body: {"url": "https://api.example.com/openapi.yaml"}
-
-Response: {
- "success": true,
- "file_id": "def456",
- "api_info": {...}
-}
-```
-
-### Download from URL
-```
-POST /api/openapi/download-from-url
-Content-Type: application/json
-Body: {"url": "https://api.example.com/openapi.yaml"}
-
-Response: {
- "success": true,
- "file_id": "ghi789"
+ "version": "1.0.0"
+ },
+ "spec_content": {...}
}
```
diff --git a/docs/explanation/features/v0.239.003/PROCESSING_THOUGHTS.md b/docs/explanation/features/v0.239.003/PROCESSING_THOUGHTS.md
new file mode 100644
index 00000000..5dad56d9
--- /dev/null
+++ b/docs/explanation/features/v0.239.003/PROCESSING_THOUGHTS.md
@@ -0,0 +1,153 @@
+---
+layout: libdoc/page
+title: Processing Thoughts
+order: 100
+category: Features
+---
+
+# Processing Thoughts
+
+## Overview
+The Processing Thoughts feature replaces the generic "AI is typing..." indicator with real-time processing step traces that show users what the system is doing during chat processing. Each step (document search, web search, agent invocation, content safety check, response generation) is persisted in Cosmos DB and can be reviewed later via a per-message collapsible section.
+
+**Version Implemented:** 0.239.003
+
+## Dependencies
+- Flask (backend routes)
+- Azure Cosmos DB (`thoughts` and `archive_thoughts` containers)
+- Bootstrap 5 (collapsible section, badges, icons)
+- ES modules (`chat-thoughts.js`)
+
+## Architecture Overview
+
+### Backend
+
+#### ThoughtTracker (`functions_thoughts.py`)
+Stateful per-request tracker that writes each thought step to Cosmos DB immediately so polling clients can see partial progress.
+
+```
+ThoughtTracker(conversation_id, message_id, thread_id, user_id)
+ .add_thought(step_type, content, detail=None) → thought_id
+ .complete_thought(thought_id, duration_ms) → updates duration
+ .enabled → checks settings['enable_thoughts']
+```
+
+Design rules:
+- Each `add_thought()` does an immediate `upsert_item()` to Cosmos DB.
+- All writes are wrapped in try/except — thought errors never crash the chat flow.
+- Auto-increments `step_index` per tracker instance.
+- Logs failures via `log_event()` at WARNING level.
+
+#### Thought Document Schema
+```json
+{
+ "id": "uuid",
+ "conversation_id": "str",
+ "message_id": "str (assistant message ID)",
+ "thread_id": "str",
+ "user_id": "str (partition key)",
+ "step_index": 0,
+ "step_type": "search | tabular_analysis | web_search | agent_tool_call | generation | content_safety",
+ "content": "Searching personal workspace documents for 'sales analysis'...",
+ "detail": "Optional technical detail",
+ "duration_ms": null,
+ "timestamp": "ISO-8601"
+}
+```
+
+#### API Endpoints (`route_backend_thoughts.py`)
+
+| Method | Endpoint | Purpose |
+|--------|----------|---------|
+| GET | `/api/conversations//messages//thoughts` | Fetch persisted thoughts for a specific assistant message (historical viewing) |
+| GET | `/api/conversations//thoughts/pending` | Fetch latest in-progress thoughts (polling while waiting for response) |
+
+Both endpoints return `{"thoughts": [...], "enabled": true/false}`. When `enable_thoughts` is off, they return `{"thoughts": [], "enabled": false}`.
+
+#### Instrumentation Points (`route_backend_chats.py`)
+
+| Step Type | Content Example | When |
+|-----------|----------------|------|
+| `content_safety` | "Checking content safety..." | Before content safety check |
+| `search` | "Searching personal documents for 'query'..." | Before hybrid search |
+| `search` | "Found 5 results from 3 documents" | After search results |
+| `tabular_analysis` | "Found tabular data — evaluating analysis..." | When tabular data detected |
+| `web_search` | "Searching the web for 'query'..." | Before web search |
+| `web_search` | "Got 8 web results" | After web search |
+| `agent_tool_call` | "Sending to agent 'Data Analyst'..." | Before agent invocation |
+| `generation` | "Generating response..." | Before GPT call |
+
+### Frontend
+
+#### Streaming Mode
+Thought events are embedded in the SSE stream as `{"type": "thought", ...}` JSON payloads. The streaming handler in `chat-streaming.js` passes these to `handleStreamingThought()` which updates the streaming placeholder badge.
+
+#### Non-Streaming Mode
+A polling mechanism in `chat-thoughts.js` fetches `/thoughts/pending` every 2 seconds while waiting for a response. The loading indicator text is updated with the latest thought step.
+
+#### Per-Message History
+Each assistant message footer includes a lightbulb toggle button (when thoughts exist). Clicking it opens a collapsible section that lazy-loads thoughts from the API. Each step shows an icon, content text, and optional duration.
+
+#### Icon Map
+| Step Type | Bootstrap Icon |
+|-----------|---------------|
+| `search` | `bi-search` |
+| `tabular_analysis` | `bi-table` |
+| `web_search` | `bi-globe` |
+| `agent_tool_call` | `bi-robot` |
+| `generation` | `bi-lightning` |
+| `content_safety` | `bi-shield-check` |
+
+## Configuration
+
+### Admin Settings
+- **Toggle**: `enable_thoughts` (default: `false`)
+- **Location**: Admin Settings > Optional Features tab > "Processing Thoughts" section
+- **Effect**: When disabled, no thoughts are recorded and no UI elements are shown
+
+### Cosmos DB Containers
+| Container | Partition Key | Purpose |
+|-----------|--------------|---------|
+| `thoughts` | `/user_id` | Active thought records |
+| `archive_thoughts` | `/user_id` | Archived thoughts from deleted conversations |
+
+## Archive and Cleanup
+
+When a conversation is deleted:
+- **Archiving enabled**: Thoughts are copied to `archive_thoughts` container, then deleted from `thoughts`
+- **Archiving disabled**: Thoughts are permanently deleted from `thoughts`
+
+This applies to both single conversation delete and bulk delete operations.
+
+## File Structure
+
+### Files Created
+| File | Purpose |
+|------|---------|
+| `functions_thoughts.py` | ThoughtTracker class, Cosmos CRUD helpers |
+| `route_backend_thoughts.py` | API endpoints for fetching thoughts |
+| `static/js/chat/chat-thoughts.js` | Frontend polling, rendering, toggle |
+
+### Files Modified
+| File | Change |
+|------|--------|
+| `config.py` | Added `thoughts` + `archive_thoughts` Cosmos containers, bumped VERSION |
+| `functions_settings.py` | Added `enable_thoughts` default setting |
+| `app.py` | Imported and registered thought routes |
+| `route_backend_chats.py` | Instrumented ~8 thought points per chat path |
+| `route_backend_conversations.py` | Added archive/delete thoughts on conversation delete |
+| `templates/admin_settings.html` | Added Processing Thoughts toggle card |
+| `static/js/admin/admin_settings.js` | Added `enable_thoughts` to settings collection |
+| `static/js/chat/chat-messages.js` | Integrated thoughts toggle in footer, polling start/stop |
+| `static/js/chat/chat-streaming.js` | Handle `type: "thought"` in SSE data |
+| `static/js/chat/chat-loading-indicator.js` | Added `updateLoadingIndicatorText()` for thought display |
+| `static/css/chats.css` | Added thought indicator, toggle, container, and dark mode styles |
+
+## Testing
+
+1. **Enable feature**: Set `enable_thoughts: True` in admin settings
+2. **Non-streaming**: Send a message with document search — verify loading indicator updates with thought steps, lightbulb icon appears after response
+3. **Streaming**: Send a message — verify streaming placeholder shows thought badges, lightbulb available after finalization
+4. **History**: Reload page, open old conversation — click lightbulb to verify lazy-loaded thoughts
+5. **Disabled**: Set `enable_thoughts: False` — verify no thoughts generated, no lightbulb icons
+6. **Archive**: Delete a conversation with archiving enabled — verify thoughts moved to `archive_thoughts`
diff --git a/docs/explanation/features/v0.239.022/CONVERSATION_EXPORT.md b/docs/explanation/features/v0.239.022/CONVERSATION_EXPORT.md
new file mode 100644
index 00000000..edb69c2e
--- /dev/null
+++ b/docs/explanation/features/v0.239.022/CONVERSATION_EXPORT.md
@@ -0,0 +1,58 @@
+# Conversation Export
+
+## Overview
+Snapshot of the Conversation Export feature as implemented in version **0.239.022**.
+
+This version updates export generation so JSON includes modern citation buckets, normalized citation summaries, and processing thoughts, while Markdown becomes a transcript-first report with appendix sections and optional AI-generated intro summaries.
+
+**Version Implemented:** 0.239.022
+**Dependencies:** Flask export route, Azure Cosmos DB conversations/messages/thoughts, Bootstrap modal workflow, chat-export.js, Azure OpenAI/APIM chat models
+
+## Technical Summary
+
+### Backend
+- Filters out deleted messages and inactive-thread retries
+- Reapplies thread-aware ordering to align with the live chat view
+- Includes both normalized and raw citations per message
+- Joins persisted processing thoughts by `message_id`
+- Supports optional per-conversation `summary_intro` generation using a selected model
+
+### Frontend
+- Adds a summary step to the export wizard
+- Lets users enable or disable intro summaries
+- Reuses the existing chat model selector options for summary model choice
+
+## Export Shape
+
+### JSON
+Each conversation entry contains:
+- `conversation`
+- `summary_intro`
+- `messages`
+
+Each message can include:
+- `content`
+- `content_text`
+- `details`
+- `citations`
+- `legacy_citations`
+- `hybrid_citations`
+- `web_search_citations`
+- `agent_citations`
+- `thoughts`
+
+### Markdown
+Markdown exports contain:
+- metadata header
+- optional abstract
+- transcript body
+- appendices for metadata, message details, references, thoughts, and supplemental messages
+
+## Files Updated
+- `application/single_app/route_backend_conversation_export.py`
+- `application/single_app/static/js/chat/chat-export.js`
+- `application/single_app/config.py`
+- `functional_tests/test_conversation_export.py`
+
+## Testing
+Validated by `functional_tests/test_conversation_export.py`.
diff --git a/docs/explanation/features/v0.239.123/CHAT_SEARCHABLE_SELECTORS.md b/docs/explanation/features/v0.239.123/CHAT_SEARCHABLE_SELECTORS.md
new file mode 100644
index 00000000..c4859bb2
--- /dev/null
+++ b/docs/explanation/features/v0.239.123/CHAT_SEARCHABLE_SELECTORS.md
@@ -0,0 +1,64 @@
+# Chat Searchable Selectors
+
+## Overview
+Snapshot of the searchable chat selector update as implemented in version **0.239.123**.
+
+This version adds in-menu search to the chat workspace scope and tag filters, and rebuilds the prompt, model, and agent toolbar selectors as searchable dropdowns while keeping the hidden native selects in place for existing chat integrations.
+
+**Version Implemented:** 0.239.123
+**Dependencies:** chats.html toolbar and workspace filter markup, chats.css dropdown styling, chat-documents.js, chat-prompts.js, chat-model-selector.js, chat-agents.js, chat-searchable-select.js
+
+## Technical Specifications
+
+### Architecture Overview
+- Adds a shared `chat-searchable-select.js` helper for two selector patterns:
+ - searchable single-select dropdowns for prompts, models, and agents
+ - searchable filter overlays for the existing scope, tags, and documents dropdowns
+- Keeps `#prompt-select`, `#model-select`, and `#agent-select` as the canonical state holders so existing chat modules can continue reading native select values and option metadata.
+- Extends the existing search-documents card instead of introducing a second workspace filtering UI.
+
+### Prompt Loading
+- Prompt loading now walks paginated `/api/prompts`, `/api/group_prompts`, and `/api/public_prompts` responses using `page_size=100` until the full prompt set is loaded.
+- This removes the prior first-page cap from the chat prompt picker without changing shared prompt API semantics used elsewhere in the application.
+- Scope filtering still applies after prompt data is loaded, so the searchable list only shows prompt categories that match the current chat workspace scope.
+
+### File Structure
+- `application/single_app/templates/chats.html`
+- `application/single_app/static/css/chats.css`
+- `application/single_app/static/js/chat/chat-searchable-select.js`
+- `application/single_app/static/js/chat/chat-documents.js`
+- `application/single_app/static/js/chat/chat-prompts.js`
+- `application/single_app/static/js/chat/chat-model-selector.js`
+- `application/single_app/static/js/chat/chat-agents.js`
+- `functional_tests/test_chat_searchable_selectors.py`
+- `application/single_app/config.py`
+
+## Usage Instructions
+
+### Scope, Tags, and Documents
+- Open **Workspaces** on the chat page.
+- Use **Search workspaces...** to narrow the scope dropdown.
+- Use **Search tags...** to narrow tag and classification choices.
+- Use **Search documents...** to narrow the document list after scope and tag filtering have been applied.
+
+### Prompts
+- Open **Prompts** on the chat page.
+- Search within the prompt dropdown to find a saved prompt by name.
+- Select a single prompt to mirror the value back into the hidden prompt select used by message submission.
+
+### Models and Agents
+- Use the model dropdown search to quickly locate a deployment in long GPT model lists.
+- When agent mode is enabled, the model control swaps to a searchable agent dropdown with the same search interaction.
+- Agent labels continue to include group/global context when needed to distinguish duplicate display names.
+
+## Testing and Validation
+
+### Functional Coverage
+- `functional_tests/test_chat_searchable_selectors.py`
+- `functional_tests/test_workspace_scope_prompts_fix.py`
+
+### Validation Focus
+- Confirms the chat template contains searchable selector markup for scope, tags, prompts, models, and agents.
+- Confirms the shared helper supports both dropdown filtering and searchable single-select behavior.
+- Confirms prompt loading pages through all available prompt results instead of stopping at the default first page.
+- Confirms the app version bump for the feature.
\ No newline at end of file
diff --git a/docs/explanation/fixes/CHANGED_FILES_GITHUB_ACTION_SUPPLY_CHAIN_FIX.md b/docs/explanation/fixes/CHANGED_FILES_GITHUB_ACTION_SUPPLY_CHAIN_FIX.md
new file mode 100644
index 00000000..3533aa74
--- /dev/null
+++ b/docs/explanation/fixes/CHANGED_FILES_GITHUB_ACTION_SUPPLY_CHAIN_FIX.md
@@ -0,0 +1,48 @@
+# Changed-Files GitHub Action Supply Chain Fix
+
+Fixed/Implemented in version: **0.239.135**
+
+## Issue Description
+
+The repository's release notes workflow used `tj-actions/changed-files@v44`, a tag family affected by the March 2025 supply chain incident involving retroactively modified action tags.
+
+Even though the malicious window has been closed and current tags were restored, keeping the workflow on the older tag family left the repository behind the patched release identified by the advisory.
+
+## Root Cause Analysis
+
+- `.github/workflows/release-notes-check.yml` depended on `tj-actions/changed-files@v44`.
+- The security advisory identifies `46.0.1` as the patched version for the compromised action.
+
+## Technical Details
+
+### Files Modified
+
+- `.github/workflows/release-notes-check.yml`
+- `application/single_app/config.py`
+- `functional_tests/test_changed_files_action_version.py`
+
+### Code Changes Summary
+
+- Updated the release notes workflow to use `tj-actions/changed-files@v46.0.1`.
+- Added a functional regression test that verifies the patched action reference and rejects the known malicious commit SHA.
+- Bumped the application version to `0.239.135` for traceability.
+
+### Testing Approach
+
+- Added `functional_tests/test_changed_files_action_version.py` to validate the workflow pin, version bump, and fix documentation marker.
+
+## Validation
+
+### Before
+
+- The workflow referenced `tj-actions/changed-files@v44`.
+- There was no repository-level regression check guarding against reintroduction of the malicious commit SHA.
+
+### After
+
+- The workflow references the patched `v46.0.1` release.
+- The regression test asserts that the known malicious SHA is absent and the patched version remains in place.
+
+### Impact Analysis
+
+This is a narrow CI supply-chain remediation. It does not change application runtime behavior, but it does reduce the risk of reintroducing a compromised GitHub Action reference in repository automation.
\ No newline at end of file
diff --git a/docs/explanation/fixes/CHAT_COMPLETION_PERSONAL_SCOPE_GATE_FIX.md b/docs/explanation/fixes/CHAT_COMPLETION_PERSONAL_SCOPE_GATE_FIX.md
new file mode 100644
index 00000000..336d7052
--- /dev/null
+++ b/docs/explanation/fixes/CHAT_COMPLETION_PERSONAL_SCOPE_GATE_FIX.md
@@ -0,0 +1,47 @@
+# Chat Completion Personal Scope Gate Fix
+
+Fixed/Implemented in version: **0.239.133**
+
+## Issue Description
+
+Personal chat responses could complete and save successfully after the user navigated away, but no completion notification appeared and no green unread dot was shown when returning to the chat page.
+
+## Root Cause Analysis
+
+The streaming completion path decided whether to create chat-completion notifications by checking `active_group_id` and `active_public_workspace_id` from the request/session state.
+
+Those workspace identifiers can stay populated even when the actual conversation being completed is still a personal chat. In that case, the route incorrectly skipped the personal unread-state and notification writes.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_chat_completion_notifications.py`
+- `functional_tests/test_chat_stream_background_execution.py`
+- `functional_tests/test_streaming_only_chat_path.py`
+
+### Code Changes Summary
+
+- Added `is_personal_chat_conversation(...)` to classify the completed conversation from its saved `chat_type`.
+- Updated the streaming completion path to use conversation metadata instead of active workspace session values when deciding whether to create personal unread-state and notification side effects.
+- Added debug logging for the non-personal skip path to make future scope mismatches easier to diagnose.
+- Bumped the application version to `0.239.133`.
+
+### Testing Approach
+
+- Extended the chat completion notification regression to verify the streaming completion path uses the personal-conversation helper instead of the old active-workspace gate.
+- Updated the streaming route regressions so their version assertions stay aligned with the current app version.
+
+## Validation
+
+### Before
+
+- Personal chats could complete and persist the assistant answer without receiving completion-side unread state or notifications.
+- A stale active group or public workspace in session could suppress personal notifications incorrectly.
+
+### After
+
+- Personal chat completion side effects are now keyed off the saved conversation type.
+- Personal chats continue to receive unread markers and completion notifications even if unrelated workspace session state is populated.
\ No newline at end of file
diff --git a/docs/explanation/fixes/CHAT_COMPLETION_STREAM_FINALIZATION_FIX.md b/docs/explanation/fixes/CHAT_COMPLETION_STREAM_FINALIZATION_FIX.md
new file mode 100644
index 00000000..45a060fc
--- /dev/null
+++ b/docs/explanation/fixes/CHAT_COMPLETION_STREAM_FINALIZATION_FIX.md
@@ -0,0 +1,44 @@
+# Chat Completion Stream Finalization Fix
+
+Fixed/Implemented in version: **0.239.130**
+
+## Issue Description
+
+Personal chat responses could finish successfully in the background, but the user would not receive a completion notification and the conversation would not show the green unread dot when returning to the chat page.
+
+## Root Cause Analysis
+
+The streaming `/api/chat/stream` completion path persisted the final assistant message and updated conversation metadata, but it did not mark the conversation as unread or create the personal `chat_response_complete` notification before emitting the terminal SSE payload.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_chat_completion_notifications.py`
+
+### Code Changes Summary
+
+- Restored unread-state writes in the streaming completion branch using `mark_conversation_unread(...)`.
+- Restored personal completion-notification creation using `create_chat_response_notification(...)`.
+- Kept the behavior scoped to personal chats only by skipping group and public workspace completions.
+- Added regression coverage that inspects the streaming completion path for the unread-state and notification calls.
+- Bumped the application version to `0.239.130`.
+
+### Testing Approach
+
+- Extended `functional_tests/test_chat_completion_notifications.py` to verify the SSE completion branch includes unread-state and notification creation.
+
+## Validation
+
+### Before
+
+- Background chat completion could persist the assistant message without creating a notification.
+- Returning to the chat page showed no unread dot because the conversation unread fields were never written.
+
+### After
+
+- Personal streaming completions mark the conversation unread before the final SSE payload is sent.
+- Personal streaming completions create a `chat_response_complete` notification with the conversation deep link.
+- Returning to the chats page shows the green unread dot until the conversation is opened or marked read.
\ No newline at end of file
diff --git a/docs/explanation/fixes/CHAT_STREAM_BACKGROUND_BRIDGE_RESTORE_FIX.md b/docs/explanation/fixes/CHAT_STREAM_BACKGROUND_BRIDGE_RESTORE_FIX.md
new file mode 100644
index 00000000..778b9201
--- /dev/null
+++ b/docs/explanation/fixes/CHAT_STREAM_BACKGROUND_BRIDGE_RESTORE_FIX.md
@@ -0,0 +1,45 @@
+# Chat Stream Background Bridge Restore Fix
+
+Fixed/Implemented in version: **0.239.132**
+
+## Issue Description
+
+Leaving the chat page during a streamed response could still cause the assistant answer to disappear entirely from the conversation, with no completion notification and no unread marker when returning later.
+
+## Root Cause Analysis
+
+The active `route_backend_chats.py` route had drifted back to returning `stream_with_context(generate())` directly for both streaming entry points. That made the request-bound SSE generator the owner of assistant generation again, so browser navigation could terminate the stream before final assistant persistence and notification side effects ran.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_chat_completion_notifications.py`
+
+### Code Changes Summary
+
+- Restored a queue-backed `BackgroundStreamBridge` to decouple SSE delivery from the background chat worker.
+- Wrapped the worker with `copy_current_request_context()` so existing request/session-dependent logic remains available during background completion.
+- Routed both the main streaming path and compatibility streaming path through the background bridge helper.
+- Advanced the application version to `0.239.132` to match the active streaming regression suite.
+
+### Testing Approach
+
+- Reused the existing streaming background execution regression to verify the bridge class, executor submission, fallback thread path, and route wiring.
+- Updated the chat completion notification regression to validate the current app version after the restore.
+
+## Validation
+
+### Before
+
+- Chat logs could show intermediate tool work but never reach final assistant persistence.
+- Returning to the conversation could show only the user message because the response died with the detached request.
+- Completion notifications and unread dots were skipped because the finalization block never ran.
+
+### After
+
+- Streaming chat work is executed in background execution and only relayed through the HTTP response.
+- Browser disconnects detach the consumer without canceling the background worker.
+- Final assistant persistence, unread state, and completion notifications remain reachable after navigation away from the chat page.
\ No newline at end of file
diff --git a/docs/explanation/fixes/CHAT_STREAM_BACKGROUND_EXECUTION_FIX.md b/docs/explanation/fixes/CHAT_STREAM_BACKGROUND_EXECUTION_FIX.md
new file mode 100644
index 00000000..5a94c022
--- /dev/null
+++ b/docs/explanation/fixes/CHAT_STREAM_BACKGROUND_EXECUTION_FIX.md
@@ -0,0 +1,54 @@
+# Chat Stream Background Execution Fix
+
+Fixed/Implemented in version: **0.239.129**
+
+## Issue Description
+
+Leaving a streaming chat page before the assistant finished could stop the server-side chat execution entirely. In practice, backend logs stopped as soon as the browser disconnected, and the assistant response never reached the final persistence, unread-state, or notification paths.
+
+## Root Cause Analysis
+
+The normal `/api/chat/stream` implementation performed the model call and all downstream persistence directly inside the request-bound SSE generator.
+
+That meant the request response loop was also the worker. Once the browser navigated away and the streaming response was torn down, the long-running chat work could stop with it.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_chat_stream_background_execution.py`
+- `functional_tests/test_streaming_only_chat_path.py`
+- `functional_tests/test_chat_completion_notifications.py`
+
+### Code Changes Summary
+
+- Added a queue-backed `BackgroundStreamBridge` to decouple SSE delivery from chat execution.
+- Wrapped the streaming worker with `copy_current_request_context()` so existing request/session-dependent chat logic can still run in background execution.
+- Started the streaming worker through Flask-Executor when available, with a daemon-thread fallback.
+- Routed both the normal streaming path and the compatibility streaming path through the same background bridge helper.
+- Bumped the application version to `0.239.129`.
+
+### Testing Approach
+
+- Added `functional_tests/test_chat_stream_background_execution.py` to verify the background bridge, executor submission, and versioned fix documentation.
+- Updated the relevant streaming and chat notification regression tests so their version checks stay aligned with the current app version.
+
+## Validation
+
+### Before
+
+- Normal streaming chat execution lived inside the response generator.
+- Navigating away from the chat page could terminate the in-flight assistant generation.
+- Completion-side effects such as final message persistence and notifications could be skipped.
+
+### After
+
+- Chat generation is started in background execution and the HTTP response only relays queued SSE events.
+- If the browser disconnects, the consumer detaches but the background chat worker can continue to completion.
+- Final assistant persistence, unread markers, and completion notifications remain reachable even when the user leaves the page.
+
+### Impact Analysis
+
+This fix keeps the current streaming UX for connected users while removing the request lifecycle as the owner of the chat workload. It is intentionally minimal: the existing streaming generator remains the source of chat behavior, and the new bridge only changes how those events are delivered to the browser.
\ No newline at end of file
diff --git a/docs/explanation/fixes/CHAT_STREAM_COMPATIBILITY_SSE_SYNTAX_FIX.md b/docs/explanation/fixes/CHAT_STREAM_COMPATIBILITY_SSE_SYNTAX_FIX.md
new file mode 100644
index 00000000..752f3de1
--- /dev/null
+++ b/docs/explanation/fixes/CHAT_STREAM_COMPATIBILITY_SSE_SYNTAX_FIX.md
@@ -0,0 +1,54 @@
+# Chat Stream Compatibility SSE Syntax Fix
+
+Fixed/Implemented in version: **0.239.134**
+
+## Issue Description
+
+The compatibility branch inside the streaming chat route emitted image-generation thought events through multi-line f-strings that embedded `json.dumps({...})` directly inside the interpolation expression.
+
+In CI, that block was parsed as an unterminated string literal in `route_backend_chats.py`, which stopped the job before runtime tests could begin.
+
+## Root Cause Analysis
+
+The SSE compatibility bridge assembled dictionary literals inline inside an f-string expression across multiple lines.
+
+That formatting is fragile and parser-hostile in this file, especially in automated validation paths that compile the module directly.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_chat_stream_compatibility_sse_syntax.py`
+- `functional_tests/test_chat_stream_background_execution.py`
+- `functional_tests/test_streaming_only_chat_path.py`
+- `functional_tests/test_chat_completion_notifications.py`
+
+### Code Changes Summary
+
+- Replaced the three multi-line SSE `yield` statements in the compatibility image-generation path with explicit payload dictionaries stored in local variables.
+- Kept the emitted SSE payload shape unchanged while making the code parser-safe.
+- Added a regression test that compiles `route_backend_chats.py` and verifies the new payload-variable pattern.
+- Bumped the application version to `0.239.134`.
+
+### Testing Approach
+
+- Added `functional_tests/test_chat_stream_compatibility_sse_syntax.py` to compile the route module and verify the fixed compatibility SSE block.
+- Updated existing streaming-related functional tests so their version checks align with the current app version.
+
+## Validation
+
+### Before
+
+- The compatibility SSE branch embedded multi-line dictionary literals directly inside f-string interpolation.
+- CI could fail during parsing with `SyntaxError: unterminated string literal` near the first compatibility image-generation event.
+
+### After
+
+- The compatibility SSE branch builds JSON payload dictionaries first and interpolates only the serialized variable into the SSE frame.
+- The route module compiles cleanly and preserves the same thought-event content for image-generation compatibility mode.
+
+### Impact Analysis
+
+This is a narrow, low-risk parser-safety fix. It does not change the compatibility mode contract or the streamed payload content, but it does prevent a syntax-level failure that blocked the chat route from loading.
\ No newline at end of file
diff --git a/docs/explanation/fixes/CHAT_STREAM_DEBUG_LOGGING_FIX.md b/docs/explanation/fixes/CHAT_STREAM_DEBUG_LOGGING_FIX.md
new file mode 100644
index 00000000..efbfe68b
--- /dev/null
+++ b/docs/explanation/fixes/CHAT_STREAM_DEBUG_LOGGING_FIX.md
@@ -0,0 +1,52 @@
+# Chat Stream Debug Logging Fix
+
+Fixed/Implemented in version: **0.239.142**
+
+## Issue Description
+
+Normal chat usage goes through `/api/chat/stream`, but the streaming path did not emit enough unconditional `debug_print()` output to make local troubleshooting practical. Startup logging still worked, while key runtime steps in the streaming request and Semantic Kernel orchestration path were too quiet.
+
+## Root Cause Analysis
+
+The codebase still contained many `debug_print()` statements in `route_backend_chats.py`, but many of them were in the non-streaming `/api/chat` handler or inside narrower conditional branches. The frontend chat UI uses the streaming route by default, so important request entry and plugin orchestration events were not consistently visible in the local console.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_chat_stream_debug_logging.py`
+- `functional_tests/test_chat_stream_compatibility_sse_syntax.py`
+- `functional_tests/test_chat_stream_background_execution.py`
+
+### Code Changes Summary
+
+- Added unconditional streaming-route `debug_print()` output for request entry, compatibility-mode routing, normalized request state, model initialization, conversation load/create, and final stream completion.
+- Added explicit streaming Semantic Kernel orchestration logging for plugin invocation clearing, tabular-analysis entry/exit, response-path selection, plugin callback registration, plugin callback execution, and callback deregistration.
+- Bumped the application version to `0.239.142`.
+- Added a regression test that checks the required streaming debug markers remain present.
+- Updated existing streaming regression tests to use the current application version.
+
+### Testing Approach
+
+- Added `functional_tests/test_chat_stream_debug_logging.py` to verify the new streaming debug markers exist.
+- Re-ran existing streaming regression tests covering SSE syntax and background execution.
+
+## Validation
+
+### Before
+
+- Startup debug output proved `debug_print()` still worked.
+- Regular UI chat requests still appeared quiet in the console.
+- Plugin execution visibility depended on hitting narrower branches instead of the main streaming path.
+
+### After
+
+- The streaming route now logs a request summary as soon as `/api/chat/stream` is entered.
+- The console shows which response path was selected, when plugin callbacks were registered and fired, and when the stream finalized.
+- Stream-focused regression tests pass with the updated instrumentation and version bump.
+
+### Impact Analysis
+
+This change is deliberately narrow: it restores operational visibility for the route the frontend already uses, without changing the streaming contract or response payload shape.
\ No newline at end of file
diff --git a/docs/explanation/fixes/CHAT_WORKSPACE_SELECTION_RESET_FIX.md b/docs/explanation/fixes/CHAT_WORKSPACE_SELECTION_RESET_FIX.md
new file mode 100644
index 00000000..4f2fe8d2
--- /dev/null
+++ b/docs/explanation/fixes/CHAT_WORKSPACE_SELECTION_RESET_FIX.md
@@ -0,0 +1,55 @@
+# Chat Workspace Selection Reset Fix
+
+## Fix Title
+Implicit chat conversation creation now preserves selected workspace scope, tags, and documents.
+
+## Issue Description
+When a user arrived on the chat page with workspace context already selected, either by choosing documents manually or by coming from a workspace link that preselected scope, tags, or documents, clicking into the message input created a conversation and immediately reset the workspace filters back to their defaults.
+
+## Root Cause Analysis
+
+1. The chat bootstrap in `chat-onload.js` auto-created a conversation on first input focus when no conversation ID existed.
+2. `createNewConversation()` in `chat-conversations.js` always called `resetScopeLock()` in full reset mode, which restored the scope dropdown to `All` and reloaded document and tag controls.
+3. That full reset path rebuilt the document and tag UI in `chat-documents.js`, which cleared the preselected workspace context before the user sent the first message.
+
+## Version Implemented
+Fixed in version: **0.239.105**
+
+## Files Modified
+
+| File | Change |
+|------|--------|
+| `application/single_app/static/js/chat/chat-documents.js` | Added a preserve-selection mode to `resetScopeLock()` so lock state can be cleared without rebuilding current scope, tag, and document selections. |
+| `application/single_app/static/js/chat/chat-conversations.js` | Added a `preserveSelections` option to `createNewConversation()` and reused in-flight create requests so implicit creation does not race duplicate conversations. |
+| `application/single_app/static/js/chat/chat-onload.js` | Changed implicit auto-create entry points for input focus, prompt selection, and file selection to preserve current workspace filters. |
+| `application/single_app/static/js/chat/chat-input-actions.js` | Updated file-upload auto-create flows to preserve workspace selections. |
+| `application/single_app/static/js/chat/chat-messages.js` | Updated the first-send auto-create flow to preserve workspace selections. |
+| `functional_tests/test_chat_preserves_workspace_selection_on_auto_create.py` | Added regression coverage for preserve-selection reset logic and implicit conversation creation call sites. |
+| `application/single_app/config.py` | Version bump to `0.239.105`. |
+
+## Code Changes Summary
+
+### Preserve Selection State During Implicit Auto-Create
+- Added an options-based preserve path to the scope reset helper so a new conversation can start unlocked without forcing the workspace picker back to `All`.
+- Updated implicit conversation creation flows to request preserved selections.
+
+### Prevent Duplicate Conversation Creation
+- Reused a single in-flight create-conversation request so focus-triggered creation and an immediate send action do not create duplicate empty conversations.
+
+### Keep Explicit Reset Behavior Intact
+- Left the explicit `New Conversation` button on the default reset path so a deliberate fresh chat still restores default workspace scope.
+
+## Testing Approach
+- Added functional regression coverage in `functional_tests/test_chat_preserves_workspace_selection_on_auto_create.py`.
+- Validates that `resetScopeLock()` supports a preserve-selection path.
+- Validates that `createNewConversation()` preserves selections when requested and reuses a pending create request.
+- Validates that implicit creation call sites in the chat bootstrap, first-send flow, and file upload flow all request preserved selections.
+
+## Impact Analysis
+- Users can now click into the message input and continue with the workspace, tag, and document filters they already chose.
+- Workspace deep links remain stable through the first interaction instead of reverting to the default chat scope.
+- Explicit new chat creation still resets to the default workspace scope, so existing fresh-start behavior remains available.
+
+## Validation
+- Before: first focus in the message box could silently reset workspace-related filters before the user sent a message.
+- After: implicit conversation creation preserves the active workspace context, while explicit new chat creation keeps the full reset behavior.
\ No newline at end of file
diff --git a/docs/explanation/fixes/CONTROL_CENTER_GROUP_MANAGER_REFRESHGROUPS_OVERWRITE_FIX.md b/docs/explanation/fixes/CONTROL_CENTER_GROUP_MANAGER_REFRESHGROUPS_OVERWRITE_FIX.md
new file mode 100644
index 00000000..5b226a9d
--- /dev/null
+++ b/docs/explanation/fixes/CONTROL_CENTER_GROUP_MANAGER_REFRESHGROUPS_OVERWRITE_FIX.md
@@ -0,0 +1,57 @@
+# Control Center GroupManager refreshGroups Overwrite Fix
+
+Fixed in version: **0.239.145**
+
+## Issue Description
+
+The embedded `GroupManager` object in `control_center.html` defined `refreshGroups`
+twice. In JavaScript object literals, the later property overwrote the earlier
+one, which triggered the overwritten-property warning and discarded the version
+that showed a loading placeholder before refreshing the groups list.
+
+## Root Cause
+
+An older backward-compatibility alias for `refreshGroups` remained in the same
+object literal after the richer implementation had been added earlier in the
+file. Because both members used the same property name, the later alias replaced
+the intended implementation.
+
+## Files Modified
+
+- `application/single_app/templates/control_center.html`
+- `application/single_app/config.py`
+- `functional_tests/test_control_center_group_manager_refresh_groups_duplicate_fix.py`
+
+## Code Changes Summary
+
+- Removed the duplicate trailing `refreshGroups` alias from `GroupManager`.
+- Kept the single `refreshGroups` implementation that updates the table with a
+ loading message before delegating to `loadGroups()`.
+- Added a regression test that fails if `refreshGroups` is defined more than
+ once in the control center template.
+- Updated the application version to `0.239.145`.
+
+## Testing Approach
+
+- Added a focused functional test that scans the control center template and
+ asserts `refreshGroups` appears exactly once.
+- The same test also verifies the retained implementation still includes the
+ loading placeholder text.
+
+## Impact Analysis
+
+The fix removes a static-analysis warning, preserves the intended refresh UX,
+and reduces the chance of future regressions caused by duplicated object literal
+members in the control center group management script.
+
+## Validation
+
+Before:
+
+- `GroupManager` contained two `refreshGroups` members.
+- The second definition silently overwrote the first.
+
+After:
+
+- `GroupManager` contains a single `refreshGroups` member.
+- The groups table retains the loading placeholder behavior during refresh.
\ No newline at end of file
diff --git a/docs/explanation/fixes/DOCS_JSON_GEM_SECURITY_FIX.md b/docs/explanation/fixes/DOCS_JSON_GEM_SECURITY_FIX.md
new file mode 100644
index 00000000..ebe30eac
--- /dev/null
+++ b/docs/explanation/fixes/DOCS_JSON_GEM_SECURITY_FIX.md
@@ -0,0 +1,57 @@
+# Docs JSON Gem Security Fix
+
+Fixed/Implemented in version: **0.239.136**
+
+## Issue Description
+
+The docs site bundle resolved the Ruby `json` gem to `2.15.0`, which falls in the
+advisory's affected range for format string injection when parsing untrusted JSON
+with `allow_duplicate_key: false`.
+
+## Root Cause Analysis
+
+The docs Jekyll bundle relied on transitive dependency resolution for `json`
+without an explicit minimum version constraint, so `bundle update` had previously
+locked the site to a vulnerable release.
+
+## Technical Details
+
+### Files Modified
+
+- `docs/Gemfile`
+- `docs/Gemfile.lock`
+- `application/single_app/config.py`
+- `functional_tests/test_docs_json_gem_security_fix.py`
+
+### Code Changes Summary
+
+- Added an explicit `json >= 2.19.2` dependency to the docs bundle.
+- Updated the docs lockfile from `json 2.15.0` to `json 2.19.2`.
+- Added a regression test that verifies the Gemfile floor, resolved lockfile version,
+ and application version bump.
+- Bumped the application version to `0.239.136`.
+
+### Testing Approach
+
+- Ran a targeted `bundle update json` in `docs/` to resolve the patched gem version.
+- Added `functional_tests/test_docs_json_gem_security_fix.py` to verify the fix stays
+ in place.
+
+## Validation
+
+### Before
+
+- `docs/Gemfile.lock` resolved `json (2.15.0)`.
+- The patched version was not enforced directly in `docs/Gemfile`.
+
+### After
+
+- `docs/Gemfile.lock` resolves `json (2.19.2)`.
+- `docs/Gemfile` enforces a patched minimum so future dependency refreshes do not
+ drift back into the vulnerable range.
+
+### Impact Analysis
+
+This is a low-risk dependency hardening change scoped to the docs site bundle. It
+does not alter application runtime behavior, but it removes a known vulnerable gem
+version from the repository-managed Ruby dependencies.
\ No newline at end of file
diff --git a/docs/explanation/fixes/EMBEDDING_RATE_LIMIT_WAIT_TIME_FIX.md b/docs/explanation/fixes/EMBEDDING_RATE_LIMIT_WAIT_TIME_FIX.md
new file mode 100644
index 00000000..40b5a9a7
--- /dev/null
+++ b/docs/explanation/fixes/EMBEDDING_RATE_LIMIT_WAIT_TIME_FIX.md
@@ -0,0 +1,44 @@
+# Embedding Rate Limit Wait Time Fix
+
+## Fix Title
+Embedding retries now honor server-provided wait times from Azure OpenAI rate-limit responses.
+
+## Issue Description
+The embedding helpers retried `429 Too Many Requests` failures using only local exponential backoff with jitter. When Azure OpenAI returned a `Retry-After` style header, the application ignored that server guidance and retried on its own schedule.
+
+## Root Cause Analysis
+- `generate_embedding()` and `generate_embeddings_batch()` only used a client-side backoff calculation after `RateLimitError`.
+- The underlying OpenAI/Azure OpenAI `429` response headers were available on the exception response, but the helper never parsed them.
+- As a result, retries could happen earlier than the service requested, increasing the chance of repeated throttling.
+
+## Version Implemented
+Fixed in version: **0.239.116**
+
+## Files Modified
+| File | Change |
+|------|--------|
+| `application/single_app/functions_content.py` | Added retry header parsing and applied it to both embedding retry loops |
+| `functional_tests/test_embedding_rate_limit_wait_time.py` | Added regression coverage for `Retry-After` parsing and embedding retry timing |
+| `application/single_app/config.py` | Version bump to 0.239.116 |
+
+## Code Changes Summary
+- Added a shared helper to parse `retry-after-ms`, `x-ms-retry-after-ms`, and `retry-after` values from rate-limit responses.
+- Updated both single-item and batched embedding generation to prefer the server-provided wait time when it is available and reasonable.
+- Kept the existing jittered exponential backoff as a fallback when the response does not provide a usable retry delay.
+
+## Testing Approach
+- Added `functional_tests/test_embedding_rate_limit_wait_time.py`.
+- The functional test stubs the embedding client and rate-limit exception so it can verify:
+ - Header parsing for millisecond and date-based retry values.
+ - Single embedding retries sleep for the server-provided duration.
+ - Batched embedding retries sleep for the server-provided duration.
+
+## Impact Analysis
+- Embedding retries now align more closely with Azure OpenAI throttling guidance.
+- This reduces avoidable repeat `429` responses during document ingestion and batched embedding creation.
+- Existing fallback behavior remains in place for responses that do not include a usable retry hint.
+
+## Validation
+- Regression test: `functional_tests/test_embedding_rate_limit_wait_time.py`
+- Before: embedding retries always used local backoff, even when the `429` response included a wait time.
+- After: embedding retries use the server-provided wait time when available, then fall back to local backoff only when necessary.
\ No newline at end of file
diff --git a/docs/explanation/fixes/GLOBAL_ACTION_AUDIT_USER_FALLBACK_FIX.md b/docs/explanation/fixes/GLOBAL_ACTION_AUDIT_USER_FALLBACK_FIX.md
new file mode 100644
index 00000000..011091f2
--- /dev/null
+++ b/docs/explanation/fixes/GLOBAL_ACTION_AUDIT_USER_FALLBACK_FIX.md
@@ -0,0 +1,45 @@
+# Global Action Audit User Fallback Fix
+
+## Fix Title
+Global Action Save Path Defaults Missing Audit User IDs
+
+## Issue Description
+`save_global_action()` accepted an optional `user_id`, but callers that omitted it could persist `created_by` and `modified_by` as `null`. This affected flows such as plugin validation repair, which saves plugin manifests through the global action helper without explicitly passing a user ID.
+
+## Root Cause Analysis
+- `save_global_action()` never mirrored the existing `save_global_agent()` behavior that resolves `user_id` through `get_current_user_id()` when the caller passes `None`.
+- The helper wrote audit fields directly from the unresolved `user_id`, so create operations stored `null` values.
+- Update operations preserved an existing `created_by` value even when it was already `null`, which meant previously corrupted audit data could survive indefinitely.
+
+## Version Implemented
+Fixed in version: **0.239.103**
+
+## Files Modified
+| File | Change |
+|------|--------|
+| `application/single_app/functions_global_actions.py` | Default missing `user_id` from `get_current_user_id()`, fall back to `system`, and repair null `created_by` on update |
+| `functional_tests/test_global_action_user_audit_fallback.py` | Added regression coverage for create and update audit-field fallback behavior |
+| `application/single_app/config.py` | Version bump to 0.239.103 |
+
+## Code Changes Summary
+- Imported `get_current_user_id()` into `functions_global_actions.py`.
+- When `user_id` is `None`, the helper now resolves the current authenticated user.
+- If no authenticated user is available, the helper falls back to `system` so audit fields remain non-null.
+- Existing actions with `created_by=None` are repaired on save by substituting the resolved fallback value.
+
+## Testing Approach
+- Added `functional_tests/test_global_action_user_audit_fallback.py`.
+- The test stubs the config, authentication, and Key Vault dependencies so it can exercise `save_global_action()` directly.
+- Coverage verifies both:
+ - Create flow uses `get_current_user_id()` when `user_id` is omitted.
+ - Update flow repairs a previously null `created_by` and falls back to `system` when no current user exists.
+
+## Impact Analysis
+- Global plugin/action saves now produce stable audit metadata even for internal or repair flows that do not pass a user ID explicitly.
+- Existing global actions with missing `created_by` values are corrected the next time they are saved.
+- No route or payload contract changes were introduced.
+
+## Validation
+- Regression test: `functional_tests/test_global_action_user_audit_fallback.py`
+- Before: `created_by` and `modified_by` could be stored as `null`.
+- After: both fields resolve to the current user ID or `system`.
\ No newline at end of file
diff --git a/docs/explanation/fixes/GROUP_PUBLIC_WORKSPACE_EXPANDED_TAGS_FIX.md b/docs/explanation/fixes/GROUP_PUBLIC_WORKSPACE_EXPANDED_TAGS_FIX.md
new file mode 100644
index 00000000..2f255b9a
--- /dev/null
+++ b/docs/explanation/fixes/GROUP_PUBLIC_WORKSPACE_EXPANDED_TAGS_FIX.md
@@ -0,0 +1,47 @@
+# Group/Public Workspace Expanded Tags Fix
+
+## Fix Title
+Group and public workspace expanded list rows now display document tags like the personal workspace.
+
+## Issue Description
+When a user expanded a document in list view inside a group workspace or public workspace, the metadata panel omitted the document's tags. Personal workspace already showed tags in the same expanded view, and the backend APIs for group and public workspaces were already returning each document's `tags` array.
+
+## Root Cause Analysis
+- The group workspace expanded-details renderer in `group_workspaces.html` never added a `Tags:` row.
+- The public workspace expanded-details renderer in `public_workspace.js` had the same omission.
+- Both workspaces already loaded workspace tag definitions and document tag arrays for filtering and metadata editing, so the gap was limited to list-view UI rendering rather than missing backend data.
+
+## Version Implemented
+Fixed in version: **0.239.113**
+
+## Files Modified
+| File | Change |
+|------|--------|
+| `application/single_app/templates/group_workspaces.html` | Added a local tag badge renderer and inserted a `Tags:` row into expanded list-view document details. |
+| `application/single_app/static/js/public/public_workspace.js` | Added a local tag badge renderer and inserted a `Tags:` row into expanded list-view document details. |
+| `functional_tests/test_group_public_workspace_expanded_tags.py` | Added regression coverage for the group/public expanded tag rows and helper usage. |
+| `application/single_app/config.py` | Version bump to `0.239.113`. |
+
+## Code Changes Summary
+- Added `renderGroupTagBadges()` in the group workspace page and `renderPublicTagBadges()` in the public workspace script.
+- Reused existing workspace tag definitions and color utilities so tags render with configured colors when available.
+- Added a neutral fallback badge color for unknown tag definitions and `No tags` text when a document has no tags.
+- Inserted the new `Tags:` row between `Keywords:` and `Abstract:` to match the personal workspace expanded-details layout.
+
+## Testing Approach
+- Added `functional_tests/test_group_public_workspace_expanded_tags.py`.
+- The test validates that:
+ - Personal workspace still provides the parity reference for expanded tag rendering.
+ - Group workspace defines a local badge helper and renders a `Tags:` row in expanded document details.
+ - Public workspace defines a local badge helper and renders a `Tags:` row in expanded document details.
+ - The `Tags:` row appears between `Keywords:` and `Abstract:` in both renderers.
+
+## Impact Analysis
+- Group workspace users now see document tags immediately when expanding a file in list view.
+- Public workspace users now get the same visibility without needing to open metadata editing flows.
+- The experience is now consistent across personal, group, and public workspaces while keeping backend contracts unchanged.
+
+## Validation
+- Before: group and public expanded list rows showed metadata such as version, authors, keywords, and abstract, but omitted tags.
+- After: both workspaces render color-coded tag badges or a `No tags` fallback in the expanded details row.
+- Regression test: `functional_tests/test_group_public_workspace_expanded_tags.py`
\ No newline at end of file
diff --git a/docs/explanation/fixes/OPENAPI_URL_IMPORT_REMOVAL_FIX.md b/docs/explanation/fixes/OPENAPI_URL_IMPORT_REMOVAL_FIX.md
new file mode 100644
index 00000000..f1b6f751
--- /dev/null
+++ b/docs/explanation/fixes/OPENAPI_URL_IMPORT_REMOVAL_FIX.md
@@ -0,0 +1,67 @@
+# OpenAPI URL Import Removal Fix
+
+Fixed/Implemented in version: **0.239.143**
+
+## Issue Description
+
+SimpleChat had moved the OpenAPI plugin UI to an upload-only workflow, but the backend still exposed authenticated URL import endpoints and legacy URL-based plugin creation paths.
+
+That mismatch left dead functionality in place and preserved an unnecessary server-side URL fetch surface that was no longer part of the supported product flow.
+
+## Root Cause Analysis
+
+The frontend plugin modal had already standardized on uploaded OpenAPI file content, but the backend still retained:
+
+- `/api/openapi/validate-url`
+- `/api/openapi/download-from-url`
+- URL-fetch validation helpers in `openapi_security.py`
+- a deprecated `openapi_source_type == 'url'` branch in the OpenAPI plugin factory
+
+Because those code paths still existed, authenticated callers could continue to exercise an unsupported backend URL import path even though the web UI no longer offered it.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_openapi.py`
+- `application/single_app/openapi_security.py`
+- `application/single_app/semantic_kernel_plugins/openapi_plugin_factory.py`
+- `application/single_app/config.py`
+- `docs/explanation/features/v0.229.001/OPENAPI_ACTION.md`
+- `functional_tests/test_openapi_upload_only_flow.py`
+
+### Code Changes Summary
+
+- Removed the backend URL import endpoints for validating and downloading OpenAPI specifications from remote URLs.
+- Removed URL-fetch validation helpers from the OpenAPI security validator so it now focuses on uploaded file content only.
+- Removed deprecated URL-based plugin factory handling to align runtime behavior with the upload/content-based configuration flow.
+- Updated the OpenAPI feature documentation to reflect the supported upload-only workflow.
+- Added regression coverage to ensure URL import routes and URL source handling do not return unintentionally.
+- Bumped the application version to `0.239.143`.
+
+### Testing Approach
+
+- Added `functional_tests/test_openapi_upload_only_flow.py` to verify the backend no longer exposes URL import routes or URL-based factory handling.
+- The regression test also checks that the frontend still requires uploaded OpenAPI content and that the config version matches the implementation.
+
+## Validation
+
+### Before
+
+- The modal required an uploaded OpenAPI file.
+- The backend still registered authenticated URL import endpoints.
+- The plugin factory still contained a deprecated URL source path.
+
+### After
+
+- OpenAPI configuration is consistently upload-only across the frontend and backend.
+- The unsupported server-side URL import surface has been removed.
+- The factory and documentation now match the supported content-based plugin configuration flow.
+
+### Impact Analysis
+
+This change is intentionally narrow:
+
+- the supported upload workflow remains unchanged
+- frontend configuration still stores validated OpenAPI spec content directly
+- only dead URL import behavior was removed
\ No newline at end of file
diff --git a/docs/explanation/fixes/PER_MESSAGE_WORD_EXPORT_ROUTE_FIX.md b/docs/explanation/fixes/PER_MESSAGE_WORD_EXPORT_ROUTE_FIX.md
new file mode 100644
index 00000000..7b3ab4dc
--- /dev/null
+++ b/docs/explanation/fixes/PER_MESSAGE_WORD_EXPORT_ROUTE_FIX.md
@@ -0,0 +1,49 @@
+# Per-Message Word Export Route Fix
+
+Fixed/Implemented in version: **0.239.128**
+
+## Issue Description
+
+The chat message dropdown still offered "Export to Word", but requests to `POST /api/message/export-word` returned `405 METHOD NOT ALLOWED`.
+
+## Root Cause Analysis
+
+The backend export module no longer registered the explicit `/api/message/export-word` route even though the frontend, release notes, feature documentation, and functional tests still referenced it.
+
+Because the explicit route was missing, Flask matched the request path against the generic `/api/message/` route instead. That route only supports `DELETE`, so a `POST` to `/api/message/export-word` failed with `405`.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_conversation_export.py`
+- `application/single_app/config.py`
+- `functional_tests/test_per_message_export.py`
+
+### Code Changes Summary
+
+- Restored the explicit `POST /api/message/export-word` route in the conversation export module.
+- Added DOCX rendering helpers for single-message export, including basic markdown formatting and citation output.
+- Added regression coverage to verify that the backend source continues to define the explicit route.
+- Bumped the application version to `0.239.128`.
+
+### Testing Approach
+
+- Extended `functional_tests/test_per_message_export.py` with an AST-based regression check for the missing backend route.
+- Preserved the existing content normalization and Word document generation checks for the per-message export feature.
+
+## Validation
+
+### Before
+
+- `POST /api/message/export-word` returned `405 METHOD NOT ALLOWED`.
+- The frontend Word export action could not download a `.docx` file.
+
+### After
+
+- `POST /api/message/export-word` is explicitly registered again.
+- The frontend request can resolve to the intended Word export handler instead of the generic message delete route.
+
+### User Experience Improvement
+
+Users can export a single chat message to Word from the message dropdown without hitting a method error.
\ No newline at end of file
diff --git a/docs/explanation/fixes/PILLOW_PSD_UPLOAD_HARDENING_FIX.md b/docs/explanation/fixes/PILLOW_PSD_UPLOAD_HARDENING_FIX.md
new file mode 100644
index 00000000..7b9a3e8d
--- /dev/null
+++ b/docs/explanation/fixes/PILLOW_PSD_UPLOAD_HARDENING_FIX.md
@@ -0,0 +1,46 @@
+# Pillow PSD Upload Hardening Fix
+
+Fixed/Implemented in version: **0.239.134**
+
+## Issue Description
+
+The application pinned Pillow to `11.1.0`, which falls in the vulnerable range for an out-of-bounds write when parsing specially crafted PSD images.
+
+Although the admin settings page only intends to accept PNG and JPEG uploads for logos and favicons, those uploads were still passed directly to `Image.open(...)` without an explicit decoder allowlist.
+
+## Root Cause Analysis
+
+- `application/single_app/requirements.txt` pinned Pillow to a vulnerable version.
+- `application/single_app/route_frontend_admin_settings.py` relied on filename extensions before calling Pillow, but did not constrain Pillow to the actual image formats the route supports.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/requirements.txt`
+- `application/single_app/route_frontend_admin_settings.py`
+- `application/single_app/config.py`
+- `functional_tests/test_pillow_psd_upload_hardening.py`
+
+### Code Changes Summary
+
+- Updated the Pillow dependency pin to `12.1.1`.
+- Added `open_allowed_uploaded_image(...)` so admin logo and favicon uploads only open through Pillow with `PNG` and `JPEG` decoders enabled.
+- Reused that helper for standard logo, dark-mode logo, and favicon uploads.
+- Bumped the application version to `0.239.134`.
+
+### Testing Approach
+
+- Added `functional_tests/test_pillow_psd_upload_hardening.py` to verify the patched dependency pin, the route-level Pillow format allowlist, and the version bump.
+
+## Validation
+
+### Before
+
+- The app installed a Pillow version in the vulnerable range.
+- A disguised PSD upload could still be handed to Pillow from the admin image upload route.
+
+### After
+
+- The app pins Pillow to the patched version.
+- The admin image upload route now restricts Pillow parsing to the PNG and JPEG formats already allowed by the UI.
\ No newline at end of file
diff --git a/docs/explanation/fixes/REASONING_EFFORT_INITIAL_SYNC_FIX.md b/docs/explanation/fixes/REASONING_EFFORT_INITIAL_SYNC_FIX.md
new file mode 100644
index 00000000..b6efec4f
--- /dev/null
+++ b/docs/explanation/fixes/REASONING_EFFORT_INITIAL_SYNC_FIX.md
@@ -0,0 +1,45 @@
+# Reasoning Effort Initial Sync Fix
+
+## Fix Title
+Reasoning effort button now reflects the saved level for the selected model on the first chat-page load.
+
+## Issue Description
+The reasoning effort button could show the wrong initial icon and tooltip when a user first opened the chat page. If the user opened the reasoning modal and changed the level, the button updated immediately and stayed correct for the rest of that session.
+
+## Root Cause Analysis
+- `chat-onload.js` applied the preferred model only after a broader startup `Promise.all()` that also waited on document and prompt loading.
+- `chat-reasoning.js` fetched user settings independently and initialized the reasoning button as soon as its own request resolved.
+- When the reasoning settings request finished before the preferred model was applied, the button synced itself against the default model instead of the user's actual selected model.
+- Because the preferred model was assigned programmatically without a later reasoning-state refresh, the stale icon and tooltip remained until the user changed the reasoning level manually.
+
+## Version Implemented
+Fixed in version: **0.239.125**
+
+## Files Modified
+| File | Change |
+|------|--------|
+| `application/single_app/static/js/chat/chat-onload.js` | Applied user settings earlier in startup and initialized reasoning after the preferred model is set |
+| `application/single_app/static/js/chat/chat-reasoning.js` | Added deterministic reasoning-state sync using already-loaded settings |
+| `functional_tests/test_reasoning_effort_initial_sync.py` | Added regression coverage for the startup ordering and reasoning sync path |
+| `functional_tests/test_chat_searchable_selectors.py` | Updated version metadata/assertion for the new release |
+| `functional_tests/test_workspace_scope_prompts_fix.py` | Updated version metadata/assertion for the new release |
+| `application/single_app/config.py` | Version bump to 0.239.125 |
+
+## Code Changes Summary
+- Added a shared reasoning-state sync path so the reasoning button can be refreshed explicitly for the current model.
+- Updated reasoning initialization to accept already-loaded user settings instead of always starting a second settings fetch race.
+- Changed chat startup so the preferred model is applied before the reasoning toggle is initialized.
+
+## Testing Approach
+- Added `functional_tests/test_reasoning_effort_initial_sync.py`.
+- Updated existing versioned functional tests so their release assertions match the new config version.
+
+## Impact Analysis
+- The reasoning button now shows the saved effort level and tooltip immediately for the active model on initial page load.
+- Startup remains responsive because user settings are still loaded in parallel with document and prompt data.
+- Real-time reasoning updates after a manual change continue to work through the existing model-change and save flow.
+
+## Validation
+- Regression test: `functional_tests/test_reasoning_effort_initial_sync.py`
+- Before: the first visible reasoning icon could reflect the wrong model context until the user manually changed reasoning.
+- After: reasoning state is synchronized after the preferred model is applied, so the initial button state matches the saved setting.
\ No newline at end of file
diff --git a/docs/explanation/fixes/REDUNDANT_CONVERSATION_ID_ASSIGNMENT_FIX.md b/docs/explanation/fixes/REDUNDANT_CONVERSATION_ID_ASSIGNMENT_FIX.md
new file mode 100644
index 00000000..ea588b24
--- /dev/null
+++ b/docs/explanation/fixes/REDUNDANT_CONVERSATION_ID_ASSIGNMENT_FIX.md
@@ -0,0 +1,36 @@
+# Redundant Conversation ID Assignment Fix
+
+Fixed/Implemented in version: **0.239.148**
+
+## Issue Description
+
+A standalone assignment in `route_backend_chats.py` reassigned `conversation_id` to itself during chat request processing.
+
+## Root Cause Analysis
+
+The statement `conversation_id = conversation_id` was left in a setup block where nearby lines initialize local state. Because it had no effect, it only introduced a redundant-assignment warning and suggested a likely copy-paste mistake.
+
+## Technical Details
+
+- Files modified:
+ - `application/single_app/route_backend_chats.py`
+ - `application/single_app/config.py`
+ - `functional_tests/test_route_backend_chats_redundant_assignment.py`
+- Code changes summary:
+ - Removed the no-op `conversation_id = conversation_id` assignment from the chat handling path.
+ - Added a functional test that parses `route_backend_chats.py` with `ast` and fails if any standalone self-assignment remains.
+ - Bumped the application version to `0.239.148`.
+- Testing approach:
+ - Added a targeted regression test for standalone self-assignment detection.
+
+## Impact Analysis
+
+Removing the redundant assignment does not change runtime behavior because the previous statement had no effect. It does remove a misleading warning and reduces the chance of masking a real state-initialization bug later.
+
+## Validation
+
+- Before:
+ - `route_backend_chats.py` contained a standalone `conversation_id = conversation_id` statement.
+- After:
+ - The redundant assignment is removed.
+ - A regression test now checks the file for the same class of no-op assignment.
diff --git a/docs/explanation/fixes/SQL_PLUGIN_KEY_VAULT_SECRET_STORAGE_FIX.md b/docs/explanation/fixes/SQL_PLUGIN_KEY_VAULT_SECRET_STORAGE_FIX.md
new file mode 100644
index 00000000..dcee21df
--- /dev/null
+++ b/docs/explanation/fixes/SQL_PLUGIN_KEY_VAULT_SECRET_STORAGE_FIX.md
@@ -0,0 +1,55 @@
+# SQL Plugin Key Vault Secret Storage Fix
+
+## Fix Title
+SQL plugin credentials now use Azure Key Vault secret storage when it is enabled.
+
+## Issue Description
+SQL plugin configuration stored sensitive values such as `connection_string` and `password` directly in plugin manifests because those fields did not pass through the existing plugin Key Vault helper. The helper already supported `auth.key` and dynamic `__Secret` additional fields, but SQL credentials used regular field names, so Key Vault-enabled deployments still left SQL secrets in stored plugin data.
+
+## Root Cause Analysis
+- The shared plugin Key Vault helper only recognized `auth.key` and additional field names ending in `__Secret`.
+- SQL plugins used standard additional field names like `connection_string` and `password`, so those values bypassed Key Vault storage.
+- Edit flows returned `Stored_In_KeyVault` placeholders to the browser, but several save and delete paths did not reliably load the stored Key Vault reference names during updates and deletes.
+- The personal workspace bulk-save flow dropped plugin ids, which made rename and placeholder-preservation scenarios unreliable.
+
+## Version Implemented
+Fixed in version: **0.239.114**
+
+## Files Modified
+| File | Change |
+|------|--------|
+| `application/single_app/functions_keyvault.py` | Added SQL secret-field handling for plugin save/get/delete helpers and a shared plugin redaction helper |
+| `application/single_app/functions_personal_actions.py` | Preserved existing Key Vault references during personal action updates and deletes |
+| `application/single_app/functions_global_actions.py` | Preserved existing Key Vault references during global action updates and deletes |
+| `application/single_app/functions_group_actions.py` | Passed existing group action manifests into the Key Vault helper for placeholder-preserving updates |
+| `application/single_app/route_backend_plugins.py` | Preserved personal plugin ids during bulk saves, resolved stored SQL Key Vault secrets for edit-time connection tests, removed delete-then-save global edit behavior, and redacted plugin logs |
+| `application/single_app/static/js/plugin_modal_stepper.js` | Fixed SQL edit population, mapped SQL service-principal auth to the shared auth schema, and sent scope/id context for edit-time SQL connection tests |
+| `application/single_app/static/js/workspace/workspace_plugins.js` | Preserved plugin ids across personal workspace edits so stored Key Vault references survive rename/update flows |
+| `application/single_app/semantic_kernel_plugins/plugin_health_checker.py` | Validated SQL manifests using nested `additionalFields` values as well as top-level fields |
+| `functional_tests/test_sql_plugin_key_vault_secret_storage.py` | Added regression coverage for helper behavior and personal/global/group wrapper flows |
+| `application/single_app/config.py` | Version bump to 0.239.114 |
+
+## Code Changes Summary
+- SQL plugin `connection_string` and `password` additional fields are now treated as secret-bearing fields by the shared plugin Key Vault helper.
+- Existing stored Key Vault references are preserved during edit flows instead of being regenerated or dropped when the UI submits `Stored_In_KeyVault` placeholders.
+- Personal workspace plugin edits now preserve plugin ids so updates can target the existing stored document even when the plugin name changes.
+- The SQL connection test endpoint can now resolve previously stored Key Vault-backed SQL secrets during edit flows without forcing the user to re-enter them.
+- Plugin logging now redacts secret-bearing values before writing plugin manifests to logs.
+
+## Testing Approach
+- Added `functional_tests/test_sql_plugin_key_vault_secret_storage.py`.
+- The functional test stubs Key Vault, Cosmos, and action-helper dependencies so it can exercise:
+ - Shared SQL Key Vault secret save/get/delete behavior.
+ - Placeholder-preserving personal action save/delete flows.
+ - Placeholder-preserving global and group action save/delete flows.
+
+## Impact Analysis
+- New and updated SQL plugins now store secret-bearing configuration in Key Vault when `enable_key_vault_secret_storage` is enabled.
+- Existing plaintext SQL plugin records are not backfilled automatically; they remain unchanged until the plugin is saved again.
+- Edit flows for SQL plugins no longer require re-entering an unchanged stored connection string or password just to test or save the plugin.
+- Non-SQL plugin Key Vault behavior for `auth.key` and `additionalFields.*__Secret` remains intact.
+
+## Validation
+- Regression test: `functional_tests/test_sql_plugin_key_vault_secret_storage.py`
+- Before: SQL plugin `connection_string` and `password` values could remain in stored plugin data even when Key Vault was enabled.
+- After: those values are stored as Key Vault references, resolved at runtime and test time, preserved across edits, and cleaned up on delete.
\ No newline at end of file
diff --git a/docs/explanation/fixes/SQL_QUERY_PLUGIN_SCHEMA_AWARENESS_FIX.md b/docs/explanation/fixes/SQL_QUERY_PLUGIN_SCHEMA_AWARENESS_FIX.md
new file mode 100644
index 00000000..9713a62c
--- /dev/null
+++ b/docs/explanation/fixes/SQL_QUERY_PLUGIN_SCHEMA_AWARENESS_FIX.md
@@ -0,0 +1,160 @@
+# SQL Query Plugin Schema Awareness Fix
+
+## Fix Title
+SQL Query Plugin - Schema Awareness, Companion Plugin Auto-Creation, and Workflow Guidance
+
+## Issue Description
+When users asked database-related questions (e.g., "what is user1 licensed to use?"), agents connected to SQL databases would ask for clarification about table/column names instead of querying the database directly. The agent had no citations, meaning it never actually called any database tools.
+
+## Root Cause Analysis
+
+### Original Root Causes (v0.239.014)
+Three interconnected issues caused the initial failure:
+
+1. **Generic `@kernel_function` descriptions**: The SQL Query and SQL Schema plugin function descriptions were terse and generic (e.g., "Execute a SQL query and return results"). They provided no workflow guidance telling the LLM to discover the schema first before writing queries.
+
+2. **No schema context in agent instructions**: Agent instructions were passed through verbatim from configuration with no automatic injection of database schema information.
+
+3. **Independent, disconnected plugins**: The SQL Schema Plugin and SQL Query Plugin operated as completely independent plugins with no linkage.
+
+### Deeper Root Causes Discovered (v0.239.015)
+The v0.239.014 fix improved descriptions but actually made things **worse** because:
+
+4. **No companion schema plugin was ever loaded**: The ESAM agent only had ONE action configured (`sql_query` type). No `sql_schema` action existed in the agent's actions. The `_create_sql_plugin()` method creates exactly what the manifest requests — so only `SQLQueryPlugin` was loaded, never `SQLSchemaPlugin`.
+
+5. **Descriptions demanded non-existent functions**: The v0.239.014 descriptions said "you MUST first call get_database_schema or get_table_list from the SQL Schema plugin" — but those functions didn't exist in the kernel since no schema plugin was loaded. This created an **impossible dependency** that made the LLM ask for clarification instead.
+
+6. **Schema extraction found nothing**: `_extract_sql_schema_for_instructions()` only searched for `SQLSchemaPlugin` instances. Since none existed in the kernel, it returned an empty string, so no schema was injected into agent instructions.
+
+7. **SQLPluginFactory was disconnected**: The `SQLPluginFactory` class was designed to create `(SQLSchemaPlugin, SQLQueryPlugin)` pairs, but was never called by the `LoggedPluginLoader` pipeline.
+
+### Empty Schema Tables from INFORMATION_SCHEMA (v0.239.016)
+After the v0.239.015 fix, the agent could answer simple queries (e.g., "what is user1 licensed to use?" correctly returned Office 365 license data). However, complex multi-table JOIN queries (e.g., "which department is spending the most on licensing?") still failed because:
+
+8. **INFORMATION_SCHEMA views returned empty results on Azure SQL**: The `_get_tables_query()`, `_get_columns_query()`, and `_get_primary_keys_query()` methods used `INFORMATION_SCHEMA.TABLES`, `INFORMATION_SCHEMA.COLUMNS`, and `INFORMATION_SCHEMA.KEY_COLUMN_USAGE` respectively. These views returned **zero rows** in this Azure SQL environment, even though the database contained 5 user tables.
+
+9. **sys.\* catalog views worked correctly**: The `_get_relationships_data()` method used `sys.foreign_keys`, `sys.tables`, and `sys.columns` — and successfully returned 4 foreign key relationships. This proved the database connection and permissions were fine, but `INFORMATION_SCHEMA` access was restricted or misconfigured.
+
+10. **pyodbc.Row type mismatch**: The table iteration code used `isinstance(table, tuple)` to check row types, but `pyodbc.Row` objects may not pass this check depending on the pyodbc version. When `isinstance` returned `False`, the code fell into an `else` branch that assigned the entire Row object as the table name, causing subsequent SQL queries to fail silently in the exception handler.
+
+11. **Result**: `get_database_schema` returned `{'tables': {}, 'relationships': [4 items]}` — the agent had foreign key metadata but no table/column definitions, making it impossible to construct multi-table JOINs.
+
+## Version Implemented
+**Initial fix in version: 0.239.014**
+**Companion plugin fix in version: 0.239.015**
+**Schema catalog views fix in version: 0.239.016**
+
+## Files Modified
+
+| File | Change |
+|------|--------|
+| `application/single_app/semantic_kernel_plugins/sql_schema_plugin.py` | Rewrote all `@kernel_function` descriptions with prescriptive workflow guidance (v0.239.014); migrated all SQL Server queries from INFORMATION_SCHEMA to sys.\* catalog views and fixed pyodbc.Row handling (v0.239.016) |
+| `application/single_app/semantic_kernel_plugins/sql_query_plugin.py` | Rewrote all `@kernel_function` descriptions with resilient conditional guidance (v0.239.015); added `query_database` convenience function (v0.239.014); updated `metadata` property description |
+| `application/single_app/semantic_kernel_loader.py` | Added `_extract_sql_schema_for_instructions()` helper function; auto-injects database schema into agent instructions; added SQLQueryPlugin fallback detection (v0.239.015) |
+| `application/single_app/semantic_kernel_plugins/logged_plugin_loader.py` | Enabled SQL plugin creation path (v0.239.014); added `_auto_create_companion_schema_plugin()` method that auto-creates a SQLSchemaPlugin whenever a SQLQueryPlugin is loaded (v0.239.015) |
+| `application/single_app/config.py` | Version bump to 0.239.016 |
+
+## Code Changes Summary
+
+### v0.239.014 Changes
+
+#### 1. Prescriptive Function Descriptions (sql_schema_plugin.py)
+- `get_database_schema`: Now says "ALWAYS call this function FIRST before executing any SQL queries"
+- `get_table_list`: Now says "Use this function first to discover which tables are available"
+- `get_table_schema`: Now says "Call this after discovering tables via get_database_schema or get_table_list"
+- `get_relationships`: Now says "Use this to understand how tables connect via JOIN conditions"
+
+#### 2. New `query_database` Convenience Function (sql_query_plugin.py)
+- Accepts `question` (natural language) and `query` (SQL) parameters
+- Returns results with the original question context for better LLM response formatting
+
+#### 3. Auto Schema Injection (semantic_kernel_loader.py)
+- New `_extract_sql_schema_for_instructions()` function detects SQL Schema plugins in the kernel
+- Calls `get_database_schema()` at agent load time to fetch full schema
+- Formats schema as markdown tables (table names, columns, types, relationships)
+- Appends schema to agent instructions with directive: "Do NOT ask the user for table or column names"
+
+#### 4. Enabled SQL Plugin Creation Path (logged_plugin_loader.py)
+- Uncommented the `elif plugin_type in ['sql_schema', 'sql_query']` branch
+
+### v0.239.015 Changes (Complete Fix)
+
+#### 5. Auto-Create Companion Schema Plugin (logged_plugin_loader.py)
+- New `_auto_create_companion_schema_plugin()` method
+- When a `sql_query` plugin is loaded, automatically creates a companion `SQLSchemaPlugin` using the same connection details
+- Derives schema plugin name: `enterprise_software_asset_management_query` → `enterprise_software_asset_management_schema`
+- Checks if the companion already exists (idempotent)
+- Enables logging, wraps functions, registers with kernel
+- This is the **critical fix** — ensures schema discovery is always available even when only `sql_query` is configured
+
+#### 6. Resilient Function Descriptions (sql_query_plugin.py)
+- Changed from "you MUST first call get_database_schema" to "If the database schema is provided in your instructions, use those exact table and column names. If no schema is available, call get_database_schema"
+- This dual-path approach works whether schema is injected in instructions OR available via schema plugin functions
+
+#### 7. SQLQueryPlugin Fallback in Schema Extraction (semantic_kernel_loader.py)
+- Added fallback in `_extract_sql_schema_for_instructions()` that also detects `SQLQueryPlugin` instances
+- If no `SQLSchemaPlugin` is found, creates a temporary `SQLSchemaPlugin` from the query plugin's connection config
+- Belt-and-suspenders safety net in case companion auto-creation fails
+- Appends schema to agent instructions with directive: "Do NOT ask the user for table or column names"
+- This ensures the LLM ALWAYS has schema context even if it doesn't call the schema plugin
+
+### 4. Enabled SQL Plugin Creation Path (logged_plugin_loader.py)
+- Uncommented the `elif plugin_type in ['sql_schema', 'sql_query']` branch
+- SQL plugins now use the explicit `_create_sql_plugin()` method instead of generic discovery fallback
+
+### v0.239.016 Changes (Schema Catalog Views Fix)
+
+#### 8. Migrated SQL Server Queries to sys.\* Catalog Views (sql_schema_plugin.py)
+- **`_get_tables_query()`**: Replaced `INFORMATION_SCHEMA.TABLES` with `sys.tables t INNER JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE t.type = 'U'`
+- **`_get_columns_query()`**: Replaced `INFORMATION_SCHEMA.COLUMNS` with `sys.columns c INNER JOIN sys.tables t ... LEFT JOIN sys.default_constraints dc ...` using `TYPE_NAME(c.user_type_id)` for data type resolution
+- **`_get_primary_keys_query()`**: Replaced `INFORMATION_SCHEMA.KEY_COLUMN_USAGE` with `sys.index_columns ic INNER JOIN sys.indexes i ... WHERE i.is_primary_key = 1`
+- This makes all SQL Server schema queries consistent with `_get_relationships_data()`, which already used sys.\* views successfully
+
+#### 9. Robust pyodbc.Row Handling (sql_schema_plugin.py)
+- **`get_database_schema()`**: Replaced `isinstance(table, tuple)` checks with try/except indexing; all row field values cast to `str()` before use as dict keys
+- **`get_table_list()`**: Same robust Row handling pattern applied to table row iteration
+- **`_get_table_schema_data()`**: Primary key list comprehension updated to use `str(pk[0])` without isinstance checks
+- This ensures the code works correctly regardless of whether `pyodbc.Row` inherits from `tuple` in the installed pyodbc version
+
+## Testing Approach
+- Functional test (v0.239.014): `functional_tests/test_sql_query_plugin_schema_awareness.py`
+- Functional test (v0.239.015): `functional_tests/test_sql_auto_schema_companion.py`
+- Validates `_auto_create_companion_schema_plugin` method exists with correct signature
+- Confirms companion creation is triggered in `load_plugin_from_manifest` for `sql_query` type
+- Verifies schema plugin name derivation logic (`_query` → `_schema` suffix swap)
+- Checks `@kernel_function` descriptions are resilient (no hard dependency on non-existent functions)
+- Validates `_extract_sql_schema_for_instructions` has SQLQueryPlugin fallback
+- Confirms version updated to 0.239.015
+- Functional test (v0.239.016): `functional_tests/test_sql_schema_sys_catalog_views.py`
+- Validates all SQL Server queries use `sys.tables`, `sys.columns`, `sys.indexes` instead of `INFORMATION_SCHEMA`
+- Confirms pyodbc.Row-safe iteration (no `isinstance(table, tuple)` checks)
+- Verifies primary key query uses `sys.index_columns` with `is_primary_key = 1`
+- Checks PostgreSQL/MySQL/SQLite queries remain unchanged
+- Confirms version updated to 0.239.016
+
+## Impact Analysis
+- **SQL-connected agents**: Will now automatically have BOTH a query plugin AND a companion schema plugin, even when only `sql_query` is configured. Schema is injected into agent instructions at load time.
+- **Non-SQL agents**: Completely unaffected (companion creation only triggers for `sql_query` type)
+- **LogAnalytics agents**: Unaffected (different plugin type)
+- **Performance**: One-time schema fetch at agent load time adds minimal latency; schema is cached in instructions for the session
+- **Backwards compatible**: If both `sql_query` and `sql_schema` actions are explicitly configured, the companion auto-creation is skipped (checks for existing plugin)
+
+## Before/After Comparison
+
+### Before (v0.239.014)
+- User: "What is user1 licensed to use?"
+- Agent: "I need the exact user identifier... Please provide the identifier..." (no database call, no citations)
+- Root cause: Descriptions demanded calling schema functions that didn't exist in the kernel
+
+### After v0.239.015
+- User: "What is user1 licensed to use?"
+- Agent: Correctly returns Office 365 license data with LicenseID 1, TotalQuantity 52 (simple single-table queries work)
+- User: "Which department is spending the most on licensing?"
+- Agent: Fails — says "I don't see a department dimension in the current schema" and calls `get_database_schema` which returns `{'tables': {}}` (empty)
+- Root cause: INFORMATION_SCHEMA views returned no results on Azure SQL
+
+### After v0.239.016
+- User: "What is user1 licensed to use?"
+- Agent: Correctly returns license data (still works)
+- User: "Which department is spending the most on licensing?"
+- Agent: Has full schema with all 5 tables and their columns → can construct multi-table JOINs (Licenses → Procurements for cost, Usage for department) → returns department-level spending analysis
diff --git a/docs/explanation/fixes/STREAMING_ONLY_CHAT_PATH_FIX.md b/docs/explanation/fixes/STREAMING_ONLY_CHAT_PATH_FIX.md
new file mode 100644
index 00000000..9c6dc831
--- /dev/null
+++ b/docs/explanation/fixes/STREAMING_ONLY_CHAT_PATH_FIX.md
@@ -0,0 +1,74 @@
+# Streaming-Only Chat Path Fix
+
+Fixed in version: **0.239.127**
+
+## Issue Description
+
+The chat experience still maintained two first-party execution paths:
+
+- A streaming SSE path for normal chat responses.
+- A legacy non-streaming JSON path used as a direct fallback by the main send flow, retry flow, and edit flow.
+
+That duplication created drift between the two implementations. Features such as image generation, retry/edit behavior, and final message handling existed in the legacy path, while the product direction is to make streaming the only chat path used by the application.
+
+## Root Cause Analysis
+
+The frontend still posted directly to `/api/chat` from multiple modules, and the chat toolbar still presented streaming as optional. At the same time, the streaming finalizer did not fully support all terminal payload shapes already used by the legacy route, especially image results and reload-driven completion behavior.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/static/js/chat/chat-messages.js`
+- `application/single_app/static/js/chat/chat-streaming.js`
+- `application/single_app/static/js/chat/chat-edit.js`
+- `application/single_app/static/js/chat/chat-retry.js`
+- `application/single_app/static/js/chat/chat-input-actions.js`
+- `application/single_app/templates/chats.html`
+- `application/single_app/templates/profile.html`
+- `application/single_app/functions_settings.py`
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_streaming_only_chat_path.py`
+
+### Code Changes Summary
+
+- Removed the first-party chat UI fallback that posted directly to `/api/chat`.
+- Moved retry and edit flows onto the shared streaming helper.
+- Removed the chat toolbar streaming toggle so streaming is no longer presented as optional.
+- Extended the streaming finalizer to support image-generation results and reload-driven completion handling.
+- Added a streaming compatibility bridge in the backend for parity-sensitive requests, including image generation and retry/edit, while keeping `/api/chat` available as a temporary compatibility shim.
+- Updated defaults and profile messaging to reflect streaming-only chat behavior.
+- Added image-generation thought events on the streaming compatibility bridge so users see progress before the final image arrives.
+- Bumped the application version to `0.239.127`.
+
+## Testing Approach
+
+A functional regression test was added at `functional_tests/test_streaming_only_chat_path.py` to verify:
+
+- Main chat, retry, and edit entry points do not call `/api/chat` directly.
+- The streaming helper still uses `/api/chat/stream`.
+- The backend streaming route contains the compatibility bridge.
+- Image-generation compatibility requests emit useful streaming thoughts before the final image payload.
+- The chat template no longer includes the streaming toggle button.
+- The default setting and app version were updated.
+
+## Impact Analysis
+
+This change makes streaming the only first-party chat path exposed by the UI while preserving legacy behaviors through the streaming endpoint for image generation and retry/edit flows. It reduces the risk of feature drift between chat implementations and provides a safer base for removing the legacy shim entirely in a later cleanup pass.
+
+## Validation
+
+### Before
+
+- Main send flow could fall back to `/api/chat`.
+- Retry and edit always posted to `/api/chat`.
+- Image generation was blocked on the streaming route.
+- The toolbar exposed streaming as an optional toggle.
+
+### After
+
+- First-party chat flows send through `/api/chat/stream`.
+- Retry and edit are routed through the same streaming helper.
+- Image-generation requests are supported through the streaming route via the backend compatibility bridge.
+- Streaming is treated as required chat behavior in the UI.
diff --git a/docs/explanation/fixes/STREAMING_THOUGHT_FINALIZATION_FIX.md b/docs/explanation/fixes/STREAMING_THOUGHT_FINALIZATION_FIX.md
new file mode 100644
index 00000000..75d73c20
--- /dev/null
+++ b/docs/explanation/fixes/STREAMING_THOUGHT_FINALIZATION_FIX.md
@@ -0,0 +1,43 @@
+# Streaming Thought Finalization Fix
+
+## Fix Title
+Streaming chat responses now finalize reliably even when SSE events arrive across chunk boundaries or trailing thought events arrive after answer text has started streaming.
+
+## Issue Description
+In streaming mode, some responses would briefly show the full assistant answer and then revert to the final pulsing thought badge. The UI could remain stuck on that thought placeholder until the page was refreshed, even though the backend had already saved the assistant message.
+
+## Root Cause Analysis
+- The streaming client parsed each `reader.read()` chunk independently and split it by newline, which is not safe for SSE because a single event can arrive across multiple network chunks.
+- When the final `done` event was split across chunks, the client could miss it and never finalize the temporary streaming message.
+- The streaming thought renderer also replaced the entire temporary message body. If a late thought event arrived after answer text had already started rendering, it could overwrite the visible answer with the pulsing thought badge.
+
+## Version Implemented
+Fixed in version: **0.239.116**
+
+## Files Modified
+| File | Change |
+|------|--------|
+| `application/single_app/static/js/chat/chat-streaming.js` | Added buffered SSE frame parsing, explicit incomplete-stream handling, and content-start tracking for the temporary streaming message |
+| `application/single_app/static/js/chat/chat-thoughts.js` | Prevented streaming thoughts from replacing the temporary message once answer content has begun streaming |
+| `functional_tests/test_streaming_thought_finalization.py` | Added focused regression coverage for buffered SSE parsing and late-thought overwrite guards |
+| `application/single_app/config.py` | Version bump to 0.239.116 |
+
+## Code Changes Summary
+- Added a stateful SSE buffer so JSON payloads are parsed only after a full SSE event block is available.
+- Flushed the decoder and processed any trailing event data when the stream closes.
+- Added a fallback error path when a stream ends without completion metadata so the UI does not hang indefinitely on the temporary placeholder.
+- Marked the temporary streaming message once real answer content starts rendering.
+- Ignored subsequent streaming-thought placeholder renders after that point so answer text stays visible until finalization replaces the temporary message with the permanent assistant message.
+
+## Testing Approach
+- Added `functional_tests/test_streaming_thought_finalization.py`.
+- Re-ran the existing thoughts feature structural coverage to confirm the edited modules still expose the expected thought integration points.
+
+## Impact Analysis
+- Streaming responses should now remain stable on screen after answer text begins rendering.
+- Split SSE frames should no longer prevent the final `done` payload from being processed.
+- If the server ever closes a stream without completion metadata, the user now sees a partial-response warning instead of a permanent pulsing placeholder.
+
+## Validation
+- Before: final streamed answers could be replaced by the last thought badge and remain stuck until refresh.
+- After: answer text remains visible once content starts, and the temp streaming message either finalizes correctly or degrades into an explicit interrupted-stream state.
\ No newline at end of file
diff --git a/docs/explanation/fixes/TABULAR_COMPUTED_RESULTS_PROMPT_PRIORITY_FIX.md b/docs/explanation/fixes/TABULAR_COMPUTED_RESULTS_PROMPT_PRIORITY_FIX.md
new file mode 100644
index 00000000..11065e38
--- /dev/null
+++ b/docs/explanation/fixes/TABULAR_COMPUTED_RESULTS_PROMPT_PRIORITY_FIX.md
@@ -0,0 +1,42 @@
+# Tabular Computed Results Prompt Priority Fix
+
+## Fix Title
+Successful tabular tool analysis now has prompt priority over excerpt-only retrieval instructions in the final GPT response.
+
+## Issue Description
+The tabular SK pass could recover, find the correct worksheet, and compute the needed row-level values, but the outer GPT response could still answer as if those results were unavailable. In practice, the final response sometimes fell back to the search-excerpt framing and said it did not have direct access to the requested record even after the tool pass succeeded.
+
+## Root Cause Analysis
+- The retrieval augmentation prompt told the outer GPT response to answer only from retrieved excerpts and to say so when the answer was not present in those excerpts.
+- The tabular-computed-results handoff was added as a separate system message later in the prompt assembly.
+- That created a prompt conflict: search excerpts often contained only workbook schema context, while the successful tabular pass contained the actual computed row-level values.
+- The final model could anchor on the excerpt-only instruction and ignore the later tool-backed analysis, producing a cautious but incorrect fallback-style answer.
+
+## Version Implemented
+Fixed in version: **0.239.118**
+
+## Files Modified
+| File | Change |
+|------|--------|
+| `application/single_app/route_backend_chats.py` | Added shared prompt helpers so retrieval augmentation explicitly allows later tool-backed results and successful tabular analysis is marked authoritative |
+| `functional_tests/test_tabular_computed_results_prompt_priority.py` | Added regression coverage for the search-prompt contract and authoritative tabular handoff |
+| `application/single_app/config.py` | Version bump to 0.239.118 |
+
+## Code Changes Summary
+- Replaced the repeated search augmentation prompt text with a shared helper.
+- Updated the retrieval prompt so it permits and respects computed tool-backed results that appear in later system messages.
+- Replaced repeated successful-tabular-analysis handoff text with a shared helper that explicitly marks those results as authoritative for calculations and row-level facts.
+- Added a regression test to block reintroduction of the older excerpt-only wording.
+
+## Testing Approach
+- Added `functional_tests/test_tabular_computed_results_prompt_priority.py`.
+- Planned focused validation against the prompt helpers plus existing tabular orchestration coverage.
+
+## Impact Analysis
+- Successful tabular recovery should now survive the final answer synthesis step instead of being overwritten by schema-only search guidance.
+- The final GPT response should stop claiming it lacks direct access when tool-backed values are already present in the prompt.
+- This fix is general for workspace and chat-upload tabular analysis paths because it updates the shared prompt handoff contract rather than a workbook-specific rule.
+
+## Validation
+- Before: a recovered tabular pass could still lead to an excerpt-only final answer.
+- After: the final answer prompt treats successful tabular results as authoritative and no longer frames the answer as limited to excerpts alone.
\ No newline at end of file
diff --git a/docs/explanation/fixes/TABULAR_CROSS_SHEET_BRIDGE_ANALYSIS_FIX.md b/docs/explanation/fixes/TABULAR_CROSS_SHEET_BRIDGE_ANALYSIS_FIX.md
new file mode 100644
index 00000000..a5177f2a
--- /dev/null
+++ b/docs/explanation/fixes/TABULAR_CROSS_SHEET_BRIDGE_ANALYSIS_FIX.md
@@ -0,0 +1,61 @@
+# Tabular Cross-Sheet Bridge Analysis Fix
+
+Fixed in version: **0.239.140**
+
+## Issue Description
+
+Grouped workbook questions could fail when the answer required combining a small reference worksheet with a larger fact worksheet.
+
+Example pattern:
+- one worksheet lists canonical entities such as solution engineers
+- another worksheet contains the fact rows such as milestones
+- the user asks for grouped results per entity
+
+The prior orchestration sometimes stayed on a single worksheet, grouped a boolean or membership-style column, or fell back to schema-only language after an incomplete analytical pass.
+
+## Root Cause
+
+Analysis mode had strong single-sheet guidance but no generalized prompt for a reference-sheet plus fact-sheet bridge.
+
+Two specific gaps caused the failure:
+- multi-sheet analysis still established a default worksheet even when the workbook structure suggested the answer needed more than one sheet
+- the prompt did not tell the model to prefer canonical entity names from a small reference sheet over boolean or membership-flag columns in a larger fact sheet
+
+## Files Modified
+
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_tabular_cross_sheet_bridge_analysis.py`
+
+## Code Changes Summary
+
+- added generalized detection for grouped cross-sheet analytical questions
+- added a lightweight bridge-plan helper that infers a smaller reference worksheet and a larger fact worksheet from workbook metadata
+- prevented analysis mode from setting a default sheet when that bridge plan is active
+- added prompt guidance to query both sheets explicitly and avoid answering “each X” by grouping yes/no or membership-flag columns
+- added regression coverage for intent detection, bridge-plan inference, and prompt guardrails
+
+## Testing Approach
+
+The new functional regression test validates:
+- grouped cross-sheet questions remain in analysis mode rather than entity-lookup mode
+- workbook metadata produces the expected reference-sheet and fact-sheet bridge plan
+- the analysis prompt includes the new bridge-plan and flag-column guardrails
+
+## Impact Analysis
+
+This change is intentionally narrow:
+- schema-summary routing is unchanged
+- entity-lookup routing is unchanged
+- normal single-sheet analysis still keeps the existing default-sheet behavior
+
+The new behavior only activates when the question looks like a grouped analytical request and the workbook structure strongly suggests a small reference sheet plus a larger fact sheet.
+
+## Validation
+
+Expected improvement:
+- grouped cross-sheet workbook questions can iterate across the relevant tabs without overfitting to a workbook-specific scenario
+- the model is less likely to mistake flag columns for the requested entity dimension
+
+Related functional test:
+- `functional_tests/test_tabular_cross_sheet_bridge_analysis.py`
\ No newline at end of file
diff --git a/docs/explanation/fixes/TABULAR_ENTITY_LOOKUP_CROSS_SHEET_RETRY_FIX.md b/docs/explanation/fixes/TABULAR_ENTITY_LOOKUP_CROSS_SHEET_RETRY_FIX.md
new file mode 100644
index 00000000..23cc1b70
--- /dev/null
+++ b/docs/explanation/fixes/TABULAR_ENTITY_LOOKUP_CROSS_SHEET_RETRY_FIX.md
@@ -0,0 +1,47 @@
+# Tabular Entity Lookup Cross-Sheet Retry Fix
+
+## Fix Title
+Cross-sheet workbook entity lookups now retry when the analytical pass only succeeds on one worksheet and stops before collecting the related records requested by the user.
+
+## Issue Description
+Questions such as finding one taxpayer and showing their profile, return summary, W-2, 1099, payment, refund, notice, audit, and installment agreement records could appear to succeed while still returning an incomplete answer. The analytical tabular pass repeatedly queried `Taxpayers`, found the primary row, and then stopped without traversing the related worksheets.
+
+## Root Cause Analysis
+- The route layer treated any successful analytical invocation as sufficient to finish the inner tabular analysis pass.
+- For cross-sheet entity questions, that success condition was too weak because the first worksheet could succeed while leaving most requested record types untouched.
+- Multi-sheet default-sheet behavior also made the initial worksheet selection too sticky for entity/profile prompts that should span several tabs.
+- Worksheet matching did not preserve `W2` cleanly for sheet names such as `W2Forms`, which weakened related-sheet hinting for tax-document lookups.
+
+## Version Implemented
+Fixed in version: **0.239.119**
+
+## Files Modified
+| File | Change |
+|------|--------|
+| `application/single_app/route_backend_chats.py` | Added `entity_lookup` routing, related-worksheet hinting, execution-gap retries for incomplete one-sheet success, and stronger worksheet tokenization for `W2` sheet names |
+| `functional_tests/test_tabular_entity_lookup_mode.py` | Added regression coverage for entity-lookup routing, related-sheet ranking, and incomplete-success retry guardrails |
+| `functional_tests/test_tabular_workbook_schema_summary_mode.py` | Updated helper extraction to include the new execution-mode dependency and refreshed the file version header |
+| `application/single_app/config.py` | Version bump to 0.239.119 |
+
+## Code Changes Summary
+- Added a dedicated `entity_lookup` execution mode for cross-sheet profile and related-record questions.
+- Prevented multi-sheet entity lookups from relying on a sticky default worksheet during the analytical pass.
+- Added execution-gap retry feedback so the inner SK loop retries when successful tool calls only touched one worksheet or when the narrative still claims the data is unavailable.
+- Improved worksheet tokenization so `W2Forms` contributes a usable `w2` token during related-sheet scoring.
+
+## Testing Approach
+- Added `functional_tests/test_tabular_entity_lookup_mode.py`.
+- Re-ran focused tabular functional tests covering workbook schema-summary routing, retry-sheet recovery, and the new cross-sheet entity-lookup path.
+
+## Impact Analysis
+- Cross-sheet taxpayer and case-history questions should now keep traversing related worksheets instead of stopping after the first successful row.
+- Existing workbook summary and wrong-sheet recovery behavior remain intact because the new retry logic is scoped to `entity_lookup` mode.
+- Related-sheet hinting is stronger for IRS-style workbook tabs that encode tax forms directly in sheet names.
+
+## Validation
+- Before: a taxpayer lookup could query `Taxpayers` successfully several times, never inspect the other tabs, and still finish with a generic answer.
+- After: incomplete one-sheet success is treated as an execution gap, the analytical pass is retried with explicit cross-sheet guidance, and related tax-form worksheets such as `W2Forms` remain visible to the ranking logic.
+
+## Related Config Update
+- `application/single_app/config.py` now sets `VERSION = "0.239.119"`.
+- Related functional tests: `functional_tests/test_tabular_entity_lookup_mode.py` and `functional_tests/test_tabular_workbook_schema_summary_mode.py`.
\ No newline at end of file
diff --git a/docs/explanation/fixes/TABULAR_POPUP_DOWNLOAD_FIX.md b/docs/explanation/fixes/TABULAR_POPUP_DOWNLOAD_FIX.md
new file mode 100644
index 00000000..5dad38bd
--- /dev/null
+++ b/docs/explanation/fixes/TABULAR_POPUP_DOWNLOAD_FIX.md
@@ -0,0 +1,49 @@
+# Tabular Popup Download Fix
+
+Fixed/Implemented in version: **0.239.124**
+
+## Issue Description
+
+Downloading a workbook from the chat tabular preview popup could fail with a generic browser download error, while the app showed no JavaScript error and the backend logged no application error.
+
+## Root Cause Analysis
+
+The popup used a plain anchor-based download control for a session-protected endpoint.
+
+That meant the browser handled the request outside the app's normal error flow, so failures surfaced only as a generic download error and bypassed the UI's toast/error handling.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/static/js/chat/chat-enhanced-citations.js`
+- `application/single_app/config.py`
+- `functional_tests/test_tabular_popup_download_fix.py`
+
+### Code Changes Summary
+
+- Replaced the tabular popup download anchor with a controlled button-driven download flow.
+- Added an authenticated `fetch()` request for the tabular download endpoint using same-origin credentials.
+- Added blob-based client download handling and explicit toast/error reporting when the request fails.
+- Updated `config.py` to version `0.239.124` for this fix.
+
+### Testing Approach
+
+- Added a functional regression test that inspects the chat enhanced citations JavaScript for the fetch-to-blob download flow.
+- Added coverage to verify the popup no longer uses the old blank-target anchor download path.
+
+## Validation
+
+### Before
+
+- The tabular popup download used a browser-managed anchor request.
+- When the download failed, the user saw a generic browser download error without an app-level error message.
+
+### After
+
+- The tabular popup download is handled explicitly in JavaScript with `fetch()` and blob download logic.
+- Failures now route through the app's error handling and can surface a toast message instead of failing silently.
+
+### User Experience Improvement
+
+Users can download tabular files from the chat preview popup through a controlled download path that is more reliable and easier to troubleshoot when something goes wrong.
\ No newline at end of file
diff --git a/docs/explanation/fixes/TABULAR_RETRY_SHEET_RECOVERY_FIX.md b/docs/explanation/fixes/TABULAR_RETRY_SHEET_RECOVERY_FIX.md
new file mode 100644
index 00000000..3a2020d4
--- /dev/null
+++ b/docs/explanation/fixes/TABULAR_RETRY_SHEET_RECOVERY_FIX.md
@@ -0,0 +1,43 @@
+# Tabular Retry Sheet Recovery Fix
+
+## Fix Title
+Multi-sheet tabular analysis now recovers from a wrong initial worksheet guess by promoting candidate recovery sheets from failed analytical tool calls.
+
+## Issue Description
+Identifier-based workbook questions could fail even when the needed row existed in the workbook and document search had already surfaced the file. The analytical tabular pass sometimes started on a plausible but wrong worksheet, then kept retrying analytical tools against that same sheet until it exhausted retries and fell back to schema-only context.
+
+## Root Cause Analysis
+- The route layer used a lightweight likely-sheet heuristic to establish a default worksheet for multi-sheet analytical calls.
+- That heuristic did not tokenize camel-case sheet names such as `TaxReturns` very well, which weakened the initial sheet guess for many workbook naming conventions.
+- When a tool call failed because the requested column was missing on the chosen sheet, the tool only returned a generic missing-column error. The retry loop had no structured signal telling it which other worksheet was a better candidate.
+- As a result, retries could keep hitting the same wrong worksheet even though the workbook schema already contained enough information to steer recovery.
+
+## Version Implemented
+Fixed in version: **0.239.117**
+
+## Files Modified
+| File | Change |
+|------|--------|
+| `application/single_app/semantic_kernel_plugins/tabular_processing_plugin.py` | Added workbook-aware missing-column payloads with `selected_sheet`, `missing_column`, and ordered `candidate_sheets` recovery hints |
+| `application/single_app/route_backend_chats.py` | Added retry-sheet override helpers, camel-case sheet tokenization, and retry-time default-sheet promotion based on failed tool payloads |
+| `functional_tests/test_tabular_retry_sheet_recovery.py` | Added regression coverage for camel-case sheet tokenization, candidate-sheet error payloads, and retry-sheet override selection |
+| `application/single_app/config.py` | Version bump to 0.239.117 |
+
+## Code Changes Summary
+- Improved worksheet tokenization so camel-case sheet names participate in likely-sheet matching more accurately.
+- Extended analytical tool errors so missing-column failures identify the current sheet and suggest candidate recovery sheets from the same workbook.
+- Added retry orchestration that reads those candidate sheets and updates the plugin's default worksheet before the next analytical attempt.
+- Updated the analytical system prompt so recovery-sheet hints override the original likely-sheet guess after a wrong-sheet failure.
+
+## Testing Approach
+- Added `functional_tests/test_tabular_retry_sheet_recovery.py`.
+- Re-ran focused multi-sheet and tool-error tabular functional tests to confirm retry recovery stays compatible with the existing analytical-only orchestration.
+
+## Impact Analysis
+- Identifier-based workbook questions should now recover when the first worksheet guess is wrong instead of repeating the same failing call.
+- This remains tool-driven behavior inside the analytical SK pass; it does not rely on schema-only fallback to answer workbook calculation questions.
+- The recovery behavior is generic across multi-sheet workbooks because it is based on missing-column signals and workbook sheet/column structure rather than workbook-specific rules.
+
+## Validation
+- Before: a wrong initial worksheet guess could lead to repeated analytical retries on the same sheet until the route fell back to schema context.
+- After: missing-column failures expose better candidate sheets and the next analytical retry can be redirected to the stronger worksheet automatically.
\ No newline at end of file
diff --git a/docs/explanation/fixes/TABULAR_WORKBOOK_SCHEMA_SUMMARY_ROUTING_FIX.md b/docs/explanation/fixes/TABULAR_WORKBOOK_SCHEMA_SUMMARY_ROUTING_FIX.md
new file mode 100644
index 00000000..1a6e40e8
--- /dev/null
+++ b/docs/explanation/fixes/TABULAR_WORKBOOK_SCHEMA_SUMMARY_ROUTING_FIX.md
@@ -0,0 +1,47 @@
+# Tabular Workbook Schema Summary Routing Fix
+
+## Fix Title
+Workbook-structure questions now use a schema-summary tabular mode instead of being forced through analytical-only tool retries.
+
+## Issue Description
+Selected or cited Excel workbooks were always routed into the analytical mini Semantic Kernel pass. That worked well for value lookups, aggregations, and grouped analysis, but workbook-summary prompts such as asking what worksheets exist, what each worksheet represents, and how they relate were not true analytical questions.
+
+## Root Cause Analysis
+- The tabular mini-agent was intentionally hardened to allow only analytical functions during its retry path.
+- Workbook-summary prompts still triggered that same analytical path, even though the correct tool for those questions is `describe_tabular_file()`.
+- As a result, the model sometimes chose analytical functions like `aggregate_column()` just to satisfy the forced tool-use requirement, which then failed on multi-sheet workbooks because no `sheet_name` was supplied.
+- When the mini-agent failed, the outer fallback prompt still told the final GPT pass to use plugin functions even though that stage could not actually invoke them.
+
+## Version Implemented
+Fixed in version: **0.239.115**
+
+## Files Modified
+| File | Change |
+|------|--------|
+| `application/single_app/route_backend_chats.py` | Added workbook-schema intent detection, schema-summary execution mode, and safer fallback prompt handling |
+| `functional_tests/test_tabular_workbook_schema_summary_mode.py` | Added regression coverage for workbook-summary intent routing, fallback prompts, and citation preservation |
+| `application/single_app/config.py` | Version bump to 0.239.115 |
+
+## Code Changes Summary
+- Added a narrow workbook-structure intent heuristic so prompts about worksheets, tabs, workbook summaries, and cross-sheet relationships route into a schema-summary tabular mode.
+- Extended the mini tabular SK executor with a `schema_summary` mode that allows `describe_tabular_file()` and treats it as a successful tool-backed result.
+- Kept the existing analytical-only path unchanged for value lookup, aggregation, filtering, and grouped-analysis questions.
+- Updated the workspace fallback prompt so the final GPT pass no longer gets impossible instructions to call plugin tools after the mini SK pass has already failed.
+- Preserved `describe_tabular_file()` citations when they are the only successful tabular tool calls.
+
+## Testing Approach
+- Added `functional_tests/test_tabular_workbook_schema_summary_mode.py`.
+- Re-ran the focused tabular regression suite to confirm the analytical path stayed intact:
+ - `functional_tests/test_tabular_analysis_rejects_discovery_only.py`
+ - `functional_tests/test_tabular_tool_error_retry_and_thoughts.py`
+ - `functional_tests/test_tabular_multisheet_workbook_support.py`
+ - `functional_tests/test_workspace_tabular_trigger_and_thoughts.py`
+
+## Impact Analysis
+- Workbook-summary questions should now reach the correct tabular tool path with fewer retries and lower latency.
+- Analytical questions keep the stricter analytical-only guardrails that were added to prevent discovery-only answers.
+- When the mini SK pass still fails, the outer fallback is now more honest about using schema-only context rather than implying that more tool calls will happen.
+
+## Validation
+- Before: workbook-summary questions could trigger repeated `aggregate_column()` failures on multi-sheet workbooks and then fall back through a contradictory prompt.
+- After: workbook-summary questions route to `describe_tabular_file()`-based schema summarization, while analytical questions remain on the analytical-only path.
\ No newline at end of file
diff --git a/docs/explanation/fixes/v0.239.008/CHAT_TABULAR_SK_TRIGGER_FIX.md b/docs/explanation/fixes/v0.239.008/CHAT_TABULAR_SK_TRIGGER_FIX.md
new file mode 100644
index 00000000..d525e2d1
--- /dev/null
+++ b/docs/explanation/fixes/v0.239.008/CHAT_TABULAR_SK_TRIGGER_FIX.md
@@ -0,0 +1,67 @@
+# Chat-Uploaded Tabular File SK Mini-Agent Trigger Fix
+
+## Issue Description
+
+When a user uploads a tabular file (CSV, XLSX, XLS, XLSM) directly to a chat conversation and asks a question in model-only mode (no agent selected), the SK mini-agent (`run_tabular_sk_analysis`) did not trigger. The model would see instructions to "use plugin functions" but could not call them without an agent, resulting in the model describing what it would do instead of providing actual analysis results.
+
+The full agent mode worked correctly because the agent has direct access to the `TabularProcessingPlugin` and can call its functions.
+
+## Root Cause
+
+Three gaps prevented the mini SK agent from activating for chat-uploaded tabular files:
+
+1. **Streaming path ignored `file` role messages**: The streaming conversation history loop (`/api/chat/stream`) only processed `user` and `assistant` roles, making chat-uploaded files completely invisible to the model.
+
+2. **Mini SK only triggered from search results**: Both streaming and non-streaming paths only invoked `run_tabular_sk_analysis()` when tabular files appeared in hybrid search results (`combined_documents`). Chat-uploaded files are stored in blob storage as `file` role messages and are not indexed in Azure AI Search, so they never appeared in search results.
+
+3. **Model-only mode can't call plugin functions**: The non-streaming path's file handler injected "Use the tabular_processing plugin functions" as a system message, but in model-only mode the model has no function-calling capability.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_chats.py` — All code changes
+- `application/single_app/config.py` — Version bump to 0.239.008
+
+### Code Changes
+
+#### Non-streaming path (`/api/chat`)
+
+1. Added `chat_tabular_files = set()` tracker before the conversation history loop (~line 1896)
+2. Added `chat_tabular_files.add(filename)` inside the `if is_table and file_content_source == 'blob':` block (~line 1936)
+3. After the history loop, added a block that checks `chat_tabular_files` and calls `run_tabular_sk_analysis(source_hint="chat")`, injecting pre-computed results as a system message (~line 2027)
+
+#### Streaming path (`/api/chat/stream`)
+
+4. Replaced the simple 8-line history loop (which only handled `user`/`assistant`) with expanded logic that mirrors the non-streaming path's `file` role handling, including blob tabular file tracking (~line 3687)
+5. Added the same mini SK trigger block after the expanded loop (~line 3751)
+
+### How It Works After Fix
+
+1. User uploads `sales.xlsx` to chat, asks "analyze sales/profit"
+2. During conversation history building, the `file` role message with `is_table=True` and `file_content_source='blob'` is detected
+3. The filename is collected into `chat_tabular_files`
+4. After the history loop, `run_tabular_sk_analysis()` is called with `source_hint="chat"`, which resolves the file from the `personal-chat` blob container
+5. The mini SK agent pre-loads the file schema, calls plugin functions (aggregate, filter, etc.), and returns computed results
+6. Results are injected as a system message so the model can present accurate numbers
+7. Plugin invocation citations are collected for transparency
+
+## Testing
+
+1. Upload a tabular file (xlsx/csv) directly to chat
+2. With no agent selected, send a data analysis question
+3. Verify the response contains actual computed data (not just a description of steps)
+4. Check logs for `[Chat Tabular SK]` entries confirming the mini SK trigger
+5. Verify agent mode still works as before
+
+## Impact
+
+- Enables tabular data analysis in model-only chat mode for chat-uploaded files
+- No changes to existing search-result-based tabular detection
+- No changes to full agent mode behavior
+- Streaming and non-streaming paths both fixed
+
+## Version
+
+- **Version**: 0.239.008
+- **Implemented in**: 0.239.008
diff --git a/docs/explanation/fixes/v0.239.032/TABULAR_WORKSPACE_TRIGGER_AND_THOUGHTS_FIX.md b/docs/explanation/fixes/v0.239.032/TABULAR_WORKSPACE_TRIGGER_AND_THOUGHTS_FIX.md
new file mode 100644
index 00000000..0ab4e9c2
--- /dev/null
+++ b/docs/explanation/fixes/v0.239.032/TABULAR_WORKSPACE_TRIGGER_AND_THOUGHTS_FIX.md
@@ -0,0 +1,65 @@
+# Tabular Workspace Trigger and Thoughts Fix
+
+## Issue Description
+Users could ask multiple questions against the same selected tabular workspace file and see inconsistent behavior. A simple aggregation question could trigger the tabular SK mini-agent, while a later question against the same selected file could fall back to schema-only reasoning. In addition, the Processing Thoughts UI did not show any explicit `tabular_analysis` step even when tabular functions were used.
+
+**Version implemented:** 0.239.032
+
+Fixed/Implemented in version: **0.239.032**
+
+Related `config.py` update: `VERSION` was bumped to `0.239.032`.
+
+## Root Cause Analysis
+1. **Workspace trigger depended too heavily on search results**
+ - The tabular trigger only inspected `combined_documents` returned from hybrid search.
+ - If the selected tabular file produced sparse retrieval output or schema-only chunks, the trigger could miss the explicit workspace selection.
+2. **Mini-agent responses were not hardened against no-tool replies**
+ - For more complex analytical prompts, the mini-agent could return narrative text without actually calling the `TabularProcessingPlugin`.
+ - That produced no tool citations and left the final model with schema-only context.
+3. **Processing thoughts missed tabular work entirely**
+ - The chat flow recorded search, web, and generation steps, but never wrote a `tabular_analysis` thought for workspace or chat tabular runs.
+
+## Technical Details
+### Files Modified
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_workspace_tabular_trigger_and_thoughts.py`
+
+### Code Changes Summary
+- Added shared helpers to:
+ - detect supported tabular filenames consistently,
+ - resolve explicitly selected workspace tabular documents, and
+ - merge search-result files with selected-document files before triggering analysis.
+- Moved workspace tabular trigger logic so it can run from explicit workspace selection, not just retrieved chunks.
+- Hardened `run_tabular_sk_analysis()` with a retry path that requires actual tabular tool usage before accepting the result.
+- Added `tabular_analysis` thoughts for:
+ - workspace tabular analysis start/completion in non-streaming mode,
+ - workspace tabular analysis start/completion in streaming mode,
+ - chat-uploaded tabular analysis start/completion in non-streaming mode,
+ - chat-uploaded tabular analysis start/completion in streaming mode.
+
+### Testing Approach
+- Added `functional_tests/test_workspace_tabular_trigger_and_thoughts.py` to verify:
+ - explicit workspace-selected tabular files participate in trigger detection,
+ - tabular analysis thoughts are emitted in both chat paths,
+ - the mini-agent prompt now requires tool execution and retries when it answers without tools.
+
+## Impact Analysis
+- Explicitly selected CSV/Excel workspace files now have a more reliable analysis trigger path.
+- Complex tabular prompts are less likely to degrade into schema-only answers.
+- Users can now see tabular analysis activity directly in Processing Thoughts, improving transparency and debugging.
+
+## Validation
+### Before
+- Some workspace-selected tabular questions skipped the SK mini-agent even though the same file was still selected.
+- Thoughts could show search and generation steps without any indication that tabular analysis ran.
+
+### After
+- Workspace tabular analysis considers both retrieved tabular documents and explicitly selected tabular files.
+- Mini-agent retries are stricter when the first response skips tool execution.
+- Processing Thoughts now includes clear `tabular_analysis` steps whenever tabular analysis is attempted.
+
+## Related Validation Assets
+- Functional test: `functional_tests/test_workspace_tabular_trigger_and_thoughts.py`
+- Related feature documentation: `docs/explanation/features/v0.239.003/PROCESSING_THOUGHTS.md`
+- Related earlier fix: `docs/explanation/fixes/v0.239.008/CHAT_TABULAR_SK_TRIGGER_FIX.md`
diff --git a/docs/explanation/fixes/v0.239.033/TABULAR_DATETIME_COMPONENT_ANALYSIS_FIX.md b/docs/explanation/fixes/v0.239.033/TABULAR_DATETIME_COMPONENT_ANALYSIS_FIX.md
new file mode 100644
index 00000000..fafcb2f5
--- /dev/null
+++ b/docs/explanation/fixes/v0.239.033/TABULAR_DATETIME_COMPONENT_ANALYSIS_FIX.md
@@ -0,0 +1,64 @@
+# Tabular Datetime Component Analysis Fix
+
+## Issue Description
+Some tabular questions still fell back to schema-only context with the thought message `Tabular analysis could not compute results; using schema context instead`. This happened most often for time-based questions such as identifying peak hours, busiest weekdays, or monthly patterns from datetime columns.
+
+**Version implemented:** 0.239.033
+
+Fixed/Implemented in version: **0.239.033**
+
+Related `config.py` update: `VERSION` was bumped to `0.239.033`.
+
+## Root Cause Analysis
+1. **The plugin lacked datetime component grouping support**
+ - Existing tabular functions could aggregate and group by existing columns, but they could not directly derive `hour`, `day_of_week`, `month`, or similar components from datetime-like fields.
+ - Questions like “During what hours of the day do departure queues peak?” therefore required a transformation step the plugin did not expose.
+2. **The SK mini-agent could still fail even when the file triggered correctly**
+ - If the model could not find a tool sequence that matched the requested transformation, the tabular analysis flow returned `None` and the chat fell back to schema-only context.
+3. **There was no deterministic recovery path for common time-based questions**
+ - Even when datetime columns and queue/delay metrics were clearly present, the system did not attempt a direct computed fallback.
+
+## Technical Details
+### Files Modified
+- `application/single_app/semantic_kernel_plugins/tabular_processing_plugin.py`
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_tabular_datetime_component_analysis.py`
+
+### Code Changes Summary
+- Added datetime parsing helpers to `TabularProcessingPlugin` to support:
+ - ISO datetime strings,
+ - time-only strings,
+ - `HHMM` and `HHMMSS` compact time formats.
+- Added a new tabular plugin function: `group_by_datetime_component`
+ - Supports grouping by `year`, `month`, `month_name`, `day`, `date`, `hour`, `minute`, `day_name`, `weekday_number`, `quarter`, and `week`.
+ - Supports `count`, `sum`, `mean`, `min`, `max`, `median`, and `std` aggregations.
+ - Supports optional pre-group filtering with a pandas query expression.
+- Updated the tabular SK prompt and fallback guidance so time-based questions explicitly use `group_by_datetime_component`.
+- Added a direct datetime-aware fallback in `run_tabular_sk_analysis()` so common time-based questions can still return computed results even if the SK mini-agent does not successfully plan the tool sequence.
+
+### Testing Approach
+- Added `functional_tests/test_tabular_datetime_component_analysis.py` to verify:
+ - hour grouping works for ISO datetime strings,
+ - compact `HHMM` values are parsed correctly,
+ - route and plugin integration text references the new datetime grouping capability.
+
+## Impact Analysis
+- Time-based tabular questions now have a dedicated computation path instead of relying on schema-only reasoning.
+- Questions about peak hours, busiest weekdays, and similar datetime-derived trends are much less likely to fall back to the schema preview.
+- The direct fallback keeps user experience resilient even when the mini-agent does not autonomously choose the new function on the first try.
+
+## Validation
+### Before
+- Tabular analysis could trigger correctly but still fail to compute answers for questions requiring datetime-derived grouping.
+- Users saw the thought step `Tabular analysis could not compute results; using schema context instead` for time-based questions.
+
+### After
+- The tabular plugin can directly compute datetime component groupings.
+- The chat route can recover with a deterministic datetime-based fallback for common time-oriented questions.
+- Time-based questions now have a much stronger chance of returning computed results instead of schema-only context.
+
+## Related Validation Assets
+- Functional test: `functional_tests/test_tabular_datetime_component_analysis.py`
+- Related fix: `docs/explanation/fixes/v0.239.032/TABULAR_WORKSPACE_TRIGGER_AND_THOUGHTS_FIX.md`
+- Related thoughts documentation: `docs/explanation/features/v0.239.003/PROCESSING_THOUGHTS.md`
diff --git a/docs/explanation/fixes/v0.239.034/TABULAR_COMPUTED_ANALYSIS_ENFORCEMENT_FIX.md b/docs/explanation/fixes/v0.239.034/TABULAR_COMPUTED_ANALYSIS_ENFORCEMENT_FIX.md
new file mode 100644
index 00000000..58dc263a
--- /dev/null
+++ b/docs/explanation/fixes/v0.239.034/TABULAR_COMPUTED_ANALYSIS_ENFORCEMENT_FIX.md
@@ -0,0 +1,60 @@
+# Tabular Computed Analysis Enforcement Fix
+
+## Issue Description
+Some analytical tabular questions still completed after a schema-only discovery call such as `describe_tabular_file`. That let the model answer from preview rows instead of using computed query, filter, aggregate, or grouped results from the full dataset.
+
+**Version implemented:** 0.239.034
+
+Fixed/Implemented in version: **0.239.034**
+
+Related `config.py` update: `VERSION` was bumped to `0.239.034`.
+
+## Root Cause Analysis
+1. **Any plugin call counted as successful analysis**
+ - `run_tabular_sk_analysis()` accepted the first response as long as any plugin invocation occurred.
+ - Discovery calls such as `describe_tabular_file` therefore counted the same as real analytical operations.
+2. **Schema discovery citations overshadowed computed analysis intent**
+ - When discovery calls happened before analytical calls, citations could emphasize schema inspection rather than the computed operations that actually answered the question.
+3. **Prompt guidance did not explicitly reject discovery-only behavior**
+ - Even with pre-loaded schemas, the mini-agent could still call discovery helpers and stop there.
+
+## Technical Details
+### Files Modified
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_tabular_analysis_rejects_discovery_only.py`
+
+### Code Changes Summary
+- Added tabular invocation classification helpers in `route_backend_chats.py` to separate:
+ - discovery functions: `list_tabular_files`, `describe_tabular_file`
+ - analytical functions: `aggregate_column`, `filter_rows`, `query_tabular_data`, `group_by_aggregate`, `group_by_datetime_component`
+- Updated `run_tabular_sk_analysis()` so a response is accepted only when at least one analytical tabular function ran.
+- Added retry logging for discovery-only attempts so those paths are visible in diagnostics.
+- Updated tabular prompt guidance to explicitly reject discovery-only tool usage.
+- Filtered tabular citations so discovery-only calls are hidden when analytical tabular calls are present in the same analysis run.
+
+### Testing Approach
+- Added `functional_tests/test_tabular_analysis_rejects_discovery_only.py` to verify:
+ - discovery-only calls are explicitly rejected by prompt and retry guardrails,
+ - citation filtering prefers analytical calls,
+ - retry evaluation can isolate new invocations from the latest attempt.
+
+## Impact Analysis
+- Analytical tabular questions are less likely to be answered from schema previews or sample rows.
+- The mini-agent now has to perform a real computation before its output is trusted.
+- Citations better reflect the actual analytical operations used to answer the question.
+
+## Validation
+### Before
+- A single `describe_tabular_file` call could mark tabular analysis as complete.
+- Users could receive answers based on preview rows with thoughts showing tabular analysis as successful.
+
+### After
+- Discovery-only tool usage triggers a retry instead of being accepted as completed analysis.
+- Successful tabular analysis now requires a computed analytical call.
+- When analytical calls exist, tabular citations focus on those calls instead of schema-only discovery helpers.
+
+## Related Validation Assets
+- Functional test: `functional_tests/test_tabular_analysis_rejects_discovery_only.py`
+- Related fix: `docs/explanation/fixes/v0.239.033/TABULAR_DATETIME_COMPONENT_ANALYSIS_FIX.md`
+- Related fix: `docs/explanation/fixes/v0.239.032/TABULAR_WORKSPACE_TRIGGER_AND_THOUGHTS_FIX.md`
diff --git a/docs/explanation/fixes/v0.239.035/TABULAR_TOOL_CALL_THOUGHTS_FIX.md b/docs/explanation/fixes/v0.239.035/TABULAR_TOOL_CALL_THOUGHTS_FIX.md
new file mode 100644
index 00000000..15481301
--- /dev/null
+++ b/docs/explanation/fixes/v0.239.035/TABULAR_TOOL_CALL_THOUGHTS_FIX.md
@@ -0,0 +1,61 @@
+# Tabular Tool Call Thoughts Fix
+
+## Issue Description
+Tabular analysis thoughts were summarized as generic wrapper messages such as `Running tabular analysis on 1 workspace file(s)` and `Tabular analysis completed using 1 tool call(s)`. That hid which specific tabular tools actually ran, making it harder to understand whether the system queried, filtered, grouped, or only inspected the file.
+
+**Version implemented:** 0.239.035
+
+Fixed/Implemented in version: **0.239.035**
+
+Related `config.py` update: `VERSION` was bumped to `0.239.035`.
+
+## Root Cause Analysis
+1. **Thoughts were recorded at the workflow level instead of the tool level**
+ - The workspace and chat tabular paths emitted only start/completion wrapper thoughts.
+ - Individual plugin invocations were collected for citations but not surfaced as separate tabular thoughts.
+2. **Users could not see what analysis actually happened**
+ - A completion message with a tool count did not reveal whether the mini-agent used `query_tabular_data`, `group_by_datetime_component`, `aggregate_column`, or other functions.
+3. **The agent tool-call pattern already existed elsewhere**
+ - Agent execution paths already emitted one thought per plugin invocation, but the tabular pre-analysis flow had not adopted the same level of detail.
+
+## Technical Details
+### Files Modified
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_workspace_tabular_trigger_and_thoughts.py`
+
+### Code Changes Summary
+- Added helpers in `route_backend_chats.py` to:
+ - format concise tabular tool thought content,
+ - sanitize thought detail fields,
+ - convert tabular plugin invocations into per-tool thought payloads.
+- Replaced generic workspace and chat tabular wrapper thoughts with one `tabular_analysis` thought per tabular plugin invocation.
+- Preserved failure thoughts when tabular analysis cannot compute results.
+- Kept enhanced citations behavior unchanged while making the thoughts feed more transparent.
+
+### Testing Approach
+- Updated `functional_tests/test_workspace_tabular_trigger_and_thoughts.py` to verify:
+ - per-tool tabular thought helpers exist,
+ - workspace and streaming paths emit tool-level thought payload loops,
+ - generic completion wrapper thoughts are no longer used,
+ - formatted thought payloads contain useful parameters while excluding user and conversation identifiers.
+
+## Impact Analysis
+- Processing Thoughts now shows which tabular tool functions actually ran.
+- Users can distinguish schema inspection, filtering, grouping, and datetime analysis directly from the thoughts timeline.
+- Debugging tabular behavior is easier because the thought feed reflects the real analysis steps instead of only wrapper status messages.
+
+## Validation
+### Before
+- Thoughts showed only generic tabular wrapper messages.
+- Users could not tell which tabular function actually answered the question.
+
+### After
+- Thoughts include individual entries such as the exact tabular function invoked and its key parameters.
+- Generic wrapper completion thoughts are replaced by specific tabular tool-call thoughts.
+- Failure thoughts still appear when tabular analysis cannot compute results.
+
+## Related Validation Assets
+- Functional test: `functional_tests/test_workspace_tabular_trigger_and_thoughts.py`
+- Related fix: `docs/explanation/fixes/v0.239.034/TABULAR_COMPUTED_ANALYSIS_ENFORCEMENT_FIX.md`
+- Related fix: `docs/explanation/fixes/v0.239.033/TABULAR_DATETIME_COMPONENT_ANALYSIS_FIX.md`
diff --git a/docs/explanation/fixes/v0.239.036/TABULAR_GROUPED_PEAK_SUMMARY_FIX.md b/docs/explanation/fixes/v0.239.036/TABULAR_GROUPED_PEAK_SUMMARY_FIX.md
new file mode 100644
index 00000000..5dd1a069
--- /dev/null
+++ b/docs/explanation/fixes/v0.239.036/TABULAR_GROUPED_PEAK_SUMMARY_FIX.md
@@ -0,0 +1,74 @@
+# Tabular Grouped Peak Summary Fix
+
+## Issue Description
+Peak-style analytical questions such as `During what hours of the day do departure queues peak?` still depended too heavily on the model interpreting raw grouped output. The plugin could group by hour, but it did not return explicit highest and lowest group summary fields, and datetime parsing relied too much on generic inference for common US-style timestamps.
+
+**Version implemented:** 0.239.036
+
+Fixed/Implemented in version: **0.239.036**
+
+Related `config.py` update: `VERSION` was bumped to `0.239.036`.
+
+## Root Cause Analysis
+1. **Grouped outputs lacked explicit extremes**
+ - `group_by_datetime_component` and `group_by_aggregate` returned grouped data, but not direct highest and lowest group summaries.
+ - For peak-style questions, the model had to infer the answer from raw JSON instead of using clearly labeled summary fields.
+2. **Common US-style datetime strings were not parsed as explicitly as they should be**
+ - Real data such as the FAA sample file uses values like `5/14/2026 8:31:36 AM`.
+ - The plugin relied on generic fallback parsing too early, which is weaker and noisier than handling common formats directly.
+3. **The tabular prompt did not teach the model to use grouped summary fields**
+ - Even when grouped results were available, the prompt did not explicitly steer peak-style questions toward the strongest summary outputs.
+
+## Technical Details
+### Files Modified
+- `application/single_app/semantic_kernel_plugins/tabular_processing_plugin.py`
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_tabular_grouped_peak_summary.py`
+
+### Code Changes Summary
+- Improved `_parse_datetime_like_series()` to explicitly handle common date and datetime formats before generic fallback parsing.
+- Added `_build_grouped_summary()` so grouped outputs can expose:
+ - `highest_group`
+ - `highest_value`
+ - `lowest_group`
+ - `lowest_value`
+ - `average_group_value`
+ - `median_group_value`
+ - `second_highest_group`
+ - `second_highest_value`
+- Extended `group_by_aggregate()` to support:
+ - `median`
+ - `std`
+ - `top_n`
+ - `sort_descending`
+ - grouped summary fields and ranked `top_results`
+- Extended `group_by_datetime_component()` to return grouped summary fields alongside `top_results`.
+- Updated the tabular SK prompt so peak-style questions explicitly use the grouped summary fields.
+
+### Testing Approach
+- Added `functional_tests/test_tabular_grouped_peak_summary.py` to verify:
+ - artifact-style `M/D/YYYY h:mm:ss AM/PM` timestamps group correctly by hour,
+ - grouped datetime outputs return highest and lowest summaries,
+ - grouped aggregate outputs return generic peak summaries,
+ - the route prompt mentions the new grouped summary guidance.
+
+## Impact Analysis
+- Peak-style questions are easier for the model to answer correctly because the plugin now returns explicit extremes.
+- Common tabular files with US-style date/time strings are parsed more reliably.
+- The enhancements remain generic and reusable for any grouped categorical or time-based tabular analysis.
+
+## Validation
+### Before
+- The plugin could compute grouped values but forced the model to infer peaks from raw grouped JSON.
+- Timestamp parsing depended more than necessary on generic datetime inference.
+
+### After
+- Grouped tools return explicit highest and lowest summary fields for peak-style interpretation.
+- Artifact-style timestamps like those observed in the FAA CSV are parsed directly by known formats.
+- The route prompt now encourages the model to use the summary fields when answering peak and busiest questions.
+
+## Related Validation Assets
+- Functional test: `functional_tests/test_tabular_grouped_peak_summary.py`
+- Related fix: `docs/explanation/fixes/v0.239.035/TABULAR_TOOL_CALL_THOUGHTS_FIX.md`
+- Related fix: `docs/explanation/fixes/v0.239.033/TABULAR_DATETIME_COMPONENT_ANALYSIS_FIX.md`
diff --git a/docs/explanation/fixes/v0.239.037/TABULAR_TOOL_ERROR_RETRY_AND_THOUGHTS_FIX.md b/docs/explanation/fixes/v0.239.037/TABULAR_TOOL_ERROR_RETRY_AND_THOUGHTS_FIX.md
new file mode 100644
index 00000000..52625425
--- /dev/null
+++ b/docs/explanation/fixes/v0.239.037/TABULAR_TOOL_ERROR_RETRY_AND_THOUGHTS_FIX.md
@@ -0,0 +1,65 @@
+# Tabular Tool Error Retry and Thoughts Fix
+
+## Issue Description
+A failed analytical tabular tool call could still be treated as successful analysis when the plugin returned a JSON error payload rather than raising an exception. This let the chat stop after a single failed tabular tool attempt and produce a weak follow-up answer instead of retrying or falling back. It also left the visible thought feed too thin compared with the internal debug trail.
+
+**Version implemented:** 0.239.037
+
+Fixed/Implemented in version: **0.239.037**
+
+Related `config.py` update: `VERSION` was bumped to `0.239.037`.
+
+## Root Cause Analysis
+1. **Analytical tool presence was treated as success even when the result payload contained an error**
+ - `run_tabular_sk_analysis()` counted analytical function invocations without inspecting whether the returned JSON contained an `error` field.
+ - A single failed call such as `group_by_datetime_component` missing `aggregate_column` could therefore stop the retry flow early.
+2. **Retry attempts did not receive the previous tool error context**
+ - When the first tool call failed, the next SK attempt had no direct feedback about what argument was wrong.
+3. **Thoughts surfaced the tool call but not the recovery path**
+ - The UI could show a failed tool invocation, but not whether the system retried, recovered via fallback, or simply stopped.
+
+## Technical Details
+### Files Modified
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_tabular_tool_error_retry_and_thoughts.py`
+
+### Code Changes Summary
+- Added helpers to inspect tabular invocation result payloads and extract embedded JSON error messages.
+- Updated analytical invocation classification so tool calls returning JSON errors are treated as failed, not successful.
+- Updated citation filtering so failed analytical tabular calls do not appear as successful tool citations.
+- Fed previous tool error messages back into subsequent SK retry prompts.
+- Added tabular status thoughts for:
+ - recovery after retrying tool errors,
+ - recovery via internal fallback after tool errors,
+ - tool-error state before fallback when computation still fails.
+- Updated tabular tool-call thoughts so JSON error payloads render as failed tool thoughts in the UI.
+
+### Testing Approach
+- Added `functional_tests/test_tabular_tool_error_retry_and_thoughts.py` to verify:
+ - JSON error payloads are classified as failed analytical calls,
+ - failed analytical calls do not become citations,
+ - failed tool thoughts show error details,
+ - recovery thoughts are emitted for internal fallback,
+ - retry prompts include previous tool error feedback.
+
+## Impact Analysis
+- A single failed analytical tabular tool call no longer ends the analysis prematurely.
+- Retry attempts have better context to correct bad tool arguments.
+- The thought feed now better explains the difference between a failed tool call and a recovered final analysis.
+
+## Validation
+### Before
+- The system could stop after one failed analytical tool call.
+- A JSON error payload could still be treated like successful analysis.
+- Thoughts did not clearly show recovery after tool errors.
+
+### After
+- Failed analytical tool calls trigger retry or fallback instead of being accepted as success.
+- Previous tool errors are fed back into the retry prompt.
+- The UI can show failed tool calls and the recovery/fallback status more clearly.
+
+## Related Validation Assets
+- Functional test: `functional_tests/test_tabular_tool_error_retry_and_thoughts.py`
+- Related fix: `docs/explanation/fixes/v0.239.036/TABULAR_GROUPED_PEAK_SUMMARY_FIX.md`
+- Related fix: `docs/explanation/fixes/v0.239.035/TABULAR_TOOL_CALL_THOUGHTS_FIX.md`
diff --git a/docs/explanation/fixes/v0.239.038/TABULAR_YEAR_TREND_AND_SUMMARY_GUARDRAILS_FIX.md b/docs/explanation/fixes/v0.239.038/TABULAR_YEAR_TREND_AND_SUMMARY_GUARDRAILS_FIX.md
new file mode 100644
index 00000000..d92b0384
--- /dev/null
+++ b/docs/explanation/fixes/v0.239.038/TABULAR_YEAR_TREND_AND_SUMMARY_GUARDRAILS_FIX.md
@@ -0,0 +1,65 @@
+# Tabular Year Trend And Summary Guardrails Fix
+
+Fixed in version: **0.239.038**
+
+## Issue Description
+
+Broad tabular questions against workbook data could still produce weak behavior in two places:
+
+1. Year-based trend intent was under-inferred in the direct datetime fallback logic, even though the plugin already supports `year` grouping.
+2. Broad summaries could still mention speculative parser or follow-up analysis failures that were not the main requested outcome.
+
+This showed up with the Superstore workbook where yearly profit analysis should be computable from `Order Date`, but the response still mentioned a parameter parsing issue.
+
+## Root Cause Analysis
+
+- The route-level `infer_datetime_component()` helper recognized hour, weekday, month, quarter, week, and date intent, but not year, yearly, or annual intent.
+- The plugin-level `_try_numeric_conversion()` step converted already-parsed Excel datetime columns into numeric values before the datetime grouping logic ran, which broke valid workbook date columns like `Order Date`.
+- The mini-agent system prompt strongly required computed analysis, but it did not explicitly forbid narrating hypothetical or secondary failures when the user asked for a broad business summary.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/semantic_kernel_plugins/tabular_processing_plugin.py`
+- `application/single_app/config.py`
+- `functional_tests/test_tabular_datetime_component_analysis.py`
+
+### Code Changes Summary
+
+- Added `year`, `years`, `yearly`, `annual`, and `annually` keyword inference to the route datetime-component helper.
+- Preserved datetime and timedelta columns during the plugin numeric-conversion pass so Excel date columns remain usable for datetime grouping.
+- Expanded the tabular tool prompt text to frame `group_by_datetime_component` as the trend-analysis tool for year, quarter, month, week, day, and hour groupings.
+- Added a stronger prompt guardrail telling the tabular mini-agent not to mention hypothetical follow-up analyses, parser errors, or failed attempts unless the user explicitly asks about failures and real tool error output exists.
+- Extended the existing datetime regression test to cover yearly grouping behavior and the new prompt guidance.
+
+### Testing Approach
+
+- Reused the existing datetime component functional test file.
+- Added an in-memory yearly grouping scenario modeled on workbook-style date columns like `Order Date`.
+- Verified route prompt text contains the new yearly trend guidance and speculative-failure guardrail.
+
+## Impact Analysis
+
+- Improves reliability for yearly or annual time-series questions on CSV and Excel files.
+- Reduces the chance of broad tabular summaries surfacing distracting parser-error commentary when the user did not ask for failure details.
+- Keeps the tabular tool behavior generic rather than special-casing the Superstore workbook.
+
+## Validation
+
+### Test Results
+
+- `functional_tests/test_tabular_datetime_component_analysis.py`
+
+### Before
+
+- Year intent was not part of the direct datetime inference keywords.
+- Excel datetime columns could be converted away from datetime dtype before grouping.
+- The prompt left more room for broad summaries to mention hypothetical or secondary parsing failures.
+
+### After
+
+- Yearly and annual phrasing now map to `year` grouping intent.
+- Excel datetime columns remain intact for yearly and other datetime-component grouping.
+- The prompt explicitly steers the model toward computed findings only, without narrating unrelated failed attempts.
\ No newline at end of file
diff --git a/docs/explanation/fixes/v0.239.112/AGENT_AUDIT_METADATA_VALIDATION_FIX.md b/docs/explanation/fixes/v0.239.112/AGENT_AUDIT_METADATA_VALIDATION_FIX.md
new file mode 100644
index 00000000..0672c25d
--- /dev/null
+++ b/docs/explanation/fixes/v0.239.112/AGENT_AUDIT_METADATA_VALIDATION_FIX.md
@@ -0,0 +1,34 @@
+# Agent Audit Metadata Validation Fix (v0.239.112)
+
+## Issue Description
+Saving an existing agent could fail with `Agent validation failed: Additional properties are not allowed ('created_at', 'created_by', 'modified_at', 'modified_by' were unexpected)` when the browser sent back a round-tripped agent object that included server-managed audit metadata.
+
+## Root Cause Analysis
+The backend sanitized user-editable agent fields, but it did not strip server-managed audit or Cosmos metadata before schema validation. As a result, valid agent edits could be rejected purely because the payload still contained fields previously added by the backend.
+
+## Version Implemented
+Fixed/Implemented in version: **0.239.112**
+
+## Technical Details
+### Files Modified
+- application/single_app/functions_agent_payload.py
+- application/single_app/config.py
+- functional_tests/test_agent_audit_metadata_validation_fix.py
+
+### Code Changes Summary
+- Strip server-managed agent metadata such as `created_at`, `created_by`, `modified_at`, `modified_by`, `updated_at`, `last_updated`, `user_id`, `group_id`, and Cosmos system fields during payload sanitization.
+- Preserve existing save behavior where the backend rehydrates authoritative audit fields before persistence.
+- Add a regression test that validates a round-tripped agent payload can still pass schema validation.
+
+### Testing Approach
+- Run the functional test `functional_tests/test_agent_audit_metadata_validation_fix.py`.
+
+## Impact Analysis
+- Editing and saving existing agents no longer fails when the client includes backend-managed metadata.
+- The backend now treats audit metadata as authoritative server state rather than client-provided input.
+
+## Validation
+- Functional test: functional_tests/test_agent_audit_metadata_validation_fix.py
+
+## Reference to Config Version Update
+- Version updated in application/single_app/config.py to **0.239.112**.
\ No newline at end of file
diff --git a/docs/explanation/index.md b/docs/explanation/index.md
index cddbc6ba..8930a58c 100644
--- a/docs/explanation/index.md
+++ b/docs/explanation/index.md
@@ -3,6 +3,8 @@ Welcome to the **Explanation** section. Here you'll find understanding-oriented
- [Architecture](/explanation/architecture/)
- [Design Principles](/explanation/design_principles/)
- [Feature Guidance](/explanation/feature_guidance/)
+- [Running Simple Chat Locally](/explanation/running_simplechat_locally/)
+- [Running Simple Chat in Azure Production](/explanation/running_simplechat_azure_production/)
- **Scenarios:**
- [Agent Examples](/explanation/scenarios/agents/)
- [Workspace Examples](/explanation/scenarios/workspaces/)
diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md
index 8077ff3f..09224196 100644
--- a/docs/explanation/release_notes.md
+++ b/docs/explanation/release_notes.md
@@ -2,7 +2,343 @@
# Feature Release
-### **(v0.239.001)**
+### **(v0.240.001)**
+
+#### Bug Fixes
+
+* **Pillow PSD Upload Hardening**
+ * Updated the application to use `pillow==12.1.1`, moving the app off the vulnerable Pillow range for specially crafted PSD image parsing.
+ * Hardened admin logo and favicon uploads so Pillow now only opens the PNG and JPEG formats already allowed by the route, preventing disguised PSD content from being decoded during upload processing.
+ * (Ref: `application/single_app/requirements.txt`, `application/single_app/route_frontend_admin_settings.py`, `functional_tests/test_pillow_psd_upload_hardening.py`)
+
+* **Changed-Files GitHub Action Supply Chain Remediation**
+ * Updated the release-notes pull request workflow to use the patched `tj-actions/changed-files@v46.0.1` release after the March 2025 supply chain compromise affecting older tag families.
+ * Added a functional regression check to ensure the workflow does not drift back to the known malicious commit or an older vulnerable action reference.
+ * (Ref: `release-notes-check.yml`, `test_changed_files_action_version.py`, GitHub Actions workflow security, CI dependency pinning)
+
+* **Personal Conversation Notification Scope Detection**
+ * Fixed a scope-detection bug where personal chat completions could save successfully without creating a completion notification or unread dot when unrelated active workspace state was still present in session.
+ * Personal completion-side effects are now determined from the saved conversation type instead of active workspace session values.
+ * (Ref: personal chat scope gating, `route_backend_chats.py`, `test_chat_completion_notifications.py`)
+
+* **Distributed Background Task Locks**
+ * Added Cosmos-backed distributed lock documents for approval expiry and retention policy background jobs so duplicate execution is reduced across multiple Gunicorn workers and App Service instances.
+ * Kept the current web-app-hosted scheduler model intact so teams can continue running these jobs from the existing App Service while improving cross-worker coordination.
+ * Updated the startup documentation and added functional validation for the distributed lock wiring.
+ * (Ref: `background_tasks.py`, `SIMPLECHAT_STARTUP.md`, `test_background_task_distributed_locks.py`, `test_startup_scheduler_support.py`)
+
+* **Background Task Default-On Gating**
+ * Updated the web runtime background task gate so scheduler loops now start by default even when `SIMPLECHAT_RUN_BACKGROUND_TASKS` is unset.
+ * Only explicit false-like values such as `0`, `false`, `no`, or `off` now disable the background loops, which matches the requested deployment behavior.
+ * Updated the startup guide and Gunicorn runtime validation test to reflect the new default-on behavior.
+ * (Ref: `app.py`, `SIMPLECHAT_STARTUP.md`, `test_gunicorn_startup_support.py`)
+
+* **Gunicorn Production Startup Support**
+ * Updated the app bootstrap so production deployments can run cleanly under Gunicorn instead of relying on Flask's built-in server, which is a poor fit for long-lived streaming chat requests on App Service.
+ * Added a shared Gunicorn config, switched the container entrypoint to Gunicorn, and made application initialization idempotent so startup logic can run safely in multi-worker web processes.
+ * Background timer and retention loops are now disabled by default under Gunicorn workers to avoid duplicating scheduler-style threads across workers, while local debug startup continues to use the Flask development server.
+ * (Ref: `app.py`, `gunicorn.conf.py`, `Dockerfile`, `test_gunicorn_startup_support.py`)
+
+* **Streaming-Only Chat Path**
+ * Updated the first-party chat experience so normal sends, retries, and message edits now use the streaming chat path instead of maintaining a separate non-streaming UI path.
+ * Preserved parity-sensitive behavior by extending the streaming flow to finalize image-generation responses correctly and by adding a backend compatibility bridge for retry, edit, and image-generation requests while the legacy `/api/chat` route remains in transition.
+ * Removed the chat-page streaming toggle, updated the UI to treat streaming as required behavior, and added regression coverage to prevent first-party chat modules from drifting back to direct `/api/chat` calls.
+ * (Ref: `route_backend_chats.py`, `chat-messages.js`, `chat-streaming.js`, `chat-retry.js`, `chat-edit.js`, `chats.html`, `test_streaming_only_chat_path.py`)
+
+* **Embedding Retry-After Wait Time Handling**
+ * Fixed embedding retries so `429 Too Many Requests` responses now honor server-provided wait times from `Retry-After` style headers instead of always using local backoff timing.
+ * This reduces avoidable repeat throttling during document processing, batched embedding generation, and search embedding requests when Azure OpenAI asks the client to wait.
+ * The existing exponential backoff behavior remains in place as a fallback when the service does not provide a usable retry delay.
+ * (Ref: `functions_content.py`, embedding retry logic, `test_embedding_rate_limit_wait_time.py`)
+
+* **SQL Plugin Key Vault Secret Storage**
+ * New and updated SQL Query and SQL Schema actions now store sensitive values such as connection strings and passwords in Azure Key Vault when Key Vault secret storage is enabled.
+ * Editing an existing SQL action now preserves stored Key Vault-backed credentials, including the SQL test connection flow, so users do not need to re-enter unchanged secrets just to validate or save the action.
+ * Personal, group, and global action flows now preserve existing secret references during updates, clean them up correctly on delete, and redact secret-bearing plugin values from logs.
+ * Existing plaintext SQL action credentials are not backfilled automatically; they move to Key Vault the next time the action is saved while Key Vault storage is enabled.
+ * (Ref: `functions_keyvault.py`, `route_backend_plugins.py`, `plugin_modal_stepper.js`, `workspace_plugins.js`, SQL action configuration)
+
+* **Group/Public Expanded Document Tags**
+ * Fixed group and public workspace list views so expanding a document now shows its tags, matching the personal workspace experience.
+ * The fix adds color-coded tag badges with a `No tags` fallback in expanded document details without changing the existing backend document APIs.
+ * (Ref: `group_workspaces.html`, `public_workspace.js`, expanded document details, workspace tag rendering)
+
+* **Agent Save Validation for Round-Tripped Metadata**
+ * Fixed agent saves failing when an existing personal, group, or global agent was edited and the browser sent back backend-managed audit fields such as `created_at`, `created_by`, `modified_at`, and `modified_by`.
+ * Agent payload sanitization now strips backend-managed audit and Cosmos metadata before schema validation, while preserving server-side tracking during persistence.
+ * (Ref: `functions_agent_payload.py`, `route_backend_agents.py`, agent schema validation, functional test coverage)
+
+* **Multi-Sheet Workbook Tabular Analysis**
+ * Fixed multi-sheet Excel workbooks being analyzed from the wrong worksheet during tabular chat responses. Questions that clearly target a specific tab, such as asset values in a workbook with `Assets`, `Balance`, and `Income` sheets, no longer silently default to the first sheet.
+ * Tabular runtime analysis now requires explicit `sheet_name` or `sheet_index` selection for analytical calls on multi-sheet workbooks, and the SK mini-agent preload now includes workbook sheet inventory and per-sheet schemas so the model can choose the correct worksheet before computing results.
+ * Enhanced citations and tabular previews now preserve worksheet context, using `Sheet: ` for sheet-specific references and `Location: Workbook Schema` for workbook-level schema citations instead of generic `Page 1` labels. The tabular preview modal also supports switching between workbook sheets.
+ * (Ref: `tabular_processing_plugin.py`, `route_backend_chats.py`, `route_enhanced_citations.py`, `chat-enhanced-citations.js`, `chat-citations.js`, `chat-messages.js`)
+
+* **Tabular Citation Conversation Ownership Check**
+ * Fixed an IDOR vulnerability on `/api/enhanced_citations/tabular` where any authenticated user who could guess a `conversation_id` and `file_id` could download another user's chat-uploaded tabular files.
+ * The endpoint now reads the conversation document from Cosmos DB and verifies that `conversation.user_id` matches the current user before serving the blob. Returns 403 Forbidden on mismatch and 404 if the conversation does not exist.
+ * (Ref: `route_enhanced_citations.py`, `cosmos_conversations_container`)
+
+* **Tabular Preview `max_rows` Parameter Validation**
+ * The `max_rows` query parameter on `/api/enhanced_citations/tabular_preview` was parsed with bare `int()`, causing a 500 error on non-integer input. Switched to Flask's `request.args.get(..., type=int)` which silently falls back to the default on invalid input, matching the pattern used by other endpoints.
+ * (Ref: `route_enhanced_citations.py`)
+
+* **On-Demand Summary Generation — Content Normalization Fix**
+ * Fixed the `POST /api/conversations//summary` endpoint failing with an error when generating summaries from the conversation details modal.
+ * Root cause: message `content` in Cosmos DB can be a list of content parts (e.g., `[{type: "text", text: "..."}]`) rather than a plain string. The endpoint was passing the raw list as `content_text`, which either stringified incorrectly or produced empty transcript text.
+ * Now uses `_normalize_content()` to properly flatten list/dict content into plain text, matching the export pipeline's behavior.
+ * (Ref: `route_backend_conversations.py`, `_normalize_content`, `generate_conversation_summary`)
+
+* **Export Summary Reasoning-Model Compatibility**
+ * Fixed export intro summary generation failing or returning empty content with reasoning-series models (gpt-5, o1, o3) through a series of incremental fixes: using `developer` role instead of `system` for instruction messages, removing all `max_tokens` / `max_completion_tokens` caps so the model decides output length naturally, and adding null-safe content extraction for `None` responses.
+ * Summary now includes ALL messages (user, assistant, system, file, image analysis) for full context, with a simplified prompt producing 1-2 factual paragraphs.
+ * Added detailed debug logging showing message count, character count, model name, role, and finish reason.
+ * (Ref: `route_backend_conversation_export.py`, `_build_summary_intro`, `generate_conversation_summary`)
+
+* **Conversation Export Schema and Markdown Refresh**
+ * Fixed conversation exports lagging behind the live chat schema. JSON exports now include processing thoughts, normalized citations, and the raw document/web/tool citation buckets stored with assistant messages.
+ * Fixed Markdown exports being too flat and text-heavy by reorganizing them into a transcript-first layout with appendices for metadata, message details, references, thoughts, and supplemental records.
+ * Fixed exported conversations including content that no longer matched the visible chat by filtering deleted messages and inactive-thread retries, then reapplying thread-aware ordering before export.
+ * (Ref: `route_backend_conversation_export.py`, `test_conversation_export.py`, conversation export rendering)
+
+* **Export Tag/Classification Rendering Fix**
+ * Fixed conversation tags and classifications rendering as raw Python dicts (e.g., `{'category': 'model', 'value': 'gpt-5'}`) in both Markdown and PDF exports.
+ * Tags now display as readable `category: value` strings, with smart handling for participant names, document titles, and generic category/value pairs.
+ * (Ref: `route_backend_conversation_export.py`, `_format_tag` helper, Markdown/PDF metadata rendering)
+
+* **Export Summary Error Visibility**
+ * Added `debug_print` and `log_event` logging to all summary generation error paths, including the empty-response path that previously failed silently.
+ * The actual error detail is now shown in both Markdown and PDF exports when summary generation fails, replacing the generic "could not be generated" message.
+ * (Ref: `route_backend_conversation_export.py`, `_build_summary_intro`, export error rendering)
+
+* **Content Safety for Streaming Chat Path**
+ * Added full Azure AI Content Safety checking to the streaming (`/api/chat/stream`) SSE path, matching the existing non-streaming (`/api/chat`) implementation.
+ * Previously, only the non-streaming path performed content safety analysis; streaming conversations bypassed safety checks entirely.
+ * Implementation includes: `AnalyzeTextOptions` analysis, severity threshold checking (severity ≥ 4 blocks the message), blocklist matching, persistence of blocked messages to `cosmos_safety_container`, creation of safety-role message documents, and proper SSE event delivery of blocked status to the client.
+ * On block, the streaming generator yields the safety message and `[DONE]` event, then stops — preventing any further LLM invocation.
+ * Errors in the content safety call are caught and logged without breaking the chat flow, consistent with the non-streaming behavior.
+ * (Ref: `route_backend_chats.py`, streaming SSE generator, `AnalyzeTextOptions`, `cosmos_safety_container`)
+
+* **SQL Schema Plugin — Eliminate Redundant Schema Calls**
+ * Fixed agent calling `get_database_schema` twice per query even though the full schema was already injected into the agent's instructions at load time.
+ * Root cause: The `@kernel_function` descriptions in `sql_schema_plugin.py` said "ALWAYS call this function FIRST," which overrode the schema context already available in the instructions.
+ * Updated all four function descriptions (`get_database_schema`, `get_table_schema`, `get_table_list`, `get_relationships`) to use the resilient pattern: "If the database schema is already provided in your instructions, use that directly and do NOT call this function."
+ * This eliminates ~400ms+ of unnecessary database round trips per query and aligns with the same pattern already used in `sql_query_plugin.py`.
+ * (Ref: `sql_schema_plugin.py`, `@kernel_function` descriptions, schema injection)
+
+* **SQL Schema Plugin — Empty Tables from INFORMATION_SCHEMA**
+ * Fixed `get_database_schema` returning `'tables': {}` (empty) despite the database having tables, while relationships were returned correctly.
+ * Root cause: SQL Server table/column enumeration used `INFORMATION_SCHEMA.TABLES` and `INFORMATION_SCHEMA.COLUMNS` views, which returned empty results in the Azure SQL environment. Meanwhile, the relationships query used `sys.foreign_keys`/`sys.tables`/`sys.columns` catalog views which worked perfectly.
+ * Migrated all SQL Server schema queries to use `sys.*` catalog views consistently: `sys.tables`/`sys.schemas` for table enumeration, `sys.columns` with `TYPE_NAME()` for column details, and `sys.indexes`/`sys.index_columns` for primary key detection.
+ * Fixed `pyodbc.Row` handling throughout the plugin — removed all `isinstance(table, tuple)` checks that could fail with pyodbc Row objects, replaced with robust try/except indexing.
+ * This enables the full schema (tables, columns, types, PKs, FKs) to be injected into agent instructions, allowing agents to construct complex multi-table JOINs for analytical queries.
+ * (Ref: `sql_schema_plugin.py`, `sys.tables`, `sys.columns`, `sys.indexes`, pyodbc.Row handling)
+
+* **SQL Query Plugin — Auto-Create Companion Schema Plugin**
+ * Fixed the remaining issue where SQL-connected agents still asked for clarification instead of querying the database, even after description improvements.
+ * Root cause: Agents configured with only a `sql_query` action never had a `SQLSchemaPlugin` loaded in the kernel. The descriptions demanded calling `get_database_schema` — a function that didn't exist — creating an impossible dependency that caused the LLM to ask for clarification.
+ * `LoggedPluginLoader` now automatically creates a companion `SQLSchemaPlugin` whenever a `SQLQueryPlugin` is loaded, using the same connection details. This ensures schema discovery is always available.
+ * Updated `@kernel_function` descriptions to be resilient: "If the database schema is provided in your instructions, use it directly. Otherwise, call get_database_schema." This dual-path approach works whether schema is injected via instructions or available via plugin functions.
+ * Added fallback in `_extract_sql_schema_for_instructions()` to also detect `SQLQueryPlugin` instances and create a temporary schema extractor if no `SQLSchemaPlugin` is found.
+ * (Ref: `logged_plugin_loader.py`, `sql_query_plugin.py`, `semantic_kernel_loader.py`)
+
+* **SQL Query Plugin Schema Awareness**
+ * Fixed agents connected to SQL databases asking users for clarification about table/column names instead of querying the database directly.
+ * Root cause: SQL Query and SQL Schema plugin `@kernel_function` descriptions were generic with no workflow guidance, agent instructions had no database schema context, and the two plugins operated independently with no linkage.
+ * Rewrote all `@kernel_function` descriptions in both SQL plugins to be prescriptive workflow guides (modeled after the working LogAnalyticsPlugin), explicitly instructing the LLM to discover schema first before generating queries.
+ * Added auto-injection of database schema into agent instructions at load time — when SQL Schema plugins are detected, the full schema (tables, columns, types, relationships) is fetched and appended to the agent's system prompt.
+ * Added new `query_database(question, query)` convenience function to `SQLQueryPlugin` for intent-aligned tool calling.
+ * Enabled the SQL-specific plugin creation path in `logged_plugin_loader.py` (was previously commented out).
+ * (Ref: `sql_query_plugin.py`, `sql_schema_plugin.py`, `semantic_kernel_loader.py`, `logged_plugin_loader.py`)
+
+* **Chat-Uploaded Tabular Files Now Trigger SK Mini-Agent in Model-Only Mode**
+ * Fixed an issue where tabular files (CSV, XLSX, XLS, XLSM) uploaded directly to a chat conversation were not analyzed by the SK mini-agent when no agent was selected. The model would describe what analysis it would perform instead of returning actual computed results.
+ * **Root Cause**: The mini SK agent only triggered from search results, but chat-uploaded files are stored in blob storage and not indexed in Azure AI Search. Additionally, the streaming path completely ignored `file` role messages in conversation history.
+ * **Fix**: Both streaming and non-streaming chat paths now detect chat-uploaded tabular files during conversation history building and trigger `run_tabular_sk_analysis(source_hint="chat")` to pre-compute results. The streaming path also now properly handles `file` role messages (tabular and non-tabular) matching the non-streaming path's behavior.
+ * (Ref: `route_backend_chats.py`, `run_tabular_sk_analysis()`, `collect_tabular_sk_citations()`)
+
+* **Group SQL Action/Plugin Save Failure**
+ * Fixed group SQL actions (sql_query and sql_schema types) failing to save correctly due to missing endpoint placeholder. Group routes now apply the same `sql://sql_query` / `sql://sql_schema` endpoint logic as personal action routes.
+ * Fixed Step 4 (Advanced) dynamic fields overwriting Step 3 (Configuration) SQL values with empty strings during form data collection. SQL types now skip the dynamic field merge entirely since Step 3 already provides all necessary configuration.
+ * Fixed auth type definition schemas (`sql_query.definition.json`, `sql_schema.definition.json`) only allowing `connection_string` auth type, blocking `user`, `identity`, and `servicePrincipal` types that the UI and runtime support.
+ * Fixed `__Secret` key suffix mismatch in additional settings schemas where `connection_string__Secret` and `password__Secret` didn't match the runtime's expected `connection_string` and `password` field names. Also removed duplicate `azuresql` enum value.
+ * (Ref: `route_backend_plugins.py`, `plugin_modal_stepper.js`, `sql_query.definition.json`, `sql_schema.definition.json`, `sql_query_plugin.additional_settings.schema.json`, `sql_schema_plugin.additional_settings.schema.json`)
+
+#### New Features
+
+* **Conversation Completion Notifications**
+ * Added personal chat completion notifications so users who leave a conversation before the assistant finishes can still see that a response is ready.
+ * Notification clicks deep-link back into the completed conversation, and personal conversations now show a green unread dot until the assistant response is opened.
+ * The unread state and notification lifecycle are wired into the chat conversation list, sidebar list, and mark-read flow so the indicator clears once the conversation is actually viewed.
+ * (Ref: conversation notifications, unread assistant responses, `route_backend_chats.py`, `route_backend_conversations.py`, `functions_notifications.py`, `functions_conversation_unread.py`, `chat-conversations.js`, `chat-sidebar-conversations.js`)
+
+* **Background Chat Completion Away From Chat Page**
+ * Updated streaming chat execution so assistant responses can continue running after the user leaves the chat page instead of stopping when the browser disconnects from the stream.
+ * This keeps final assistant persistence, unread markers, and completion notifications reachable even when users navigate into Personal, Group, or other pages while a reply is still generating.
+ * (Ref: background stream execution, `BackgroundStreamBridge`, `route_backend_chats.py`, `test_chat_stream_background_execution.py`, `test_streaming_only_chat_path.py`)
+
+* **SimpleChat Startup and Scheduler Separation**
+ * Added deployment guidance for local development, Azure App Service native Python startup, and container runtimes so administrators can choose between direct Gunicorn startup and optional `python app.py` handoff behavior with clear environment-variable guidance.
+ * Extracted the scheduler-style logging timer, approval expiration, and retention loops into a shared background task module and added a dedicated `simplechat_scheduler.py` entrypoint so scheduled work can run in a separate process or job.
+ * This allows the web app to use Gunicorn with `workers=2` without duplicating scheduler loops inside every worker process, while keeping a legacy override available for single-process environments.
+ * (Ref: `app.py`, `background_tasks.py`, `simplechat_scheduler.py`, `SIMPLECHAT_STARTUP.md`, `test_startup_scheduler_support.py`)
+
+* **Chat Completion Notifications**
+ * Added personal chat completion notifications so users who leave a streaming conversation before the assistant finishes now receive a notification when the AI response is ready.
+ * Notification clicks deep-link directly back to the completed conversation, and personal conversations now show a green unread dot in both chat conversation lists until that response is opened.
+ * The unread state is cleared automatically when the conversation is opened or when the user stays on the chat page through stream completion, keeping the active-view experience clean without adding heartbeat tracking.
+ * (Ref: `route_backend_chats.py`, `route_backend_conversations.py`, `functions_notifications.py`, `functions_conversation_unread.py`, `chat-conversations.js`, `chat-sidebar-conversations.js`, `chat-streaming.js`, `test_chat_completion_notifications.py`)
+
+* **Configurable Tabular Preview Blob Size Limit**
+ * Added an admin-configurable maximum blob size for tabular file previews, replacing the previous hardcoded limit. Default is 200 MB.
+ * New **Tabular Preview Limits** card in the Enhanced Citations section of Admin Settings (Citations tab) lets admins increase or decrease the limit based on their compute resources and user population.
+ * Setting is stored as `tabular_preview_max_blob_size_mb` and accepts values from 1 to 1024 MB.
+ * (Ref: `route_enhanced_citations.py`, `functions_settings.py`, `admin_settings.html`)
+
+* **Tabular Preview Memory Optimization**
+ * The `/api/enhanced_citations/tabular_preview` endpoint no longer loads entire files into a DataFrame. It now uses `nrows` limits in `pandas.read_csv`/`read_excel` to read only the rows needed for the preview, and checks blob size before downloading to reject oversized files early.
+ * (Ref: `route_enhanced_citations.py`)
+
+* **Persistent Conversation Summaries**
+ * Summaries generated during conversation export are now saved to the conversation document in Cosmos DB for future reuse.
+ * Cached summaries include `message_time_start` and `message_time_end` — when a conversation has new messages beyond the cached range, a fresh summary is generated automatically.
+ * The conversation details modal now shows a **Summary** card at the top. If a summary exists it displays the content, generation date, and model used. If no summary exists a **Generate Summary** button with model selector lets users create one on demand.
+ * A **Regenerate** button is available on existing summaries to force a refresh with the currently selected model.
+ * New `POST /api/conversations//summary` endpoint accepts an optional `model_deployment` and returns the generated summary.
+ * The `GET /api/conversations//metadata` response now includes a `summary` field.
+ * Extracted `generate_conversation_summary()` as a shared helper used by both the export pipeline and the new API endpoint.
+ * (Ref: `route_backend_conversation_export.py`, `route_backend_conversations.py`, `chat-conversation-details.js`, `functions_conversation_metadata.py`)
+
+* **PDF Conversation Export**
+ * Added PDF as a third export format option alongside JSON and Markdown, giving users a print-ready, visually styled conversation archive.
+ * PDF output renders chat messages with colored bubbles that mirror the live chat UI: blue for user messages, gray for assistant messages, green for file messages, and amber for system messages.
+ * Message content is converted from Markdown to HTML for rich formatting (bold, italic, code blocks, lists, tables) inside the PDF.
+ * Full appendix structure is included (metadata, message details, references, processing thoughts, supplemental messages), matching the Markdown export layout.
+ * Rendering uses PyMuPDF's Story API on US Letter paper with 0.5-inch margins and automatic multi-page overflow.
+ * Works with both single-file and ZIP packaging; intro summaries are supported in PDF as well.
+ * Frontend format step updated to a 3-column card grid with a new PDF card using the `bi-filetype-pdf` icon.
+ * (Ref: `route_backend_conversation_export.py`, `chat-export.js`, PyMuPDF Story API, conversation export workflow)
+
+* **Conversation Export Intro Summaries**
+ * Added an optional AI-generated intro summary step to the conversation export workflow, so each exported chat can begin with a short abstract before the full transcript.
+ * Summary model selection now reuses the same model list shown in the chat composer, keeping the export flow aligned with the main chat experience.
+ * Works for both JSON and Markdown exports, including ZIP exports where each conversation keeps its own summary metadata.
+ * (Ref: `route_backend_conversation_export.py`, `chat-export.js`, conversation export workflow)
+
+* **Agent & Action User Tracking (created_by / modified_by)**
+ * All agent and action documents (personal, group, and global) now include `created_by`, `created_at`, `modified_by`, and `modified_at` fields that track which user created or last modified the entity.
+ * On updates, the original `created_by` and `created_at` values are preserved while `modified_by` and `modified_at` are refreshed with the current user and timestamp.
+ * New optional `user_id` parameter added to `save_group_agent`, `save_global_agent`, `save_group_action`, and `save_global_action` for caller-supplied user tracking (backward-compatible, defaults to `None`).
+ * (Ref: `functions_personal_agents.py`, `functions_group_agents.py`, `functions_global_agents.py`, `functions_personal_actions.py`, `functions_group_actions.py`, `functions_global_actions.py`)
+
+* **Activity Logging for Agent & Action CRUD Operations**
+ * Every create, update, and delete operation on agents and actions now generates an activity log record in the `activity_logs` Cosmos DB container and Application Insights.
+ * Six new logging functions: `log_agent_creation`, `log_agent_update`, `log_agent_deletion`, `log_action_creation`, `log_action_update`, `log_action_deletion`.
+ * Activity records include: `user_id`, `activity_type`, `entity_type` (agent/action), `operation` (create/update/delete), `workspace_type` (personal/group/global), and `workspace_context` (group_id when applicable).
+ * Logging is fire-and-forget — failures never break the CRUD operation.
+ * All personal, group, and admin routes for both agents and actions are wired up.
+ * (Ref: `functions_activity_logging.py`, `route_backend_agents.py`, `route_backend_plugins.py`)
+
+* **Tabular Data Analysis — SK Mini-Agent for Normal Chat**
+ * Tabular files (CSV, XLSX, XLS, XLSM) detected in search results now trigger a lightweight Semantic Kernel mini-agent that pre-computes data analysis before the main LLM response. This brings the same analytical depth previously only available in full agent mode to every normal chat conversation.
+ * **Automatic Detection**: When AI Search results include tabular files from any workspace (personal, group, or public) or chat-uploaded documents, the system automatically identifies them via the `TABULAR_EXTENSIONS` configuration and routes the query through the SK mini-agent pipeline.
+ * **Unified Workspace and Chat Handling**: Tabular files are processed identically regardless of their storage location. The plugin resolves blob paths across all four container types (`user-documents`, `group-documents`, `public-documents`, `personal-chat`) with automatic fallback resolution if the primary source lookup fails. A user asking about an Excel file in their personal workspace gets the same analytical treatment as one asking about a CSV uploaded directly to a chat.
+ * **Six Data Analysis Functions**: The `TabularProcessingPlugin` exposes `describe_tabular_file`, `aggregate_column` (sum, mean, count, min, max, median, std, nunique, value_counts), `filter_rows` (==, !=, >, <, >=, <=, contains, startswith, endswith), `query_tabular_data` (pandas query syntax), `group_by_aggregate`, and `list_tabular_files` — all registered as Semantic Kernel functions that the mini-agent orchestrates autonomously.
+ * **Pre-Computed Results Injected as Context**: The mini-agent's computed analysis (exact numerical results, aggregations, filtered data) is injected into the main LLM's system context so it can present accurate, citation-backed answers without hallucinating numbers.
+ * **Graceful Degradation**: If the mini-agent analysis fails for any reason, the system falls back to instructing the main LLM to use the tabular processing plugin functions directly, preserving full functionality.
+ * **Non-Streaming and Streaming Support**: Both chat modes are supported. The mini-agent runs synchronously before the main LLM call in both paths.
+ * **Requires Enhanced Citations**: The tabular processing plugin depends on the blob storage client initialized by the enhanced citations system. The `enable_enhanced_citations` admin setting must be enabled for tabular data analysis to activate.
+ * (Ref: `run_tabular_sk_analysis()`, `TabularProcessingPlugin`, `collect_tabular_sk_citations()`, `TABULAR_EXTENSIONS`)
+
+* **Tabular Tool Execution Citations**
+ * Every tool call made by the SK mini-agent during tabular analysis is captured and surfaced as an agent citation, providing full transparency into the data analysis pipeline.
+ * **Automatic Capture**: The existing `@plugin_function_logger` decorator on all `TabularProcessingPlugin` functions records each invocation including function name, input parameters, returned results, execution duration, and success/failure status.
+ * **Citation Format**: Tool execution citations appear in the same "Agent Tool Execution" modal used by full agent mode, showing `tool_name` (e.g., `TabularProcessingPlugin.aggregate_column`), `function_arguments` (the exact parameters passed), and `function_result` (the computed data returned).
+ * **End-to-End Auditability**: Users can verify exactly which aggregations, filters, or queries were run against their data, what parameters were used, and what raw results were returned — before the LLM summarized them into the final response.
+ * (Ref: `collect_tabular_sk_citations()`, `plugin_invocation_logger.py`)
+
+* **SK Mini-Agent Performance Optimization**
+ * Reduced typical tabular analysis time from ~74 seconds to an estimated ~30-33 seconds (55-60% reduction) through three complementary optimizations.
+ * **DataFrame Caching**: Per-request in-memory cache eliminates redundant blob downloads. Previously, each of the ~8 tool calls in a typical analysis downloaded and parsed the same file independently. Now the file is downloaded once and subsequent calls read from cache. Cache is automatically scoped to the request (new plugin instance per analysis) and garbage-collected afterward.
+ * **Pre-Dispatch Schema Injection**: File schemas (columns, data types, row counts, and a 3-row preview) are pre-loaded and injected into the SK mini-agent's system prompt before execution begins. This eliminates 2 LLM round-trips that were previously spent on file discovery (`list_tabular_files`) and schema inspection (`describe_tabular_file`), allowing the model to jump directly to analysis tool calls.
+ * **Async Plugin Functions**: All six `@kernel_function` methods converted to `async def` using `asyncio.to_thread()`. This enables Semantic Kernel's built-in `asyncio.gather()` to truly parallelize batched tool calls (e.g., 3 simultaneous `aggregate_column` calls) instead of executing them serially on the event loop.
+ * **Batching Instructions**: The system prompt now instructs the model to batch multiple independent function calls in a single response, reducing LLM round-trips further.
+ * (Ref: `_df_cache`, `asyncio.to_thread`, pre-dispatch schema injection in `run_tabular_sk_analysis()`)
+
+* **SQL Test Connection Button**
+ * Added a "Test Connection" button to the SQL Database Configuration section (Step 3) of the action wizard, allowing users to validate database connectivity before saving.
+ * Supports all database types: SQL Server, Azure SQL (with managed identity), PostgreSQL, MySQL, and SQLite.
+ * Shows inline success/failure alerts with a 15-second timeout cap and sanitized error messages.
+ * New backend endpoint: `POST /api/plugins/test-sql-connection`.
+ * (Ref: `route_backend_plugins.py`, `plugin_modal_stepper.js`, `_plugin_modal.html`)
+
+* **Per-Message Export**
+ * Added export and action options to the three-dots dropdown menu on individual chat messages (both AI and user messages).
+ * **Export to Markdown**: Downloads the message as a `.md` file with a role header. Entirely client-side.
+ * **Export to Word**: Generates a styled `.docx` document via a new backend endpoint (`POST /api/message/export-word`). Includes Markdown-to-Word formatting (headings, bold, italic, code blocks, lists) and a citations section when present.
+ * **Use as Prompt**: Inserts the raw message content directly into the chat input box for reuse — no clipboard, one click and it's ready to edit and send.
+ * **Open in Email**: Opens the user's default email client with the message pre-filled in the subject and body via `mailto:`.
+ * New options appear below a divider in the dropdown, preserving existing actions (Delete, Retry, Edit, Feedback).
+ * (Ref: `chat-message-export.js`, `chat-messages.js`, `route_backend_conversation_export.py`, per-message export)
+
+* **Custom Azure Environment Support in Bicep Deployment**
+ * Added `custom` as a supported `cloudEnvironment` value alongside `public` and `usgovernment`, enabling deployment to sovereign or custom Azure environments via Bicep.
+ * New Bicep parameters for custom environments: `customBlobStorageSuffix`, `customGraphUrl`, `customIdentityUrl`, `customResourceManagerUrl`, `customCognitiveServicesScope`, and `customSearchResourceUrl`. All of these are automatically populated from `az.environment()` defaults except `customGraphUrl`, which must be explicitly provided for custom cloud environments and can be overridden as needed.
+ * The `cloudEnvironment` parameter now defaults intelligently based on `az.environment().name`, and legacy values (`AzureCloud`, `AzureUSGovernment`) are mapped to SimpleChat's expected values (`public`, `usgovernment`).
+ * Custom environment app settings (`CUSTOM_GRAPH_URL_VALUE`, `CUSTOM_IDENTITY_URL_VALUE`, `CUSTOM_RESOURCE_MANAGER_URL_VALUE`, etc.) are conditionally injected only when `azurePlatform == 'custom'`.
+ * Replaced hardcoded ACR domain logic and auth issuer URLs with dynamic `az.environment()` lookups for better cross-cloud compatibility.
+ * Fixed trailing slash handling in `AUTHORITY` URL construction in `config.py` using `rstrip('/')`.
+ * (Ref: `deployers/bicep/main.bicep`, `deployers/bicep/modules/appService.bicep`, `config.py`, sovereign cloud support)
+
+* **Redis Key Vault Authentication**
+ * Added a new `key_vault` authentication type for Redis, allowing the Redis access key to be retrieved securely from Azure Key Vault at runtime rather than stored directly in settings.
+ * Applies across all Redis usage paths: app settings cache (`app_settings_cache.py`), session management (`app.py`), and the Redis test connection flow (`route_backend_settings.py`).
+ * Uses `retrieve_secret_direct()` from `functions_keyvault.py` to fetch the Redis key by its Key Vault secret name. Respects `key_vault_identity` for a user-assigned managed identity on the Key Vault client.
+ * New admin setting fields: `redis_auth_type` (values: `key`, `managed_identity`, `key_vault`) and `redis_key` (used as the Key Vault secret name when `key_vault` auth type is selected).
+ * **Files Modified**: `app_settings_cache.py`, `app.py` `configure_sessions`, `route_backend_settings.py` `_test_redis_connection`, `functions_keyvault.py` `retrieve_secret_direct`
+
+#### User Interface Enhancements
+
+* **Agent Responded Thought — Seconds & Total Duration**
+ * The "responded" thought now shows time in **seconds** instead of milliseconds, and clarifies it is the total time from the initial user message (e.g., `'gpt-5-nano' responded (16.3s from initial message)`).
+ * A `request_start_time` is now captured at the top of both the non-streaming and streaming chat handlers, so the duration reflects the full request lifecycle — including content safety, hybrid search, and agent invocation — not just the model response time.
+ * Applies to all three agent paths: local SK agents (non-streaming), Azure AI Foundry agents, and streaming SK agents.
+ * (Ref: `route_backend_chats.py`, `request_start_time`, agent responded thoughts)
+
+* **Enhanced Agent Execution Thoughts**
+ * Added detailed model-level status messages during agent execution, giving users full visibility into each stage of the AI pipeline.
+ * **Model Identification**: A new "Sending to '{deployment_name}'" thought appears immediately after "Sending to agent", showing the exact model deployment being used (e.g., `gpt-5-nano`).
+ * **Generating Response**: A "Generating response..." thought now appears before the agent begins its invocation loop, matching the existing behavior for non-agent GPT calls.
+ * **Model Responded with Duration**: A "'{deployment_name}' responded ({duration}ms)" thought appears after the agent completes, showing total wall-clock execution time.
+ * Applies to all three agent paths: local SK agents (streaming and non-streaming) and Azure AI Foundry agents.
+ * Uses the existing `generation` step type (lightning bolt icon) — no frontend changes required.
+ * (Ref: `route_backend_chats.py`, `ThoughtTracker`, agent execution pipeline)
+
+* **List/Grid View Toggle for Agents and Actions**
+ * Added a list/grid view toggle to all four workspace areas: personal agents, personal actions, group agents, and group actions.
+ * **Grid View**: Large cards with type icon, humanized name, truncated description, and action buttons (Chat, View, Edit, Delete as applicable).
+ * **List View**: Improved table layout with fixed column widths (28%/47%/25%), humanized display names, and truncated descriptions with hover tooltips for full text.
+ * **View Button**: New eye-icon button on every agent and action that opens a read-only detail modal with gradient-header summary cards (Basic Information, Model Configuration, Instructions for agents; Basic Information, Configuration for actions).
+ * **Name Humanization**: Display names are now automatically parsed — underscores and camelCase/PascalCase boundaries are converted to properly spaced, title-cased words (e.g., `myCustomAgent` → `My Custom Agent`).
+ * **Persistent Preference**: View mode selection (list/grid) is saved per area in localStorage and restored on page load.
+ * New shared utility module `view-utils.js` provides reusable functions for all four workspace areas.
+ * (Ref: `view-utils.js`, `workspace_agents.js`, `workspace_plugins.js`, `plugin_common.js`, `group_agents.js`, `group_plugins.js`, `workspace.html`, `group_workspaces.html`, `styles.css`)
+
+* **Chat with Agent Button for Group Agents**
+ * Added a "Chat" button to each group agent row, allowing users to quickly select a group agent and navigate to the chat page.
+ * (Ref: `group_agents.js`, `group_workspaces.html`)
+
+* **Hidden Deprecated Action Types**
+ * Deprecated action types (`sql_schema`, `ui_test`, `queue_storage`, `blob_storage`, `embedding_model`) are now hidden from the action creation wizard type selector. Existing actions of these types remain functional.
+ * (Ref: `plugin_modal_stepper.js`)
+
+* **Advanced Settings Collapse Toggle**
+ * Step 4 (Advanced) content is now hidden behind a collapsible toggle button ("Show Advanced Settings") instead of being displayed by default. Reduces visual noise for most users.
+ * For SQL action types, the redundant additional fields UI in Step 4 is hidden entirely since all SQL configuration is already handled in Step 3.
+ * Step 5 (Summary) no longer shows the raw additional fields JSON dump for SQL types, since that data is already shown in the SQL Database Configuration summary card.
+ * (Ref: `_plugin_modal.html`, `plugin_modal_stepper.js`)
+
+### **(v0.239.002)**
#### New Features
@@ -16,7 +352,7 @@
* **Retention Policy UI for Groups and Public Workspaces**
* Can now configure conversation and document retention periods directly from the workspace and group management page.
* Choose from preset retention periods ranging from 7 days to 10 years, use the organization default, or disable automatic deletion entirely.
-
+
* **Owner-Only Group Agent and Action Management**
* New admin setting to restrict group agent and group action management (create, edit, delete) to only the group Owner role.
* **Admin Toggle**: "Require Owner to Manage Group Agents and Actions" located in Admin Settings > My Groups section, under the existing group creation membership setting.
@@ -92,19 +428,6 @@
* **Files Modified**: `chat-documents.js`, `chat-messages.js`, `functions_search.py`, `route_backend_chats.py`, `chats.html`.
* (Ref: Multi-document selection, tag filtering, OData search integration, `CHAT_DOCUMENT_AND_TAG_FILTERING.md`)
-#### New Features
-
-* **Conversation Export**
- * Export one or multiple conversations from the Chat page in JSON or Markdown format.
- * **Single Export**: Use the ellipsis menu on any conversation to quickly export it.
- * **Multi-Export**: Enter selection mode, check the conversations you want, and click the export button.
- * A guided 4-step wizard walks you through selection review, format choice, packaging options (single file or ZIP archive), and download.
- * Sensitive internal metadata is automatically stripped from exported data for security.
-
-* **Retention Policy UI for Groups and Public Workspaces**
- * Can now configure conversation and document retention periods directly from the workspace and group management page.
- * Choose from preset retention periods ranging from 7 days to 10 years, use the organization default, or disable automatic deletion entirely.
-
#### Bug Fixes
* **Citation Parsing Bug Fix**
@@ -120,7 +443,7 @@
* Removed the membership verification from the `setActive` endpoint; the route still requires authentication (`@login_required`, `@user_required`) and the public workspaces feature flag (`@enabled_required`).
* Other admin-level endpoints (listing members, viewing stats, ownership transfer) retain their membership checks.
* (Ref: `route_backend_public_workspaces.py`, `api_set_active_public_workspace`)
-
+
* **Chats Page User Settings Hardening**
* Fixed a user-specific chats page failure where only one affected user could not load `/chats` due to malformed per-user settings data.
* **Root Cause**: The chats route assumed `user_settings["settings"]` was always a dictionary. If that field existed but had an invalid type (for example string, null, or list), the page could fail before rendering.
@@ -175,7 +498,6 @@
* **Solution**: Removed the post-save `global_selected_agent` enforcement from the add and edit routes. The delete route already correctly prevents deletion of the selected agent.
* (Ref: `route_backend_agents.py`, global agent add/edit routes, `global_selected_agent` setting)
-### **(v0.237.008)**
### **(v0.237.011)**
#### Bug Fixes
@@ -195,7 +517,7 @@
* **Removed Duplicate Comment**: Cleaned up duplicate "Render user-search results" comment.
* **Impact**: Member management buttons now render and function correctly, provide better error feedback, and auto-recover from stale member data.
* (Ref: `manage_group.js`, event handler deduplication, error handling improvements, toast notifications)
-
+
### **(v0.237.009)**
#### New Features
diff --git a/docs/explanation/running_simplechat_azure_production.md b/docs/explanation/running_simplechat_azure_production.md
new file mode 100644
index 00000000..7e6e3841
--- /dev/null
+++ b/docs/explanation/running_simplechat_azure_production.md
@@ -0,0 +1,105 @@
+# explanation/running_simplechat_azure_production.md
+---
+layout: libdoc/page
+title: Running Simple Chat in Azure Production
+order: 150
+category: Explanation
+---
+
+This guide explains the supported production startup patterns for Simple Chat in Azure.
+
+Current documentation version: 0.239.139
+
+## Default Azure Production Model in This Repo
+
+The repo-provided Azure deployment paths are container-based App Service deployments.
+
+That includes the deployers documented in this repository for:
+
+- `azd`
+- Bicep
+- Terraform
+- Azure CLI
+
+In those deployment models:
+
+- Azure App Service runs the published container image
+- the container entrypoint already starts Gunicorn
+- you do not need to set an App Service Stack Settings Startup command
+
+The web container entrypoint is:
+
+```text
+python3 -m gunicorn -c /app/gunicorn.conf.py app:app
+```
+
+## Native Python App Service Option
+
+If you intentionally deploy Simple Chat as a native Python App Service instead of using the repo container image, deploy the `application/single_app` folder and set the web startup command explicitly.
+
+Use this Startup command:
+
+```bash
+python -m gunicorn -c gunicorn.conf.py app:app
+```
+
+## Background Scheduler Guidance
+
+For production, keep scheduler-style work separate from multi-worker web processes when possible.
+
+Recommended web-process setting when scheduler work runs elsewhere:
+
+```bash
+SIMPLECHAT_RUN_BACKGROUND_TASKS=0
+```
+
+Recommended scheduler command:
+
+```bash
+python simplechat_scheduler.py
+```
+
+Operationally, that scheduler can run as:
+
+- a separate App Service or worker process
+- a scheduled container or job
+- another automation path that launches the same codebase with the scheduler command
+
+## Gunicorn Guidance for Azure
+
+Gunicorn is the production web server for Simple Chat in Azure-oriented deployments.
+
+The shared runtime config supports these tuning variables:
+
+- `GUNICORN_BIND`
+- `GUNICORN_WORKERS`
+- `GUNICORN_THREADS`
+- `GUNICORN_TIMEOUT`
+- `GUNICORN_GRACEFUL_TIMEOUT`
+- `GUNICORN_KEEPALIVE`
+- `GUNICORN_MAX_REQUESTS`
+- `GUNICORN_MAX_REQUESTS_JITTER`
+
+Use multiple workers only after you have decided how scheduler work is isolated.
+
+## Recommended Azure Production Pattern
+
+For most production environments in this repository:
+
+1. Deploy the container image through the repo-supported deployer.
+2. Let the container entrypoint launch Gunicorn.
+3. Do not configure an extra App Service Startup command.
+4. Move scheduler work into a separate runtime if you want clean multi-worker web behavior.
+
+## What Not to Do
+
+- Do not configure a second Gunicorn startup layer on top of the container deployer.
+- Do not treat Windows local development startup as proof of Gunicorn production behavior.
+- Do not leave scheduler decisions implicit if you plan to scale out workers or instances.
+
+## Summary
+
+- Repo deployers: container-based, Gunicorn already handled.
+- Native Python App Service: set the Gunicorn startup command explicitly.
+- Multi-worker production: separate scheduler work deliberately.
+- Local developer startup and Azure production startup should be treated as different runtime concerns.
\ No newline at end of file
diff --git a/docs/explanation/running_simplechat_locally.md b/docs/explanation/running_simplechat_locally.md
new file mode 100644
index 00000000..c1afdc51
--- /dev/null
+++ b/docs/explanation/running_simplechat_locally.md
@@ -0,0 +1,101 @@
+# explanation/running_simplechat_locally.md
+---
+layout: libdoc/page
+title: Running Simple Chat Locally
+order: 140
+category: Explanation
+---
+
+This guide explains the recommended local developer workflow for Simple Chat.
+
+Current documentation version: 0.239.136
+
+## Recommended Local Startup
+
+For normal development, start the app directly with Python:
+
+```bash
+python app.py
+```
+
+Set:
+
+```bash
+FLASK_DEBUG=1
+```
+
+This keeps Simple Chat on the Flask development server, enables local HTTPS behavior, and avoids unnecessary production-runtime complexity while you are editing and debugging the application.
+
+## Windows Developer Workflow
+
+Windows developers should use the direct Python startup path.
+
+Recommended local settings:
+
+```dotenv
+FLASK_DEBUG="1"
+SIMPLECHAT_USE_GUNICORN="1"
+SIMPLECHAT_RUN_BACKGROUND_TASKS="1"
+```
+
+Why this still works:
+
+- When `FLASK_DEBUG="1"`, `python app.py` stays on the Flask development server.
+- `SIMPLECHAT_USE_GUNICORN` is ignored while debug mode is enabled.
+- Background tasks continue to run in the single local process unless explicitly disabled.
+
+## Linux and macOS Developer Workflow
+
+Linux and macOS developers can use the same default local workflow:
+
+```bash
+FLASK_DEBUG=1 python app.py
+```
+
+That remains the recommended path for everyday development even on systems that can run Gunicorn.
+
+## When You Need Gunicorn-Specific Validation
+
+Use a Linux-compatible runtime only when you specifically need to validate:
+
+- multi-worker behavior
+- Gunicorn thread settings
+- keepalive and timeout behavior
+- production-like streaming behavior
+
+Example Gunicorn command:
+
+```bash
+gunicorn --bind=0.0.0.0:5000 --worker-class gthread --workers 2 --threads 8 --timeout 900 --graceful-timeout 60 --keep-alive 75 --max-requests 500 --max-requests-jitter 50 app:app
+```
+
+On Windows, use one of these options for that kind of validation:
+
+- Docker Desktop running the repo container image
+- WSL2 with a Linux shell
+- another Linux environment
+
+Native Windows Python should not be used to run Gunicorn directly.
+
+## Scheduler Behavior in Local Development
+
+By default, background loops remain enabled in local development.
+
+Use this variable only if you want to disable them in the current process:
+
+```bash
+SIMPLECHAT_RUN_BACKGROUND_TASKS=0
+```
+
+If you want to test the scheduler separately, run:
+
+```bash
+python simplechat_scheduler.py
+```
+
+## Practical Guidance
+
+- Use `python app.py` for normal development.
+- Keep `FLASK_DEBUG=1` on local developer machines.
+- Treat Gunicorn as a production-runtime validation tool, not the default local developer startup path.
+- On Windows, move to Docker or WSL2 when testing Gunicorn workers and threads matters.
\ No newline at end of file
diff --git a/docs/how-to/docker_customization.md b/docs/how-to/docker_customization.md
new file mode 100644
index 00000000..23812966
--- /dev/null
+++ b/docs/how-to/docker_customization.md
@@ -0,0 +1,8 @@
+# Docker Customization
+
+## Custom Certificate Authorities
+
+Add custom certification authorities to the `docker-customization/custom-ca-certificates/` directory in the repository root, and they will be pulled into the system CAs during docker build. Must be in `.crt` format.
+
+## Custom pip conf
+Add customization as needed to the `docker-customization/pip.conf` file in the repository root. This will be used during docker build.
\ No newline at end of file
diff --git a/docs/how-to/upgrade_paths.md b/docs/how-to/upgrade_paths.md
new file mode 100644
index 00000000..50d70c00
--- /dev/null
+++ b/docs/how-to/upgrade_paths.md
@@ -0,0 +1,144 @@
+# Upgrade Paths
+
+Use this guide when you already have SimpleChat deployed and want to update the application without rediscovering the initial deployment steps.
+
+## Choose the Right Upgrade Path
+
+| If you deployed SimpleChat as... | Use this path | Default upgrade command or method |
+| :--- | :--- | :--- |
+| **Native Python Azure App Service** | [Native Python App Service Upgrades](#native-python-app-service-upgrades) | VS Code deployment or Azure CLI ZIP deploy |
+| **Container-based Azure App Service** using the repo `azd`, Bicep, Terraform, or Azure CLI deployers | [Container-Based App Service Upgrades](#container-based-app-service-upgrades) | `azd deploy` for code-only updates |
+
+## Native Python App Service Upgrades
+
+This path applies when you deployed the application code directly to Azure App Service instead of using the repo's container image.
+
+### Required Startup Command Check
+
+For native Python App Service upgrades, do **not** leave the App Service Stack Settings Startup command blank.
+
+Deploy and run the `application/single_app` folder in App Service.
+
+Use this Startup command:
+
+```bash
+python -m gunicorn -c gunicorn.conf.py app:app
+```
+
+Validate this before or during the upgrade. A missing or incorrect Startup command is one of the fastest ways to turn a straightforward code update into an outage.
+
+### Recommended Native Upgrade Methods
+
+#### Option 1: Visual Studio Code Deployment
+
+Use this when you want the simplest manual update path.
+
+1. Sign in to Azure from VS Code.
+2. Open the Azure extension.
+3. Find the existing App Service.
+4. Right-click the App Service.
+5. Select **Deploy to Web App...**.
+6. Deploy the `application/single_app` folder.
+
+This is the same deployment mechanism used for an initial native Python deployment. It is also a valid upgrade method.
+
+#### Option 2: Azure CLI ZIP Deploy
+
+Use this when you want a repeatable manual package-and-deploy flow.
+
+1. Create a deployment ZIP from the required application contents.
+2. Build that ZIP from inside `application/single_app` so the deployed package contains the app files directly.
+3. Confirm `SCM_DO_BUILD_DURING_DEPLOYMENT=true` in App Service configuration.
+4. Deploy the ZIP with Azure CLI:
+
+```bash
+az webapp deploy \
+ --resource-group \
+ --name \
+ --src-path ../deployment.zip \
+ --type zip
+```
+
+This is an upgrade path, not only an initial deployment path. Package the new version, deploy the ZIP, and validate the Startup command before closing the change.
+
+#### Option 3: Deployment Slots for Production
+
+Use deployment slots when you want staged validation and rollback capability for native Python deployments.
+
+Recommended flow:
+
+1. Deploy the updated code to a staging slot.
+2. Validate the staging slot URL.
+3. Swap staging into production.
+4. Roll back with another swap if needed.
+
+### Native Python References
+
+- [Manual setup instructions](../setup_instructions_manual.md)
+- [Manual deployment notes](../reference/deploy/manual_deploy.md)
+
+## Container-Based App Service Upgrades
+
+This path applies to the repo-provided `azd`, Bicep, Terraform, and Azure CLI deployers. These deployers run SimpleChat as a **container** on Azure App Service.
+
+### Important Runtime Rule
+
+For container-based deployments, do **not** add a native Python App Service Startup command. Gunicorn is started by the container entrypoint in `application/single_app/Dockerfile`.
+
+### Upgrade Decision Guide
+
+| Situation | Recommended action | Why |
+| :--- | :--- | :--- |
+| **Application code change only** | `azd deploy` | Updates the app without treating the release like a full infrastructure event |
+| **Infrastructure change only** | `azd provision` | Applies Azure resource/configuration changes without redeploying the app container |
+| **Application code and infrastructure changed together** | `azd up` | Runs the combined app + infrastructure workflow |
+| **You are considering `azd down --purge` for a normal release** | Avoid this for routine upgrades | This is destructive and not a standard upgrade path |
+
+### Recommended Default for Container Releases
+
+For a normal code release, start with:
+
+```bash
+azd deploy
+```
+
+Do **not** assume `azd up` is required for every upgrade. Use `azd up` only when the release also needs infrastructure updates.
+
+When you are unsure whether infrastructure changes are included, review them first:
+
+```bash
+azd provision --preview
+```
+
+### Advanced Option: ACR/Image-Only Rollout
+
+If your App Service is already configured to pull its image from Azure Container Registry and your goal is to avoid any infrastructure reprovisioning, you can use an image-only rollout.
+
+The repo already contains an image publish workflow:
+
+- [.github/workflows/docker_image_publish.yml](../../.github/workflows/docker_image_publish.yml)
+
+That workflow publishes:
+
+1. A timestamped image tag for rollback-friendly releases.
+2. A `latest` tag for the current build.
+
+Use this path when your operations model is:
+
+1. Build and push the updated image to ACR.
+2. Refresh App Service to use the new image tag, or restart it if your container configuration intentionally tracks `latest`.
+3. Roll back by moving App Service back to the prior known-good tag.
+
+This is an **advanced operational option**, not the default repo deployment workflow. It exists specifically for teams that want to update the container image without treating every release like a provisioning event.
+
+### Container Upgrade References
+
+- [AZD deployment guide](../reference/deploy/azd-cli_deploy.md)
+- [Bicep deployment guide](../../deployers/bicep/README.md)
+- [Terraform deployment guide](../../deployers/terraform/ReadMe.md)
+
+## Summary
+
+- Native Python App Service upgrades: validate the Startup command, then use VS Code deploy, ZIP deploy, or deployment slots.
+- Container-based upgrades: prefer `azd deploy` for code-only changes and reserve `azd up` for releases that also change infrastructure.
+- If you already operate App Service against ACR and want lower-touch rollouts, use an image-only update process instead of full reprovisioning.
\ No newline at end of file
diff --git a/docs/reference/deploy/azd-cli_deploy.md b/docs/reference/deploy/azd-cli_deploy.md
index 1e2a2a19..fa95181c 100644
--- a/docs/reference/deploy/azd-cli_deploy.md
+++ b/docs/reference/deploy/azd-cli_deploy.md
@@ -30,6 +30,17 @@ Azure Developer CLI (azd) provides the fastest and most automated way to deploy
## Quick Start
+## Runtime Startup Behavior
+
+- The current `azd` deployment path in this repo is a **container-based App Service** deployment.
+- Gunicorn is started by the container entrypoint in `application/single_app/Dockerfile`.
+- You do **not** need to populate App Service Stack Settings Startup command when deploying through this `azd` path.
+- If you later switch to native Python App Service instead, deploy the `application/single_app` folder and use this startup command:
+
+```bash
+python -m gunicorn -c gunicorn.conf.py app:app
+```
+
### 1. Clone Repository
```bash
git clone https://github.com/microsoft/simplechat.git
@@ -265,6 +276,18 @@ azd up
## Management Commands
+### Upgrade Decision Guide
+
+Use the command that matches the type of change you are making.
+
+| If you changed... | Use | Why |
+| :--- | :--- | :--- |
+| **Application code only** | `azd deploy` | Recommended default for routine container upgrades |
+| **Infrastructure only** | `azd provision` | Updates Azure resources without treating the release like a full app deployment |
+| **Application code and infrastructure together** | `azd up` | Runs the combined deployment flow |
+
+Do **not** assume `azd up` is required for every release. For normal code-only container updates, start with `azd deploy`.
+
### Application Lifecycle
**Deploy application updates:**
@@ -272,17 +295,23 @@ azd up
azd deploy
```
+Recommended for routine container-based application upgrades when infrastructure is unchanged.
+
**Provision infrastructure changes:**
```bash
azd provision
```
+Use `azd provision --preview` first when you want to review infrastructure impact before applying it.
+
**Full redeployment:**
```bash
azd down --purge
azd up
```
+Do not use this as a standard upgrade flow. This is a destructive reprovisioning path.
+
### Environment Management
**List environments:**
diff --git a/docs/reference/deploy/manual_deploy.md b/docs/reference/deploy/manual_deploy.md
index e69de29b..d8173d57 100644
--- a/docs/reference/deploy/manual_deploy.md
+++ b/docs/reference/deploy/manual_deploy.md
@@ -0,0 +1,57 @@
+# Manual Deployment Notes
+
+Use this path when deploying SimpleChat to **native Python Azure App Service** instead of the repo's container-based deployers.
+
+For the combined native-vs-container decision guide, see [../../how-to/upgrade_paths.md](../../how-to/upgrade_paths.md).
+
+## Native Python App Service Startup Command
+
+Set the App Service Stack Settings Startup command explicitly.
+
+Do **not** leave the Startup command empty during an upgrade. Validate it before or during the release.
+
+Deploy and run the `application/single_app` folder in App Service.
+
+Use this Startup command:
+
+```bash
+python -m gunicorn -c gunicorn.conf.py app:app
+```
+
+## Native Python Upgrade Checklist
+
+Use this checklist when updating an existing native Python App Service deployment.
+
+1. Confirm the deployment model is **native Python App Service**, not container-based App Service.
+2. Confirm the `application/single_app` folder is the deployment unit and the Startup command is present and correct.
+3. Choose an upgrade method:
+ - **VS Code deployment** when you want the simplest manual update path.
+ - **Azure CLI ZIP deploy** when you want a repeatable package-and-deploy path.
+ - **Deployment slots** when you want validation and rollback for production.
+4. If you use ZIP deploy, confirm `SCM_DO_BUILD_DURING_DEPLOYMENT=true` so App Service installs dependencies from `requirements.txt`.
+5. Validate the site after deployment.
+
+## Native Python Upgrade Methods
+
+### Visual Studio Code Deployment
+
+Deploy the updated code from VS Code by right-clicking the existing App Service and selecting **Deploy to Web App...**.
+
+### Azure CLI ZIP Deploy
+
+Package the updated application into a deployment ZIP, then deploy it:
+
+```bash
+az webapp deploy \
+ --resource-group \
+ --name \
+ --src-path ../deployment.zip \
+ --type zip
+```
+
+This is an upgrade method, not only an initial deployment method.
+
+## Important Distinction
+
+- Native Python App Service needs the Startup command above.
+- The repo-provided `azd`, Bicep, Terraform, and Azure CLI deployers do not need this because they deploy a container image whose entrypoint already launches Gunicorn.
diff --git a/docs/reference/deploy/terraform_deploy.md b/docs/reference/deploy/terraform_deploy.md
index e69de29b..8b449694 100644
--- a/docs/reference/deploy/terraform_deploy.md
+++ b/docs/reference/deploy/terraform_deploy.md
@@ -0,0 +1,17 @@
+# Terraform Deployment Notes
+
+The current Terraform deployer in this repo provisions a **container-based Azure Linux Web App**.
+
+## Current Behavior
+
+- Terraform sets the App Service to run the published container image.
+- Gunicorn startup is already handled by the container entrypoint in `application/single_app/Dockerfile`.
+- You do **not** need to configure App Service Stack Settings Startup command for the current Terraform deployment.
+
+## If You Switch Terraform to Native Python Later
+
+If you change the Terraform deployment model away from containers and into native Python App Service, deploy the `application/single_app` folder and use this Startup command:
+
+```bash
+python -m gunicorn -c gunicorn.conf.py app:app
+```
diff --git a/docs/setup_instructions.md b/docs/setup_instructions.md
index 199ffa5f..560a7442 100644
--- a/docs/setup_instructions.md
+++ b/docs/setup_instructions.md
@@ -20,6 +20,7 @@ The options are:
- [Azure CLI with Powershell](#azure-cli-with-powershell)
- [BICEP](#bicep)
- [Terraform](#hashicorp-terraform)
+- [Upgrade Existing Deployments](#upgrade-existing-deployments)
**Note:** Terraform is the most robust and requires the least manual post-deployment actions at this time.
@@ -35,6 +36,12 @@ This is the step by step process required to deploy the infrastructure and confi
[Link to manual deployment steps](./setup_instructions_manual.md)
+## Upgrade Existing Deployments
+
+If you already have Simple Chat deployed and only need to update the application, use the dedicated upgrade guide instead of rerunning the full setup flow.
+
+[Link to upgrade paths](./how-to/upgrade_paths.md)
+
## Azure CLI with Powershell
All Azure resource provisioning happens with Azure CLI. Powershell is used for the control flow of the script only.
diff --git a/docs/setup_instructions_manual.md b/docs/setup_instructions_manual.md
index c7e13ca3..92e9421e 100644
--- a/docs/setup_instructions_manual.md
+++ b/docs/setup_instructions_manual.md
@@ -471,7 +471,7 @@ Deploy the application code from your local repository to the Azure App Service.
- Expand **App Service**, find your subscription and the App Service instance you created.
- **Right-click** on the App Service name.
- Select **Deploy to Web App...**.
- - Browse and select the folder containing the application code (the root folder you cloned, e.g., SimpleChat).
+ - Browse and select the `application/single_app` folder from the repository.
- VS Code will prompt to confirm the deployment, potentially warning about overwriting existing content. Click **Deploy**.
- Make sure your requirements.txt file is up-to-date before deploying. The deployment process (SCM_DO_BUILD_DURING_DEPLOYMENT=true) will use this file to install dependencies on the App Service.
- Monitor the deployment progress in the VS Code Output window.
@@ -484,7 +484,7 @@ This method involves creating a zip file of the application code and uploading i
1. **Create the ZIP file**:
- - Navigate into the application's root directory (e.g., SimpleChat) in your terminal.
+ - Navigate into `application/single_app` in your terminal.
- Create a zip file containing **only** the necessary application files and folders. **Crucially, zip the contents, not the parent folder itself.**
- **Include**:
- static/ folder
@@ -529,6 +529,20 @@ This method involves creating a zip file of the application code and uploading i
> Return to top
+This section covers **native Python Azure App Service** upgrades for the manual deployment path.
+
+Before upgrading a native Python deployment, confirm that the App Service Stack Settings Startup command is set correctly and is not blank.
+
+Deploy and run the `application/single_app` folder in App Service.
+
+Use this Startup command:
+
+```bash
+python -m gunicorn -c gunicorn.conf.py app:app
+```
+
+For a shorter decision guide that also covers container-based upgrades, see [Upgrade Paths](./how-to/upgrade_paths.md).
+
Keeping your Simple Chat application up-to-date involves deploying the newer version of the code. Using **Deployment Slots** is the recommended approach for production environments to ensure zero downtime and provide easy rollback capabilities.

diff --git a/functional_tests/test_access_denied_message_feature.py b/functional_tests/test_access_denied_message_feature.py
new file mode 100644
index 00000000..4a0a2194
--- /dev/null
+++ b/functional_tests/test_access_denied_message_feature.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+# test_access_denied_message_feature.py
+"""
+Functional regression test for admin-configurable access denied message.
+
+Version: 0.239.002
+Implemented in: 0.239.002
+
+This test ensures that:
+1. The Admin Settings template exposes a textarea with name="access_denied_message".
+2. route_frontend_admin_settings.py reads the field from form_data and falls back
+ to the existing stored value (not '') when the field is absent -- preventing
+ silent data loss from cached/older form submissions.
+3. index.html renders app_settings.access_denied_message through the nl2br filter
+ without a redundant hardcoded fallback string.
+4. functions_settings.py defines a non-empty default for access_denied_message so
+ the field is always present after get_settings() deep-merges defaults.
+"""
+
+import sys
+import os
+import re
+
+# Resolve paths relative to repo root
+REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+ADMIN_TEMPLATE = os.path.join(REPO_ROOT, "application", "single_app", "templates", "admin_settings.html")
+INDEX_TEMPLATE = os.path.join(REPO_ROOT, "application", "single_app", "templates", "index.html")
+ROUTE_FILE = os.path.join(REPO_ROOT, "application", "single_app", "route_frontend_admin_settings.py")
+SETTINGS_FILE = os.path.join(REPO_ROOT, "application", "single_app", "functions_settings.py")
+
+
+# ---------------------------------------------------------------------------
+# Test 1 – Admin Settings template has the access_denied_message field
+# ---------------------------------------------------------------------------
+
+def test_admin_template_has_field():
+ """Admin Settings template must expose a textarea named access_denied_message."""
+ print("Testing admin_settings.html contains access_denied_message field...")
+ errors = []
+
+ with open(ADMIN_TEMPLATE, encoding="utf-8") as f:
+ content = f.read()
+
+ # textarea with correct name attribute
+ if 'name="access_denied_message"' not in content:
+ errors.append("No