Repository navigation
feat: PLTF-3553 Add Helm values for org condenser defaults - #1214
Conversation
Co-authored-by: openhands <openhands@all-hands.dev>
aivong-openhands
left a comment
There was a problem hiding this comment.
Code review — feat: PLTF-3553 Add Helm values for org condenser defaults
The chart plumbing itself is clean: it follows the dailyConversationLimit / duplicateEmailCheck precedent, it goes through openhands.env.defaults so .Values.env de-duplication still lets consumers override, the schema is tightened alongside the values, and both the Helm unit tests and the Replicated contract test exercise the real files rather than mocks. Defaults are null/false, which is the right posture for a self-hosted user. CI is green.
I'm commenting rather than approving because of three things worth resolving before this ships.
1. The env contract lands ahead of the app that reads it (important)
OPENHANDS_ORG_DEFAULTS_CONDENSER_* exists only in the still-open OpenHands/enterprise#337. This chart pins appVersion: 1.59.0, which has no reader for those variables. Merged as-is, an operator can set orgDefaults.condenser.maxTokens — or, more likely, fill in the new Replicated field whose help text describes concrete behavior — and get silence.
The Replicated exposure is the risky half. KOTS config is the self-hosted operator's contract surface, and a toggle that does nothing is worse than an absent one: there's no feedback that tells them it isn't wired yet. Suggestion: land enterprise#337 and the image-tag bump first, or split this so the chart values merge now and the replicated/config.yaml surface follows the appVersion that supports it.
2. applyToExisting + overwriteExisting is one knob split across two booleans (important)
Two booleans give four combinations, and one of them lies: applyToExisting: false with overwriteExisting: true renders ..._OVERWRITE_EXISTING=true into the pod and does nothing, because the backend returns early when apply_to_existing is false. That's the "booleans must stay truthful in every value combination" test in this repo's own review guidelines.
Replicated is protected here — the when: conditions plus the and (...) in openhands.yaml make the invalid combination unreachable — but a plain Helm consumer has no such guard, and the chart values interface is the one self-hosted users read.
These are really three states of one rollout decision. Either collapse them:
orgDefaults:
condenser:
maxTokens: null
# none | fillUnset | overwrite
reconcileExisting: none…or, if you'd rather keep the shape aligned 1:1 with the backend env vars, add a guard in templates/validations.yaml, which exists for exactly this class of problem (ingress.host, minio.buckets[].purge).
3. "Rollout reconciliation, not continuous enforcement" isn't true of a Helm value (important)
The README frames this as a one-time rollout, but reconciliation runs from SaasAppLifespanService.__aenter__ — i.e. on every app start — while the Helm value that arms it is declarative and persistent. Leaving overwriteExisting: true in a site values file means every pod restart, rollout, HPA scale-up, and node eviction silently re-clobbers any condenser.max_tokens an org admin changed in the UI since the last restart. That's continuous enforcement with an unpredictable trigger, and "take a database backup before enabling it" doesn't help against the second and later occurrences.
Two ways out, either is fine:
- Say plainly in the README (and the KOTS help text) that the flag stays armed until it's removed from values, and should be reverted once the rollout completes.
- Model the reconciliation as something inherently one-shot — a
helm.sh/hook: post-upgradeJob, the pattern the chart already uses for the MinIO bucket job — rather than a startup side effect of a long-lived Deployment.
Minor
Three inline notes; none of them block.
This review was generated by an AI agent (OpenHands) on behalf of the user, following .agents/skills/code-review.md.
Co-authored-by: openhands <openhands@all-hands.dev>
|
Addressed the review feedback in 848f9a9:
Local validation and GitHub checks are passing. This comment was generated by an AI agent (OpenHands) on behalf of the user. |
aivong-openhands
left a comment
There was a problem hiding this comment.
Code review — feat: PLTF-3553 Add Helm values for org condenser defaults (round 2)
Good progress on the three items from the last round:
- The Replicated/KOTS surface is gone, so the risky half of "the env contract lands ahead of the app" — an operator-facing toggle with concrete help text and no reader — is no longer shipping. Deferring it until the appVersion carries the backend is the right call.
templates/validations.yamlnow rejectsoverwriteExistingwithoutapplyToExisting, and all three env vars render only undermaxTokens, so the two-boolean shape no longer has a combination that lies and default installs get no inert env. That was one of the two options I offered, and the guard follows theingress.host/minio.buckets[].purgeprecedent.- The README now says plainly that reconciliation runs on every app start and stays armed until the values are removed, and calls out the rollback caveat.
I re-ran the suites locally against 6d0a05c (helm unittest -f tests/org_defaults_condenser_test.yaml -f tests/validations_test.yaml -f tests/values_schema_test.yaml charts/openhands → 39 passed) and CI is green.
Commenting rather than approving because of one render bug and one limit I think is wrong for self-hosted users.
1. Unguarded nested dereference breaks renders that null the block (important)
_env.yaml:37 and both new guards in validations.yaml walk .Values.orgDefaults.condenser.maxTokens with no nil guard. A site values file that nulls either level — a normal way to say "I don't use this" — takes the whole render down with an opaque error instead of a chart message. Verified on this branch:
--set orgDefaults=null
_env.yaml:37: nil pointer evaluating interface {}.condenser
--set orgDefaults.condenser=null
_env.yaml:37: nil pointer evaluating interface {}.maxTokens
Empty maps (orgDefaults: {}, orgDefaults.condenser: {}) render fine, so it's specifically the explicit-null path. _env.yaml already handles this everywhere else with | default dict (lines 2, 427, 799), and the dailyConversationLimit precedent this change follows is a top-level key that can't hit the problem. Either adopt the default dict chain, or tighten the schema so the failure is a schema message rather than a template panic. Worth a test case either way — the new suite covers the happy paths but not this one.
2. maximum: 1000000 is a ceiling the chart invented (important)
The schema hard-caps maxTokens at 1,000,000. The backend contract in OpenHands/enterprise#337 has no such limit — _parse_positive_int_env only requires a positive decimal integer — so the ceiling exists solely in the chart, and the only escape for a consumer who needs more is a raw .Values.env override or a fork.
That's a defaults-serve-the-self-hosted-user problem: models with context windows above 1M are already shipping, and an operator running one gets a render failure with no chart-level way to say what they mean. minimum: 1 earns its keep — it blocks a nonsense value. The maximum blocks a legitimate one. Suggest dropping it, or raising it well clear of real model limits if you want a sanity bound.
Related: README:23 says higher values are "rejected at render time instead of relying on backend clamping to the model input limit," and that "the effective threshold cannot exceed the model input limit." I can't find that clamp in enterprise#337 — the value is parsed and written through as-is. If it lives elsewhere in the stack, a pointer would settle it; if it doesn't exist yet, the README describes behavior an operator will rely on and not get, which is the same class of problem as an unwired toggle.
3. Still worth a decision: does the chart value merge before the backend?
enterprise#337 is still open and this chart pins appVersion: 1.59.1, which has no reader for OPENHANDS_ORG_DEFAULTS_CONDENSER_*. Dropping the KOTS surface removes most of the blast radius, and a Helm value documented in the chart README is a much softer contract than a KOTS field — so I won't argue this blocks. But an operator who sets maxTokens on 1.59.1 today still gets silence, and nothing in the chart tells them. If you'd rather not gate the merge on enterprise#337, consider a one-line README note that these values take effect from the appVersion that ships the reader.
Minor
Stray blank lines: values.yaml:44 and README.md:38-39.
This review was generated by an AI agent (OpenHands) on behalf of @aivong-openhands, following .agents/skills/code-review.md.
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 <openhands@all-hands.dev>
|
Addressed the round-2 feedback in d7de21c. 1. Unguarded nested dereference — both 2. 3. README clamping claim — the clamp is real but lives in the SDK, not Also from item 3 of the review — added a one-line README note that these values take effect only from the app version that reads Minor — stray blank lines in Validation: This comment was generated by an AI agent (OpenHands) on behalf of the user. |
aivong-openhands
left a comment
There was a problem hiding this comment.
Code review — feat: PLTF-3553 Add Helm values for org condenser defaults
Reviewed against the repo's .agents/skills/code-review.md guidelines. I cloned the branch, built dependencies, and ran the suite locally: 152 tests pass, helm lint clean, CI green on 9907d95.
The chart mechanics here are good. The env vars flow through openhands.env.defaults, so they inherit the de-duplication wrapper for free and consumers can override via .Values.env — the established pattern is followed, not reinvented. The nil-guarding on (.Values.orgDefaults | default dict).condenser | default dict is correct, and I verified the explicitly-nulled-block renders. The tests are genuinely load-bearing rather than decorative: I mutation-tested the notExists assertions by forcing the _env.yaml conditional to true, and 3 tests correctly failed.
Holding at COMMENT rather than approving because of one finding I think is important and two I'd like answered before this leaves draft.
1. The reconciliation env vars reach 6 workloads, including a CronJob that runs every 15 minutes
This is my main concern. Because the vars are added to the shared openhands.env.defaults helper, they render into far more than the app Deployment. With default values only:
| Workload | Cadence |
|---|---|
Deployment/openhands |
rollout / restart |
Deployment/openhands-integrations |
rollout / restart |
Deployment/openhands-mcp |
rollout / restart |
CronJob/…-app-conversation-start-task-clean |
0 3 * * * |
CronJob/…-maintenance-tasks |
0 6 * * * |
CronJob/…-budget-maintenance |
*/15 * * * * |
All six run the same enterprise-server:1.59.1 image. If the reconciliation is triggered on process startup — which is what the README describes — then overwriteExisting: true is not a rollout-scoped event at all; budget-maintenance alone would re-run it 96 times a day, and the three Deployments each carry it per-replica and per-restart.
That undercuts the README's mitigation advice, which frames removal as something you do "after the rollout completes." There may be no window in which the rollout is complete and reconciliation has stopped.
I can't verify the trigger point from this repo (see #3), so this is a genuine question rather than an assertion: does the reconciliation run on interpreter startup, or only under the uvicorn/saas_server app path? If it's startup-scoped, a destructive overwrite firing on a 15-minute timer is a serious problem, and the env vars should be scoped to the specific workload that owns the migration instead of the shared helper.
2. A one-shot destructive migration expressed as a persistent declarative value
The PR is admirably candid that overwriteExisting: true is destructive and one-way — backup first, no rollback, corrective SQL if you need to undo. But Helm values are persistent and declarative, while this operation is imperative and one-time. The intent "overwrite once during this rollout" has to be encoded as a value that then sits in a site values file indefinitely, which is exactly how the README's pod-restart/HPA/eviction footgun arises.
This bumps against the skill's "booleans must stay truthful in every value combination" test and the OHE-3033 lesson about mismatched lifetimes. As written, the README mitigates a structural problem with documentation and operator discipline.
There's already precedent in this chart for one-shot work: laminar-clickhouse-diagnostic-retention-job.yaml uses helm.sh/hook: post-install,post-upgrade with a hook-delete-policy. A hook Job would make the migration fire exactly once per upgrade, keep it out of the long-running Deployments and CronJobs entirely, and resolve #1 as a side effect. Worth considering — I recognise it's a bigger change and depends on where the backend puts the logic.
3. No backend reads these variables yet, and the README's version reference is unbounded
An org-wide code search for ORG_DEFAULTS_CONDENSER returns 0 results, and chart appVersion is 1.59.1. So today these env vars render into six pods and do nothing — which the PR acknowledges by deferring the Replicated/KOTS surface.
The problem is that the README says these "only take effect from the app version that reads the OPENHANDS_ORG_DEFAULTS_CONDENSER_* environment variables" without ever naming that version. A self-hosted operator can't act on that. Meanwhile the guard messages in validations.yaml assert behaviour ("overwrite mode only runs while reconciling existing organizations") that no shipped code implements, so the chart is now the de-facto spec for an unwritten backend and the two can drift.
Are the variable names and the apply/overwrite semantics locked with the backend work? If so, naming the target appVersion — even approximately — would make the docs actionable.
Nits (non-blocking)
maxTokensreads like an LLM output-token cap; it's actually a condensation trigger threshold. Something likecondensationTriggerTokenswould be self-describing per the skill's "name the effect, not the scenario" test. The README explains it well, but that explanation doesn't travel into consumer values files alongside the key.values.schema.jsonuses"type": "boolean"for the two flags whilemaxTokensallowsnull. Not a bug — I verified a null in user values drops the key and| default falsecovers it — just noting the asymmetry looks deliberate and is fine.
Risk assessment
Medium. The chart-side implementation is careful, well-tested, and inert by default (maxTokens: null ships disabled), so a default install is unaffected — that's the right call per "defaults serve the self-hosted user." Risk concentrates entirely on the opt-in path: an operator who sets overwriteExisting: true is performing an irreversible database mutation whose actual execution frequency is, per #1, likely much higher than the documentation implies. Findings #1 and #3 are worth resolving before this leaves draft; #2 is a design discussion.
This review was generated by an AI agent (OpenHands) on behalf of @aivong-openhands.
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 <openhands@all-hands.dev>
|
Thanks — the CronJob question in #1 turned out to be the important one, and the answer changes the finding. Addressed in cb9f6b0. 1. Reconciliation is app-path scoped, not interpreter-startup scopedYou asked directly: does the reconciliation run on interpreter startup, or only under the The chain:
I checked by AST-walking the imports of all three, plus a plain grep for Your instinct that the count is higher than "once per rollout" was right, though, just in a different place. All three Deployments run On the remedy — scoping the vars out of 2. One-shot migration as a persistent value — agreed, but not fixable in this chartYou're right about the lifetime mismatch, and a Meanwhile the README no longer frames removal as a rollout-completion step; it states plainly that this is not a rollout-scoped operation and that the flag re-applies on every start until removed. 3. Version reference is now actionableFair — "the app version that reads these" with no version named isn't something an operator can act on. I can't name a released version because On the guard messages asserting unimplemented behavior: the Nits
This reply was generated by an AI agent (OpenHands) on behalf of the user. |
aivong-openhands
left a comment
There was a problem hiding this comment.
Code review — feat: PLTF-3553 Add Helm values for org condenser defaults (round 4)
Reviewed against .agents/skills/code-review.md. I cloned cb9f6b0, added the missing chart repos, ran helm dependency build, helm lint (clean), and helm unittest charts/openhands → 153 passed. CI is green on this commit.
The round-3 fixes hold up under independent checking, and I want to be specific about what I actually verified rather than restating the PR description:
- The nil guards work.
(.Values.orgDefaults | default dict).condenser | default dictrenders cleanly for both--set orgDefaults=nulland--set orgDefaults.condenser=null. I also checked the case the tests don't cover — explicitapplyToExisting: null/overwriteExisting: nullin a real values file rather than via--set— and it correctly renders"false". - The tests are load-bearing, not decorative. I mutation-tested three ways: dropping
| default falsefrom_env.yamlfails 1 test, forcing thene $orgCondenser.maxTokens nilconditional totruefails 3, and deleting theoverwriteExisting-without-applyToExistingguard fails 18. Nothing passes vacuously. - The env vars go through
openhands.env.defaults, so they inherit the de-duplication wrapper and.Values.envoverride behaviour for free. Established pattern followed, not reinvented. - Defaults ship inert (
maxTokens: nullrenders no env at all), which is the right posture for a self-hosted user.
Holding at COMMENT for one new finding I think is important, plus a correction to the review record from the last round.
1. A schema-valid maxTokens can crashloop all three Deployments (important)
_env.yaml:45 renders the value with | quote and no integer coercion. YAML parses 1e6 and 200000.0 as floats, Go renders those in exponent form, and values.schema.json's "type": ["integer", "null"] does not reject them — a float with a zero fractional part satisfies JSON Schema's integer type.
Verified against this branch:
$ cat /tmp/v1.yaml
orgDefaults:
condenser:
maxTokens: 1e6
$ helm template t charts/openhands -f /tmp/v1.yaml | grep -A1 MAX_TOKENS
- name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS
value: "1e+06"helm lint passes, schema validation passes, all 153 tests pass. Note this only reproduces through a values file — --set orgDefaults.condenser.maxTokens=1e6 is correctly rejected by the schema, which is precisely why the existing coverage misses it. 200000.0 renders "200000" and is harmless; 1e6 is not.
The backend failure mode is worse than a wrong number. In enterprise#337, _parse_positive_int_env gates on raw.isdecimal(), and '1e+06'.isdecimal() is False, so it raises ValueError. That parse happens in get_org_defaults_condenser_config(), called from _reconcile_org_condenser_defaults at line 89 — outside the try that wraps _reconcile_org_condenser_defaults_once. The exception propagates out of SaasAppLifespanService.__aenter__, the FastAPI lifespan fails, and the uvicorn worker dies at startup.
Two consequences worth calling out:
- It fires regardless of
applyToExisting, becausemax_tokensis parsed before theif config.max_tokens is None or not config.apply_to_existingearly return. An operator who sets only the threshold, with both rollout flags off, still gets it. - It hits
openhands,openhands-integrations, andopenhands-mcpat once, so the symptom is a full crashloop of the app tier traced back to a values file that every validation layer in this repo accepted.
One-token fix, verified to render "1000000" for both 1e6 and 200000.0:
value: {{ $orgCondenser.maxTokens | int64 | quote }}For the record, dailyConversationLimit has identical behaviour (1e6 → "1e+06"), so the pattern is pre-existing rather than introduced here — but the blast radius differs sharply: a bad daily limit is a wrong number, this is a failed process start. The guard belongs in the chart rather than the backend, since the chart is where the operator's 1e6 becomes a string.
2. Correction: the CronJobs do import these modules — but your conclusion still holds
Your round-3 reply reported an AST import walk of all three CronJob entrypoints with "zero hits" for saas_server / app_lifespan. That isn't right, and I'd rather flag it than leave it in the record as a verified trace. Walking the transitive import closure of each entrypoint in enterprise#337:
| Entrypoint | files in closure | reaches saas_app_lifespan_service / org_defaults_config? |
|---|---|---|
run_budget_maintenance |
201 | yes — both, plus storage.org_default_settings, org_store, user_store |
run_maintenance_tasks |
196 | yes — same set |
sync.clean_app_conversation_start_tasks |
195 | yes — same set |
A direct-import grep returns zero, which is presumably what got measured. The closure does not.
Your conclusion survives, for a better reason than "they don't import it": importing doesn't execute. _reconcile_org_condenser_defaults has exactly one non-test caller (__aenter__), and SaasAppLifespanService only ever becomes a lifespan via get_app_lifespan_service() → app.py:52. The CronJobs run python -m entrypoints that never construct the FastAPI app, so reconciliation cannot fire there regardless of what they import. I'm satisfied budget-maintenance does not run this 96 times a day, and I withdraw my round-3 finding #1.
Worth getting on the record correctly, though, because the two arguments have different durability: the call-graph argument holds as long as the only caller is the lifespan, while the import-graph argument would break the moment someone adds an import. The inline comment at _env.yaml:38-42 already gives the right reason ("never start the app") — good comment to have left.
3. Minor notes (non-blocking)
.Values.envbypasses both guards.--set env.OPENHANDS_ORG_DEFAULTS_CONDENSER_OVERWRITE_EXISTING=truerenders the destructive flag with noapplyToExistingand nomaxTokens, because the dedup wrapper appends overrides aftervalidations.yamlhas already run. Inherent to the override mechanism and true of every guarded value in this chart, so I don't think it needs fixing here — noting it so nobody mistakes the guards for a hard invariant.- Guards are scoped to
.Values.enabled.enabled=falsewithoverwriteExisting: trueand nomaxTokensrenders silently. That matches the convention documented at the top ofvalidations.yamland is harmless since no workload renders. Correct as-is. maxTokensnaming — I accept your reasoning and withdraw the nit. One greppable name across values file, pod spec, and thecondenser.max_tokensDB field is worth more than a self-describing chart key, andvalues.yamlnow leads with what the value actually does, which was the real substance of the objection.
On the still-open items
Round-3 findings #2 (one-shot migration expressed as a persistent declarative value) and #3 (no merged backend reader) both remain, and I think your responses are the right calls. A helm.sh/hook Job genuinely can't be built until the backend exposes a standalone entrypoint, and that belongs on enterprise#337 rather than being approximated chart-side. The README now links the backend PR and states plainly that the current appVersion has no reader. For a draft that intentionally lands ahead of its backend, that's an acceptable place to be.
Risk assessment
Medium. A default install is unaffected — maxTokens: null ships disabled and renders no env, verified. Risk sits entirely on the opt-in path, in two layers. The documented layer: overwriteExisting: true is an irreversible DB mutation that re-applies on every worker start — 7 times on a fresh default install given --workers 3/2/2 — which the README now states accurately and the advisory lock serializes. The undocumented layer is #1, where a schema-valid values file takes down the app tier with an error that points at nothing. The first is well handled; the second is a one-token fix and the only thing I'd want changed before this leaves draft.
This review was generated by an AI agent (OpenHands) on behalf of @aivong-openhands.
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 <openhands@all-hands.dev>
|
Thanks — the round-4 findings are addressed in f12c06c. 1. Schema-valid
|
aivong-openhands
left a comment
There was a problem hiding this comment.
🟢 Taste Rating: Good taste — the chart change follows the established openhands.env.defaults / de-duplication pattern, keeps the default install inert, and covers the real edge cases that came up in prior review rounds.
I re-ran the repo-specific .agents/skills/code-review.md guidance against the current head (037b7d5). I could not run Helm locally because this sandbox does not have helm installed, but GitHub checks are green on the current head, including helm-unittest, lint, test-scripts, lint-and-test (openhands, charts/openhands), and chart publish checks.
The round-4 crashloop finding is addressed: maxTokens is coerced with | int64 | quote, and the new unit coverage pins exponent notation rendering to "1000000". The env names match the referenced backend parser in enterprise#337, null orgDefaults / condenser blocks remain guarded, consumers can still override through .Values.env, and the README now accurately documents the app-server startup trigger plus the destructive/repeated nature of overwriteExisting.
No blocking issues found.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🟡 MEDIUM
Default installs are unaffected because orgDefaults.condenser.maxTokens: null renders no org-condenser env vars. The remaining risk is entirely opt-in: enabling applyToExisting / overwriteExisting mutates persisted org settings and can re-apply on app-worker startup. The chart now makes that explicit, defaults the destructive flags off, validates contradictory rollout combinations, and covers the important render-time failure modes.
VERDICT:
✅ Worth merging: Core chart logic is sound, the previous important issue is fixed, and CI is green.
KEY INSIGHT:
The implementation keeps the hazardous database mutation behind explicit opt-in values while preserving the chart’s override-friendly env pattern.
This review was generated by an AI agent (OpenHands) on behalf of @aivong-openhands. Submitted as a COMMENT rather than APPROVE because the available GitHub token authenticates as the PR author.
aivong-openhands
left a comment
There was a problem hiding this comment.
Code review — feat: PLTF-3553 Add Helm values for org condenser defaults (round 5)
Reviewed against .agents/skills/code-review.md. I cloned f12c06c, added the five missing chart repos, ran helm dependency build, helm lint (clean), and helm unittest charts/openhands → 154 passed. All 23 CI checks are green on this commit (1 skipped).
Verdict: approve. The round-4 finding is fixed, I re-verified that fix and the earlier rounds' fixes independently, and nothing left is above nitpick level. Posted as a comment rather than a formal approval only because GitHub rejects self-approval from the PR author's token; per the repo guidelines this is an APPROVE, and it needs a second pair of eyes to register as one.
Verified, not restated
I re-derived each claim rather than trusting the description:
- The
int64fix works, and the test covering it is load-bearing.maxTokens: 1e6in a values file renders"1000000"on this commit (it rendered"1e+06"oncb9f6b0).1.5e6→"1500000",200000.0→"200000". Mutation-checked by dropping| int64from_env.yaml:50— the new exponent-notation test fails, the other six in the suite still pass. Your call to drop the second200000.0case was right: it passes with or without the pipe, so it would have been a test that cannot fail for the reason it appears to test. - Both render guards are load-bearing. Forcing
validations.yaml:19toif falsefails 1 test; forcing:22toif falsefails 1. Neither passes vacuously. - The nil guards hold on paths the tests do not cover.
--set orgDefaults=nulland--set orgDefaults.condenser=nullboth render clean, and so does a real values file with explicitapplyToExisting: null/overwriteExisting: null— renders"false", which--set-based tests cannot reach. - Schema rejects what it should.
0and-1rejected onminimum;200000.5and0.5rejected as non-integer;"200000"rejected as a string.2000000accepted, so the round-2 removal of the invented ceiling stands. - The CronJob question from rounds 3/4 is settled. With
maxTokensset, the env lands on 6 workloads —Deployment/openhands(--workers 3),openhands-integrations(2),openhands-mcp(2), and CronJobst-app-conversation-start-task-clean(0 3 * * *),t-budget-maintenance(*/15 * * * *),t-maintenance-tasks(0 6 * * *). I dumped each workload's command: the three Deployments runuvicorn saas_server:app, the three CronJobs runpython -mentrypoints that never construct the FastAPI app. Since_reconcile_org_condenser_defaultshas exactly one non-test caller (SaasAppLifespanService.__aenter__), the CronJob copies are inert — 7 executions on a fresh install, none of them every 15 minutes. The inline comment at_env.yaml:38-42records the call-graph reason rather than the import-graph one, which is the durable argument. - Default install is unaffected.
helm templatewith stock values renders zeroCONDENSERenv vars. - Established pattern followed. The vars go through
openhands.env.defaults, so.Values.envoverride works — I confirmedenv.OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS: "999"wins over a configuredmaxTokens: 200000.
Minor notes (non-blocking, no action needed here)
1. int64 has the same failure mode at the top of its range. It overflows silently rather than erroring — with maxTokens: 1e30 in a values file:
$ helm template t charts/openhands -f /tmp/v.yaml | grep -A1 MAX_TOKENS
- name: OPENHANDS_ORG_DEFAULTS_CONDENSER_MAX_TOKENS
value: "-9223372036854775808"9223372036854775807 and 99999999999999999999 do the same; 9007199254740993 renders 9007199254740992 (float64 precision). '-9223372036854775808'.isdecimal() is False, so _parse_positive_int_env raises and you get exactly the round-4 crashloop.
I am not asking for a change. The round-4 input was 1e6, a plausible way to write a million tokens; nothing in this range is a plausible token count, and the obvious guard — a schema maximum — is the chart-invented ceiling you correctly removed in round 2 on my own advice. Noting it so the int64 fix is not mistaken for a total one.
2. Misspelled keys are silently ignored. orgDefaults.condensor.maxTokens and orgDefaults.condenser.maxToken both render nothing and exit 0 — values.schema.json does not set additionalProperties: false on the new objects. That is chart-wide convention rather than something this PR introduced, so tightening it here would be inconsistent.
3. README rollback wording is slightly off from the guard. Line 39 offers "Remove applyToExisting/overwriteExisting ... or remove orgDefaults.condenser.maxTokens." Taking only the second branch fails the render:
Error: ... orgDefaults.condenser.maxTokens must be set before applyToExisting or overwriteExisting can be enabled.
The guard message says exactly what to do next, so an operator is never stuck — but the two branches are not independent, and "remove the flags, then the threshold" would be more accurate.
4. Prior rounds' accepted items still stand as accepted. .Values.env bypasses both guards (inherent to the dedup wrapper, true of every guarded value in this chart); guards scoped to .Values.enabled (matches the convention documented atop validations.yaml); enterprise#337 still open with appVersion: 1.59.1 carrying no reader (the README states this plainly and links the PR, and this is a draft landing deliberately ahead of its backend).
Against the repo guidelines
- Consumer-friendly configuration — the three keys name their effect, not our environment. No
saasMode-style scenario key. - Booleans truthful in every combination — the
overwriteExisting-without-applyToExistingcombination is rejected at render time rather than allowed to lie. - One knob, one effect —
maxTokenssets the threshold, the two flags scope reconciliation. No knob smuggles a second effect. - Defaults serve the self-hosted user —
null/false, inert, and the PR does not justify anything by pointing at our dev/staging/prod. - Override-friendly — every value overridable via
.Values.envwithout forking. - Data outlives the pod — N/A, no storage backend selection here.
Risk assessment
Low-to-medium. Default installs render nothing, verified. Risk is confined to the opt-in path: overwriteExisting: true is an irreversible DB mutation re-applied on every worker start (7 times on a fresh default install), which the README now states accurately and the backend serializes with pg_advisory_xact_lock. The undocumented crashloop path that held this at COMMENT last round is closed for every realistic input. What remains is documented, guarded, or implausible.
This review was generated by an AI agent (OpenHands) on behalf of the user.
Co-authored-by: openhands <openhands@all-hands.dev>
|
Addressed the round-5 README rollback wording nit in 1c87257. The docs no longer present removing Docs-only change; no chart behavior changed. This comment was created by an AI agent (OpenHands) on behalf of the user. |
aivong-openhands
left a comment
There was a problem hiding this comment.
🟢 Taste Rating: Good taste — the latest head keeps the chart behavior simple, override-friendly, and guarded against the real rollout hazards identified in prior rounds.
Reviewed PR #1214 against the repository-specific .agents/skills/code-review.md guidance at head 1c8725761bece9879cfc7c03ed3fa187fad17eee.
The only change since the prior approval-equivalent review is docs-only: charts/openhands/README.md now correctly tells operators to remove applyToExisting / overwriteExisting after the rollout before optionally removing orgDefaults.condenser.maxTokens. That fixes the misleading alternative wording without changing rendered chart behavior.
I re-checked the important chart mechanics on the current head:
OPENHANDS_ORG_DEFAULTS_CONDENSER_*env vars are emitted only whenorgDefaults.condenser.maxTokensis set.- The new defaults flow through the existing
openhands.envde-duplication wrapper, so consumers can override them via.Values.envwithout duplicate env vars. - Explicit
orgDefaults: nullandorgDefaults.condenser: nullremain guarded by thedefault dictpattern. - The rollout guard rejects
overwriteExisting: trueunlessapplyToExisting: true, and rejects rollout flags without a threshold. maxTokensis coerced throughint64 | quote, so schema-valid exponent/float integer forms render as decimal strings the backend parser can accept.- Current GitHub checks are green on this head, including
helm-unittest,lint,test-scripts, chart lint/test, publish-chart checks, and image scans. I could not run Helm locally in this sandbox becausehelmis not installed. - There are no unresolved review threads.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🟢 LOW
This is a small Helm values/env/docs/test change with inert defaults, explicit opt-in for persisted-data reconciliation, schema and render-time guards for unsafe combinations, and passing CI. The destructive overwriteExisting path is documented and gated behind explicit operator configuration.
VERDICT:
✅ Worth merging: Core logic is sound, the docs now describe the operational lifecycle accurately, and no important issues remain.
KEY INSIGHT:
The implementation preserves the existing chart contract: default installs do nothing, while opt-in operators get explicit values, override compatibility, and guardrails around repeated reconciliation.
This review was created by an AI agent (OpenHands) on behalf of the user.
|
live validation OpenHands/enterprise#337 (comment) |
|
🚀 Released in openhands/0.68.0. |
Summary
orgDefaults.condenser.maxTokens,applyToExisting, andoverwriteExistingvalues.maxTokensis set, guarding the nested lookup so an explicitly nulledorgDefaults/condenserblock renders cleanly.maxTokensthroughint64before quoting, so a values file using YAML float or exponent notation (1e6) renders"1000000"rather than"1e+06". JSON Schema'sintegertype accepts a zero-fraction float, and the backend parses the value withisdecimal()ahead of itsapply_to_existingearly return, so the unconverted form raised out of the app-server startup lifespan and crashlooped the app tier regardless of the rollout flags.saas_serverworker process and never from the maintenance CronJobs that share the env block — along with rollback caveats, the SDK-side effective-limit behavior, and the tracking PR for the app version that reads these vars.Validation
helm unittest charts/openhands(154 passed)helm lint charts/openhandshelm dependency build charts/openhandshelm templatewith--set orgDefaults=nulland--set orgDefaults.condenser=nullhelm templatewith a values file settingmaxTokens: 1e6andmaxTokens: 200000.0(render"1000000"and"200000")| default falseand| int64from_env.yaml(each fails as expected)_reconcile_org_condenser_defaultshas exactly one non-test caller (SaasAppLifespanService.__aenter__), which only becomes a lifespan viaget_app_lifespan_service()inopenhands/app_server/app.py, and the three CronJob entrypoints runpython -mpaths that never construct the FastAPI app. An earlier revision of this description claimed an AST walk showed the entrypoints do not importsaas_server/app_lifespan; that measured direct imports only and was wrong about the transitive closure — see the round-4 review. Importing does not execute, so the conclusion stands for the call-graph reason.uv run --with pytest --with requests --with PyGithub --with fastapi --with uvicorn --with httpx --with ruamel.yaml --with PyYAML python -m pytest scripts -qThis PR was updated by an AI agent (OpenHands) on behalf of the user.
@aivong-openhands can click here to continue refining the PR