Server half of the "fail soft" initiative: the server never emits malformed renders, tool/provider failures become structured error parts, and every turn ends. Adds the client error report endpoint the Next UI will call.
Render validation (engine/RenderShapes): every part, tool render (records, record, count, widget, history, pending_action, bulk_load_dry_run) and presentation (table, chart, metrics, filterLinks, recordLinks, actions blocks) is normalized before it is persisted or sent over SSE, and again when stored parts / turn events are read (repair on read). Required lists are always present; a render that cannot be repaired is dropped and replaced by an error notice ("This result could not be displayed."); unknown part and render types pass through. Problems are logged with type + fixed code only.
Failures as error parts: tool exceptions (including Errors and toolbox failures) and timeouts become an ERROR (is_error) result for the model, an ERROR tool part and an error notice; internal details go only to the log.
Timeouts: new toolCallTimeoutSeconds (default 60) per tool call, measured from when the call gets a permit and capped by the turn deadline; the existing maxTurnSeconds (default 300) still ends a turn with status ERROR and an error event code TIMEOUT.
Terminal state always: terminal events (usage, error, message.end) are emitted independently, so a failing emit never skips message.end or the terminal turn row. Conversation detail repairs assistant messages left STREAMING (orphaned runs are reaped; a run that already ended gives its status); the retention process finalizes runs still RUNNING past the hard stream limit (e.g. after a pod restart). The recovery writes are shared in engine/AbandonedTurns.
POST /qqq/v1/assistant/client-errors (UI API extension assistant 1.5.0): request AssistantClientErrorReportV1, response 202 AssistantClientErrorResponseV1 {accepted}. Any signed-in user; scope and message required (400 otherwise, and for a non-object body). Fields bounded, message/stacks scrubbed (secrets via AiRedactor, e-mails, digit runs > 6, mixed tokens, query strings), route path only, ids kept only when they are the user's own. Over clientErrorsPerUserPerMinute (default 30): 202 {"accepted": false}, dropped. Rows go to the new owner-locked assistant_client_error table (Assistant app nav for admins, record screens, help, channel wall via the assistant_* pattern, SET NULL FKs in the reference DDL, retention after toolResultRetentionDays). Health check assistant.client-errors is DEGRADED at clientErrorWarnPerHour (default 20) reports/hour, which also shows on the Assistant health card.
Upgrade
Apply the Fail soft block at the end of docs/schema-postgres.sql (creates assistant_client_error; IF NOT EXISTS). See UPGRADE.md / CHANGELOG.md.
FailSoftTurnsTest: tool exception → error part, tool timeout, turn timeout (error code TIMEOUT), exception in the loop → message.end ERROR + turn/message finalized + unregistered + no lockout, repair on read of stuck/legacy messages, retention sweep.
ClientErrorsTest: bounds and scrubbing, owned ids only, required fields, per-user rate limit, owner security, HTTP 202/400, health check, retention.
Contract (AssistantSpecsContractTest, regenerated assistant.yaml), guardrails (tables, help bundle, FK rules), producer nav and health registration updated.
## Summary
Server half of the "fail soft" initiative: the server never emits malformed renders, tool/provider failures become structured error parts, and every turn ends. Adds the client error report endpoint the Next UI will call.
- **Render validation** (`engine/RenderShapes`): every part, tool render (records, record, count, widget, history, pending_action, bulk_load_dry_run) and presentation (table, chart, metrics, filterLinks, recordLinks, actions blocks) is normalized before it is persisted or sent over SSE, and again when stored parts / turn events are read (repair on read). Required lists are always present; a render that cannot be repaired is dropped and replaced by an error notice ("This result could not be displayed."); unknown part and render types pass through. Problems are logged with type + fixed code only.
- **Failures as error parts**: tool exceptions (including `Error`s and toolbox failures) and timeouts become an ERROR (`is_error`) result for the model, an ERROR tool part and an error notice; internal details go only to the log.
- **Timeouts**: new `toolCallTimeoutSeconds` (default 60) per tool call, measured from when the call gets a permit and capped by the turn deadline; the existing `maxTurnSeconds` (default 300) still ends a turn with status ERROR and an `error` event code `TIMEOUT`.
- **Terminal state always**: terminal events (`usage`, `error`, `message.end`) are emitted independently, so a failing emit never skips `message.end` or the terminal turn row. Conversation detail repairs assistant messages left `STREAMING` (orphaned runs are reaped; a run that already ended gives its status); the retention process finalizes runs still `RUNNING` past the hard stream limit (e.g. after a pod restart). The recovery writes are shared in `engine/AbandonedTurns`.
- **`POST /qqq/v1/assistant/client-errors`** (UI API extension assistant **1.5.0**): request `AssistantClientErrorReportV1`, response `202 AssistantClientErrorResponseV1 {accepted}`. Any signed-in user; `scope` and `message` required (400 otherwise, and for a non-object body). Fields bounded, message/stacks scrubbed (secrets via AiRedactor, e-mails, digit runs > 6, mixed tokens, query strings), route path only, ids kept only when they are the user's own. Over `clientErrorsPerUserPerMinute` (default 30): `202 {"accepted": false}`, dropped. Rows go to the new owner-locked `assistant_client_error` table (Assistant app nav for admins, record screens, help, channel wall via the `assistant_*` pattern, SET NULL FKs in the reference DDL, retention after `toolResultRetentionDays`). Health check `assistant.client-errors` is DEGRADED at `clientErrorWarnPerHour` (default 20) reports/hour, which also shows on the Assistant health card.
## Upgrade
Apply the **Fail soft** block at the end of `docs/schema-postgres.sql` (creates `assistant_client_error`; `IF NOT EXISTS`). See UPGRADE.md / CHANGELOG.md.
## Tests
- `RenderShapesTest`: normalization per render type, presentation blocks, unknown types, unrepairable renders → notices, events.
- `FailSoftTurnsTest`: tool exception → error part, tool timeout, turn timeout (`error` code TIMEOUT), exception in the loop → `message.end` ERROR + turn/message finalized + unregistered + no lockout, repair on read of stuck/legacy messages, retention sweep.
- `ClientErrorsTest`: bounds and scrubbing, owned ids only, required fields, per-user rate limit, owner security, HTTP 202/400, health check, retention.
- Contract (`AssistantSpecsContractTest`, regenerated `assistant.yaml`), guardrails (tables, help bundle, FK rules), producer nav and health registration updated.
`./mvnw verify` passes locally (JaCoCo gate met).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Server half of the "fail soft" initiative: the server never emits malformed renders, tool/provider failures become structured error parts, and every turn ends. Adds the client error report endpoint the Next UI will call.
engine/RenderShapes): every part, tool render (records, record, count, widget, history, pending_action, bulk_load_dry_run) and presentation (table, chart, metrics, filterLinks, recordLinks, actions blocks) is normalized before it is persisted or sent over SSE, and again when stored parts / turn events are read (repair on read). Required lists are always present; a render that cannot be repaired is dropped and replaced by an error notice ("This result could not be displayed."); unknown part and render types pass through. Problems are logged with type + fixed code only.Errors and toolbox failures) and timeouts become an ERROR (is_error) result for the model, an ERROR tool part and an error notice; internal details go only to the log.toolCallTimeoutSeconds(default 60) per tool call, measured from when the call gets a permit and capped by the turn deadline; the existingmaxTurnSeconds(default 300) still ends a turn with status ERROR and anerrorevent codeTIMEOUT.usage,error,message.end) are emitted independently, so a failing emit never skipsmessage.endor the terminal turn row. Conversation detail repairs assistant messages leftSTREAMING(orphaned runs are reaped; a run that already ended gives its status); the retention process finalizes runs stillRUNNINGpast the hard stream limit (e.g. after a pod restart). The recovery writes are shared inengine/AbandonedTurns.POST /qqq/v1/assistant/client-errors(UI API extension assistant 1.5.0): requestAssistantClientErrorReportV1, response202 AssistantClientErrorResponseV1 {accepted}. Any signed-in user;scopeandmessagerequired (400 otherwise, and for a non-object body). Fields bounded, message/stacks scrubbed (secrets via AiRedactor, e-mails, digit runs > 6, mixed tokens, query strings), route path only, ids kept only when they are the user's own. OverclientErrorsPerUserPerMinute(default 30):202 {"accepted": false}, dropped. Rows go to the new owner-lockedassistant_client_errortable (Assistant app nav for admins, record screens, help, channel wall via theassistant_*pattern, SET NULL FKs in the reference DDL, retention aftertoolResultRetentionDays). Health checkassistant.client-errorsis DEGRADED atclientErrorWarnPerHour(default 20) reports/hour, which also shows on the Assistant health card.Upgrade
Apply the Fail soft block at the end of
docs/schema-postgres.sql(createsassistant_client_error;IF NOT EXISTS). See UPGRADE.md / CHANGELOG.md.Tests
RenderShapesTest: normalization per render type, presentation blocks, unknown types, unrepairable renders → notices, events.FailSoftTurnsTest: tool exception → error part, tool timeout, turn timeout (errorcode TIMEOUT), exception in the loop →message.endERROR + turn/message finalized + unregistered + no lockout, repair on read of stuck/legacy messages, retention sweep.ClientErrorsTest: bounds and scrubbing, owned ids only, required fields, per-user rate limit, owner security, HTTP 202/400, health check, retention.AssistantSpecsContractTest, regeneratedassistant.yaml), guardrails (tables, help bundle, FK rules), producer nav and health registration updated../mvnw verifypasses locally (JaCoCo gate met).🤖 Generated with Claude Code