From f16eb02f4c8ce6d1fede2176b7feb62aab39f6ba Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 9 Sep 2026 14:41:47 +0000 Subject: [PATCH 1/6] Add Helm org condenser default settings Co-authored-by: openhands --- charts/openhands/README.md | 17 ++++++ charts/openhands/templates/_env.yaml | 8 +++ .../tests/org_defaults_condenser_test.yaml | 50 +++++++++++++++++ .../openhands/tests/values_schema_test.yaml | 33 ++++++++++++ charts/openhands/values.schema.json | 13 +++++ charts/openhands/values.yaml | 13 +++++ replicated/config.yaml | 22 ++++++++ replicated/openhands.yaml | 6 +++ .../test_replicated_org_defaults_condenser.py | 54 +++++++++++++++++++ 9 files changed, 216 insertions(+) create mode 100644 charts/openhands/tests/org_defaults_condenser_test.yaml create mode 100644 scripts/test_replicated_org_defaults_condenser.py diff --git a/charts/openhands/README.md b/charts/openhands/README.md index 7d41af244..c52c305ef 100644 --- a/charts/openhands/README.md +++ b/charts/openhands/README.md @@ -18,6 +18,23 @@ This Helm chart deploys the complete OpenHands stack, including all required dep See the [values.yaml](values.yaml) file for the full list of configurable parameters. Make sure to update all values marked with "REQUIRED" comments. +### Organization condenser defaults + +Set `orgDefaults.condenser.maxTokens` to add a token-based condensation threshold for applicable OpenHands organization settings. The effective threshold cannot exceed the model input limit and does not change the model context window. Event-count condensation may still occur first. + +```yaml +orgDefaults: + condenser: + maxTokens: 200000 + applyToExisting: true + overwriteExisting: true +``` + +This is rollout reconciliation, not continuous enforcement. New applicable OpenHands org settings receive the configured value. Existing org rows are only updated when `applyToExisting: true`; existing non-null values are only replaced when `overwriteExisting: true`. Missing and JSON-null `condenser.max_tokens` values are treated as unset. + +`overwriteExisting: true` is destructive for prior org-level `max_tokens` values. Take a database backup before enabling it. Removing `orgDefaults.condenser.maxTokens` later stops future defaulting/reconciliation but does not restore previous org-specific values; restore from backup or run corrective SQL if rollback is required. + + ### Email (Resend) To enable organization invitation emails via Resend, set `resend.enabled: true` and create a Kubernetes secret named `resend-api-key` with key `resend-api-key` containing your Resend API key. The secret name can be overridden with `resend.auth.existingSecret`. diff --git a/charts/openhands/templates/_env.yaml b/charts/openhands/templates/_env.yaml index 87f10a21c..6819f8514 100644 --- a/charts/openhands/templates/_env.yaml +++ b/charts/openhands/templates/_env.yaml @@ -34,6 +34,14 @@ - name: OH_DAILY_CONVERSATION_LIMIT value: {{ .Values.dailyConversationLimit | quote }} {{- end }} +{{- with .Values.orgDefaults.condenser.maxTokens }} +- name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS + value: {{ . | quote }} +{{- end }} +- name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING + value: {{ .Values.orgDefaults.condenser.applyToExisting | quote }} +- name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING + value: {{ .Values.orgDefaults.condenser.overwriteExisting | quote }} - name: DUPLICATE_EMAIL_CHECK value: {{ .Values.duplicateEmailCheck | quote }} - name: OH_RESOLVER_LABEL diff --git a/charts/openhands/tests/org_defaults_condenser_test.yaml b/charts/openhands/tests/org_defaults_condenser_test.yaml new file mode 100644 index 000000000..40d5bc517 --- /dev/null +++ b/charts/openhands/tests/org_defaults_condenser_test.yaml @@ -0,0 +1,50 @@ +suite: org defaults condenser env wiring +templates: + - deployment.yaml + - litellm-config-script.yaml +tests: + - it: leaves token threshold unset by default + set: + enabled: true + asserts: + - template: deployment.yaml + notExists: + path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS")] + - template: deployment.yaml + contains: + path: spec.template.spec.containers[0].env + content: + name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING + value: "false" + - template: deployment.yaml + contains: + path: spec.template.spec.containers[0].env + content: + name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING + value: "false" + + - it: wires configured org condenser rollout settings + set: + enabled: true + orgDefaults.condenser.maxTokens: 200000 + orgDefaults.condenser.applyToExisting: true + orgDefaults.condenser.overwriteExisting: true + asserts: + - template: deployment.yaml + contains: + path: spec.template.spec.containers[0].env + content: + name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS + value: "200000" + - template: deployment.yaml + contains: + path: spec.template.spec.containers[0].env + content: + name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING + value: "true" + - template: deployment.yaml + contains: + path: spec.template.spec.containers[0].env + content: + name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING + value: "true" diff --git a/charts/openhands/tests/values_schema_test.yaml b/charts/openhands/tests/values_schema_test.yaml index 0b18b7fc0..fa5387cc8 100644 --- a/charts/openhands/tests/values_schema_test.yaml +++ b/charts/openhands/tests/values_schema_test.yaml @@ -20,6 +20,36 @@ tests: asserts: - failedTemplate: errorPattern: "ingress/enabled.*want boolean" + - it: rejects orgDefaults.condenser.maxTokens below the minimum + set: + orgDefaults.condenser.maxTokens: 0 + asserts: + - failedTemplate: + errorPattern: "orgDefaults/condenser/maxTokens" + - it: rejects negative orgDefaults.condenser.maxTokens + set: + orgDefaults.condenser.maxTokens: -1 + asserts: + - failedTemplate: + errorPattern: "orgDefaults/condenser/maxTokens" + - it: rejects a non-integer orgDefaults.condenser.maxTokens + set: + orgDefaults.condenser.maxTokens: "200000" + asserts: + - failedTemplate: + errorPattern: "orgDefaults/condenser/maxTokens" + - it: rejects a non-boolean orgDefaults.condenser.applyToExisting + set: + orgDefaults.condenser.applyToExisting: "true" + asserts: + - failedTemplate: + errorPattern: "orgDefaults/condenser/applyToExisting.*want boolean" + - it: rejects a non-boolean orgDefaults.condenser.overwriteExisting + set: + orgDefaults.condenser.overwriteExisting: "false" + asserts: + - failedTemplate: + errorPattern: "orgDefaults/condenser/overwriteExisting.*want boolean" - it: rejects a non-integer daily conversation limit set: dailyConversationLimit: "twenty" @@ -95,5 +125,8 @@ tests: enterpriseSSO.idpMetadataUrl: https://idp.example.com/metadata filestore.secretAccessKeyKey: secret-key laminar.clickhouse.diagnostics.retentionDays: 2 + orgDefaults.condenser.maxTokens: 200000 + orgDefaults.condenser.applyToExisting: true + orgDefaults.condenser.overwriteExisting: false asserts: - notFailedTemplate: {} diff --git a/charts/openhands/values.schema.json b/charts/openhands/values.schema.json index f118aaa72..8cb7c39f4 100644 --- a/charts/openhands/values.schema.json +++ b/charts/openhands/values.schema.json @@ -8,6 +8,19 @@ "enabled": { "type": "boolean" }, "dailyConversationLimit": { "type": ["integer", "null"] }, "duplicateEmailCheck": { "type": "boolean" }, + "orgDefaults": { + "type": "object", + "properties": { + "condenser": { + "type": "object", + "properties": { + "maxTokens": { "type": ["integer", "null"], "minimum": 1 }, + "applyToExisting": { "type": "boolean" }, + "overwriteExisting": { "type": "boolean" } + } + } + } + }, "image": { "type": "object", "required": ["repository", "tag"], diff --git a/charts/openhands/values.yaml b/charts/openhands/values.yaml index 9a2cb846e..58c8eb8af 100644 --- a/charts/openhands/values.yaml +++ b/charts/openhands/values.yaml @@ -41,6 +41,19 @@ dailyConversationLimit: null # a plus-address suffix (for example, name+test@example.com and name@example.com). duplicateEmailCheck: true + +# Organization-default condenser settings. These seed newly-created applicable +# OpenHands org settings. Existing org rows are only reconciled when explicitly +# enabled below because that mutates persisted database settings. +orgDefaults: + condenser: + # Adds a token-based condensation threshold; null preserves app/SDK defaults. + maxTokens: null + # Mutate existing org rows during rollout reconciliation. + applyToExisting: false + # Force existing non-null org-level values to maxTokens during reconciliation. + overwriteExisting: false + datadog: enabled: false env: "dev" # Default environment, can be overridden diff --git a/replicated/config.yaml b/replicated/config.yaml index bd93fa525..6763027f0 100644 --- a/replicated/config.yaml +++ b/replicated/config.yaml @@ -734,6 +734,28 @@ spec: type: bool default: "1" when: 'repl{{ and (ConfigOptionEquals "default_openhands_org_enabled" "1") (ConfigOptionEquals "default_openhands_org_auto_add_users" "1") }}' + - name: org_condenser_max_tokens + title: Organization Condenser Token Threshold + help_text: 'Optional. Adds a token-based condensation threshold for applicable OpenHands organization settings. The effective threshold cannot exceed the model input limit and does not change the model context window. Leave blank to preserve app defaults.' + type: text + default: "" + validation: + regex: + pattern: '^$|^[1-9][0-9]*$' + message: Must be blank or a positive base-10 integer. + - name: org_condenser_apply_to_existing + title: Apply Condenser Threshold to Existing Organizations + help_text: 'Apply this value to existing applicable OpenHands organization settings during rollout reconciliation. This mutates persisted organization settings. Missing and JSON-null values are treated as unset.' + type: bool + default: "0" + when: 'repl{{ ne (ConfigOption "org_condenser_max_tokens") "" }}' + - name: org_condenser_overwrite_existing + title: Overwrite Existing Organization Condenser Thresholds + help_text: 'Overwrite mode replaces existing org-level max_tokens values and cannot restore prior values unless you have a database backup.' + type: bool + default: "0" + when: 'repl{{ and (ne (ConfigOption "org_condenser_max_tokens") "") (ConfigOptionEquals "org_condenser_apply_to_existing" "1") }}' + - name: authentication_security title: Authentication Security diff --git a/replicated/openhands.yaml b/replicated/openhands.yaml index 22410553a..814381841 100644 --- a/replicated/openhands.yaml +++ b/replicated/openhands.yaml @@ -41,6 +41,12 @@ spec: duplicateEmailCheck: repl{{ ConfigOptionEquals "duplicate_email_check" "1" }} + orgDefaults: + condenser: + maxTokens: repl{{ $orgCondenserMaxTokens := trim (ConfigOption "org_condenser_max_tokens") }}repl{{ if $orgCondenserMaxTokens }}repl{{ $orgCondenserMaxTokens }}repl{{ else }}nullrepl{{ end }} + applyToExisting: repl{{ and (ne (ConfigOption "org_condenser_max_tokens") "") (ConfigOptionEquals "org_condenser_apply_to_existing" "1") }} + overwriteExisting: repl{{ and (ne (ConfigOption "org_condenser_max_tokens") "") (ConfigOptionEquals "org_condenser_apply_to_existing" "1") (ConfigOptionEquals "org_condenser_overwrite_existing" "1") }} + env: # Default model for new users/orgs: the first non-empty line of the # "Anthropic Models" KOTS textarea (anthropic is the base provider; the diff --git a/scripts/test_replicated_org_defaults_condenser.py b/scripts/test_replicated_org_defaults_condenser.py new file mode 100644 index 000000000..9d6cb789b --- /dev/null +++ b/scripts/test_replicated_org_defaults_condenser.py @@ -0,0 +1,54 @@ +"""Tests for Replicated org condenser default configuration.""" + +from __future__ import annotations + +from pathlib import Path + +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[1] +REPLICATED_CONFIG = REPO_ROOT / "replicated" / "config.yaml" +REPLICATED_OPENHANDS = REPO_ROOT / "replicated" / "openhands.yaml" + + +def replicated_config_item(name: str) -> dict[str, object]: + config = yaml.safe_load(REPLICATED_CONFIG.read_text(encoding="utf-8")) + for group in config["spec"]["groups"]: + for item in group["items"]: + if item["name"] == name: + return item + raise AssertionError(f"Replicated config item {name!r} was not found") + + +def test_replicated_exposes_org_condenser_options() -> None: + max_tokens = replicated_config_item("org_condenser_max_tokens") + apply_existing = replicated_config_item("org_condenser_apply_to_existing") + overwrite_existing = replicated_config_item("org_condenser_overwrite_existing") + + assert max_tokens["type"] == "text" + assert max_tokens["default"] == "" + assert max_tokens["validation"] == { + "regex": { + "pattern": "^$|^[1-9][0-9]*$", + "message": "Must be blank or a positive base-10 integer.", + } + } + assert apply_existing["type"] == "bool" + assert apply_existing["default"] == "0" + assert overwrite_existing["type"] == "bool" + assert overwrite_existing["default"] == "0" + + +def test_replicated_maps_blank_to_yaml_null_and_positive_value_to_integer() -> None: + values = REPLICATED_OPENHANDS.read_text(encoding="utf-8") + + assert "orgDefaults:\n condenser:\n" in values + assert "maxTokens: repl{{ $orgCondenserMaxTokens := trim" in values + assert "repl{{ $orgCondenserMaxTokens }}repl{{ else }}null" in values + assert 'ConfigOption "org_condenser_max_tokens"' in values + assert 'applyToExisting: repl{{ and (ne (ConfigOption "org_condenser_max_tokens") "") (ConfigOptionEquals "org_condenser_apply_to_existing" "1") }}' in values + assert 'overwriteExisting: repl{{ and (ne (ConfigOption "org_condenser_max_tokens") "") (ConfigOptionEquals "org_condenser_apply_to_existing" "1") (ConfigOptionEquals "org_condenser_overwrite_existing" "1") }}' in values + + assert 'maxTokens: "repl{{' not in values + assert "maxTokens: 'repl{{" not in values From 848f9a9fea1245ef60fa367427d4b8cfa8ae373e Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 9 Sep 2026 15:49:18 +0000 Subject: [PATCH 2/6] Address org condenser chart feedback Co-authored-by: openhands --- charts/openhands/README.md | 8 +-- charts/openhands/templates/_env.yaml | 6 +-- charts/openhands/templates/validations.yaml | 6 +++ .../tests/org_defaults_condenser_test.yaml | 20 ++++++- charts/openhands/tests/validations_test.yaml | 24 +++++++++ .../openhands/tests/values_schema_test.yaml | 6 +++ charts/openhands/values.schema.json | 2 +- charts/openhands/values.yaml | 1 + replicated/config.yaml | 22 -------- replicated/openhands.yaml | 6 --- .../test_replicated_org_defaults_condenser.py | 54 ------------------- 11 files changed, 65 insertions(+), 90 deletions(-) delete mode 100644 scripts/test_replicated_org_defaults_condenser.py diff --git a/charts/openhands/README.md b/charts/openhands/README.md index c52c305ef..70f680430 100644 --- a/charts/openhands/README.md +++ b/charts/openhands/README.md @@ -20,7 +20,7 @@ Make sure to update all values marked with "REQUIRED" comments. ### Organization condenser defaults -Set `orgDefaults.condenser.maxTokens` to add a token-based condensation threshold for applicable OpenHands organization settings. The effective threshold cannot exceed the model input limit and does not change the model context window. Event-count condensation may still occur first. +Set `orgDefaults.condenser.maxTokens` to add a token-based condensation threshold for applicable OpenHands organization settings. Values must be between 1 and 1,000,000 tokens; higher values are rejected at render time instead of relying on backend clamping to the model input limit. The effective threshold cannot exceed the model input limit and does not change the model context window. Event-count condensation may still occur first. ```yaml orgDefaults: @@ -30,9 +30,11 @@ orgDefaults: overwriteExisting: true ``` -This is rollout reconciliation, not continuous enforcement. New applicable OpenHands org settings receive the configured value. Existing org rows are only updated when `applyToExisting: true`; existing non-null values are only replaced when `overwriteExisting: true`. Missing and JSON-null `condenser.max_tokens` values are treated as unset. +New applicable OpenHands org settings receive the configured value. Existing org rows are only updated when `applyToExisting: true`; existing non-null values are only replaced when `overwriteExisting: true`. Missing and JSON-null `condenser.max_tokens` values are treated as unset. -`overwriteExisting: true` is destructive for prior org-level `max_tokens` values. Take a database backup before enabling it. Removing `orgDefaults.condenser.maxTokens` later stops future defaulting/reconciliation but does not restore previous org-specific values; restore from backup or run corrective SQL if rollback is required. +Reconciliation runs on every app start while these values remain in the release. If `overwriteExisting: true` stays in a site values file, a pod restart, rollout, HPA scale-up, or node eviction can overwrite org-admin UI changes made after the initial rollout. Remove `applyToExisting`/`overwriteExisting` after the rollout completes, or remove `orgDefaults.condenser.maxTokens` to stop future defaulting/reconciliation. + +`overwriteExisting: true` is destructive for prior org-level `max_tokens` values. Take a database backup before enabling it. Removing `orgDefaults.condenser.maxTokens` later does not restore previous org-specific values; restore from backup or run corrective SQL if rollback is required. ### Email (Resend) diff --git a/charts/openhands/templates/_env.yaml b/charts/openhands/templates/_env.yaml index 6819f8514..3c22069a9 100644 --- a/charts/openhands/templates/_env.yaml +++ b/charts/openhands/templates/_env.yaml @@ -34,14 +34,14 @@ - name: OH_DAILY_CONVERSATION_LIMIT value: {{ .Values.dailyConversationLimit | quote }} {{- end }} -{{- with .Values.orgDefaults.condenser.maxTokens }} +{{- if ne .Values.orgDefaults.condenser.maxTokens nil }} - name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS - value: {{ . | quote }} -{{- end }} + value: {{ .Values.orgDefaults.condenser.maxTokens | quote }} - name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING value: {{ .Values.orgDefaults.condenser.applyToExisting | quote }} - name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING value: {{ .Values.orgDefaults.condenser.overwriteExisting | quote }} +{{- end }} - name: DUPLICATE_EMAIL_CHECK value: {{ .Values.duplicateEmailCheck | quote }} - name: OH_RESOLVER_LABEL diff --git a/charts/openhands/templates/validations.yaml b/charts/openhands/templates/validations.yaml index 9896d4b40..7f42ed3dd 100644 --- a/charts/openhands/templates/validations.yaml +++ b/charts/openhands/templates/validations.yaml @@ -15,6 +15,12 @@ are never rendered, so a server-less release must not trip them. {{- if and .Values.enabled .Values.enterpriseSSO.idpMetadataUrl (not (include "openhands.keycloakProvisionRealm" .)) -}} {{- fail "enterpriseSSO.idpMetadataUrl requires chart-managed Keycloak realm provisioning. Set keycloak.enabled=true or keycloak.provisionRealm=true, or leave enterpriseSSO.idpMetadataUrl empty and manage the provider manually." -}} {{- end -}} +{{- if and .Values.enabled (eq .Values.orgDefaults.condenser.maxTokens nil) (or .Values.orgDefaults.condenser.applyToExisting .Values.orgDefaults.condenser.overwriteExisting) -}} +{{- fail "orgDefaults.condenser.maxTokens must be set before applyToExisting or overwriteExisting can be enabled." -}} +{{- end -}} +{{- if and .Values.enabled .Values.orgDefaults.condenser.overwriteExisting (not .Values.orgDefaults.condenser.applyToExisting) -}} +{{- fail "orgDefaults.condenser.overwriteExisting requires orgDefaults.condenser.applyToExisting=true because overwrite mode only runs while reconciling existing organizations." -}} +{{- end -}} {{/* Data-loss guard for the bundled MinIO. Its chart runs the bucket job on diff --git a/charts/openhands/tests/org_defaults_condenser_test.yaml b/charts/openhands/tests/org_defaults_condenser_test.yaml index 40d5bc517..0168706f2 100644 --- a/charts/openhands/tests/org_defaults_condenser_test.yaml +++ b/charts/openhands/tests/org_defaults_condenser_test.yaml @@ -3,13 +3,31 @@ templates: - deployment.yaml - litellm-config-script.yaml tests: - - it: leaves token threshold unset by default + - it: leaves org condenser env unset by default set: enabled: true asserts: - template: deployment.yaml notExists: path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS")] + - template: deployment.yaml + notExists: + path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING")] + - template: deployment.yaml + notExists: + path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING")] + + - it: wires false rollout flags when only a threshold is configured + set: + enabled: true + orgDefaults.condenser.maxTokens: 200000 + asserts: + - template: deployment.yaml + contains: + path: spec.template.spec.containers[0].env + content: + name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS + value: "200000" - template: deployment.yaml contains: path: spec.template.spec.containers[0].env diff --git a/charts/openhands/tests/validations_test.yaml b/charts/openhands/tests/validations_test.yaml index 0645eed30..06232667b 100644 --- a/charts/openhands/tests/validations_test.yaml +++ b/charts/openhands/tests/validations_test.yaml @@ -64,6 +64,30 @@ tests: - hasDocuments: count: 0 + - it: fails when org condenser rollout flags are set without a threshold + set: + orgDefaults.condenser.maxTokens: null + orgDefaults.condenser.applyToExisting: true + asserts: + - failedTemplate: + errorMessage: "orgDefaults.condenser.maxTokens must be set before applyToExisting or overwriteExisting can be enabled." + - it: fails when org condenser overwrite is enabled without applyToExisting + set: + orgDefaults.condenser.maxTokens: 200000 + orgDefaults.condenser.applyToExisting: false + orgDefaults.condenser.overwriteExisting: true + asserts: + - failedTemplate: + errorMessage: "orgDefaults.condenser.overwriteExisting requires orgDefaults.condenser.applyToExisting=true because overwrite mode only runs while reconciling existing organizations." + - it: allows org condenser overwrite when existing-org reconciliation is enabled + set: + orgDefaults.condenser.maxTokens: 200000 + orgDefaults.condenser.applyToExisting: true + orgDefaults.condenser.overwriteExisting: true + asserts: + - hasDocuments: + count: 0 + # The bundled MinIO bucket job runs on post-upgrade too, so purge: true wipes # conversation and session data on every ordinary upgrade of a persistent # install. These cases pin the guard that rejects it. diff --git a/charts/openhands/tests/values_schema_test.yaml b/charts/openhands/tests/values_schema_test.yaml index fa5387cc8..3c1b2b9a4 100644 --- a/charts/openhands/tests/values_schema_test.yaml +++ b/charts/openhands/tests/values_schema_test.yaml @@ -32,6 +32,12 @@ tests: asserts: - failedTemplate: errorPattern: "orgDefaults/condenser/maxTokens" + - it: rejects orgDefaults.condenser.maxTokens above the maximum + set: + orgDefaults.condenser.maxTokens: 1000001 + asserts: + - failedTemplate: + errorPattern: "orgDefaults/condenser/maxTokens.*maximum" - it: rejects a non-integer orgDefaults.condenser.maxTokens set: orgDefaults.condenser.maxTokens: "200000" diff --git a/charts/openhands/values.schema.json b/charts/openhands/values.schema.json index 8cb7c39f4..79efba7c4 100644 --- a/charts/openhands/values.schema.json +++ b/charts/openhands/values.schema.json @@ -14,7 +14,7 @@ "condenser": { "type": "object", "properties": { - "maxTokens": { "type": ["integer", "null"], "minimum": 1 }, + "maxTokens": { "type": ["integer", "null"], "minimum": 1, "maximum": 1000000 }, "applyToExisting": { "type": "boolean" }, "overwriteExisting": { "type": "boolean" } } diff --git a/charts/openhands/values.yaml b/charts/openhands/values.yaml index 58c8eb8af..983a202b4 100644 --- a/charts/openhands/values.yaml +++ b/charts/openhands/values.yaml @@ -48,6 +48,7 @@ duplicateEmailCheck: true orgDefaults: condenser: # Adds a token-based condensation threshold; null preserves app/SDK defaults. + # Must be between 1 and 1,000,000 when set. maxTokens: null # Mutate existing org rows during rollout reconciliation. applyToExisting: false diff --git a/replicated/config.yaml b/replicated/config.yaml index 6763027f0..bd93fa525 100644 --- a/replicated/config.yaml +++ b/replicated/config.yaml @@ -734,28 +734,6 @@ spec: type: bool default: "1" when: 'repl{{ and (ConfigOptionEquals "default_openhands_org_enabled" "1") (ConfigOptionEquals "default_openhands_org_auto_add_users" "1") }}' - - name: org_condenser_max_tokens - title: Organization Condenser Token Threshold - help_text: 'Optional. Adds a token-based condensation threshold for applicable OpenHands organization settings. The effective threshold cannot exceed the model input limit and does not change the model context window. Leave blank to preserve app defaults.' - type: text - default: "" - validation: - regex: - pattern: '^$|^[1-9][0-9]*$' - message: Must be blank or a positive base-10 integer. - - name: org_condenser_apply_to_existing - title: Apply Condenser Threshold to Existing Organizations - help_text: 'Apply this value to existing applicable OpenHands organization settings during rollout reconciliation. This mutates persisted organization settings. Missing and JSON-null values are treated as unset.' - type: bool - default: "0" - when: 'repl{{ ne (ConfigOption "org_condenser_max_tokens") "" }}' - - name: org_condenser_overwrite_existing - title: Overwrite Existing Organization Condenser Thresholds - help_text: 'Overwrite mode replaces existing org-level max_tokens values and cannot restore prior values unless you have a database backup.' - type: bool - default: "0" - when: 'repl{{ and (ne (ConfigOption "org_condenser_max_tokens") "") (ConfigOptionEquals "org_condenser_apply_to_existing" "1") }}' - - name: authentication_security title: Authentication Security diff --git a/replicated/openhands.yaml b/replicated/openhands.yaml index 814381841..22410553a 100644 --- a/replicated/openhands.yaml +++ b/replicated/openhands.yaml @@ -41,12 +41,6 @@ spec: duplicateEmailCheck: repl{{ ConfigOptionEquals "duplicate_email_check" "1" }} - orgDefaults: - condenser: - maxTokens: repl{{ $orgCondenserMaxTokens := trim (ConfigOption "org_condenser_max_tokens") }}repl{{ if $orgCondenserMaxTokens }}repl{{ $orgCondenserMaxTokens }}repl{{ else }}nullrepl{{ end }} - applyToExisting: repl{{ and (ne (ConfigOption "org_condenser_max_tokens") "") (ConfigOptionEquals "org_condenser_apply_to_existing" "1") }} - overwriteExisting: repl{{ and (ne (ConfigOption "org_condenser_max_tokens") "") (ConfigOptionEquals "org_condenser_apply_to_existing" "1") (ConfigOptionEquals "org_condenser_overwrite_existing" "1") }} - env: # Default model for new users/orgs: the first non-empty line of the # "Anthropic Models" KOTS textarea (anthropic is the base provider; the diff --git a/scripts/test_replicated_org_defaults_condenser.py b/scripts/test_replicated_org_defaults_condenser.py deleted file mode 100644 index 9d6cb789b..000000000 --- a/scripts/test_replicated_org_defaults_condenser.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Tests for Replicated org condenser default configuration.""" - -from __future__ import annotations - -from pathlib import Path - -import yaml - - -REPO_ROOT = Path(__file__).resolve().parents[1] -REPLICATED_CONFIG = REPO_ROOT / "replicated" / "config.yaml" -REPLICATED_OPENHANDS = REPO_ROOT / "replicated" / "openhands.yaml" - - -def replicated_config_item(name: str) -> dict[str, object]: - config = yaml.safe_load(REPLICATED_CONFIG.read_text(encoding="utf-8")) - for group in config["spec"]["groups"]: - for item in group["items"]: - if item["name"] == name: - return item - raise AssertionError(f"Replicated config item {name!r} was not found") - - -def test_replicated_exposes_org_condenser_options() -> None: - max_tokens = replicated_config_item("org_condenser_max_tokens") - apply_existing = replicated_config_item("org_condenser_apply_to_existing") - overwrite_existing = replicated_config_item("org_condenser_overwrite_existing") - - assert max_tokens["type"] == "text" - assert max_tokens["default"] == "" - assert max_tokens["validation"] == { - "regex": { - "pattern": "^$|^[1-9][0-9]*$", - "message": "Must be blank or a positive base-10 integer.", - } - } - assert apply_existing["type"] == "bool" - assert apply_existing["default"] == "0" - assert overwrite_existing["type"] == "bool" - assert overwrite_existing["default"] == "0" - - -def test_replicated_maps_blank_to_yaml_null_and_positive_value_to_integer() -> None: - values = REPLICATED_OPENHANDS.read_text(encoding="utf-8") - - assert "orgDefaults:\n condenser:\n" in values - assert "maxTokens: repl{{ $orgCondenserMaxTokens := trim" in values - assert "repl{{ $orgCondenserMaxTokens }}repl{{ else }}null" in values - assert 'ConfigOption "org_condenser_max_tokens"' in values - assert 'applyToExisting: repl{{ and (ne (ConfigOption "org_condenser_max_tokens") "") (ConfigOptionEquals "org_condenser_apply_to_existing" "1") }}' in values - assert 'overwriteExisting: repl{{ and (ne (ConfigOption "org_condenser_max_tokens") "") (ConfigOptionEquals "org_condenser_apply_to_existing" "1") (ConfigOptionEquals "org_condenser_overwrite_existing" "1") }}' in values - - assert 'maxTokens: "repl{{' not in values - assert "maxTokens: 'repl{{" not in values From d7de21cdd85af19bc94c5f8f566fb005cbb36e47 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 9 Sep 2026 19:53:47 +0000 Subject: [PATCH 3/6] fix: nil-safe org condenser guards, drop invented maxTokens ceiling Address round-2 review feedback on PLTF-3553: - Guard the orgDefaults.condenser walk in _env.yaml and validations.yaml with the `default dict` idiom so explicit nulls no longer panic the render. - Drop the chart-invented `maximum: 1000000` on maxTokens; the backend accepts any positive integer. - Correct the README clamping claim to describe actual SDK behavior (the smaller of the value and the LLM effective input limit) and note that these values require an app version that reads the env vars. - Remove stray blank lines in values.yaml and README.md. Co-authored-by: openhands --- charts/openhands/README.md | 5 +++-- charts/openhands/templates/_env.yaml | 9 +++++---- charts/openhands/templates/validations.yaml | 5 +++-- .../tests/org_defaults_condenser_test.yaml | 19 +++++++++++++++++++ charts/openhands/tests/validations_test.yaml | 13 +++++++++++++ .../openhands/tests/values_schema_test.yaml | 7 +++---- charts/openhands/values.schema.json | 6 +++--- charts/openhands/values.yaml | 3 +-- 8 files changed, 50 insertions(+), 17 deletions(-) diff --git a/charts/openhands/README.md b/charts/openhands/README.md index 70f680430..394f03768 100644 --- a/charts/openhands/README.md +++ b/charts/openhands/README.md @@ -20,7 +20,9 @@ Make sure to update all values marked with "REQUIRED" comments. ### Organization condenser defaults -Set `orgDefaults.condenser.maxTokens` to add a token-based condensation threshold for applicable OpenHands organization settings. Values must be between 1 and 1,000,000 tokens; higher values are rejected at render time instead of relying on backend clamping to the model input limit. The effective threshold cannot exceed the model input limit and does not change the model context window. Event-count condensation may still occur first. +Set `orgDefaults.condenser.maxTokens` to add a token-based condensation threshold for applicable OpenHands organization settings. Any positive integer is accepted. Condensation triggers on the smaller of this value and the agent LLM's effective input limit, so a value above that limit has no effect; it does not change the model context window, and event-count condensation may still occur first. + +These values only take effect from the app version that reads the `OPENHANDS_ORG_DEFAULTS_CONDENSER_*` environment variables. On earlier app versions they render into the pod but do nothing. ```yaml orgDefaults: @@ -36,7 +38,6 @@ Reconciliation runs on every app start while these values remain in the release. `overwriteExisting: true` is destructive for prior org-level `max_tokens` values. Take a database backup before enabling it. Removing `orgDefaults.condenser.maxTokens` later does not restore previous org-specific values; restore from backup or run corrective SQL if rollback is required. - ### Email (Resend) To enable organization invitation emails via Resend, set `resend.enabled: true` and create a Kubernetes secret named `resend-api-key` with key `resend-api-key` containing your Resend API key. The secret name can be overridden with `resend.auth.existingSecret`. diff --git a/charts/openhands/templates/_env.yaml b/charts/openhands/templates/_env.yaml index 3c22069a9..6ff29fc40 100644 --- a/charts/openhands/templates/_env.yaml +++ b/charts/openhands/templates/_env.yaml @@ -1,5 +1,6 @@ {{- define "openhands.env.defaults" }} {{- $integrations := .Values.integrations | default dict }} +{{- $orgCondenser := (.Values.orgDefaults | default dict).condenser | default dict }} - name: RUNTIME value: remote - name: OH_SANDBOX_SPEC_KIND @@ -34,13 +35,13 @@ - name: OH_DAILY_CONVERSATION_LIMIT value: {{ .Values.dailyConversationLimit | quote }} {{- end }} -{{- if ne .Values.orgDefaults.condenser.maxTokens nil }} +{{- if ne $orgCondenser.maxTokens nil }} - name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS - value: {{ .Values.orgDefaults.condenser.maxTokens | quote }} + value: {{ $orgCondenser.maxTokens | quote }} - name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING - value: {{ .Values.orgDefaults.condenser.applyToExisting | quote }} + value: {{ $orgCondenser.applyToExisting | default false | quote }} - name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING - value: {{ .Values.orgDefaults.condenser.overwriteExisting | quote }} + value: {{ $orgCondenser.overwriteExisting | default false | quote }} {{- end }} - name: DUPLICATE_EMAIL_CHECK value: {{ .Values.duplicateEmailCheck | quote }} diff --git a/charts/openhands/templates/validations.yaml b/charts/openhands/templates/validations.yaml index 7f42ed3dd..bb7cf79be 100644 --- a/charts/openhands/templates/validations.yaml +++ b/charts/openhands/templates/validations.yaml @@ -15,10 +15,11 @@ are never rendered, so a server-less release must not trip them. {{- if and .Values.enabled .Values.enterpriseSSO.idpMetadataUrl (not (include "openhands.keycloakProvisionRealm" .)) -}} {{- fail "enterpriseSSO.idpMetadataUrl requires chart-managed Keycloak realm provisioning. Set keycloak.enabled=true or keycloak.provisionRealm=true, or leave enterpriseSSO.idpMetadataUrl empty and manage the provider manually." -}} {{- end -}} -{{- if and .Values.enabled (eq .Values.orgDefaults.condenser.maxTokens nil) (or .Values.orgDefaults.condenser.applyToExisting .Values.orgDefaults.condenser.overwriteExisting) -}} +{{- $orgCondenser := (.Values.orgDefaults | default dict).condenser | default dict -}} +{{- if and .Values.enabled (eq $orgCondenser.maxTokens nil) (or $orgCondenser.applyToExisting $orgCondenser.overwriteExisting) -}} {{- fail "orgDefaults.condenser.maxTokens must be set before applyToExisting or overwriteExisting can be enabled." -}} {{- end -}} -{{- if and .Values.enabled .Values.orgDefaults.condenser.overwriteExisting (not .Values.orgDefaults.condenser.applyToExisting) -}} +{{- if and .Values.enabled $orgCondenser.overwriteExisting (not $orgCondenser.applyToExisting) -}} {{- fail "orgDefaults.condenser.overwriteExisting requires orgDefaults.condenser.applyToExisting=true because overwrite mode only runs while reconciling existing organizations." -}} {{- end -}} diff --git a/charts/openhands/tests/org_defaults_condenser_test.yaml b/charts/openhands/tests/org_defaults_condenser_test.yaml index 0168706f2..83575bf81 100644 --- a/charts/openhands/tests/org_defaults_condenser_test.yaml +++ b/charts/openhands/tests/org_defaults_condenser_test.yaml @@ -17,6 +17,25 @@ tests: notExists: path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING")] + - it: renders without org condenser env when the orgDefaults block is null + set: + enabled: true + orgDefaults: null + asserts: + - template: deployment.yaml + notExists: + path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS")] + + - it: renders without org condenser env when the condenser block is null + set: + enabled: true + orgDefaults: + condenser: null + asserts: + - template: deployment.yaml + notExists: + path: spec.template.spec.containers[0].env[?(@.name=="OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS")] + - it: wires false rollout flags when only a threshold is configured set: enabled: true diff --git a/charts/openhands/tests/validations_test.yaml b/charts/openhands/tests/validations_test.yaml index 06232667b..d8ce40aad 100644 --- a/charts/openhands/tests/validations_test.yaml +++ b/charts/openhands/tests/validations_test.yaml @@ -87,6 +87,19 @@ tests: asserts: - hasDocuments: count: 0 + - it: renders no resources when the orgDefaults block is null + set: + orgDefaults: null + asserts: + - hasDocuments: + count: 0 + - it: renders no resources when the org condenser block is null + set: + orgDefaults: + condenser: null + asserts: + - hasDocuments: + count: 0 # The bundled MinIO bucket job runs on post-upgrade too, so purge: true wipes # conversation and session data on every ordinary upgrade of a persistent diff --git a/charts/openhands/tests/values_schema_test.yaml b/charts/openhands/tests/values_schema_test.yaml index 3c1b2b9a4..d8d5c011b 100644 --- a/charts/openhands/tests/values_schema_test.yaml +++ b/charts/openhands/tests/values_schema_test.yaml @@ -32,12 +32,11 @@ tests: asserts: - failedTemplate: errorPattern: "orgDefaults/condenser/maxTokens" - - it: rejects orgDefaults.condenser.maxTokens above the maximum + - it: accepts orgDefaults.condenser.maxTokens above one million set: - orgDefaults.condenser.maxTokens: 1000001 + orgDefaults.condenser.maxTokens: 2000000 asserts: - - failedTemplate: - errorPattern: "orgDefaults/condenser/maxTokens.*maximum" + - notFailedTemplate: {} - it: rejects a non-integer orgDefaults.condenser.maxTokens set: orgDefaults.condenser.maxTokens: "200000" diff --git a/charts/openhands/values.schema.json b/charts/openhands/values.schema.json index 79efba7c4..304db5650 100644 --- a/charts/openhands/values.schema.json +++ b/charts/openhands/values.schema.json @@ -9,12 +9,12 @@ "dailyConversationLimit": { "type": ["integer", "null"] }, "duplicateEmailCheck": { "type": "boolean" }, "orgDefaults": { - "type": "object", + "type": ["object", "null"], "properties": { "condenser": { - "type": "object", + "type": ["object", "null"], "properties": { - "maxTokens": { "type": ["integer", "null"], "minimum": 1, "maximum": 1000000 }, + "maxTokens": { "type": ["integer", "null"], "minimum": 1 }, "applyToExisting": { "type": "boolean" }, "overwriteExisting": { "type": "boolean" } } diff --git a/charts/openhands/values.yaml b/charts/openhands/values.yaml index 1469f93d2..1d1a01e46 100644 --- a/charts/openhands/values.yaml +++ b/charts/openhands/values.yaml @@ -41,14 +41,13 @@ dailyConversationLimit: null # a plus-address suffix (for example, name+test@example.com and name@example.com). duplicateEmailCheck: true - # Organization-default condenser settings. These seed newly-created applicable # OpenHands org settings. Existing org rows are only reconciled when explicitly # enabled below because that mutates persisted database settings. orgDefaults: condenser: # Adds a token-based condensation threshold; null preserves app/SDK defaults. - # Must be between 1 and 1,000,000 when set. + # Must be a positive integer when set. maxTokens: null # Mutate existing org rows during rollout reconciliation. applyToExisting: false From cb9f6b010494acff70d8728d901d8d9da171fedf Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 10 Sep 2026 18:03:22 +0000 Subject: [PATCH 4/6] docs: clarify org condenser reconciliation trigger and target version The README's "runs on every app start" phrasing left the trigger point ambiguous enough to read as firing in the maintenance CronJobs that share openhands.env.defaults. Reconciliation is called from the app server's startup lifespan, so it is scoped to saas_server worker processes; the CronJobs run standalone `python -m` entrypoints and never reach it. State that explicitly, name the enterprise PR that ships the env reader so the version note is actionable, and move the condensation-threshold semantics into values.yaml so they travel with the key. Co-authored-by: openhands --- charts/openhands/README.md | 6 ++++-- charts/openhands/templates/_env.yaml | 5 +++++ .../tests/org_defaults_condenser_test.yaml | 20 +++++++++++++++++++ charts/openhands/values.yaml | 10 +++++++--- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/charts/openhands/README.md b/charts/openhands/README.md index 394f03768..a5e314d18 100644 --- a/charts/openhands/README.md +++ b/charts/openhands/README.md @@ -22,7 +22,7 @@ Make sure to update all values marked with "REQUIRED" comments. Set `orgDefaults.condenser.maxTokens` to add a token-based condensation threshold for applicable OpenHands organization settings. Any positive integer is accepted. Condensation triggers on the smaller of this value and the agent LLM's effective input limit, so a value above that limit has no effect; it does not change the model context window, and event-count condensation may still occur first. -These values only take effect from the app version that reads the `OPENHANDS_ORG_DEFAULTS_CONDENSER_*` environment variables. On earlier app versions they render into the pod but do nothing. +These values only take effect from the app version that reads the `OPENHANDS_ORG_DEFAULTS_CONDENSER_*` environment variables, which is tracked in [OpenHands/enterprise#337](https://github.com/OpenHands/enterprise/pull/337). The current chart `appVersion` does not include that reader, so on it these values render into the pod and do nothing. ```yaml orgDefaults: @@ -34,7 +34,9 @@ orgDefaults: New applicable OpenHands org settings receive the configured value. Existing org rows are only updated when `applyToExisting: true`; existing non-null values are only replaced when `overwriteExisting: true`. Missing and JSON-null `condenser.max_tokens` values are treated as unset. -Reconciliation runs on every app start while these values remain in the release. If `overwriteExisting: true` stays in a site values file, a pod restart, rollout, HPA scale-up, or node eviction can overwrite org-admin UI changes made after the initial rollout. Remove `applyToExisting`/`overwriteExisting` after the rollout completes, or remove `orgDefaults.condenser.maxTokens` to stop future defaulting/reconciliation. +Reconciliation is triggered from the app server's startup lifespan, so it runs once per `saas_server` worker process every time one starts, and it stays armed for as long as these values remain in the release. The maintenance CronJobs that share this env block never trigger it, because they run standalone `python -m` entrypoints that do not start the app. A concurrent database lock serializes overlapping runs, so repeated runs converge on the same result rather than conflicting. + +The practical consequence is that this is not a rollout-scoped, one-time operation. If `overwriteExisting: true` stays in a site values file, a pod restart, rollout, HPA scale-up, or node eviction re-applies it and overwrites org-admin UI changes made since the last start. Remove `applyToExisting`/`overwriteExisting` once the rollout completes, or remove `orgDefaults.condenser.maxTokens` to stop future defaulting and reconciliation. `overwriteExisting: true` is destructive for prior org-level `max_tokens` values. Take a database backup before enabling it. Removing `orgDefaults.condenser.maxTokens` later does not restore previous org-specific values; restore from backup or run corrective SQL if rollback is required. diff --git a/charts/openhands/templates/_env.yaml b/charts/openhands/templates/_env.yaml index 6ff29fc40..52d68e17e 100644 --- a/charts/openhands/templates/_env.yaml +++ b/charts/openhands/templates/_env.yaml @@ -35,6 +35,11 @@ - name: OH_DAILY_CONVERSATION_LIMIT value: {{ .Values.dailyConversationLimit | quote }} {{- end }} +{{- /* + Only the app-server startup lifespan reads these, so the copies that land in + the maintenance CronJobs sharing this block are inert -- those run standalone + `python -m` entrypoints that never start the app. + */}} {{- if ne $orgCondenser.maxTokens nil }} - name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS value: {{ $orgCondenser.maxTokens | quote }} diff --git a/charts/openhands/tests/org_defaults_condenser_test.yaml b/charts/openhands/tests/org_defaults_condenser_test.yaml index 83575bf81..b2cd0efa1 100644 --- a/charts/openhands/tests/org_defaults_condenser_test.yaml +++ b/charts/openhands/tests/org_defaults_condenser_test.yaml @@ -60,6 +60,26 @@ tests: name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING value: "false" + - it: defaults null rollout flags to false rather than an empty value + set: + enabled: true + orgDefaults.condenser.maxTokens: 200000 + orgDefaults.condenser.applyToExisting: null + orgDefaults.condenser.overwriteExisting: null + asserts: + - template: deployment.yaml + contains: + path: spec.template.spec.containers[0].env + content: + name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING + value: "false" + - template: deployment.yaml + contains: + path: spec.template.spec.containers[0].env + content: + name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING + value: "false" + - it: wires configured org condenser rollout settings set: enabled: true diff --git a/charts/openhands/values.yaml b/charts/openhands/values.yaml index 1d1a01e46..f2f0d687c 100644 --- a/charts/openhands/values.yaml +++ b/charts/openhands/values.yaml @@ -46,12 +46,16 @@ duplicateEmailCheck: true # enabled below because that mutates persisted database settings. orgDefaults: condenser: - # Adds a token-based condensation threshold; null preserves app/SDK defaults. - # Must be a positive integer when set. + # Conversation-history size that triggers condensation, NOT an LLM output-token + # cap. Named to match the backend's condenser.max_tokens field. Condensation + # fires on the smaller of this and the agent LLM's effective input limit, so a + # larger value has no effect. Positive integer, or null for app/SDK defaults. maxTokens: null - # Mutate existing org rows during rollout reconciliation. + # Mutate existing org rows during reconciliation, which the app server runs on + # every start while these values are set -- not just once per rollout. applyToExisting: false # Force existing non-null org-level values to maxTokens during reconciliation. + # Destructive and repeated on every app start; see README before enabling. overwriteExisting: false datadog: From f12c06cc265841a71d05595626202a315b68f6e3 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 10 Sep 2026 20:43:28 +0000 Subject: [PATCH 5/6] fix: coerce org condenser maxTokens to an integer before quoting A YAML float with a zero fraction satisfies JSON Schema's integer type, so a values file setting maxTokens: 1e6 passed schema validation and rendered OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS="1e+06". The backend parses that with isdecimal() before the apply_to_existing early return, so the ValueError propagates out of the app-server startup lifespan and crashloops the app tier regardless of the rollout flags. Co-authored-by: openhands --- charts/openhands/templates/_env.yaml | 7 ++++++- .../tests/org_defaults_condenser_test.yaml | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/charts/openhands/templates/_env.yaml b/charts/openhands/templates/_env.yaml index 52d68e17e..65c46320f 100644 --- a/charts/openhands/templates/_env.yaml +++ b/charts/openhands/templates/_env.yaml @@ -41,8 +41,13 @@ `python -m` entrypoints that never start the app. */}} {{- if ne $orgCondenser.maxTokens nil }} +{{- /* + int64 before quoting: JSON Schema's integer type accepts YAML floats with a + zero fraction, and Go renders 1e6 as "1e+06", which the backend's isdecimal + parse rejects by raising out of the startup lifespan. + */}} - name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS - value: {{ $orgCondenser.maxTokens | quote }} + value: {{ $orgCondenser.maxTokens | int64 | quote }} - name: OPENHANDS_ORG_DEFAULTS_CONDENSER_APPLY_TO_EXISTING value: {{ $orgCondenser.applyToExisting | default false | quote }} - name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING diff --git a/charts/openhands/tests/org_defaults_condenser_test.yaml b/charts/openhands/tests/org_defaults_condenser_test.yaml index b2cd0efa1..76f69ec59 100644 --- a/charts/openhands/tests/org_defaults_condenser_test.yaml +++ b/charts/openhands/tests/org_defaults_condenser_test.yaml @@ -80,6 +80,20 @@ tests: name: OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING value: "false" + - it: renders an exponent-notation threshold as a plain integer + set: + enabled: true + orgDefaults: + condenser: + maxTokens: 1.0e+6 + asserts: + - template: deployment.yaml + contains: + path: spec.template.spec.containers[0].env + content: + name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS + value: "1000000" + - it: wires configured org condenser rollout settings set: enabled: true From 1c8725761bece9879cfc7c03ed3fa187fad17eee Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 10 Sep 2026 21:17:35 +0000 Subject: [PATCH 6/6] docs: clarify org condenser removal order Co-authored-by: openhands --- charts/openhands/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/openhands/README.md b/charts/openhands/README.md index a5e314d18..60fed4eeb 100644 --- a/charts/openhands/README.md +++ b/charts/openhands/README.md @@ -36,7 +36,7 @@ New applicable OpenHands org settings receive the configured value. Existing org Reconciliation is triggered from the app server's startup lifespan, so it runs once per `saas_server` worker process every time one starts, and it stays armed for as long as these values remain in the release. The maintenance CronJobs that share this env block never trigger it, because they run standalone `python -m` entrypoints that do not start the app. A concurrent database lock serializes overlapping runs, so repeated runs converge on the same result rather than conflicting. -The practical consequence is that this is not a rollout-scoped, one-time operation. If `overwriteExisting: true` stays in a site values file, a pod restart, rollout, HPA scale-up, or node eviction re-applies it and overwrites org-admin UI changes made since the last start. Remove `applyToExisting`/`overwriteExisting` once the rollout completes, or remove `orgDefaults.condenser.maxTokens` to stop future defaulting and reconciliation. +The practical consequence is that this is not a rollout-scoped, one-time operation. If `overwriteExisting: true` stays in a site values file, a pod restart, rollout, HPA scale-up, or node eviction re-applies it and overwrites org-admin UI changes made since the last start. Remove `applyToExisting`/`overwriteExisting` once the rollout completes. Then remove `orgDefaults.condenser.maxTokens` as well if you want to stop future defaulting and reconciliation. `overwriteExisting: true` is destructive for prior org-level `max_tokens` values. Take a database backup before enabling it. Removing `orgDefaults.condenser.maxTokens` later does not restore previous org-specific values; restore from backup or run corrective SQL if rollback is required.