From 833627b950f3944862b7f9de0b72676e4d06e054 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Sat, 7 Nov 2026 17:25:38 -0500 Subject: [PATCH] Fix canonical plugin discovery for document orchestration Reuse canonical package imports so native document guards receive the same reader class as discovery. Preserve source isolation and recovery policy while adding safe action-stage failures and real-loader format/startup regressions. Pin the required template parser, add the CI gate, and update documentation and release pages for 0.261.421. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../cosmos-response-contract-check.yml | 7 + application/single_app/config.py | 2 +- .../functions_orchestration_actions.py | 37 +- .../functions_orchestration_adapters.py | 23 +- .../functions_orchestration_schema.py | 18 + application/single_app/requirements.txt | 2 + .../semantic_kernel_plugins/plugin_loader.py | 36 +- .../features/CHAT_ORCHESTRATION_ACTIONS.md | 14 +- ...ESTRATION_PLUGIN_DISCOVERY_IDENTITY_FIX.md | 213 ++++++ docs/explanation/release-notes/index.md | 607 +++++++++--------- .../release-notes/v0.261-part-1.md | 86 +-- .../release-notes/v0.261-part-2.md | 105 ++- .../release-notes/v0.261-part-3.md | 85 ++- .../release-notes/v0.261-part-4.md | 97 +-- .../release-notes/v0.261-part-5.md | 157 ++--- .../release-notes/v0.261-part-6.md | 167 ++--- .../release-notes/v0.261-part-7.md | 78 ++- docs/explanation/release_notes.md | 18 +- docs/reference/actions/document-search.md | 16 + docs/reference/logging-tags.md | 47 ++ ...rchestration_document_action_end_to_end.py | 13 +- ...chestration_document_loader_reliability.py | 290 +++++++++ .../test_plugin_discovery_identity.py | 243 +++++++ 23 files changed, 1668 insertions(+), 693 deletions(-) create mode 100644 docs/explanation/fixes/ORCHESTRATION_PLUGIN_DISCOVERY_IDENTITY_FIX.md create mode 100644 functional_tests/test_orchestration_document_loader_reliability.py create mode 100644 functional_tests/test_plugin_discovery_identity.py diff --git a/.github/workflows/cosmos-response-contract-check.yml b/.github/workflows/cosmos-response-contract-check.yml index 33d0bd068..f1e7caa85 100644 --- a/.github/workflows/cosmos-response-contract-check.yml +++ b/.github/workflows/cosmos-response-contract-check.yml @@ -40,5 +40,12 @@ jobs: run: python -c "import tiktoken; tiktoken.get_encoding('cl100k_base'); tiktoken.get_encoding('o200k_base')" - name: Require reviewed storage owners and doubles run: python scripts/check_cosmos_response_boundaries.py + - name: Verify real plugin discovery and document-action runtime + run: >- + python -m pytest -q -p no:cacheprovider -p test_support.cosmos_network_guard + --disable-warnings --tb=short + functional_tests/test_plugin_discovery_identity.py + functional_tests/test_orchestration_document_action_end_to_end.py + functional_tests/test_orchestration_document_loader_reliability.py - name: Run contracts with external network blocked before collection run: python scripts/check_cosmos_response_boundaries.py --run-contracts diff --git a/application/single_app/config.py b/application/single_app/config.py index 925b952b1..c82e58803 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -103,7 +103,7 @@ EXECUTOR_MAX_WORKERS = 30 CHAT_STREAM_EXECUTOR_MAX_WORKERS = max(8, int(os.getenv('SIMPLECHAT_CHAT_STREAM_WORKERS', '32') or '32')) SESSION_TYPE = 'filesystem' -VERSION = "0.261.420" +VERSION = "0.261.421" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/application/single_app/functions_orchestration_actions.py b/application/single_app/functions_orchestration_actions.py index 374d52463..ec2184865 100644 --- a/application/single_app/functions_orchestration_actions.py +++ b/application/single_app/functions_orchestration_actions.py @@ -5,10 +5,11 @@ kernel holds only the built-in chart tools, never the action's own functions, so saved visual preferences can be applied there without reaching calls to the integration. -Version: 0.261.321 +Version: 0.261.421 Microsoft 365 actions authorized for their own step in: 0.261.238 SharePoint and OneDrive steps bind their model's token budget and analysis model in: 0.261.300 Explicit operation intent and named input consumption added in: 0.261.321 +Canonical plugin identity and safe action-stage failures added in: 0.261.421 """ import asyncio @@ -87,6 +88,10 @@ class ActionExecutionError(RuntimeError): """A user-safe failure of the selected action.""" + def __init__(self, message="", *, failure_code=None): + super().__init__(message) + self.orchestration_failure_code = failure_code + def _check_access(settings, catalog, action_ref): # The invocation's own source preflight owns the runtime services; this checks the @@ -791,7 +796,10 @@ async def guard_function(invocation, next): payload=invocation.result.value if invocation.result is not None else None, ) if document_error: - failure = ActionExecutionError('The selected document could not be read by this action.') + failure = ActionExecutionError( + 'The selected document could not be read by this action.', + failure_code='document_action_read_failed', + ) raise failure if invocation.result is not None: outputs.append(invocation.result.value) @@ -829,16 +837,25 @@ async def stop_after_failure(invocation, next): } capture_manifest(manifest, current_settings) if not loader.load_plugin_from_manifest(prepared, user_id): - raise ActionExecutionError('The selected action could not be loaded.') + raise ActionExecutionError( + 'The selected action could not be loaded.', + failure_code='action_initialization_failed', + ) if bound_ids: # The actual native reader, not a configured name or description, owns these reads. from semantic_kernel_plugins.document_search_plugin import DocumentSearchPlugin if not any(isinstance(plugin, DocumentSearchPlugin) for plugin in loader.plugin_instances): - raise ActionExecutionError('The action does not provide the native document reader.') + raise ActionExecutionError( + 'The action does not provide the native document reader.', + failure_code='document_reader_identity_mismatch', + ) functions = kernel.get_list_of_function_metadata({}) if not functions: - raise ActionExecutionError('The selected action has no enabled functions.') + raise ActionExecutionError( + 'The selected action has no enabled functions.', + failure_code='action_initialization_failed', + ) if cancel_requested(): raise AgentExecutionCancelled('Action execution was cancelled.') kernel.add_filter(FilterTypes.FUNCTION_INVOCATION, guard_function) @@ -853,7 +870,10 @@ async def stop_after_failure(invocation, next): service = _build_action_model(current_settings, context, user_id) kernel.add_service(service) if not getattr(service, 'SUPPORTS_FUNCTION_CALLING', False): - raise ActionExecutionError('The selected model does not support action functions.') + raise ActionExecutionError( + 'The selected model does not support action functions.', + failure_code='action_initialization_failed', + ) if file_source: # Chat binds these through the selected agent; this step's own model stands in. kernel.add_filter(FilterTypes.AUTO_FUNCTION_INVOCATION, file_step_filter( @@ -938,7 +958,10 @@ async def stop_after_failure(invocation, next): if invocation_capture is not None: invocation_capture.require_valid(captured=True) if not calls or not outputs: - raise ActionExecutionError('The action did not return any function results.') + raise ActionExecutionError( + 'The action did not return any function results.', + failure_code='action_results_missing', + ) findings = '\n\n'.join(str(reply) for reply in replies or [] if str(reply).strip()) if not findings: findings = '\n\n'.join( diff --git a/application/single_app/functions_orchestration_adapters.py b/application/single_app/functions_orchestration_adapters.py index db5ecbb54..4169fae70 100644 --- a/application/single_app/functions_orchestration_adapters.py +++ b/application/single_app/functions_orchestration_adapters.py @@ -2357,13 +2357,30 @@ def recheck_document_sources(): raise except Exception as exc: # This is the boundary for arbitrary plugin/provider code; never expose its exceptions. + failed_result = _failed_result('The action could not complete.', exc) + failure_code = failed_result['failure']['code'] + failure_stage = { + 'action_initialization_failed': 'action_initialization', + 'document_reader_identity_mismatch': 'action_initialization', + 'document_action_read_failed': 'document_action_read', + 'action_results_missing': 'action_results', + }.get(failure_code, 'action_invocation') log_event( f'{_LOG_PREFIX} Direct action execution failed.', level=logging.ERROR, - extra={'action_ref': action_ref, 'step_id': (step or {}).get('step_id'), - 'error_type': type(exc).__name__}, + extra={ + **workflow_log_context( + conversation_id=_ctx(context, 'conversation_id'), turn_id=_ctx(context, 'turn_id'), + run_id=_ctx(context, 'run_id'), step_id=step['step_id'], + ), + 'capability_id': CAPABILITY_ACTION_INVOKE, + 'stage': failure_stage, 'failure_code': failure_code, + 'error_type': type(exc).__name__, + **({'function_calls_attempted': failure_stage == 'document_action_read'} + if failure_stage in ('action_initialization', 'document_action_read') else {}), + }, ) - return _failed_result('The action could not complete.', exc) + return failed_result citations = _agent_citations( None, user_id, _ctx(context, 'conversation_id'), set(), root_id=result['root_id'], scoped_invocations=result['invocations'], diff --git a/application/single_app/functions_orchestration_schema.py b/application/single_app/functions_orchestration_schema.py index f5759540f..017f12d3e 100644 --- a/application/single_app/functions_orchestration_schema.py +++ b/application/single_app/functions_orchestration_schema.py @@ -2037,6 +2037,22 @@ def summarize_plan(plan): "This step's agent or action isn't available to you right now, so it didn't run. Check " 'that it still exists, is enabled and is shared with you, then retry.' ), + 'action_initialization_failed': ( + "The action's tools or model could not be initialized. Ask an admin to check the " + 'action and model configuration, then retry.' + ), + 'document_reader_identity_mismatch': ( + "The document-reading tool did not match the application's expected tool. No " + 'document-reading function ran. Ask an admin to repair or update the application, then retry.' + ), + 'document_action_read_failed': ( + 'The action could not complete the selected document read. Check the file availability ' + 'and requested inspection, then retry.' + ), + 'action_results_missing': ( + 'The action returned no usable function results. No findings from this step were used. ' + 'Ask an admin to check the action and model tool support if it keeps failing.' + ), 'operation_recovery_required': ( 'An operation may already have run, or its material changed. Check its saved outgoing ' 'action and the external service before creating a new plan. It was not repeated.' @@ -2213,6 +2229,8 @@ def summarize_plan(plan): # Failures an exception may carry by code, through its ``orchestration_failure_code``. EXCEPTION_FAILURE_CODES = M365_STEP_FAILURE_CODES | { 'external_session_required', 'operation_recovery_required', 'integration_input_unavailable', + 'action_initialization_failed', 'document_reader_identity_mismatch', + 'document_action_read_failed', 'action_results_missing', STORAGE_FAILURE_CODE, } # Steps that hand work to an integration; input-reading failures have their own code. diff --git a/application/single_app/requirements.txt b/application/single_app/requirements.txt index e74aba6fe..5587903a7 100644 --- a/application/single_app/requirements.txt +++ b/application/single_app/requirements.txt @@ -53,6 +53,8 @@ pillow==13.0.0 ffmpeg-binaries-compat==1.0.1 ffmpeg-python==0.2.0 semantic-kernel==1.39.4 +# Required by Semantic Kernel's Handlebars template parser. +PyMeta3==0.5.1 snowflake-connector-python[pandas]==4.7.3 tableauserverclient==0.40 yamcs-client==2.1.0 diff --git a/application/single_app/semantic_kernel_plugins/plugin_loader.py b/application/single_app/semantic_kernel_plugins/plugin_loader.py index 9e897c4ed..b4759f4bd 100644 --- a/application/single_app/semantic_kernel_plugins/plugin_loader.py +++ b/application/single_app/semantic_kernel_plugins/plugin_loader.py @@ -1,13 +1,19 @@ -import os -import importlib.util +# plugin_loader.py +"""Discover installed plugins without duplicating their runtime class identities.""" + +import importlib import inspect import logging -from functions_appinsights import log_event +import os from typing import Dict, Type, List + +from functions_appinsights import log_event from semantic_kernel_plugins.base_plugin import BasePlugin + PLUGIN_DIR = os.path.dirname(__file__) + def discover_plugins() -> Dict[str, Type[BasePlugin]]: """ Dynamically discover all BasePlugin subclasses in the semantic_kernel_plugins directory. @@ -18,24 +24,26 @@ def discover_plugins() -> Dict[str, Type[BasePlugin]]: for filename in os.listdir(PLUGIN_DIR): if filename.endswith('.py') and not filename.startswith('__') and filename != 'base_plugin.py' and filename != 'plugin_loader.py': module_name = filename[:-3] - module_path = os.path.join(PLUGIN_DIR, filename) - + try: - spec = importlib.util.spec_from_file_location(f"semantic_kernel_plugins.{module_name}", module_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - + # Runtime guards and other loaders must see the same imported classes. + module = importlib.import_module(f"semantic_kernel_plugins.{module_name}") + for name, obj in inspect.getmembers(module, inspect.isclass): if issubclass(obj, BasePlugin) and obj is not BasePlugin: plugins[name] = obj - - except Exception as e: - # Log the error but continue with other plugins - log_event(f"Failed to load plugin module {module_name}: {str(e)}") + + except Exception as exc: + log_event( + "[PLUGIN_LOADER] Plugin module could not be loaded.", + level=logging.WARNING, + extra={"plugin_name": module_name, "error_type": type(exc).__name__}, + ) continue - + return plugins + def get_all_plugin_metadata() -> List[dict]: """ Instantiate each discovered plugin and return a list of their metadata dicts. diff --git a/docs/explanation/features/CHAT_ORCHESTRATION_ACTIONS.md b/docs/explanation/features/CHAT_ORCHESTRATION_ACTIONS.md index ba98fa396..f168d7358 100644 --- a/docs/explanation/features/CHAT_ORCHESTRATION_ACTIONS.md +++ b/docs/explanation/features/CHAT_ORCHESTRATION_ACTIONS.md @@ -1,6 +1,6 @@ # Chat Orchestration Action Access -Version: **0.261.420** +Version: **0.261.421** Implemented in version: **0.261.098** @@ -10,6 +10,8 @@ Operation intent and named inputs implemented in version: **0.261.321** Selected-document action binding implemented in version: **0.261.419** +Canonical plugin discovery and action-stage diagnostics implemented in version: **0.261.421** + Application version tracking remains in `application/single_app/config.py`. ## Purpose @@ -102,6 +104,16 @@ and binds their arguments. Avoiding agent setup does **not** mean avoiding model Conversational follow-ups retain their resolved request and bounded, authorized conversation reference when handed to an action, including accepted clarification answers. +Shared discovery imports each installed plugin through its registered package module. +The native-reader guard therefore sees the same class as the logged and classic loaders, +without sharing plugin instances or their user-specific configuration. +Loading/identity failures, failed document reads, and missing function results retain +distinct safe failure codes through saved step details and the final failure explanation. +They are not successful findings or automatic transient-retry conditions. + +See the [plugin discovery identity fix](../fixes/ORCHESTRATION_PLUGIN_DISCOVERY_IDENTITY_FIX.md) +for the real-loader regression and recovery boundaries. + ### Selected documents and native inspection Since **0.261.419**, a registered Document Search action can bind exact workspace diff --git a/docs/explanation/fixes/ORCHESTRATION_PLUGIN_DISCOVERY_IDENTITY_FIX.md b/docs/explanation/fixes/ORCHESTRATION_PLUGIN_DISCOVERY_IDENTITY_FIX.md new file mode 100644 index 000000000..8b573b5ee --- /dev/null +++ b/docs/explanation/fixes/ORCHESTRATION_PLUGIN_DISCOVERY_IDENTITY_FIX.md @@ -0,0 +1,213 @@ +# Orchestration plugin discovery identity fix (v0.261.421) + +Fixed/Implemented in version: **0.261.421**, tracked in +`application/single_app/config.py`. + +## Issue and root cause + +Selected-document orchestration could validate its plan and permissions, load the +Document Search action, and then fail before its first document-reading function. +Later report-writing steps were skipped because their required evidence had never +been collected. Similar failures across different files were not evidence that +each file was corrupt. + +In plain language, the application made two copies of the same tool. A safety +check expected one copy, but the loader supplied the other. Python treated them +as different classes even though they shared a name and source file. + +`semantic_kernel_plugins/plugin_loader.py` executed a fresh module object for each +discovery call without reusing Python's registered module. The source-binding +guard added in 0.261.419 imported the canonical `DocumentSearchPlugin` and used +`isinstance` to verify the actual reader. That correct guard exposed the older +duplicate-class loading behavior. + +The previous authenticated end-to-end fixture replaced discovery with a direct +reference to the canonical class. Its successful replay therefore did not test +the loader/guard combination used in production. + +Read-only Application Insights, Log Analytics, and App Service console correlation +on version 0.261.420 showed the same boundary in the six reported runs: verified +source identity/access, successful plugin registration, immediate action failure, +and no native read checkpoint. An isolated replay using real discovery reproduced +the exact reader rejection with zero action-model calls and zero report reads. +Hosting metrics did not indicate CPU or memory exhaustion as their cause. + +## Changes + +`plugin_loader.py` now uses `importlib.import_module` for installed plugins. +Repeated and concurrent discovery reuse canonical class objects while logged +loading continues to construct separate instances with their own configurations. +No additional registry, reload mechanism, source widening, or class-name-only +validation was introduced. Failed module imports emit a tagged warning with the +module name and exception type, not the exception message. + +`functions_orchestration_actions.py` attaches an application-owned failure code +only at known initialization, reader identity, document-result error, and missing +function-result boundaries. Unclassified action errors still preserve their +existing provider causes. The document-reader type guard, source rechecks, +function-call budget, and cancellation behavior remain in place. + +`functions_orchestration_schema.py` admits those codes through the existing safe +failure contract. `functions_orchestration_adapters.py` logs the safe code/stage +and hashed run/step/conversation identity. Saved step details and the final +failure explanation retain the original action failure instead of explaining +only the dependent step's skip. + +| Code | Meaning and response | +| --- | --- | +| `action_initialization_failed` | Check action/model configuration; the tools or function-calling capabilities could not initialize. | +| `document_reader_identity_mismatch` | Repair or update the deployed application before retrying. Re-uploading or changing timeouts does not repair duplicate class identity. | +| `document_action_read_failed` | Check the selected source's availability and requested inspection; a failed read was not accepted as findings. | +| `action_results_missing` | No usable function results were returned; review the action/model tool support if it repeats. | + +These codes are not automatic transient retries. Permitted manual failed-step +recovery remains available after the underlying problem is repaired. No unknown +exception code or private exception message is accepted as public failure text. + +### Fresh-container template dependency + +The container startup probe exposed a separate installation problem: the +configured feed installed Semantic Kernel's Handlebars adapter without its +required `pymeta` module. This was not the cause of the six historical failures, +which had already reached planning and action loading. + +`application/single_app/requirements.txt` now explicitly pins the existing +template-parser dependency, `PyMeta3==0.5.1`, matching the working test environment. +Fresh normal/optimized import tests and the Linux image probe exercise the real +Semantic Kernel import rather than treating a completed package install as proof +that the application starts. + +## Validation + +`functional_tests/test_orchestration_document_action_end_to_end.py` now runs +production discovery. Its authenticated HTTP-to-saved-answer cases verify native +PDF/Word facts, limits, citations, failed reads, and debug on/off. + +`functional_tests/test_plugin_discovery_identity.py` verifies every discovered +class against its registered module, repeated/concurrent loading, separate +request-local users and configuration, safe import-error logging, and real +bootstrap/web/headless imports in fresh normal and optimized processes. + +`functional_tests/test_orchestration_document_loader_reliability.py` derives its +format matrix from `config.py` rather than listing only the reported extensions. +It exercises actual source checks, SDK-shaped document records, ordered chunk +retrieval, real action loading, retention, and saved cited answers using indexed +text. This matrix does not claim that every upload format has native PDF/Office +properties or that a filename change proves extraction support. + +Fault-injected initialization, wrong reader identity, document-read, and missing +result cases verify the exact safe code in the saved step API, its user-facing +explanation, hashed telemetry, attempt flags, privacy, and absence of composition +after failed acquisition. Provider timeout/connection causes remain transient; +the new code faults do not. + +Existing real-package PDF, Word, PowerPoint, and Excel accessibility/native +inspection fixtures separately cover extraction and its stated limitations. +The CI storage-contract workflow explicitly executes the real discovery and +document-action regressions with network access blocked before collection. + +| Before | After | +| --- | --- | +| Discovery made another class object that the native-reader guard rejected. | Discovery and the guard use the same registered class. | +| A shortcut fixture hid production loading behavior. | The authenticated regression uses the production loader. | +| Distinct action failures collapsed into a generic operation message. | Safe initialization, identity, read, and missing-result meanings survive saved details and the failure explanation. | +| Debug-off errors lacked usable run/step correlation at the action boundary. | The first action error retains safe stage/code and hashed correlation with debug on or off. | + +### Executed checks + +The shared discovery/native/action-runtime selection passed **81 tests** with the +eight fresh-process import-order cases excluded from that combined selection. +Those eight normal/optimized startup cases passed in the preceding discovery +selection. Real PDF/Word/PowerPoint/Excel extraction and Excel native-inspection +fixtures passed **63 tests** separately. + +The complete config-derived stored-text format matrix passed independently, +requiring a saved cited answer for every allowed extension. The **15** remaining +failure/contract cases passed independently, including the eight debug-on/off +saved-step failures. These selections are not presented as an aggregate test count. + +```powershell +$env:PYTHONPATH = "$PWD\application\single_app;$PWD\functional_tests" +python -m pytest -q -p no:cacheprovider -p test_support.cosmos_network_guard --disable-warnings --tb=short --show-capture=no .\functional_tests\test_plugin_discovery_identity.py .\functional_tests\test_orchestration_document_action_end_to_end.py .\functional_tests\test_orchestration_action_runtime.py .\functional_tests\test_orchestration_document_action_binding.py -k 'not real_cold_import_orders' +``` + +```powershell +python -m pytest -q -p no:cacheprovider -p test_support.cosmos_network_guard --disable-warnings --tb=short --show-capture=no .\functional_tests\test_document_accessibility_extraction.py .\functional_tests\test_excel_document_accessibility_extraction.py .\functional_tests\test_excel_native_inspection_chat_evidence.py +``` + +The reviewed SDK boundary inventory remains at **1,094 owners, 425 doubles and +zero review gaps**; regeneration introduced no inventory changes. + +After the final version update, the exact new CI regression selection passed +**37 tests**, including the complete **81-extension** retrieval matrix and all +fresh-process cases. The route-policy/manual-recovery selection passed **77 +tests**. The full reviewed offline SDK gate passed **2,026 tests and 431 subtests**: + +```powershell +python .\scripts\check_cosmos_response_boundaries.py --run-contracts +``` + +Documentation surface coverage and site quality passed **7/7** and **6/6** +checks. Changed Python compilation, CI YAML/network-guard validation, the changed +browser-sink scan and whitespace checks passed. + +The fresh local production Docker build passed the V2/native build stages but +stopped during dependency installation with an HTTPS handshake failure to +`files.pythonhosted.org`. Normal certificate-verified host and container probes +reproduced that network failure. TLS verification was not disabled, and this is +not attributed to the document-reader defect. + +Using the machine's existing verified-HTTPS package feed in a temporary build +context resolved that transport problem. The feed's missing FFmpeg metadata +sidecar was avoided by selecting the exact verified `ffmpeg-binaries-compat` +1.0.1 wheel with its SHA-256 hash in that temporary context; the repository's +FFmpeg pin and pip configuration were not changed. + +After the explicit PyMeta3 declaration, the full CI-equivalent regression gate +passed **38 tests**. The production Linux image built successfully and its real +application/WSGI authentication, canonical reader loading, compiled V2 assets, +five implementation-file hashes, Gunicorn configuration, and installed-package +consistency checks passed with external networking disabled. The default +external healthcheck switch remains disabled in the offline fixture; its 400 +response was not reported as a healthy endpoint or silently enabled in Azure. + +Release pages were regenerated and preserve all **313** source releases. + +### Packaged-runtime replay + +The actual Linux production image also passed **101 existing regression cases**: +the **38** discovery/native-action/runtime cases, including the full +**81-extension** indexed-text matrix, plus **63** real PDF/Word/PowerPoint/Excel +extraction and evidence cases. There were zero errors, failures, or skipped +cases. + +All **221** original production package versions remained unchanged. Only the +four missing, pinned pytest tooling packages were installed into the disposable +container's owned scratch user site. The shipped image, production dependencies, +and application source were not replaced to make the tests pass. Docker +networking was disabled, fixtures were mounted read-only, and the existing +pre-collection network guard remained active. + +All five raw implementation-file hashes also matched the worktree. The earlier +smoke fingerprints explicitly correspond to UTF-8 text with CRLF converted to +LF; a raw-byte comparison against those normalized fingerprints is not a valid +stale-build test. + +## Scope and rollout + +No new upload type, extractor, model provider, administrator toggle, route, or +browser dependency is added. Existing workspace permissions, selected-source +bindings, native-inspection limitations, and historical run records are preserved. +A missing native property in an available report is still different from a report +that cannot be read. + +Offline regressions are not a guarantee that every live model response will be +valid. Deploy the verified version/build, then prove actual saved cited answers +with approved live canaries. HTTP 200 and a healthy web endpoint do not establish +successful orchestration. A separately observed partial document-search run was +not attributed to this loader defect and must be diagnosed independently if it +persists. + +Related references: [Document Search](../../reference/actions/document-search.md), +[action access](../features/CHAT_ORCHESTRATION_ACTIONS.md), and +[plugin/action failure logging](../../reference/logging-tags.md#plugin-discovery-and-action-failures). diff --git a/docs/explanation/release-notes/index.md b/docs/explanation/release-notes/index.md index bd084f34b..e587d9865 100644 --- a/docs/explanation/release-notes/index.md +++ b/docs/explanation/release-notes/index.md @@ -20,6 +20,9 @@ This page includes the latest release notes inline. Older release sections are s | Version | Page | | --- | --- | +| v0.261.421 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.410 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | +| v0.261.408 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | | v0.261.392 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | | v0.261.390 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | | v0.261.385 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | @@ -29,262 +32,262 @@ This page includes the latest release notes inline. Older release sections are s | v0.261.333 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | | v0.261.315 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | | v0.261.309 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.308 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.306 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.305 | [Release notes index]({{ '/explanation/release_notes/' | relative_url }}) | -| v0.261.304 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.303 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.302 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.301 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.300 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.299 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.298 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.297 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.296 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.295 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.293 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.292 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.291 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.290 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.289 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.288 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.287 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.286 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.285 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.284 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.283 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.282 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.280 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.279 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.278 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.277 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.276 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.275 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.274 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.271 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.270 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.269 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.268 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.267 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.265 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.264 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.263 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.262 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.261 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.260 | [Release notes 0.261.304 – 0.261.260]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | -| v0.261.259 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.258 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.257 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.256 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.253 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.252 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.251 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.250 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.248 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.247 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.246 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.245 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.244 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.243 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.242 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.241 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.240 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.239 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.238 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.237 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.236 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.235 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.234 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.233 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.232 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.231 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.230 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.229 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.228 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.227 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.226 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.225 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.224 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.223 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.222 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.221 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.220 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.219 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.218 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.217 | [Release notes 0.261.259 – 0.261.217]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | -| v0.261.216 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.215 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.214 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.213 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.212 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.210 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.209 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.208 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.207 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.205 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.204 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.203 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.202 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.201 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.200 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.199 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.195 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.194 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.193 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.192 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.191 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.190 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.189 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.188 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.187 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.186 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.185 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.184 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.183 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.182 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.181 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.180 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.179 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.178 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.177 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.176 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.175 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.174 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.173 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.172 | [Release notes 0.261.216 – 0.261.172]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | -| v0.261.171 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.170 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.169 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.168 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.167 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.166 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.165 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.164 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.163 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.162 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.161 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.160 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.159 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.158 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.157 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.156 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.155 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.154 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.153 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.152 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.151 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.150 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.149 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.148 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.147 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.146 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.145 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.144 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.143 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.142 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.141 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.140 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.139 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.138 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.137 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.136 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.135 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.134 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.133 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.132 | [Release notes 0.261.171 – 0.261.132]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | -| v0.261.131 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.129 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.128 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.127 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.123 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.104 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.102 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.100 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.099 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.098 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.096 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.095 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.093 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.092 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.091 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.090 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.089 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.088 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.087 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.086 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.085 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.084 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.083 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.082 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.081 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.074 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.071 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.070 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.065 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.064 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.063 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.062 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.061 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.060 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.059 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.058 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.057 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.056 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.055 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.054 | [Release notes 0.261.131 – 0.261.054]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | -| v0.261.053 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.052 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.051 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.050 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.049 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.048 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.047 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.046 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.045 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.044 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.043 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.042 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.041 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.040 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.039 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.038 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.037 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.036 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.035 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.034 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.033 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.032 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.031 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.030 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.029 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.028 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.027 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.026 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.025 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.024 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.023 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.022 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.021 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.020 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.019 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.018 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.017 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.016 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.015 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.014 | [Release notes 0.261.053 – 0.261.014]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | -| v0.261.013 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.012 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.011 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.010 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.009 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.008 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.007 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.006 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.005 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.004 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.003 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.002 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | -| v0.261.001 | [Release notes 0.261.013 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.308 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.306 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.305 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.304 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.303 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.302 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.301 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.300 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.299 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.298 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.297 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.296 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.295 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.293 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.292 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.291 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.290 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.289 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.288 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.287 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.286 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.285 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.284 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.283 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.282 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.280 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.279 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.278 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.277 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.276 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.275 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.274 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.271 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.270 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.269 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.268 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.267 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.265 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.264 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.263 | [Release notes 0.261.308 – 0.261.263]({{ '/explanation/release-notes/v0.261-part-1/' | relative_url }}) | +| v0.261.262 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.261 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.260 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.259 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.258 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.257 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.256 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.253 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.252 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.251 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.250 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.248 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.247 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.246 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.245 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.244 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.243 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.242 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.241 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.240 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.239 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.238 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.237 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.236 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.235 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.234 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.233 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.232 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.231 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.230 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.229 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.228 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.227 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.226 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.225 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.224 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.223 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.222 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.221 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.220 | [Release notes 0.261.262 – 0.261.220]({{ '/explanation/release-notes/v0.261-part-2/' | relative_url }}) | +| v0.261.219 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.218 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.217 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.216 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.215 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.214 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.213 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.212 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.210 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.209 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.208 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.207 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.205 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.204 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.203 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.202 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.201 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.200 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.199 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.195 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.194 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.193 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.192 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.191 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.190 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.189 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.188 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.187 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.186 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.185 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.184 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.183 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.182 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.181 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.180 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.179 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.178 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.177 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.176 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.175 | [Release notes 0.261.219 – 0.261.175]({{ '/explanation/release-notes/v0.261-part-3/' | relative_url }}) | +| v0.261.174 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.173 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.172 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.171 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.170 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.169 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.168 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.167 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.166 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.165 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.164 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.163 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.162 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.161 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.160 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.159 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.158 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.157 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.156 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.155 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.154 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.153 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.152 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.151 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.150 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.149 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.148 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.147 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.146 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.145 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.144 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.143 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.142 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.141 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.140 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.139 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.138 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.137 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.136 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.135 | [Release notes 0.261.174 – 0.261.135]({{ '/explanation/release-notes/v0.261-part-4/' | relative_url }}) | +| v0.261.134 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.133 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.132 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.131 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.129 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.128 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.127 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.123 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.104 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.102 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.100 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.099 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.098 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.096 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.095 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.093 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.092 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.091 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.090 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.089 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.088 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.087 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.086 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.085 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.084 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.083 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.082 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.081 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.074 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.071 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.070 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.065 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.064 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.063 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.062 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.061 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.060 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.059 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.058 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.057 | [Release notes 0.261.134 – 0.261.057]({{ '/explanation/release-notes/v0.261-part-5/' | relative_url }}) | +| v0.261.056 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.055 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.054 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.053 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.052 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.051 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.050 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.049 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.048 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.047 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.046 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.045 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.044 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.043 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.042 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.041 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.040 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.039 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.038 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.037 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.036 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.035 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.034 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.033 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.032 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.031 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.030 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.029 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.028 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.027 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.026 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.025 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.024 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.023 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.022 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.021 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.020 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.019 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.018 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.017 | [Release notes 0.261.056 – 0.261.017]({{ '/explanation/release-notes/v0.261-part-6/' | relative_url }}) | +| v0.261.016 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.015 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.014 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.013 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.012 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.011 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.010 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.009 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.008 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.007 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.006 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.005 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.004 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.003 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.002 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | +| v0.261.001 | [Release notes 0.261.016 – 0.261.001]({{ '/explanation/release-notes/v0.261-part-7/' | relative_url }}) | | v0.260.025 | [Release notes 0.260 series]({{ '/explanation/release-notes/v0.260/' | relative_url }}) | | v0.260.024 | [Release notes 0.260 series]({{ '/explanation/release-notes/v0.260/' | relative_url }}) | | v0.260.023 | [Release notes 0.260 series]({{ '/explanation/release-notes/v0.260/' | relative_url }}) | @@ -333,6 +336,67 @@ This page includes the latest release notes inline. Older release sections are s ## Latest release notes +### **(v0.261.421)** + +#### Bug Fixes + +* **Selected-Document Orchestration Tool Loading** + * Fixed document-backed runs failing before their first read because plugin discovery created a different copy of the document-reader class than the source-binding safety check expected. + * Shared discovery now reuses registered plugin classes while preserving separate instances, current permissions, selected-file restrictions, and native-inspection limits. + * Explicitly pins the existing Handlebars template-parser dependency so fresh container installations do not report a successful install and then fail to import Semantic Kernel. + * Added production-loader, fresh-start, concurrent-user, and config-derived supported-format retrieval regressions instead of substituting a simplified loader in the end-to-end test. + * (Ref: `semantic_kernel_plugins/plugin_loader.py`, `functions_orchestration_actions.py`, `docs/explanation/fixes/ORCHESTRATION_PLUGIN_DISCOVERY_IDENTITY_FIX.md`) + +* **Action Failure Reasons Retained Through Recovery Details** + * Tool initialization, document-reader identity, failed document reads, and missing function results now retain distinct safe reasons in saved steps and the final failure explanation. + * The first action error includes hashed run/step correlation with debug logging on or off, without exposing private exception text. Code faults are not automatically retried; existing permitted manual recovery remains available. + * (Ref: `functions_orchestration_adapters.py`, `functions_orchestration_schema.py`, `docs/reference/logging-tags.md`) + +{% raw %} +### **(v0.261.410)** + +#### New Features + +* **Microsoft MAI-Thinking-1 In AI Connections** + * A single native Foundry resource/project connection can now route OpenAI, Grok, and Microsoft MAI models to their appropriate inference APIs automatically, including managed identity, streaming, and function tools. + * Built-in model selections preconfigure canonical model identity while retaining automatic exact name matching, deployment names, explicit overrides, and publication choices. + * Preserved Custom/APIM base paths, authentication headers, and request aliases. MAI keeps its native Chat Completions contract even when the shared connection prefers Responses for compatible OpenAI-style models. + * Added the verified MAI profile, generation limits, token audience, and setup guidance without changing existing chat, planner, image, or embedding defaults. + * (Ref: `model_endpoint_clients.py`, `functions_model_endpoint_runtime.py`, `functions_model_capabilities.py`, `ModelConnectionsManager.tsx`, `model_capabilities.json`, [AI connection configuration]({{ '/guides/configure-ai-connections/' | relative_url }}#microsoft-mai-text-models), `docs/explanation/fixes/MICROSOFT_MAI_MODEL_CONNECTIONS_FIX.md`) +{% endraw %} + +### **(v0.261.408)** + +#### Bug Fixes + +* **Excel Comment Contents And Correct Thread Counts** + * Supplies actual native comment records and saved cell context to chat instead of only counts and shortened relevance excerpts. Reviewer requests remain separate from worksheet values. + * Distinguishes genuine legacy notes, threaded roots/replies, and identifiable compatibility copies so mirrored comments do not inflate counts. + * Existing retained originals can be inspected without re-uploading; missing formula caches remain unavailable rather than becoming zero. + * (Ref: `functions_excel_accessibility.py`, `functions_excel_inventory.py`, `functions_search_service.py`, `route_backend_chats.py`, `docs/explanation/fixes/EXCEL_COMMENT_CONTENT_AND_TABLE_HEADERS_FIX.md`) + +#### New Features + +* **Scalable, Table-Aware Saved Excel Inspection** + * Discovers independent named tables and ordinary cell blocks with their own ranges, title/header rows, and source coordinates across worksheets. + * Keeps compact document metadata while full native inventories use private, revision-bound pages. Analytical selection and durable replay retain table/range/header context. + * Adds saved properties, links/validation, readable chart/object content, and configured Office-image analysis with available Excel anchors. Formulas, macros, and external links are never executed/refreshed. + * (Ref: `excel_workbook_reader.py`, `tabular_processing_plugin.py`, `functions_tabular_generated_exports.py`, `docs/explanation/features/EXCEL_NATIVE_INSPECTION.md`) + +#### User Interface Enhancements + +* **Workbook Citation Tables And Bounded Record Navigation** + * Identifies the selected worksheet/table and actual header/row locations, with table selection, record paging, record jumps, keyboard navigation, and retryable selection errors. + * Uses workbook/CSV evidence labels instead of image wording and keeps the originally cited passage independent of preview navigation. + * (Ref: `EnhancedCitationViewer.tsx`, `route_enhanced_citations.py`, `docs/guides/inspect-excel-workbooks.md`) + +#### Breaking Changes + +* **Legacy Queued Workbook Replay Requires Restart** + * Pending workbook computations using v1 worksheet-header descriptors no longer resume under changed table/header semantics. + * **Migration**: Start a new computation to pin the intended worksheet/table/range and header. Existing completed exports are unchanged; CSV replay compatibility is retained. + * (Ref: `functions_tabular_generated_exports.py`, versioned workbook source descriptors) + ### **(v0.261.392)** #### Bug Fixes @@ -444,34 +508,3 @@ This page includes the latest release notes inline. Older release sections are s * A run whose result a plan used says **Its results were used in a chat answer.** on its card under the answer, because there is no posted result to jump to. No bell notification is sent for it. * Workflow names are shown as text. * (Ref: `orchestrationPlan.ts` `describeRunProgress`, `workflowRunStatus.ts` `workflowFinishedResultsText`, `WorkflowRunCard.tsx`) - -### **(v0.261.308)** - -#### New Features - -* **Repeat a Chat Plan on a Schedule** - * With **Repeat Chat Plans On A Schedule** on (`enable_workflow_plan_replay`, off by default; it needs Chat Orchestration and personal workflows), a finished orchestrated answer offers **Repeat on a schedule**. The card freezes the plan that just ran and shows the request, how dates are handled, the shortest allowed interval, the step cap and every step with its capability before anything is saved. Saving creates a paused personal workflow unless the user turns the schedule on. - * Each run replays the frozen plan as the person who saved it, in the workflow's own conversation, never the chat it came from. The plan is SHA-256 hashed when it's saved and checked again before every run, and the model never adds, removes or rewrites a step. Each run also checks again that the setting, Chat Orchestration, every step's capability, and every document, group and public workspace the plan reads are still available to that person. A refused run fails with a fixed reason before any step runs. - * Only steps that are safe to repeat are allowed: Search documents, Analyse documents, Compare documents, Merge documents, Inspect spreadsheets, Prepare content and Generate image. Web search, page fetches, deep research, actions, agents, workflow steps, steps that wait for a file or result, and plans that asked the user a question are refused, with a reason for each step. A run is limited to 8 steps and 15 minutes, and the schedule follows the chat-created workflow minimum interval and quota. - * The run inspector shows a typed result for each run: the orchestration run, each step's status, the final response and its image or file artifacts. The V2 workflow editor shows the frozen steps read-only; the name, schedule and alerts stay editable. The Classic workflow page sends these workflows to V2. - * A saved workflow starts with one **Run failed** alert rule, so a refused or failed scheduled run isn't missed: it sends a high-severity notification to the bell, never a pop-up, with the fixed reason. The card says so before saving, and the rule stays editable under **Alerts**. - * Part of #1550, refs #1543. - * (Ref: `functions_workflow_plan_replay.py`, `functions_workflow_runner.py`, `route_backend_orchestration.py` `/api/v2/orchestration/runs//plan-replay`, `PlanReplaySaveCard.tsx`, `WorkflowEditorDialog.tsx`, `WorkflowRunHistory.tsx`, [Workflow plan replay](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/WORKFLOW_PLAN_REPLAY.md)) - -### **(v0.261.306)** - -#### Bug Fixes - -* **Consistent V2 Scope Response Contract** - * Scope confirmation now uses the same response tuple format for successful reads and error paths, resolving the CodeQL mixed-tuple-returns finding without changing status codes, response bodies, or no-store behavior. - * (Ref: `route_backend_v2.py` `v2_scope`, `test_v2_scope_and_bootstrap_timings.py`) - -### **(v0.261.305)** - -#### Bug Fixes - -* **V2 Group Switching No Longer Rebuilds All Startup Data** - * A group switch now confirms the saved selection through a lightweight scope read instead of waiting for the complete startup payload. Current-access checks, sign-in checks, and read-only recovery after an uncertain save remain in place. - * An overlapping startup refresh cannot restore the previous selection. Startup and scope reads bypass the browser's HTTP cache. - * Startup requests now report per-phase timing so the remaining first-load bottleneck can be measured; first-load optimization is a separate follow-up. - * (Ref: `route_backend_v2.py`, `bootstrapStore.ts`, `groupWorkspaceStore.ts`, [V2 Group Switch Latency Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/V2_GROUP_SWITCH_LATENCY_FIX.md), [#1725](https://github.com/microsoft/simplechat/issues/1725)) diff --git a/docs/explanation/release-notes/v0.261-part-1.md b/docs/explanation/release-notes/v0.261-part-1.md index 98a339fc6..f748f8f27 100644 --- a/docs/explanation/release-notes/v0.261-part-1.md +++ b/docs/explanation/release-notes/v0.261-part-1.md @@ -1,16 +1,47 @@ --- -title: "Release notes 0.261.304 – 0.261.260" -description: "SimpleChat release notes for 0.261.304 – 0.261.260." +title: "Release notes 0.261.308 – 0.261.263" +description: "SimpleChat release notes for 0.261.308 – 0.261.263." section: "Reference" layout: page --- -# Release notes 0.261.304 – 0.261.260 +# Release notes 0.261.308 – 0.261.263 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(v0.261.308)** + +#### New Features + +* **Repeat a Chat Plan on a Schedule** + * With **Repeat Chat Plans On A Schedule** on (`enable_workflow_plan_replay`, off by default; it needs Chat Orchestration and personal workflows), a finished orchestrated answer offers **Repeat on a schedule**. The card freezes the plan that just ran and shows the request, how dates are handled, the shortest allowed interval, the step cap and every step with its capability before anything is saved. Saving creates a paused personal workflow unless the user turns the schedule on. + * Each run replays the frozen plan as the person who saved it, in the workflow's own conversation, never the chat it came from. The plan is SHA-256 hashed when it's saved and checked again before every run, and the model never adds, removes or rewrites a step. Each run also checks again that the setting, Chat Orchestration, every step's capability, and every document, group and public workspace the plan reads are still available to that person. A refused run fails with a fixed reason before any step runs. + * Only steps that are safe to repeat are allowed: Search documents, Analyse documents, Compare documents, Merge documents, Inspect spreadsheets, Prepare content and Generate image. Web search, page fetches, deep research, actions, agents, workflow steps, steps that wait for a file or result, and plans that asked the user a question are refused, with a reason for each step. A run is limited to 8 steps and 15 minutes, and the schedule follows the chat-created workflow minimum interval and quota. + * The run inspector shows a typed result for each run: the orchestration run, each step's status, the final response and its image or file artifacts. The V2 workflow editor shows the frozen steps read-only; the name, schedule and alerts stay editable. The Classic workflow page sends these workflows to V2. + * A saved workflow starts with one **Run failed** alert rule, so a refused or failed scheduled run isn't missed: it sends a high-severity notification to the bell, never a pop-up, with the fixed reason. The card says so before saving, and the rule stays editable under **Alerts**. + * Part of #1550, refs #1543. + * (Ref: `functions_workflow_plan_replay.py`, `functions_workflow_runner.py`, `route_backend_orchestration.py` `/api/v2/orchestration/runs//plan-replay`, `PlanReplaySaveCard.tsx`, `WorkflowEditorDialog.tsx`, `WorkflowRunHistory.tsx`, [Workflow plan replay](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/WORKFLOW_PLAN_REPLAY.md)) + +### **(v0.261.306)** + +#### Bug Fixes + +* **Consistent V2 Scope Response Contract** + * Scope confirmation now uses the same response tuple format for successful reads and error paths, resolving the CodeQL mixed-tuple-returns finding without changing status codes, response bodies, or no-store behavior. + * (Ref: `route_backend_v2.py` `v2_scope`, `test_v2_scope_and_bootstrap_timings.py`) + +### **(v0.261.305)** + +#### Bug Fixes + +* **V2 Group Switching No Longer Rebuilds All Startup Data** + * A group switch now confirms the saved selection through a lightweight scope read instead of waiting for the complete startup payload. Current-access checks, sign-in checks, and read-only recovery after an uncertain save remain in place. + * An overlapping startup refresh cannot restore the previous selection. Startup and scope reads bypass the browser's HTTP cache. + * Startup requests now report per-phase timing so the remaining first-load bottleneck can be measured; first-load optimization is a separate follow-up. + * (Ref: `route_backend_v2.py`, `bootstrapStore.ts`, `groupWorkspaceStore.ts`, [V2 Group Switch Latency Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/V2_GROUP_SWITCH_LATENCY_FIX.md), [#1725](https://github.com/microsoft/simplechat/issues/1725)) + ### **(v0.261.304)** #### Bug Fixes @@ -707,52 +738,3 @@ layout: page * V2 Admin Settings now shows the workspace search result cache in a **Search result cache** group under **Knowledge › Search Index › Azure AI Search**. The group explains what is cached, what forces a fresh search, and why the cache should stay on. Previously the fallback scan matched the word "search" and placed it under Web Search as a bare switch labelled with its key, `enable_search_result_caching`. * **Cache lifetime (seconds)** can now be changed, from 60 to 3,600 seconds with a default of 300. It appears only while caching is on. Caching stays on by default, and a value an administrator saved as off is kept. * (Ref: `admin_settings_fields.py`, `test_search_result_cache_admin_setting.py`, `test_v2_admin_search_result_cache.py`, [Search Result Cache Admin Setting Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/SEARCH_RESULT_CACHE_ADMIN_SETTING_FIX.md)) - -### **(v0.261.262)** - -#### User Interface Enhancements - -* **Media Galleries in V2 Replies** - * Two or more images or clips in a row now show as a gallery of tiles, three to a row, each with its caption underneath, instead of one full-width card each. A short caption line above an image or clip becomes its caption. In a list, the clip and still at the end of each item sit side by side under the item's text. A single image keeps its full-width card, and recordings keep their player bar. - * A clip tile shows its first frame and length, and plays where it is. **View larger** continues it in the viewer. - * Grouping happens on the parsed markdown: raw HTML stays disabled and react-markdown's URL checks still apply to every image and link. - * (Ref: `mediaGallery.ts`, `MediaGallery.tsx`, `MediaTiles.tsx`, `AssistantMarkdown.tsx`, [V2 Media Galleries and Viewer](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_MEDIA_GALLERIES_AND_VIEWER.md)) - -* **Media Viewer** - * Opening an image or clip shows it large, with **Previous** and **Next** buttons and the arrow keys stepping through the gallery, its position ("3 of 15"), View at actual size, Download and Open in a new tab. Focus returns to the tile when it closes. - * (Ref: `MediaViewer.tsx`, `mediaDownload.ts`, `useMediaDownload.ts`) - -* **Drawer Media Section Groups Images, Videos and Audio** - * The Documents drawer now lists media under **Images**, **Videos** and **Audio** with counts. Clips show as tiles with their first frame instead of text rows, and images and clips open one viewer across all of them, with **Show in conversation** to scroll to the message. - * Recordings play in the drawer, with play, stop, seek, volume, speed, Download and Show in conversation. - * (Ref: `DrawerAssets.tsx`, `ConversationDrawer.tsx`, `InlineAudioPlayer.tsx`) - -* **Download for Recordings, Clips and Images** - * Recordings, clips and images can be saved from the player and the viewer, under the file name in their link. When a host does not let the page read its files, the file opens in a new tab instead, where the browser can save it. An expired link reports that it has expired. - * (Ref: `mediaDownload.ts`, `InlineAudioPlayer.tsx`, `MediaViewer.tsx`) - -#### Bug Fixes - -* **Escape in a Drawer Dialog No Longer Closes the Drawer** - * Pressing Escape in a dialog opened from the conversation drawer, such as a document preview or the media viewer, also closed the drawer. Escape now closes only the dialog, and a second Escape closes the drawer. - * (Ref: `ConversationDrawer.tsx`, `test_v2_media_galleries.py`) - -### **(v0.261.261)** - -#### Bug Fixes - -* **Always-On Tabular Switches Removed From V2 Admin Settings** - * V2 Admin Settings no longer shows an unlabelled **Tabular search shared preflight** switch under Workspaces › Files & Sharing › Shared Conversation File Approvals. It landed there only because its key contains the word "shared". It also no longer shows `enable_tabular_analyze_durable_preflight` or `enable_tabular_hierarchical_analysis` under Other capabilities. - * All three flags send exhaustive row-by-row requests against CSV and XLSX files to a durable background job that covers every row, instead of a foreground answer that truncates. They are always on: settings loading resets them to on every time, so the switches reverted after every save. Behavior is unchanged, and the `SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT` environment variable is still the emergency off switch. - * The capability placement test now fails if any flag that settings loading resets is drawn as a switch, or if a guessed switch lands in the Workspaces group. - * (Ref: `admin_settings_fields.SUPPRESSED_CAPABILITY_KEYS`, `TABULAR_PARITY_DURABLE_PREFLIGHT_ACTIVE_DEFAULTS`, `test_v2_admin_capability_placement.py`, [V2 Admin Tabular Preflight Switch Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md)) - -### **(v0.261.260)** - -#### Bug Fixes - -* **V2 Web Search Test Uses the Saved Foundry Connection** - * **Test web search** in V2 Admin Settings reported "Foundry Project Endpoint is required", "Foundry API Version is required" and "Foundry Agent ID is required" for a saved connection whose values were on screen. The Foundry connection is saved inside `web_search_agent`, but the test read each field as a top-level setting, so it only worked before the values were saved. It now reads the saved values, including a service principal's authentication type and masked client secret, which the server swaps for the stored secret. - * The same fix shows **Managed Identity Type**, or the service principal fields, for the saved authentication type, and marks a saved connection as configured. Clearing a saved client secret now shows the removal warning and **Undo**. - * The **Document action capabilities** card now shows the chat and workflow limits for Analyze, Comparison and Merge while that action is enabled. The section card used to hide them once the setting was saved. - * (Ref: `adminFields.ts` `readSettingValue` and `buildConnectionTestPayload`, `adminSections.ts`, `ConnectionTest.tsx`, `SettingsSection.tsx`, `AdminSettingsPage.tsx`, `test_v2_admin_nested_setting_reads.py`, [V2 Admin Web Search Nested Settings Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/V2_ADMIN_WEB_SEARCH_NESTED_SETTINGS_FIX.md)) diff --git a/docs/explanation/release-notes/v0.261-part-2.md b/docs/explanation/release-notes/v0.261-part-2.md index c7c4f5db7..4087aa7c3 100644 --- a/docs/explanation/release-notes/v0.261-part-2.md +++ b/docs/explanation/release-notes/v0.261-part-2.md @@ -1,16 +1,65 @@ --- -title: "Release notes 0.261.259 – 0.261.217" -description: "SimpleChat release notes for 0.261.259 – 0.261.217." +title: "Release notes 0.261.262 – 0.261.220" +description: "SimpleChat release notes for 0.261.262 – 0.261.220." section: "Reference" layout: page --- -# Release notes 0.261.259 – 0.261.217 +# Release notes 0.261.262 – 0.261.220 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(v0.261.262)** + +#### User Interface Enhancements + +* **Media Galleries in V2 Replies** + * Two or more images or clips in a row now show as a gallery of tiles, three to a row, each with its caption underneath, instead of one full-width card each. A short caption line above an image or clip becomes its caption. In a list, the clip and still at the end of each item sit side by side under the item's text. A single image keeps its full-width card, and recordings keep their player bar. + * A clip tile shows its first frame and length, and plays where it is. **View larger** continues it in the viewer. + * Grouping happens on the parsed markdown: raw HTML stays disabled and react-markdown's URL checks still apply to every image and link. + * (Ref: `mediaGallery.ts`, `MediaGallery.tsx`, `MediaTiles.tsx`, `AssistantMarkdown.tsx`, [V2 Media Galleries and Viewer](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_MEDIA_GALLERIES_AND_VIEWER.md)) + +* **Media Viewer** + * Opening an image or clip shows it large, with **Previous** and **Next** buttons and the arrow keys stepping through the gallery, its position ("3 of 15"), View at actual size, Download and Open in a new tab. Focus returns to the tile when it closes. + * (Ref: `MediaViewer.tsx`, `mediaDownload.ts`, `useMediaDownload.ts`) + +* **Drawer Media Section Groups Images, Videos and Audio** + * The Documents drawer now lists media under **Images**, **Videos** and **Audio** with counts. Clips show as tiles with their first frame instead of text rows, and images and clips open one viewer across all of them, with **Show in conversation** to scroll to the message. + * Recordings play in the drawer, with play, stop, seek, volume, speed, Download and Show in conversation. + * (Ref: `DrawerAssets.tsx`, `ConversationDrawer.tsx`, `InlineAudioPlayer.tsx`) + +* **Download for Recordings, Clips and Images** + * Recordings, clips and images can be saved from the player and the viewer, under the file name in their link. When a host does not let the page read its files, the file opens in a new tab instead, where the browser can save it. An expired link reports that it has expired. + * (Ref: `mediaDownload.ts`, `InlineAudioPlayer.tsx`, `MediaViewer.tsx`) + +#### Bug Fixes + +* **Escape in a Drawer Dialog No Longer Closes the Drawer** + * Pressing Escape in a dialog opened from the conversation drawer, such as a document preview or the media viewer, also closed the drawer. Escape now closes only the dialog, and a second Escape closes the drawer. + * (Ref: `ConversationDrawer.tsx`, `test_v2_media_galleries.py`) + +### **(v0.261.261)** + +#### Bug Fixes + +* **Always-On Tabular Switches Removed From V2 Admin Settings** + * V2 Admin Settings no longer shows an unlabelled **Tabular search shared preflight** switch under Workspaces › Files & Sharing › Shared Conversation File Approvals. It landed there only because its key contains the word "shared". It also no longer shows `enable_tabular_analyze_durable_preflight` or `enable_tabular_hierarchical_analysis` under Other capabilities. + * All three flags send exhaustive row-by-row requests against CSV and XLSX files to a durable background job that covers every row, instead of a foreground answer that truncates. They are always on: settings loading resets them to on every time, so the switches reverted after every save. Behavior is unchanged, and the `SIMPLECHAT_DISABLE_TABULAR_PARITY_DURABLE_PREFLIGHT` environment variable is still the emergency off switch. + * The capability placement test now fails if any flag that settings loading resets is drawn as a switch, or if a guessed switch lands in the Workspaces group. + * (Ref: `admin_settings_fields.SUPPRESSED_CAPABILITY_KEYS`, `TABULAR_PARITY_DURABLE_PREFLIGHT_ACTIVE_DEFAULTS`, `test_v2_admin_capability_placement.py`, [V2 Admin Tabular Preflight Switch Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/V2_ADMIN_TABULAR_PREFLIGHT_SWITCH_FIX.md)) + +### **(v0.261.260)** + +#### Bug Fixes + +* **V2 Web Search Test Uses the Saved Foundry Connection** + * **Test web search** in V2 Admin Settings reported "Foundry Project Endpoint is required", "Foundry API Version is required" and "Foundry Agent ID is required" for a saved connection whose values were on screen. The Foundry connection is saved inside `web_search_agent`, but the test read each field as a top-level setting, so it only worked before the values were saved. It now reads the saved values, including a service principal's authentication type and masked client secret, which the server swaps for the stored secret. + * The same fix shows **Managed Identity Type**, or the service principal fields, for the saved authentication type, and marks a saved connection as configured. Clearing a saved client secret now shows the removal warning and **Undo**. + * The **Document action capabilities** card now shows the chat and workflow limits for Analyze, Comparison and Merge while that action is enabled. The section card used to hide them once the setting was saved. + * (Ref: `adminFields.ts` `readSettingValue` and `buildConnectionTestPayload`, `adminSections.ts`, `ConnectionTest.tsx`, `SettingsSection.tsx`, `AdminSettingsPage.tsx`, `test_v2_admin_nested_setting_reads.py`, [V2 Admin Web Search Nested Settings Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/V2_ADMIN_WEB_SEARCH_NESTED_SETTINGS_FIX.md)) + ### **(v0.261.259)** #### Bug Fixes @@ -587,53 +636,3 @@ layout: page * Its reasoning efforts are low, medium, high and xhigh. Chat now sends the selected effort; before, with no policy, it sent no effort and the model used its default. None and other unsupported selections are sent as Low. Agents still send their saved effort as is. * Azure Chat Completions rejects function tools for this model at every effort it accepts, so agents with actions can't run on it yet. The catalog records this in its notes. #1606 tracks a Responses API path and an early, clear error. * (Ref: #1606, `model_capabilities.json`, [GPT-6 Astra Model Catalog Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/GPT6_ASTRA_MODEL_CATALOG_FIX.md)) - -### **(v0.261.219)** - -#### Bug Fixes - -* **GPT-6 Models Use Reasoning Model Request Parameters** - * Requests to GPT-6 deployments failed whenever a response length was set: "Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead." Every workflow run on a GPT-6 agent failed on its first model call once token limits were configured. - * GPT-6 is now recognized alongside GPT-5, by name prefix and as a family name anywhere in a deployment or display name. Response length is sent as `max_completion_tokens`, and no temperature is sent. Workflow budgets, chat, prompt variables, content screening and orchestration all follow the shared policy. - * (Ref: #1605, `model_endpoint_clients.py`, [GPT-6 Reasoning Model Parameters Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/GPT6_REASONING_MODEL_PARAMETERS_FIX.md)) - -### **(v0.261.218)** - -#### Bug Fixes - -* **Scheduled Workflow Runs Act As Their Owner** - * A scheduled run had no signed-in session, so the runner created one that held only the owner's object ID. Groups and conversation messages the run created showed that ID instead of the owner's name, and the run then wrote the ID over the owner's stored display name. The runner now uses the owner's stored display name and email. A manual run keeps the signed-in session, as before. - * The SimpleChat action's `add_user_to_group` now accepts `user_id`. Given with an email or display name, it adds the user without a directory lookup, as the REST members route already does. Scheduled runs have no delegated token for Microsoft Graph, so they couldn't add members before. - * Group conversation invites first match each identifier against the group's current owner and members, by user ID, email or display name. An identifier that matches no member, or more than one, still goes to the directory lookup. - * (Ref: `functions_workflow_runner.py`, `functions_simplechat_operations.py`, `simplechat_plugin.py`, [Workflow Background Identity Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/WORKFLOW_BACKGROUND_IDENTITY_FIX.md), [SimpleChat action](https://microsoft.github.io/simplechat/reference/actions/simplechat/)) - -### **(v0.261.217)** - -#### New Features - -* **Read Saved Workflow Results From Chat Orchestration** - * Chat orchestration can now read the stored result of one of your own finished personal workflow runs while it answers, for example "what did my Monday email digest workflow say this week?" or "compare yesterday's and today's run of my contract review workflow". A plan step names the workflow and reads its latest finished run, or the newest run that finished on a day you name in your own time zone, optionally only a completed, failed or cancelled one. The workflow isn't re-run, and a plan reads at most two results. - * The new capability, **Read workflow results** (`workflow_results`), is read-only with no approval floor, so a plan that uses it follows the user's approval mode. It's offered only while **Use Workflow Results In Chat** (`enable_chat_workflow_results`) is on, to users who may use personal workflows, in their own private conversations, and only for workflows in the request's workflow catalog. The step checks all of this again when it runs. With the setting off, which is the default, planning is unchanged. - * The planner and the plan name a workflow by a per-request handle, never by its ID. The server picks the run with bounded queries on the user's own runs and reads it through the Phase 6a result reader, which authorizes the run again. At most 24 KB of each run's result, from up to 8 of its outputs, reaches the answer, only as fenced, untrusted notes and never as a cited source. Failed and cancelled runs are reported by their status and time only, and a run still in progress is reported without any of its output. - * The answer adds a server-written **Saved workflow results:** note, with Phase 6a's line for each run it used and the reason for each result it didn't read, and no IDs. The answer stores `workflow_result_contexts`, so Phase 6a's masking withholds it, on the same reads that withhold Follow up answers, once a result it used is no longer available or the chat is no longer the owner's private chat. Each result is authorized again while the answer is written and twice more before it's published. If one changed, the answer is replaced with "A workflow result this answer used changed or is no longer available, so the answer was not saved. Ask again to read the current result." - * Later turns never reuse a read: result aliases skip any run whose plan reads a workflow result, and a new attempt or revised plan runs its reads and the steps built on them again instead of reusing an earlier answer. A plan edit that breaks a results rule is refused, and the previous plan stays. - * Admins can keep it out of plans by clearing **Read workflow results** in the orchestration Capabilities list. Group workflows, structured (version 3) runs and runs that saved an analysis can't be read from a plan, and a plan never waits for a run it starts to finish. - * (Ref: #1546, #1543, `functions_orchestration_workflow_results.py`, `functions_orchestration_registry.py`, `functions_orchestration_workflow_context.py`, `functions_orchestration_planner.py`, `functions_orchestration_schema.py`, `functions_orchestration_execution.py`, `functions_orchestration_executor.py`, `functions_orchestration_composition.py`, `functions_orchestration_services.py`, `functions_orchestration_recovery.py`, `route_backend_orchestration.py`, [Chat Orchestration Workflow Results](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/CHAT_ORCHESTRATION_WORKFLOW_RESULTS.md), [Ask about workflow results](https://microsoft.github.io/simplechat/guides/ask-about-workflow-results/#ask-orchestrate-about-workflow-results)) - -#### Bug Fixes - -* **Group Document Uploads With Enhanced Citations** - * With Enhanced Citations on, every document uploaded to a group workspace failed with "Document processing failed." That included Word and Markdown documents an agent or workflow saved to a group. The upload's own status update changed the document record's etag between the read and the guarded write, so the write always saw a conflict. - * The status is now posted before the record is read. A change made by anyone else during the upload is still rejected. - * (Ref: `functions_documents.py`, [Group Document Blob Upload Etag Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/GROUP_DOCUMENT_BLOB_UPLOAD_ETAG_FIX.md)) - -* **Call Agent Actions Save Again** - * Saving any Call agent action, whether personal, group or global, failed with "Invalid plugin configuration." The manifest check rejected the server-owned `scope_id` field that binding to a collection adds, so agents couldn't delegate to other agents. - * `scope_id` is now accepted. The server always sets it from the authorized collection, and every other extra field is still rejected. - * (Ref: `functions_agent_delegation.py`, [Call Agent Action Save Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/CALL_AGENT_ACTION_SAVE_FIX.md)) - -#### User Interface Enhancements - -* **Saved Workflow Results In The V2 Run View** - * A plan step that reads a saved workflow's result shows **Workflow**, the workflow's own name, and **Run**, the run it reads in words, such as **Latest run**, **Latest completed run** or **Run finished on 2025-06-02**, in place of its raw arguments. The name is shown as text, never markup, and a selector the card doesn't recognize shows **Run details unavailable**. - * (Ref: `OrchestrationRunView.tsx`, `orchestrationWorkflowResults.ts`, `orchestrationPlan.ts`, `orchestration.ts`, [Chat controls](https://microsoft.github.io/simplechat/reference/chat-controls/#saved-workflow-results-in-a-plan)) diff --git a/docs/explanation/release-notes/v0.261-part-3.md b/docs/explanation/release-notes/v0.261-part-3.md index cbbf484d9..10453e681 100644 --- a/docs/explanation/release-notes/v0.261-part-3.md +++ b/docs/explanation/release-notes/v0.261-part-3.md @@ -1,16 +1,66 @@ --- -title: "Release notes 0.261.216 – 0.261.172" -description: "SimpleChat release notes for 0.261.216 – 0.261.172." +title: "Release notes 0.261.219 – 0.261.175" +description: "SimpleChat release notes for 0.261.219 – 0.261.175." section: "Reference" layout: page --- -# Release notes 0.261.216 – 0.261.172 +# Release notes 0.261.219 – 0.261.175 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(v0.261.219)** + +#### Bug Fixes + +* **GPT-6 Models Use Reasoning Model Request Parameters** + * Requests to GPT-6 deployments failed whenever a response length was set: "Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead." Every workflow run on a GPT-6 agent failed on its first model call once token limits were configured. + * GPT-6 is now recognized alongside GPT-5, by name prefix and as a family name anywhere in a deployment or display name. Response length is sent as `max_completion_tokens`, and no temperature is sent. Workflow budgets, chat, prompt variables, content screening and orchestration all follow the shared policy. + * (Ref: #1605, `model_endpoint_clients.py`, [GPT-6 Reasoning Model Parameters Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/GPT6_REASONING_MODEL_PARAMETERS_FIX.md)) + +### **(v0.261.218)** + +#### Bug Fixes + +* **Scheduled Workflow Runs Act As Their Owner** + * A scheduled run had no signed-in session, so the runner created one that held only the owner's object ID. Groups and conversation messages the run created showed that ID instead of the owner's name, and the run then wrote the ID over the owner's stored display name. The runner now uses the owner's stored display name and email. A manual run keeps the signed-in session, as before. + * The SimpleChat action's `add_user_to_group` now accepts `user_id`. Given with an email or display name, it adds the user without a directory lookup, as the REST members route already does. Scheduled runs have no delegated token for Microsoft Graph, so they couldn't add members before. + * Group conversation invites first match each identifier against the group's current owner and members, by user ID, email or display name. An identifier that matches no member, or more than one, still goes to the directory lookup. + * (Ref: `functions_workflow_runner.py`, `functions_simplechat_operations.py`, `simplechat_plugin.py`, [Workflow Background Identity Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/WORKFLOW_BACKGROUND_IDENTITY_FIX.md), [SimpleChat action](https://microsoft.github.io/simplechat/reference/actions/simplechat/)) + +### **(v0.261.217)** + +#### New Features + +* **Read Saved Workflow Results From Chat Orchestration** + * Chat orchestration can now read the stored result of one of your own finished personal workflow runs while it answers, for example "what did my Monday email digest workflow say this week?" or "compare yesterday's and today's run of my contract review workflow". A plan step names the workflow and reads its latest finished run, or the newest run that finished on a day you name in your own time zone, optionally only a completed, failed or cancelled one. The workflow isn't re-run, and a plan reads at most two results. + * The new capability, **Read workflow results** (`workflow_results`), is read-only with no approval floor, so a plan that uses it follows the user's approval mode. It's offered only while **Use Workflow Results In Chat** (`enable_chat_workflow_results`) is on, to users who may use personal workflows, in their own private conversations, and only for workflows in the request's workflow catalog. The step checks all of this again when it runs. With the setting off, which is the default, planning is unchanged. + * The planner and the plan name a workflow by a per-request handle, never by its ID. The server picks the run with bounded queries on the user's own runs and reads it through the Phase 6a result reader, which authorizes the run again. At most 24 KB of each run's result, from up to 8 of its outputs, reaches the answer, only as fenced, untrusted notes and never as a cited source. Failed and cancelled runs are reported by their status and time only, and a run still in progress is reported without any of its output. + * The answer adds a server-written **Saved workflow results:** note, with Phase 6a's line for each run it used and the reason for each result it didn't read, and no IDs. The answer stores `workflow_result_contexts`, so Phase 6a's masking withholds it, on the same reads that withhold Follow up answers, once a result it used is no longer available or the chat is no longer the owner's private chat. Each result is authorized again while the answer is written and twice more before it's published. If one changed, the answer is replaced with "A workflow result this answer used changed or is no longer available, so the answer was not saved. Ask again to read the current result." + * Later turns never reuse a read: result aliases skip any run whose plan reads a workflow result, and a new attempt or revised plan runs its reads and the steps built on them again instead of reusing an earlier answer. A plan edit that breaks a results rule is refused, and the previous plan stays. + * Admins can keep it out of plans by clearing **Read workflow results** in the orchestration Capabilities list. Group workflows, structured (version 3) runs and runs that saved an analysis can't be read from a plan, and a plan never waits for a run it starts to finish. + * (Ref: #1546, #1543, `functions_orchestration_workflow_results.py`, `functions_orchestration_registry.py`, `functions_orchestration_workflow_context.py`, `functions_orchestration_planner.py`, `functions_orchestration_schema.py`, `functions_orchestration_execution.py`, `functions_orchestration_executor.py`, `functions_orchestration_composition.py`, `functions_orchestration_services.py`, `functions_orchestration_recovery.py`, `route_backend_orchestration.py`, [Chat Orchestration Workflow Results](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/CHAT_ORCHESTRATION_WORKFLOW_RESULTS.md), [Ask about workflow results](https://microsoft.github.io/simplechat/guides/ask-about-workflow-results/#ask-orchestrate-about-workflow-results)) + +#### Bug Fixes + +* **Group Document Uploads With Enhanced Citations** + * With Enhanced Citations on, every document uploaded to a group workspace failed with "Document processing failed." That included Word and Markdown documents an agent or workflow saved to a group. The upload's own status update changed the document record's etag between the read and the guarded write, so the write always saw a conflict. + * The status is now posted before the record is read. A change made by anyone else during the upload is still rejected. + * (Ref: `functions_documents.py`, [Group Document Blob Upload Etag Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/GROUP_DOCUMENT_BLOB_UPLOAD_ETAG_FIX.md)) + +* **Call Agent Actions Save Again** + * Saving any Call agent action, whether personal, group or global, failed with "Invalid plugin configuration." The manifest check rejected the server-owned `scope_id` field that binding to a collection adds, so agents couldn't delegate to other agents. + * `scope_id` is now accepted. The server always sets it from the authorized collection, and every other extra field is still rejected. + * (Ref: `functions_agent_delegation.py`, [Call Agent Action Save Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/CALL_AGENT_ACTION_SAVE_FIX.md)) + +#### User Interface Enhancements + +* **Saved Workflow Results In The V2 Run View** + * A plan step that reads a saved workflow's result shows **Workflow**, the workflow's own name, and **Run**, the run it reads in words, such as **Latest run**, **Latest completed run** or **Run finished on 2025-06-02**, in place of its raw arguments. The name is shown as text, never markup, and a selector the card doesn't recognize shows **Run details unavailable**. + * (Ref: `OrchestrationRunView.tsx`, `orchestrationWorkflowResults.ts`, `orchestrationPlan.ts`, `orchestration.ts`, [Chat controls](https://microsoft.github.io/simplechat/reference/chat-controls/#saved-workflow-results-in-a-plan)) + ### **(v0.261.216)** #### Bug Fixes @@ -794,32 +844,3 @@ Merged from the React V2 branch at `d243b0ca`. These changes shipped there as 0. * It reads a new route, `GET /api/public_workspaces/directory`, which the V2 public workspace picker now uses too. Its rows carry no owner email, unlike the classic list route. * The directory, public workspace pages, the picker and the chat handoffs use the public workspace names an administrator configures. * (Ref: `route_backend_public_directory.py`, `functions_public_directory.py`, `PublicDirectoryPage.tsx`, `lib/publicVisibility.ts`, `lib/publicWorkspaceLabels.ts`, [V2 Public Workspace Directory](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_PUBLIC_DIRECTORY.md), [Public Directory APIs](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/PUBLIC_DIRECTORY_APIS.md)) - -### **(v0.261.174)** - -#### User Interface Enhancements - -* **Content Screening Scans From A Group's Documents** - * A group's Owner, Admins and DocumentManagers now get **Screening scans** in the V2 group Documents section, the same modal personal documents offer. They can scan the group, follow recent scans with cancel, resume and retry, see the policy additions, and open Content review. - * The server decides who sees it: the workspace context publishes the members the screening routes accept, so a member is never offered a control the server would refuse. - * (Ref: `DocumentsSection.tsx`, `ScreeningWorkspaceControls.tsx`, `functions_workspace_context.py`, [V2 Group Document Management](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_GROUP_DOCUMENT_MANAGEMENT.md)) - -### **(v0.261.173)** - -#### Bug Fixes - -* **Public Workspace Changes No Longer Overwrite Each Other** - * Every classic writer of a public workspace, 27 in all, saved a copy it had read earlier. They covered members, requests, roles, ownership, settings, logo, downloads, retention, tags and seven Control Center actions. So one change could undo another made at the same moment, and a save could bring back a deleted workspace. They now write conditionally, keep unrelated changes, never recreate a deleted workspace, and answer "The public workspace changed while your request was being saved. Try again." when a workspace keeps changing. - * Admins promoted in classic can now see and decide document manager requests, add members and receive ownership. A legacy member entry no longer breaks the members routes, and a role change or ownership transfer keeps the member's name and email. - * Unknown workspace statuses now get the inactive permissions instead of the active ones. Statistics refuse out-of-range dates with a reviewed message instead of failing. The update, download and logo routes answer reviewed messages instead of raw error text, including for an oversized image. Invalid download and retention settings are refused. - * A tag change that loses to a concurrent change answers the coded vocabulary conflict, and metadata edits and bulk tagging save nothing when refused. - * (Ref: `functions_public_workspaces.py`, `route_backend_public_workspaces.py`, `route_backend_public_documents.py`, `route_backend_control_center.py`, `route_backend_retention_policy.py`, [Public Workspace Writer Safety Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/PUBLIC_WORKSPACE_WRITER_SAFETY_FIX.md)) - -### **(v0.261.172)** - -#### Bug Fixes - -* **Ignoring A Browsed File Takes Effect** - * In the V2 group file source editor, **Ignore** sent the file's path under the source's root, but the sync engine tracks each file by its full remote path, so the ignore never matched and the file kept syncing. Folders were also offered Ignore, which the engine can't honour. - * Browse now gives every file its full remote path, built the way the engine builds it for SMB, Azure Files, Azure Blob and OneDrive. **Ignore** and **Restore** send it, and appear on files only. - * (Ref: `functions_file_sync.py`, `FileSourceEditorDialog.tsx`, [File Source Browse Ignore Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/FILE_SOURCE_BROWSE_IGNORE_FIX.md)) diff --git a/docs/explanation/release-notes/v0.261-part-4.md b/docs/explanation/release-notes/v0.261-part-4.md index f16e3fd90..b435d2289 100644 --- a/docs/explanation/release-notes/v0.261-part-4.md +++ b/docs/explanation/release-notes/v0.261-part-4.md @@ -1,16 +1,45 @@ --- -title: "Release notes 0.261.171 – 0.261.132" -description: "SimpleChat release notes for 0.261.171 – 0.261.132." +title: "Release notes 0.261.174 – 0.261.135" +description: "SimpleChat release notes for 0.261.174 – 0.261.135." section: "Reference" layout: page --- -# Release notes 0.261.171 – 0.261.132 +# Release notes 0.261.174 – 0.261.135 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(v0.261.174)** + +#### User Interface Enhancements + +* **Content Screening Scans From A Group's Documents** + * A group's Owner, Admins and DocumentManagers now get **Screening scans** in the V2 group Documents section, the same modal personal documents offer. They can scan the group, follow recent scans with cancel, resume and retry, see the policy additions, and open Content review. + * The server decides who sees it: the workspace context publishes the members the screening routes accept, so a member is never offered a control the server would refuse. + * (Ref: `DocumentsSection.tsx`, `ScreeningWorkspaceControls.tsx`, `functions_workspace_context.py`, [V2 Group Document Management](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_GROUP_DOCUMENT_MANAGEMENT.md)) + +### **(v0.261.173)** + +#### Bug Fixes + +* **Public Workspace Changes No Longer Overwrite Each Other** + * Every classic writer of a public workspace, 27 in all, saved a copy it had read earlier. They covered members, requests, roles, ownership, settings, logo, downloads, retention, tags and seven Control Center actions. So one change could undo another made at the same moment, and a save could bring back a deleted workspace. They now write conditionally, keep unrelated changes, never recreate a deleted workspace, and answer "The public workspace changed while your request was being saved. Try again." when a workspace keeps changing. + * Admins promoted in classic can now see and decide document manager requests, add members and receive ownership. A legacy member entry no longer breaks the members routes, and a role change or ownership transfer keeps the member's name and email. + * Unknown workspace statuses now get the inactive permissions instead of the active ones. Statistics refuse out-of-range dates with a reviewed message instead of failing. The update, download and logo routes answer reviewed messages instead of raw error text, including for an oversized image. Invalid download and retention settings are refused. + * A tag change that loses to a concurrent change answers the coded vocabulary conflict, and metadata edits and bulk tagging save nothing when refused. + * (Ref: `functions_public_workspaces.py`, `route_backend_public_workspaces.py`, `route_backend_public_documents.py`, `route_backend_control_center.py`, `route_backend_retention_policy.py`, [Public Workspace Writer Safety Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/PUBLIC_WORKSPACE_WRITER_SAFETY_FIX.md)) + +### **(v0.261.172)** + +#### Bug Fixes + +* **Ignoring A Browsed File Takes Effect** + * In the V2 group file source editor, **Ignore** sent the file's path under the source's root, but the sync engine tracks each file by its full remote path, so the ignore never matched and the file kept syncing. Folders were also offered Ignore, which the engine can't honour. + * Browse now gives every file its full remote path, built the way the engine builds it for SMB, Azure Files, Azure Blob and OneDrive. **Ignore** and **Restore** send it, and appear on files only. + * (Ref: `functions_file_sync.py`, `FileSourceEditorDialog.tsx`, [File Source Browse Ignore Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/FILE_SOURCE_BROWSE_IGNORE_FIX.md)) + ### **(v0.261.171)** #### New Features @@ -597,65 +626,3 @@ layout: page * The native explorer was not affected, because it decides whether to offer downloads from the workspace's advertised operations and each document's own action list. Other consumers of the API would have been told downloads were off. * The flag is now derived from the same policy that decides whether the workspace advertises downloads, so the two cannot disagree. * (Ref: `route_backend_public_document_reads.py`, `public_document_management_operations`, `test_public_document_read_apis.py`) - -### **(v0.261.134)** - -#### New Features - -* **Native Public Workspace Artifact Approval** - * When someone asks to publish a generated artifact into a public workspace, reviewers can now approve, reject, or withdraw the request in the native explorer instead of switching to the classic page. - * Each decision is tied to the version of the request the reviewer actually saw. If the document changed in the meantime, the reviewer's input is kept and they are asked to refresh before deciding. - * Only the original requester can withdraw a request, and doing so grants no authority over anyone else's. - * Content screening holds are never released early by an approval; approval only starts screening on the new document. - * (Ref: `functions_public_document_publication.py`, `route_backend_public_document_collaboration.py`, `documentCollaboration.ts`, `DocumentCollaborationDialog.tsx`, [V2 Public Artifact Approval](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_PUBLIC_ARTIFACT_APPROVAL.md), [Artifact Approval APIs](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/PUBLIC_DOCUMENT_ARTIFACT_APPROVAL_APIS.md)) - -#### Bug Fixes - -* **Public Workspace Document Actions Were Unavailable** - * Fixed public workspace documents offering no per-document actions against a real backend. Delete, download, metadata editing, extraction, reprocessing, and bulk tagging were all unavailable, although upload and tag management worked. - * The server returned an empty action list left over from the read-only browsing release, and the explorer only offers actions that list contains. It now computes each document's actions from the user's current permissions. - * The failure blocked actions rather than allowing them, so no data was exposed and every operation was still authorized on the server. - * (Ref: `functions_public_document_reads.py`, `get_public_document_actions`, `test_document_action_hint_seam.py`) - -#### Known Limitations - -* **Public Workspace Sharing Is Not Yet Available** - * Public workspace documents cannot yet be shared into other workspaces. The search index used by public workspaces has no field for shared access, so adding it requires an index schema change and a migration for existing deployments. Public workspaces show no share controls until that work is done. - -### **(v0.261.133)** - -#### New Features - -* **Native Public Workspace Document Management** - * Public workspace documents can now be managed in the native explorer, not just browsed: upload, metadata editing, tags and bulk tagging, permitted downloads, metadata extraction, reprocessing, and revision-aware deletion. Previously every one of these required switching to the classic page. - * Each operation names its workspace directly rather than depending on the account's stored selection, so changing workspaces mid-operation cannot redirect an upload, edit, or deletion, and an older server rejects the request instead of acting in the wrong place. - * Every operation receipt is checked against the workspace that was asked for, so a late or misrouted response cannot be shown as a successful change to the wrong workspace. - * Manager roles keep the content-manager boundary, ordinary members retain read access only, and locked or upload-disabled workspaces keep their existing restrictions. Queued work revalidates permission when it runs rather than trusting the moment it was requested. - * Sharing and generated-artifact approval remain on the classic page and arrive natively in a later release. - * (Ref: `functions_public_document_management.py`, `functions_public_document_policy.py`, `route_backend_public_document_management.py`, `documentOperations.ts`, [V2 Public Document Management](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_PUBLIC_DOCUMENT_MANAGEMENT.md), [Management APIs](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/PUBLIC_DOCUMENT_MANAGEMENT_APIS.md)) - -### **(v0.261.132)** - -#### New Features - -* **Native Public Workspace Document Browsing** - * Public workspace documents can be browsed in the same native explorer used for My Workspace and group workspaces, instead of only on the classic page. Filtering, sorting, pagination, facets, tags, document details, and version history all behave the way they already do elsewhere. - * Selecting a public workspace navigates by its identifier, and every document request names that workspace directly rather than depending on the account's stored selection. Returned documents are checked against the workspace that was asked for, so a late or misrouted response cannot populate the wrong workspace. - * The workspace is still recorded as the active selection, because chat document scoping and the classic page both read it. That step is deliberately non-blocking: if it fails, browsing still works, because nothing on the page depends on it. - * This release covers browsing only. Upload, metadata editing, tags, downloads, extraction, reprocessing, deletion, and approval remain on the classic page and arrive natively in later releases. - * (Ref: `functions_public_document_reads.py`, `functions_public_document_access.py`, `route_backend_public_document_reads.py`, `documentReadAdapter.ts`, `PublicWorkspacePage.tsx`, [V2 Public Document Browsing](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_PUBLIC_DOCUMENT_BROWSING.md), [Public Document Read APIs](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/PUBLIC_DOCUMENT_READ_APIS.md)) - -#### Bug Fixes - -* **Charts, Mermaid Diagrams, And Image Proposals In Orchestrated Answers** - * Fixed orchestrated requests such as "Plot BatteryVoltage1 over the last 15 minutes" answering that a plot could not be produced. No orchestration step had a chart tool, the retrieved rows never reached the answer, and the answer step had none of the chart, Mermaid, or image-proposal guidance ordinary chat uses. - * A requested chart is now drawn from the exact rows an action retrieved, by a chart step that can use only the built-in chart tools. Long series are reduced to the 200 points a chart shows while keeping each segment's highest and lowest values, and the chart subtitle states the sampling. - * The answer places those charts, writes Mermaid diagrams, and proposes images when image generation is enabled, including when an image would help but was not requested. Each image is generated only after the user approves its card. - * Saved Instruction memories shape these visuals, so "I don't like charts" stops charts you did not ask for. An explicit request in the current message still wins, and recalled facts never reach the chart step. - * (Ref: `functions_orchestration_visuals.py`, `functions_orchestration_actions.py`, `functions_orchestration_adapters.py`, `functions_chart_operations.py`, [Visual Outputs Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/ORCHESTRATION_VISUAL_OUTPUTS_FIX.md)) - -#### User Interface Enhancements - -* **Image In Orchestrate** - * The composer's Image control now works in Orchestrate, where it asks the answer for at least one image proposal card instead of blocking the message. A status line explains that the images wait for approval. - * (Ref: `Composer.tsx`, [Chat controls](https://microsoft.github.io/simplechat/reference/chat-controls/)) diff --git a/docs/explanation/release-notes/v0.261-part-5.md b/docs/explanation/release-notes/v0.261-part-5.md index ee02227c2..c8aa05636 100644 --- a/docs/explanation/release-notes/v0.261-part-5.md +++ b/docs/explanation/release-notes/v0.261-part-5.md @@ -1,16 +1,78 @@ --- -title: "Release notes 0.261.131 – 0.261.054" -description: "SimpleChat release notes for 0.261.131 – 0.261.054." +title: "Release notes 0.261.134 – 0.261.057" +description: "SimpleChat release notes for 0.261.134 – 0.261.057." section: "Reference" layout: page --- -# Release notes 0.261.131 – 0.261.054 +# Release notes 0.261.134 – 0.261.057 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(v0.261.134)** + +#### New Features + +* **Native Public Workspace Artifact Approval** + * When someone asks to publish a generated artifact into a public workspace, reviewers can now approve, reject, or withdraw the request in the native explorer instead of switching to the classic page. + * Each decision is tied to the version of the request the reviewer actually saw. If the document changed in the meantime, the reviewer's input is kept and they are asked to refresh before deciding. + * Only the original requester can withdraw a request, and doing so grants no authority over anyone else's. + * Content screening holds are never released early by an approval; approval only starts screening on the new document. + * (Ref: `functions_public_document_publication.py`, `route_backend_public_document_collaboration.py`, `documentCollaboration.ts`, `DocumentCollaborationDialog.tsx`, [V2 Public Artifact Approval](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_PUBLIC_ARTIFACT_APPROVAL.md), [Artifact Approval APIs](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/PUBLIC_DOCUMENT_ARTIFACT_APPROVAL_APIS.md)) + +#### Bug Fixes + +* **Public Workspace Document Actions Were Unavailable** + * Fixed public workspace documents offering no per-document actions against a real backend. Delete, download, metadata editing, extraction, reprocessing, and bulk tagging were all unavailable, although upload and tag management worked. + * The server returned an empty action list left over from the read-only browsing release, and the explorer only offers actions that list contains. It now computes each document's actions from the user's current permissions. + * The failure blocked actions rather than allowing them, so no data was exposed and every operation was still authorized on the server. + * (Ref: `functions_public_document_reads.py`, `get_public_document_actions`, `test_document_action_hint_seam.py`) + +#### Known Limitations + +* **Public Workspace Sharing Is Not Yet Available** + * Public workspace documents cannot yet be shared into other workspaces. The search index used by public workspaces has no field for shared access, so adding it requires an index schema change and a migration for existing deployments. Public workspaces show no share controls until that work is done. + +### **(v0.261.133)** + +#### New Features + +* **Native Public Workspace Document Management** + * Public workspace documents can now be managed in the native explorer, not just browsed: upload, metadata editing, tags and bulk tagging, permitted downloads, metadata extraction, reprocessing, and revision-aware deletion. Previously every one of these required switching to the classic page. + * Each operation names its workspace directly rather than depending on the account's stored selection, so changing workspaces mid-operation cannot redirect an upload, edit, or deletion, and an older server rejects the request instead of acting in the wrong place. + * Every operation receipt is checked against the workspace that was asked for, so a late or misrouted response cannot be shown as a successful change to the wrong workspace. + * Manager roles keep the content-manager boundary, ordinary members retain read access only, and locked or upload-disabled workspaces keep their existing restrictions. Queued work revalidates permission when it runs rather than trusting the moment it was requested. + * Sharing and generated-artifact approval remain on the classic page and arrive natively in a later release. + * (Ref: `functions_public_document_management.py`, `functions_public_document_policy.py`, `route_backend_public_document_management.py`, `documentOperations.ts`, [V2 Public Document Management](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_PUBLIC_DOCUMENT_MANAGEMENT.md), [Management APIs](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/PUBLIC_DOCUMENT_MANAGEMENT_APIS.md)) + +### **(v0.261.132)** + +#### New Features + +* **Native Public Workspace Document Browsing** + * Public workspace documents can be browsed in the same native explorer used for My Workspace and group workspaces, instead of only on the classic page. Filtering, sorting, pagination, facets, tags, document details, and version history all behave the way they already do elsewhere. + * Selecting a public workspace navigates by its identifier, and every document request names that workspace directly rather than depending on the account's stored selection. Returned documents are checked against the workspace that was asked for, so a late or misrouted response cannot populate the wrong workspace. + * The workspace is still recorded as the active selection, because chat document scoping and the classic page both read it. That step is deliberately non-blocking: if it fails, browsing still works, because nothing on the page depends on it. + * This release covers browsing only. Upload, metadata editing, tags, downloads, extraction, reprocessing, deletion, and approval remain on the classic page and arrive natively in later releases. + * (Ref: `functions_public_document_reads.py`, `functions_public_document_access.py`, `route_backend_public_document_reads.py`, `documentReadAdapter.ts`, `PublicWorkspacePage.tsx`, [V2 Public Document Browsing](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_PUBLIC_DOCUMENT_BROWSING.md), [Public Document Read APIs](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/PUBLIC_DOCUMENT_READ_APIS.md)) + +#### Bug Fixes + +* **Charts, Mermaid Diagrams, And Image Proposals In Orchestrated Answers** + * Fixed orchestrated requests such as "Plot BatteryVoltage1 over the last 15 minutes" answering that a plot could not be produced. No orchestration step had a chart tool, the retrieved rows never reached the answer, and the answer step had none of the chart, Mermaid, or image-proposal guidance ordinary chat uses. + * A requested chart is now drawn from the exact rows an action retrieved, by a chart step that can use only the built-in chart tools. Long series are reduced to the 200 points a chart shows while keeping each segment's highest and lowest values, and the chart subtitle states the sampling. + * The answer places those charts, writes Mermaid diagrams, and proposes images when image generation is enabled, including when an image would help but was not requested. Each image is generated only after the user approves its card. + * Saved Instruction memories shape these visuals, so "I don't like charts" stops charts you did not ask for. An explicit request in the current message still wins, and recalled facts never reach the chart step. + * (Ref: `functions_orchestration_visuals.py`, `functions_orchestration_actions.py`, `functions_orchestration_adapters.py`, `functions_chart_operations.py`, [Visual Outputs Fix](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/ORCHESTRATION_VISUAL_OUTPUTS_FIX.md)) + +#### User Interface Enhancements + +* **Image In Orchestrate** + * The composer's Image control now works in Orchestrate, where it asks the answer for at least one image proposal card instead of blocking the message. A status line explains that the images wait for approval. + * (Ref: `Composer.tsx`, [Chat controls](https://microsoft.github.io/simplechat/reference/chat-controls/)) + ### **(v0.261.131)** #### New Features @@ -1510,92 +1572,3 @@ part of the Chat group feature work. * **Live Reasoning Steps Were All Labelled "Thinking"** * The new interface read a step's title from a field the server does not send, so every step of a live response was labelled identically until the page was reloaded. Steps now carry the name the server gives them as they arrive. * (Ref: `chatStore.ts`, thought stream frames) - -### **(v0.261.056)** - -#### New Features - -* **Generated Images Can Be Changed Where They Are, Instead Of Regenerated** - * A generated image used to be final. Changing one meant asking again, which cost another paid generation and added another image to the thread — so refining an image a few times left the conversation full of near-duplicates with no way to tell which was current. - * Every generated image now has an **Edit** button, in the thread, in the full-size viewer, and on an approved image proposal card. The editor opens with four tabs: **Ask AI** for describing a change, **Prompt** for the wording that produced the image, **Controls** for shape, quality and background, and **History** for every version it has had. - * **You can point at the part you want changed.** Select a region with a box or a freehand brush and the change is applied there, leaving the rest of the image alone. The editor reports how much of the image is selected, and says plainly that the selection guides the model rather than fixing every pixel outside it. - * **Masking is not mouse-only.** A nine-region grid selects the same areas from the keyboard, producing exactly the shapes a drag would. - * **Nothing is ever deleted.** History shows every version as a thumbnail with who made it and why, holding **compare** reveals the previous one, and restoring an older version moves a pointer rather than discarding newer ones. The image the model originally produced is always kept. - * **Each change builds on the version you are looking at**, so successive edits accumulate instead of each one starting from the original. - * **If your image model cannot edit, the editor says so.** Region editing needs a `gpt-image` deployment; DALL·E 3 has no editing capability at all. On a model that cannot, the selection tools are hidden and the panel names the model or the API version setting responsible, rather than failing after you have selected a region and waited. - * Works in shared conversations as well as personal ones. Any participant can change an image, each change is attributed, and the edit is written through to the underlying image so the owner and exports see the same version; the other participants see it change as it happens. - * The classic interface shows whichever version is current, so a conversation read in either place shows the same image. - * (Ref: `functions_message_image_revisions.py`, `functions_image_edit.py`, `ImageEditor.tsx`, `ImageMaskCanvas.tsx`, `/api/message//image-revision`, `/api/collaboration/conversations//messages//image-revision`, [V2 Inline Image Editing](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_INLINE_IMAGE_EDITING.md)) - -* **Act On Several Conversations At Once In The Sidebar** - * Hovering a conversation in the chat sidebar now reveals a checkbox at its left edge. Ticking it starts a selection and a compact bar appears above the list offering **Pin**, **Hide**, **Export** and **Delete** for everything selected. Previously the only thing a selection could do was export, and pinning, hiding or deleting several conversations meant opening the same menu once per row. - * **Ctrl-click adds a conversation and Shift-click selects a run of them**, matching the workspace documents explorer — the two lists now share one implementation of those rules, so they cannot drift apart. - * **A plain click still opens a conversation.** Modifier keys select; an unmodified click opens and drops the selection. Once anything is selected every row shows its checkbox, so the state is visible before a click discards it. - * **The permanent "Select" button is gone**, so the sidebar is a line shorter at rest than it was before this feature was added. Checkboxes appear on touch devices permanently, since there is no hover to reveal them with. - * **Pin adapts to what is selected**: it offers Unpin only when everything selected is already pinned, and otherwise pins. Hiding stays one-way, matching the single-row action. - * **Shared conversations are handled correctly rather than skipped.** The bulk endpoints only cover personal conversations, so a mixed selection is split — and removing a shared conversation is a deletion for its owner and a departure for everybody else, chosen per conversation rather than assumed. - * `Escape` clears the selection, and `Ctrl+A` inside the sidebar selects everything loaded. - * (Ref: `ConversationRail.tsx`, `listSelection.ts`, `conversationSelection.ts`, `POST /api/delete_multiple_conversations`, `POST /api/conversations/bulk-pin`, `POST /api/conversations/bulk-hide`, [V2 Conversation Multi-Select](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_CONVERSATION_MULTI_SELECT.md)) - -#### Bug Fixes - -* **Images In Shared Conversations Now Display In The New Interface** - * An image in a shared conversation is served from a different address than one in a personal conversation, and the new interface only recognised the personal form. A shared image therefore rendered as "Image unavailable" rather than as a picture. - * (Ref: `images.ts`, `resolveImageSource`, collaboration image URLs) - -* **Deleting A Conversation Now Asks First** - * Deleting a conversation from the sidebar used to happen on a single click, with no confirmation and no way back — one mis-click on a menu destroyed a conversation and every message in it. - * Both the single-row delete and the new bulk delete now confirm, and the confirmation says what will really happen: a shared conversation you can only step out of is described as being left rather than deleted, and a mixed selection reports the two counts separately. - * (Ref: conversation deletion, `ConfirmDialog.tsx`, `ConversationRail.tsx`) - -* **A Sidebar Selection Can No Longer Outlive The Rows It Named** - * Searching or paging the conversation list left a stale selection behind, so a bulk action taken afterwards could apply to conversations that were no longer on screen. - * The selection is now pruned whenever the list reloads, whenever a conversation is removed or hidden, and when a shared conversation is taken away by its owner while you have it selected. Every bulk action also resolves its selection against the visible list before sending anything. - * (Ref: conversation list paging, selection pruning, `chatStore.ts`) - -* **Leaving A Shared Conversation Could Delete It For Everybody** - * The sidebar decided whether removing a shared conversation meant *leaving* it or *deleting* it from two copies of your permissions that were refreshed independently: opening a conversation and role-change events updated one, while the sidebar row kept the value it was loaded with. - * If you were promoted to owner while the conversation was open, the menu still offered **Leave** — but confirming posted a delete, destroying the conversation and all of its messages for every participant. A demoted owner hit the reverse, being told they had deleted something that still existed. - * Both copies are now kept in step, and the decision is made once and carried through to the request, so the wording you confirm is always the action performed. - * (Ref: shared conversation removal, `can_delete_conversation`, `conversationSelection.ts`, `chatStore.ts`) - -### **(v0.261.055)** - -#### User Interface Enhancements - -* **Admin Settings Has Moved Into The Account Menu In The V2 Interface** - * In the V2 interface, an administrator's left-hand menu listed **Admin Settings** alongside Chats, Agents and the workspaces, mixing the places you work with the place you administer. - * It now lives in the account menu at the bottom of the rail, under your name — the same place the classic interface keeps **App Settings**. The primary list is once again only the places you work. - * **Settings** in that menu is now **User Settings**, so the two entries a line apart say which is which, and the page it opens is titled to match. - * (Ref: V2 interface, left-hand menu, account menu, Admin Settings, personal settings) - -* **Your Profile Picture Now Appears In The V2 Interface** - * Your Microsoft 365 profile photo is already retrieved and stored when you first sign in, and the classic interface has always shown it. The V2 interface drew your initials instead. - * It now appears on the account control at the bottom of the left-hand menu and in the header of your **User Settings** page. Initials are still used when you have no photo. - * (Ref: V2 interface, account menu, User Settings, profile photo) - -#### Bug Fixes - -* **Custom Pages And External Links Now Collapse And Stay That Way In The V2 Interface** - * The **Custom Pages** and **External Links** groups in the V2 left-hand menu only became collapsible once a group held three or more links, and even then reopened on the next page load. A group put away never stayed away. - * Every group now collapses from its heading, whatever the link count, and the choice is remembered against your account. It is the same preference the classic interface uses, so a group you put away in one interface stays away in the other. - * Because every group in the V2 menu is now collapsible, **Force Menu Display** in Admin Settings no longer changes it. The setting continues to work in the classic interface. - * (Ref: V2 interface, left-hand menu, Custom Pages, External Links, `sidebarMenuState`, [V2 Sidebar Navigation And Account Menu](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/V2_SIDEBAR_NAV_AND_ACCOUNT_MENU_FIX.md)) - -* **The Account Menu Could Not Be Opened From The Collapsed V2 Menu** - * With the V2 left-hand menu collapsed to icons, clicking your account picture did nothing at all. Your settings, the way back to the classic interface and **Sign out** could only be reached by expanding the menu first, and nothing said so. - * The menu now opens beside the icon strip and names the account it belongs to. It also closes when you press Escape or click elsewhere, which it previously did not. - * (Ref: V2 interface, left-hand menu, collapsed rail, account menu) - -### **(v0.261.054)** - -#### Bug Fixes - -* **Image Generation No Longer Goes Quiet When You Look Away** - * Approving inline image proposals and then opening another conversation used to leave the cards looking untouched when you came back — no progress, **Approve** enabled again, **Approve all** back — even though those images were still being generated and did arrive a moment later. Pressing **Approve** again would have paid for the same image twice. - * **Approvals now keep reporting themselves wherever you go.** Leave the conversation, visit My Workspace, come back an hour later: the cards still show their queue position and "Generating image…", because that state no longer belongs to the view you left. - * **Reloading the page no longer loses track of them either.** Generation runs on the server and finishes whether or not your browser is still connected, so the only thing a reload ever really lost was the knowledge that it was coming. Restored cards say "Still generating from before the page reloaded…" and fill in as each image lands. - * **You can tell from anywhere in the app.** A conversation still generating images shows a spinner and a count on its row in the conversation list, and a single notice reports images being generated whose cards are not on screen — whether you have moved to another conversation or left chat entirely. The notice is deliberately absent while you are looking at those cards, where it would only repeat what they already say. - * **An approval that never finishes is written off rather than left spinning.** After ten minutes the card explains that the page reloaded while it was generating and offers to approve it again. - * This is per browser tab. An image approved in one tab is not tracked in another, though the image itself is stored and appears there as soon as the conversation is read. - * (Ref: `imageProposalStore.ts`, `imageProposalTracking.ts`, `imageProposalResume.ts`, `GET /api/chat/image-proposals/status/`, [V2 Inline Image Proposal Resume](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/V2_INLINE_IMAGE_PROPOSAL_RESUME_FIX.md)) diff --git a/docs/explanation/release-notes/v0.261-part-6.md b/docs/explanation/release-notes/v0.261-part-6.md index 310c9b30d..54d92b89e 100644 --- a/docs/explanation/release-notes/v0.261-part-6.md +++ b/docs/explanation/release-notes/v0.261-part-6.md @@ -1,16 +1,105 @@ --- -title: "Release notes 0.261.053 – 0.261.014" -description: "SimpleChat release notes for 0.261.053 – 0.261.014." +title: "Release notes 0.261.056 – 0.261.017" +description: "SimpleChat release notes for 0.261.056 – 0.261.017." section: "Reference" layout: page --- -# Release notes 0.261.053 – 0.261.014 +# Release notes 0.261.056 – 0.261.017 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(v0.261.056)** + +#### New Features + +* **Generated Images Can Be Changed Where They Are, Instead Of Regenerated** + * A generated image used to be final. Changing one meant asking again, which cost another paid generation and added another image to the thread — so refining an image a few times left the conversation full of near-duplicates with no way to tell which was current. + * Every generated image now has an **Edit** button, in the thread, in the full-size viewer, and on an approved image proposal card. The editor opens with four tabs: **Ask AI** for describing a change, **Prompt** for the wording that produced the image, **Controls** for shape, quality and background, and **History** for every version it has had. + * **You can point at the part you want changed.** Select a region with a box or a freehand brush and the change is applied there, leaving the rest of the image alone. The editor reports how much of the image is selected, and says plainly that the selection guides the model rather than fixing every pixel outside it. + * **Masking is not mouse-only.** A nine-region grid selects the same areas from the keyboard, producing exactly the shapes a drag would. + * **Nothing is ever deleted.** History shows every version as a thumbnail with who made it and why, holding **compare** reveals the previous one, and restoring an older version moves a pointer rather than discarding newer ones. The image the model originally produced is always kept. + * **Each change builds on the version you are looking at**, so successive edits accumulate instead of each one starting from the original. + * **If your image model cannot edit, the editor says so.** Region editing needs a `gpt-image` deployment; DALL·E 3 has no editing capability at all. On a model that cannot, the selection tools are hidden and the panel names the model or the API version setting responsible, rather than failing after you have selected a region and waited. + * Works in shared conversations as well as personal ones. Any participant can change an image, each change is attributed, and the edit is written through to the underlying image so the owner and exports see the same version; the other participants see it change as it happens. + * The classic interface shows whichever version is current, so a conversation read in either place shows the same image. + * (Ref: `functions_message_image_revisions.py`, `functions_image_edit.py`, `ImageEditor.tsx`, `ImageMaskCanvas.tsx`, `/api/message//image-revision`, `/api/collaboration/conversations//messages//image-revision`, [V2 Inline Image Editing](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_INLINE_IMAGE_EDITING.md)) + +* **Act On Several Conversations At Once In The Sidebar** + * Hovering a conversation in the chat sidebar now reveals a checkbox at its left edge. Ticking it starts a selection and a compact bar appears above the list offering **Pin**, **Hide**, **Export** and **Delete** for everything selected. Previously the only thing a selection could do was export, and pinning, hiding or deleting several conversations meant opening the same menu once per row. + * **Ctrl-click adds a conversation and Shift-click selects a run of them**, matching the workspace documents explorer — the two lists now share one implementation of those rules, so they cannot drift apart. + * **A plain click still opens a conversation.** Modifier keys select; an unmodified click opens and drops the selection. Once anything is selected every row shows its checkbox, so the state is visible before a click discards it. + * **The permanent "Select" button is gone**, so the sidebar is a line shorter at rest than it was before this feature was added. Checkboxes appear on touch devices permanently, since there is no hover to reveal them with. + * **Pin adapts to what is selected**: it offers Unpin only when everything selected is already pinned, and otherwise pins. Hiding stays one-way, matching the single-row action. + * **Shared conversations are handled correctly rather than skipped.** The bulk endpoints only cover personal conversations, so a mixed selection is split — and removing a shared conversation is a deletion for its owner and a departure for everybody else, chosen per conversation rather than assumed. + * `Escape` clears the selection, and `Ctrl+A` inside the sidebar selects everything loaded. + * (Ref: `ConversationRail.tsx`, `listSelection.ts`, `conversationSelection.ts`, `POST /api/delete_multiple_conversations`, `POST /api/conversations/bulk-pin`, `POST /api/conversations/bulk-hide`, [V2 Conversation Multi-Select](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/features/V2_CONVERSATION_MULTI_SELECT.md)) + +#### Bug Fixes + +* **Images In Shared Conversations Now Display In The New Interface** + * An image in a shared conversation is served from a different address than one in a personal conversation, and the new interface only recognised the personal form. A shared image therefore rendered as "Image unavailable" rather than as a picture. + * (Ref: `images.ts`, `resolveImageSource`, collaboration image URLs) + +* **Deleting A Conversation Now Asks First** + * Deleting a conversation from the sidebar used to happen on a single click, with no confirmation and no way back — one mis-click on a menu destroyed a conversation and every message in it. + * Both the single-row delete and the new bulk delete now confirm, and the confirmation says what will really happen: a shared conversation you can only step out of is described as being left rather than deleted, and a mixed selection reports the two counts separately. + * (Ref: conversation deletion, `ConfirmDialog.tsx`, `ConversationRail.tsx`) + +* **A Sidebar Selection Can No Longer Outlive The Rows It Named** + * Searching or paging the conversation list left a stale selection behind, so a bulk action taken afterwards could apply to conversations that were no longer on screen. + * The selection is now pruned whenever the list reloads, whenever a conversation is removed or hidden, and when a shared conversation is taken away by its owner while you have it selected. Every bulk action also resolves its selection against the visible list before sending anything. + * (Ref: conversation list paging, selection pruning, `chatStore.ts`) + +* **Leaving A Shared Conversation Could Delete It For Everybody** + * The sidebar decided whether removing a shared conversation meant *leaving* it or *deleting* it from two copies of your permissions that were refreshed independently: opening a conversation and role-change events updated one, while the sidebar row kept the value it was loaded with. + * If you were promoted to owner while the conversation was open, the menu still offered **Leave** — but confirming posted a delete, destroying the conversation and all of its messages for every participant. A demoted owner hit the reverse, being told they had deleted something that still existed. + * Both copies are now kept in step, and the decision is made once and carried through to the request, so the wording you confirm is always the action performed. + * (Ref: shared conversation removal, `can_delete_conversation`, `conversationSelection.ts`, `chatStore.ts`) + +### **(v0.261.055)** + +#### User Interface Enhancements + +* **Admin Settings Has Moved Into The Account Menu In The V2 Interface** + * In the V2 interface, an administrator's left-hand menu listed **Admin Settings** alongside Chats, Agents and the workspaces, mixing the places you work with the place you administer. + * It now lives in the account menu at the bottom of the rail, under your name — the same place the classic interface keeps **App Settings**. The primary list is once again only the places you work. + * **Settings** in that menu is now **User Settings**, so the two entries a line apart say which is which, and the page it opens is titled to match. + * (Ref: V2 interface, left-hand menu, account menu, Admin Settings, personal settings) + +* **Your Profile Picture Now Appears In The V2 Interface** + * Your Microsoft 365 profile photo is already retrieved and stored when you first sign in, and the classic interface has always shown it. The V2 interface drew your initials instead. + * It now appears on the account control at the bottom of the left-hand menu and in the header of your **User Settings** page. Initials are still used when you have no photo. + * (Ref: V2 interface, account menu, User Settings, profile photo) + +#### Bug Fixes + +* **Custom Pages And External Links Now Collapse And Stay That Way In The V2 Interface** + * The **Custom Pages** and **External Links** groups in the V2 left-hand menu only became collapsible once a group held three or more links, and even then reopened on the next page load. A group put away never stayed away. + * Every group now collapses from its heading, whatever the link count, and the choice is remembered against your account. It is the same preference the classic interface uses, so a group you put away in one interface stays away in the other. + * Because every group in the V2 menu is now collapsible, **Force Menu Display** in Admin Settings no longer changes it. The setting continues to work in the classic interface. + * (Ref: V2 interface, left-hand menu, Custom Pages, External Links, `sidebarMenuState`, [V2 Sidebar Navigation And Account Menu](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/V2_SIDEBAR_NAV_AND_ACCOUNT_MENU_FIX.md)) + +* **The Account Menu Could Not Be Opened From The Collapsed V2 Menu** + * With the V2 left-hand menu collapsed to icons, clicking your account picture did nothing at all. Your settings, the way back to the classic interface and **Sign out** could only be reached by expanding the menu first, and nothing said so. + * The menu now opens beside the icon strip and names the account it belongs to. It also closes when you press Escape or click elsewhere, which it previously did not. + * (Ref: V2 interface, left-hand menu, collapsed rail, account menu) + +### **(v0.261.054)** + +#### Bug Fixes + +* **Image Generation No Longer Goes Quiet When You Look Away** + * Approving inline image proposals and then opening another conversation used to leave the cards looking untouched when you came back — no progress, **Approve** enabled again, **Approve all** back — even though those images were still being generated and did arrive a moment later. Pressing **Approve** again would have paid for the same image twice. + * **Approvals now keep reporting themselves wherever you go.** Leave the conversation, visit My Workspace, come back an hour later: the cards still show their queue position and "Generating image…", because that state no longer belongs to the view you left. + * **Reloading the page no longer loses track of them either.** Generation runs on the server and finishes whether or not your browser is still connected, so the only thing a reload ever really lost was the knowledge that it was coming. Restored cards say "Still generating from before the page reloaded…" and fill in as each image lands. + * **You can tell from anywhere in the app.** A conversation still generating images shows a spinner and a count on its row in the conversation list, and a single notice reports images being generated whose cards are not on screen — whether you have moved to another conversation or left chat entirely. The notice is deliberately absent while you are looking at those cards, where it would only repeat what they already say. + * **An approval that never finishes is written off rather than left spinning.** After ten minutes the card explains that the page reloaded while it was generating and offers to approve it again. + * This is per browser tab. An image approved in one tab is not tracked in another, though the image itself is stored and appears there as soon as the conversation is read. + * (Ref: `imageProposalStore.ts`, `imageProposalTracking.ts`, `imageProposalResume.ts`, `GET /api/chat/image-proposals/status/`, [V2 Inline Image Proposal Resume](https://github.com/microsoft/simplechat/blob/13db876626a043e2c7b30f054354f07c9a1633b9/docs/explanation/fixes/V2_INLINE_IMAGE_PROPOSAL_RESUME_FIX.md)) + {% raw %} ### **(v0.261.053)** @@ -872,75 +961,3 @@ Tracking: [#1489](https://github.com/microsoft/simplechat/issues/1489); implemen * Added a **Use this URL exactly as entered** option for gateways that serve the API at a path SimpleChat cannot infer. * **Test Connection now reports the URL that was actually called.** URL normalization rewrites the configured endpoint, and that rewrite was previously invisible, so a misdirected request looked identical to a correct one. * (Ref: `model_endpoint_clients.py`, `route_backend_models.py`, `_multiendpoint_modal.html`, `admin_model_endpoints.js`, `workspace_model_endpoints.js`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) - -### **(v0.261.016)** - -#### Bug Fixes - -* **Document Search Failed In V2** - * Turning on **Documents** in the V2 chat page ended with "Something went wrong while streaming the response", while the same search worked in the classic interface. - * Two request problems were behind it. First, V2 identified the chosen model by its deployment name alone, but when multiple model endpoints are configured a name is not unique — the server could not resolve the selection and quietly fell back to a different endpoint than the one shown in the picker. Second, the document scope was fixed to "all workspaces" but the workspace identifiers were never sent, so the search was asked to cover workspaces it was given no way to look in. - * V2 now sends the full model identity with every message and retry, so the model you pick is the model that answers. The document scope is worked out from the workspaces actually in use — personal only, or personal plus the group or public workspace you are working in — and the matching identifiers travel with it. - * (Ref: V2 model selection, document search scope, `/api/chat/stream`) - -* **Custom Model Endpoint Failures Are Now Diagnosable** - * Every Custom endpoint failure produced the same sentence — "Custom model request failed." — with the underlying cause discarded before it reached the log. A wrong path, a wrong API key, a wrong model name, a TLS failure, and a blocked address were indistinguishable, and nothing anywhere explained which had happened. - * The browser message stays sanitized, because an upstream error body can echo back a URL, a header, or an API key. The real cause is now recorded server-side with the API type, the resolved request URL, the upstream status code, and the upstream error body. - * Both the message and the log entry now carry a short reference id, so an administrator can join the message a user reports to the log entry that explains it. - * Credentials are redacted before anything is written to the log, covering API keys, bearer tokens, `x-api-key`, `x-goog-api-key`, and key query parameters. A logging failure never replaces the original error. - * The resolved request URL is included deliberately: URL normalization can rewrite what the administrator typed, and that rewrite was previously invisible. - * (Ref: `functions_model_endpoint_diagnostics.py`, `model_endpoint_clients.py`, `functions_model_endpoint_runtime.py`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) - -### **(v0.261.015)** - -#### New Features - -* **V2 Conversation Details Rebuilt** - * The details panel previously listed every tag as one undifferentiated row of chips, which mixed documents in with model names, participants and topics, and reduced source documents to a bare count. - * Each kind of information now has its own section: **Source documents**, **Workspaces**, **Models and agents**, **Participants**, **Topics** and **Web sources**. - * **Source documents** are listed properly — file name, the pages or sheets used, how many excerpts, workspace and classification — and marked as **cited** when a response actually referenced them. The list is paged, since a long conversation can draw on dozens of documents. - * Conversations that predate citation tracking say so, rather than showing every document as though it went unused. - * A conversation **summary** can now be generated on demand from the panel, and regenerated later, showing the model used and when it was produced. - * (Ref: V2 conversation details, `/api/conversations//metadata`, `/api/conversations//summary`) - -* **Custom Model Endpoints Now Support Google Gemini, Through A Provider Registry** - * Custom endpoints supported exactly three API types, and each one was hard-coded in five separate places: the allowlist, the request-model resolver, the protocol inference chain, the admin template's option list, and the admin JavaScript. Adding a provider meant editing all five and hoping none were missed. - * An API type is now a single declarative registry entry that carries its wire protocol, which field names the model, how its URL is built, which authentication types it accepts, and which version field applies. The admin API Type list, the model identifier label, and the version fields all render from that registry. - * **Google Gemini is now selectable as a Custom endpoint API type**, reached through its OpenAI-compatible surface so it still runs on SimpleChat's validated-DNS pinned transport. - * Fixed URL handling for endpoints that already carry a version segment. SimpleChat appended `/v1` unconditionally, which turned Gemini's `…/v1beta/openai` base into `…/v1beta/openai/v1` and produced a 404. URL construction is now per-provider, so `/v1` is appended only where it belongs. - * The three existing API types are unchanged, and an unregistered API type is still refused. - * (Ref: `functions_model_endpoint_providers.py`, `functions_model_endpoint_types.py`, `model_endpoint_clients.py`, `admin_model_endpoints.js`, `workspace_model_endpoints.js`, `_multiendpoint_modal.html`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) - -#### User Interface Enhancements - -* **Composer Controls Appear When They Are Relevant** - * **Read URLs** now appears only once the message you are writing actually contains a URL, and **Deep research** only when there is something for it to research — web search being on, or URLs in the message. - * Turning on **Image generation** now disables the controls it is incompatible with and hides the model picker, since the request goes to an image endpoint that ignores them. - * A control that disappears also clears its setting, so a request never carries a capability you can no longer see you enabled. - * Together this removes the permanently crowded row of buttons in the composer. - * (Ref: V2 composer gating) - -* **Chat Width Can Be Changed** - * A control in the chat header switches between a comfortable reading width and using the full width of the pane. The choice is remembered. - * The message thread and the composer widen together, so the composer controls get the extra room too. - * (Ref: V2 chat width) - -### **(v0.261.014)** - -#### New Features - -* **Message Masking In V2** - * You can now mask content in the V2 chat page, which the classic interface has always supported. Masked content is withheld from the AI model when it builds conversation history, so it is a real protection rather than a display setting. - * Select any text in a message and a small **Mask selection** control appears over it. The selected text is replaced with a redaction marker. - * The hover row also offers masking the whole message, and clearing masks once any exist. - * Hovering a redaction shows who applied it and when, so in a shared conversation it is clear whose masking you are looking at. - * Masked text is removed before the message is rendered rather than hidden with styling, so it is never present in the page. - * If a selection cannot be matched back to the stored message — which can happen when it spans citations or formatting — the interface says so instead of appearing to succeed. - * (Ref: V2 message masking, `/api/message//mask`) - -* **Model Capabilities Now Come From The Model Catalog Instead Of The Model's Name** - * SimpleChat previously worked out what a model could do by pattern-matching its name, so a model the catalog did not know about — an on-premises or customer-supplied model reached through a Custom endpoint — silently received wrong answers for vision, tool calling, streaming, and reasoning. A model named `corp-llm-v2` was treated as having no capabilities at all. - * Capability answers now resolve through a precedence chain: a per-model override, then an endpoint-level override, then the shipped model catalog, then the original name heuristics. Administrators can describe a model the catalog has never heard of without waiting for a catalog update, and models absent from the catalog behave exactly as before. - * The catalog gained `supportsStreaming` and `reasoning` flags for every model, and now covers Google Gemini, which had no entries at all. Claude, Llama 4, and Phi-4 multimodal models are correctly recognised as vision-capable for the first time; the `-chat` variants of the GPT-5.x families are correctly recognised as not vision-capable. - * The catalog is now validated against a published JSON schema, so a malformed or incomplete model record fails a test rather than silently degrading capability answers at runtime. - * (Ref: `functions_model_capabilities.py`, `model_capabilities.json`, `model_capabilities.schema.json`, `MODEL_CAPABILITY_CATALOG.md`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) diff --git a/docs/explanation/release-notes/v0.261-part-7.md b/docs/explanation/release-notes/v0.261-part-7.md index bad987360..c1a1b75f0 100644 --- a/docs/explanation/release-notes/v0.261-part-7.md +++ b/docs/explanation/release-notes/v0.261-part-7.md @@ -1,16 +1,88 @@ --- -title: "Release notes 0.261.013 – 0.261.001" -description: "SimpleChat release notes for 0.261.013 – 0.261.001." +title: "Release notes 0.261.016 – 0.261.001" +description: "SimpleChat release notes for 0.261.016 – 0.261.001." section: "Reference" layout: page --- -# Release notes 0.261.013 – 0.261.001 +# Release notes 0.261.016 – 0.261.001 [Back to release notes index]({{ '/explanation/release_notes/' | relative_url }}) +### **(v0.261.016)** + +#### Bug Fixes + +* **Document Search Failed In V2** + * Turning on **Documents** in the V2 chat page ended with "Something went wrong while streaming the response", while the same search worked in the classic interface. + * Two request problems were behind it. First, V2 identified the chosen model by its deployment name alone, but when multiple model endpoints are configured a name is not unique — the server could not resolve the selection and quietly fell back to a different endpoint than the one shown in the picker. Second, the document scope was fixed to "all workspaces" but the workspace identifiers were never sent, so the search was asked to cover workspaces it was given no way to look in. + * V2 now sends the full model identity with every message and retry, so the model you pick is the model that answers. The document scope is worked out from the workspaces actually in use — personal only, or personal plus the group or public workspace you are working in — and the matching identifiers travel with it. + * (Ref: V2 model selection, document search scope, `/api/chat/stream`) + +* **Custom Model Endpoint Failures Are Now Diagnosable** + * Every Custom endpoint failure produced the same sentence — "Custom model request failed." — with the underlying cause discarded before it reached the log. A wrong path, a wrong API key, a wrong model name, a TLS failure, and a blocked address were indistinguishable, and nothing anywhere explained which had happened. + * The browser message stays sanitized, because an upstream error body can echo back a URL, a header, or an API key. The real cause is now recorded server-side with the API type, the resolved request URL, the upstream status code, and the upstream error body. + * Both the message and the log entry now carry a short reference id, so an administrator can join the message a user reports to the log entry that explains it. + * Credentials are redacted before anything is written to the log, covering API keys, bearer tokens, `x-api-key`, `x-goog-api-key`, and key query parameters. A logging failure never replaces the original error. + * The resolved request URL is included deliberately: URL normalization can rewrite what the administrator typed, and that rewrite was previously invisible. + * (Ref: `functions_model_endpoint_diagnostics.py`, `model_endpoint_clients.py`, `functions_model_endpoint_runtime.py`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) + +### **(v0.261.015)** + +#### New Features + +* **V2 Conversation Details Rebuilt** + * The details panel previously listed every tag as one undifferentiated row of chips, which mixed documents in with model names, participants and topics, and reduced source documents to a bare count. + * Each kind of information now has its own section: **Source documents**, **Workspaces**, **Models and agents**, **Participants**, **Topics** and **Web sources**. + * **Source documents** are listed properly — file name, the pages or sheets used, how many excerpts, workspace and classification — and marked as **cited** when a response actually referenced them. The list is paged, since a long conversation can draw on dozens of documents. + * Conversations that predate citation tracking say so, rather than showing every document as though it went unused. + * A conversation **summary** can now be generated on demand from the panel, and regenerated later, showing the model used and when it was produced. + * (Ref: V2 conversation details, `/api/conversations//metadata`, `/api/conversations//summary`) + +* **Custom Model Endpoints Now Support Google Gemini, Through A Provider Registry** + * Custom endpoints supported exactly three API types, and each one was hard-coded in five separate places: the allowlist, the request-model resolver, the protocol inference chain, the admin template's option list, and the admin JavaScript. Adding a provider meant editing all five and hoping none were missed. + * An API type is now a single declarative registry entry that carries its wire protocol, which field names the model, how its URL is built, which authentication types it accepts, and which version field applies. The admin API Type list, the model identifier label, and the version fields all render from that registry. + * **Google Gemini is now selectable as a Custom endpoint API type**, reached through its OpenAI-compatible surface so it still runs on SimpleChat's validated-DNS pinned transport. + * Fixed URL handling for endpoints that already carry a version segment. SimpleChat appended `/v1` unconditionally, which turned Gemini's `…/v1beta/openai` base into `…/v1beta/openai/v1` and produced a 404. URL construction is now per-provider, so `/v1` is appended only where it belongs. + * The three existing API types are unchanged, and an unregistered API type is still refused. + * (Ref: `functions_model_endpoint_providers.py`, `functions_model_endpoint_types.py`, `model_endpoint_clients.py`, `admin_model_endpoints.js`, `workspace_model_endpoints.js`, `_multiendpoint_modal.html`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) + +#### User Interface Enhancements + +* **Composer Controls Appear When They Are Relevant** + * **Read URLs** now appears only once the message you are writing actually contains a URL, and **Deep research** only when there is something for it to research — web search being on, or URLs in the message. + * Turning on **Image generation** now disables the controls it is incompatible with and hides the model picker, since the request goes to an image endpoint that ignores them. + * A control that disappears also clears its setting, so a request never carries a capability you can no longer see you enabled. + * Together this removes the permanently crowded row of buttons in the composer. + * (Ref: V2 composer gating) + +* **Chat Width Can Be Changed** + * A control in the chat header switches between a comfortable reading width and using the full width of the pane. The choice is remembered. + * The message thread and the composer widen together, so the composer controls get the extra room too. + * (Ref: V2 chat width) + +### **(v0.261.014)** + +#### New Features + +* **Message Masking In V2** + * You can now mask content in the V2 chat page, which the classic interface has always supported. Masked content is withheld from the AI model when it builds conversation history, so it is a real protection rather than a display setting. + * Select any text in a message and a small **Mask selection** control appears over it. The selected text is replaced with a redaction marker. + * The hover row also offers masking the whole message, and clearing masks once any exist. + * Hovering a redaction shows who applied it and when, so in a shared conversation it is clear whose masking you are looking at. + * Masked text is removed before the message is rendered rather than hidden with styling, so it is never present in the page. + * If a selection cannot be matched back to the stored message — which can happen when it spans citations or formatting — the interface says so instead of appearing to succeed. + * (Ref: V2 message masking, `/api/message//mask`) + +* **Model Capabilities Now Come From The Model Catalog Instead Of The Model's Name** + * SimpleChat previously worked out what a model could do by pattern-matching its name, so a model the catalog did not know about — an on-premises or customer-supplied model reached through a Custom endpoint — silently received wrong answers for vision, tool calling, streaming, and reasoning. A model named `corp-llm-v2` was treated as having no capabilities at all. + * Capability answers now resolve through a precedence chain: a per-model override, then an endpoint-level override, then the shipped model catalog, then the original name heuristics. Administrators can describe a model the catalog has never heard of without waiting for a catalog update, and models absent from the catalog behave exactly as before. + * The catalog gained `supportsStreaming` and `reasoning` flags for every model, and now covers Google Gemini, which had no entries at all. Claude, Llama 4, and Phi-4 multimodal models are correctly recognised as vision-capable for the first time; the `-chat` variants of the GPT-5.x families are correctly recognised as not vision-capable. + * The catalog is now validated against a published JSON schema, so a malformed or incomplete model record fails a test rather than silently degrading capability answers at runtime. + * (Ref: `functions_model_capabilities.py`, `model_capabilities.json`, `model_capabilities.schema.json`, `MODEL_CAPABILITY_CATALOG.md`, [#1228](https://github.com/microsoft/simplechat/pull/1228)) + ### **(v0.261.013)** #### New Features diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md index d529d110d..3a245d2c9 100644 --- a/docs/explanation/release_notes.md +++ b/docs/explanation/release_notes.md @@ -2,6 +2,22 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes). +### **(v0.261.421)** + +#### Bug Fixes + +* **Selected-Document Orchestration Tool Loading** + * Fixed document-backed runs failing before their first read because plugin discovery created a different copy of the document-reader class than the source-binding safety check expected. + * Shared discovery now reuses registered plugin classes while preserving separate instances, current permissions, selected-file restrictions, and native-inspection limits. + * Explicitly pins the existing Handlebars template-parser dependency so fresh container installations do not report a successful install and then fail to import Semantic Kernel. + * Added production-loader, fresh-start, concurrent-user, and config-derived supported-format retrieval regressions instead of substituting a simplified loader in the end-to-end test. + * (Ref: `semantic_kernel_plugins/plugin_loader.py`, `functions_orchestration_actions.py`, `docs/explanation/fixes/ORCHESTRATION_PLUGIN_DISCOVERY_IDENTITY_FIX.md`) + +* **Action Failure Reasons Retained Through Recovery Details** + * Tool initialization, document-reader identity, failed document reads, and missing function results now retain distinct safe reasons in saved steps and the final failure explanation. + * The first action error includes hashed run/step correlation with debug logging on or off, without exposing private exception text. Code faults are not automatically retried; existing permitted manual recovery remains available. + * (Ref: `functions_orchestration_adapters.py`, `functions_orchestration_schema.py`, `docs/reference/logging-tags.md`) + ### **(v0.261.410)** #### New Features @@ -11,7 +27,7 @@ For feature-focused and fix-focused drill-downs by version, see [Features by Ver * Built-in model selections preconfigure canonical model identity while retaining automatic exact name matching, deployment names, explicit overrides, and publication choices. * Preserved Custom/APIM base paths, authentication headers, and request aliases. MAI keeps its native Chat Completions contract even when the shared connection prefers Responses for compatible OpenAI-style models. * Added the verified MAI profile, generation limits, token audience, and setup guidance without changing existing chat, planner, image, or embedding defaults. - * (Ref: `model_endpoint_clients.py`, `functions_model_endpoint_runtime.py`, `functions_model_capabilities.py`, `ModelConnectionsManager.tsx`, `model_capabilities.json`, [AI connection configuration]({{ '/guides/configure-ai-connections/' | relative_url }}#microsoft-mai-text-models), [Microsoft MAI Connection Compatibility Fix](fixes/MICROSOFT_MAI_MODEL_CONNECTIONS_FIX.md)) + * (Ref: `model_endpoint_clients.py`, `functions_model_endpoint_runtime.py`, `functions_model_capabilities.py`, `ModelConnectionsManager.tsx`, `model_capabilities.json`, [AI connection configuration]({{ '/guides/configure-ai-connections/' | relative_url }}#microsoft-mai-text-models), `docs/explanation/fixes/MICROSOFT_MAI_MODEL_CONNECTIONS_FIX.md`) ### **(v0.261.408)** diff --git a/docs/reference/actions/document-search.md b/docs/reference/actions/document-search.md index 99b3a003e..2729da3be 100644 --- a/docs/reference/actions/document-search.md +++ b/docs/reference/actions/document-search.md @@ -77,6 +77,22 @@ A denied or unavailable report is not a successful inspection; a missing propert inside an available report must still be distinguished from an inaccessible one. This does not add native-report access for chat-only attachments. +### Loading failures and recovery + +From **0.261.421** (`application/single_app/config.py`), shared plugin discovery +reuses the registered Python module instead of creating another copy of the +document reader. This fixes selected-document actions being rejected before their +first read even when the plan and source permissions were valid. + +Run details distinguish a tool initialization or reader identity failure from a +failed document read and an action that returned no function results. For a reader +identity failure, ask an administrator to repair/update the application before +retrying; changing the model, increasing timeouts, or re-uploading the same source +does not fix that loading defect. + +The source checks and native-inspection limits are unchanged. A missing report or +unavailable native property does not become evidence of absence or compliance. + ### Paged Excel inspection Implemented in version: **0.261.408** (`application/single_app/config.py`). diff --git a/docs/reference/logging-tags.md b/docs/reference/logging-tags.md index ed99b8db9..641cb3837 100644 --- a/docs/reference/logging-tags.md +++ b/docs/reference/logging-tags.md @@ -42,6 +42,52 @@ and capability codes. A cancelled stream is informational, not an error. Orchestration records an outcome only after its saved state is confirmed; replaying a cached response does not record another execution. +## Plugin discovery and action failures + +Implemented in version **0.261.421**, tracked in +`application/single_app/config.py`. + +`[PLUGIN_LOADER] Plugin module could not be loaded.` is a Warning with the +installed module's `sc_plugin_name` and safe `sc_error_type`. Discovery continues +for other modules; the exception's message is not logged. + +`[ORCHESTRATION_ADAPTERS] Direct action execution failed.` is an Error that +records the first failed action's `sc_failure_code`, `sc_stage`, +`sc_capability_id`, `sc_error_type`, and hashed conversation/run/step identifiers. +These records are available with debug logging disabled. They distinguish the +action failure from later steps that did not run because their dependency failed. + +| Failure code | Stage | Meaning | +| --- | --- | --- | +| `action_initialization_failed` | `action_initialization` | Tools could not load, expose functions, or use the selected model's function-calling capabilities. | +| `document_reader_identity_mismatch` | `action_initialization` | The loaded plugin was not the application's verified document-reader class; no document-reading function ran. | +| `document_action_read_failed` | `document_action_read` | A bound document function returned a failed read, not successful findings. | +| `action_results_missing` | `action_results` | The action returned no usable function results, so none were retained as findings. | + +`sc_function_calls_attempted` is false for the two initialization classifications +and true for a failed document-function result. It is omitted where that fact is +unknown, including provider errors and missing-result cases. Other failures keep +their existing provider/cancellation/authority classifications. The new codes do +not enable automatic transient retries or disable permitted manual recovery. + +To inspect a specific failed run without logging its raw identifier: + +```kusto +let runHash = hash_sha256(""); +AppTraces +| where TimeGenerated > ago(2h) +| where tostring(Properties.sc_run_id_hash) == runHash +| where tostring(Properties.sc_message) == "[ORCHESTRATION_ADAPTERS] Direct action execution failed." +| project TimeGenerated, + code=tostring(Properties.sc_failure_code), + stage=tostring(Properties.sc_stage), + stepHash=tostring(Properties.sc_step_id_hash), + functionsAttempted=tostring(Properties.sc_function_calls_attempted), + errorType=tostring(Properties.sc_error_type) +| order by TimeGenerated asc +| take 50 +``` + ## SQL connection diagnostics Since **0.261.351**, `[SQL_CONNECTION]` records connection-check and runtime @@ -404,6 +450,7 @@ for availability states and live-UI validation limits. - `[PLUGIN_FUNCTION_LOGGER]` - `[PLUGIN_HEALTH]` - `[PLUGIN_INVOCATION]` +- `[PLUGIN_LOADER]` - `[PLUGIN_LOGGING_API]` - `[PLUGIN_RECOVERY]` - `[PLUGIN_REPAIR]` diff --git a/functional_tests/test_orchestration_document_action_end_to_end.py b/functional_tests/test_orchestration_document_action_end_to_end.py index 8e8bf20aa..07667fa07 100644 --- a/functional_tests/test_orchestration_document_action_end_to_end.py +++ b/functional_tests/test_orchestration_document_action_end_to_end.py @@ -1,12 +1,14 @@ # test_orchestration_document_action_end_to_end.py """ Authenticated native-document orchestration from HTTP submission to saved answer. -Version: 0.261.420 +Version: 0.261.421 Implemented in: 0.261.419 +Production plugin discovery coverage added in: 0.261.421 -Real routes, approval/leases, native action filters, report/citation projection, -retention, composition, completeness and publication run with external storage, -configuration and model I/O isolated. No production service or data is touched. +Real routes, plugin discovery, approval/leases, native action filters, +report/citation projection, retention, composition, completeness and publication +run with external storage, configuration and model I/O isolated. No production +service or data is touched. """ import asyncio @@ -57,8 +59,6 @@ def native_http(source_runtime, modules, monkeypatch): settings_owner = importlib.import_module("functions_settings") governance = importlib.import_module("functions_governance") runner = importlib.import_module("functions_orchestration_actions") - loader = importlib.import_module("semantic_kernel_plugins.logged_plugin_loader") - plugin = importlib.import_module("semantic_kernel_plugins.document_search_plugin") search = importlib.import_module("functions_search_service") telemetry = importlib.import_module("functions_appinsights") logger = logging.getLogger("test.orchestration.native.http") @@ -84,7 +84,6 @@ def native_http(source_runtime, modules, monkeypatch): monkeypatch.setattr( importlib.import_module("functions_group"), "get_user_groups", lambda user_id: [], ) - monkeypatch.setattr(loader, "discover_plugins", lambda: {"DocumentSearchPlugin": plugin.DocumentSearchPlugin}) monkeypatch.setattr(telemetry, "_load_logging_settings", lambda: { "enable_debug_logging": harness.settings.get("enable_debug_logging", False), }) diff --git a/functional_tests/test_orchestration_document_loader_reliability.py b/functional_tests/test_orchestration_document_loader_reliability.py new file mode 100644 index 000000000..a1612961f --- /dev/null +++ b/functional_tests/test_orchestration_document_loader_reliability.py @@ -0,0 +1,290 @@ +# test_orchestration_document_loader_reliability.py +""" +Functional tests for shared document-action loading and safe failure outcomes. +Version: 0.261.421 +Implemented in: 0.261.421 + +Real authenticated routes, discovery, source checks, SDK-shaped document stores, +chunk readers, action filters, retention, and final publication run offline. +The all-format matrix reads already indexed text; it does not claim that every +format exposes native Office/PDF metadata or that extraction is tested here. +""" + +import hashlib +import importlib +import importlib.util +import json +import logging +from copy import deepcopy +from types import SimpleNamespace + +import pytest +from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion +from semantic_kernel.contents import AuthorRole, ChatMessageContent, FunctionCallContent + +from functions_task_completeness import NARRATIVE_OBLIGATIONS +from test_orchestration_document_action_end_to_end import ( + LEFT, PRIVATE, frames, modules, native_http, real_http_harness, source_runtime, +) +from test_orchestration_planner_contract_recovery import action_proposal + + +__all__ = ["modules", "native_http", "real_http_harness", "source_runtime"] +FAILURE_CASES = ( + ("loading", "action_initialization_failed", "action_initialization", 0, 0), + ("identity", "document_reader_identity_mismatch", "action_initialization", 0, 0), + ("read", "document_action_read_failed", "document_action_read", 1, 1), + ("results", "action_results_missing", "action_results", 1, 0), +) + + +def submit_action_turn(state, message, answer, turn_id): + proposal = action_proposal((LEFT,), action=state.action) + proposal["steps"][0]["arguments"]["task"] = message + proposal["steps"][1]["arguments"]["instruction"] = ( + "Use only the returned source evidence and copy its citation." + ) + state.runtime.harness.replies = [ + json.dumps({ + "relationship": "new_topic", "resolved_message": message, "message_ids": [], + "requires_retrieval": True, "clarification": "", + }), + json.dumps(proposal), + answer, + json.dumps({"requirements": [{ + "id": identifier, "requirement": requirement, "status": "satisfied", + "reason": "", "evidence_refs": [], "answer_quote": answer, + } for identifier, requirement in NARRATIVE_OBLIGATIONS]}), + ] + response = state.runtime.client.post("/api/v2/orchestration/plan", json={ + "conversation_id": "conversation-1", "turn_id": turn_id, "message": message, + "planning_submission_id": f"{turn_id}-submit", "approval_mode": "auto", + "selected_document_ids": [LEFT], + }, buffered=True) + events = frames(response) + plans = [event["plan"] for event in events if event.get("type") == "orchestration_plan"] + if len(plans) != 1: + raise AssertionError(f"The request did not produce one approved plan: {response.get_data(as_text=True)}") + plan = plans[0] + saved = state.runtime.harness.runs.read_item(plan["run_id"], "conversation-1") + return response, events, plan, saved + + +def test_every_supported_format_reaches_a_saved_cited_stored_chunk_answer(native_http, monkeypatch): + state = native_http + harness = state.runtime.harness + documents = harness.config.cosmos_user_documents_container + owner = importlib.import_module("functions_documents") + citations = importlib.import_module("functions_agent_document_citations") + extensions = sorted(harness.config.get_allowed_extensions(enable_video=True, enable_audio=True)) + categories = harness.config.get_allowed_extension_categories( + enable_video=True, enable_audio=True, enable_xsd=True, + ) + covered = {extension for category in categories for extension in category["extensions"]} + assert covered == set(extensions) + search_calls = [] + native_attempts = [] + monkeypatch.setattr(owner, "cosmos_user_documents_container", documents) + plugin = importlib.import_module("semantic_kernel_plugins.document_search_plugin") + + def reject_native_report(**kwargs): + native_attempts.append(deepcopy(kwargs)) + raise AssertionError("Stored-text retrieval must not request or infer a native report.") + + monkeypatch.setattr(plugin, "get_document_accessibility_payload", reject_native_report) + + def stored_chunk(document): + return { + "id": f"{LEFT}_1", "document_id": LEFT, "user_id": "owner", + "file_name": document["file_name"], "version": document["version"], + "chunk_id": "1", "chunk_sequence": 1, "page_number": 1, + "chunk_text": "The stored source states that the review is complete.", + } + + def search(**kwargs): + search_calls.append(deepcopy(kwargs)) + if kwargs.get("filter") != f"document_id eq '{LEFT}'": + raise AssertionError("The stored-chunk reader did not use the bound document filter.") + document = documents.read_item(LEFT, LEFT) + return [stored_chunk(document)] + + def search_client(*, group_id=None, public_workspace_id=None): + if group_id is not None or public_workspace_id is not None: + raise AssertionError("A personal document was routed to another search index.") + return SimpleNamespace(search=search) + + monkeypatch.setattr(owner, "_get_search_client", search_client) + + async def stored_reply(self, history, settings): + state.native_calls.append(deepcopy([message.to_dict() for message in history.messages])) + results = [ + item.result for message in history.messages for item in message.items + if hasattr(item, "result") + ] + if not results: + return [ChatMessageContent(role=AuthorRole.ASSISTANT, items=[FunctionCallContent( + id="read-stored", name="document_search-retrieve_document_chunks", + arguments=json.dumps({"document_id": LEFT, "doc_scope": "personal"}), + )])] + value = json.loads(results[-1]) if isinstance(results[-1], str) else results[-1] + return [ChatMessageContent(role=AuthorRole.ASSISTANT, content=json.dumps({ + "stored_text": value["chunks"][0]["chunk_text"], "citation": value["chunks"][0]["citation"], + }))] + + monkeypatch.setattr(AzureChatCompletion, "_inner_get_chat_message_contents", stored_reply) + message = "Quote the selected file's already stored extracted text and cite it. Do not infer native properties." + for index, extension in enumerate(extensions): + document = documents.read_item(LEFT, LEFT) + document.update({ + "file_name": f"selected.{extension}", "num_chunks": 1, + "indexed_chunk_count_version": document["version"], + }) + for field in ("archived_blob_path", "accessibility_metadata"): + document.pop(field, None) + documents.upsert_item(document) + marker = citations.annotate_document_search_payload({ + "document": {"id": LEFT, "file_name": document["file_name"], "version": document["version"]}, + "scope": "personal", "chunks": [stored_chunk(document)], + }, "retrieve_document_chunks")["chunks"][0]["citation"] + answer = f"The stored source states that the review is complete. {marker}" + before_calls = len(state.native_calls) + before_search = len(search_calls) + before_context_reads = len(state.report_reads) + response, events, plan, saved = submit_action_turn( + state, message, answer, f"format-{index}-{extension}", + ) + assert response.status_code == 200 and saved["status"] == "completed", ( + extension, saved.get("failure"), state.failures, + ) + assert plan["steps"][0]["arguments"]["document_ids"] == [LEFT] + assert len(state.native_calls) - before_calls == 2, extension + assert len(search_calls) - before_search == 1, extension + assert len(state.report_reads) - before_context_reads == 1, extension + assert native_attempts == [], extension + assert len(harness.replies) == 0, extension + saved_answer = harness.assistant_messages()[-1] + assert answer == saved_answer["content"], extension + assert not any(event.get("type") == "error" for event in events), extension + assert len(search_calls) == len(extensions) + assert len(harness.assistant_messages()) == len(extensions) + + +@pytest.mark.parametrize("debug", [False, True]) +@pytest.mark.parametrize("mode,code,stage,model_calls,report_reads", FAILURE_CASES) +def test_owned_action_failure_survives_saved_steps_and_private_logging( + native_http, monkeypatch, caplog, debug, mode, code, stage, model_calls, report_reads, +): + state = native_http + harness = state.runtime.harness + harness.settings["enable_debug_logging"] = debug + document = harness.config.cosmos_user_documents_container.read_item(LEFT, LEFT) + document.update({ + "file_name": "selected.pdf", + "accessibility_metadata": { + "format": "pdf", "schema_version": "1.0", "title": PRIVATE, "is_tagged": False, + }, + }) + harness.config.cosmos_user_documents_container.upsert_item(document) + loader = importlib.import_module("semantic_kernel_plugins.logged_plugin_loader") + if mode == "loading": + monkeypatch.setattr(loader.LoggedPluginLoader, "load_plugin_from_manifest", lambda *args, **kwargs: False) + elif mode == "identity": + reader = importlib.import_module("semantic_kernel_plugins.document_search_plugin") + spec = importlib.util.spec_from_file_location(reader.__name__, reader.__file__) + duplicate = importlib.util.module_from_spec(spec) + spec.loader.exec_module(duplicate) + monkeypatch.setattr( + loader.LoggedPluginLoader, "_create_plugin_instance", + lambda self, manifest: duplicate.DocumentSearchPlugin(deepcopy(manifest)), + ) + elif mode == "read": + state.fail_report = True + else: + async def no_results(self, history, settings): + state.native_calls.append(deepcopy([message.to_dict() for message in history.messages])) + return [ChatMessageContent(role=AuthorRole.ASSISTANT, content=PRIVATE)] + + monkeypatch.setattr(AzureChatCompletion, "_inner_get_chat_message_contents", no_results) + with caplog.at_level(logging.INFO, logger=state.logger.name): + response, events, plan, saved = submit_action_turn( + state, "Inspect only the selected document's native properties.", + "This draft must never be composed after a failed acquisition.", f"failure-{mode}", + ) + assert response.status_code == 200 and saved["status"] == "failed" + assert saved.get("task_results") == {} + assert len(state.native_calls) == model_calls + assert len(state.report_reads) == report_reads + assert len(harness.model_calls) == 2 and len(harness.replies) == 2 + steps_response = state.runtime.client.get( + f"/api/v2/orchestration/runs/{plan['run_id']}/steps", + query_string={"conversation_id": "conversation-1"}, + ) + steps_payload = steps_response.get_json() + assert steps_response.status_code == 200 + failed_step = next( + step for step in steps_payload["steps"] if step["step_id"] == plan["steps"][0]["step_id"] + ) + assert failed_step["status"] == "failed" + assert failed_step["failure"]["code"] == code + schema = importlib.import_module("functions_orchestration_schema") + expected = schema.build_failure(code) + assert failed_step["failure"]["message"] == expected["message"] + answers = harness.assistant_messages() + assert len(answers) == 1 and expected["message"] in answers[0]["content"] + assert PRIVATE not in response.get_data(as_text=True) + json.dumps(steps_payload) + json.dumps(answers) + records = [ + record for record in caplog.records + if getattr(record, "sc_message", None) == "[ORCHESTRATION_ADAPTERS] Direct action execution failed." + ] + assert len(records) == 1 + record = records[0] + assert record.sc_failure_code == code and record.sc_stage == stage + assert record.sc_run_id_hash == hashlib.sha256(plan["run_id"].encode()).hexdigest() + assert record.sc_step_id_hash == hashlib.sha256(plan["steps"][0]["step_id"].encode()).hexdigest() + assert record.sc_conversation_id_hash == hashlib.sha256(b"conversation-1").hexdigest() + if stage in ("action_initialization", "document_action_read"): + assert record.sc_function_calls_attempted is (stage == "document_action_read") + else: + assert not hasattr(record, "sc_function_calls_attempted") + serialized = json.dumps({ + name: value for name, value in record.__dict__.items() if name.startswith("sc_") + }) + assert PRIVATE not in serialized and LEFT not in serialized + assert document["file_name"] not in serialized + + +@pytest.mark.parametrize("code", [case[1] for case in FAILURE_CASES]) +def test_owned_action_codes_are_safe_nontransient_and_allow_manual_recovery(modules, code): + actions = importlib.import_module("functions_orchestration_actions") + schema = importlib.import_module("functions_orchestration_schema") + failure = schema.failure_from_exception(actions.ActionExecutionError(PRIVATE, failure_code=code)) + transient = schema.failure_is_transient(failure) + repeats = schema.failure_repeats_on_retry(failure) + assert failure == schema.build_failure(code) + assert not transient and not repeats + assert PRIVATE not in json.dumps(failure) + + +@pytest.mark.parametrize("cause,code", [(TimeoutError, "provider_timeout"), (ConnectionError, "connection_failed")]) +def test_unclassified_action_errors_preserve_transient_provider_causes(modules, cause, code): + actions = importlib.import_module("functions_orchestration_actions") + schema = importlib.import_module("functions_orchestration_schema") + error = actions.ActionExecutionError(PRIVATE) + error.__cause__ = cause(PRIVATE) + failure = schema.failure_from_exception(error) + transient = schema.failure_is_transient(failure) + assert failure["code"] == code and transient + assert PRIVATE not in json.dumps(failure) + + +def test_arbitrary_exception_failure_codes_are_not_admitted(modules): + actions = importlib.import_module("functions_orchestration_actions") + schema = importlib.import_module("functions_orchestration_schema") + failure = schema.failure_from_exception(actions.ActionExecutionError(PRIVATE, failure_code=PRIVATE)) + expected = schema.build_failure("step_failed") + assert failure == expected + assert PRIVATE not in json.dumps(failure) + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__, "-q"])) diff --git a/functional_tests/test_plugin_discovery_identity.py b/functional_tests/test_plugin_discovery_identity.py new file mode 100644 index 000000000..b13c0f2f4 --- /dev/null +++ b/functional_tests/test_plugin_discovery_identity.py @@ -0,0 +1,243 @@ +# test_plugin_discovery_identity.py +""" +Functional tests for canonical plugin discovery and instance isolation. +Version: 0.261.421 +Implemented in: 0.261.421 + +Production discovery, logged registration, classic loading, and real bootstrap +imports run with external I/O isolated. Fresh normal and optimized processes +check import order without substituting config or plugin modules. +""" + +import importlib +import json +import logging +import os +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy +from importlib.metadata import version +from pathlib import Path + +import pytest +from flask import Flask, session +from semantic_kernel import Kernel + +from test_orchestration_harness_routes import modules + + +__all__ = ["modules"] +ROOT = Path(__file__).resolve().parents[1] +APP = ROOT / "application" / "single_app" +TESTS = ROOT / "functional_tests" +IMPORT_ORDERS = ( + ("document_search_plugin", "plugin_loader", "logged_plugin_loader", "classic"), + ("plugin_loader", "logged_plugin_loader", "classic", "document_search_plugin"), + ("logged_plugin_loader", "classic", "plugin_loader", "document_search_plugin"), + ("classic", "plugin_loader", "logged_plugin_loader", "document_search_plugin"), +) +COLD_IMPORT_PROBE = """ +import importlib +import json +import socket +import sys +from pathlib import Path +from unittest.mock import patch +from test_support.offline_bootstrap import offline_app_imports + +dns_attempts = [] +def no_dns(*args, **kwargs): + dns_attempts.append(True) + raise AssertionError("Cold plugin import attempted DNS access.") + +with offline_app_imports() as environment, patch.object(socket, "getaddrinfo", no_dns): + for name in json.loads(sys.argv[1]): + module_name = "semantic_kernel_loader" if name == "classic" else "semantic_kernel_plugins." + name + importlib.import_module(module_name) + if name == "plugin_loader": + discovered_early = importlib.import_module(module_name).discover_plugins() + reader_early = importlib.import_module("semantic_kernel_plugins.document_search_plugin") + if discovered_early.get("DocumentSearchPlugin") is not reader_early.DocumentSearchPlugin: + raise AssertionError("Discovery-first loading created a different reader class.") + discovery = importlib.import_module("semantic_kernel_plugins.plugin_loader") + first = discovery.discover_plugins() + second = discovery.discover_plugins() + if "DocumentSearchPlugin" not in first or first.keys() != second.keys(): + raise AssertionError("Installed plugin discovery changed between calls.") + for name, plugin_class in first.items(): + owner = importlib.import_module(plugin_class.__module__) + canonical = vars(owner).get(plugin_class.__name__) + if canonical is not plugin_class or second[name] is not plugin_class: + raise AssertionError("Installed plugin class identity was not preserved.") + importlib.import_module("functions_orchestration_bootstrap") + importlib.import_module("app") + importlib.import_module("functions_orchestration_execution") + config = importlib.import_module("config") + if Path(config.__file__).resolve() != Path(sys.argv[2]).resolve(): + raise AssertionError("The real config module was not imported.") + if environment.network_attempts or dns_attempts: + raise AssertionError("A blocked network attempt was swallowed during startup.") + print("PLUGIN_IDENTITY_RESULT:" + json.dumps({"plugins": len(first), "network_attempts": 0})) +""" + + +@pytest.fixture +def discovery(modules): + return importlib.import_module("semantic_kernel_plugins.plugin_loader") + + +@pytest.fixture +def request_app(): + app = Flask(__name__) + app.secret_key = "plugin-identity-test" + return app + + +def test_repeated_discovery_reuses_every_canonical_plugin_class(discovery): + first = discovery.discover_plugins() + second = discovery.discover_plugins() + assert "DocumentSearchPlugin" in first + assert first.keys() == second.keys() + for name, plugin_class in first.items(): + owner = importlib.import_module(plugin_class.__module__) + canonical = vars(owner).get(plugin_class.__name__) + assert canonical is plugin_class, name + assert second[name] is plugin_class, name + + +def test_required_template_parser_is_explicitly_pinned_and_importable(modules): + requirements = (APP / "requirements.txt").read_text(encoding="utf-8").splitlines() + pins = [line.partition("==")[2] for line in requirements if line.lower().startswith("pymeta3==")] + parser = importlib.import_module("pymeta.grammar") + installed = version("PyMeta3") + assert len(pins) == 1 and installed == pins[0] + assert callable(parser.OMeta) + + +def test_logged_reader_instances_keep_separate_configuration(discovery, request_app): + loader_module = importlib.import_module("semantic_kernel_plugins.logged_plugin_loader") + reader_module = importlib.import_module("semantic_kernel_plugins.document_search_plugin") + manifests = [ + { + "name": name, "type": "document_search", + "additionalFields": {"allowed_scopes": [scope]}, + } + for name, scope in (("personal_reader", "personal"), ("public_reader", "public")) + ] + originals = deepcopy(manifests) + loaders = [loader_module.LoggedPluginLoader(Kernel()) for _ in manifests] + loaded = [ + loader.load_plugin_from_manifest(manifest, user_id="owner") + for loader, manifest in zip(loaders, manifests) + ] + assert loaded == [True, True] + readers = [loader.plugin_instances[0] for loader in loaders] + assert all(type(reader) is reader_module.DocumentSearchPlugin for reader in readers) + assert readers[0] is not readers[1] + with request_app.test_request_context(): + session["user"] = {"oid": "owner"} + initial_scopes = [reader._resolve_doc_scope_requests("all") for reader in readers] + readers[0].manifest["additionalFields"]["allowed_scopes"].append("group") + changed_scopes = [reader._resolve_doc_scope_requests("all") for reader in readers] + assert initial_scopes == [["personal"], ["public"]] + assert changed_scopes == [["personal", "group"], ["public"]] + assert manifests == originals + functions = [loader.kernel.get_list_of_function_metadata({}) for loader in loaders] + assert all(len(items) == 4 for items in functions) + + +def test_concurrent_discovery_and_loading_keep_reader_identity_and_scope(discovery, request_app): + loader_module = importlib.import_module("semantic_kernel_plugins.logged_plugin_loader") + reader_module = importlib.import_module("semantic_kernel_plugins.document_search_plugin") + + def load_reader(index): + scope = ("personal", "group", "public")[index % 3] + user_id = f"owner-{index}" + with request_app.test_request_context(): + session["user"] = {"oid": user_id} + classes = discovery.discover_plugins() + loader = loader_module.LoggedPluginLoader(Kernel()) + loaded = loader.load_plugin_from_manifest({ + "name": f"reader_{index}", "type": "document_search", + "additionalFields": {"allowed_scopes": [scope]}, + }, user_id=user_id) + reader = loader.plugin_instances[0] if loaded else None + scopes = reader._resolve_doc_scope_requests("all") if reader is not None else None + observed_user = reader._get_user_id() if reader is not None else None + return loaded, classes.get("DocumentSearchPlugin"), reader, scopes, scope, observed_user, user_id + + with ThreadPoolExecutor(max_workers=4) as pool: + results = list(pool.map(load_reader, range(12))) + readers = [] + for loaded, plugin_class, reader, scopes, expected_scope, observed_user, expected_user in results: + assert loaded + assert plugin_class is reader_module.DocumentSearchPlugin + assert type(reader) is reader_module.DocumentSearchPlugin + assert scopes == [expected_scope] + assert observed_user == expected_user + readers.append(reader) + assert len({id(reader) for reader in readers}) == len(readers) + + +def test_module_import_failure_is_logged_without_private_exception_text( + discovery, monkeypatch, caplog, +): + telemetry = importlib.import_module("functions_appinsights") + logger = logging.getLogger("test.plugin.discovery") + monkeypatch.setattr(telemetry, "get_appinsights_logger", lambda: logger) + monkeypatch.setattr(telemetry, "_load_logging_settings", lambda: {}) + original_import = importlib.import_module + + def fail_optional_module(name, package=None): + if name == "semantic_kernel_plugins.unavailable_plugin": + raise ImportError("PRIVATE_PLUGIN_IMPORT_SENTINEL") + return original_import(name, package) + + monkeypatch.setattr(discovery.os, "listdir", lambda path: [ + "document_search_plugin.py", "unavailable_plugin.py", "base_plugin.py", "__init__.py", + ]) + monkeypatch.setattr(discovery.importlib, "import_module", fail_optional_module) + with caplog.at_level(logging.WARNING, logger=logger.name): + found = discovery.discover_plugins() + assert "DocumentSearchPlugin" in found + records = [ + record for record in caplog.records + if getattr(record, "sc_message", None) == "[PLUGIN_LOADER] Plugin module could not be loaded." + ] + assert len(records) == 1 + assert records[0].sc_error_type == "ImportError" + properties = [ + {key: value for key, value in record.__dict__.items() if key.startswith("sc_")} + for record in records + ] + assert "PRIVATE_PLUGIN_IMPORT_SENTINEL" not in json.dumps(properties) + + +@pytest.mark.parametrize("order", IMPORT_ORDERS, ids=("reader-first", "discovery-first", "logged-first", "classic-first")) +@pytest.mark.parametrize("optimized", [False, True], ids=("normal", "optimized")) +def test_real_cold_import_orders_preserve_identity_and_network_free_startup(order, optimized): + environment = os.environ.copy() + environment["PYTHONPATH"] = os.pathsep.join((str(APP), str(TESTS))) + environment["PYTHONDONTWRITEBYTECODE"] = "1" + command = [ + sys.executable, *(["-O"] if optimized else []), "-c", COLD_IMPORT_PROBE, + json.dumps(order), str(APP / "config.py"), + ] + result = subprocess.run( + command, cwd=ROOT, env=environment, capture_output=True, text=True, + encoding="utf-8", errors="replace", timeout=180, + ) + assert result.returncode == 0, result.stdout + result.stderr + records = [ + line.removeprefix("PLUGIN_IDENTITY_RESULT:") + for line in result.stdout.splitlines() if line.startswith("PLUGIN_IDENTITY_RESULT:") + ] + assert len(records) == 1 + report = json.loads(records[0]) + assert report["plugins"] > 1 + assert report["network_attempts"] == 0 + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__, "-q"]))