Navigácia
ELOWEN · Dokumentácia

Posledná aktualizácia: 20. októbra 2026

Prechádzať dokumentáciu · Collectors and durable delivery

Tento článok zatiaľ nie je preložený. Je dostupný v angličtine.

Developer reference

Collectors and durable delivery

Proactive platform delivery

PlatformOrchestrator.notify keeps each failed adapter's native cause in an AggregateError when no adapter delivers. The diagnostic retains platform names and the existing message, while isInstanceCondition recognizes an aggregate only when every member is local. Cronjob uses that classification without deleting the pending payload or changing its retry threshold. Missing readiness alone is not a local condition: an adapter that omits onConnectionState must remain reportable. One successful adapter still acknowledges delivery; an instance with no notification sinks still resolves.

Daemon collector and durable delivery

The sole private logger sink at bootstrap composes the plugin ring and ProblemService independently. Typed plugin identity owns ring entries. Warn/error events reach the collector regardless of the local log threshold, while console, files and ring retain that threshold. No prefix/scope parser remains. Core Logger.warn/error require an explicit third metadata argument: a declared problem code, or reporting: 'local-only' for a specific operational diagnostic. Merely naming a plugin is not a reporting decision. External provider, process and transport error prose travels like any other report, with its redacted sample, while the local log keeps the diagnostic. Normal progress and successful fallbacks use info. No message-text classification or collector suppression is involved. The collector projects a problem code, level, real typed plugin names, validated inventory versions, counts and at most three distinct redacted message samples per aggregate. The code is the call site's declared PROBLEM_CODES entry. Live metadata validates the code against that catalog and permits only the declared local-only reporting policy. An untyped producer with absent or invalid classification is counted as runtime.reporting_invalid or plugin.reporting_invalid, without a sample, so metadata faults stay visible without exporting unknown prose. Codes are never derived from scope or message text. Restored immutable batches keep their original validated codes, including older scope catch-alls and unclassified, so already queued reports remain deliverable. Samples pass src/problems/redact.ts inside ProblemService.project(), which observe() calls, before anything reaches the outbox, and are single lines of at most 500 characters. Every entry that is not local-only carries its sample. All plugin names are included, including private, unlicensed and non-bundled plugins. There is no synthetic local bucket. The shared strict entry schema validates the same name/version rule for live and restored batches: names follow the canonical 2–64 character grammar; versions are numeric triplets or null. No-owner daemon events use none with a null version. Plugins actually named local or none remain ordinary validated names. Plugin version inventory is a boot snapshot. Observation performs no credential, filesystem or database reads; only the independent sender resolves the current credential before transport. Reportability is decided at the producer: a diagnostic is sent when it can drive an Elowen bug fix or refactor, not merely because it is useful to the local operator, while a host condition is the one audience the collector settles itself. Awaited boot-step completion timings and Sites application HTTP 5xx responses are local-only. runtime.event_loop_lag is the sole daemon-overload report; runtime.slow_step preserves its local severity, timings and attribution rings without a duplicate report. Unexpected provider-cache drops inside the TTL and missing hosted-search replay remain reported. A HostConditionError marks the host itself rather than Elowen: a database pool-acquisition failure (DatabaseConnectionError) or a severe event-loop stall explained by measured scheduling gaps. Each occurrence stays in the local log, and ProblemService reports the three that fall inside five minutes once the same problem identity occurs three times, then one per further occurrence while the condition keeps recurring; five quiet minutes start the window over. Explicit local-only metadata still wins, and no emitter derives that policy from a host condition: the shared turn runner marks only non-host instance causes local. See host conditions. runtime.stuck_step reports once on the third consecutive 30-second non-provider wait observation; provider-wait observations reset that sequence. Successful impersonation transitions are info-level audit facts. PostgreSQL query and transaction timing diagnostics remain local-only. The event-loop watchdog retains its 150 ms sustained p99 and 2-second severe-stall budget; an unattributed multi-second stall remains actionable even without a known producer, while a sync-step aggregate alone stays local. The same monitor exposes gcMs in /health.eventLoop and warnings: native GC durations clipped to the older staggered window, accumulated into two scalar sums by a PerformanceObserver. Pending records are drained before reads and window rolls; stop() disconnects the observer. Asynchronous delivery can lag a stall, and activity is not a causal attribution. No collector message filtering is used. Provider retries retain one elapsed timer across attempts and reset it at settlement or a new turn. guardProviderStreamIdle aborts silent bodies at its existing 120 s deadline and logs the attempt locally at INFO. Its stream wrapper retains timeout evidence on the actual failed assistant object in a WeakSet without parsing provider prose or changing the wire transcript. The terminal spawnEventReducer reads that evidence and reports provider.stream_idle_timeout at WARN only when agent_end.willRetry is false; a successful retry produces no timeout problem. ProviderRequestRecorder emits transport-attempt diagnostics at info/local-only; SpawnEventReducer owns terminal provider reporting. It defers rendering and reporting the final provider error until agent_settled; a successful account failover clears the pending error before it can report. Retry exhaustion comes from typed auto_retry_start attempt/maxAttempts, cleared on successful recovery, settlement and a newly admitted turn. Provider prose never determines retry exhaustion, timeout or image-rejection notices. Retry scheduling, permitted model fallback, cache eviction, tool activation and fan-out misses are info; retry exhaustion emits one terminal classification, and unexpected prefix mutation remains reportable. The per-retry notice is logged at info with the provider.retry code (src/brain/service/spawnEventReducer.ts:335), and the collector observes only warn and error lines (src/problems/service.ts:127), so the notice never enters the outbox. In-session compaction measures normalized system, tools and history plus its appended instruction and output allowance before sending. A warm request that cannot fit selects the standalone span, then PI's native compaction when needed. Fallback selection is info; terminal summarization failure is reportable. Hosted replay loss is reportable only after hosted content occurred, including content observed after capture abandonment. Reference filtering is info. Unsafe capture, refusal, successful repair, unsuccessful repair and wrapper failure declare distinct codes; their external prose travels redacted. Delegated progress returns SubagentProgressOutcome: accepted, already-terminal, or rejected with a validation, ownership or persistence reason. The durable lifecycle owns finality. Only accepted writes publish persisted progress and update live claims. Terminal updates are consumed without publication or claim changes. Background completion commits its durable result and terminal run projection in one PostgreSQL transaction and returns the same typed outcome. packages/db/src/delegation/runs.ts locks the parent, child and run rows before completion; live notifications run after commit. Accepted completion publishes the persisted terminal row, releases its progress claim and starts the existing result drain. Already-terminal callbacks are consumed silently; validation and ownership refusals report their explicit completion codes. Persistence failure leaves the run open for restart recovery. Delegate and DelegateContinue issue no independent terminal progress update after background completion. Live results enter through completeSubagentRun; restart and late-result recovery use the existing recovery ingress. There is no public non-atomic child-result enqueue path.

Runner startup carries a typed SpawnFailureCode through validated IPC and RunnerStartupError. The pool owns its one report and health classification; diagnostic prose never determines the category. Dispatch may fall back locally because a startup failure occurs before delegated turn execution.

Pending-delivery recovery snapshots raw pending result IDs, including malformed payloads. Wake-accounting and abandonment affect only captured IDs that remain pending for that parent, through bounded PostgreSQL updates and the durable counters in packages/db/src/delegation/inbox.ts. Empty batches do no work; later arrivals keep their own budget. Recovery joins the existing delivery drain outside turn locks after continuation, including a continuation that throws. slowStep accepts the live waitingOnProvider lifecycle predicate. Active provider compaction logs waiting and elapsed duration as local information; ordinary stuck-step diagnostics resume when it ends. The provider stream watchdog continues to own detection of a silent stream.

Dashboard digest eligibility is checked from bounded user messages and conversation summaries before claiming a daily generation. Usage counters, conversation titles and long-term memories alone do not trigger inference. Empty material does not consume an attempt; newly arriving material can make the next request eligible.

events.digests in packages/db/src/events/digests.ts owns the claim rule, and canClaim exposes it to the pure GET /dash/recap, so refreshDue is reported only when beginGeneration would claim. A new run starts on a day without a row, after a refresh window (digestPerDay) or on newer material past the one-hour floor, and only these count against digestPerDay. A retry continues the run on the row: a failed row after an hour, or a generating row older than ten minutes, at most three attempts, without spending a daily run. The admin's POST /dash/recap/regenerate claims with force: a counted new run past the window and the limit, starting from an empty payload so the dashboard polls its generating state, answering 409 when nothing could start. Nothing claims over a generating row that is not stale.

shapeDigestPayload in src/brain/dashDigest.ts removes ** emphasis markers from greetings, questions and action labels, then calls collapseWhitespace from src/shared/text.ts. Summaries retain their emphasis; payload caps, variant mirroring and persistence are unchanged. This synchronous cleanup adds no inference, scheduling or lifecycle work.

Digest text is validated by sanitizePayload in packages/db/src/events/digestPayload.ts on both write and read. After trimming, the caps are 96 characters for greeting, 64 for ask, 40 for action label, 500 for action prompt, and 400 for summary. Over-cap text becomes empty rather than a sliced fragment; an action missing either label or prompt is dropped. Other usable fields in the variant remain. The generator requests at most eight words and under 90 characters for a greeting, at most six words and under 60 characters for a question, and explicit character bounds for labels, prompts and summaries. Both the dashboard and new-conversation hero consume these variants through web/lib/heroCopy.ts: an empty greeting uses the localized time-of-day greeting and the reader's first name once the browser clock is available.

Maintenance shutdown cancels its timers and catalog/embedding workers, then awaits owned asynchronous sweeps before dependencies close. Plugin interval shutdown stops all host timers and drains returned tick promises before stopping contributed services, independent of registration order. Plugins must return their asynchronous work for the host to own it. The daemon checkpoints resumable turns first and keeps its bounded exit guard; wedged cleanup exits without closing dependencies underneath live work. Forwarded plugin metadata is retained while the host stamps the actual plugin owner. Runtime capability refusals and operator configuration diagnostics remain local-only.

Hook registry failures use plugin.registry_unavailable, except the licensing report endpoint, which stays local-only to prevent reporting transport recursion. Project provider failures carry explicit plugin ownership and operation-specific codes. Conversation titling and activity event persistence retain local diagnostics while reporting with their redacted sample. Core-owned logger dependencies and the brainStatusFor warning callback use canonical Logger signatures.

Reporter and report-ingress diagnostics are typed local-only. Hook failures (published pages, webhooks) are typed hooks.handler_failed or hooks.stream_failed and carry their redacted request path by default. Names may stay. A plugin HTTP route can declare problemReporting: 'local-only', which applies to that route's failures (src/api/routes/hooks.ts:79, src/api/routes/hooks.ts:87, src/plugins/api.ts:564). Batches pack entries under both the 50-entry and the 16 KiB body limit. This is intentionally daemon-only: early construction, locked boot, runners and web are outside coverage.

Every alert producer supplies an explicit reporting decision: local-only, already-reported, or problem with a code-defined category. AlertService validates it and reports created, escalated or reopened warning/critical transitions after any hold, once per condition rather than per recipient. Dynamic alert keys, rendered templates and business parameters never enter reports; a category report carries only its code-defined category. An alert accompanying an existing error emitter uses already-reported. Provider usage-window alerts and account spend-limit alerts stay local-only (src/daemon/resourceAlerts.ts, src/brain/session/accountSpend.ts), while host disk pressure is reported as the host-disk category. Source core has no plugin owner; plugin sources retain the host-stamped identity and inventory version. Unchanged, dismissed, info and clear transitions do not report. Problem categories map to five codes: alert.host_disk, alert.environment_disk, alert.gateway_unavailable, alert.worker_unavailable and alert.scheduled_job_failed (src/alerts/alertService.ts:126-130), all declared in src/licence/contract.ts:140.

update.rolled_back, update.blocked and update.needs_operator project the updater's existing validated journal at boot, on enable and each existing 60-second flush. The follow-up read is necessary: rollback writes its terminal state only after proving the restored daemon's boot. A single problem_update_marker row records the last transaction handed off, in the same PostgreSQL transaction as its immutable redacted error batch. config.problems in packages/db/src/config/problems.ts owns both tables; the transaction-bound lockSettings() keeps the singleton settings row locked through state, outbox and marker writes. Restart, successful delivery and consent Off do not erase that marker. No journal mutations or journald parsing are involved; the journal's core from/to versions and persisted rollback note supply the sample. The updater replaces the one retained journal with the next transaction, so both the journal and marker stay bounded. While Off, neither journal reads nor marker writes run. Enabling later can hand off only the latest retained failure, not a historical flood. A failed storage transaction advances neither batch nor marker. Existing outbox limits, expiry, permanent rejection and explicit Off still apply after handoff: exactly-once here means durable handoff, not guaranteed network delivery. A daemon that cannot boot cannot report until it runs again; replacing a failure journal before the next successful flush also removes its evidence.

ProblemService holds at most 200 aggregate keys and 1,000,000 counts per key. A 60-second flush freezes immutable UUID batches in elowen.problem_outbox. Pending storage is bounded to 200 rows, 4 MiB and seven days. Pruning runs one flush interval early so normal scheduling stays under the age maximum, and never evicts the one bounded request in flight. Its expiry may overshoot by at most the 15-second request deadline, an accepted limit. Drop counters change only after their transaction commits. Every restored row is strictly validated before transport; malformed, oversized or mismatched batches are discarded with a fixed local-only warning and one lost unit when their original count is unknowable. Boot replays leftovers. One request runs at a time through licence/reports.ts, using the existing 15-second deadline and Bearer credential without redirects, token renewal, locking or update coupling. Jittered retries run from 30 seconds to 30 minutes and honor bounded Retry-After. Permanent payload failures drop the batch; 401/403 pause the current credential, and 404 retries slowly. Reporter-owned metadata in settings preserves the config revision. ConfigRepository carries it opaquely during ordinary config reads and writes; only problemState/setProblemState validate it. Invalid metadata emits a fixed local-only warning and disables the reporter with storage status, without resetting other settings or fabricating successful status. The stored consent choice remains intact. Explicit off followed by confirmed on resets metadata and recovers reporting. Disable atomically clears pending rows and last-sent metadata, clears memory and aborts transport. An explicit off write clears leftovers even if already off; an off boot also purges restored leftovers without a config revision or consent event. Failed off-boot cleanup remains mandatory before any later enable can send. A generation check discards stale replies. Shutdown stops observation and aborts transport synchronously, then awaits pending configuration, the asynchronous outbox flush and owned cleanup before maintenance dependencies close. It does not wait for successful delivery. Repeated stop calls join the same settlement; bootstrap awaits this stop before pausing the privileged worker. The daemon exit guard still bounds a wedged cleanup. Expected setup and licence states and successful orphan-process cleanup use info; boot, stdio and snapshot failures retain typed reports with their redacted samples. A hard crash can lose the unfinished memory window.

ConfigRepository.update in packages/db/src/config/configStore.ts guards the problem-reporting switch. An explicit authenticated admin or explicit local-admin actor must accompany a change to problemReporting.enabled, with confirmation on enable. The local-admin actor kind exists in packages/db/src/repositories/types.ts, but no code in src/ constructs it; the config route passes kind: 'admin' in src/api/routes/config.ts, and a change without an actor is refused. Enabling without problemReportingConfirmation and expectedRevision answers 409 confirmation_required. The configuration repository locks elowen.settings with SELECT FOR UPDATE and calls the transaction-bound events.recordAdminSetting in packages/db/src/events/index.ts. That writes one nonaggregated consent record in the same transaction, including actor, timestamp and config revision. Explicit Off also clears the outbox atomically. Reporter callbacks run through sql.afterCommit; network delivery runs after the sender's selection transaction has released its settings lock. These records use the existing runtime.limits.eventRetentionDays retention. Only authenticated administrators see them in the activity read (src/api/routes/activity.ts:409). The existing CLI API door uses authenticated daemon routes, not an inferred local-admin actor.