Skip to content

Releases: ProofOfTechOrg/anchorage

@proofoftech/flowsafe@0.10.0

Choose a tag to compare

@github-actions github-actions released this 02 Aug 05:42
895959f

Minor Changes

  • cb0f861: Replace connector ID approval arrays with structured connector grants. Durable-agent approvals now bind to the exact Mastra tool call, workflow approvals bind to the exact suspension, and standing grants require explicit run scope.

    This is intentionally breaking: APPROVED_CONNECTORS_CONTEXT_KEY, BREAKWATER_APPROVED_CONNECTORS_KEY, and approvedConnectorsForLeg() are removed. Legacy arrays and approval rows without explicit scope fail closed. Migrate trusted hosts to CONNECTOR_GRANTS_CONTEXT_KEY, CONNECTOR_EXECUTION_CONTEXT_KEY, and connectorGrantsForLeg().

Patch Changes

  • f654696: Register runtime-driven durable agents with the runtime-owned Mastra instance so approved runs can resolve their agent and resume after isolate eviction on newer Mastra versions.

@proofoftech/breakwater@0.8.0

Choose a tag to compare

@github-actions github-actions released this 02 Aug 05:42
895959f

Minor Changes

  • cb0f861: Replace connector ID approval arrays with structured connector grants. Durable-agent approvals now bind to the exact Mastra tool call, workflow approvals bind to the exact suspension, and standing grants require explicit run scope.

    This is intentionally breaking: APPROVED_CONNECTORS_CONTEXT_KEY, BREAKWATER_APPROVED_CONNECTORS_KEY, and approvedConnectorsForLeg() are removed. Legacy arrays and approval rows without explicit scope fail closed. Migrate trusted hosts to CONNECTOR_GRANTS_CONTEXT_KEY, CONNECTOR_EXECUTION_CONTEXT_KEY, and connectorGrantsForLeg().

@proofoftech/flowsafe@0.9.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 06:54
3a80d88

Minor Changes

  • 3a259b8: Add first-class execution principals so automated work stops impersonating people.

    Every automated path previously fabricated a human to satisfy the one identity the platform had: the schedule tick, cron SLA maintenance, signal-provider delivery, and the suspension-reconcile bridge all minted role: 'operator'. That lost provenance and gave autonomous execution an operator's authority.

    Breakwater's Actor gains an optional kind (human | service | agent | system, absent meaning human), and both RBACMiddleware and createGuardedAgent gain allowedPrincipalKinds, defaulting to ['human']. The gate checks kind before role and does not consult the role allowlist for a non-human kind, because an automated principal carries a role only to satisfy the required field — consulting it would either admit whatever role the host projected, or force hosts to allow that role and thereby admit real humans holding it. Both the processor gate and the direct-call gate enforce it. An existing agent therefore denies every automated principal without a config change.

    Flowsafe adds ExecutionPrincipal, with purpose required on every automated kind, and persists it in agent-run state and approval resume targets. AgentMeta.allowedAutomation declares which principal kinds may enter on which entry paths; absent or empty denies all automated entry, and an optional host authorizer can only narrow it further. ApprovalActor is unchanged and still means an authenticated human at the HTTP boundary or a reviewer deciding an approval — a human approval never transfers the decider's authority into the resumed run.

    The @proofoftech/flowsafe/agent-host entry point exports its automation policy types, including AgentAutomationRule, AutomationCheck, AutomatedEntryRequest, and AutomatedEntryAuthorizer, so public catalog and host signatures never require deep imports.

    ApprovalService gains createAsPrincipal and supersedeStaleAsPrincipal for trusted platform bridges. They replace the human role gate with a kind-and-tenant check rather than widening it. There is deliberately no principal-taking decide, claim, or delegate.

    trustAutomationPrincipal() returns a branded, frozen canonical clone rather than the caller's own object. Validating a principal and handing the same reference back left the vouch time-of-check/time-of-use: the caller kept a mutable alias and could rewrite a vouched system principal into {kind:'human', role:'admin'} before the service read kind. The trusted entries now recheck the own brand, the automated shape, the kind, and that every field is a plain data property — an accessor survives Object.freeze and would reopen the same hole — instead of trusting a parameter type that does not exist at runtime. ExecutionPrincipal fields are readonly.

    AutomatedExecutionPrincipal is added for duties that want provenance but derive no authority from the principal, so the trust brand is demanded only where it is read. sweepSLA and SlaSweepMaintenanceOptions take it, and sweepSLA refuses a human or malformed principal outright: it writes across every tenant, and a human there would stamp principalKind: 'human' onto cron escalations. TRUSTED_AUTOMATION is not on the package barrel — trustAutomationPrincipal is the sanctioned constructor.

    Audit correlation now carries principalKind, principalId, purpose, and delegatedBy alongside the existing tenant, run, thread, and entry-path fields.

    x-flowsafe-actor and x-flowsafe-role are retired from the wire. The principal is now the sole identity channel: a thread Durable Object projects scope.actor from it, so a host's separate TenantContext.actor can no longer disagree with what executes. Both header constants are removed from @proofoftech/flowsafe/do-runner; the topology strips the names on send and forward, and createTenantResolver still refuses them on inbound requests so a mixed-version client fails loudly.

    queueApprovalForSuspension, reconcileApprovalsForSummary, and resumeRunWithRequeue take a systemActorId string instead of a principal, and mint their own bookkeeping identity against the service's tenant binding. Hosts no longer perform a trust assertion for the platform's own bookkeeping. ApprovalService exposes its tenantId for that.

    The principal travels to a Durable Object in a trusted x-flowsafe-principal header that createThreadTopology stamps on every send and forward. A thread DO refuses a request that carries none rather than treating the caller as a human, and createTenantResolver refuses the header on inbound requests exactly as it does the tenant, actor, and role headers.

    BREAKING for in-flight state, deliberately and without an upgrade path: AgentRunRecord is version 2 and agent-thread resume targets now store an ExecutionPrincipal. Records written by the previous release fail closed, so a suspended agent run started before this upgrade cannot resume. A version-1 record cannot be upgraded honestly — a schedule.fire run stored role: 'operator', so reading it back as a human would launder exactly the authority this change removes. Flowsafe's breakwater peer floor moves to >=0.7.0. rejectReservedAgentContext is removed from @proofoftech/flowsafe/agent-host; it was exported but never called on any path, and every real caller uses sanitizeStoredAgentContext.

    A thread Durable Object now requires the principal header on every request, so a deployment whose Worker and Durable Object resolve different @proofoftech/flowsafe versions returns 403 until both sides ship this release. Cloudflare's single-bundle model makes that skew unlikely, but there is no negotiation.

@proofoftech/breakwater@0.7.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 06:54
3a80d88

Minor Changes

  • 3a259b8: Add first-class execution principals so automated work stops impersonating people.

    Every automated path previously fabricated a human to satisfy the one identity the platform had: the schedule tick, cron SLA maintenance, signal-provider delivery, and the suspension-reconcile bridge all minted role: 'operator'. That lost provenance and gave autonomous execution an operator's authority.

    Breakwater's Actor gains an optional kind (human | service | agent | system, absent meaning human), and both RBACMiddleware and createGuardedAgent gain allowedPrincipalKinds, defaulting to ['human']. The gate checks kind before role and does not consult the role allowlist for a non-human kind, because an automated principal carries a role only to satisfy the required field — consulting it would either admit whatever role the host projected, or force hosts to allow that role and thereby admit real humans holding it. Both the processor gate and the direct-call gate enforce it. An existing agent therefore denies every automated principal without a config change.

    Flowsafe adds ExecutionPrincipal, with purpose required on every automated kind, and persists it in agent-run state and approval resume targets. AgentMeta.allowedAutomation declares which principal kinds may enter on which entry paths; absent or empty denies all automated entry, and an optional host authorizer can only narrow it further. ApprovalActor is unchanged and still means an authenticated human at the HTTP boundary or a reviewer deciding an approval — a human approval never transfers the decider's authority into the resumed run.

    The @proofoftech/flowsafe/agent-host entry point exports its automation policy types, including AgentAutomationRule, AutomationCheck, AutomatedEntryRequest, and AutomatedEntryAuthorizer, so public catalog and host signatures never require deep imports.

    ApprovalService gains createAsPrincipal and supersedeStaleAsPrincipal for trusted platform bridges. They replace the human role gate with a kind-and-tenant check rather than widening it. There is deliberately no principal-taking decide, claim, or delegate.

    trustAutomationPrincipal() returns a branded, frozen canonical clone rather than the caller's own object. Validating a principal and handing the same reference back left the vouch time-of-check/time-of-use: the caller kept a mutable alias and could rewrite a vouched system principal into {kind:'human', role:'admin'} before the service read kind. The trusted entries now recheck the own brand, the automated shape, the kind, and that every field is a plain data property — an accessor survives Object.freeze and would reopen the same hole — instead of trusting a parameter type that does not exist at runtime. ExecutionPrincipal fields are readonly.

    AutomatedExecutionPrincipal is added for duties that want provenance but derive no authority from the principal, so the trust brand is demanded only where it is read. sweepSLA and SlaSweepMaintenanceOptions take it, and sweepSLA refuses a human or malformed principal outright: it writes across every tenant, and a human there would stamp principalKind: 'human' onto cron escalations. TRUSTED_AUTOMATION is not on the package barrel — trustAutomationPrincipal is the sanctioned constructor.

    Audit correlation now carries principalKind, principalId, purpose, and delegatedBy alongside the existing tenant, run, thread, and entry-path fields.

    x-flowsafe-actor and x-flowsafe-role are retired from the wire. The principal is now the sole identity channel: a thread Durable Object projects scope.actor from it, so a host's separate TenantContext.actor can no longer disagree with what executes. Both header constants are removed from @proofoftech/flowsafe/do-runner; the topology strips the names on send and forward, and createTenantResolver still refuses them on inbound requests so a mixed-version client fails loudly.

    queueApprovalForSuspension, reconcileApprovalsForSummary, and resumeRunWithRequeue take a systemActorId string instead of a principal, and mint their own bookkeeping identity against the service's tenant binding. Hosts no longer perform a trust assertion for the platform's own bookkeeping. ApprovalService exposes its tenantId for that.

    The principal travels to a Durable Object in a trusted x-flowsafe-principal header that createThreadTopology stamps on every send and forward. A thread DO refuses a request that carries none rather than treating the caller as a human, and createTenantResolver refuses the header on inbound requests exactly as it does the tenant, actor, and role headers.

    BREAKING for in-flight state, deliberately and without an upgrade path: AgentRunRecord is version 2 and agent-thread resume targets now store an ExecutionPrincipal. Records written by the previous release fail closed, so a suspended agent run started before this upgrade cannot resume. A version-1 record cannot be upgraded honestly — a schedule.fire run stored role: 'operator', so reading it back as a human would launder exactly the authority this change removes. Flowsafe's breakwater peer floor moves to >=0.7.0. rejectReservedAgentContext is removed from @proofoftech/flowsafe/agent-host; it was exported but never called on any path, and every real caller uses sanitizeStoredAgentContext.

    A thread Durable Object now requires the principal header on every request, so a deployment whose Worker and Durable Object resolve different @proofoftech/flowsafe versions returns 403 until both sides ship this release. Cloudflare's single-bundle model makes that skew unlikely, but there is no negotiation.

@proofoftech/flowsafe@0.8.0

Choose a tag to compare

@github-actions github-actions released this 28 Jul 03:48
76d6c4e

Minor Changes

  • 09a4406: Add guarded Breakwater agents and Flowsafe's authenticated, catalog-driven agent host. Agent starts now derive trusted identity and execution context, agent resumes require an approval-bound capability, and status and NDJSON observation remain tenant-bound.

Patch Changes

  • 6670285: Prevent approval resume after isolate eviction from rerunning application input processors or input policy evaluation. Durable-agent recovery now reauthorizes the stored principal, restores both Mastra run registries with complete runtime processor chains, and fails before resumed tool execution when authorization is denied.

@proofoftech/breakwater@0.6.0

Choose a tag to compare

@github-actions github-actions released this 28 Jul 03:48
76d6c4e

Minor Changes

  • 09a4406: Add guarded Breakwater agents and Flowsafe's authenticated, catalog-driven agent host. Agent starts now derive trusted identity and execution context, agent resumes require an approval-bound capability, and status and NDJSON observation remain tenant-bound.

@proofoftech/flowsafe@0.7.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 09:39
9cc6ae2

Minor Changes

  • def3b37: Complete and document the public flowsafe surface. The root entry point now
    re-exports the approval API, Durable Object runner, artifacts, and audit export
    surfaces with parity tests. Signal-provider hosts gain a tenant-safe topology,
    stable polling alarms, and automatic post-mutation polling reconciliation.
    Publish comprehensive package, deployment, approval, durable-agent, operations,
    and API-reference documentation, plus a full advanced starter host.

@proofoftech/breakwater@0.5.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 09:39
9cc6ae2

Minor Changes

  • def3b37: Harden public connector and Agent CLI boundaries for the first public release.
    Agent CLI connectors now expose structured, redacted errors, pass workspace-edit
    permission flags to Claude Code and Codex, and keep prompts and option values out
    of diagnostics and audit events. Connector, policy-evaluator, and actor-lookup
    failures now emit static safe audit reasons. Add exhaustive export sentinels and
    a packed-tarball consumer test, move Zod to runtime dependencies, and publish
    complete package and connector guides.

@proofoftech/flowsafe@0.6.0

Choose a tag to compare

@github-actions github-actions released this 25 Jul 18:53
d3ea840

Minor Changes

  • eca3b6e: Close the durable-agent, agent-schedule, notification-dispatch, and D1 background-task execution residuals with tenant-safe thread routing and eviction-safe approval resume. Harden stored schedule context and core schedule-contract validation; make D1 notification creation preserve explicit-id coalescing, insertion-order targets, rollback-safe atomic migration, and concurrent partial updates; priority-plan summary and individual delivery across state-stable 100-id batches; accept Mastra's raw constructor pubsub with rollback-safe workers and a synchronous enqueue shutdown gate; close failed resume streams; validate public numeric configuration synchronously; preserve nested Mastra background-task SSE events; and require a proven process shutdown before spike restart.

  • d54d2be: Track D — schedules (@proofoftech/flowsafe/schedules, additive, opt-in). A new
    subpath ships the D1 schedules domain, a CAS-driven tick we own, and a tenant
    facade — all on the single DO + D1 RunnerRuntime substrate (P1), no new
    ApprovalRecord shape or existing signature changed, unconfigured hosts
    byte-identical.

    • D1SchedulesStorage + createScheduleStorageDomains — the flowsafe-owned D1
      domain over mastra_schedules / mastra_schedule_triggers (the
      @mastra/cloudflare-d1 adapter ships neither), mirroring core's
      SchedulesStorage contract incl. the CAS updateScheduleNextFire. Composed
      into createD1Storage via the injected domains seam.
    • createScheduleTick — we OWN the tick (DL-012): listDueSchedules → CAS claim
      → workflow targets mint a fresh INV-1 runId and fire through the host's
      run-start seam; agent targets are GUARDED OFF (their only public fire path,
      schedules.run(id), enqueues onto core's pubsub worker loop we do not run, so
      firing is a fail-closed audited skip — agent-target execution is deferred). An
      injectable run-cap seam (DL-007) skips a capped tenant while the schedule stays
      healthy. The P4 stored-context barrier strips reserved keys before any leg.
    • createScheduleRouter — the tenant facade (DL-013): server-minted ids,
      metadata.tenantId stamping, tenant-filtered reads, ownership 404s (no
      oracle), per-tenant count + fire-rate caps, and P4 reserved-key rejection on
      create/update (the whole breakwater. namespace + mastra:goal).
    • Storage triad (DL-003): both tables register in the schema-guard inventory
      (8 → 10) with a new metadata-filtered purgeTenant kind
      (TENANT_METADATA_PURGE_TABLES), plus purgeExpiredScheduleTriggers for the
      trigger-history TTL. createFlowsafeWorker gains an opt-in scheduleTick seam
      (its own failure-isolated cron duty) and a SCHEDULE_TRIGGER_RETENTION_DAYS
      purge duty.
  • 0f4f70a: Track E (M-007) — signal providers: a new subpath @proofoftech/flowsafe/signal-providers
    (additive, opt-in, subpath-only). Host external-event providers on a Durable
    Object with alarm-driven polling, terminate provider webhooks on the Worker, and
    persist subscriptions in a flowsafe-owned D1 table.

    • SignalProviderHost — a per-tenant provider host DO (idFromName(tenantId))
      whose alarm rehydrates subscriptions from D1 (core's registry is in-memory,
      lost on eviction) and polls each of the tenant's providers with per-provider +
      per-delivery failure isolation, delivering through Track C's thread-DO topology.
    • D1SubscriptionStoreFactory — a flowsafe-owned, tenant-columned
      flowsafe_signal_subscriptions store mirroring the approval store's INV-2
      posture (.forTenant() tenant-bound, .system().listByResource() the webhook's
      cross-tenant authority). Registered in purgeTenant (PurgeTenantResult.subscriptions);
      retention is none (standing config reaped only at offboarding).
    • createWebhookRouter — webhook ingress that verifies the provider signature
      over the RAW bytes BEFORE parsing, maps the payload to a tenant via the
      subscription ROW only (never the payload), rate-caps per provider+tenant, and
      audits every ingest with a bounded forgery audit. createSubscriptionRouter
      the human-only HTTP subscribe/unsubscribe surface (never exposed as model
      tools; mints no capability).
    • githubSignalProvider — a binding-gated GitHub reference provider
      (X-Hub-Signature-256 verified constant-time via WebCrypto). createWebhookSignalProvider
      is the generic path.

    Also adds a subscriptions counter to PurgeTenantResult (the DL-003 offboarding
    coverage for the new flowsafe-owned table).

  • 6c80e92: Track F (M-005) — goals. New subpath-only export @proofoftech/flowsafe/goals:
    createObjectiveRouter, a role-gated + audited objective HTTP surface
    (set/get/update/clear over /api/threads/:threadId/goal) that writes the
    thread-scoped goal record in Track C's mastra_thread_state domain
    (GOAL_STATE_TYPE 'goal'). The write path is a P6-lite ingestion boundary
    (auth → coarse role → thread-prefix ownership 404 → size cap →
    assertNoClientMemoryIds → field allowlist → maxRuns host cap → audit) and
    persists through @mastra/core's own writeObjective/readObjective/
    clearObjective, so a record it writes is byte-identical to what the durable
    goal step reads via resolveGoalStore (DL-018 — no thread-DO affinity needed
    for the write). A requested maxRuns above the host cap is rejected, not
    clamped (default the core DEFAULT_GOAL_MAX_RUNS, 50; DL-007). Goals never mint
    capability (P8) and Track F starts no runs — per-tenant run budgets stay
    enforced at the existing seams. GOAL_REQUEST_CONTEXT_KEY ('mastra:goal') is
    reserved with a no-collision pin against the runtime's requestContext base keys.
    Hosts mount the surface opt-in through createFlowsafeWorker's new
    buildObjectiveRouter seam; absent config is byte-identical. Additive only — no
    new table, schema-guard, purge, or TTL change (reuses the Track C thread-state
    domain), and no existing signature or ApprovalRecord shape changes.

Patch Changes

  • 8e3562f: Make approval filing atomic across terminal and open records for the same captured suspension fingerprint, preventing stale reconciliation from filing over a decision.
  • 97cb097: Harden the P6 ingestion routers against a pre-auth malformed-path fault. The
    signal, goal, and schedule routers decoded the threadId/schedule-id path
    segment with bare decodeURIComponent before authentication, and
    createFlowsafeWorker's fetch handler did not wrap the router calls — so an
    unauthenticated request with malformed percent-encoding (e.g.
    POST /api/threads/%/message) threw a URIError out of fetch() as a
    per-request 500. The three routers now use a shared safeDecodeSegment
    (host-kit) that treats malformed encoding as route-absent (byte-identical to a
    non-matching path), matching the Track E webhook router; the worker fetch
    handler gains a top-level try/catch that contains any handler throw as a
    generic 500 without leaking error.message. The same helper closes the whole
    class: the background-tasks read route (post-auth, DO-mounted) adopts it too, so
    a malformed taskId returns the no-oracle 404 instead of throwing. Additive and
    behavior-preserving for all valid paths.

@proofoftech/breakwater@0.4.0

Choose a tag to compare

@github-actions github-actions released this 25 Jul 18:53
d3ea840

Minor Changes

  • 15d4ec3: Track B (background tasks): the additive, opt-in substrate + defenses for
    Mastra background tasks on the one Durable-Object + D1 chokepoint. No existing
    signature or the ApprovalRecord shape changed; hosts stay byte-identical with
    background tasks unconfigured.

    • breakwater _background model-override defense (DL-005), the ONE breakwater
      change (MINOR).
      createConnector's wrapped execute/dryRunExecute reject
      tool-call args carrying a _background field (core LLMBackgroundOverride)
      unless the manifest opts in via permissions.background — the argv-flag-
      smuggling posture of the agent-cli buildFlags defense. background: true is
      allowed only on a read-only connector (a write-class opt-in throws at
      construction); v1 keeps write/approval-carrying connectors foreground-only.
      Plus a backgroundExecution tool-policy evaluator (deny-by-default for the
      write class) as the defense-in-depth counterpart at the gate loop. Both are
      DEFENSE-IN-DEPTH for DIRECT / NESTED calls, NOT the agent-path guard: on the
      agent path core deletes _background from the args before dispatch (schema or
      not), and core's own resolveBackgroundConfig baseEnabled gate — a breakwater
      connector sets no background config — already prevents the model from
      backgrounding an ineligible tool, so the breakwater reads see stripped args and
      fire on nothing there. The real write boundary on every path (including inside
      the background executor) is the requestContext grant.
    • mastra_background_tasks adopted into the D1 substrate in ONE change
      (DL-003).
      Registered in the schema-guard inventory (coverage tenant-range,
      a new background-task-ttl retention kind), in purgeTenant (ranged over the
      INV-1 salted run_id; new PurgeTenantResult.backgroundTasks), and given a
      storage-layer TTL cleanup purgeExpiredBackgroundTasks (+
      BACKGROUND_TASK_TTL_PURGE_TABLES) mirroring core's two-window
      BackgroundTaskManager.cleanup so a purge cron reaps terminal rows without a
      live manager. Surfaced through FlowsafeWorkerConfig.backgroundTasks as the
      purge cron's own failure-isolated duty (undefined = no duty, byte-identical).
    • @proofoftech/flowsafe/background-tasks (new subpath): backgroundTasksStore
      (the async accessor onto @mastra/cloudflare-d1's BackgroundTasksStorageD1
      the D1 domain the adapter already ships; not reimplemented, per "what NOT to
      build"), BackgroundTaskHost (hosts a BackgroundTaskManager on a DO with the
      DL-015 boot/alarm lifecycle), and createBackgroundTaskRoutes (READ-only,
      tenant-bound by construction, DL-014: list/stream REQUIRE a runId/threadId
      filter and validate its salted prefix; getTask 404s a missing OR foreign
      task with no oracle; the raw manager is never exposed).
    • Recovery seam pinned (R-002, spike B-S2): DO eviction is survived by
      re-registering the static tool executors and calling the PUBLIC async
      manager.init(pubsub) at DO boot — which fires the manager's own (private)
      recoverStaleTasks() internally. No private method is ever called.

    Known substrate limitation (spike B-S1 findings R-B1/R-B2/R-B3, documented in
    background-tasks/host.ts):
    durable background-task execution does not yet
    run on the Cloudflare substrate. Core runs task bodies on the evented
    execution engine, which refuses to createRun unless the workflows store
    reports supportsConcurrentUpdates(). @mastra/cloudflare-d1 returns false
    AND leaves updateWorkflowResults/updateWorkflowState as unimplemented throws
    ("D1 does not support atomic read-modify-write") — so R-B1 is NOT a flag to
    flip: overriding it passes core's gate then throws on the first step-update,
    stranding the task at running. The P9 fix is an adapter that implements
    atomic partial-updates (the DO's single-threaded lease makes that safe), plus
    mastra.startWorkers() to run the evented workers (R-B2 — the two close
    together). A latent tenant-isolation residual (R-B3) rides along: core keys the
    internal __background-task run by the UNSALTED taskId, so its snapshot row
    escapes tenant offboarding — inert while execution is blocked, but it MUST be
    closed in the same change that enables execution, and a CI guard
    (background-tasks/d1-storage.test.ts) fails the instant
    supportsConcurrentUpdates() returns true. Persistence, the recovery seam,
    tenant purge + TTL, the read routes, and the _background defense all work
    regardless. BackgroundTaskHost.boot() warns once so the limitation is loud,
    not a stray async throw.