Typed logging and reports
Typed logger ownership and local-only report infrastructure
ctx.logger binds plugin identity independently of formatted text. PluginLogger's info accepts optional extra data and
LogMetadata. warn and error require three arguments: message, extra data (use undefined when absent), and
metadata naming an optional declared code or explicitly selecting local-only. PluginLogger reuses core Logger's
warning/error signatures; injected logger types must use PluginLogger rather than weakening this rule.
Registry forwarding always stamps the owning plugin, so a plugin cannot change its ring owner. Metadata may carry one of core's declared problem codes (PROBLEM_CODES in src/licence/contract.ts) and reporting: 'local-only' | 'code-only'.
With problem reporting enabled, a plugin warning or error with an absent or undeclared code, or an invalid reporting policy, reports plugin.reporting_invalid under the plugin's name with no text sample. A core-owned warning or error with the same fault reports runtime.reporting_invalid instead (src/problems/service.ts:118). Live codes must belong to PROBLEM_CODES; restored immutable batches retain their original validated wire codes. Declared codes may carry up to three redacted samples unless they select code-only (src/problems/redact.ts removes secret-named values such as password= or apiKey:, bearer tokens, cookies, URL userinfo and query strings, e-mail addresses, IP addresses and UUID-like ids, keeps only the last segment of absolute paths, and replaces long hex and mixed-case base64 runs; it cuts the text to 500 characters, including the trailing ellipsis). Mark a line code-only when its text can carry message content or business data (chat or e-mail text, prompts, model output, order or CRM records): it is counted without text. Choose reportability where the typed cause is known: every warning/error emitter declares a precise code or explicitly selects local-only. Both core's Logger and PluginLogger require that third argument. A plugin owner alone is not a reporting decision. Provider, process and transport error prose uses code-only rather than relying on text redaction. Normal progress and successful fallback are info. A warning/error should be reported when it can drive an Elowen bug fix or refactor. Operator-only conditions such as a customer's application returning HTTP 5xx use local-only; successful administrative audit transitions use info. Unexpected failures of Elowen's own transport remain reported. Do not filter message strings in the collector or remove local diagnostics. Names of people may stay. local-only lines never leave the instance. Absent or invalid classification is a counted metadata fault with no exported text. Human logs keep the original text. Only the daemon bootstrap owns the private observer; plugins cannot register a log observer. registerInterval owns both its host timer and promises returned by ticks. Return asynchronous tick work: ticks scheduled while that promise is pending are skipped, and shutdown awaits it before
contributed services stop, regardless of registration order. A failure series warns on its first
failed tick and at most once per ten minutes with the cumulative count; the next successful tick
logs local INFO recovery and resets the series. Forwarded logger metadata stays intact while core stamps the true owner.
Capture subagentEmitter in the parent turn before spawning and handle its typed durable outcome. Accepted publishes; already-terminal is consumed silently; rejected prevents admission of an orphan background handle and distinguishes validation, ownership and persistence failure. Boolean and void progress sinks are unsupported. Background completion returns the same typed outcome after atomically persisting the result and terminal run projection. Only accepted completion publishes the stored row and releases the progress claim. Persistence rejection leaves the run recoverable; do not follow completion with a separate terminal progress write. Child unwind owns detached completion delivery.
A delegated run(source, text, onEvent) reports session after core persists the child's identity
and scope, before runner capacity can block execution. Persist the run or workflow node in that
synchronous callback. The bundled Delegate and workflow engine use this to protect queued background
children through core's single sparedChildSessionIds rule. Pool session progress carries
status: 'queued' | 'running' and optional detail; fold it through
elowen-plugin-shared/subagentProgress and retain the ordinary running lifecycle while queued.
The existing detail renders in CLI/web rails and is returned by the delegated-children listing.
Background Delegate returns a queued job receipt, with automatic result delivery unchanged.
No callback means no plugin progress row, not a second admission mechanism. No pool means ordinary
in-process execution. Stop and boot recovery own terminalization; never infer completion from a
session event or from the length of a queue wait.
PluginAlertInput requires reporting from the core AlertReporting union. The host-disk problem category maps to alert.host_disk; core maintenance uses it for warning and critical host filesystem pressure. It uses the same once-per-transition reporting as other alert categories, sends no measurements or paths, and adds no separate polling. Use local-only for operator conditions, already-reported when an existing error emitter owns the problem, or problem with a declared category for a standalone condition. The host validates this decision and reports visible transitions once per condition without dynamic keys, rendered text or business parameters.
Registry plugins preserve host metadata through injected logger callbacks. Chatbot action policy refusals and optional Sites thumbnail retries are info. Failed chatbot turns, unanswered actions, gateway synchronization and publication transport failures use distinct code-only classifications. Sites application HTTP 5xx diagnostics remain local-only, as selected from typed transport/probe outcomes. Registry brainStatusFor warnings forward the real provider owner through the same third metadata argument, including thrown, non-object, duplicate-field and invalid-statusline output. With reporting enabled, all valid typed plugin names are exported, including private or unlicensed ones; there is no local bucket.
PluginHttpRoute supports problemReporting: 'local-only'. The report receiver uses this on its exact v1/report streaming mount, with maxStreamBodyBytes: 16384. The host stamps handler/stream failures from resolved route metadata; global/request-boundary failure exclusions use the canonical report route. Unrelated routes remain collected. With no marker, normal diagnostics apply. Cost is one optional field, no extra body buffering or observer registration. The real consumer is the licensing digest receiver.
Elowen plugins are trusted ESM packages. A plugin is a directory containing an
elowen-plugin.json manifest and an ESM entry module that exports
register(ctx). The manifest is the declaration and audit surface. The
registration function is the executable source of the plugin's contributions.
A plugin contributes through a host seam. The plugin owns domain behavior, domain data presentation, and provider-specific copy. Core owns shared protocol, authentication and authorization boundaries, tenancy, persistence boundaries, process supervision, lifecycle framing, generic UI hosting, and generic fallbacks. Do not add a plugin-name condition to a shared host surface when a contribution seam can express the behavior.
A plugin may contribute tools, skills, slash commands, prompt context, hooks, HTTP routes, WebSocket routes, platform adapters, services, controls, MCP tools, configuration forms, and browser UI. Every contribution is validated at the plugin boundary and every host capability is deny-by-default unless the manifest grants it. Every seam has an explicit absent behavior: an unavailable result, an omitted optional surface, or the existing generic fallback. A disabled or missing provider must never look like a successful empty result.
The cache rule is important. Stable instructions belong in the system prompt.
Content that can change on every turn belongs in turn context, preferably with
placement: "after-user" when it qualifies the request. Never put per-turn
state in a system-prompt fragment: the system prompt is the cached prefix.
Owning code and minimal use
The owning code is src/shared/logger.ts (LogMetadata, the Logger type and the single setLogSink observer), src/plugins/api.ts (PluginLogger, PluginAlertInput, PluginHttpRoute and subagentEmitter), src/plugins/registry.ts (the scoped plugin logger that stamps the owner), src/problems/service.ts (projection of each event into a report entry), src/problems/redact.ts (sample redaction), src/licence/contract.ts (PROBLEM_CODES and PROBLEM_LIMITS) and src/alerts/alertService.ts (the AlertReporting union).
A minimal warning from a plugin declares a code from PROBLEM_CODES and a classification:
ctx.logger.warn('brain status provider failed', error, {
code: 'plugin.status_failed',
reporting: 'code-only',
});
An operator-only condition that should stay on the instance declares local-only and no code:
ctx.logger.warn('upstream answered HTTP 503', undefined, { reporting: 'local-only' });
Absent behavior: with problem reporting disabled, nothing is exported, and the human log still prints the line. When a classification is missing, the line is counted as plugin.reporting_invalid and carries no text. Samples are bounded: at most three per declared code, at most 500 characters each (src/licence/contract.ts:211). The collector keeps at most 200 distinct entries (code, level, plugin and version) and counts occurrences beyond that as dropped (src/problems/service.ts:111-112); a batch holds at most 50 entries. Redaction runs once per reported warning or error, when the daemon builds the entry (src/problems/service.ts:121-122). The real consumer of the typed logger is the Chatbot plugin in the registry, which classifies failed turns as chatbot.turn_failed with code-only reporting and refusals as info.
The PluginHttpRoute marker has one real consumer: the licensing plugin's v1/report receiver mounts with problemReporting: 'local-only' and maxStreamBodyBytes equal to PROBLEM_LIMITS.bodyBytes, which is 16384. A stream body limit applies only to an exact mount; every other hook keeps the ordinary 1 MiB buffered limit (src/plugins/api.ts:585), and the registry accepts a value up to 10 MiB (src/plugins/registry.ts:1647). Without the marker, hook handler and stream failures are reported as code-only (src/api/routes/hooks.ts:86 and :93).