diff --git a/apps/backend/internal/system/frontenderrors/handler.go b/apps/backend/internal/system/frontenderrors/handler.go index 9400a8b246..6b0052c74b 100644 --- a/apps/backend/internal/system/frontenderrors/handler.go +++ b/apps/backend/internal/system/frontenderrors/handler.go @@ -1,5 +1,5 @@ -// Package frontenderrors accepts bounded, best-effort reports for error toasts -// already displayed by the browser. +// Package frontenderrors accepts bounded, best-effort browser reports for +// displayed error toasts and backend-reload recovery. package frontenderrors import ( @@ -21,13 +21,20 @@ import ( ) const ( - maxRequestBodyBytes = 64 * 1024 - titleByteLimit = 8 * 1024 - descriptionByteLimit = 8 * 1024 - urlByteLimit = 8 * 1024 - taskIDByteLimit = 128 - browserFieldByteLimit = 2 * 1024 - stackByteLimit = 16 * 1024 + maxRequestBodyBytes = 64 * 1024 + titleByteLimit = 8 * 1024 + descriptionByteLimit = 8 * 1024 + urlByteLimit = 8 * 1024 + taskIDByteLimit = 128 + browserFieldByteLimit = 2 * 1024 + stackByteLimit = 16 * 1024 + sourceSonner = "sonner" + sourceToastProvider = "toast-provider" + sourceBackendReload = "backend-reload" + reloadBootIDChanged = "boot_id_changed" + reloadInterlockRejected = "settings_interlock_rejected" + frontendErrorMessage = "frontend error toast" + backendReloadMessage = "frontend backend reload required" ) type Viewport struct { @@ -128,12 +135,21 @@ func decodeRequest(c *gin.Context) (Request, int, int, error) { } func normalize(request Request) (normalizedRequest, error) { - if request.Source != "sonner" && request.Source != "toast-provider" { - return normalizedRequest{}, errors.New("unsupported toast source") + if request.Source != sourceSonner && request.Source != sourceToastProvider && + request.Source != sourceBackendReload { + return normalizedRequest{}, errors.New("unsupported report source") } if strings.TrimSpace(request.Title) == "" && strings.TrimSpace(request.Description) == "" { return normalizedRequest{}, errors.New("title or description is required") } + if request.Source == sourceBackendReload { + if request.Title != reloadBootIDChanged && request.Title != reloadInterlockRejected { + return normalizedRequest{}, errors.New("unsupported backend reload signal") + } + if request.Description != "" || request.Stack != "" || request.Error != nil { + return normalizedRequest{}, errors.New("backend reload report contains error data") + } + } if request.ClientTimestamp != "" { if _, err := time.Parse(time.RFC3339Nano, request.ClientTimestamp); err != nil { return normalizedRequest{}, errors.New("client_timestamp must be RFC3339") @@ -208,5 +224,9 @@ func (s *Service) logRequest(userID string, request normalizedRequest) { zap.String("error_stack", request.Error.Stack), ) } - s.log.Error("frontend error toast", fields...) + if request.Source == sourceBackendReload { + s.log.Info(backendReloadMessage, fields...) + return + } + s.log.Error(frontendErrorMessage, fields...) } diff --git a/apps/backend/internal/system/frontenderrors/handler_test.go b/apps/backend/internal/system/frontenderrors/handler_test.go index 4c1c1fd4e9..a3a328bc09 100644 --- a/apps/backend/internal/system/frontenderrors/handler_test.go +++ b/apps/backend/internal/system/frontenderrors/handler_test.go @@ -17,6 +17,7 @@ import ( "go.uber.org/zap/zaptest/observer" ) +// @covers AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.18 func TestHandlerLogsBoundedStructuredFrontendError(t *testing.T) { router, observed := newHandlerTestRouter(t, "user-1") body := `{ @@ -55,6 +56,65 @@ func TestHandlerLogsBoundedStructuredFrontendError(t *testing.T) { } } +// @covers AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.17 +func TestHandlerLogsBackendReloadDiagnosticAtInfo(t *testing.T) { + for _, signal := range []string{"boot_id_changed", "settings_interlock_rejected"} { + t.Run(signal, func(t *testing.T) { + router, observed := newHandlerTestRouter(t, "user-1") + body := `{ + "client_timestamp":"2026-09-05T15:52:11.255Z", + "source":"backend-reload", + "task_id":"task-123", + "title":"` + signal + `", + "url":"http://localhost/t/task-123" + }` + + response := performFrontendErrorRequest(router, body) + if response.Code != http.StatusNoContent { + t.Fatalf("status = %d, body = %s", response.Code, response.Body.String()) + } + + entries := observed.All() + if len(entries) != 1 { + t.Fatalf("logged entries = %d, want 1", len(entries)) + } + entry := entries[0] + if entry.Level != zapcore.InfoLevel || entry.Message != "frontend backend reload required" { + t.Fatalf("entry = %s %q", entry.Level, entry.Message) + } + fields := entry.ContextMap() + if fields["source"] != "backend-reload" || fields["title"] != signal { + t.Fatalf("structured fields = %#v", fields) + } + }) + } +} + +func TestHandlerRejectsInvalidBackendReloadReports(t *testing.T) { + tests := []struct { + name string + body string + }{ + {name: "unknown signal", body: `{"source":"backend-reload","title":"other"}`}, + {name: "error data", body: `{"source":"backend-reload","title":"boot_id_changed","stack":"toast stack"}`}, + {name: "description data", body: `{"source":"backend-reload","title":"boot_id_changed","description":"should be rejected"}`}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + router, observed := newHandlerTestRouter(t, "user-1") + response := performFrontendErrorRequest(router, test.body) + if response.Code != http.StatusBadRequest { + t.Fatalf("status = %d, want %d, body = %s", + response.Code, http.StatusBadRequest, response.Body.String()) + } + if observed.Len() != 0 { + t.Fatalf("invalid request produced %d log entries", observed.Len()) + } + }) + } +} + func TestHandlerRejectsInvalidAndOversizedRequests(t *testing.T) { router, observed := newHandlerTestRouter(t, "user-1") tests := []struct { diff --git a/apps/web/lib/api/domains/frontend-error-log-api.test.ts b/apps/web/lib/api/domains/frontend-error-log-api.test.ts index 7e65fe0c31..a6528e4274 100644 --- a/apps/web/lib/api/domains/frontend-error-log-api.test.ts +++ b/apps/web/lib/api/domains/frontend-error-log-api.test.ts @@ -43,6 +43,35 @@ describe("frontend error log API", () => { expect(JSON.stringify(report)).not.toContain("must-not-be-walked"); }); + it("builds backend-reload reports without a synthetic toast stack", () => { + const report = buildFrontendErrorReport({ + source: "backend-reload", + title: "boot_id_changed", + }); + + expect(report.stack).toBeUndefined(); + }); + + it("omits error objects from backend-reload reports", () => { + const report = buildFrontendErrorReport({ + source: "backend-reload", + title: "boot_id_changed", + error: new Error("not a toast error"), + }); + + expect(report.error).toBeUndefined(); + }); + + it("omits descriptions from backend-reload reports", () => { + const report = buildFrontendErrorReport({ + source: "backend-reload", + title: "boot_id_changed", + description: "should be suppressed", + }); + + expect(report.description).toBeUndefined(); + }); + it("uses an authenticated two-second request and silently accepts 204", async () => { const fetchMock = vi .spyOn(globalThis, "fetch") diff --git a/apps/web/lib/api/domains/frontend-error-log-api.ts b/apps/web/lib/api/domains/frontend-error-log-api.ts index 0c092ceb4b..5ce9549c35 100644 --- a/apps/web/lib/api/domains/frontend-error-log-api.ts +++ b/apps/web/lib/api/domains/frontend-error-log-api.ts @@ -8,7 +8,7 @@ const BROWSER_FIELD_LIMIT = 2 * 1024; const STACK_LIMIT = 16 * 1024; const MAX_REACT_TEXT_NODES = 32; -export type FrontendErrorSource = "sonner" | "toast-provider"; +export type FrontendErrorSource = "sonner" | "toast-provider" | "backend-reload"; export type FrontendErrorReportInput = { source: FrontendErrorSource; @@ -49,16 +49,20 @@ export function buildFrontendErrorReport(input: FrontendErrorReportInput): Front client_timestamp: new Date().toISOString(), source: input.source, title: visibleText(input.title), - description: visibleText(input.description), - stack: bounded(new Error("error toast emitted").stack, STACK_LIMIT), + ...(input.source === "backend-reload" ? {} : { description: visibleText(input.description) }), }; + if (input.source !== "backend-reload") { + report.stack = bounded(new Error("error toast emitted").stack, STACK_LIMIT); + } if (currentURL) { report.task_id = deriveTaskID(currentURL); report.url = bounded(`${currentURL.origin}${currentURL.pathname}`, TEXT_LIMIT); } addBrowserContext(report); - const error = errorDetails(input.error); - if (error) report.error = error; + if (input.source !== "backend-reload") { + const error = errorDetails(input.error); + if (error) report.error = error; + } return report; } diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index d5c5092791..9ab1767aef 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -23,7 +23,7 @@ installVitePreloadRecovery(); installBfcacheRestoreReload(); markRenderingEngine(document.documentElement); setBackendReloadDiagnosticReporter((source) => { - scheduleFrontendErrorReport({ source: "toast-provider", title: source }); + scheduleFrontendErrorReport({ source: "backend-reload", title: source }); }); const AUTH_ROUTE_PATHS = new Set(["/login", "/setup", "/invite"]); diff --git a/docs/plans/backend-reload-diagnostic-severity/plan.md b/docs/plans/backend-reload-diagnostic-severity/plan.md new file mode 100644 index 0000000000..f33f567cb6 --- /dev/null +++ b/docs/plans/backend-reload-diagnostic-severity/plan.md @@ -0,0 +1,103 @@ +--- +created: 2026-09-05 +status: done +requirements: + - REQ-PLATFORM-BACKEND-RESTART-PAGE-RECOVERY-001 + - REQ-PLATFORM-DIAGNOSTIC-LOGGING-001 + - REQ-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001 +system_design: + - ../../specs/platform/system-design/backend-restart-page-recovery.md + - ../../specs/platform/system-design/diagnostic-logging-01.md + - ../../specs/platform/system-design/diagnostic-logging-02.md + - ../../specs/platform/system-design/expected-runtime-log-severity.md +legacy_specs: [] +--- + +# Implementation Plan: Backend Reload Diagnostic Severity + +## Overview + +This fix keeps the reload-required behavior and records its report at info +level. One vertical work order changes the browser report contract and the +backend severity mapping together. + +## Root cause + +The backend-reload coordinator sends its expected lifecycle signal through the +error-toast report source. The backend treats every accepted report as an error +and uses the fixed message `frontend error toast`. + +## Scope + +### In scope + +- Give backend-reload reports a distinct, allow-listed source. +- Record valid backend-reload reports at info level with a fixed message. +- Keep actual Sonner and legacy error-toast reports at error level. +- Remove synthetic error-toast data from backend-reload reports. + +### Out of scope + +- Backend restart detection and boot ID generation. +- The reload-required state, alert, copy, and action. +- The report endpoint path, rate limits, field limits, or storage policy. +- General log-severity changes for other frontend events. + +## Technical approach + +### Browser report contract + +`apps/web/lib/api/domains/frontend-error-log-api.ts` adds a dedicated +backend-reload report path. The path uses `backend-reload` as its source and +does not create an error-toast stack or error object. + +`apps/web/src/main.tsx` uses the dedicated path. The title remains the +stable recovery signal from `BackendReloadSignal`. + +### Backend classification + +`apps/backend/internal/system/frontenderrors/handler.go` accepts +`backend-reload` only with `boot_id_changed` or +`settings_interlock_rejected`. It records these reports at info level with +the fixed message `frontend backend reload required`. + +The existing `sonner` and `toast-provider` sources keep the fixed +`frontend error toast` error entry. Client fields remain structured fields +and cannot select the severity. + +## Tests + +- `AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.17` maps to a new observer + test in `apps/backend/internal/system/frontenderrors/handler_test.go`. +- `AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.18` maps to the existing + error-toast observer test and an explicit unchanged-severity assertion. +- Frontend report tests cover the new source and the absence of synthetic + toast-error data. +- Existing generation-guard and coordinator tests confirm that restart + detection remains unchanged. + +The backend observer test is the regression test. Before the correction, a +backend-reload report is rejected or represented as an error-toast entry. + +## E2E tests + +No E2E change is required. The existing restart-recovery E2E behavior and user +interface remain unchanged. + +## Work orders + +- [x] [Task 01: Classify backend-reload reports](task-01-classify-backend-reload-reports.md) + +## Verification results + +- `go test ./internal/system/frontenderrors -count=1`: 17 tests passed. +- Focused Vitest command: 3 files and 20 tests passed. +- `pnpm run typecheck`: passed. +- Specification lint and `git diff --check`: passed. + +## Risks + +- An untrusted browser can try to use the new source to reduce severity. The + backend accepts only the two stable recovery titles. +- Info entries do not appear on default warning-only stdout. They remain in the + retained backend log and diagnostic bundles. diff --git a/docs/plans/backend-reload-diagnostic-severity/task-01-classify-backend-reload-reports.md b/docs/plans/backend-reload-diagnostic-severity/task-01-classify-backend-reload-reports.md new file mode 100644 index 0000000000..092a64871f --- /dev/null +++ b/docs/plans/backend-reload-diagnostic-severity/task-01-classify-backend-reload-reports.md @@ -0,0 +1,104 @@ +--- +id: "01-classify-backend-reload-reports" +title: "Classify backend-reload reports" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +requirements: + - REQ-PLATFORM-BACKEND-RESTART-PAGE-RECOVERY-001 + - REQ-PLATFORM-DIAGNOSTIC-LOGGING-001 + - REQ-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001 +acceptance_criteria: + - AC-PLATFORM-BACKEND-RESTART-PAGE-RECOVERY-001.3 + - AC-PLATFORM-BACKEND-RESTART-PAGE-RECOVERY-001.5 + - AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.17 + - AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.18 +system_design: + - ../../specs/platform/system-design/backend-restart-page-recovery.md + - ../../specs/platform/system-design/diagnostic-logging-01.md + - ../../specs/platform/system-design/diagnostic-logging-02.md + - ../../specs/platform/system-design/expected-runtime-log-severity.md +--- + +# Task 01: Classify Backend-Reload Reports + +## Summary + +Add a distinct source for expected backend-reload reports. Record this source +at info level while actual error-toast reports remain errors. + +## In scope + +- Add the `backend-reload` source to the frontend and backend contracts. +- Restrict the source to the two stable reload signals. +- Omit synthetic error-toast stacks and error objects from reload reports. +- Add backend observer tests and frontend payload tests. + +## Out of scope + +- Change the restart detector, reload coordinator, or reload alert. +- Rename the existing endpoint. +- Reclassify any other frontend or backend event. + +## Acceptance + +- A valid backend-reload report creates one info entry named + `frontend backend reload required` and no error entry. +- Sonner and legacy error-toast reports keep their current error entry. +- The frontend sends one reload report with browser context and no synthetic + toast-error data. + +## Verification + +```bash +cd apps/backend +go test ./internal/system/frontenderrors -count=1 +``` + +```bash +cd apps/web +pnpm exec vitest run lib/api/domains/frontend-error-log-api.test.ts lib/platform/backend-reload-coordinator.test.ts hooks/domains/system/use-backend-generation-guard.test.ts +``` + +```bash +cd apps/web +pnpm run typecheck +``` + +## Files likely touched + +- `apps/backend/internal/system/frontenderrors/handler.go` +- `apps/backend/internal/system/frontenderrors/handler_test.go` +- `apps/web/lib/api/domains/frontend-error-log-api.ts` +- `apps/web/lib/api/domains/frontend-error-log-api.test.ts` +- `apps/web/src/main.tsx` + +## Dependencies + +None. + +## Risks + +- The backend must validate the source and signal before it selects info level. +- Existing toast reports must keep their message, fields, and error severity. + +## Parallelism + +`sequential` + +## Inputs + +- The requirements and system designs in the frontmatter. +- The observer-backed frontend-error handler tests. +- The backend-reload coordinator and generation-guard tests. + +## Results + +- RED: the backend rejected `backend-reload` with HTTP 400. +- RED: the frontend added the synthetic toast stack and error object. +- RED: the incomplete allow-list accepted invalid recovery reports and rejected + `settings_interlock_rejected`. +- GREEN: the backend package passed 17 tests. +- GREEN: the three focused frontend files passed 20 tests. +- GREEN: the web typecheck passed. diff --git a/docs/specs/platform/requirements/expected-runtime-log-severity.md b/docs/specs/platform/requirements/expected-runtime-log-severity.md index 6e9cb34b23..e225249679 100644 --- a/docs/specs/platform/requirements/expected-runtime-log-severity.md +++ b/docs/specs/platform/requirements/expected-runtime-log-severity.md @@ -2,7 +2,7 @@ status: active system: platform created: 2026-08-23 -updated: 2026-08-28 +updated: 2026-09-05 owners: - kandev --- @@ -37,6 +37,8 @@ This behavior obscures actionable errors and makes diagnostic bundles harder to - **AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.14:** **GIVEN** a child emits unstructured stderr, **WHEN** the launcher forwards the line, **THEN** the parent records it at warning level. - **AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.15:** **GIVEN** a session references a missing task environment, **WHEN** workspace information is loaded, **THEN** the task fallback succeeds without a warning entry. - **AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.16:** **GIVEN** a task-environment lookup returns another error, **WHEN** workspace information is loaded, **THEN** the existing fallback and warning behavior remains unchanged. +- **AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.17:** **GIVEN** an open page detects a changed backend `boot_id`, **WHEN** its recovery report is accepted, **THEN** the backend records one info-level reload entry. It records no `frontend error toast` error entry. +- **AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.18:** **GIVEN** Sonner or the legacy toast provider displays an error toast, **WHEN** its report is accepted, **THEN** the backend records one `frontend error toast` error entry. ## Migrated source detail @@ -91,6 +93,9 @@ status and message path. The task fallback remains authoritative for workspace information. - Other task-environment lookup errors can indicate a storage problem and remain warnings. +- A changed backend generation is an expected lifecycle condition. Its browser + report remains diagnostic evidence, but it is not an error-toast report. +- Actual error toasts keep their error-level records. ## Persistence guarantees @@ -125,6 +130,11 @@ persistence, or cleanup behavior across backend restarts. - **GIVEN** a task-environment lookup returns another error, **WHEN** workspace information is loaded, **THEN** the existing fallback and warning behavior remains unchanged. +- **GIVEN** an open page detects a changed backend `boot_id`, **WHEN** its + recovery report is accepted, **THEN** one info entry records the event and no + `frontend error toast` error entry is emitted. +- **GIVEN** an actual error toast is displayed, **WHEN** its report is accepted, + **THEN** the existing `frontend error toast` error entry remains unchanged. ## Out of scope @@ -133,3 +143,5 @@ persistence, or cleanup behavior across backend restarts. or startup reconciliation. - Reclassifying integration failures, raw ACP frames, host-utility idle repair, network disconnects, or authentication warnings. +- Changing backend-restart detection, the reload-required state, or the reload + alert. diff --git a/docs/specs/platform/system-design/backend-restart-page-recovery.md b/docs/specs/platform/system-design/backend-restart-page-recovery.md index 1b5d3b9ed1..f79e146e9a 100644 --- a/docs/specs/platform/system-design/backend-restart-page-recovery.md +++ b/docs/specs/platform/system-design/backend-restart-page-recovery.md @@ -4,7 +4,7 @@ system: platform requirements: - REQ-PLATFORM-BACKEND-RESTART-PAGE-RECOVERY-001 created: 2026-09-03 -updated: 2026-09-03 +updated: 2026-09-05 owners: - kandev --- @@ -167,9 +167,15 @@ not expose the token. ## Observability The coordinator sends one frontend diagnostic report when a document first -enters reload-required state. The report includes the signal source and no -token value. Valid sources are `boot_id_changed` and -`settings_interlock_rejected`. +enters reload-required state. The report uses the `backend-reload` report +source. Its title is `boot_id_changed` or `settings_interlock_rejected`. + +The report includes browser context and no token value. It does not include a +synthetic error-toast stack or error object. The backend records the report at +info level with the fixed message `frontend backend reload required`. + +Actual error-toast reports keep their error severity and fixed message. The +client cannot supply a log level or message template. Backend logs keep the current HTTP request and restart evidence. This design does not add a metric or durable event. diff --git a/docs/specs/platform/system-design/diagnostic-logging-01.md b/docs/specs/platform/system-design/diagnostic-logging-01.md index fc9740b31d..1fdaea377e 100644 --- a/docs/specs/platform/system-design/diagnostic-logging-01.md +++ b/docs/specs/platform/system-design/diagnostic-logging-01.md @@ -80,6 +80,8 @@ history or returning an unbounded log export. - Every error toast displayed through either supported frontend toast system is reported immediately to the backend and written as an `error` entry named `frontend error toast`. +- A backend-reload alert is not an error toast. Its allow-listed recovery report + uses the info-level contract in part 2 of this design. - Toast reporting includes the visible toast text plus rich browser context: the browser URL origin and pathname with query and fragment removed, browser identity fields, viewport dimensions, a client timestamp, the toast diff --git a/docs/specs/platform/system-design/diagnostic-logging-02.md b/docs/specs/platform/system-design/diagnostic-logging-02.md index 134f8ce92e..f10e89f906 100644 --- a/docs/specs/platform/system-design/diagnostic-logging-02.md +++ b/docs/specs/platform/system-design/diagnostic-logging-02.md @@ -51,7 +51,10 @@ The authenticated browser submits: } ``` -- `source` is `sonner` or `toast-provider`. +- `source` is `sonner`, `toast-provider`, or `backend-reload`. +- For `backend-reload`, `title` is `boot_id_changed` or + `settings_interlock_rejected`. The report omits `description`, `stack`, and + `error`. - `title` and `description` are optional individually, but at least one must contain visible text. - `client_timestamp` is RFC 3339 when supplied. The backend log timestamp @@ -73,8 +76,12 @@ The authenticated browser submits: - An exhausted identity or process-wide report bucket returns `429 Too Many Requests` with `Retry-After`. -The endpoint logs one structured `error` entry. Client fields remain fields; -they cannot replace the fixed `frontend error toast` message. +For `sonner` and `toast-provider`, the endpoint logs one structured error entry +with the fixed message `frontend error toast`. + +For `backend-reload`, the endpoint logs one structured info entry with the +fixed message `frontend backend reload required`. Client fields cannot replace +the fixed message or select a log level. ### Diagnostic bundle jobs @@ -307,8 +314,8 @@ with the same task/session rules and excludes message bodies and user identity. the caller's equivalent active job. It does not create unbounded concurrent archive or capture work. - If the browser report endpoint receives a network, authentication, - validation, or server failure, the original error toast remains unchanged - and the reporting promise is discarded without retry. + validation, or server failure, the original UI state remains unchanged. The + reporting promise is discarded without retry. - Unsupported or non-text toast content is omitted from the corresponding text field; reporting proceeds only when visible text can be extracted. - Daily rollover uses UTC calendar boundaries and never uses client timestamps @@ -339,7 +346,8 @@ with the same task/session rules and excludes message bodies and user identity. after becoming downloadable. They are not durable product data. - Improve Kandev's task-context ZIP is a separate internal lease on the shared archive builder and remains available for up to 24 hours. -- No toast report is stored in SQLite or queued in browser storage for retry. +- No toast or backend-reload report is stored in SQLite or queued in browser + storage for retry. - Concurrent backend processes sharing one Kandev home are unsupported. ## Scenarios @@ -367,6 +375,9 @@ with the same task/session rules and excludes message bodies and user identity. - **GIVEN** an error toast on a recognized task route, **WHEN** its report is accepted, **THEN** one backend error entry includes its visible text, browser context, and `task_id`. +- **GIVEN** a page detects a changed backend generation, **WHEN** its report is + accepted, **THEN** one backend info entry includes the recovery signal and + browser context. No error-toast entry is emitted. - **GIVEN** console activity over three days, **WHEN** browser retention runs, **THEN** expired and oldest-over-cap entries are removed without uploading retained entries. diff --git a/docs/specs/platform/system-design/expected-runtime-log-severity.md b/docs/specs/platform/system-design/expected-runtime-log-severity.md index 40e79320b3..c00c9dd0f7 100644 --- a/docs/specs/platform/system-design/expected-runtime-log-severity.md +++ b/docs/specs/platform/system-design/expected-runtime-log-severity.md @@ -4,6 +4,7 @@ system: platform requirements: - REQ-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001 created: 2026-08-28 +updated: 2026-09-05 owners: - kandev --- @@ -11,17 +12,18 @@ owners: ## Purpose and boundaries -This design preserves the severity of recognized child logs. It also removes -warnings from one expected task-environment fallback. +This design preserves the severity of recognized child logs. It also assigns +non-error levels to expected task-environment and backend-restart conditions. -The design does not change process control, workspace selection, or returned -errors. Unknown child stderr remains visible at warning level. +The design does not change process control, workspace selection, restart +detection, or returned errors. Unknown child stderr remains visible at warning +level. ## Requirement mapping | Requirement | Design section | | --- | --- | -| `REQ-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001` | [Child log forwarding](#child-log-forwarding) and [Task-environment fallback](#task-environment-fallback) | +| `REQ-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001` | [Child log forwarding](#child-log-forwarding), [Task-environment fallback](#task-environment-fallback), and [Frontend restart recovery](#frontend-restart-recovery) | ## Child log forwarding @@ -52,6 +54,24 @@ lookup error keeps the current warning and fallback behavior. The fallback result, workspace metadata, and caller response do not change. +## Frontend restart recovery + +An open application page compares its boot ID with the boot ID of a reconnected +backend. A mismatch is an expected process-lifecycle condition. + +The frontend sends this condition with the allow-listed `backend-reload` +report source. The title contains `boot_id_changed` or +`settings_interlock_rejected`. The report does not contain an error-toast stack +or error object. + +The backend accepts only these two titles for the `backend-reload` source. It +records the report at info level with the fixed message +`frontend backend reload required`. + +The `sonner` and `toast-provider` sources keep the fixed +`frontend error toast` message at error level. A client cannot select an +arbitrary severity or log message. + ## Failure and recovery An unknown child record remains a warning. This rule prevents a parser change @@ -61,6 +81,10 @@ An unexpected task-environment lookup error remains a warning. The task-based fallback can still return usable workspace information, but the storage error remains diagnostic evidence. +An invalid frontend report remains a bad request. A valid reload report cannot +reduce the severity of an actual toast report because each source has a fixed +severity. + ## Observability The parent log keeps the original child line and `stream` field. Only the parent @@ -69,7 +93,14 @@ severity changes when the parser recognizes the child severity. The stale task-environment path keeps the session and environment identifiers at debug level. Unexpected lookup errors keep the existing warning fields. +The reload entry keeps the browser context and stable recovery signal. It does +not use the error-toast message or a synthetic error stack. + ## Test strategy Table tests cover Zap, Go `slog`, malformed, and unstructured child lines. Observer-backed service tests cover typed not-found and unexpected lookup errors. + +Observer-backed frontend-report tests cover the reload info entry and the +unchanged error-toast entry. Frontend tests cover the distinct report source +and the absence of a synthetic toast stack.