close_trace() called the coroutine TelemetryManager.start_send_task without
awaiting or scheduling it, so every payload closed on that path was silently
dropped and the runtime logged "coroutine ... was never awaited". The only
other caller awaits it correctly, so both call styles stay supported: call the
manager, schedule the returned coroutine on the running loop, and close it with
a debug log when no loop is available.
Regression case added in tests/unit_tests/telemetry/test_trace.py: it fails
before this change (payload never delivered) and passes after.
Chains share the execution telemetry channel: one bounded, content-free chain
per inbound platform event (platform event -> routing -> Pipeline/Agent/plugin
processor -> platform API calls), sampled by space.execution_trace
(off | failures | sampled | all). Chains keep only code-defined identifiers,
report routing targets as type:uuid instead of user-defined rule names, and
share the 30-day retention of execution records.
Execution telemetry already reported window counters. This adds one bounded,
content-free chain per inbound platform event, so a single event can be
followed from its source platform through routing and processing to the
platform API calls it caused.
- telemetry/trace.py: ContextVar trace identity with route and run scopes, and
a 32-stage bound per chain.
- telemetry/execution.py: keeps at most 64 chains for 120s, decides sampling
from the observed outcome, and sends one payload per chain keyed by
query_id = trace_id, so Space fetches a chain by primary identity. Window
counters are unchanged and remain the source of coverage statistics.
- botmgr / pipelinemgr / orchestrator: bind the trace at the ingress boundary,
scope routing identity per dispatch, and attach the run identity, so nested
lanes (event -> route -> pipeline/runner -> platform API) reuse one chain. A
run reached without an ingress (WebUI debug, service API) owns its own chain.
- space.execution_trace selects off | failures | sampled | all (default
sampled: failures, WebUI debug runs and every N-th success).
Chains carry only code-defined identifiers (adapter and runner types, route
identity as type:uuid, run id); never message content, tool arguments or
platform user identifiers.
Carries the plugin worker process label: workers now show `--tag
author/plugin_name` in ps/htop, including the nsjail supervisor row.
Verified locally: the published wheel hash matches the pin, the shared runtime
launches workers with the tag, plugins register and knowledge retrieval is
unaffected.
scan_skill_directory is disabled for Cloud-managed sandboxes, but the
register_skill tool was still advertised to the model whenever a backend was
available, so every call failed closed without a usable explanation. Advertise
register_skill only when the runtime may scan sandbox directories, and keep
activate available in both modes.
The workspace file tools (read/write/edit/glob/grep) ran a script through
`python`, which a sandbox built from a host rootfs may not provide: those hosts
ship `python3` only, and a read-only /usr prevents creating a `python` shim
inside the jail. Every file tool then failed with `/bin/sh: 1: python: not
found`. Resolve the interpreter with `command -v python3 || command -v python`,
matching the interpreter the Box service already uses for its own scripts.
A full-text search sends no query vector by design, but search() validated
len([]) == 0 as the expected embedding dimension, so the request failed with
'Embedding dimension 0 is not enabled for this deployment' and hid the
backend capability error. An empty query vector now means no expected
dimension to verify.
The NDJSON debug stream emits its frames after the request handler returned,
so the request-scoped tenant scope is already closed by then. The deferred
execution therefore failed with TenantScopeRequiredError on its first
persistence access and the client only saw a generic runner_error frame with
no log entry.
Re-enter the Workspace tenant scope around the deferred execution and log
unexpected failures with their traceback.
Pick up the shared-worker startup and installation-binding fixes released as
langbot-plugin 0.7.5 (tag v0.7.5), so Core deployments stop installing the
0.7.4 wheel that cannot launch shared pool workers and drops the installation
binding on plugin dispatches and relays.
A Workspace installs a plugin once, but that single installation is reused by
several product objects: every processor for a Runner, every knowledge base for
a KnowledgeEngine / Parser / Retriever, and many conversations or page requests
for the other component kinds. The platform storage key carries no processor or
knowledge-base dimension, so a plugin must fold that identity into its own keys.
- certified-plugins: add "one installation, many usages" with the isolation
dimensions per component kind, and "when an installation is revoked"
documenting BasePlugin.on_installation_revoked, its trigger set, the absent
invocation context and the no-op/failure semantics.
- component pages (runner, knowledge-engine, knowledge-retriever, parser, tool,
command, event-listener, page): add a short component-specific section with
that component's own rules and a link to the contract page.
- Align the background-work rule with the SDK contract: the SDK does not track
ownership of detached work, so a plugin must keep its own binding-keyed
registry and stop it in the revocation hook.
All three locales carry the same sections.
The dev server mounts the site at basePath "/docs/" but never runs the
prefixing that the production build performs in
scripts/finalize-base-path.mjs. Any asset referenced as a literal
root-absolute URL therefore 404s locally: icons configured in docs.json
(navigation pages[].icon) and the brand logo in press.config.tsx.
Markdown images are unaffected, because the asset pipeline rewrites
those itself ("/docs/public/..." in dev, "/docs/assets/<hash>..." in the
build), which is why broken icons and working screenshots appeared on
the same page.
Add a serve-only Vite middleware that rewrites the unprefixed forms to
the "/docs/..." mount point. The rule set deliberately mirrors
finalize-base-path.mjs exactly, so a URL that breaks locally also breaks
in production rather than being masked by the dev server.
Verified: 60 images across four pages in three locales load with no
broken requests (the guide page alone went from 17 broken to 0),
tsc --noEmit passes, and npm run build still succeeds with the static
suite at 31/31 and no changes to generated tracked files.
Add .gitattributes with * text=auto eol=lf plus explicit binary markers.
The repository stores LF, but with core.autocrlf=true a Windows checkout
rewrites every text file to CRLF, which breaks assertions that depend on
byte-exact content or LF-only patterns in the test suite.
Rewrite plugin/certified-plugins in all three locales around the
questions a plugin developer actually asks, and make it the single home
for certification: what it is, why it exists, when it is needed, what
changes after approval, and what the contract requires.
- Decision framing restored, including a needs/does-not-need table and
the explicit statement that most plugins do not need certification.
- Full manifest example, accepted values for both declaration fields and
the SDK version that recognises them.
- Constraints kept and tightened: stateless rules, allowed/forbidden
table, anti-patterns with correct/incorrect snippets, Runner notes,
self-check list.
- Submission flow moved onto this page so it no longer depends on the
publishing page.
- Operator content that duplicates the Core architecture document
removed (Cloud admission matrix, OSS issuer keys, certificate fields,
internal review tiers).
publish/market is now publishing-only with a one-line pointer, and its
misplaced plugin_submission screenshot moves back under the upload
section where it belongs. dev/basic-info replaces the commented-out
manifest fields with a link to the contract page.
ja had been missing whole sections (36 lines); all three locales now
share the same section structure.
Passkeys and passwords belong to the Account, but those routes required a Workspace -- which the WebUI never sends for them -- and then resolved the caller by looking the login name up as an email address. Every Account whose login name is not its email answered 404, so the passkey panel could neither add nor list a key.
The routes now authenticate with the account token and address the Account the caller authenticated as, matching the self-service second-factor routes in the same file. /space/bind-authorize-url carried the same Workspace requirement.
The login page also surfaced a TypeError instead of the passkey login failure message when an assertion was rejected.
Cloud runtime refuses unscoped database access and workspace_operation_logs is row-level-secured on the bound Workspace. The background writer, the retention pass and the metadata reads all run outside a request lifetime, so each of them failed and the failure was swallowed by the best-effort handler: enabling tracing recorded nothing at all.
The route wrapper also traces after the handler released its scope, so level resolution has to carry the Workspace itself. Without it the level read back as 0 and the request was dropped before it was even queued.
The startup gate now enumerates the Workspaces through this instance's execution bindings, because a cross-Workspace metadata scan cannot see them under RLS, and it runs after the Workspace service exists. The writer starts with a clean context so it cannot inherit the scope of the request that first enqueued a trace, and a dropped write is reported instead of being logged only at debug.
The second-factor listing was instance-wide and the three mutating endpoints never checked that the target Account belongs to the caller's Workspace, so an owner or admin could see and reset another tenant's account. The listing now resolves the Workspace members and the mutations answer 404 for a foreign account, which does not disclose that it exists.
Also fixes the TOTP write path on Postgres: six write points passed timezone-aware datetimes into timestamp-without-time-zone columns, which asyncpg rejects, so every enroll, confirm, revoke and recovery-code consumption raised a 500 there. SQLite tolerated the same values.
The panel derives its oversight view from the API authorization instead of the length of the account list, which would have hidden it in a single-member Workspace.
Rename the traceability surface to "operation log", move its entry from the sidebar into the settings dialog, and collect the capture level and the retention policy in one dialog. The retention fields are shown inline instead of behind a disclosure; edits stay a draft until Save, and Reset restores the persisted policy. Copy updated across the eight locales.
- render the banner through the Alert API so its grid keeps one full-width text column instead of wrapping inside the empty icon column
- point the cloud link at space.langbot.app/cloud?environment=dedicated and the self-hosted link at the GitHub repository
- keep the last known state and retry the probe (1/2/4/8s plus a re-check on window focus) so a restarting backend cannot hide the banner for the session
- publish --beta-banner-height so the fixed sidebar container, the sidebar wrapper and the wizard shell start below the banner