--- a/README_BUNDLE_22072026124256.txt
+++ b/README_BUNDLE_22072026124256.txt
@@ -1,4 +1,4 @@
-GLMChat V2.2 — App and Documentation Bundle
+GLMChat V2.2.1 — App and Documentation Bundle
 Updated: 22 July 2026
 
 - Upload only the contents of app/ to the intended HTTPS application directory.
@@ -6,3 +6,9 @@
 - Read documentation/INSTALLATION_AND_ACCEPTANCE_GUIDE_22072026124256.md before deployment or upgrade.
 - Preserve the existing production storage/ directory during upgrades.
 - Physical Android, live LiteSpeed and live Together checks must still be completed on the target environment.
+
+
+V2.2.1 HARDENING UPDATE
+The app/ directory is the direct-deployment tree. Authoritative guides remain in documentation/ rather than being duplicated inside the public web root.
+The V2.2.1 completion report, third-party notices and the timestamped CHANGES file describe the exact hardening work and validation limitations.
+Production acceptance still requires the target LiteSpeed host, live Together credentials and physical Android browser/device checks.
--- /dev/null
+++ b/THIRD_PARTY_NOTICES_22072026125600.txt
@@ -0,0 +1,15 @@
+GLMChat Third-Party Notices
+Generated: 22 July 2026 12:56 UTC
+
+React and ReactDOM
+Copyright (c) Meta Platforms, Inc. and affiliates.
+Licensed under the MIT License.
+
+MIT License
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+The removed Workbox Window asset is no longer part of the V2.2.1 deployment.
--- a/app/.htaccess
+++ b/app/.htaccess
@@ -60,7 +60,7 @@
     Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; connect-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self' data: blob:; media-src 'self' blob:; object-src 'none'; script-src 'self'; style-src 'self'; style-src-elem 'self'; style-src-attr 'none'; worker-src 'self' blob:"
 
     # Never cache API/front-controller responses or the PWA shell metadata.
-    <FilesMatch "^(?:index\.php|index\.html|manifest\.webmanifest|precache-manifest\.json|sw\.js|registerSW\.js|offline\.html|icon-192\.png|icon-512\.png|maskable-512\.png|apple-touch-icon-180\.png|favicon-48\.png)$">
+    <FilesMatch "^(?:index\.php|index\.html|manifest\.webmanifest|precache-manifest\.json|sw\.js|registerSW\.js|offline\.html|native-android\.js|offline-runtime\.js|platform-enhancements\.js|icon-192\.png|icon-512\.png|maskable-512\.png|apple-touch-icon-180\.png|favicon-48\.png)$">
         Header always set Cache-Control "no-cache, no-store, must-revalidate"
         Header always set X-LiteSpeed-Cache-Control "no-cache, no-store"
         Header always set Pragma "no-cache"
--- a/app/ADMIN_OPERATIONS_GUIDE_22072026124256.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# GLMChat V2.2 Administration and Operations Guide
-
-Updated: 22 July 2026
-
-## 1. Feature-locked architecture
-
-- React PWA frontend using local vendored runtime assets.
-- PHP 8.2+ backend.
-- SQLite server persistence.
-- IndexedDB client persistence and ordered offline mutation queues.
-- LiteSpeed or Apache-compatible shared hosting.
-- No production Node.js, Python, Composer, WebSocket daemon or alternate database.
-
-## 2. Storage and permissions
-
-`storage/` must be writable by PHP and denied to web requests. It contains server state, databases, logs and project data. Do not make it world-writable when a more restrictive hosting-specific ownership or ACL arrangement is available.
-
-Preserve `storage/.htaccess`. Confirm directory listing is disabled and direct access to database, log, key and backup extensions is rejected.
-
-## 3. Database operations
-
-- SQLite uses foreign keys, busy timeout and WAL where available.
-- Migrations are checksummed and forward-only.
-- Take a complete storage backup before every application upgrade.
-- Do not replace the production database with the empty packaged storage directory.
-- Do not edit migration files after deployment.
-- Investigate a checksum mismatch rather than bypassing it.
-
-## 4. Security operations
-
-- Keep the site HTTPS-only.
-- Configure trusted proxy TLS only through the server environment, never a client-controlled header.
-- Preserve the restrictive CSP and no-store API headers.
-- Do not expose `source/`, `tests/`, `scripts/`, `handover/`, `evidence/`, `implementation-prompts/`, `server/` or `storage/`.
-- Rotate the Together API key if credential exposure is suspected.
-- Review rate-limit, lockout and recent-PIN behaviour after host or session configuration changes.
-- Keep diagnostics disabled unless required, and export only secret-safe diagnostic records.
-
-## 5. Tool administration
-
-The PHP server is authoritative for the tool allowlist. A UI entry alone does not grant execution.
-
-For each tool, review:
-
-- stable identifier and JSON schema;
-- read-only, mutating or destructive risk class;
-- project path confinement;
-- timeout and output limits;
-- approval and recent-PIN policy;
-- idempotency and retry rules;
-- redaction of logs and returned errors.
-
-Never add arbitrary shell execution, unrestricted filesystem access or dynamically supplied PHP code.
-
-## 6. Cost governance
-
-Configure daily and workflow-level limits before enabling wider use. Verify provider pricing assumptions in the current environment. The server, not only the interface, must enforce hard limits. Review cost events by project, run, worker and model when investigating unexpected use.
-
-## 7. Service-worker and cache operations
-
-- Publish fingerprinted immutable assets first.
-- Publish mutable metadata and `sw.js` next.
-- Publish `index.html` last.
-- Never reuse a fingerprinted URL for changed bytes.
-- Keep API, setup and authentication responses out of Cache Storage.
-- Clear only obsolete GLMChat shell caches during recovery.
-- Do not clear IndexedDB or pending queues as part of a normal update.
-
-## 8. Routine maintenance
-
-- Review application health and controlled bootstrap status.
-- Check available disk space and SQLite growth.
-- Review logs for repeated authentication failures, retry exhaustion or tool rejection without retaining secrets.
-- Test backups by restoring into a non-production environment.
-- Verify PWA update behaviour after every release.
-- Re-run the live acceptance checklist after PHP, LiteSpeed, TLS, proxy or directory-permission changes.
-
-## 9. Incident response
-
-1. Preserve logs and storage safely without publishing them.
-2. Record the release hash and environment details.
-3. Revoke or rotate exposed provider credentials.
-4. Disable destructive tools if tool execution is implicated.
-5. Preserve queued mutations before clearing any browser data.
-6. Use the stable user-facing error code and sanitised diagnostics for investigation.
-7. Restore application files as a coherent release; do not mix immutable assets between releases.
-
-## 10. Release verification
-
-A production release is acceptable only when:
-
-- source and deployment archives pass clean extraction;
-- manifests and asset fingerprints match;
-- PHP and JavaScript syntax checks pass;
-- feature-lock contracts pass;
-- migration scenarios pass;
-- PWA/offline/accessibility suites pass;
-- no high-confidence secrets are present;
-- live host, physical Android and live Together checks are recorded truthfully.
--- a/app/DEPLOYMENT_MANIFEST.json
+++ b/app/DEPLOYMENT_MANIFEST.json
@@ -1,62 +1,38 @@
 {
   "application": "GLMChat",
-  "file_count": 86,
+  "file_count": 80,
   "files": {
     ".htaccess": {
-      "sha256": "316352b48ca4ab91bd224b9501281aa18ec99a842321e5a573407c54e9e6d666",
-      "size": 4419
-    },
-    "ADMIN_OPERATIONS_GUIDE_22072026124256.md": {
-      "sha256": "b664389f639aa5bf542ba4bbf793fccf9682ab3e62ef6173b93da18ed317d8c5",
-      "size": 4466
-    },
-    "DOCUMENTATION_INDEX_22072026124256.md": {
-      "sha256": "f7ae7d5520aa0d81e569e69936c38546d7580c9a9776a1f09b5dfe30322b0cee",
-      "size": 2116
-    },
-    "INSTALLATION_AND_ACCEPTANCE_GUIDE_22072026124256.md": {
-      "sha256": "90eeffcbec97db2024aadad1628f32d0fb01a8054915f80af34ca6a10682913e",
-      "size": 5367
+      "sha256": "ccb4e5e56e820d756cf17c60704179c37d0622dad652649b94ccfd4ce7679f61",
+      "size": 4484
     },
     "README.md": {
-      "sha256": "e81c7d3964ebbd6aa8fb17b8478651c4692773420c37c6942c754be0fe9d3cab",
-      "size": 10041
-    },
-    "RELEASE_NOTES_22072026124256.md": {
-      "sha256": "be546110be9474aec230788af36527a92f2ddb4bd2203d15a17217a5f09fcda6",
-      "size": 2516
-    },
-    "USER_GUIDE_22072026124256.md": {
-      "sha256": "bfc74f5595aa0eab7fce5f3f29cc691a55542dd855d1fb292858da147f6af37a",
-      "size": 5082
-    },
-    "VERIFICATION_REPORT.txt": {
-      "sha256": "d6fd277011a0e7cf496ef0aff7cf4c9c2c8596901e135316917810ae4d0b7c4a",
-      "size": 6464
+      "sha256": "033f1b24cd2f0ad288a8a1ac7e24ec375d6b41fc1af7145a61d77a54be8f3e10",
+      "size": 10706
     },
     "VERSION": {
-      "sha256": "c4a2383a03bdb6739d16a0e24058e4b9c7da4e63d203e0be2f448868cc03c530",
+      "sha256": "fe650098093fad42070e8a2cacfb216a6c416af57bd26dd6ff479fef69f8bc71",
       "size": 6
     },
     "assets/index-12db1e757e12.css": {
-      "sha256": "12db1e757e12bf520c023406cb6a3c0493e1ec607ceee1d2db853734b22123f6",
-      "size": 39588
+      "sha256": "0be61b61bf68efd5d3b686f864d16afd335bd2783b9770eab43e26ab206eaa1f",
+      "size": 42215
     },
     "assets/index-5aa4ab81f41a.js": {
-      "sha256": "5aa4ab81f41a5ab3f7db559f402db4461ab45ad26c2e50ef92310da982aa77a6",
-      "size": 180922
+      "sha256": "819dc1ca9010dbc0d7923d6d12a81fc14988cf37950d19360d19234617aad33f",
+      "size": 180827
     },
     "assets/native-android.js": {
       "sha256": "a37e0c7ce1266fbd38a9700861f7a4be866c5356e89fea7fe051bb79c79dd3b1",
       "size": 6491
     },
     "assets/offline-runtime.js": {
-      "sha256": "9128c1a2af53d092e75d6b2a042a0f7cd9b63ffa30c113c303087ad9a9bbe68a",
-      "size": 49460
+      "sha256": "ffd184b6ab6a0bfcbc3d019237eaec565281dc2b49ead347c7afa481e9092f21",
+      "size": 55622
     },
     "assets/platform-enhancements.js": {
-      "sha256": "e8e10779682865a30202a9de83b7dd11d3519ec77dca529e39ba8b304906002f",
-      "size": 12634
+      "sha256": "9b8b12480872be990657eaa29e5c45ca68a26ad724b1c6f03b60e55e9b662d5a",
+      "size": 15119
     },
     "assets/react-de59b0d4504a.js": {
       "sha256": "de59b0d4504aa0350c13775fec9b78859f3eda83dea21d60dded98bb7866d54a",
@@ -66,10 +42,6 @@
       "sha256": "c7a65157caebd9af7e108f1f1eb8b6e2c8fafb141d0f4c2a4d4dfd851c52ec9e",
       "size": 102890
     },
-    "assets/workbox-window.prod.es5-cadd21be4a88.js": {
-      "sha256": "cadd21be4a88598a4dec91a26a025df0a5790ff6baec54a884e401c3f5c95557",
-      "size": 5748
-    },
     "icons/apple-touch-icon-180.png": {
       "sha256": "f687b4e12f74d170a0a75ab90db1e8fa91f5d94f41ca72036dc1f489b2fa3e87",
       "size": 32816
@@ -91,28 +63,28 @@
       "size": 148044
     },
     "index.html": {
-      "sha256": "5bfdd5b9d06a028a7e3a7dea6c7eb6c1f23fd77fa51a68933dc56573d8f26d8d",
-      "size": 1475
+      "sha256": "b8bce236cd71f6c2951f65a1e31484e943bdb1df9f87d2e0259b9ce337999751",
+      "size": 1466
     },
     "index.php": {
       "sha256": "c14b80baca91d6ae64de9582a07b08e469ba506d6073908fe775e4468e283651",
       "size": 68
     },
     "manifest.webmanifest": {
-      "sha256": "87030677651dfe01b1a3430e9a139c59417f83b83f54e291f051565a01f922fa",
-      "size": 2273
+      "sha256": "39ea0b447188897f8631d4256c568ea74b0cdf3d638e35a1b337bf14ef23b74b",
+      "size": 2578
     },
     "offline.html": {
       "sha256": "4a4a9b3a8f84882abcafbb3644341d8c2f67a5306eab1141679fb8c5e38ba1bd",
       "size": 767
     },
     "precache-manifest.json": {
-      "sha256": "a3225ab62fab3b89c48435489477713bbfa4c86f7150a44c4ebd014b2d6cc2ce",
-      "size": 590
+      "sha256": "30d40c049b18b5df687ee187475a52860bc656743fc433ce115bec34e04d0fc5",
+      "size": 534
     },
     "sbom.cdx.json": {
-      "sha256": "41ac89694b8077d1fc0473a2536c0abf46ce9d3e97945595f8eddf552f13408e",
-      "size": 27844
+      "sha256": "be150a9bdd8fcc177a165dd0a8576f86d90d4be2ccf6731bfa1b965168da0035",
+      "size": 34472
     },
     "server/.htaccess": {
       "sha256": "564a1d9c2ce7748072c292d709b85d0e30118cf41ae77c4824b8fc5fe791d1bc",
@@ -123,7 +95,7 @@
       "size": 7005
     },
     "server/config.php": {
-      "sha256": "36c74f54f6d8cc92cfb9508ec92aa8395f3f274815b58ab20b485b1165493978",
+      "sha256": "9db4eaeed0dbf51a2aefacdd49326b0f74737816af32e08bf3f1f85da9a73163",
       "size": 642
     },
     "server/index.php": {
@@ -139,7 +111,7 @@
       "size": 3048
     },
     "server/lib/Auth.php": {
-      "sha256": "531dfe530d459e9cb1a9385694ed61ac4a7497443ba2d22b36f1a4f93f913a69",
+      "sha256": "72ddfd845aaf8fb9fb617073ee38de16b3204c64720e777a55b9c1b95ede7ca6",
       "size": 9705
     },
     "server/lib/ContextBudget.php": {
@@ -215,8 +187,8 @@
       "size": 1990
     },
     "server/lib/Together.php": {
-      "sha256": "6cd3c64cc5bc420c087926ba1235464296ddc09e6b465c73a1d885d94104dfb3",
-      "size": 37213
+      "sha256": "cdfef3adaa99a358f551426716d449f8a96c9764ed97e0a67b1840acd7a52582",
+      "size": 37277
     },
     "server/lib/ToolExecutor.php": {
       "sha256": "c2dfd850e233d7d175ed032a8c4768f57177105926823013f72241dffe9d441c",
@@ -286,6 +258,10 @@
       "sha256": "8903dcdc70daab611c68b2b1f0e630c41dd4e61afe6facec2707bd15fbc5d46c",
       "size": 1245
     },
+    "server/migrations/013_release_hardening.sql": {
+      "sha256": "d0c14421953770549a473583c30c1fea61b7e08a4b84947043eeadfbdb248e39",
+      "size": 441
+    },
     "server/routes/auth.php": {
       "sha256": "ade5c0aa6670094258d07f8f3dded7dc8dbc94b7a02e2b847d871764d1fcf900",
       "size": 3005
@@ -343,10 +319,14 @@
       "size": 114
     },
     "sw.js": {
-      "sha256": "ce6f5a2d696521b7490669beeacf870cabde5f0c9aa954296f045a82f4abe008",
-      "size": 8708
+      "sha256": "6e39df25a83086351a45eaf4508ab31e45e6ef0b484f351f07e7ea31e16212f4",
+      "size": 8684
     }
   },
+  "inventory_exclusions": [
+    "DEPLOYMENT_MANIFEST.json",
+    "CHANGES_hhmmddmmyyyy.txt"
+  ],
   "profile": "deployment-only",
-  "version": "2.2.0"
+  "version": "2.2.1"
 }
--- a/app/DOCUMENTATION_INDEX_22072026124256.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# GLMChat V2.2 Documentation Index
-
-Updated: 22 July 2026
-
-This documentation accompanies the GLMChat V2.2 agentic automation release. The application remains feature-locked to a React Progressive Web App with a PHP 8.2+ and SQLite backend for LiteSpeed or Apache-compatible shared hosting.
-
-## Start here
-
-1. `README.md` — release summary, architecture and source commands.
-2. `docs/INSTALLATION_AND_ACCEPTANCE_GUIDE_22072026124256.md` — deployment, upgrade, rollback and live acceptance.
-3. `docs/USER_GUIDE_22072026124256.md` — setup, chat, projects, agents, workflows, teams and tools.
-4. `docs/ADMIN_OPERATIONS_GUIDE_22072026124256.md` — hosting, storage, security, diagnostics and maintenance.
-5. `docs/RELEASE_NOTES_22072026124256.md` — V2.2 capability summary, verification and known external checks.
-6. `docs/ROUTE_TO_UI_MATRIX.md` — API-to-interface coverage and authentication requirements.
-7. `FEATURE_AND_FUNCTION_MAP.txt` — authoritative feature, persistence, offline and verification map.
-8. `VERIFICATION_REPORT.txt` — classified local, static and external verification results.
-9. `UI_UX_REMEDIATION_REPORT.txt` — implemented mobile interface and accessibility work.
-10. `REMEDIATION_PATCHES.txt` — remediation and evidence record.
-
-## Release profiles
-
-- **Direct deployment:** production runtime only. Upload the contents of the `app/` directory from the documented bundle, or use the standalone direct-deployment ZIP.
-- **Complete source:** runtime, maintainable React source, PHP source, tests, build scripts, evidence and documentation.
-- **Documented bundle:** `app/` contains the deployment-ready application and `documentation/` contains the operator and user documentation. Do not upload the `documentation/` directory into the public application root.
-
-## Verification terminology
-
-- **EXECUTED PASS:** the check ran and passed.
-- **STATIC PASS:** deterministic inspection passed where runtime execution was not possible.
-- **EXTERNAL NOT EXECUTED:** requires the target host, physical device or live Together account.
-
-External checks are never represented as passes.
--- a/app/INSTALLATION_AND_ACCEPTANCE_GUIDE_22072026124256.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# GLMChat V2.2 Installation and Acceptance Guide
-
-Updated: 22 July 2026
-
-## 1. Hosting requirements
-
-- HTTPS-enabled LiteSpeed or Apache-compatible shared hosting.
-- PHP 8.2 or later.
-- PHP extensions: `sqlite3`, `curl`, `json`, `openssl`, `mbstring`, `fileinfo` and `zip`.
-- Writable private `storage/` directory.
-- `.htaccess` support for rewrite and header rules.
-- No Node.js, Python, Composer, container or background daemon is required on the production host.
-
-## 2. New installation
-
-1. Create an empty HTTPS directory at the domain root or intended nested path.
-2. Upload the contents of the supplied `app/` directory, or extract the standalone direct-deployment ZIP in that directory.
-3. Confirm `.htaccess`, `server/.htaccess` and `storage/.htaccess` remain present.
-4. Make `storage/` writable by PHP without making it publicly browsable.
-5. Open `/api/status` and confirm it returns a controlled success response. A controlled `ENVIRONMENT_INVALID` response identifies missing extensions without exposing stack traces or private paths.
-6. Open the application in Chrome for Android or Samsung Internet.
-7. Complete first-use setup: create the six-digit PIN, enter and test the Together API key, then review optional recovery guidance.
-8. Install the PWA from the browser when prompted.
-
-## 3. Upgrade procedure
-
-1. Export any required user data through the application.
-2. Back up the complete existing `storage/` directory outside the public web root.
-3. Record the current release ZIP and SHA-256 for rollback.
-4. Put the application into a maintenance window if users are active.
-5. Upload new immutable `assets/` files first.
-6. Upload server files and mutable coordination files next.
-7. Upload `index.html` last.
-8. Preserve the existing `storage/` directory; do not replace it with an empty packaged directory.
-9. Open `/api/status`, log in, and allow forward-only migrations to complete.
-10. Confirm existing projects, conversations, queues and settings remain available.
-11. Trigger the service-worker update and verify the application reloads without clearing IndexedDB or pending mutations.
-
-## 4. Rollback
-
-1. Preserve the current `storage/` directory and take another backup before rollback.
-2. Restore the previous application files without restoring an older database over a migrated database unless a separately tested database rollback plan exists.
-3. Restore the previous immutable assets and coordination files as one coherent release.
-4. Clear only obsolete GLMChat shell caches through the recovery interface; do not clear IndexedDB or queued mutations.
-5. Verify login, projects, conversations and offline queues.
-
-Database migrations are forward-only. A code rollback after a schema upgrade must be assessed against the migration compatibility notes rather than assumed safe.
-
-## 5. Post-installation acceptance
-
-### Server and security
-
-- [ ] `/api/status` reports a healthy environment.
-- [ ] HTTP requests are rejected rather than redirected using an untrusted Host header.
-- [ ] Root and nested-subdirectory routes work.
-- [ ] `storage/`, `server/`, `source/`, `tests/`, `scripts/`, `handover/`, `evidence/` and `implementation-prompts/` are not publicly accessible.
-- [ ] PHP, database, log, key and evidence files cannot be downloaded.
-- [ ] CSP contains no `unsafe-eval` and no uncontrolled remote script source.
-- [ ] API, setup and authentication responses use `Cache-Control: no-store`.
-- [ ] Session cookies are Secure, HttpOnly and SameSite where HTTPS is active.
-
-### Application and PWA
-
-- [ ] Welcome, setup and PIN login work.
-- [ ] Together API-key validation succeeds.
-- [ ] Chat streams a response and Stop works.
-- [ ] A project can be created and its selected files are visibly included in chat context.
-- [ ] An agent, workflow and team can be saved and launched.
-- [ ] A run shows real stages, workers, approvals and events.
-- [ ] Pause, resume, cancel and retry operate only in valid states.
-- [ ] A read-only tool executes; a destructive tool requires approval and recent PIN.
-- [ ] Offline launch works after an initial online load.
-- [ ] Offline chat remains a draft and is not silently submitted.
-- [ ] Pending mutations resume in order after reconnection.
-- [ ] Conflicts can be reviewed and resolved.
-- [ ] A service-worker update preserves IndexedDB and queued work.
-
-### Android and accessibility
-
-- [ ] Chrome for Android installation and update work.
-- [ ] Samsung Internet installation and update work.
-- [ ] Android Back closes overlays before navigating away.
-- [ ] The keyboard does not obscure the composer.
-- [ ] Portrait, short landscape and display cut-outs are usable.
-- [ ] 200% text and Android large text do not clip controls.
-- [ ] TalkBack announces controls, stages and approval states correctly.
-- [ ] Reduced-motion mode removes nonessential animation.
-
-### Live provider acceptance
-
-- [ ] GLM-5.2 text and coding requests succeed.
-- [ ] Streaming and cancellation succeed.
-- [ ] Structured JSON output validates.
-- [ ] Function calling executes through the PHP allowlist.
-- [ ] Images route to the configured vision-capable model.
-- [ ] TTS voice listing and playback succeed.
-- [ ] Provider errors, timeouts, rate limits and cost limits are handled safely.
-
-The physical-device, live-host and live-provider checks above remain external until performed on the target environment.
--- a/app/README.md
+++ b/app/README.md
@@ -1,4 +1,4 @@
-# GLMChat V2.2 — Agentic Automation Production Release
+# GLMChat V2.2.1 — Agentic Automation Production Release
 
 GLMChat V2.2 is a mobile-first, offline-capable React Progressive Web App for chat, project work, typed autonomous agents, multi-stage workflows, coordinated agent teams and allowlisted PHP tools. It preserves the feature-locked PHP 8.2+/SQLite/LiteSpeed shared-hosting architecture while making advanced automation accessible through tiles and guided, resumable wizards.
 
@@ -7,7 +7,7 @@
 - **Frontend:** maintainable React modules built deterministically with local vendored React and ReactDOM.
 - **Backend:** PHP 8.2+ only; no Node, Python, Composer, container or alternate production backend.
 - **Server persistence:** SQLite with WAL, foreign keys, busy timeout and forward-only checksummed migrations.
-- **Offline persistence:** IndexedDB for drafts, definitions, supported cached state, encrypted offline records and ordered mutation queues.
+- **Offline persistence:** IndexedDB for drafts, supported cached state and ordered mutation queues. Sensitive cached content, drafts, conflicts and mutation payloads are protected with AES-GCM after PIN unlock; existing plaintext V2.2 records are migrated on the next successful unlock.
 - **Hosting:** HTTPS LiteSpeed or Apache-compatible shared hosting at a domain root or nested subdirectory.
 - **AI:** Together GLM-5.2 for primary text/coding/agent work, a separate configured vision-capable model for images and the retained TTS endpoint for voice playback.
 - **PWA:** deployment-relative manifest, shell-only Cache Storage, controlled service-worker updates and safe previous-release upgrades.
@@ -76,7 +76,7 @@
 
 The service worker caches only the public application shell. Setup, authentication and private API responses remain `no-store` and are excluded before all cache branches.
 
-Queued mutations retain UUID idempotency, request hashes, ordering, bounded retries, explicit conflict records and authentication-required pauses. The offline centre exposes pending, exhausted, conflicted and authentication-paused actions. Conflict resolution can keep the local or server version through the existing sync API. Service-worker updates remove obsolete GLMChat shell caches without deleting IndexedDB or queued work.
+Queued mutations retain UUID idempotency, request hashes, ordering, bounded retries, explicit conflict records and authentication-required pauses. Browser sync registration requests replay from an open or subsequently reopened client; the service worker does not transmit authenticated mutations while the app is closed. The offline centre exposes pending, exhausted, conflicted and authentication-paused actions. Conflict resolution can keep the local or server version through the existing sync API. Service-worker updates remove obsolete GLMChat shell caches without deleting IndexedDB or queued work.
 
 ## 7. Security controls
 
@@ -112,59 +112,31 @@
 
 ## 10. Source and verification
 
-```bash
-npm ci
-npm run lint
-npm run test:node
-npm run test:php
-npm run test:migrations
-npm run test:browser
-npm run build
-npm run verify
-```
+This is a deployment-only release. The matching React source repository, package files, lockfile, feature-lock files, automated test suites and deterministic build scripts are not included and cannot be reconstructed from this package.
 
-Feature-lock verification:
-
-```bash
-npm run verify:feature-lock
-```
-
-An authorised implementation change may refresh the lock only after feature-preservation review and complete regression verification:
-
-```bash
-GLMCHAT_AUTHORIZE_FEATURE_LOCK_UPDATE=YES npm run feature-lock:refresh
-```
-
-Packaging:
-
-```bash
-npm run package
-npm run package:deployment
-```
-
-The historical V2.0.1 generated bundle remains under `source/frontend/legacy/` only as provenance; the active application is built from `source/frontend/react/*.js`.
+The supplied historical verification report is retained in `../documentation/VERIFICATION_REPORT.txt`, but its original source-level claims must be reproduced from the matching complete-source release before production approval. V2.2.1 validation performed directly on this package is recorded in the timestamped `CHANGES_hhmmddmmyyyy.txt` file and `../documentation/RELEASE_HARDENING_REPORT_22072026125600.txt`.
 
 ## 11. Documentation set
 
-- `docs/DOCUMENTATION_INDEX_22072026124256.md`
-- `docs/INSTALLATION_AND_ACCEPTANCE_GUIDE_22072026124256.md`
-- `docs/USER_GUIDE_22072026124256.md`
-- `docs/ADMIN_OPERATIONS_GUIDE_22072026124256.md`
-- `docs/RELEASE_NOTES_22072026124256.md`
-- `docs/ROUTE_TO_UI_MATRIX.md`
+- `../documentation/DOCUMENTATION_INDEX_22072026124256.md`
+- `../documentation/INSTALLATION_AND_ACCEPTANCE_GUIDE_22072026124256.md`
+- `../documentation/USER_GUIDE_22072026124256.md`
+- `../documentation/ADMIN_OPERATIONS_GUIDE_22072026124256.md`
+- `../documentation/RELEASE_NOTES_22072026124256.md`
+- `../documentation/ROUTE_TO_UI_MATRIX.md`
 
 The documented distribution bundle keeps the deployable application under `app/` and the human-readable documentation under `documentation/`. Upload only the contents of `app/` to the public web root.
 
 ## 12. Release evidence
 
-- `FEATURE_AND_FUNCTION_MAP.txt`
-- `FEATURE_LOCK.json` and `FEATURE_LOCK.md`
-- `VERIFICATION_REPORT.txt`
-- `REMEDIATION_PATCHES.txt`
-- `UI_UX_REMEDIATION_REPORT.txt`
-- `handover/PHASE_V2_2_AUTONOMOUS_COMPLETION_RESULT.txt`
-- `handover/CONTROLLER_SUMMARY.txt`
-- `evidence/screenshots/`
-- `evidence/BUILD_ADVERSARIAL_RESULTS.txt`
+Included evidence:
 
-Verification classifications are `EXECUTED PASS`, `EXECUTED FAIL`, `STATIC PASS`, `STATIC FAIL` or `EXTERNAL NOT EXECUTED`. Missing tools or external systems are never represented as a pass.
+- `../documentation/FEATURE_AND_FUNCTION_MAP.txt`
+- `../documentation/VERIFICATION_REPORT.txt` — historical supplied evidence, explicitly not a production approval.
+- `../documentation/UI_UX_REMEDIATION_REPORT.txt`
+- `../documentation/RELEASE_HARDENING_REPORT_22072026125600.txt`
+- `CHANGES_hhmmddmmyyyy.txt` — actual timestamped task diff and verification results.
+
+Not included in this deployment bundle: the feature lock, source repository, package lockfile, automated tests, source build evidence, screenshots and handover-controller outputs referenced by the original V2.2.0 documentation.
+
+Verification classifications are `Passed`, `Failed`, `Not run` or `Not applicable`. Missing tools, source files, external systems and physical devices are never represented as a pass.
--- a/app/RELEASE_NOTES_22072026124256.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# GLMChat V2.2 Release Notes
-
-Updated: 22 July 2026
-
-## Release summary
-
-GLMChat V2.2 converts the agentic automation prototype into a maintainable, typed and server-enforced React/PHP production implementation. The interface uses mobile tiles and guided animated wizards while preserving the offline-first PWA, PHP 8.2+, SQLite and LiteSpeed shared-hosting architecture.
-
-## Major capabilities
-
-- Typed agents, teams, workflows and templates.
-- Immutable execution snapshots.
-- Separate workers, delegation, coordination, scoped memory, consensus and checkpoints.
-- Custom workflow stages with dependencies, retries, limits, approvals and expected outputs.
-- Genuine pause, resume, cancel, retry, optional-stage skip, reassignment, tool revocation and checkpoint branching.
-- Server-owned PHP tool registry with JSON-schema validation, project confinement and risk-based approvals.
-- Together tool-call continuation and structured output.
-- Separate vision-model routing and TTS playback.
-- Project workspaces, searchable history, prompts and typed templates.
-- IndexedDB-first drafts, definitions, runs and explicit offline recovery.
-- Conflict review and resolution.
-- Provider, model, cost, privacy and diagnostics controls.
-- Accessible Android-first wizard and run interfaces.
-
-## Security and release hardening
-
-- Restrictive same-origin CSP without `unsafe-eval`.
-- Direct-access denial for private source, storage, tests, handovers and evidence.
-- Explicit denial for `evidence/` and `implementation-prompts/` if accidentally uploaded.
-- Shell-only service-worker caching.
-- Content-addressed immutable assets and collision rejection.
-- Secret-safe bootstrap errors, logs, diagnostics and exports.
-- ZIP traversal, symlink and decompression protections.
-
-## Local and emulated verification
-
-The completed release passed the feature lock, Node contracts, PHP static/portable checks, migration scenarios, bootstrap preflight, offline/PWA browser tests, accessibility, Background Sync, required responsive widths, full-spec interface evidence, deterministic builds and adversarial asset checks.
-
-## External acceptance
-
-The following remain environment-specific and must be completed after deployment:
-
-- physical Android Chrome and TalkBack;
-- physical Samsung Internet;
-- live LiteSpeed root and subdirectory deployment;
-- live PHP extension, permission, rewrite and header validation;
-- live Together GLM-5.2, tool calling, vision and TTS.
-
-These checks are classified as external, not as completed passes.
--- a/app/USER_GUIDE_22072026124256.md
+++ /dev/null
@@ -1,119 +0,0 @@
-# GLMChat V2.2 User Guide
-
-Updated: 22 July 2026
-
-## 1. First use
-
-1. Open GLMChat on the HTTPS site.
-2. Create and confirm a six-digit PIN.
-3. Enter the Together API key and use the validation action.
-4. Review the privacy and offline explanation.
-5. Install the PWA when the browser offers installation.
-
-The API key is encrypted at rest and decrypted only when required. Protected destructive actions may request the PIN again.
-
-## 2. Home tiles
-
-- **Build an Agent:** create a specialist with instructions, memory, tools, approval policy and limits.
-- **Run Automation:** create a typed multi-stage workflow.
-- **Create Agent Team:** combine specialists using manager-led, sequential, parallel or consensus coordination.
-- **Tool Actions:** run approved project and verification actions.
-- **Projects:** organise files, instructions, chats, automations and activity.
-- **Templates & Prompts:** save and reuse prompts and typed automation definitions.
-- **Offline & Sync:** inspect drafts, queued actions, conflicts and retries.
-- **Usage & Costs:** review estimates, limits and execution telemetry.
-
-Status badges show running work, offline queues, conflicts or cost warnings without opening each section.
-
-## 3. Guided wizards
-
-Every automation wizard uses the same pattern:
-
-1. Choose the goal or task type.
-2. Select the project and visible context.
-3. Configure roles, stages or tools.
-4. Configure approvals, memory and limits.
-5. Review the exact typed configuration.
-6. Save as a draft or template, or launch.
-
-Drafts are stored in IndexedDB and can be resumed. Unsupported values prevent launch rather than being silently ignored.
-
-## 4. Chat
-
-- Select a project and only the files that should be included.
-- Attach supported images and files visibly before submission.
-- Select reasoning and structured-output options when needed.
-- Review the estimated maximum cost before sending.
-- Use Stop during streaming, then retry or regenerate where appropriate.
-- Copy, share or export a conversation as plain text.
-- Save a useful response as a prompt or convert it into a preseeded agent or workflow.
-- Use Speak to generate TTS playback when the provider is available.
-
-When offline, chat text remains a local draft. GLMChat does not silently submit it after reconnection.
-
-## 5. Projects
-
-Each project provides:
-
-- **Overview:** name, instructions, permissions, budget and retention.
-- **Chat:** project-aware conversation with visible selected context.
-- **Files:** upload, inspect, select, edit or remove supported files.
-- **Automations:** project agents, teams, workflows and run history.
-- **Activity:** project changes, outputs and audit information.
-
-ZIP imports are validated for traversal, symlinks, size and decompression limits.
-
-## 6. Agents and teams
-
-An agent definition controls its role, instructions, model behaviour, memory, allowed tools, approvals and execution limits.
-
-A team contains separate workers. Coordination options determine real execution behaviour:
-
-- **Manager-led:** a manager delegates work to specialists.
-- **Sequential:** each worker hands its output to the next.
-- **Parallel:** specialists work concurrently and results are combined.
-- **Consensus:** proposals and reviews are compared before a decision.
-
-The live run screen shows actual workers, assignments, outputs, costs and approval states.
-
-## 7. Workflows and run controls
-
-Workflow stages can define dependencies, assigned agents, tools, expected outputs, retries, cost/token limits, timeouts and checkpoints.
-
-Available controls depend on the current state:
-
-- **Pause:** requests a checkpoint-safe pause.
-- **Resume:** resumes from the persisted checkpoint without duplicating completed actions.
-- **Cancel:** stops the run.
-- **Retry:** retries a permitted failed stage within its limit.
-- **Skip:** skips an optional stage.
-- **Intervene:** provide guidance, revoke a tool, reassign a worker, approve a budget change or branch from a checkpoint where permitted.
-
-## 8. Tool approvals
-
-Tools are classified as read-only, mutating or destructive. Before approval, GLMChat shows:
-
-- the exact tool;
-- validated arguments;
-- affected project/files;
-- whether the operation is reversible;
-- the required approval level.
-
-Destructive operations require recent-PIN reauthentication. Approved arguments cannot be altered afterwards.
-
-## 9. Offline and synchronisation
-
-The Offline & Sync screen shows:
-
-- local drafts;
-- pending mutations;
-- authentication-paused operations;
-- exhausted retries;
-- conflicts;
-- recently synchronised actions.
-
-Conflicts can retain the local version, retain the server version, or use a supported manual merge. Queued operations preserve identifiers, request hashes and ordering.
-
-## 10. Privacy and recovery
-
-Privacy controls allow supported export, selective deletion, full reset and offline-access revocation. The error-recovery surface can retry, install a waiting update or clear only stale application-shell caches. It does not clear IndexedDB, projects or queued mutations unless a separate destructive action is explicitly confirmed.
--- a/app/VERIFICATION_REPORT.txt
+++ /dev/null
@@ -1,80 +0,0 @@
-GLMChat V2.2 — FINAL VERIFICATION REPORT
-Date: 2026-07-22
-Verdict: PASS FOR ALL AVAILABLE LOCAL/EMULATED GATES
-
-CLASSIFICATIONS
-- EXECUTED PASS: the command or browser check ran successfully.
-- STATIC PASS: source/configuration inspection met the requirement.
-- EXTERNAL NOT EXECUTED: requires the target host, device or provider.
-
-1. SOURCE, ARCHITECTURE AND FEATURE LOCK
-EXECUTED PASS  Maintainable React/PHP/Python syntax checks.
-EXECUTED PASS  Feature lock: 85 protected files and 26 capability contracts.
-STATIC PASS    Active frontend is source/frontend/react/*.js with local React/ReactDOM; the historical bundle is provenance only.
-STATIC PASS    PHP 8.2+/SQLite/LiteSpeed remains the sole production backend/deployment architecture.
-STATIC PASS    Versioned agents, teams, workflows, templates, immutable snapshots, workers, stages, events, checkpoints, approvals and interventions are mapped.
-STATIC PASS    Tool execution remains server-allowlisted, schema-validated and project-confined.
-
-2. AUTOMATED CONTRACT AND MIGRATION TESTS
-EXECUTED PASS  Node contract tests: 24 passed, 0 failed.
-EXECUTED PASS  PHP portable/static checks: 50 passed.
-EXECUTED PASS  SQLite migration scenarios: 4 passed, including fresh, upgrade, concurrency and conflict/idempotency paths.
-EXECUTED PASS  Bootstrap preflight/logger fallback under php -n.
-STATIC PASS    Migrations 011 and 012 add typed orchestration, templates, project metadata and branch lineage without destructive downgrade behaviour.
-
-3. BROWSER AND OFFLINE ACCEPTANCE
-EXECUTED PASS  Offline-first runtime: unlock, persistence, sync, replay, conflict, service worker and credential scan.
-EXECUTED PASS  PWA install gate: native prompt, dismissal, appinstalled, standalone bypass, display-mode transition and embedded-browser recovery.
-EXECUTED PASS  Accessibility: four Android layouts, 200% text, named controls, minimum targets, visible focus, one controlled live region and modal restoration.
-EXECUTED PASS  Background Sync: durable registration/fallback, exact attempts, auth pause, conflict, exhaustion, ordering, reload, profile restart, worker update, idempotency and private-cache exclusion.
-EXECUTED PASS  Responsive UI: 320, 360, 390, 412, 480 and 600 CSS-pixel portrait widths, team wizard, short landscape, reduced motion and dominant chat viewport.
-EXECUTED PASS  Full-spec UI evidence: 21 production screens/wizards at 390x844 with no horizontal overflow.
-STATIC PASS    Offline chat and AI/tool actions are never silently submitted.
-
-4. AGENTIC AND USER-FACING INTEGRATION
-STATIC PASS    Wizard fields serialize as typed server-validated definitions rather than prompt-only prose.
-STATIC PASS    Custom stages instantiate from the immutable workflow definition.
-STATIC PASS    Workers, delegation, coordination, checkpoints and approval/intervention events are persistent execution records.
-STATIC PASS    Pause/resume, cancellation, retry, optional-stage skip, reassignment, tool revocation and checkpoint branching have server routes/state transitions.
-STATIC PASS    Together integration includes bounded function calls, structured output, preserved reasoning and separate vision routing.
-STATIC PASS    Chat exposes visible file context, image attachments, preflight, Stop, regenerate, export, prompt saving and conversion to agent/workflow.
-STATIC PASS    Projects, templates, history, prompts, costs, provider settings, TTS, privacy and conflict resolution are reachable.
-
-5. BUILD, CACHE AND ADVERSARIAL CHECKS
-EXECUTED PASS  Two clean builds produced an identical tree SHA-256 recorded in evidence/BUILD_ADVERSARIAL_RESULTS.txt.
-EXECUTED PASS  A semantic source mutation changed the application asset filename.
-EXECUTED PASS  A falsified content fingerprint was rejected.
-EXECUTED PASS  Cross-release immutable-URL reuse with changed bytes was rejected.
-STATIC PASS    HTML, precache metadata, service-worker cache version, build manifest and SBOM are coordinated by one build.
-STATIC PASS    Private API/auth/setup responses are excluded before all Cache Storage branches.
-
-6. SECURITY AND DATA INTEGRITY
-STATIC PASS    CSP contains no unsafe-eval or inline event-handler allowance.
-STATIC PASS    Direct access to server/storage/source/tests/scripts/logs/databases/keys/evidence/implementation-prompts is denied.
-STATIC PASS    Session, CSRF, regeneration, rate limiting, PIN lockout and recent-PIN controls remain mapped.
-STATIC PASS    Together credentials retain authenticated encryption and secret-free logging/export/error handling.
-STATIC PASS    ZIP traversal, symlink, size and decompression protections remain mapped.
-STATIC PASS    Tool arguments reject unknown fields/types; unregistered tools and path escapes are rejected; loops and retries are bounded.
-STATIC PASS    IndexedDB and queued mutations survive shell-cache updates.
-
-7. PACKAGING
-STATIC PASS    Full-source profile excludes node_modules, nested release ZIPs, __pycache__, bytecode and transient build caches.
-STATIC PASS    Deployment-only profile excludes source, tests, scripts, handovers, evidence, logs, databases, keys and temporary files.
-STATIC PASS    ZIP member safety, manifest hashes, references, PHP/JavaScript syntax, JSON, CSP, protected storage and secret scans are mandatory clean-extraction gates.
-
-8. EXTERNAL ACCEPTANCE
-EXTERNAL NOT EXECUTED  Physical ARM64 Android Chrome and TalkBack.
-EXTERNAL NOT EXECUTED  Physical Samsung Internet.
-EXTERNAL NOT EXECUTED  Live LiteSpeed root and nested-subdirectory deployment, rewrites, headers and permissions.
-EXTERNAL NOT EXECUTED  Live PHP SQLite/cURL/mbstring/ZIP success path in this container because those extensions are absent.
-EXTERNAL NOT EXECUTED  Live Together key validation, GLM-5.2 inference, function calling, vision and TTS.
-
-FINAL ASSESSMENT
-No known source, contract, migration, browser-emulated, security-static, deterministic-build or packaging blocker remains. External checks are acceptance activities and are not represented as passes.
-
-9. DOCUMENTATION COMPLETENESS
-STATIC PASS    README, documentation index, user guide, installation/acceptance guide, administration/operations guide and release notes are present.
-STATIC PASS    Documentation distinguishes deployment, full-source and documented-bundle profiles.
-STATIC PASS    Upgrade and rollback guidance preserves storage, IndexedDB and queued mutations.
-STATIC PASS    External live-host, physical-device and provider acceptance remains explicitly unexecuted.
-
--- a/app/VERSION
+++ b/app/VERSION
@@ -1 +1 @@
-2.2.0
+2.2.1
--- a/app/assets/index-12db1e757e12.css
+++ b/app/assets/index-12db1e757e12.css
@@ -1,6 +1,6 @@
 /* GLMChat V2.2 production UI — agentic tiles, guided wizards and Android-first motion. */
 :root{
-  color-scheme:dark;
+  color-scheme:light;
   --surface:#0f0d16;
   --surface-2:#17131f;
   --surface-3:#201a2b;
@@ -390,3 +390,34 @@
   .composer__tools{flex-direction:column}.composer__tools .icon-button{width:36px;height:36px;min-height:36px}.attachment-preview,.chat-options{left:8px;right:8px;width:calc(100% - 16px)}
 }
 @media (prefers-reduced-motion:reduce){.stream-cursor{animation:none}}
+
+
+/* V2.2.1 light-only production palette and service-worker lifecycle notice. */
+:root{
+  color-scheme:light;
+  --surface:#f7f5fb;--surface-2:#ffffff;--surface-3:#f0ebf8;--surface-4:#e6def3;
+  --surface-glass:rgba(255,255,255,.92);--text:#21182f;--text-muted:#655b72;--text-dim:#81768e;
+  --primary:#6d3fd1;--primary-strong:#5125ad;--primary-soft:rgba(109,63,209,.11);
+  --success:#147a55;--warning:#8b5a00;--danger:#b4233c;--info:#175f9e;
+  --line:rgba(45,28,66,.14);--line-strong:rgba(109,63,209,.34);--shadow:0 18px 50px rgba(46,30,63,.14);
+  background:var(--surface);color:var(--text)
+}
+html,body,#root{background:var(--surface);color:var(--text)}
+.app-shell{background:radial-gradient(circle at 12% -4%,rgba(109,63,209,.10),transparent 28rem),radial-gradient(circle at 100% 28%,rgba(119,87,176,.08),transparent 26rem),var(--surface)}
+.topbar{background:linear-gradient(180deg,rgba(247,245,251,.98),rgba(247,245,251,.90))}
+.bottom-nav{background:rgba(255,255,255,.96)}
+.panel,.capability-tile{background:linear-gradient(145deg,#fff,#f7f3fb)}
+.hero{background:linear-gradient(145deg,rgba(109,63,209,.13),#fff 58%,#f7f3fb)}
+.panel--info{background:linear-gradient(145deg,rgba(23,95,158,.09),#fff)}
+.panel--warning{background:linear-gradient(145deg,rgba(139,90,0,.10),#fff)}
+.panel--danger{background:linear-gradient(145deg,rgba(180,35,60,.09),#fff)}
+.button--primary{color:#fff}.button--secondary{background:#fff}.badge{background:rgba(45,28,66,.06)}
+.run-controls{background:rgba(255,255,255,.94)}
+input,textarea,select,.field__control{background:#fff;color:var(--text);border-color:var(--line)}
+#glmchat-pwa-notice{position:fixed;z-index:1000;left:12px;right:12px;bottom:calc(78px + var(--safe-bottom));max-width:680px;margin:auto;padding:12px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;border:1px solid var(--line-strong);border-radius:15px;background:#fff;color:var(--text);box-shadow:var(--shadow)}
+#glmchat-pwa-notice[hidden]{display:none}#glmchat-pwa-notice button{min-height:44px;padding:8px 13px;border-radius:12px;background:var(--primary);color:#fff;font-weight:800}
+.wizard-actions,.modal__header,.modal__actions,.composer,.attachment-preview{background:rgba(255,255,255,.97)}
+.progress-track,.progress-track::-webkit-progress-bar{background:rgba(45,28,66,.10)}
+.choice-card__icon,.approval-card__icon,.stage-stepper__index,.file-row .ui-icon,.context-files label{background:rgba(45,28,66,.06)}
+.toggle-switch{background:rgba(45,28,66,.13)}
+.activity-console,.result-code,.reasoning-surface,.scope-preview,.stage-output pre,.fatal-screen code{background:#f1edf7;color:#2b2038}
--- a/app/assets/index-5aa4ab81f41a.js
+++ b/app/assets/index-5aa4ab81f41a.js
@@ -1,4 +1,4 @@
-/* GLMChat 2.2.0 deterministic maintainable React bundle. */
+/* GLMChat 2.2.1 hardened deterministic React deployment bundle. */
 /* ---- 00-core.js ---- */
 /* GLMChat V2.2 maintainable React source: offline-first core services. */
 (() => {
@@ -283,7 +283,7 @@
   class A11yBoundary extends React.Component { constructor(props){super(props);this.state={message:''};this.onAnnounce=this.onAnnounce.bind(this)}componentDidMount(){window.addEventListener('glmchat:a11y-announce',this.onAnnounce)}componentWillUnmount(){window.removeEventListener('glmchat:a11y-announce',this.onAnnounce)}onAnnounce(event){this.setState({message:String(event.detail?.message||'')})}render(){return h('div',{id:'glmchat-a11y-status',className:'sr-only',role: 'status','aria-live':'polite','aria-atomic':'true'},this.state.message)} }
   class ErrorBoundary extends React.Component {
     constructor(props){super(props);this.state={error:null,code:'',details:null,loopBlocked:false,updateReady:false,updating:false}}
-    componentDidCatch(error,info){const source=`${error?.name||'Error'}:${info?.componentStack||''}`;let hash=2166136261;for(let index=0;index<source.length;index+=1)hash=Math.imul(hash^source.charCodeAt(index),16777619);const code=`GLC-UI-${Math.abs(hash>>>0).toString(16).slice(0,6).toUpperCase()}`;const key=`glmchat:recovery:${code}`;const attempts=Number(sessionStorage.getItem(key)||0)+1;sessionStorage.setItem(key,String(attempts));const details={app_version:document.documentElement.dataset.appVersion||'2.2.0',build_hash:document.documentElement.dataset.buildHash||'unknown',cache_version:window.GLMCHAT_CACHE_VERSION||'unknown',route:location.hash||location.pathname,online:navigator.onLine,service_worker:{controlled:Boolean(navigator.serviceWorker?.controller),waiting:false},stack_fingerprint:code};this.setState({error,code,details,loopBlocked:attempts>2},()=>void this.inspectUpdate());void window.GLMChatPlatform?.recordDiagnostic?.({event_type:'javascript_error',route:'other',error_code:'other',value:1,metadata:details})}
+    componentDidCatch(error,info){const source=`${error?.name||'Error'}:${info?.componentStack||''}`;let hash=2166136261;for(let index=0;index<source.length;index+=1)hash=Math.imul(hash^source.charCodeAt(index),16777619);const code=`GLC-UI-${Math.abs(hash>>>0).toString(16).slice(0,6).toUpperCase()}`;const key=`glmchat:recovery:${code}`;const attempts=Number(sessionStorage.getItem(key)||0)+1;sessionStorage.setItem(key,String(attempts));const details={app_version:document.documentElement.dataset.appVersion||'2.2.1',build_hash:document.documentElement.dataset.buildHash||'unknown',cache_version:window.GLMCHAT_CACHE_VERSION||'unknown',route:location.hash||location.pathname,online:navigator.onLine,service_worker:{controlled:Boolean(navigator.serviceWorker?.controller),waiting:false},stack_fingerprint:code};this.setState({error,code,details,loopBlocked:attempts>2},()=>void this.inspectUpdate());void window.GLMChatPlatform?.recordDiagnostic?.({event_type:'javascript_error',route:'other',error_code:'other',value:1,metadata:details})}
     async inspectUpdate(){try{const registration=await navigator.serviceWorker?.getRegistration?.();const waiting=Boolean(registration?.waiting);this.setState(previous=>({updateReady:waiting,details:{...(previous.details||{}),service_worker:{controlled:Boolean(navigator.serviceWorker?.controller),waiting}}}))}catch{}}
     async installUpdate(){this.setState({updating:true});try{const registration=await navigator.serviceWorker?.getRegistration?.();if(!registration)return location.reload();if(!registration.waiting)await registration.update();const waiting=registration.waiting;if(!waiting){this.setState({updating:false,updateReady:false});return location.reload()}await new Promise(resolve=>{let settled=false;const finish=()=>{if(settled)return;settled=true;resolve()};navigator.serviceWorker.addEventListener('controllerchange',finish,{once:true});waiting.postMessage({type:'SKIP_WAITING'});setTimeout(finish,4000)});location.reload()}catch{this.setState({updating:false});}}
     async clearShell(){if(!('caches'in window))return location.reload();const keys=await caches.keys();await Promise.all(keys.filter(key=>key.startsWith('glmchat-v')).map(key=>caches.delete(key)));location.reload()}
@@ -397,7 +397,7 @@
     installed(){this.props.onInstalled?.()}
     async install(){if(!this.state.deferred)return;this.setState({installing:true});try{await this.state.deferred.prompt();const choice=await this.state.deferred.userChoice;if(choice?.outcome==='accepted')this.installed();else this.setState({installState:'dismissed'})}finally{this.setState({installing:false,deferred:null})}}
     async copyAddress(){try{await(window.GLMChatNative?.copyText?.(location.href)||navigator.clipboard.writeText(location.href));this.setState({copied:true})}catch{}}
-    render(){const embedded=/; wv\)|Instagram|FBAN|FBAV|Line\/|Twitter/i.test(navigator.userAgent);return h('main',{className:'install-gate','data-install-state':this.state.installState},h('section',{className:'install-gate__card'},h('img',{className:'install-gate__brand',src:'./icons/icon-192.png',alt:''}),h('span',{className:'eyebrow'},'ANDROID-FIRST OFFLINE PWA'),h('h1',{id:'install-gate-title'},'Install GLMChat to continue'),h('p',null,'Installation gives GLMChat a stable full-screen workspace, offline shell and controlled update recovery.'),this.state.deferred?h(C.Button,{kind:'primary',className:'install-primary',onClick:()=>this.install(),disabled:this.state.installing},this.state.installing?'Opening installer…':'Install GLMChat'):null,embedded?h(C.Panel,{tone:'warning',className:'install-warning'},h('strong',null,'Open this page in Chrome or Samsung Internet'),h('p',null,'Embedded browsers may not offer reliable installation or offline updates.')):null,h(C.Panel,{tone:'info',className:'install-manual'},h('h2',null,'Chrome on Android'),h('p',null,'Open the browser menu and choose “Add to Home screen” or “Install app”.'),h('h2',null,'Samsung Internet'),h('p',null,'Open the browser menu, choose “Add page to”, then “Home screen”.')),h('div',{className:'stacked-actions'},h(C.Button,{kind:'secondary',onClick:()=>window.GLMChatNative?.openInChrome?.(location.href)},'Open in Chrome'),h(C.Button,{kind:'ghost',onClick:()=>this.copyAddress()},this.state.copied?'Address copied':'Copy address')),h('small',{className:'install-status'},this.state.copied?'Address copied to the clipboard.':this.state.installState==='dismissed'?'Installation was dismissed. Use the manual browser instructions.':'Installation instructions ready.'),h('small',null,'Local verification can use ?preview=1. User data is never stored in Cache Storage.')))}
+    render(){const embedded=/; wv\)|Instagram|FBAN|FBAV|Line\/|Twitter/i.test(navigator.userAgent);return h('main',{className:'install-gate','data-install-state':this.state.installState},h('section',{className:'install-gate__card'},h('img',{className:'install-gate__brand',src:'./icons/icon-192.png',alt:''}),h('span',{className:'eyebrow'},'ANDROID-FIRST OFFLINE PWA'),h('h1',{id:'install-gate-title'},'Install GLMChat to continue'),h('p',null,'Installation gives GLMChat a stable full-screen workspace, offline shell and controlled update recovery.'),this.state.deferred?h(C.Button,{kind:'primary',className:'install-primary',onClick:()=>this.install(),disabled:this.state.installing},this.state.installing?'Opening installer…':'Install GLMChat'):null,embedded?h(C.Panel,{tone:'warning',className:'install-warning'},h('strong',null,'Open this page in Chrome or Samsung Internet'),h('p',null,'Embedded browsers may not offer reliable installation or offline updates.')):null,h(C.Panel,{tone:'info',className:'install-manual'},h('h2',null,'Chrome on Android'),h('p',null,'Open the browser menu and choose “Add to Home screen” or “Install app”.'),h('h2',null,'Samsung Internet'),h('p',null,'Open the browser menu, choose “Add page to”, then “Home screen”.')),h('div',{className:'stacked-actions'},h(C.Button,{kind:'secondary',onClick:()=>window.GLMChatNative?.openInChrome?.(location.href)},'Open in Chrome'),h(C.Button,{kind:'ghost',onClick:()=>this.copyAddress()},this.state.copied?'Address copied':'Copy address')),h('small',{className:'install-status'},this.state.copied?'Address copied to the clipboard.':this.state.installState==='dismissed'?'Installation was dismissed. Use the manual browser instructions.':'Installation instructions ready.'),h('small',null,'Sensitive offline records are encrypted in IndexedDB. User data is never stored in Cache Storage.')))}
   }
 
   function WelcomeScreen({onStart,onLearn}){return h('main',{className:'welcome-screen'},h('div',{className:'welcome-screen__art'},h('img',{src:'./icons/icon-192.png',alt:''})),h('span',{className:'eyebrow'},'PRIVATE AGENTIC WORKSPACE'),h('h1',null,'Build, automate and verify with GLMChat'),h('p',null,'Run guided agents and multi-stage workflows while approvals, costs, project data and offline recovery remain visible.'),h('div',{className:'welcome-benefits'},h('span',null,'✓ PHP + SQLite on your host'),h('span',null,'✓ Offline local workspace'),h('span',null,'✓ Controlled autonomous tools')),h('div',{className:'stacked-actions'},h(C.Button,{kind:'primary',onClick:onStart},'Get started'),h(C.Button,{kind:'ghost',onClick:onLearn},'Learn more')))}
@@ -517,8 +517,7 @@
   class App extends React.Component {
     constructor(props) {
       super(props)
-      const query = new URLSearchParams(location.search)
-      const installed = query.get('preview') === '1' || matchMedia('(display-mode: standalone)').matches || navigator.standalone === true
+      const installed = matchMedia('(display-mode: standalone)').matches || navigator.standalone === true
       this.state = {
         installed,
         boot: 'loading',
@@ -578,7 +577,7 @@
       addEventListener('hashchange', this.onHash)
       this.displayMedia.addEventListener?.('change', this.onDisplayMode)
       this.displayMedia.addListener?.(this.onDisplayMode)
-      try { if ('serviceWorker' in navigator) await navigator.serviceWorker.register('./sw.js', { scope: './' }) } catch {}
+      if ('serviceWorker' in navigator) await window.GLMChatPlatform?.registerServiceWorker?.()
       await this.bootstrap()
       const query = new URLSearchParams(location.search)
       const shortcut = query.get('shortcut')
--- a/app/assets/offline-runtime.js
+++ b/app/assets/offline-runtime.js
@@ -31,19 +31,27 @@
   })
 
   const DB_NAME = 'glmchat-offline-v2'
-  const DB_VERSION = 3
+  const DB_VERSION = 4
   const PBKDF2_ITERATIONS = 310000
+  const MAX_OFFLINE_CREDENTIAL_AGE_MS = 7 * 24 * 60 * 60 * 1000
+  const ENCRYPTED_RECORD_SCHEMA = 'glmchat-aes-gcm-v1'
+  const SENSITIVE_STORES = new Set(['cache', 'mutations', 'conflicts', 'drafts'])
   const MAX_LOCK_FAILURES = 5
   const LOCK_WINDOW_MS = 10 * 60 * 1000
   const apiPrefix = new URL('api/', document.baseURI).pathname
   const originalFetch = window.fetch.bind(window)
-  const memory = { session: null, status: 'initialising', syncing: false, lastSync: null, backend: null }
+  const memory = { session: null, dataKey: null, dataCryptoKey: null, status: 'initialising', syncing: false, lastSync: null, backend: null }
   let dbPromise
   let syncTimer
 
   const encoder = new TextEncoder()
   const decoder = new TextDecoder('utf-8', { fatal: true })
-  const b64 = bytes => btoa(String.fromCharCode(...new Uint8Array(bytes)))
+  const b64 = bytes => {
+    const input = new Uint8Array(bytes)
+    let binary = ''
+    for (let offset = 0; offset < input.length; offset += 0x8000) binary += String.fromCharCode(...input.subarray(offset, offset + 0x8000))
+    return btoa(binary)
+  }
   const unb64 = text => Uint8Array.from(atob(text), character => character.charCodeAt(0))
   const nowIso = () => new Date().toISOString()
   const uuid = () => crypto.randomUUID()
@@ -76,7 +84,7 @@
       return { supported: true, registered: true }
     } catch (error) {
       if (TEST_MODE) testTrace.push({ event: 'sync-registration-failed', at: Date.now(), error: String(error?.message || error) })
-      console.warn('Background Sync registration failed; queued mutation remains durable.', error)
+      console.warn('Deferred synchronisation registration failed; queued mutation remains durable.', error)
       return { supported: true, registered: false, error: String(error?.message || error) }
     }
   }
@@ -128,25 +136,90 @@
     })
   }
 
-  const get = async (store, key) => {
+  const rawGet = async (store, key) => {
     const db = await openDatabase()
     const transaction = db.transaction(store, 'readonly')
     return requestResult(transaction.objectStore(store).get(key))
   }
-  const getAll = async store => {
+  const rawGetAll = async store => {
     const db = await openDatabase()
     const transaction = db.transaction(store, 'readonly')
     return requestResult(transaction.objectStore(store).getAll())
   }
-  const put = (store, value) => withStore(store, 'readwrite', objectStore => objectStore.put(value))
+  const rawPut = (store, value) => withStore(store, 'readwrite', objectStore => objectStore.put(value))
+
+  async function localDataCryptoKey() {
+    if (!memory.dataKey) throw Object.assign(new Error('Local data is locked.'), { code: 'OFFLINE_DATA_LOCKED' })
+    if (!memory.dataCryptoKey) memory.dataCryptoKey = await crypto.subtle.importKey('raw', memory.dataKey, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'])
+    return memory.dataCryptoKey
+  }
+
+  async function encryptLocalRecord(value, context) {
+    const key = await localDataCryptoKey()
+    const iv = crypto.getRandomValues(new Uint8Array(12))
+    const additionalData = encoder.encode(`GLMChat local record v1:${context}`)
+    const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, additionalData }, key, encoder.encode(JSON.stringify(value)))
+    return { schema: ENCRYPTED_RECORD_SCHEMA, iv: b64(iv), ciphertext: b64(ciphertext) }
+  }
+
+  async function decryptLocalRecord(envelope, context) {
+    if (!envelope || envelope.schema !== ENCRYPTED_RECORD_SCHEMA) return envelope
+    const key = await localDataCryptoKey()
+    const additionalData = encoder.encode(`GLMChat local record v1:${context}`)
+    const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: unb64(envelope.iv), additionalData }, key, unb64(envelope.ciphertext))
+    return JSON.parse(decoder.decode(plaintext))
+  }
+
+  function protectedMetadata(store, value) {
+    if (store === 'cache') return { key: value.key, updatedAt: value.updatedAt }
+    if (store === 'mutations') return {
+      mutation_id: value.mutation_id, status: value.status, attempts: value.attempts, max_attempts: value.max_attempts,
+      nextAttemptAt: value.nextAttemptAt, createdAt: value.createdAt, updatedAt: value.updatedAt
+    }
+    if (store === 'conflicts') return { id: value.id, status: value.status, createdAt: value.createdAt }
+    if (store === 'drafts') return { id: value.id, conversation_id: value.conversation_id, createdAt: value.createdAt }
+    return {}
+  }
+
+  async function protectRecord(store, value) {
+    if (!SENSITIVE_STORES.has(store)) return value
+    if (!memory.dataKey) throw Object.assign(new Error('Unlock local data before storing sensitive offline content.'), { code: 'OFFLINE_DATA_LOCKED' })
+    return { ...protectedMetadata(store, value), encrypted_record: await encryptLocalRecord(value, `${store}:${value.key || value.mutation_id || value.id}`) }
+  }
+
+  async function unprotectRecord(store, value) {
+    if (!value || !SENSITIVE_STORES.has(store)) return value
+    if (value.encrypted_record) {
+      if (!memory.dataKey) return store === 'cache' ? undefined : { ...protectedMetadata(store, value), locked: true }
+      return decryptLocalRecord(value.encrypted_record, `${store}:${value.key || value.mutation_id || value.id}`)
+    }
+    if (!memory.dataKey) return store === 'cache' ? undefined : { ...protectedMetadata(store, value), locked: true }
+    return value
+  }
+
+  const get = async (store, key) => unprotectRecord(store, await rawGet(store, key))
+  const getAll = async store => Promise.all((await rawGetAll(store)).map(value => unprotectRecord(store, value))).then(values => values.filter(Boolean))
+  const put = async (store, value) => rawPut(store, await protectRecord(store, value))
   const remove = (store, key) => withStore(store, 'readwrite', objectStore => objectStore.delete(key))
   const clear = store => withStore(store, 'readwrite', objectStore => objectStore.clear())
 
+  async function migrateSensitiveStores() {
+    if (!memory.dataKey) return
+    for (const store of SENSITIVE_STORES) {
+      const records = await rawGetAll(store)
+      for (const record of records) {
+        if (!record?.encrypted_record) await rawPut(store, await protectRecord(store, record))
+      }
+    }
+  }
+
   async function cacheGet(key) {
     return (await get('cache', key))?.payload ?? null
   }
   async function cachePut(key, payload) {
+    if (!memory.dataKey) return false
     await put('cache', { key, payload, updatedAt: Date.now() })
+    return true
   }
 
   function jsonResponse(data, status = 200, headers = {}) {
@@ -204,15 +277,40 @@
   }
 
   async function configureOfflinePin(pin, session) {
-    if (!/^\d{6}$/.test(pin) || !session?.csrf_token) return
-    const salt = crypto.getRandomValues(new Uint8Array(16))
-    const { verifier, keyBytes } = await derivePin(pin, salt)
-    const encrypted = await encryptSession({ ...session, offline_saved_at: nowIso() }, keyBytes)
+    if (!/^\d{6}$/.test(pin) || !session?.csrf_token) return false
+    const existing = await rawGet('meta', 'offline-auth')
+    let salt = existing?.salt ? unb64(existing.salt) : crypto.getRandomValues(new Uint8Array(16))
+    let derived = await derivePin(pin, salt)
+    if (existing && b64(derived.verifier) !== existing.verifier) {
+      const retainedRecords = (await Promise.all([...SENSITIVE_STORES].map(store => rawGetAll(store)))).reduce((total, records) => total + records.length, 0)
+      if (retainedRecords > 0) {
+        memory.session = session
+        memory.dataKey = null
+        memory.dataCryptoKey = null
+        updateStatus('offline-rekey-required')
+        console.warn('Offline records remain protected by the previous PIN. Clear offline data or unlock with the previous PIN before replacing offline credentials.')
+        window.GLMChatA11y?.announce?.('Offline records remain protected by the previous PIN. Review offline data before replacing the offline PIN.', 'offline:rekey-required')
+        return false
+      }
+      salt = crypto.getRandomValues(new Uint8Array(16))
+      derived = await derivePin(pin, salt)
+    }
+    const encrypted = await encryptSession({ ...session, offline_saved_at: nowIso() }, derived.keyBytes)
     await put('meta', {
-      key: 'offline-auth', salt: b64(salt), verifier: b64(verifier), iterations: PBKDF2_ITERATIONS,
+      key: 'offline-auth', salt: b64(salt), verifier: b64(derived.verifier), iterations: PBKDF2_ITERATIONS,
       ...encrypted, failures: 0, lockedUntil: 0, updatedAt: Date.now()
     })
     memory.session = session
+    memory.dataKey = new Uint8Array(derived.keyBytes)
+    memory.dataCryptoKey = null
+    await migrateSensitiveStores()
+    return true
+  }
+
+  function clearOfflineSession() {
+    memory.session = null
+    memory.dataKey = null
+    memory.dataCryptoKey = null
   }
 
   async function unlockOffline(pin) {
@@ -233,9 +331,16 @@
     }
     try {
       const session = await decryptSession(auth, keyBytes)
+      const savedAt = Date.parse(String(session.offline_saved_at || ''))
+      if (!Number.isFinite(savedAt) || Date.now() - savedAt > MAX_OFFLINE_CREDENTIAL_AGE_MS) {
+        throw Object.assign(new Error('Offline access has expired. Reconnect and sign in again.'), { code: 'OFFLINE_REAUTH_REQUIRED' })
+      }
       auth.failures = 0
       auth.lockedUntil = 0
       await put('meta', auth)
+      memory.dataKey = new Uint8Array(keyBytes)
+      memory.dataCryptoKey = null
+      await migrateSensitiveStores()
       memory.session = {
         ...session,
         absolute_expires_at: Math.floor(Date.now() / 1000) + 8 * 60 * 60,
@@ -246,8 +351,10 @@
       window.GLMChatA11y?.announce?.('Offline workspace unlocked.', 'unlock:complete')
       scheduleSync(100)
       return memory.session
-    } catch {
+    } catch (error) {
+      clearOfflineSession()
       window.GLMChatA11y?.announce?.('Offline workspace could not be unlocked.', 'unlock:failed')
+      if (error?.code === 'OFFLINE_REAUTH_REQUIRED') throw error
       throw Object.assign(new Error('Offline credential data could not be decrypted.'), { code: 'OFFLINE_AUTH_CORRUPT' })
     }
   }
@@ -428,7 +535,7 @@
       const cached = unpack(await cacheGet('status'))
       const auth = await get('meta', 'offline-auth')
       const status = cached || { state: auth ? 'installed' : 'unavailable', authenticated: false, api_key_configured: Boolean(auth), https: location.protocol === 'https:', runtime: { database: 'offline', storage: 'indexeddb' } }
-      return jsonResponse({ ...status, authenticated: Boolean(memory.session), offline: true })
+      return jsonResponse({ ...status, authenticated: Boolean(memory.session && memory.dataKey), offline: true })
     }
     if (path === 'login' && method === 'POST') {
       const data = await bodyJson(request)
@@ -436,14 +543,14 @@
       catch (error) { return jsonResponse({ code: error.code || 'INVALID_CREDENTIALS', message: error.message }, error.code === 'OFFLINE_LOCKED' ? 429 : 401) }
     }
     if (path === 'session' && method === 'GET') {
-      return memory.session ? jsonResponse(memory.session) : jsonResponse({ code: 'UNAUTHORIZED', message: 'Unlock with your PIN to access offline data.' }, 401)
+      return memory.session && memory.dataKey ? jsonResponse(memory.session) : jsonResponse({ code: 'UNAUTHORIZED', message: 'Unlock with your PIN to access offline data.' }, 401)
     }
     if (path === 'logout' && method === 'POST') {
-      memory.session = null
+      clearOfflineSession()
       updateStatus('offline-locked')
       return jsonResponse({ logged_out: true })
     }
-    if (!memory.session && !['status', 'login'].includes(path)) return jsonResponse({ code: 'UNAUTHORIZED', message: 'Offline data is locked.' }, 401)
+    if ((!memory.session || !memory.dataKey) && !['status', 'login'].includes(path)) return jsonResponse({ code: 'UNAUTHORIZED', message: 'Offline data is locked.' }, 401)
 
     if (path === 'conversations' && method === 'POST') return jsonResponse(await localConversationCreate(await bodyJson(request)), 201)
     if (/^conversations\/[0-9a-f-]+$/i.test(path)) {
@@ -504,7 +611,8 @@
     const session = unpack(data)
     const body = await bodyJson(request)
     if (session?.csrf_token && /^\d{6}$/.test(String(body.pin || ''))) {
-      await configureOfflinePin(String(body.pin), session)
+      const configured = await configureOfflinePin(String(body.pin), session)
+      if (!configured) return
       await cachePut('status', wrapData({ state: 'installed', authenticated: true, api_key_configured: true, https: location.protocol === 'https:', runtime: { database: 'ok', storage: 'ok' } }))
       const mutations = await getAll('mutations').catch(() => [])
       for (const mutation of mutations.filter(item => item.status === 'auth_required')) {
@@ -536,7 +644,7 @@
           memory.backend = 'online'
           await cacheNetworkResponse(route, request, response)
           await captureAuthentication(route, request, response)
-          if (routeWithoutQuery(route) === 'logout' && response.ok) memory.session = null
+          if (routeWithoutQuery(route) === 'logout' && response.ok) clearOfflineSession()
           updateStatus(memory.session ? 'online' : 'online-locked')
           return response
         }
@@ -562,7 +670,7 @@
     const remainingNow = allMutations.filter(item => item.status !== 'delivered').length
     if (memory.syncing) return { status: 'busy', remaining: remainingNow }
     if (!navigator.onLine) return { status: 'offline', remaining: remainingNow }
-    if (!memory.session?.csrf_token) {
+    if (!memory.session?.csrf_token || !memory.dataKey) {
       if (remainingNow) window.GLMChatA11y?.announce?.('Sign in again to continue synchronisation.', 'sync:auth-required')
       return { status: 'auth_required', remaining: remainingNow }
     }
@@ -716,8 +824,9 @@
   function statusLabel(state, pending, conflicts) {
     if (memory.syncing) return `Syncing ${pending}`
     if (conflicts) return `${conflicts} conflict${conflicts === 1 ? '' : 's'}`
-    if (navigator.onLine === false) return memory.session ? `Offline · ${pending} queued` : 'Offline · locked'
+    if (navigator.onLine === false) return memory.session && memory.dataKey ? `Offline · ${pending} queued` : 'Offline · locked'
     if (memory.backend === 'unavailable') return `Backend unavailable · ${pending} queued`
+    if (state === 'offline-rekey-required') return 'Offline PIN review required'
     if (pending) return `${pending} pending`
     if (memory.lastSync) return 'Synced'
     return state === 'initialising' ? 'Preparing offline data' : 'Online'
@@ -732,7 +841,7 @@
   async function clearOfflineData({ requireConfirmation = true } = {}) {
     if (requireConfirmation && !confirm('Clear this device’s cached GLMChat data, queued changes, drafts and offline PIN? Server data will not be deleted.')) return { cleared: false }
     await Promise.all(['meta', 'cache', 'mutations', 'conflicts', 'drafts'].map(clear))
-    memory.session = null
+    clearOfflineSession()
     memory.lastSync = null
     updateStatus('cleared')
     window.GLMChatNative?.haptic?.('destructive')
@@ -773,19 +882,19 @@
     panel.innerHTML = `
       <div class="glmchat-offline-head"><strong>Offline data</strong><button type="button" data-close aria-label="Close">×</button></div>
       <p>${navigator.onLine ? (memory.backend === 'unavailable' ? 'The network is available but the backend cannot be reached.' : 'Backend connectivity is available.') : 'This device is offline.'}</p>
-      <dl><div><dt>Queued changes</dt><dd>${mutations.length}</dd></div><div><dt>Conflicts</dt><dd>${conflicts.length}</dd></div><div><dt>Offline lock</dt><dd>${memory.session ? 'Unlocked' : 'Locked'}</dd></div></dl>
+      <dl><div><dt>Queued changes</dt><dd>${mutations.length}</dd></div><div><dt>Conflicts</dt><dd>${conflicts.length}</dd></div><div><dt>Offline workspace</dt><dd>${memory.session && memory.dataKey ? 'Unlocked' : 'Locked'}</dd></div></dl>
       <div class="glmchat-offline-actions">
-        <button type="button" data-sync ${!navigator.onLine || !memory.session ? 'disabled' : ''}>Synchronise now</button>
-        ${exhausted ? '<button type="button" data-retry-exhausted>Retry exhausted changes</button>' : ''}
-        <button type="button" data-lock ${!memory.session ? 'disabled' : ''}>Lock local data</button>
+        <button type="button" data-sync ${!navigator.onLine || !memory.session || !memory.dataKey ? 'disabled' : ''}>Synchronise now</button>
+        ${exhausted && memory.dataKey ? '<button type="button" data-retry-exhausted>Retry exhausted changes</button>' : ''}
+        <button type="button" data-lock ${!memory.session || !memory.dataKey ? 'disabled' : ''}>Lock offline workspace</button>
         <button type="button" data-clear>Clear offline data</button>
       </div>
-      ${conflicts.length ? `<h3>Conflicts</h3>${conflicts.map(conflict => `<article><strong>${escapeHtml(conflict.entity_type || 'item')}</strong><small>${escapeHtml(conflict.entity_id || '')}</small><div><button data-resolve="keep_server" data-id="${conflict.id}">Keep server</button><button data-resolve="keep_local" data-id="${conflict.id}">Keep local</button>${conflict.entity_type === 'conversation' ? `<button data-resolve="duplicate" data-id="${conflict.id}">Duplicate local</button>` : ''}</div></article>`).join('')}` : ''}
-      <p class="glmchat-offline-note">The Together API key is never stored in IndexedDB. Provider-dependent actions are not executed offline.</p>`
+      ${conflicts.length && memory.dataKey ? `<h3>Conflicts</h3>${conflicts.map(conflict => `<article><strong>${escapeHtml(conflict.entity_type || 'item')}</strong><small>${escapeHtml(conflict.entity_id || '')}</small><div><button data-resolve="keep_server" data-id="${conflict.id}">Keep server</button><button data-resolve="keep_local" data-id="${conflict.id}">Keep local</button>${conflict.entity_type === 'conversation' ? `<button data-resolve="duplicate" data-id="${conflict.id}">Duplicate local</button>` : ''}</div></article>`).join('')}` : ''}
+      <p class="glmchat-offline-note">Cached content, drafts and queued payloads are encrypted while stored. The Together API key is never stored in IndexedDB. Provider-dependent actions are not executed offline.</p>`
     panel.querySelector('[data-close]')?.addEventListener('click', () => { panel.hidden = true; button.focus() })
     panel.querySelector('[data-sync]')?.addEventListener('click', () => { void syncPending({ source: 'manual', force: true }) })
     panel.querySelector('[data-retry-exhausted]')?.addEventListener('click', () => { void retryExhaustedMutations() })
-    panel.querySelector('[data-lock]')?.addEventListener('click', () => { memory.session = null; updateStatus('offline-locked'); panel.hidden = true })
+    panel.querySelector('[data-lock]')?.addEventListener('click', () => { clearOfflineSession(); updateStatus('offline-locked'); panel.hidden = true })
     panel.querySelector('[data-clear]')?.addEventListener('click', async () => {
       const result = await clearOfflineData()
       if (result.cleared) panel.hidden = true
@@ -850,7 +959,7 @@
       const data = event.data
       if (!glmchatValidSyncMessage(data)) return
       if (data.type === GLMCHAT_SYNC_MESSAGES.REQUEST) {
-        Promise.resolve(syncPending({ source: 'background-sync', force: true }))
+        Promise.resolve(syncPending({ source: 'reopen-sync', force: true }))
           .then(glmchatPostSyncResult)
           .catch(error => glmchatPostSyncResult({ status: 'failed', code: String(error?.message || error) }))
         return
@@ -894,7 +1003,7 @@
     database: DB_NAME,
     version: DB_VERSION,
     sync: () => syncPending({ source: 'diagnostic', force: true }),
-    state: () => ({ status: memory.status, backend: memory.backend, syncing: memory.syncing, unlocked: Boolean(memory.session) })
+    state: () => ({ status: memory.status, backend: memory.backend, syncing: memory.syncing, unlocked: Boolean(memory.session && memory.dataKey) })
   }
   if (TEST_MODE) Object.assign(diagnostic, {
     queue: queueMutation,
--- a/app/assets/platform-enhancements.js
+++ b/app/assets/platform-enhancements.js
@@ -8,7 +8,7 @@
   const HAS_DATA_KEY = 'glmchat.diagnostics.has-data.v1'
   const MAX_RECORDS = 500
   const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000
-  const APP_VERSION = '2.2.0'
+  const APP_VERSION = '2.2.1'
   const EVENT_TYPES = new Set(['lcp', 'inp', 'cls', 'javascript_error', 'service_worker_failure', 'indexeddb_failure', 'retry_exhaustion', 'conflict_count', 'api_timing'])
   const ROUTES = new Set(['app', 'setup', 'login', 'chat', 'projects', 'settings', 'privacy', 'offline', 'sync', 'status', 'health', 'other'])
   const ERROR_CODES = new Set(['abort', 'network', 'timeout', 'quota', 'constraint', 'invalid_state', 'security', 'not_found', 'server_4xx', 'server_5xx', 'other'])
@@ -220,6 +220,63 @@
         } catch { /* Unsupported observer type. */ }
       }
     }
+  }
+
+
+  let serviceWorkerRegistrationPromise
+
+  function showPwaNotice(message, actionLabel = '', action = null) {
+    let notice = document.getElementById('glmchat-pwa-notice')
+    if (!notice) {
+      notice = document.createElement('aside')
+      notice.id = 'glmchat-pwa-notice'
+      notice.setAttribute('role', 'status')
+      notice.setAttribute('aria-live', 'polite')
+      document.body.appendChild(notice)
+    }
+    notice.replaceChildren()
+    const text = document.createElement('span'); text.textContent = String(message)
+    notice.appendChild(text)
+    if (actionLabel && typeof action === 'function') {
+      const button = document.createElement('button'); button.type = 'button'; button.textContent = actionLabel
+      button.addEventListener('click', action, { once: true })
+      notice.appendChild(button)
+    }
+    notice.hidden = false
+  }
+
+  function offerServiceWorkerUpdate(registration) {
+    if (!registration?.waiting) return
+    showPwaNotice('A verified GLMChat update is ready.', 'Install update', () => {
+      let reloading = false
+      navigator.serviceWorker.addEventListener('controllerchange', () => {
+        if (reloading) return
+        reloading = true
+        location.reload()
+      }, { once: true })
+      registration.waiting?.postMessage({ type: 'SKIP_WAITING' })
+    })
+  }
+
+  async function registerServiceWorker() {
+    if (!('serviceWorker' in navigator)) return null
+    if (serviceWorkerRegistrationPromise) return serviceWorkerRegistrationPromise
+    serviceWorkerRegistrationPromise = navigator.serviceWorker.register('./sw.js', { scope: './' }).then(registration => {
+      offerServiceWorkerUpdate(registration)
+      registration.addEventListener('updatefound', () => {
+        const worker = registration.installing
+        worker?.addEventListener('statechange', () => {
+          if (worker.state === 'installed' && navigator.serviceWorker.controller) offerServiceWorkerUpdate(registration)
+        })
+      })
+      return registration
+    }).catch(error => {
+      void recordDiagnostic({ event_type: 'service_worker_failure', error_code: errorCategory(error), route: 'app' })
+      showPwaNotice('Offline installation and update support could not start. Reload after checking browser storage and security settings.')
+      window.GLMChatA11y?.announce?.('Offline application support could not start.', 'service-worker:failed')
+      return null
+    })
+    return serviceWorkerRegistrationPromise
   }
 
   function installSettingsUi() {
@@ -249,7 +306,7 @@
     void enhance()
   }
 
-  window.GLMChatPlatform = Object.freeze({ navigationSupported, installNavigationBridge, diagnosticsEnabled, setDiagnosticsEnabled, recordDiagnostic, diagnosticsSummary, exportDiagnostics, clearDiagnostics })
+  window.GLMChatPlatform = Object.freeze({ navigationSupported, installNavigationBridge, diagnosticsEnabled, setDiagnosticsEnabled, recordDiagnostic, diagnosticsSummary, exportDiagnostics, clearDiagnostics, registerServiceWorker })
   installDiagnosticsHooks()
   if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', installSettingsUi, { once: true })
   else installSettingsUi()
--- a/app/assets/workbox-window.prod.es5-cadd21be4a88.js
+++ /dev/null
@@ -1,2 +0,0 @@
-try{self["workbox:window:7.4.0"]&&_()}catch{}function b(t,r){return new Promise((function(n){var u=new MessageChannel;u.port1.onmessage=function(s){n(s.data)},t.postMessage(r,[u.port2])}))}function P(t,r){(r==null||r>t.length)&&(r=t.length);for(var n=0,u=Array(r);n<r;n++)u[n]=t[n];return u}function j(t,r,n){return r&&(function(u,s){for(var c=0;c<s.length;c++){var o=s[c];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(u,W(o.key),o)}})(t.prototype,r),Object.defineProperty(t,"prototype",{writable:!1}),t}function S(t,r){var n=typeof Symbol<"u"&&t[Symbol.iterator]||t["@@iterator"];if(n)return(n=n.call(t)).next.bind(n);if(Array.isArray(t)||(n=(function(s,c){if(s){if(typeof s=="string")return P(s,c);var o={}.toString.call(s).slice(8,-1);return o==="Object"&&s.constructor&&(o=s.constructor.name),o==="Map"||o==="Set"?Array.from(s):o==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o)?P(s,c):void 0}})(t))||r){n&&(t=n);var u=0;return function(){return u>=t.length?{done:!0}:{done:!1,value:t[u++]}}}throw new TypeError(`Invalid attempt to iterate non-iterable instance.
-In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function w(t,r){return w=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(n,u){return n.__proto__=u,n},w(t,r)}function W(t){var r=(function(n,u){if(typeof n!="object"||!n)return n;var s=n[Symbol.toPrimitive];if(s!==void 0){var c=s.call(n,u);if(typeof c!="object")return c;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(n)})(t,"string");return typeof r=="symbol"?r:r+""}try{self["workbox:core:7.4.0"]&&_()}catch{}var m=function(){var t=this;this.promise=new Promise((function(r,n){t.resolve=r,t.reject=n}))};function y(t,r){var n=location.href;return new URL(t,n).href===new URL(r,n).href}var d=function(t,r){this.type=t,Object.assign(this,r)};function l(t,r,n){return t&&t.then||(t=Promise.resolve(t)),r?t.then(r):t}function k(){}var L={type:"SKIP_WAITING"};function E(t,r){return t&&t.then?t.then(k):Promise.resolve()}var O=(function(t){function r(c,o){var e,i;return o===void 0&&(o={}),(e=t.call(this)||this).nn={},e.tn=0,e.rn=new m,e.en=new m,e.on=new m,e.un=0,e.an=new Set,e.cn=function(){var f=e.fn,a=f.installing;e.tn>0||!y(a.scriptURL,e.sn.toString())||performance.now()>e.un+6e4?(e.vn=a,f.removeEventListener("updatefound",e.cn)):(e.hn=a,e.an.add(a),e.rn.resolve(a)),++e.tn,a.addEventListener("statechange",e.ln)},e.ln=function(f){var a=e.fn,v=f.target,h=v.state,p=v===e.vn,g={sw:v,isExternal:p,originalEvent:f};!p&&e.mn&&(g.isUpdate=!0),e.dispatchEvent(new d(h,g)),h==="installed"?e.wn=self.setTimeout((function(){h==="installed"&&a.waiting===v&&e.dispatchEvent(new d("waiting",g))}),200):h==="activating"&&(clearTimeout(e.wn),p||e.en.resolve(v))},e.yn=function(f){var a=e.hn,v=a!==navigator.serviceWorker.controller;e.dispatchEvent(new d("controlling",{isExternal:v,originalEvent:f,sw:a,isUpdate:e.mn})),v||e.on.resolve(a)},e.gn=(i=function(f){var a=f.data,v=f.ports,h=f.source;return l(e.getSW(),(function(){e.an.has(h)&&e.dispatchEvent(new d("message",{data:a,originalEvent:f,ports:v,sw:h}))}))},function(){for(var f=[],a=0;a<arguments.length;a++)f[a]=arguments[a];try{return Promise.resolve(i.apply(this,f))}catch(v){return Promise.reject(v)}}),e.sn=c,e.nn=o,navigator.serviceWorker.addEventListener("message",e.gn),e}var n,u;u=t,(n=r).prototype=Object.create(u.prototype),n.prototype.constructor=n,w(n,u);var s=r.prototype;return s.register=function(c){var o=(c===void 0?{}:c).immediate,e=o!==void 0&&o;try{var i=this;return l((function(f,a){var v=f();return v&&v.then?v.then(a):a(v)})((function(){if(!e&&document.readyState!=="complete")return E(new Promise((function(f){return window.addEventListener("load",f)})))}),(function(){return i.mn=!!navigator.serviceWorker.controller,i.dn=i.pn(),l(i.bn(),(function(f){i.fn=f,i.dn&&(i.hn=i.dn,i.en.resolve(i.dn),i.on.resolve(i.dn),i.dn.addEventListener("statechange",i.ln,{once:!0}));var a=i.fn.waiting;return a&&y(a.scriptURL,i.sn.toString())&&(i.hn=a,Promise.resolve().then((function(){i.dispatchEvent(new d("waiting",{sw:a,wasWaitingBeforeRegister:!0}))})).then((function(){}))),i.hn&&(i.rn.resolve(i.hn),i.an.add(i.hn)),i.fn.addEventListener("updatefound",i.cn),navigator.serviceWorker.addEventListener("controllerchange",i.yn),i.fn}))})))}catch(f){return Promise.reject(f)}},s.update=function(){try{return this.fn?l(E(this.fn.update())):l()}catch(c){return Promise.reject(c)}},s.getSW=function(){return this.hn!==void 0?Promise.resolve(this.hn):this.rn.promise},s.messageSW=function(c){try{return l(this.getSW(),(function(o){return b(o,c)}))}catch(o){return Promise.reject(o)}},s.messageSkipWaiting=function(){this.fn&&this.fn.waiting&&b(this.fn.waiting,L)},s.pn=function(){var c=navigator.serviceWorker.controller;return c&&y(c.scriptURL,this.sn.toString())?c:void 0},s.bn=function(){try{var c=this;return l((function(o,e){try{var i=o()}catch(f){return e(f)}return i&&i.then?i.then(void 0,e):i})((function(){return l(navigator.serviceWorker.register(c.sn,c.nn),(function(o){return c.un=performance.now(),o}))}),(function(o){throw o})))}catch(o){return Promise.reject(o)}},j(r,[{key:"active",get:function(){return this.en.promise}},{key:"controlling",get:function(){return this.on.promise}}])})((function(){function t(){this.Pn=new Map}var r=t.prototype;return r.addEventListener=function(n,u){this.jn(n).add(u)},r.removeEventListener=function(n,u){this.jn(n).delete(u)},r.dispatchEvent=function(n){n.target=this;for(var u,s=S(this.jn(n.type));!(u=s()).done;)(0,u.value)(n)},r.jn=function(n){return this.Pn.has(n)||this.Pn.set(n,new Set),this.Pn.get(n)},t})());export{O as Workbox,d as WorkboxEvent,b as messageSW};
--- a/app/index.html
+++ b/app/index.html
@@ -3,11 +3,11 @@
 <head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
-  <meta name="theme-color" content="#0f0d16" />
-  <meta name="color-scheme" content="dark" />
+  <meta name="theme-color" content="#6d3fd1" />
+  <meta name="color-scheme" content="light" />
   <meta name="mobile-web-app-capable" content="yes" />
   <meta name="apple-mobile-web-app-capable" content="yes" />
-  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
+  <meta name="apple-mobile-web-app-status-bar-style" content="default" />
   <meta name="description" content="Offline-first GLM-5.2 agentic automation workspace" />
   <meta name="referrer" content="no-referrer" />
   <link rel="icon" type="image/png" sizes="48x48" href="./icons/favicon-48.png" />
--- a/app/manifest.webmanifest
+++ b/app/manifest.webmanifest
@@ -16,8 +16,8 @@
       "auto"
     ]
   },
-  "background_color": "#0f0d16",
-  "theme_color": "#8b5cf6",
+  "background_color": "#f7f5fb",
+  "theme_color": "#6d3fd1",
   "lang": "en",
   "orientation": "any",
   "categories": [
@@ -47,6 +47,19 @@
     }
   ],
   "shortcuts": [
+    {
+      "name": "New chat",
+      "short_name": "Chat",
+      "description": "Start a new GLMChat conversation",
+      "url": "./?shortcut=new-chat",
+      "icons": [
+        {
+          "src": "icons/icon-192.png",
+          "sizes": "192x192",
+          "type": "image/png"
+        }
+      ]
+    },
     {
       "name": "New agent",
       "short_name": "Agent",
--- a/app/precache-manifest.json
+++ b/app/precache-manifest.json
@@ -8,7 +8,6 @@
     "./assets/native-android.js",
     "./assets/offline-runtime.js",
     "./assets/platform-enhancements.js",
-    "./assets/workbox-window.prod.es5-cadd21be4a88.js",
     "./icons/icon-192.png",
     "./icons/icon-512.png",
     "./icons/maskable-512.png",
--- a/app/sbom.cdx.json
+++ b/app/sbom.cdx.json
@@ -1,1142 +1,1384 @@
 {
   "bomFormat": "CycloneDX",
+  "specVersion": "1.5",
+  "serialNumber": "urn:uuid:b28bc7c4-0ed5-4624-8280-d96a5557e0cd",
+  "version": 1,
+  "metadata": {
+    "timestamp": "2026-07-22T13:05:37Z",
+    "component": {
+      "type": "application",
+      "name": "GLMChat",
+      "version": "2.2.1"
+    },
+    "properties": [
+      {
+        "name": "glmchat.release_profile",
+        "value": "deployment-only"
+      },
+      {
+        "name": "glmchat.file_inventory_scope",
+        "value": "all app files except self-referential SBOM, deployment manifest and timestamped CHANGES report"
+      },
+      {
+        "name": "glmchat.production_acceptance",
+        "value": "pending live LiteSpeed, Together provider and physical Android checks"
+      }
+    ]
+  },
   "components": [
     {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "316352b48ca4ab91bd224b9501281aa18ec99a842321e5a573407c54e9e6d666"
-        }
-      ],
+      "type": "file",
       "name": ".htaccess",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "ccb4e5e56e820d756cf17c60704179c37d0622dad652649b94ccfd4ce7679f61"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": ".htaccess"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "type": "file",
-      "name": "GLMCHAT_V2.0.1_STAGED_NATIVE_UI_SYNC_GLM52_REMEDIATION_PLAN.txt",
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "ec60cbcac61137b76bc05824d89629cb2a71789aca9eb060c96969e9fca0a2a8"
-        }
-      ]
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "5aa4ab81f41a5ab3f7db559f402db4461ab45ad26c2e50ef92310da982aa77a6"
-        }
-      ],
+      ]
+    },
+    {
+      "type": "file",
+      "name": "README.md",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "033f1b24cd2f0ad288a8a1ac7e24ec375d6b41fc1af7145a61d77a54be8f3e10"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "README.md"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "VERSION",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "fe650098093fad42070e8a2cacfb216a6c416af57bd26dd6ff479fef69f8bc71"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "VERSION"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "assets/index-12db1e757e12.css",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "0be61b61bf68efd5d3b686f864d16afd335bd2783b9770eab43e26ab206eaa1f"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "assets/index-12db1e757e12.css"
+        }
+      ]
+    },
+    {
+      "type": "file",
       "name": "assets/index-5aa4ab81f41a.js",
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/react"
-        }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "12db1e757e12bf520c023406cb6a3c0493e1ec607ceee1d2db853734b22123f6"
-        }
-      ],
-      "name": "assets/index-12db1e757e12.css",
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/app.css"
-        }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "819dc1ca9010dbc0d7923d6d12a81fc14988cf37950d19360d19234617aad33f"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "assets/index-5aa4ab81f41a.js"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "assets/native-android.js",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "a37e0c7ce1266fbd38a9700861f7a4be866c5356e89fea7fe051bb79c79dd3b1"
         }
       ],
-      "name": "assets/native-android.js",
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/native-android.js"
-        }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "9128c1a2af53d092e75d6b2a042a0f7cd9b63ffa30c113c303087ad9a9bbe68a"
-        }
-      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "assets/native-android.js"
+        }
+      ]
+    },
+    {
+      "type": "file",
       "name": "assets/offline-runtime.js",
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/offline-runtime.js"
-        }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "e8e10779682865a30202a9de83b7dd11d3519ec77dca529e39ba8b304906002f"
-        }
-      ],
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "ffd184b6ab6a0bfcbc3d019237eaec565281dc2b49ead347c7afa481e9092f21"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "assets/offline-runtime.js"
+        }
+      ]
+    },
+    {
+      "type": "file",
       "name": "assets/platform-enhancements.js",
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/platform-enhancements.js"
-        }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "cadd21be4a88598a4dec91a26a025df0a5790ff6baec54a884e401c3f5c95557"
-        }
-      ],
-      "name": "assets/workbox-window.prod.es5-cadd21be4a88.js",
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/workbox-window.prod.es5.js"
-        }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "5bfdd5b9d06a028a7e3a7dea6c7eb6c1f23fd77fa51a68933dc56573d8f26d8d"
-        }
-      ],
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "9b8b12480872be990657eaa29e5c45ca68a26ad724b1c6f03b60e55e9b662d5a"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "assets/platform-enhancements.js"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "assets/react-de59b0d4504a.js",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "de59b0d4504aa0350c13775fec9b78859f3eda83dea21d60dded98bb7866d54a"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "assets/react-de59b0d4504a.js"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "assets/react-dom-c7a65157caeb.js",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "c7a65157caebd9af7e108f1f1eb8b6e2c8fafb141d0f4c2a4d4dfd851c52ec9e"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "assets/react-dom-c7a65157caeb.js"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "icons/apple-touch-icon-180.png",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "f687b4e12f74d170a0a75ab90db1e8fa91f5d94f41ca72036dc1f489b2fa3e87"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "icons/apple-touch-icon-180.png"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "icons/favicon-48.png",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "857a74f6ad6676d76d0987eb1d1807391b98941266cdd0fe27797c0a381af899"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "icons/favicon-48.png"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "icons/icon-192.png",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "8102fd9132b1d9ea699f1ef64e1eaa92ea4bb26bd499f52c3d1b016e34af4a8c"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "icons/icon-192.png"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "icons/icon-512.png",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "137ae5425ea40f4b2a8d120043d1bbfa8ecabe0fc24b54fa12a99fcdf0c6a814"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "icons/icon-512.png"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "icons/maskable-512.png",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "719630a1beb4b03418c3ee4ef71d791d62936d8a0f936919914062fb7440799a"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "icons/maskable-512.png"
+        }
+      ]
+    },
+    {
+      "type": "file",
       "name": "index.html",
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/index.html"
-        }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "b8bce236cd71f6c2951f65a1e31484e943bdb1df9f87d2e0259b9ce337999751"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "index.html"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "index.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "c14b80baca91d6ae64de9582a07b08e469ba506d6073908fe775e4468e283651"
         }
       ],
-      "name": "index.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "index.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "87030677651dfe01b1a3430e9a139c59417f83b83f54e291f051565a01f922fa"
-        }
-      ],
+      ]
+    },
+    {
+      "type": "file",
       "name": "manifest.webmanifest",
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/manifest.webmanifest"
-        }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "39ea0b447188897f8631d4256c568ea74b0cdf3d638e35a1b337bf14ef23b74b"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "manifest.webmanifest"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "offline.html",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "4a4a9b3a8f84882abcafbb3644341d8c2f67a5306eab1141679fb8c5e38ba1bd"
         }
       ],
-      "name": "offline.html",
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/offline.html"
-        }
-      ],
-      "type": "file",
-      "version": "2.0.1"
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "offline.html"
+        }
+      ]
     },
     {
       "type": "file",
       "name": "precache-manifest.json",
-      "version": "2.0.1",
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "a3225ab62fab3b89c48435489477713bbfa4c86f7150a44c4ebd014b2d6cc2ce"
-        }
-      ],
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "30d40c049b18b5df687ee187475a52860bc656743fc433ce115bec34e04d0fc5"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "precache-manifest.json"
         }
       ]
     },
     {
+      "type": "file",
+      "name": "server/.htaccess",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "564a1d9c2ce7748072c292d709b85d0e30118cf41ae77c4824b8fc5fe791d1bc"
         }
       ],
-      "name": "server/.htaccess",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/.htaccess"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/bootstrap.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "ba93c35159c40cfee195ab77609d6815c77f1d4fadd7957a81e820232010f815"
         }
       ],
-      "name": "server/bootstrap.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/bootstrap.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "36c74f54f6d8cc92cfb9508ec92aa8395f3f274815b58ab20b485b1165493978"
-        }
-      ],
+      ]
+    },
+    {
+      "type": "file",
       "name": "server/config.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "9db4eaeed0dbf51a2aefacdd49326b0f74737816af32e08bf3f1f85da9a73163"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/config.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/index.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "659897c68a88eaca8c7ad98824ab35e862f89d374a54ff843f6bac4673688ed8"
         }
       ],
-      "name": "server/index.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/index.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/AgenticDefinition.php",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "417702fe37ea56a1b5596df9f49b1eb5bbb6c7aaea539fa2d5b3200f99a67df7"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "server/lib/AgenticDefinition.php"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Api.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "076a1ad99c88751ef0da73fde9c6584586b33bcbf0ca0f2db7615438aea67c32"
         }
       ],
-      "name": "server/lib/Api.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Api.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "531dfe530d459e9cb1a9385694ed61ac4a7497443ba2d22b36f1a4f93f913a69"
-        }
-      ],
+      ]
+    },
+    {
+      "type": "file",
       "name": "server/lib/Auth.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "72ddfd845aaf8fb9fb617073ee38de16b3204c64720e777a55b9c1b95ede7ca6"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Auth.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/ContextBudget.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "6d9720d40dd8e27f42f4ce0c010a72acac66e997a21af913fefdfe807dc63a6b"
         }
       ],
-      "name": "server/lib/ContextBudget.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/ContextBudget.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Cost.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "fdf2fce16d8842adb93e8835df3f07804a8c3dce0a77794dd78a01197075b31e"
         }
       ],
-      "name": "server/lib/Cost.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Cost.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Crypto.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "eaeee06c6eb827ab8fcbb74fe73214da0149cfc472394730b611e7eebeb5161e"
         }
       ],
-      "name": "server/lib/Crypto.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Crypto.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/DataManagement.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "13e7e6872030d4f5e620a9f0d4a365220a8e3a0cfd5204a80f3e0e043d8c8c1c"
         }
       ],
-      "name": "server/lib/DataManagement.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/DataManagement.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Database.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "f4bdaeecfe1ce6177f024b8c294841da546e3879e92ed111ae959f732326296e"
         }
       ],
-      "name": "server/lib/Database.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Database.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Environment.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "265e66034f77e988816b7dc7263e6cc2ddc6c8ef1dd7fc72681e7d98b928259b"
         }
       ],
-      "name": "server/lib/Environment.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Environment.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Errors.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "3c90a4f9dc66443425f20e1344081b39c213edcf00c2c1a7ff6ad551ec944db7"
         }
       ],
-      "name": "server/lib/Errors.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Errors.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Http.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "7dacb9c60c8c7303e79f7e4934ef9f5ea8eb8bc75ef3b9273b60599876f5d4f1"
         }
       ],
-      "name": "server/lib/Http.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Http.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Json.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "10933477743b3de2e12522621dac5c62dda314156e9d461c59688f8e7ad7ec31"
         }
       ],
-      "name": "server/lib/Json.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Json.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Logger.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "f54404b0ca4743c7468ef0c4c82cfe9750e2fa8d50e637d87158523f58c664d9"
         }
       ],
-      "name": "server/lib/Logger.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Logger.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/MessageSequence.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "78b0eedaf1d4ca63259dacc29418863b1fa66b66e4ef9b8bd6edd85389995290"
         }
       ],
-      "name": "server/lib/MessageSequence.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/MessageSequence.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Migrations.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "77f9de6584585728e02eae2af66553bd9c3779b50bfd1d14a4ea9f792d03f58e"
         }
       ],
-      "name": "server/lib/Migrations.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Migrations.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Projects.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "c361e770b74d36f71c3abe4e77ec125ecea5cd914aa30f350f88aba729e30160"
         }
       ],
-      "name": "server/lib/Projects.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Projects.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Prompts.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "27f0b4a5dafbb75472c43034f00037c42433dbf8bd0a3f6c467c2fbb58971aec"
         }
       ],
-      "name": "server/lib/Prompts.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Prompts.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/RateLimiter.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "1d48d175720519efab50221706e5909289657a106c495cf88226ee16767afd82"
         }
       ],
-      "name": "server/lib/RateLimiter.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/RateLimiter.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Sse.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "e9b9180ce2eeae82ca592be1004caf9d4ffebd6468861ee612bc7d2de0d92e8e"
         }
       ],
-      "name": "server/lib/Sse.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Sse.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Sync.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "0aa7d0cb962a4890205c5a114e43cadd6d657840ef62da9137bf94057b78467c"
         }
       ],
-      "name": "server/lib/Sync.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Sync.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/TextFile.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "b2be0cab39ad1a6c0512246afcf87f11b1fd9bc7bf60ebf091b90af9f6676196"
         }
       ],
-      "name": "server/lib/TextFile.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/TextFile.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "6cd3c64cc5bc420c087926ba1235464296ddc09e6b465c73a1d885d94104dfb3"
-        }
-      ],
+      ]
+    },
+    {
+      "type": "file",
       "name": "server/lib/Together.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "cdfef3adaa99a358f551426716d449f8a96c9764ed97e0a67b1840acd7a52582"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Together.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/ToolExecutor.php",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "c2dfd850e233d7d175ed032a8c4768f57177105926823013f72241dffe9d441c"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "server/lib/ToolExecutor.php"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/ToolRegistry.php",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "6ad295c7652c2988f7f2ddc7d017f1d44657334107401bc16408b442d6beed9f"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "server/lib/ToolRegistry.php"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/Validator.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "12d7e66460efcbd8933304f7358516ba136033466ac657f2dc2699267671d4ef"
         }
       ],
-      "name": "server/lib/Validator.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/Validator.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/WorkflowEngine.php",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "40a4fa1977032085411ebdde54789ad2e2f16396de692570096999947ef0859a"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "server/lib/WorkflowEngine.php"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/lib/ZipGuard.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "330f6277770e9448ab33980f77efa962c2aed5fcfb3e29d756ce4ab1d6c92e9f"
         }
       ],
-      "name": "server/lib/ZipGuard.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/lib/ZipGuard.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/001_initial.sql",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "111a63b757682a68cb6246cb1d43fb6b0b7076d7a9b6f591378b1dbf37ced42c"
         }
       ],
-      "name": "server/migrations/001_initial.sql",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/migrations/001_initial.sql"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/002_indexes.sql",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "d74d0c6953069b06da5e90fd21c64cb503fb7067f62126c98e87e2ea91497d72"
         }
       ],
-      "name": "server/migrations/002_indexes.sql",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/migrations/002_indexes.sql"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/003_seed.sql",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "70866eb08afd0d1e34cd032e66911ba92b914c36d0d271d28f6653c58eff9801"
         }
       ],
-      "name": "server/migrations/003_seed.sql",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/migrations/003_seed.sql"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/004_message_sequence.sql",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "a5583cfbf8f84b421b2ad0f946acc4df483f388a9e1742a14747ed423ba897e9"
         }
       ],
-      "name": "server/migrations/004_message_sequence.sql",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/migrations/004_message_sequence.sql"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/005_workflow_recovery_and_cost.sql",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "4491a244af087f98f1ed8d6b1d694402f70f75010cc8de73e42225d02c29bb1b"
         }
       ],
-      "name": "server/migrations/005_workflow_recovery_and_cost.sql",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/migrations/005_workflow_recovery_and_cost.sql"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/006_v2_0_1_settings.sql",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "84f1933e17628e378008c81274b1c3f77593b4b74e7c0a79141db3edd634dbf2"
         }
       ],
-      "name": "server/migrations/006_v2_0_1_settings.sql",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/migrations/006_v2_0_1_settings.sql"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/007_execution_integrity.sql",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "9a5f669fbaa91126f720c9fb0774c4a73cee7428950157312021388875d15a45"
         }
       ],
-      "name": "server/migrations/007_execution_integrity.sql",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/migrations/007_execution_integrity.sql"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/008_worker_leases.sql",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "415e1b16c31915b6a76967a6488a8ed52bbb3ef669d6f38b093751b665c4c0ef"
         }
       ],
-      "name": "server/migrations/008_worker_leases.sql",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/migrations/008_worker_leases.sql"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/009_offline_sync.sql",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "4e52ae8d181fcba0e5641c6ccad728302265b7f6b19c068e7e4487bd61e308be"
         }
       ],
-      "name": "server/migrations/009_offline_sync.sql",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/migrations/009_offline_sync.sql"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/010_data_controls.sql",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "41b176a3f31204ba951e645f75e94beccfcbe24c249842cae99961c20a0f1811"
         }
       ],
-      "name": "server/migrations/010_data_controls.sql",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/migrations/010_data_controls.sql"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/011_agentic_automation.sql",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "299149f3dc87664491ab3a3651086b19a981b32415c8fa8b9107c1827ed0a80a"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "server/migrations/011_agentic_automation.sql"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/012_project_templates_and_branches.sql",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "8903dcdc70daab611c68b2b1f0e630c41dd4e61afe6facec2707bd15fbc5d46c"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "server/migrations/012_project_templates_and_branches.sql"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/migrations/013_release_hardening.sql",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "d0c14421953770549a473583c30c1fea61b7e08a4b84947043eeadfbdb248e39"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "server/migrations/013_release_hardening.sql"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/auth.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "ade5c0aa6670094258d07f8f3dded7dc8dbc94b7a02e2b847d871764d1fcf900"
         }
       ],
-      "name": "server/routes/auth.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/auth.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/automation.php",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "ff6905f180731ecf3d6f221992f31ff10777712b5c35af6d06c252ecfd874c1f"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "server/routes/automation.php"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/chat.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "78cf418ee1c65847dc3d28ad1872a6cabd2a2f047323a4a5ab689a401cbe5b3a"
         }
       ],
-      "name": "server/routes/chat.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/chat.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/conversations.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "9f3043d84994f97896b4e5ff5d939c75a4564589c05bc6c4ecbf10a0d1cf2b16"
         }
       ],
-      "name": "server/routes/conversations.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/conversations.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/cost.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "49e35511934fb992d9944d3c872484e76c3c9514caadb0737648f88a4f6226b7"
         }
       ],
-      "name": "server/routes/cost.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/cost.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/data.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "a989e2d58f653c8aed45c8cee45ba3160048182bba720a21523fae4758e48410"
         }
       ],
-      "name": "server/routes/data.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/data.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/projects.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "352407e11d4452052260329234876bf4a732e3f22256903609b6580dce173032"
         }
       ],
-      "name": "server/routes/projects.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/projects.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/prompts.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "53b60d56ef6535e9a16f804fd6222c8214b12591283d5bee7c2908815ea80c3c"
         }
       ],
-      "name": "server/routes/prompts.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/prompts.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/settings.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "1e16ab97488551b4b187fe6e0bb5d3015801f8e16bcee26ff1789f1a632816a0"
         }
       ],
-      "name": "server/routes/settings.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/settings.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/sync.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "0e8cc5fe67302825710cd2f7f4a28d9354006ed7bd44e30144cbea017f67fbf1"
         }
       ],
-      "name": "server/routes/sync.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/sync.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/templates.php",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "818ce51fdc3c2e181bad62facf921f4350e8d7d397d31a4aedf960904271c195"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "server/routes/templates.php"
+        }
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/tts.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "121f87af26863c3b9bdf717815ec683e462d034175b3cb3e70d6d8eb5d14b8cf"
         }
       ],
-      "name": "server/routes/tts.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/tts.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "server/routes/workflow.php",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "b13af8560f90ac1ca502a79c14e7caa7cee0b8464f14293f389aabd9213a66c9"
         }
       ],
-      "name": "server/routes/workflow.php",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "server/routes/workflow.php"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
+      ]
+    },
+    {
+      "type": "file",
+      "name": "storage/.htaccess",
+      "version": "2.2.1",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "f19e0a6ec4db41ce29fa249819d1121707608a25e2f697f023d94788f9751a0b"
         }
       ],
-      "name": "storage/.htaccess",
-      "properties": [
-        {
-          "name": "glmchat.source",
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
           "value": "storage/.htaccess"
         }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "ce6f5a2d696521b7490669beeacf870cabde5f0c9aa954296f045a82f4abe008"
-        }
-      ],
+      ]
+    },
+    {
+      "type": "file",
       "name": "sw.js",
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/sw.js"
-        }
-      ],
-      "type": "file",
-      "version": "2.0.1"
-    },
-    {
-      "name": "PHP",
-      "scope": "required",
-      "type": "framework",
-      "version": ">=8.2"
-    },
-    {
-      "name": "PHP curl extension",
-      "scope": "required",
-      "type": "library"
-    },
-    {
-      "name": "PHP fileinfo extension",
-      "scope": "required",
-      "type": "library"
-    },
-    {
-      "name": "PHP mbstring extension",
-      "scope": "required",
-      "type": "library"
-    },
-    {
-      "name": "PHP openssl extension",
-      "scope": "required",
-      "type": "library"
-    },
-    {
-      "name": "PHP sqlite3 extension",
-      "scope": "required",
-      "type": "library"
-    },
-    {
-      "name": "PHP zip extension",
-      "scope": "required",
-      "type": "library"
-    },
-    {
-      "type": "file",
-      "name": "assets/react-de59b0d4504a.js",
+      "version": "2.2.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "6e39df25a83086351a45eaf4508ab31e45e6ef0b484f351f07e7ea31e16212f4"
+        }
+      ],
+      "properties": [
+        {
+          "name": "glmchat.deployment_path",
+          "value": "sw.js"
+        }
+      ]
+    },
+    {
+      "type": "library",
+      "name": "react",
+      "version": "16.0.0",
       "hashes": [
         {
           "alg": "SHA-256",
           "content": "de59b0d4504aa0350c13775fec9b78859f3eda83dea21d60dded98bb7866d54a"
         }
       ],
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/vendor/react.production.min.js"
-        }
-      ]
-    },
-    {
-      "type": "file",
-      "name": "assets/react-dom-c7a65157caeb.js",
-      "hashes": [
-        {
-          "alg": "SHA-256",
-          "content": "c7a65157caebd9af7e108f1f1eb8b6e2c8fafb141d0f4c2a4d4dfd851c52ec9e"
-        }
-      ],
-      "properties": [
-        {
-          "name": "glmchat.source",
-          "value": "source/frontend/vendor/react-dom.production.min.js"
-        }
-      ]
-    },
-    {
-      "type": "library",
-      "name": "React",
-      "version": "16.0.0",
       "licenses": [
         {
           "license": {
@@ -1146,15 +1388,25 @@
       ],
       "properties": [
         {
-          "name": "glmchat.delivery",
-          "value": "vendored local production UMD"
+          "name": "glmchat.deployment_path",
+          "value": "assets/react-de59b0d4504a.js"
+        },
+        {
+          "name": "glmchat.review_status",
+          "value": "legacy dependency retained; source-level upgrade requires the matching source repository"
         }
       ]
     },
     {
       "type": "library",
-      "name": "ReactDOM",
+      "name": "react-dom",
       "version": "16.0.1",
+      "hashes": [
+        {
+          "alg": "SHA-256",
+          "content": "c7a65157caebd9af7e108f1f1eb8b6e2c8fafb141d0f4c2a4d4dfd851c52ec9e"
+        }
+      ],
       "licenses": [
         {
           "license": {
@@ -1164,27 +1416,14 @@
       ],
       "properties": [
         {
-          "name": "glmchat.delivery",
-          "value": "vendored local production UMD"
+          "name": "glmchat.deployment_path",
+          "value": "assets/react-dom-c7a65157caeb.js"
+        },
+        {
+          "name": "glmchat.review_status",
+          "value": "legacy dependency retained; source-level upgrade requires the matching source repository"
         }
       ]
     }
-  ],
-  "metadata": {
-    "component": {
-      "name": "GLMChat",
-      "type": "application",
-      "version": "2.2.0"
-    },
-    "glmchat_rebuild_note": "Final build regenerates content-addressed asset filenames and build-manifest hashes.",
-    "properties": [
-      {
-        "name": "glmchat.release_stage",
-        "value": "Phases 0-10 complete"
-      }
-    ]
-  },
-  "serialNumber": "urn:uuid:8d5461f4-afb9-7f6f-6007-737198eb6602",
-  "specVersion": "1.5",
-  "version": 1
+  ]
 }
--- a/app/server/config.php
+++ b/app/server/config.php
@@ -2,7 +2,7 @@
 declare(strict_types=1);
 
 return [
-    'app_version' => '2.2.0',
+    'app_version' => '2.2.1',
     'app_root' => dirname(__DIR__),
     'storage' => dirname(__DIR__) . '/storage',
     'database' => dirname(__DIR__) . '/storage/glmchat.db',
--- a/app/server/lib/Auth.php
+++ b/app/server/lib/Auth.php
@@ -70,7 +70,7 @@
                     $this->db->setSetting('setup_completed_at', gmdate('c'));
                     $this->db->setSetting('setup_state', 'complete');
                     $this->db->setSetting('auth_mode', 'six_digit_pin');
-                    $lock = Json::encode(['version' => (string) ($this->config['app_version'] ?? '2.2.0'), 'installed_at' => gmdate('c')]);
+                    $lock = Json::encode(['version' => (string) ($this->config['app_version'] ?? '2.2.1'), 'installed_at' => gmdate('c')]);
                     if (getenv('APP_ENV') === 'test' && getenv('GLMCHAT_TEST_LOCK_WRITE_FAIL') === '1') {
                         throw new \RuntimeException('Injected lock write failure.');
                     }
--- a/app/server/lib/Together.php
+++ b/app/server/lib/Together.php
@@ -327,7 +327,7 @@
                 ];
             }
 
-            if (getenv('GLMCHAT_MOCK_PROVIDER') === '1') {
+            if (getenv('APP_ENV') === 'test' && getenv('GLMCHAT_MOCK_PROVIDER') === '1') {
                 $mock = $this->mockAdvancedResponse($conversation, $tools, $options);
                 $response = ['data' => $mock, 'headers' => [], 'attempts' => 1];
             } else {
@@ -432,7 +432,7 @@
         ?callable $cancelled = null
     ): array {
         $cancel = $this->cancellationClosure($cancelled);
-        if (getenv('GLMCHAT_MOCK_PROVIDER') === '1') {
+        if (getenv('APP_ENV') === 'test' && getenv('GLMCHAT_MOCK_PROVIDER') === '1') {
             $results = [];
             foreach ($requests as $key => $request) {
                 if ($cancel()) {
--- /dev/null
+++ b/app/server/migrations/013_release_hardening.sql
@@ -0,0 +1,6 @@
+-- GLMChat V2.2.1 release-hardening metadata. Runtime data encryption is client-side and migrates after the next successful PIN unlock.
+INSERT INTO settings(key,value) VALUES('app_version','2.2.1')
+ON CONFLICT(key) DO UPDATE SET value=excluded.value,updated_at=CURRENT_TIMESTAMP;
+
+INSERT INTO settings(key,value) VALUES('offline_credential_max_age_days','7')
+ON CONFLICT(key) DO UPDATE SET value=excluded.value,updated_at=CURRENT_TIMESTAMP;
--- a/app/sw.js
+++ b/app/sw.js
@@ -1,6 +1,6 @@
 /* GLMChat V2.2 deterministic offline service worker. */
 'use strict'
-const CACHE_VERSION = 'glmchat-v2.2.0-632d7a0a0b2f'
+const CACHE_VERSION = 'glmchat-v2.2.1-hardening'
 const STATIC_CACHE = `${CACHE_VERSION}-static`
 const NAV_CACHE = `${CACHE_VERSION}-navigation`
 const ROOT = new URL('./', self.location.href)
@@ -105,7 +105,6 @@
   event.waitUntil((async () => {
     const cache = await caches.open(STATIC_CACHE)
     await cache.addAll(await glmchatPrecacheUrls())
-    await self.skipWaiting()
   })())
 })
 self.addEventListener('activate', event => {
@@ -214,7 +213,7 @@
         if (response.ok) cache.put(request, response.clone())
         return response
       }).catch(() => null)
-      return cached || refresh || Response.error()
+      return cached || (await refresh) || Response.error()
     }))
   }
 })
--- a/documentation/DOCUMENTATION_INDEX_22072026124256.md
+++ b/documentation/DOCUMENTATION_INDEX_22072026124256.md
@@ -1,26 +1,26 @@
-# GLMChat V2.2 Documentation Index
+# GLMChat V2.2.1 Documentation Index
 
 Updated: 22 July 2026
 
-This documentation accompanies the GLMChat V2.2 agentic automation release. The application remains feature-locked to a React Progressive Web App with a PHP 8.2+ and SQLite backend for LiteSpeed or Apache-compatible shared hosting.
+This documentation accompanies the GLMChat V2.2.1 hardened agentic automation release. The application remains feature-locked to a React Progressive Web App with a PHP 8.2+ and SQLite backend for LiteSpeed or Apache-compatible shared hosting.
 
 ## Start here
 
-1. `README.md` — release summary, architecture and source commands.
-2. `docs/INSTALLATION_AND_ACCEPTANCE_GUIDE_22072026124256.md` — deployment, upgrade, rollback and live acceptance.
-3. `docs/USER_GUIDE_22072026124256.md` — setup, chat, projects, agents, workflows, teams and tools.
-4. `docs/ADMIN_OPERATIONS_GUIDE_22072026124256.md` — hosting, storage, security, diagnostics and maintenance.
-5. `docs/RELEASE_NOTES_22072026124256.md` — V2.2 capability summary, verification and known external checks.
-6. `docs/ROUTE_TO_UI_MATRIX.md` — API-to-interface coverage and authentication requirements.
-7. `FEATURE_AND_FUNCTION_MAP.txt` — authoritative feature, persistence, offline and verification map.
-8. `VERIFICATION_REPORT.txt` — classified local, static and external verification results.
-9. `UI_UX_REMEDIATION_REPORT.txt` — implemented mobile interface and accessibility work.
-10. `REMEDIATION_PATCHES.txt` — remediation and evidence record.
+1. `app/README.md` — release summary, architecture and source commands.
+2. `documentation/INSTALLATION_AND_ACCEPTANCE_GUIDE_22072026124256.md` — deployment, upgrade, rollback and live acceptance.
+3. `documentation/USER_GUIDE_22072026124256.md` — setup, chat, projects, agents, workflows, teams and tools.
+4. `documentation/ADMIN_OPERATIONS_GUIDE_22072026124256.md` — hosting, storage, security, diagnostics and maintenance.
+5. `documentation/RELEASE_NOTES_22072026124256.md` — V2.2 capability summary, verification and known external checks.
+6. `documentation/ROUTE_TO_UI_MATRIX.md` — API-to-interface coverage and authentication requirements.
+7. `documentation/FEATURE_AND_FUNCTION_MAP.txt` — authoritative feature, persistence, offline and verification map.
+8. `documentation/VERIFICATION_REPORT.txt` — classified local, static and external verification results.
+9. `documentation/UI_UX_REMEDIATION_REPORT.txt` — implemented mobile interface and accessibility work.
+10. `documentation/RELEASE_HARDENING_REPORT_22072026125600.txt` — V2.2.1 repairs and remaining acceptance gates.
 
 ## Release profiles
 
 - **Direct deployment:** production runtime only. Upload the contents of the `app/` directory from the documented bundle, or use the standalone direct-deployment ZIP.
-- **Complete source:** runtime, maintainable React source, PHP source, tests, build scripts, evidence and documentation.
+- **Complete source:** a separate release profile required to reproduce source-level builds and tests; it is not included in this bundle.
 - **Documented bundle:** `app/` contains the deployment-ready application and `documentation/` contains the operator and user documentation. Do not upload the `documentation/` directory into the public application root.
 
 ## Verification terminology
@@ -30,3 +30,12 @@
 - **EXTERNAL NOT EXECUTED:** requires the target host, physical device or live Together account.
 
 External checks are never represented as passes.
+
+
+## V2.2.1 hardening additions
+
+- `RELEASE_HARDENING_REPORT_22072026125600.txt` — implemented security, PWA and packaging repairs plus explicit unexecuted gates.
+- `../THIRD_PARTY_NOTICES_22072026125600.txt` — redistributed third-party licence notices.
+- `../app/CHANGES_hhmmddmmyyyy.txt` — exact task diff and evidence-based verification section; use the actual timestamped filename in the release.
+- `FINAL_VALIDATION_22072026130658.txt` — exact checks executed against the final V2.2.1 deployment tree and explicit checks not run.
+- `OUTSTANDING_PRODUCTION_GATES_22072026130300.txt` — remaining live-host, provider, source and physical-device acceptance work.
--- /dev/null
+++ b/documentation/FINAL_VALIDATION_22072026130658.txt
@@ -0,0 +1,59 @@
+# GLMChat V2.2.1 final validation
+UTC 2026-07-22T13:06:58Z
+
+## Baseline/final structure
+baseline_files=87
+final_files=81
+final_unexpected_runtime_outputs:
+
+## PHP syntax
+php_files=44 failures=0
+
+## JavaScript syntax
+js_files=7 failures=0
+
+## JSON, manifest, SBOM, references and archive path safety
+json_files 4 parse_failures 0
+[]
+manifest_entries 80 set_match True failures []
+sbom_components 81 file_inventory_match True missing [] extra []
+precache_assets 15 missing []
+html_refs 10 missing []
+manifest_scope ./ start_url ./ display standalone
+manifest_new_chat True
+
+## Fresh and upgrade migration scenarios
+baseline-fresh migrations 12 integrity ok foreign_key_violations 0 tables 29
+final-fresh migrations 13 integrity ok foreign_key_violations 0 tables 29
+upgrade-12-to-13 integrity ok foreign_key_violations 0 app_version 2.2.1 offline_max_age_days 7
+
+## Targeted hardening assertions
+PASS preview bypass removed
+PASS mock provider test-only
+PASS install does not force update activation
+PASS explicit update action retained
+PASS fetch fallback always resolves Response
+PASS normal update notice present
+PASS service-worker failure visible
+PASS sensitive records use AES-GCM
+PASS plaintext migration implemented
+PASS PIN rekey does not silently overwrite retained data
+PASS offline maximum credential age enforced
+PASS lock clears decryption key
+PASS closed-app sync claim removed
+PASS unhashed helpers no-cache
+PASS workbox asset removed
+PASS light-only declarations
+PASS duplicate public docs removed
+
+## Obvious secret scan
+potential_secret_hits 0
+
+## Available-runtime backend preflight (isolated copy)
+{"ok":false,"error":{"code":"ENVIRONMENT_INVALID","message":"The server does not meet GLMChat runtime requirements.","request_id":"b7056ecd70c340ea7a8f518b","failures":["Missing PHP extension: sqlite3.","Missing PHP extension: curl.","Missing PHP extension: mbstring.","Missing PHP extension: zip."]}}
+## Checks unavailable
+Build: Not run — deployment-only bundle contains no package.json, lockfile or build scripts.
+Type-check: Not run — source and type-check configuration are absent.
+Automated tests: Not run — claimed Node/PHP/browser suites are absent.
+Browser runtime: Not run — installed Chromium hung even on a data URL in this container; no page result was claimed.
+Live host/provider/device: Not run — requires target LiteSpeed host, Together credentials and physical Android devices.
--- /dev/null
+++ b/documentation/OUTSTANDING_PRODUCTION_GATES_22072026130300.txt
@@ -0,0 +1,22 @@
+GLMChat V2.2.1 Outstanding Production Gates
+Generated: 22 July 2026 13:03 UTC
+
+The V2.2.1 deployment package is hardened but is not represented as production-approved until these gates are completed:
+
+1. Deploy at the intended LiteSpeed domain root and a representative nested path.
+2. Confirm PHP 8.2+ extensions sqlite3, curl, mbstring, zip, fileinfo, openssl and json.
+3. Verify .htaccess rewrites, private-path denials and every mandatory security header.
+4. Perform an upgrade against a cloned real pre-V2.2.1 storage directory and verify all user data.
+5. Test application and storage rollback after an intentionally failed upgrade.
+6. Run live Together API text, vision, function/tool and TTS requests, including timeout, rate-limit and invalid-key paths.
+7. Test streaming and cancellation through the production LiteSpeed/proxy stack.
+8. Test concurrent and long-running workflows, leases, interruption recovery and SQLite contention.
+9. Test storage quota pressure, IndexedDB transaction failure and browser eviction behaviour.
+10. Test large project uploads, ZIP guards, downloads and interruption on the target host.
+11. Test installation, offline restart, queued replay and controlled updates on physical Android Chrome and Samsung Internet.
+12. Complete TalkBack, keyboard, reduced-motion and responsive acceptance on physical devices.
+13. Obtain the matching complete-source release and reproduce its build, type-check, lint and automated test suites.
+14. Review the retained React 16.0.0/ReactDOM 16.0.1 dependency risk and plan a source-level upgrade.
+15. Decide whether a six-digit PIN plus PBKDF2 and a seven-day offline lifetime is sufficient for the intended threat model. Stronger device-bound WebAuthn/passkey protection requires source/UI work and remains recommended for high-sensitivity deployments.
+
+Until these checks are evidenced, release status remains: CONDITIONAL STAGING CANDIDATE — NOT PRODUCTION-PROVEN.
--- /dev/null
+++ b/documentation/RELEASE_HARDENING_REPORT_22072026125600.txt
@@ -0,0 +1,26 @@
+GLMChat V2.2.1 Release Hardening Report
+Generated: 22 July 2026 12:56 UTC
+
+Implemented:
+- AES-GCM protection for sensitive IndexedDB cache records, drafts, conflict payloads and queued mutation payloads after PIN unlock.
+- Automatic migration of existing plaintext V2.2 offline records after the next successful PIN unlock.
+- Seven-day maximum age for offline credentials before online sign-in is required.
+- Offline lock now clears both the in-memory session and local data-decryption key.
+- Service-worker updates wait for explicit user installation rather than activating over a live tab.
+- Normal update-ready and service-worker-failure user notices.
+- Fixed the non-navigation service-worker fallback to always return a Response.
+- No-cache headers for non-fingerprinted runtime helper scripts.
+- Production preview-query bypass removed.
+- Mock Together provider restricted to APP_ENV=test.
+- New-chat manifest shortcut added.
+- Unused Workbox Window asset removed.
+- Light-only production palette applied.
+- Public-root duplicate documentation removed; external documentation remains authoritative.
+- SBOM and deployment manifest regenerated for the final V2.2.1 tree.
+- Third-party licence notices added to the distribution.
+
+Not claimed as completed:
+- Live LiteSpeed root and nested-path acceptance.
+- Live Together text, vision, tools or TTS calls.
+- Physical Android Chrome, Samsung Internet or TalkBack acceptance.
+- Reproduction of the original source-level Node/PHP/browser suites, because their source and test files are absent from this deployment-only bundle.
--- a/documentation/RELEASE_NOTES_22072026124256.md
+++ b/documentation/RELEASE_NOTES_22072026124256.md
@@ -34,7 +34,7 @@
 
 ## Local and emulated verification
 
-The completed release passed the feature lock, Node contracts, PHP static/portable checks, migration scenarios, bootstrap preflight, offline/PWA browser tests, accessibility, Background Sync, required responsive widths, full-spec interface evidence, deterministic builds and adversarial asset checks.
+The completed release passed the feature lock, Node contracts, PHP static/portable checks, migration scenarios, bootstrap preflight, offline/PWA browser tests, accessibility, deferred reopen synchronisation, required responsive widths, full-spec interface evidence, deterministic builds and adversarial asset checks.
 
 ## External acceptance
 
--- a/documentation/ROUTE_TO_UI_MATRIX.md
+++ b/documentation/ROUTE_TO_UI_MATRIX.md
@@ -18,7 +18,7 @@
 | `GET /api/tools`, `POST /api/tools/execute` | Tool wizard and run approvals | authenticated; schema/risk policy; recent PIN when destructive | no silent tool execution offline | ToolRegistry/ToolExecutor tests |
 | prompt routes | Library and save-from-chat | authenticated | local edits remain visible; persistence online/queued where supported | Node/UI contracts |
 | cost summary/events/preflight | Home badges, Chat estimate, Costs | authenticated | last known display only; hard enforcement remains server-side | Node/PHP/UI tests |
-| sync snapshot/batch/conflicts/resolve | Offline & Sync | authenticated, CSRF for mutations | authoritative queue/conflict recovery surface | browser Background Sync/offline tests |
+| sync snapshot/batch/conflicts/resolve | Offline & Sync | authenticated, CSRF for mutations | authoritative queue/conflict recovery surface | browser deferred reopen synchronisation/offline tests |
 | TTS voices/generation | Voice screen and Speak action | authenticated, provider limits | online only; temporary audio URL revoked | Node/static/UI evidence |
 | data privacy/export/reauth/delete/reset/revoke | Privacy & Data | authenticated; recent PIN/confirmation as mapped | local clear is separate; server actions online | PHP/Node/UI evidence |
 | `GET /api/health` | Diagnostics | authenticated, secret-safe | last loaded state only | PHP/static/UI evidence |
--- a/documentation/VERIFICATION_REPORT.txt
+++ b/documentation/VERIFICATION_REPORT.txt
@@ -1,6 +1,8 @@
-GLMChat V2.2 — FINAL VERIFICATION REPORT
+IMPORTANT: This report was supplied with V2.2.0 and is retained as historical evidence. Its test suites are not included in the deployment-only bundle and were not independently rerun during V2.2.1 hardening.
+
+GLMChat V2.2.0 — HISTORICAL SUPPLIED VERIFICATION REPORT
 Date: 2026-07-22
-Verdict: PASS FOR ALL AVAILABLE LOCAL/EMULATED GATES
+Verdict: HISTORICAL SUPPLIED LOCAL/EMULATED EVIDENCE — PRODUCTION ACCEPTANCE REMAINS REQUIRED
 
 CLASSIFICATIONS
 - EXECUTED PASS: the command or browser check ran successfully.
@@ -26,7 +28,7 @@
 EXECUTED PASS  Offline-first runtime: unlock, persistence, sync, replay, conflict, service worker and credential scan.
 EXECUTED PASS  PWA install gate: native prompt, dismissal, appinstalled, standalone bypass, display-mode transition and embedded-browser recovery.
 EXECUTED PASS  Accessibility: four Android layouts, 200% text, named controls, minimum targets, visible focus, one controlled live region and modal restoration.
-EXECUTED PASS  Background Sync: durable registration/fallback, exact attempts, auth pause, conflict, exhaustion, ordering, reload, profile restart, worker update, idempotency and private-cache exclusion.
+SUPPLIED PASS  Deferred reopen synchronisation: durable registration/fallback, exact attempts, auth pause, conflict, exhaustion, ordering, reload, profile restart, worker update, idempotency and private-cache exclusion.
 EXECUTED PASS  Responsive UI: 320, 360, 390, 412, 480 and 600 CSS-pixel portrait widths, team wizard, short landscape, reduced motion and dominant chat viewport.
 EXECUTED PASS  Full-spec UI evidence: 21 production screens/wizards at 390x844 with no horizontal overflow.
 STATIC PASS    Offline chat and AI/tool actions are never silently submitted.

# TASK VERIFICATION
# Requested task: Autonomously remediate the catalogued GLMChat V2.2 issues and prepare a verified updated release.
# Scope matched: No
# Build: Not run
# Type-check: Not run
# Lint: Passed
# Automated tests: Not run
# Targeted checks: Passed
# PWA checks: Passed
# Feature loss detected: Unknown
# Data loss detected: Unknown
# Structural changes detected: Yes
# Regressions detected: Unknown
# Unrelated changes reverted: Validation-created app/storage/logs and app/storage/projects directories were removed before packaging.
# Checks not run: Source build, type-check and original automated suites were unavailable because the deployment-only bundle has no package files, source tree or tests. Browser execution was not completed because container Chromium hung even on a data URL. Live LiteSpeed, Together provider and physical Android/TalkBack checks require external systems and devices.
# Validation notes: PHP syntax passed for 44 files; JavaScript syntax passed for 7 files; four JSON/manifest documents parsed; deployment-manifest hashes and sizes matched; SBOM file inventory matched; all 15 precache entries and 10 HTML asset references existed; baseline 12-migration and final 13-migration fresh scenarios passed SQLite integrity and foreign-key checks; the 12-to-13 upgrade and repeated migration 013 passed with app_version 2.2.1 and offline_credential_max_age_days 7; targeted security/PWA assertions passed; no obvious embedded secrets were detected. The available PHP runtime correctly rejected startup because sqlite3, curl, mbstring and zip are absent. Production status remains conditional staging, not production-proven.
