Skip to content

fix(orchestrator): router owns origin-routed session completions — stop swarm-synthesis double-posts and suppressed-error leaks (#11634) - #11689

Merged
0xSolace merged 2 commits into
developfrom
sol/11634-synthesis-ownership
Jul 2, 2026
Merged

0xSolace merged 2 commits into
developfrom
sol/11634-synthesis-ownership

Conversation

@0xSolace

@0xSolace 0xSolace commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

problem

follow-up to #11578 / #11605 (which sanitized WHAT gets posted). this fixes WHO posts.

coding sub-agent sessions with chat origin metadata had two parallel completion→chat posters for the SAME session:

  • sub-agent-router.ts — origin-aware, dedupe-keyed, verify-retry-aware, suppresses respawned-session events, feeds the planner's clean user-facing reply.
  • SwarmCoordinatorService.maybeFireSwarmCompletehandleSwarmSynthesis → connector — fires on EVERY terminal event with no knowledge of the router's suppression/dedupe/retry state.

result on a discord-origin "spawn a coding agent" task: 4 messages instead of 2, including:

  1. a suppressed-error leak — the false Sub-agent state was lost (process exited without persisting). No automatic action taken. scare, posted by synthesis even though the router had already respawned the session under cap and deliberately suppresses the dead session's events.
  2. a double-post — synthesis posts the sub-agent narration AND the planner posts its own clean reply.

ownership rule

the sub-agent-router owns origin-routed sessions. maybeFireSwarmComplete now SKIPS synthesis for a session when ALL of:

  • the event is one the router actually posts — task_complete / error (ROUTER_OWNED_TERMINAL_EVENTS), not stopped (the router's shouldInject never injects stopped, so synthesis stays the sole poster for stop/cancel/no-output).
  • the session carries the router's origin routing — sessionHasRouterOrigin(meta) mirrors readOrigin's UUID gate EXACTLY (same pickUuid regex, same originRoomId ?? sourceRoomId ?? taskRoomId ?? roomId + taskRoomId ?? roomId fallthrough, reads session metadata ONLY — the same input readOrigin(session) reads). source is optional, matching readOrigin.
  • the router is actually live — isActive() (new accessor): !stopped && bound to the ACP stream. false when disabled via ACPX_SUB_AGENT_ROUTER_DISABLED, stopped, or unbound. duck-typed lookup by serviceType string so the coordinator keeps no import edge on the router module. fails safe — a missing router/accessor is treated as "not active" so synthesis keeps posting.

everything else still synthesizes so a terminal status never goes silent: stopped events, no-origin dashboard/API-spawned tasks (the gap synthesis exists to cover), a disabled/unbound router, and coordinator-generated custom-validator verdicts (dispatchCustomValidatorResult — "App verification passed." — which the router never receives; exempted via isCustomValidatorResult).

concurrency correctness

AcpService fans session events out to listeners synchronously without awaiting them, so two terminal events for one session (a router-owned task_complete/error racing a stopped, or duplicate terminals on one exit) would race the ownership/dedupe decision across the getEnrichmentMetadata await — double-posting or swallowing the stopped the router never posts. fixed by serializing terminal-event synthesis per session (terminalCompletionChains): each event chains onto the session's previous terminal handler, so the second observes the first's completed decision. the chain entry is pruned when it is the tail (no unbounded growth) and cleared on reset.

test coverage

swarm-coordinator-service.test.ts (+ sessionHasRouterOrigin unit block):

  • router-origin task_complete with active router → synthesis silent
  • router-origin error/state-lost (source optional) → synthesis silent (suppressed-error leak fixed)
  • no-origin dashboard session → synthesis posts (gap preserved)
  • router-origin stopped even with active router → synthesis posts (router never injects stopped)
  • router disabled/unbound → synthesis posts (fail-open)
  • no router service registered → synthesis posts (fail-safe)
  • room UUIDs only in event payload, not session metadata → synthesis posts (mirrors readOrigin's session-metadata-only input; no silent drop)
  • session reuse: router-owned task_complete does NOT consume the slot; a later stopped on the same session still fires
  • race: two terminal events for a non-router session → fires exactly ONCE
  • race: stopped racing a router-owned terminal on the same session → still fires (no swallow)
  • sessionHasRouterOrigin both ways (valid UUID roomId+taskRoomId, taskRoomId-from-roomId fallback, source-optional, non-UUID-originRoomId fallthrough, missing-taskRoomId false, non-UUID false, empty false)

sub-agent-router-roundtrip-getters.test.ts:

  • isActive() true once bound, false after stop(), false when disabled via ACPX_SUB_AGENT_ROUTER_DISABLED

verification

cd plugins/plugin-agent-orchestrator && npx vitest run __tests__

  • baseline (clean develop): 15 failed / 1246 passed — all pre-existing env failures (smithers-orchestrator module resolution, drizzle-orm, @noble/curves, i18n codegen; file-level module-resolution FAILs on spawn-*/stop-agent/task-history).
  • with this change: 15 failed / 1283+ passed — same 15 pre-existing failures, ZERO new failures, +net new passing tests.
  • touched suites green in isolation: swarm-coordinator-service.test.ts 41/41, sub-agent-router-roundtrip-getters.test.ts + sub-agent-router.test.ts 94/94.

closes #11634

— [sol-relay] — [sol-orch]

)

Coding sub-agent sessions with chat origin metadata had TWO parallel
completion->chat posters: the sub-agent-router (origin-aware, dedupe-keyed,
respawn/retry-suppressing) and SwarmCoordinatorService.maybeFireSwarmComplete
(fires on every terminal event with no knowledge of the router's state). This
double-posted completions AND leaked state-lost errors the router deliberately
suppresses while it respawns a session under cap (the false 'Sub-agent state
was lost... No automatic action taken' scare).

Ownership rule: the router owns origin-routed sessions. maybeFireSwarmComplete
now SKIPS synthesis for a session when ALL of:
  - the event is one the router actually posts (task_complete / error; NOT
    stopped, which the router never injects)
  - the session carries the router's origin routing (sessionHasRouterOrigin
    mirrors readOrigin's UUID gate EXACTLY, reading session metadata only)
  - the router is actually live (isActive(): bound to the ACP stream, not
    disabled/stopped)
Everything else still synthesizes so a terminal status never goes silent:
stopped events, no-origin dashboard/API tasks, and a disabled/unbound router.

- swarm-coordinator-service.ts: sessionHasRouterOrigin predicate + isRouterActive
  duck-typed lookup + the ownership skip in maybeFireSwarmComplete.
- sub-agent-router.ts: isActive() accessor (!stopped && bound to ACP stream).
- tests: origin-routed completion/error -> synthesis silent; no-origin session,
  stopped event, payload-only UUIDs, disabled/missing router -> synthesis still
  posts; sessionHasRouterOrigin both ways; router isActive() lifecycle.

Co-authored-by: wakesync <shadow@shad0w.xyz>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 99bb63c9-a508-47d0-aaee-fd6c7ed899b5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sol/11634-synthesis-ownership

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@0xSolace
0xSolace merged commit fd2e343 into develop Jul 2, 2026
30 of 33 checks passed
@0xSolace
0xSolace deleted the sol/11634-synthesis-ownership branch July 2, 2026 23:47
@lalalune

lalalune commented Jul 2, 2026

Copy link
Copy Markdown
Member

Reviewed and verified after pushing formatting cleanup commit fd04ae430be9b39d2cc6e8c13b2910138a6df431.

I compared this against the duplicate #11687 and #11634. #11689 is the stronger fix: it implements the router-owned-session skip, keeps stopped/custom-validator completions in synthesis ownership so they do not go silent, and serializes same-session terminal synthesis so duplicate/racing terminal events cannot double-post or consume the wrong dedupe slot.

Local verification on head fd04ae430be9b39d2cc6e8c13b2910138a6df431:

  • bun run --cwd plugins/plugin-agent-orchestrator test -- __tests__/unit/swarm-coordinator-service.test.ts __tests__/unit/sub-agent-router-roundtrip-getters.test.ts __tests__/unit/sub-agent-router.test.ts passed: 3 files / 135 tests.
  • tsgo --noEmit -p plugins/plugin-agent-orchestrator/tsconfig.json passed after linking worktree package deps.
  • Biome check passed for changed orchestrator service/test files.
  • git diff --check origin/develop...HEAD and git diff --check passed.

No blocking issues found. This supersedes #11687.

lalalune added a commit that referenced this pull request Jul 3, 2026
…topped (#11711) (#11720)

The verify-retry / state-lost-respawn / account-failover paths re-dispatch a
fresh successor session and tear down the old one. The old session's teardown
`stopped` is plumbing, not a user-facing terminal — but swarm-synthesis (which
#11689 keeps as the poster for `stopped`, since the router never injects it)
can't tell a handoff teardown from a real user cancel, so it fired one full
synthesis post per lineage generation: original + retry1 + retry2 = 3
completions for 1 task.

The router now stamps `handedOffToSuccessorSessionId` on the old session before
teardown (a non-throwing best-effort helper, so a transport without
updateSessionMetadata never fails the handoff), and runSwarmComplete skips a
terminal that carries the marker without claiming the dedupe slot — the
successor session posts the real completion. A genuine user stop carries no
marker and still synthesizes (the #11689 invariant), covered by a regression
test in both directions.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
0xSolace added a commit that referenced this pull request Jul 3, 2026
… enrichment cache (#11711 residual)

#11720 landed the marker plumbing for #11711: the sub-agent-router stamps
`handedOffToSuccessorSessionId` on the OLD session before teardown, and
runSwarmComplete skips synthesis for a `stopped` whose (cached) metadata
carries it. One residual race survives on develop.

The coordinator reads the handoff marker from the cached enrichment
snapshot. On the verify-retry path the OLD session's earlier same-session
`task_complete` (the one that triggered the retry) warms that cache from
the store BEFORE the router stamps the marker. So the following `stopped`
reads the stale pre-stamp snapshot, misses the marker, and synthesizes a
spurious completion post — the teardown-stop is mistaken for a user stop.

Fix: for `stopped` only, when the cached snapshot lacks the marker, do ONE
`acp.getSession()` re-read via `getFreshSessionMetadata` (which also
refreshes the cache) before concluding a genuine stop. Fail-open: an
unreadable/missing session yields {}, so an unknown session is treated as
not-superseded and still synthesizes — the #11689 genuine-user-stop
invariant is never silenced.

Scope: builds on #11720 (marker constant, router stamping, basic skip all
kept from develop). This carries ONLY the cache-staleness residual:
- coordinator: fresh-re-read guard for `stopped` + `getFreshSessionMetadata`
- tests: cache-staleness regression (task_complete warms cache pre-stamp →
  setSession stamps marker → stopped → NO synthesis) + fail-open-on-miss

Tests: swarm-coordinator-service.test.ts 45 pass (+2 new). Full plugin
suite 1295 pass / 15 fail (pre-existing module-resolution baseline, zero
new). Biome clean on touched files.

Co-authored-by: wakesync <shadow@shad0w.xyz>
0xSolace added a commit that referenced this pull request Jul 3, 2026
… enrichment cache (#11711 residual) (#11721)

Reviewed by [sol-orch]: full diff read. Surgical residual on top of #11720: stopped-only + marker-absent-only single store re-read via getFreshSessionMetadata (refreshes cache), fail-open on miss/error so a genuine user stop is never silenced (#11689 invariant preserved, covered by fail-open test). Regression test reproduces the exact live cache-warming race (task_complete pre-stamp -> setSession stamps -> stopped skipped). Independently re-ran swarm-coordinator-service tests: 45/45.

Co-authored-by: wakesync <shadow@shad0w.xyz>
lalalune pushed a commit that referenced this pull request Jul 3, 2026
…op swarm-synthesis double-posts and suppressed-error leaks (#11634) (#11689)

Reviewed by [sol-orch]: full diff read; ownership predicate mirrors readOrigin exactly (incl. non-short-circuit UUID fallthrough + optional source); stopped/custom-validator/no-router paths verified to still synthesize; per-session terminal serialization closes the double-synthesis and stopped-swallow races. Locally re-ran touched suites (46/46) + full plugin suite (1291 pass / 15 fail = exact pre-existing env baseline, zero new).

Co-authored-by: wakesync <shadow@shad0w.xyz>
lalalune added a commit that referenced this pull request Jul 3, 2026
…and accumulated worktree residuals (#11731)

* feat(security): PII pseudonymization engine — realistic reversible surrogates (#10469 / #7007)

Adds the core, dependency-free half of an NER-driven PII swap layer for the
model-call boundary: a PseudonymSession that swaps named-entity PII (person /
org / location / address) for *realistic* cached surrogates the LLM can reason
over, then reverses the mapping exactly on the way back out — so the provider
sees a fluent prompt with zero real PII, while the user and the executed tool
call get the real values.

- pii-pseudonymizer.ts — PseudonymSession: per-session salted, deterministic,
  bijective, collision-checked surrogate vault. Single-pass boundary-aware
  substitution (values ∪ surrogates, longest-first) is idempotent and never
  corrupts benign substrings ("John" ≠ "Johnson"); restore is the exact inverse.
  The one hard rule on a surrogate is surrogate != original. Framework/brand
  blocklist (elizaOS, Eliza, provider names) never swapped.
- entity-recognizer.ts — async PiiEntityRecognizer interface + EntitySpan (the
  seam the optional local NER model plugs into), a dependency-free
  RegexEntityRecognizer (street addresses; opt-in email/phone), a
  GazetteerEntityRecognizer, and a CompositeEntityRecognizer (overlap + blocklist
  + label→kind canonicalization for distilbert CoNLL labels). Swaps by value, so
  it is robust to transformers.js null char-offsets (#359).
- Tests (21): unit + a 3000-iteration seeded fuzz asserting round-trip identity,
  no-leak (surrogate-masked), bijection, idempotency, determinism, and
  boundary-safety. The fuzz caught two real bugs before commit: a real name
  leaking as a token inside a surrogate reference, and a non-idempotent
  double-substitute (which the runtime's pre/post-hook double pass relies on).

Engine only; runtime wiring at useModel and the transformers.js distilbert-NER
recognizer land in follow-up commits on this branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(security): PII swap settings + recognizer-service injection seam (#10469)

- PII_SWAP_ENABLED / EXEMPT_VALUES / DISABLED_KINDS settings + parsePiiSwapList.
- PII_ENTITY_RECOGNIZER_SERVICE + PiiEntityRecognizerService: the seam a plugin
  registers so the runtime can source a local NER model without core taking an
  ONNX dependency; regex-only fallback when absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(accounts): real-path multi-account rotation E2E + loud single-account fallback (#10696, #9960)

The multi-account crown jewel (multiple Claude + multiple Codex accounts) had a
real-path bridge test but no deterministic CI proof that "more than one of each"
truly rotates, fails over, and materializes distinct per-account credentials with
no cross-account bleed. And the orchestrator's per-spawn single-account fallback
degraded invisibly when a pool was connected-but-unhealthy.

- packages/app-core/src/services/multi-account-rotation.test.ts: new real-path
  E2E driving the REAL AccountPool + coding-account-bridge over an on-disk store
  (not in-memory stubs). Proves, secret-free in CI: two accounts per subscription
  tier surface in list + describe; round-robin alternates across both; priority
  reorder changes selection; rate-limiting the active account hands off to the
  sibling with no dropped request; each Codex account materializes its OWN
  CODEX_HOME/auth.json and each Claude account injects its OWN
  CLAUDE_CODE_OAUTH_TOKEN (zero bleed); pool metadata round-trips through
  _pool-metadata.json across a reset; disabled/re-enabled accounts leave/rejoin
  rotation.
- coding-account-selection.ts: add diagnoseCodingAccountFallback() — surfaces
  ONLY the degraded case (multi-account agent type, accounts connected, none
  healthy) so the spawn no longer degrades to single-account invisibly (#9960).
  Benign empty/single-account hosts stay quiet.
- acp-service.ts: warn loudly at the spawn site when the pool degraded, without
  hard-failing the spawn (a degraded pool must still run).
- live-multi-account-e2e.ts: assert assessCodingAccountReadiness over the real
  describe() after seeding — a thin/unhealthy live pool fails loud instead of
  silently degrading.
- tests for the new diagnostic (connected-but-unhealthy warns; empty/healthy/
  non-multi-account stay silent; never throws).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(core): wire PII pseudonymization into useModel ingress + action egress (#10469 / #7007)

Turn-scoped PseudonymSession, symmetric with the secret-swap layer:

- Ingress (runtime.ts useModel): behind ELIZA_PII_SWAP_ENABLED (default off →
  zero behavior change), one awaited recognizer pass learns every named entity
  in the assembled prompt (params + system prompt), then substitutes
  synchronously — so the provider, trajectory, and logs only ever see realistic
  surrogates. Runs after the secret pass (the NER model reads opaque secret
  placeholders, never a raw key) and re-applies (sync, idempotent) after the
  pre_model hook. Recognizer = built-in regex (street addresses) composed with a
  local NER model when a plugin registers PII_ENTITY_RECOGNIZER_SERVICE; regex-
  only otherwise. Agent's own name + brand blocklist are never swapped.
- Egress (execute-planned-tool-call.ts): restores real names/orgs/addresses into
  handler args — including the REPLY action's text — so the connector call runs
  with the real recipient and the user sees their real contacts. Best-effort (a
  model-invented name passes through), unlike the fail-loud secret restore.
- trajectory-context: carries the turn-scoped piiSwapSession like secretSwapSession.

Tests (8): ingress proves the provider receives surrogates + no real PII (via
injected NER service, a pre-seeded session, and the regex address path) and the
response keeps surrogates; egress proves the handler runs with real values
restored and a model-invented name passes through; both prove the disabled no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(accounts): #10696 domain-artifact evidence — real on-disk 2-per-tier proof

Captured from a real-path run (gen-multi-account-artifacts.mjs → the same code as
the committed multi-account-rotation.test.ts): the on-disk auth/ tree, distinct
per-account credential records, _pool-metadata.json overlay, round-robin trace,
distinct per-account CODEX_HOME/auth.json (no bleed), rate-limit failover, and the
readiness gate reporting ready (2 healthy each) then flagging the degraded pool
after a 429. Nothing mocked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(core): live-Cerebras PII-swap trajectory + evidence (#10469 / #7007)

A *.real.test.ts (excluded from PR CI, gated on CEREBRAS_API_KEY) that drives the
full PII swap against a live gpt-oss-120b: asserts the provider received only
surrogates (no real person/org/address), the live model reasoned over them, and
the execution boundary restored the real values into the SEND_EMAIL handler.
Captured trajectory (provider saw "Marco Hoffman at Ridgeline Holdings / 5591
Cypress Court"; handler received "Dana Whitfield / Acme Robotics") under
.github/issue-evidence/10469-pii-ner/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(plugin-pii-guard): local distilbert-NER recognizer for the PII swap layer (#10469 / #7007)

Supplies the local NER model to @elizaos/core's PII pseudonymization layer via the
PII_ENTITY_RECOGNIZER_SERVICE seam, so core never hard-depends on an ONNX runtime.

- NerEntityRecognizer: lazy-loads dslim/distilbert-NER (Apache-2.0, fp32 ONNX)
  through @huggingface/transformers v3 on onnxruntime-node (native CPU). Covers
  person/org/location; email/phone/address stay with core's regex recognizer.
- transformers.js v3 returns raw per-token BIO for BERT (aggregation_strategy is a
  no-op) with null offsets (#359), so we stitch BIO runs + reassemble WordPieces
  ourselves and re-locate each entity's exact substring in the source
  (relocateEntities) — the value the pseudonymizer swaps is always real source text.
  Long input is chunked into overlapping ≤512-token windows and re-based.
- PiiGuardService loads the model in the background at boot (never blocks); on load
  failure getRecognizer() returns null and the layer degrades to regex-only.
- 28 offline unit tests (injected fake pipeline — no download) + a *.real.test.ts
  that downloads + runs the real model (skips gracefully offline). The real model
  was verified live: person "Dana Whitfield" 0.94, org "Northwind Labs" 0.94,
  location "Fairhaven" 0.97 on a sample sentence.

Note: requires @huggingface/transformers in bun.lock (added separately; the shared
checkout's lockfile is concurrently contended).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(deps): add @huggingface/transformers for plugin-pii-guard (#10769)

Adds the @huggingface/transformers + onnxruntime-node chain that
@elizaos/plugin-pii-guard depends on to run the local distilbert-NER model.
Lockfile regenerated in a clean worktree off develop so the delta is only the
transformers/onnx chain + the plugin-pii-guard workspace entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* wip(core,ui,accounts): PII skip TEXT_EMBEDDING_BATCH + in-flight multi-account/PII refinements

Preserve in-progress feature work: add ModelType.TEXT_EMBEDDING_BATCH to the
PII_SWAP_SKIP_MODELS list (a per-turn-random surrogate destabilizes batch
embeddings exactly like TEXT_EMBEDDING), plus the multi-account chat/API/UI and
electrobun refinements on this branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* wip(accounts,security,evidence): push working tree as-is — multi-account connect + PII boundary + QA evidence

Snapshot of the in-flight feature branch state on request:
- accounts: connect-account action + AccountConnectBlock tests, multi-account UX.
- security/PII: entity-recognizer + pii-pseudonymizer refinements (+ fuzz/tests),
  useModel PII-swap wiring, model-boundary-privacy docs, plugin-pii-guard
  registry entry.
- planner: multistep-advance regression test.
- evidence: issue-evidence bundles for #10696 (multi-account), #10699/#10700/
  #10726 (voice), captured this session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(orchestrator): gate the PUBLIC/no-token clone path against git-remote command injection (#10980 follow-up)

PR #10980 gated only the credentialed clone override (installCredentialSafeClone)
with assertSafeGitRemote. The unauthenticated clone path in git-workspace-service
— reached whenever no credential is present (public repo / no token) — was NOT
gated. That path (tryUnauthenticatedClone) runs `git clone --branch <b> <url> .`
through a shell (promisify(exec)), interpolating a repo URL that provisionWorkspace
derived from normalizeRepositoryInput(options.repo), which returns unknown inputs
UNCHANGED. Named RCE via `ext::sh -c "…"` (and file:// disclosure, `-`-argument
injection, `$(…)` command substitution) was reachable.

Fix: hard-gate at the single application chokepoint — provisionWorkspace() now
calls assertSafeGitRemote(normalizeRepositoryInput(options.repo)) before the repo
string reaches resolveDefaultBranch (git ls-remote) OR the dependency's provision()
(both clone paths). Also gate the credentialed override for defense-in-depth.

assertSafeGitRemote (added to repo-input.ts): allowlist of https/http/ssh URLs and
scp-style ssh remotes; rejects transport helpers (ext::/fd::), leading `-`,
whitespace, shell metacharacters (`$ ; | & < > ( ) ' " \` and backtick), file:/git:
schemes, and empty/bare tokens. Rejecting `$`/parens closes a residual: URL
normalization percent-encodes most shell chars but a literal `$(…)` survives.

Tests: new __tests__/unit/git-remote-safety.test.ts — unit allowlist coverage plus
an integration suite driving the REAL CodingWorkspaceService.provisionWorkspace with
a stubbed dependency provision() that fails if ever reached, proving the malicious
public/no-token remote is rejected before the shell clone runs. 12/12 pass; full
plugin unit suite 1033/1033 pass; typecheck + biome clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(evidence): commit iOS real-device evidence for #10726 voice de-larp

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(plugin-local-inference): survive hub re-publish under same filename — stale-partial discard + bounded sha re-fetch

The HF hub re-publishes bundle files under stable names (real incident:
bundles/2b/text/eliza-1-2b-128k.gguf went qwen35 1211MB -> gemma4 4737MB).
Before this fix the downloader resumed any .part unconditionally, so a
stale partial from the old content got gemma4 bytes Range-appended onto
qwen35 bytes; the corrupt blob only died at the final sha256 gate with an
unrecoverable 'SHA256 mismatch' job failure after gigabytes of transfer.

- .part files now carry a sha256 sidecar (<staging>.part.expected)
  recording the content hash they were started against; a resume is only
  allowed when it matches the current manifest sha. Stale/unknown
  partials are discarded and fetched fresh from byte 0.
- A completed transfer that fails the sha gate is deleted and re-fetched
  from scratch once (SHA_MISMATCH_MAX_ATTEMPTS=2) before failing, so a
  stale CDN edge or mid-flight re-publish self-heals; the wrong bytes
  never survive under the final name.
- Stale completed files on disk (wrong sha vs fresh manifest) were
  already discarded; now logged via [Downloader] warn.

New real-path tests (real Downloader, real fs/hash/registry; Range-aware
fetch fixture that behaves like the HF CDN): stale completed file
re-pull, stale-partial discard (old-sha sidecar + no-sidecar), genuine
resume preserved, bounded re-fetch success, persistent-mismatch failure
leaves no wrong-content file or staging residue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(plugin-local-inference): count reclaimable pages as available on macOS via vm_stat

os.freemem() on macOS maps to Mach 'Pages free' only, which the kernel keeps
near zero by design; that tripped the arbiter's critical-pressure gate and
refused every non-text capability on healthy machines. Count
free+inactive+speculative+purgeable pages instead, mirroring Linux MemAvailable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(registry): resolve biome from the repo root, not the calling file

Under isolated installs @elizaos/registry doesn't declare @biomejs/biome, so
createRequire(import.meta.url) walked up past the repo into a parent
workspace's stale 2.4.16 hoist, which rejects biome.json's 2.5.1 schema and
failed the registry build (and with it repo-wide lint).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(cloud-shared): biome-fix import ordering left by merged money PRs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(plugin-scheduling): biome format fix in runner.test.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* evidence(local-inference): vision E2E proven via fused IMAGE_DESCRIPTION + kokoro Metal perf root-cause

- vision: real gemma4-2b + mmproj-2b through the fused libelizainference arbiter path
  (ModelType.IMAGE_DESCRIPTION) correctly describes the test image (red circle, blue
  square, doubled "HELLO") in 9.6s — the "1536!=2048 mismatch" was a stale qwen35 local
  file, not a bug; current HF gemma4 (embd 1536) matches mmproj-2b (proj 1536).
- kokoro: profiled RTF~64x root cause — iSTFTNet generator forward = 261s (87% of 302s
  synth), STFT itself only 66ms; conv stack is CPU-bound, needs Metal dispatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(evidence): gemma4 cutover final arch matrix + eliza-1 hub repair record

Live-verified matrix of every text weight on HF elizaos/eliza-1 (gemma4
128k shipping per tier; qwen35 context variants kept — referenced by the
ollama Modelfile, eagle3 smoke fallback, dflash target-meta provenance,
and the HF release audit script). Records the hub repair: both live
bundle manifests were schema-invalid for the current downloader
(files.vision object-vs-array, 4b conflicting qwen35 mtp sha for the
128k text path, 5 manifest-pinned files missing from the bundle trees)
plus globally stale SHA256SUMS. HF commits 1a7c0c7b / c6d9d5cb / 27ca1338.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(local-inference): bump llama.cpp fork — gemma4-assistant HF->GGUF converter

Pulls elizaOS/llama.cpp feat/gemma4-assistant-mtp-converter (ae0a1eed2):
convert_hf_to_gguf.py Gemma4AssistantModel + gguf-py MODEL_ARCH.GEMMA4_ASSISTANT
/ NEXTN_PROJ_PRE/POST so google/gemma-4-E{2,4}B-it-assistant MTP drafter heads
convert to the fork's gemma4-assistant runtime arch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(local-inference): enable gemma4-assistant MTP drafter for eliza-1-2b

The matched Gemma-4 E2B assistant MTP head (google/gemma-4-E2B-it-assistant)
now converts via the fork's new Gemma4AssistantModel converter and is hosted
at elizaos/eliza-1 bundles/2b/mtp/drafter-2b.gguf (sha256 0495d34e08d0…,
HF commit 0ad707bf; manifest files.mtp + lineage.drafter + evals.mtp
populated, SHA256SUMS updated). Flip ELIZA_1_HOSTED_MTP_TIER_IDS to
["eliza-1-2b"] so the catalog advertises components.mtp + runtime.mtp
(draft-mtp, draftMax=1) and the load-args resolver wires the bundled drafter.

Verified live on Apple M4 Max Metal against text/eliza-1-2b-128k.gguf
(sha-identical to the hosted artifact): drafter loads with no tensor/KV
errors via ctx_other shared-KV, and drafts get ACCEPTED during speculative
decode — 21/25 (0.84) acceptance at --spec-draft-n-max 1, ~1.53x greedy
speedup (167 vs 96 tok/s on the counting probe). Evidence:
.github/issue-evidence/gemma4-assistant-mtp/ (convert log, drafter GGUF
metadata, per-step accepted-draft server log, timings JSON, bench notes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(local-inference): fused product-path proof for the gemma4-assistant MTP drafter

Rebuilt build-desktop-metal libelizainference.dylib (the staged one predated
the streaming ABI) and drove the REAL desktop text path — bun:ffi
loadElizaInferenceFfi -> eliza_inference_llm_stream_open with
mtp_drafter_path -> the fused separate-drafter DRAFT_MTP engine
(LLAMA_CONTEXT_TYPE_MTP ctx_other shared-KV) — against the eliza-1-2b
gemma4 target with the hosted bundles/2b/mtp/drafter-2b.gguf drafter.

Drafter loads with no tensor/KV mismatch and drafts get ACCEPTED:
32/38 = 0.842 acceptance at the shipped draftMax=1 window (matches the
llama-server 0.84 measurement), ~1.2x wall speedup, coherent greedy
output. One deterministic near-tie argmax flip on the counting probe
(batched-verify Metal numerics, continuation matches baseline exactly —
rollback correct) is documented in fused-ffi-bench.txt.

Evidence: fused-ffi-harness.ts (runnable), fused-ffi-run.txt (full loader
+ per-step drafted/accepted log), fused-ffi-bench.txt (summary + honest
flags).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(kokoro): Accelerate-BLAS hot loops in the fork — synth 302s -> 0.72s (RTF 64x -> 0.15) + before/after evidence (#9033)

Bump llama.cpp submodule to 114eee08e (fork branch perf/kokoro-accelerate-blas):
kokoro-layers.h routes conv1d / conv_transpose1d / linear / LSTM-gate hot loops
through Accelerate sgemm/sgemv on __APPLE__ (scalar fallback kept elsewhere).

Root cause CORRECTED vs the baseline hypothesis: no ggml op was falling back
from Metal — the iSTFTNet generator never enters ggml at all; it is a
single-threaded scalar port, so the 261s (87% of synth) was pure scalar CPU.

Evidence (same phrase/model/voice, M4 Max): generator 261,220 -> 595 ms;
predictor 24,188 -> 113 ms; synth total 301,697 -> 723 ms. Audio unchanged:
identical 112,800 samples, corr 0.99959, identical whisper transcript.
Also force-adds the gitignored before/after-profile.log evidence files the
README references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): restore the persisted mobile on-device agent across cold launches

canRestoreActiveServer's remote-host trust gate only accepts http/https
hosts, so the persisted iOS/Android on-device agent record
(kind: remote, apiBase: eliza-local-agent://ipc) was dropped on every cold
launch — clearing the saved server AND eliza:first-run-complete, bouncing
the user back into onboarding and never starting the on-device engine.
Found live on the iOS 18.1 simulator while validating local inference:
every boot deleted first-run-complete/active-server from localStorage and
the full-Bun engine never received ElizaBunRuntime.start.

The IPC identity is a native Capacitor transport, not a network dial, and
reconcileMobileRestoredActiveServer has already validated the persisted
runtime mode before the gate runs — so restore it explicitly before the
http/https host check. Adds a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): apply the restored mobile on-device agent instead of dropping it in the remote SECURITY backstop

applyRestoredConnection routed the mobile-local IPC record
(eliza-local-agent://ipc) into the untrusted-remote backstop, clearing the
persisted active server on every cold launch even after canRestoreActiveServer
was taught to keep it. Add the explicit mobile-local branch: set the client
base at the IPC identity (native Capacitor transport, no socket, no token)
and let the full-Bun engine start lazily on the first /api request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent,core): un-break the on-device mobile agent bundle (empty feature-action barrels)

The iOS full-Bun engine died at launch in ~520ms with 'Bun exited before
ios-bridge readiness with code 1'. Root cause (reproduced on host with
'bun agent-bundle.js ios-bridge --stdio'): the bundle threw
'ReferenceError: declareSubAgentCredentialScopeAction is not defined' at
import time — @elizaos/core declares "sideEffects": false, and Bun.build's
tree-shaker (1.3.14 and 1.4.0-canary, only when the mobile build's
onResolve plugin pipeline is active) drops re-export-only action barrels
(features/{sub-agent-credentials,payments,secrets}/actions/index.ts) while
plugin.ts still references their bindings. The emitted modules came out as
'var init_actions7=()=>{}' with the action definitions absent from the
bundle entirely.

- packages/core/package.json: sideEffects false -> true. Core's barrels
  are not side-effect-free (logger bootstrap, registrations), and the
  false claim let the mobile bundle ship broken. Correctness over the
  marginal web tree-shaking win.
- build-mobile-bundle.mjs: resolve SUBPATH imports of identity-pinned
  packages (e.g. @elizaos/core/node, @elizaos/core/connectors/*) from the
  same src tree as the bare name, so the flat dist/node bundle can never
  enter the graph as a second core identity.

Verified: rebuild carries DECLARE_SUB_AGENT_CREDENTIAL_SCOPE / PAYMENT /
ASK_FOR_SECRET / CONFIGURE_SECRET and boots past import on host Bun.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ui): boot observability — log coordinator phase transitions + throttled backend-probe failures

A native WebView boot wedged on the 'Booting up…' splash was undiagnosable
from simctl/logcat console output: the coordinator's phase machine and the
backend poll's silent retry loop emit nothing at info level. Log each phase
transition once and the first probe failure per 15s window (base + message)
so a stuck boot names its phase and its actual error in the device console.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): bound the iOS full-Bun wrapper import so a hung dynamic import can't wedge boot

Observed live on the iOS 18.1 simulator: during restoring-session the
dynamic import('@elizaos/capacitor-bun-runtime') never settled in the
WKWebView. getFullBunRuntime caches its init promise, so every agent
request awaited the unsettled import forever — ElizaBunRuntime.start was
never invoked, the backend poll never even failed, and the app sat on the
'Booting up…' splash indefinitely (the QA smoke lane worked only because
it primes the runtime before the shell mounts). Race the import against a
3s timeout and recover through registerPlugin('ElizaBunRuntime'), which
the surrounding code already documents as the fallback for the import's
other failure shapes. Also log poll start + step markers around the
full-Bun bring-up so the next wedge names itself in the device console.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): resolve the iOS full-Bun plugin from Capacitor's runtime registry before any dynamic import

The wrapper chunk fetch for @elizaos/capacitor-bun-runtime was observed to
never settle in the WKWebView during normal startup on the iOS simulator
(and the timeout race cannot fire in that state either — DOM timers are
starved alongside module loads). The native plugin proxy is already in
window.Capacitor.Plugins.ElizaBunRuntime — the same object the native
AgentWatchdog probes — so prefer it outright; the wrapper module is nothing
but registerPlugin('ElizaBunRuntime', …).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): checkpoint role and relay hardening

* fix(plugin-cli-inference): declare lazy SDK dependencies

* chore: checkpoint formatting and module declarations

* fix(cloud,calendar): restore e2e coverage and grant-scoped calendar pruning

* perf(kokoro): non-Apple threaded+NEON fallback for the vocoder hot loops (submodule 2bdcef890) + evidence

Bump plugins/plugin-local-inference/native/llama.cpp to 2bdcef890
(kokoro-portable-fast): KOKORO_USE_PORTABLE_FAST path in kokoro-layers.h
— internal std::thread pool over output channels/gate rows + aarch64
NEON vfmaq_f32 AXPY/dot innermost MACs for conv1d/convtranspose1d/
linear/lstm gates; pure scalar loops retained as reference/fallback
(KOKORO_FORCE_SCALAR). 23/23 parity checks vs scalar (max |delta|
1.9e-5 < 1e-4) run natively on M4 Max arm64; NDK aarch64/x86_64
Android cross-compile clean; Apple Accelerate path rebuilt green.

Evidence: .github/issue-evidence/kokoro-metal-perf/
portable-fast-parity-microbench.log + README section (conv 91-107x,
convtranspose 9-19x, LSTM 4.6x vs scalar on the real generator shapes;
up to 173x idle-host).

Refs #9033.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cloud): use idempotent reservations for app chat billing

* feat(eliza-code): show model status and copy replies

* evidence(ios-sim): fresh console-pty boot log for local-inference session (#10727)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): log malformed fact extractor ops

* test(cloud-shared): align sandbox quota predicate assertion

* chore(app-core): satisfy biome import ordering

* style(local-inference): format lint-touched tests

* fix(cloud): refund ad campaign deletes from claimed row

* test(local-inference): fuzz the downloader manifest parsing boundary

Drives the REAL parser functions (no mocks): validateManifest /
parseManifestOrThrow random-mutation + truncation + wrong-encoding +
object-vs-array files.vision + oversized-manifest fuzz;
parseBundleManifestOrThrow catalog cross-checks; collectBundleFiles
conflicting-sha mtp entry rejection (+ cross-kind conflicts + dedup);
bundleTargetPath install-root confinement traversal fuzz. Exports the
three pure downloader helpers so the boundary is directly testable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cloud): standardize agent credit gate 402 responses

* test(cloud): cover canonical credit gate 402 edges

* test(local-inference): fuzz local inference route contracts

* test(local-inference): fuzz drafter/load-args resolution + GGUF header metadata boundary

- resolveLocalInferenceLoadArgs: missing separate-drafter GGUF throws
  (never a silent non-speculative load), manifest-over-catalog drafter
  precedence, invalid merged overrides rejected, fork KV normalization,
  mobile context-ceiling clamp incl. garbage env fuzz.
- validateLocalInferenceLoadArgs: 3000-shape differential fuzz vs an
  oracle in both allowFork modes.
- readGgufArchitecture (text-provenance): crafted valid header parses;
  every truncation, non-string arch, adversarial u64 lengths, bogus
  kv_count, bit-flip/random-byte fuzz all fail closed (null, no throw);
  non-Gemma arch surfaces as a release blocker.

Companion to downloader-manifest.fuzz + route-contracts.fuzz (already landed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(orchestrator): use structural defer reply flag

* chore: apply lint formatting cleanup

* docs(cloud): add package agent guide

* ci(benchmarks): restore lifeops quality benchmark

* docs(eliza-code): document TUI architecture

* evidence(local-inference): scenario trajectories (live Cerebras + deterministic) + fuzz-suite proof

- LIVE gpt-oss-120b runs of local-inference.start-transcription and
  vision.set-mode: both fail honestly (REPLY instead of the action; the
  vision run claims 'Vision mode turned off.' without executing VISION).
- Deterministic-proxy runs of the same scenarios: both pass, real
  START_TRANSCRIPTION / VISION set_mode executed — pipeline sound, gap
  is live routing.
- README documents the two stale-dist harness blockers fixed to get the
  live lane running (plugin-discord subpath dist, scenario-runner
  executor autoLoaded gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(cloud): pin bun test lanes

* docs: document cloud env and eliza-code providers

* evidence(xplatform-web): local-inference web surfaces — models hub (eliza-1 tiers), voice settings, camera fallback, console+network logs (#10727)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(orchestrator): keep task control structural

* evidence(xplatform-web): 25s bounded clip — models hub (eliza-1 tiers) + voice settings walkthrough, ffprobe-verified (#10727)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cloud): preserve model detail auth errors

* evidence(xplatform-web): live local-inference API state (hub/hardware/tier/providers/routing) as domain artifact (#10727)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cloud): make app reconcile refunds idempotent

* ci(ui): restore fixture e2e workflow

* evidence(xplatform-web): mobile-viewport (Pixel 7) local provider panel — eliza-1 hub renders responsive (#10727)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(example-code): typecheck bun test files

* fix(core): harden XML parser and setup replies

* fix(core): refresh trajectory pricing tables

* fix(scripts): use canonical trajectory pricing

* fix(cloud): bind x402 settle recipients

* feat(cloud-sdk): send affiliate codes on inference

* fix(ui): resolve cloud query gates from session

* fix(cloud): partial settle aborted messages streams

* evidence(ios-device): MoonCycles real-device screenshots — DDI capture path, home surface, crash-to-springboard, healthy relaunch

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(local-inference): document device recommendation policy

* fix(cloud): guard app chat settle refunds

* fix(cloud): require ad account approval before spend

* fix(core): defer unusable stage one replies

* evidence(ios-sim): STT mic clips + vision send screenshot + features console log (#10727 iOS leg)

STT: mic listening UI works after Apple speech-recognition grant, but sim
mic capture yields no transcript (first attempt errors 'Could not start
the microphone'); path = SFSpeechRecognizer (Apple), not local-inference
ASR. Vision: attach+send accepted but message silently dropped from
thread (defect, see issue comment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(inbox): route triage through classifier

* evidence(ios-sim): vision attach+prompt clips (photo picker, house image, 'Describe this image.') (#10727 iOS leg)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* evidence(ios-device): MoonCycles real-hardware local-inference run — on-device STT proven live; llama generate SIGSEGVs 4/4 in ggml Metal mul_mat pipeline

Real iPhone 16 Pro Max (iOS 18.7.8), current build (renderer a121f70a8609 built 2026-07-02T21:01Z, variant=direct full-Bun):
- Full-Bun engine boots in-process; agent home surface renders; eliza-1-2B loads
- STT: Apple on-device localspeechrecognition XPC + kTCCServiceSpeechRecognition grant + live transcript rendered (08-stt-live-transcript-yeah.png)
- Every on-device generation (LlamaBridgeImpl.generate) crashes: EXC_BAD_ACCESS NULL in ggml_metal_encoder_set_pipeline <- ggml_metal_op_mul_mat, thread ai.eliza.bun.llama.session.1 (3 identical .ips attached)
- Model load drives device-wide jetsam storm (largestProcess: App, dozens of daemons killed)
- TTS/vision unreachable on-device: no assistant reply can complete before the segfault

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(scenarios): add effect checks to action ratchet

* docs(local-inference): clarify device policy gates

* fix(ui): clean up voice listener lifecycles

* evidence(ios-sim): model-load lifecycle + silent message-drop defect screenshots (#10727 iOS leg)

now4: registry-path defect kept 'Loading Eliza-1 2B' chip stuck; reply1:
send accepted but thread renders nothing; reply4: model fully loaded
(RSS 5.4GB, chip cleared) yet thread still empty >40min; relaunch2:
foreign 'detent gesture probe' composer text proving a concurrent agent
drives the same simulator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): close voice listener lifecycle races

* fix(native): fail closed on stale diarizer gguf

* fix(security): remove unsafe stream casts

* feat(orchestrator): route coding backends per task

* fix(cloud): server-generate billing request ids

* fix(orchestrator): generalize custom app deploy target

* test(feed): harden residual endpoint coverage

* fix(examples): complete edad app oauth sessions

* fix(tui): harden key and list rendering

* fix(app-core): build dist from e2e setup cwd

* fix(benchmarks): repair swe-bench metadata

* ci(ui): widen e2e gate source triggers

* test(scenarios): cover active view planner context

* ci: add stale base guard

* feat(pty): gate vendor cli sessions

* fix(agent): harden api pagination and limits

* fix(cloud): allow org app image namespaces

* chore(agent): satisfy type safety ratchet

* chore(cloud): format stream refund test

* chore: format shared lint fixtures

* chore: refresh verify artifacts

* evidence(10727): iOS-sim vision leg — attach house photo + prompt + send (29.9s clip, ffprobe-verified)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(local-inference/ios): survive nil Metal pipelines + pullable ggml log + jetsam memory budget (#11612)

Three-part durable fix for the on-device text-gen crash-loop on
iPhone 16 Pro Max (A18 Pro):

1. Recoverable Metal failure (llama.cpp submodule -> c3b9fa647,
   branch fix/11612-metal-nil-pipeline-recoverable): a nil compute
   pipeline no longer aborts the process. The encoder latches the
   failure, the op skips encoding, and ggml_metal_graph_compute
   returns GGML_STATUS_FAILED so llama_decode surfaces an error the
   engine host can catch (clean error / cloud fallback, no crash-loop).
   GGML_METAL_ABORT_ON_NIL_PIPELINE=1 keeps the old abort for debugging.

2. GGML log file sink (runtime-symbol-shim.c eliza_llama_log_to_file +
   LlamaBridgeImpl installGgmlLogSink): iOS does not forward the
   embedded engine's stdio, so the GGML_LOG_ERROR naming the failing
   kernel was unobservable. ggml/llama logs now append to
   $ELIZA_STATE_DIR/logs/ggml.log (line-flushed), pullable with
   'xcrun devicectl device copy from'.

3. Jetsam memory budget: LlamaBridgeImpl.loadModel measures
   os_proc_available_memory() before loading, halves the context/KV
   down to 1024 to fit, and fails cleanly with a memory error when even
   the minimum cannot fit; the iOS JS bridge pre-flights model size vs
   available RAM before starting a multi-minute native load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(review): harden proxy refunds and agent routing

* chore(prompts): refresh generated action specs

* chore(app): preserve ios capture artifacts

* evidence(ios-sim): text-gen send clip + STT mic-failure proof + FullBunEngineHost host-call log (#10727 iOS-sim leg)

- ios-sim3-textgen-send.mov (29.5s): typed send on iPhone 16 sim; llama_load_model+llama_generate fired (19:08/19:11 log lines)
- ios-sim3-textgen-0{1,2}: composer + post-send thread-empty rendering symptom (generation fired in oslog regardless)
- ios-sim3-stt-mic-fail.mov (17.3s) + toast png: mic tap -> 'Could not start the microphone' despite TCC mic grant (auth_value=2) + Simulator I/O audio input = host mic; STT and kokoro TTS (voice-loop-only trigger) are therefore N/A on this sim
- ios-sim3-host-calls.txt: full [FullBunEngineHost] host call trace (llama_hardware_info/load_model/generate/free; zero eliza_tts_synthesize / eliza_asr_transcribe)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* evidence(11612): iOS device retest of nil-Metal-pipeline fix — fixed build installed (binary carries recoverable-guard + ggml-log-sink strings), 4-min soak zero new crash reports

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: sanitize local brand residue

* test(cloud): cover eliza app insufficient credits reply

* fix(cloud): hoist throwable prompt prep above credit deduction in app-automation generators

The telegram/discord/twitter app-automation generators
(generateAnnouncement / generateReply / generateAppTweet) charged credits
FIRST, then awaited getCharacterPromptContext (a DB read) BEFORE entering
the refunding try block around generateText. A throw in that
deduct->fetch window (DB error/timeout on the character lookup)
propagated out with the charge committed and no refund. These run on
schedulers/auto-reply loops, so a transient DB failure leaked a post-cost
per invocation, silently, for any app configured with agentCharacterId.

Fix: hoist the character-context fetch + prompt construction above
deductCredits in all four sites (both telegram methods, discord, twitter)
so nothing throwable sits between the charge and the refunding try. The
prep has no dependency on the deduction result; post-fix the only awaited
call after the charge is generateText, already wrapped by the refund.

Regression tests (red on the old code, green now): with
getCharacterPromptContext rejecting, deductCredits is never called for
any of the four generators.

Closes #11685

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: keep brand-neutral local integration fixtures

* fix(orchestrator): router owns origin-routed session completions — stop swarm-synthesis double-posts and suppressed-error leaks (#11634) (#11689)

Reviewed by [sol-orch]: full diff read; ownership predicate mirrors readOrigin exactly (incl. non-short-circuit UUID fallthrough + optional source); stopped/custom-validator/no-router paths verified to still synthesize; per-session terminal serialization closes the double-synthesis and stopped-swallow races. Locally re-ran touched suites (46/46) + full plugin suite (1291 pass / 15 fail = exact pre-existing env baseline, zero new).

Co-authored-by: wakesync <shadow@shad0w.xyz>

* test(app): refresh cloud audit evidence

(cherry picked from commit 9e59eeb0369eea0b5c0376873b24eae1b709f189)

* fix(ui): render app auth authorize in cloud audit

* fix(cloud): settle stale app-chat holds from immutable charge facts

* test(cloud): assert app-chat sweep uses app settlement key

* chore: format server helper config test

* chore: refresh generated registry

* test(cloud): remove duplicate billing gate mock

* test: keep lint integrity gate clean

* fix(cloud): type app credit accounting snapshots

* test(ui): use shared cloud auth query gate

* fix(inbox): resolve triage classification merge

* fix(local-inference): compile iOS metallib at MSL 3.1 so the bf16 kernel family ships on device (#11612)

Root cause of the iPhone 16 Pro Max generation failure: the iOS
embedded metallib was compiled with -std=ios-metal2.4, and
ggml-metal.metal #if's every bf16 kernel out below __METAL_VERSION__
310 — so kernel_mul_mm_bf16_f32 was missing from the library while the
A18 runtime (has_bfloat=true from the GPU family probe) still selected
it for eliza-1-2b's bf16 tensor: MTLLibraryError Code=5 and every
decode graph failed. Desktop metallibs compile at the toolchain default
MSL (3.2) and never hit this.

- build-llama-cpp-mtp.mjs: default ELIZA_IOS_METAL_STD ios-metal2.4 ->
  metal3.1 (bf16 requires MSL 3.1). This raises the metallib AIR floor
  to iOS 17; on iOS 16 the embedded library fails to load and the Metal
  backend cleanly degrades to CPU (context init returns NULL).
- submodule 58c0391eb: runtime gate — after library load, probe for
  kernel_mul_mm_bf16_f32 and downgrade props.has_bfloat when absent so
  a capability/library mismatch falls back to the CPU backend for bf16
  ops instead of failing generation.

Verified: metal3.1 iphoneos metallib contains kernel_mul_mm_bf16_f32 +
kernel_mul_mv_bf16_f32 (ios-metal2.4 control: zero bf16 symbols); all
four iOS slices rebuilt green; desktop build-desktop-metal llama-cli
still builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): stop ComputerUseApprovalOverlay hard-crashing the shell on native IPC bases

Android WebView (and WebKit) throw 'Failed to construct URL: Invalid URL'
when resolving a relative path against the non-special-scheme on-device
base eliza-local-agent://ipc. approvalStreamUrl() did that unguarded at
effect time, so the whole app shell died to the error boundary at boot on
Android on-device builds (live-verified on emulator-5554 via CDP: LDe @
index-CM6E_RB4.js:481:38365). Guard the resolution and degrade to the
existing polling path; add a WebView-parser-emulating regression test
(jsdom's WHATWG URL accepts non-special bases, which is why unit tests
never caught this).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(ios-xcframework): require kernel_mul_mm_bf16_f32 in the packaged slices (#11612)

Ratchet for the metallib MSL regression class: ggml-metal.metal drops
the whole bf16 kernel family below __METAL_VERSION__ 310, and a
metallib without them fails decode on every bf16-capable A-series GPU
once an eliza-1 GGUF (bf16 tensors) loads. With this gate, packaging
the xcframework from slices built at an MSL < 3.1 fails loudly at
build time instead of on the device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* wip: brand-residue sanitize sweep (evidence paths milady->eliza) + staged migration rename

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(app): stub orchestrator relay export in renderer build

* fix(ui): let launcher rail own back swipe

* test(ui): gate launcher rail swipe frames

* feat(ui): declutter the default launcher — fine-tuning behind Developer, feed/stream/relationships behind Preview

Fine-Tuning joins the curated developer-tool set (hidden with the rest unless
Developer Mode is on). feed/stream/relationships are forced to preview kind
for the launcher regardless of declared kind, so the out-of-the-box grid stays
to the everyday core; the Preview toggle brings them back. The legacy rolodex
tile stays fully hidden (dead alias for relationships).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(ios-uitest): real composer send + reply-wait leg in BootCaptureUITests (#11612)

New testComposerSendsPromptAndWaitsForReply: waits out the local-model
warm-up chip, types a prompt into the real composer, submits via the
send control (iOS Return never reaches the web textarea as Enter),
hard-asserts the draft cleared and the app survived, and detects an
assistant reply as a new static-text label with a screenshot filmstrip
throughout. Drives the on-device generation leg of the #11612 bf16
retest via ios-device-capture.mjs --only-testing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* evidence(11612): bf16-retest run 1 on MoonCycles — kernel now loads; decode blocked by GPU OOM

Rebuilt + reinstalled ai.elizaos.app with the bf16 fixes (metallib MSL 3.1
d2bd92e5390, submodule runtime gate 58c0391eb, packaging ratchet
0101e1bcbe8). mtp slice provenance: fork revision 58c0391eb-dirty,
air64_v26-apple-ios17.0.0, kernel-symbol audit PASS.

ggml-device-postfix.log session 5 (new container E223551C):
'loaded kernel_mul_mm_bf16_f32' — the missing-kernel MTLLibraryError is
GONE. Decode now fails later with
kIOGPUCommandBufferCallbackErrorOutOfMemory: weights MTL0 4722 MiB +
compute 1037 MiB + KV 36 MiB vs 5461 MiB working-set budget
(n_gpu_layers=999 + n_ubatch=1024 on an 8 GB iPhone). App then jetsams
proc-thrashing ~14 min later (JetsamEvent-2026-07-02-212403).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* evidence(11612): bf16-retest README — kernel resolved, GPU-OOM residual quantified

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(aosp-local-inference): unmask fused-lib FFI errors + accept flat model layout + survive KV-quant rejection

Three live-debugged Android on-device inference fixes (emulator-5554,
bun 1.3.14, fused libelizainference ABI v9):

1. readFfiPointer handed a Buffer to bun's ffi.read.ptr, which takes a raw
   Pointer NUMBER and throws 'Expected a pointer' — masking EVERY native
   error diagnostic on the fused text path (the agent only ever surfaced
   'Expected a pointer' for TEXT_SMALL/TEXT_LARGE). Read the out-param via
   DataView instead (verified on-device).
2. The fused lib resolves the chat GGUF strictly as <bundleRoot>/text/*.gguf
   but Android first-run stages the curated model FLAT under models/ —
   every stream_open failed with 'no text GGUF found'. Mirror the bionic
   host's hardlink-bundle shim on the fused musl path.
3. This fused build rejects the default eliza-1 KV-quant config ('V cache
   quantization requires flash_attn' -> llm_stream_open cannot init the
   llama context). Add ELIZA_LLAMA_KV_TYPE_K/V env override + a loud
   one-time f16 retry so local text inference stays alive instead of
   hard-failing, with the degradation logged.

Also: add the @elizaos/plugin-agent-orchestrator named-export stub
(sanitizeCompletionRelay) to the renderer native-module stub plugin — the
generic 'export default {}' fallback broke the capacitor web build after
server-helpers-swarm.ts started importing it.

bun test plugins/plugin-aosp-local-inference: 75 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): simplify local model readiness surfaces

* fix(agent): register @elizaos/plugin-vision in the static plugin map

MOBILE_CORE_PLUGINS selects plugin-vision on ELIZA_PLATFORM=android|ios
(screen understanding + the #11111 renderer-pulled ML Kit OCR bridge),
but CORE_STATIC_PLUGIN_REGISTRATIONS never included it, so the mobile
agent bundle could not resolve the module: on-device the plugin silently
never loaded and the renderer OCR/screen-capture pollers polled
/api/vision/ocr-requests into {"error":"Not found"} every ~1.2s forever
(verified live on emulator-5554). Add the deferred static registration
+ the literal dynamic-import branch so Bun.build includes the module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* evidence(11612): send-leg status — harness green, device run gated on physical passcode unlock

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* evidence(android-emu): clip 01 — chat send to on-device eliza-1 (local inference)

23.6s screenrecord, emulator-5554, ai.elizaos.app build with fused-lib fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cloud,orchestrator,core): defuse the review blockers on the residuals branch

- Restore migration 0132 exactly (filename, journal tag, SQL predicate).
  The brand sweep had renamed it AND flipped the predicate from
  'milady-core-%' to 'eliza-core-%', which rewrites applied migration
  history and, on a fresh environment, disables the CURRENT autoscaled
  eliza-core nodes instead of the legacy milady-core ones.
- Resequence 0164_ad_accounts_pending_default -> 0168 (0164 collided with
  the already-applied 0164_pooled_credentials; 0166/0167 landed on develop
  since) and register it in _journal.json — it was unjournaled, so the
  fail-closed pending default silently never ran.
- Deny OPENCODE_CONFIG_CONTENT at both sub-agent env intake paths.
  43d896a33a3 changed the test to expect this but never touched
  DENY_ENV_PATTERNS, leaving the branch red; buildOpencodeAcpEnv injects
  the sanctioned runtime-built config after the filter, so the deny only
  blocks caller/host-supplied values.
- Drop packages/core/src/security/__probe.test.ts: an assertion-free
  exploration probe that writes to tmpdir and asserts nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(ui,agent): iOS-style notification center + first-boot onboarding notifications

- NotificationCenter: the mobile pull-down sheet and desktop panel become one
  dark frosted-glass shell (bg-black/45 + backdrop-blur-2xl, flat 1px border)
  with each notification as its own rounded translucent card — the iOS
  notification-center look. White-on-glass type ramp (title/body/time), glass
  icon chips (priority tints kept), restyled filter chips / sort toggle /
  header controls / empty state / grabber. One backdrop-filter per shell (the
  cards don't stack their own blurs, keeping the phone GPU cost flat).
- Onboarding seeds: on agent boot, seedOnboardingNotifications() posts three
  getting-started notifications through the canonical NotificationService —
  'Take the tour' (/tutorial), 'Get help any time' (/help), 'Connect your
  calendar' (/connectors) — exactly once per agent (cache guard flag, so a
  dismissed/cleared inbox never re-onboards). Deep links are root-relative to
  pass the client isSafeDeepLink allowlist; stable groupKeys keep any re-seed
  collapsed. Unit-tested (seed/skip/headless-then-seed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* evidence(android-emu): kokoro TTS wav + STT input + agent-log slice + chat clip 02

- 03: 4.81s 24kHz WAV synthesized ON-DEVICE by fused kokoro (synthMs=82); NOTE:
  ASR round-trip of this wav returns an empty transcript (known kokoro vocoder
  defect) - path fires, audio not intelligible speech.
- 04: input wav for the STT leg; on-device eliza-1 ASR transcribed it exactly:
  'Hello, Eliza. This is a local speech recognition test on Android.' (25s).
- 05: [aosp-local-inference] log lines proving both native paths fired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): map plugin-birdclaw into the shared activity widget sink

The develop merge brought in @elizaos/plugin-birdclaw with no home-widget
mapping, redding the per-plugin coverage gate (#9143). Route it through the
shared activity sink like the other archive/feed-style plugins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mobile-build): stop the legacy-tree mirror from stomping the freshly synced capacitor.plugins.json

With the unified android tree (android.path=../app-core/platforms/android,
#8387) cap sync writes the fresh plugin manifest straight into androidDir.
A leftover legacy packages/app/android tree (stale assets/public from May)
still trips the two-tree mirror, which copied its months-old
capacitor.plugins.json over the fresh one — silently dropping every newer
native plugin (@elizaos/capacitor-mlkit-text OCR #11111, screencapture,
mobile-agent-bridge, contacts, phone, wifi, ...) from Capacitor
auto-registration. On-device those resolve to 'not implemented on android'
(verified live on emulator-5554: sync found 33 plugins, shipped manifest
had 15). Guard the mirror on manifest mtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* evidence(android-emu): before/after renderer-crash screenshots + chat + OCR test image

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* evidence(android-emu): ML Kit OCR result + logcat + clip + agent-log key lines

07: real on-device ML Kit Text Recognition v2 output for the test image -
'HELLO ELIZA 42' / 'OCR BRIDGE ANDROID', conf 1.0, correct block/line
grouping + boxes, via the registered @elizaos/capacitor-mlkit-text plugin
(pluginId Tesseract, #11111).
08: logcat proving the REAL native path fired (libmlkit_google_ocr_pipeline.so
+ gocr tflite models) + the renderer OCR-bridge poller hitting
/api/vision/ocr-requests (now 200 with the plugin-vision registration fix).
09: 20s screenrecord during the call. 10: agent.log key lines (fused text
gen, kokoro TTS, ASR, vision plugin registration).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* evidence(android-emu): README index for the local-inference capture bundle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(app): exclude transient chat overlay from density audit

* fix(agent): satisfy onboarding notification import order

* test(ios-uitest): symmetric 50% swipe rules on the launcher back-swipe

The rail now owns the right-swipe back home 1:1 (no reduced edge-swipe
threshold), so the on-device gesture-semantics leg asserts BOTH directions
follow the same distance rule: a slow ~28%-width right drag snaps back to the
launcher; a slow past-50% right drag commits home.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(ios-uitest): complete the first-run placement step before gesture legs

A fresh install boots into the first-run placement question, which pins the
chat sheet and locked every gesture precondition into a skip. The suite now
takes the real user path — tap 'On this device', wait (bounded, screenshotted)
for the composer lock to clear — so the pager/detent legs run on a fresh
device install instead of skipping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* evidence(11731): launcher swipe-right 1:1 — e2e + FPS gate + MoonCycles fresh-build boot capture

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(actions): refresh generated action specs

* test(ios-uitest): harden first-run completion — poll the placement modal mount, honor the agent-ready budget for the lock-clear wait

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Shaw <shawgotbags@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: NubsCarson <nubs@nubs.site>
Co-authored-by: Sol <sol@shad0w.xyz>
Co-authored-by: wakesync <shadow@shad0w.xyz>
@NubsCarson

Copy link
Copy Markdown
Member

Thanks @0xSolace — good, the sub-agent-router owning the swarm-synthesis dedup is the right call; cloud-money has zero overlap here so it's all yours. This is exactly the coordinate-don't-dup we want. 👍 [cloud-money]

lalalune pushed a commit that referenced this pull request Jul 3, 2026
lalalune added a commit that referenced this pull request Jul 3, 2026
fix(orchestrator): don't synthesize the teardown stopped that follows a router-ceded terminal (#11689 residual)
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error —— View job


I'll analyze this and get back to you.

NubsCarson added a commit that referenced this pull request Aug 20, 2026
…sis contract

Review round (ss251): the marker test now imports vitest (the package's
test runner — bun:test failed the owning gate); a new suppression suite
exercises the REAL coordinator through the house ACP-double harness:
a stopped carrying adminStopReason synthesizes nothing and claims no
dedupe slot (a later lineage task_complete still posts), while an
unmarked stop still synthesizes — the #11689 never-silent-terminal
line, now pinned by test instead of asserted in prose. Dead
.catch(() => undefined) on the non-throwing stamp removed per catch
policy.
NubsCarson added a commit that referenced this pull request Aug 20, 2026
… stop narrating as failures (#22852)

* fix(orchestrator): port administrative-stop marker so lifecycle stops stop narrating as failures

Ports the live-branch admin-stop marker to develop: one canonical
adminStopReason key stamped best-effort BEFORE every administrative
stopSession (smithers recovery, verifier teardown, user stop, task
lifecycle bulk stop, idle reclaim) and the user-interrupt cancel path;
the swarm coordinator's stopped-synthesis suppresses marked stops
without claiming the dedupe slot, so a later genuine lineage completion
still posts. Unmarked stops (crash, subprocess death) synthesize
unchanged — the #11689 never-silent-terminal invariant holds. Closes the
long-standing leak where app-control/API stops posted spurious
"stopped before completion" nags.

Marker unit suite added (stamp, no-op tolerance, fail-open). The two
verify-retry-busy-session failures are pre-existing on develop (verified
via stash baseline) and untouched by this change.

* fix(orchestrator): vitest-native admin-stop tests covering the synthesis contract

Review round (ss251): the marker test now imports vitest (the package's
test runner — bun:test failed the owning gate); a new suppression suite
exercises the REAL coordinator through the house ACP-double harness:
a stopped carrying adminStopReason synthesizes nothing and claims no
dedupe slot (a later lineage task_complete still posts), while an
unmarked stop still synthesizes — the #11689 never-silent-terminal
line, now pinned by test instead of asserted in prose. Dead
.catch(() => undefined) on the non-throwing stamp removed per catch
policy.
lalalune pushed a commit that referenced this pull request Aug 21, 2026
…#22986)

A stamped stopSession that throws leaves the surviving session wearing
adminStopReason forever; the coordinator then silently swallows the
survivor's later genuine crash on every stopped event, violating the
never-silent-terminal invariant (#11689) that the marker's own
suppression names as its regression line.

Stamp adminStopStampedAt alongside the reason and honor the marker only
within a ten-minute freshness window: duplicate teardown stopped events
from one administrative action stay suppressed, while a stale stamp - or
a timestamp-less pre-fix stamp - is cleared best-effort and the stop
synthesizes, mirroring the handoff-pending staleness contract.

Closes #22981.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

orchestrator: swarm synthesis double-posts completions and leaks router-suppressed state-lost errors to the origin channel (ownership rule needed)

4 participants