The continuation guard scanned the whole session snapshot for non-error
toolResults, so a prior turn's result with a model-reused tool-call id
could prove 'completion' for a batch that never dispatched (fresh-eyes
review finding on 2848acbbaa). Scope the scan to snapshot entries after
the terminal assistant — both assistant fields are found on the snapshot
itself, so identity lookup holds; an assistant missing from the snapshot
fails closed to the existing incomplete-turn error. Adds a stale
prior-turn-result regression.
* fix(resolve-system-bin): cap binary resolution caches with eviction
The two module-level Maps grow with every distinct binary name resolved.
A 512-entry cap prevents unbounded memory growth in long-running processes
that resolve many different system binaries.
* fix(resolve-system-bin): share LRU cache pruning
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): bound base64 image input before decode in tool-image sanitizer
* fix(agents): lower input-size cap to 10MB for OOM headroom
* fix(agents): align tool-image input-cap comment with 10 MiB ceiling
* fix(agents): typecheck-safe access in tool-image input-cap test
* test(agents): exercise real tool image input cap
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Session-catalog dispatch read the mutable `activeRegistry`, which a later
standalone plugin load can replace with providers built from default
(unbound) plugin runtimes. Those runtimes hit the unavailable nodes stub, so
Control UI Codex/Anthropic catalogs surfaced `NODE_LIST_FAILED` ("Plugin node
runtime is only available inside the Gateway") even though the gateway binds a
real nodes runtime and CLI `nodes list` works.
Read the pinned session-extension registry instead so catalog providers keep
the gateway-bound runtime across plugin registry churn. Generic across all
session-catalog plugins.
resolveTranscriptAssistantRoute read providerConfig.models.find without
guarding models, but the runtime config passed here is the raw user config
where a provider entry may omit models (e.g. models.providers.openai =
{ apiKey }). That threw "Cannot read properties of undefined (reading 'find')"
inside the SQLite transcript-append transaction, so every agent turn failed to
persist its transcript (the underlying error was swallowed to {} and surfaced
only as "Turn transcript persistence failed").
Guard the optional models array at the crash site, and fix two identical
raw-config hazards found in the same sweep (cron isolated-agent model api
resolution, microsoft-foundry image provider). Regression test drives
redactTranscriptMessage with a provider that has no models array.
* fix(git-commit): cap commit resolution cache with FIFO eviction
The module-level cachedGitCommitBySearchDir Map grows without bound.
A 256-entry FIFO cap prevents unbounded memory growth from repeated
lookups across distinct search directories.
* fix(git-commit): use shared LRU cache pruning
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Embedded runs targeting a CLI runtime provider fall through to the openclaw
harness and call the provider API directly with the runtime's credentials
(cli_runtime_passthrough_openclaw). Anthropic routes direct anthropic-messages
calls on subscription OAuth tokens to metered extra-usage billing — this is
long-standing behavior, not a recent change. Without extra-usage balance,
every such run (e.g. active-memory recall) fails with a billing error; with
extra-usage enabled, the run silently draws paid metered usage instead of the
plan limits the CLI runtime was configured for. Only CLI-backed execution
runs on plan limits for those credentials.
Add an opt-in RunEmbeddedAgentParams.cliBackendDispatch: "subscription-auth"
that dispatches the run through runCliAgent as a one-shot turn when the
provider is claude-cli, a CLI backend is registered, and the ordered auth
profile selection for the passthrough resolves to a subscription (oauth/token)
credential or nothing rather than an API key; resolution stays on stored
credential metadata, with no credential materialization or refresh on the
per-turn path. The dispatch translates toolsAllow into the selectable-backend
surface (native: [], allowlisted loopback MCP tools; wildcard allowlists stay
MCP-only), runs with a fresh CLI process (no live-session reuse; session-
scoped bundle-MCP retirement on run end rather than the process-wide loopback
close), bridges CLI tool result events to onAgentToolResult with native-path
semantics (normalizeToolName + isToolResultError), and drops CLI session
bindings from the result.
The selectable-backend MCP list now also bounds the loopback MCP grant
server-side: the grant carries a per-run gateway tool allowlist enforced in
scoped tool resolution, so tools outside the run's allowlist can be neither
listed nor called even under CLI bypass permission modes where
--allowedTools is advisory.
active-memory recall opts in so recall works on claude-cli subscription-only
instances and stops drawing metered extra usage where it previously could.
Scoped to claude-cli; other CLI runtimes keep the passthrough until their
direct-API contract is verified.
The dispatch also mirrors the run into the run's session transcript through the session
accessor (user turn, tool call/result records as they stream, final assistant
snapshot at run end) so transcript consumers keep parity with embedded runs:
active-memory's persistTranscripts, timeout partial-text salvage, and the
live terminal-search watcher that polls the session file mid-run.
Post-review hardening: canonical anthropic/<model> refs whose configured
agentRuntime is claude-cli resolve through the runtime policy before the
dispatch gate (they previously stayed on the failing passthrough); restricted
dispatches serve an exclusive loopback-only MCP bundle so user/plugin MCP
servers stay outside the run's tool universe; and the transcript recorder
flushes the latest assistant snapshot the moment the run aborts, so timeout
salvage sees partial text even while the killed CLI child is still settling.
Recalls routed to the claude-cli runtime default to a 45s budget (measured
CLI-dispatched runs take 14-20s, over the plain 15s default); explicit
timeoutMs config always wins.
Transcript mirror keeps bare-array tool_result content (claude stream-json
echoes MCP results without a {content} wrapper); dropping it classified every
successful recall as no_relevant_memory.
CLI dispatch resolves inside session/global lane admission so dispatched
runs obey the same lifecycle, placement, and concurrency gates as native
embedded runs.
LOC-ratchet offsets move the loopback grant-context builders to
cli-runner/mcp-grant-context.ts and dedupe the run/prep stage-summary
emitters into attempt-stage-timing.ts; unused type exports dropped and the
now-used stream-message baseline entry removed.
The recall timeout default now consumes the runner's own dispatch
eligibility through a new plugin-runtime seam
(agent.resolveCliBackendDispatchEligibility): API-key and missing-backend
routes keep the passthrough and its plain 15s default.
Eligibility honors an explicitly pinned authProfileId (the credential the
run executes on) before ordered profile selection, in both directions.
Transcript mirror composes buildAssistantMessage + buildUsageWithNoCost
directly; main trimmed the zero-usage wrapper export (ab0ccc244b) before
this branch's usage landed.
Dispatch eligibility is provider-owned: the anthropic plugin's claude-cli
backend declares CliBackendPlugin.subscriptionAuthDispatch and core reads
the registered descriptor instead of a core provider allowlist.
Dispatch fails closed on tool policy (only non-empty named allowlists are
expressible on the CLI surface; deny-all, wildcards, absent allowlists, and
disableTools/modelRun keep the passthrough), threads the pinned
authProfileId into CLI runtime resolution, and emits onExecutionStarted at
the admitted dispatch boundary.
Adds the sidebar approval attention chip and a dedicated approval history
page backed by the new approval.history gateway RPC (30-day retention
window). Extracts buildSidebarAttentionItems into its own module so the Lit
component consumes it as a real cross-module dependency, and wires the strict
i18n catalog for the new strings (fallbacks=0).
vi.waitFor in the ACP abort test and the shared Swift waitUntil helper
both timed out on contended runners while passing solo; polling waits
return immediately when healthy, so the higher ceilings cost nothing.
* fix(agents): keep not_required delivery status when completion_announced_at exists for expectsCompletionMessage=false runs
createDeliveryFromTypedColumns unconditionally overrode status to
'delivered' when completion_announced_at was present, even for runs
where expects_completion_message is false. This produced a contradictory
delivery state where a run that does not require completion delivery
was marked as delivered.
Only set status to 'delivered' when expects_completion_message is true.
For not_required runs that still have completion_announced_at recorded
(for informational purposes), preserve announcedAt without overriding
the status.
* test(agents): cover tainted delivery rows
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(migrate): add Hermes memory-only import and a shared memory-import core
* feat(onboarding): offer detected memory imports during CLI setup and guided onboarding
* feat(ui): show a first-run memory-import offer in Control UI onboarding mode
* feat(linux-app): open the first-run dashboard in onboarding mode
* feat(macos): add the onboarding memory-import page
* docs: document the onboarding memory-import page across surfaces
* chore(i18n): translate onboarding memory-import strings for control-ui and native locales
* refactor: keep memory-import internals unexported for deadcode gates
* fix(ci): resolve lint findings in onboarding memory import
* chore(i18n): refresh native inventory after lint refactor
* fix(ci): skip updater artifacts in the unsigned Linux companion PR lane
* chore(i18n): reconcile locale artifacts after rebase
* fix(agents,skills): keep session-identity and experience-review transcript truncation UTF-16 safe
normalizeCliContextValue (cli-output.ts) and formatSkillExperienceReviewTranscript
(experience-review-prompt.ts) both used raw .slice(0, N) / .slice(-N) to truncate
user-facing strings. When a truncation boundary falls inside a surrogate pair (emoji),
the resulting string contains a lone surrogate — encodeURIComponent throws URIError,
JSON serialisers may reject it, and downstream display surfaces can render garbage.
Replace .slice(0, 200) with truncateUtf16Safe in normalizeCliContextValue.
Replace .slice(0, 6_000) with truncateUtf16Safe and full.slice(-tailBudget) with
sliceUtf16Safe(full, tailStart) in formatSkillExperienceReviewTranscript.
Follows the pattern established by #98644, #102470, #101728 and the broader
UTF-16 safety sweep already applied across the codebase.
* chore: re-trigger CI after PR body update
* test(agents,skills): construct transcripts exceeding 60k chars, verify old slices dangle
Both experience-review fixtures now exceed EXPERIENCE_REVIEW_MAX_TRANSCRIPT_CHARS
so the truncation branch actually fires. Pre-condition assertions prove the old raw
.slice(0,6000) / .slice(tailStart) produce isolated surrogates; the production
functions (formatSkillExperienceReviewTranscript, formatCliOutputError) do not.
cli-output test also adds pre-condition check showing normalizeCliContextValue
with raw .slice(0,200) would split a surrogate pair.
* test: tighten UTF-16 truncation coverage
Co-authored-by: 毛宇豪0668001457 <mao.yuhao@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
PR #108283 started deriving a durable channel source-turn id for gateway
chat.send turns carrying originatingChannel/originatingTo overrides, while
the gateway's pre-created user-turn recorder keys the persisted turn by run
id. The same PR's admission guard then throws 'channel restart recovery
requires source-keyed user-turn admission' for every such turn — breaking
qa-lab suite runs, ACP-bridge flows, and relay integrations (reproduced
live on the codex harness chat-image probe; text-only fails identically).
Internal-origin turns (gateway chat.send stamps the internal channel as the
ingress provider) carry run ids, not provider message ids; gateway turns
already own restart via fingerprint admission and client-side idempotent
retries. Skip the mint for internal ingress at both derivation sites so
admission takes the source-less persist path, while real channel ingress
keeps the new source-keyed recovery. Also surface the full agent.wait
result in the codex harness live test error (how this was found).
Live proof: gateway-codex-harness.live.test.ts chat-image probe on a
Blacksmith Testbox with a real OpenAI key failed with the admission error
twice on main and passes admission with this fix.
* fix(shell-env): cap login shell env probe cache with eviction
The loginShellEnvProbeCache Map keys include the exec function reference
and exec environment, which can vary across calls. A 64-entry eviction
cap prevents unbounded growth.
* refactor(shell-env): share bounded probe cache insertion
Co-authored-by: 陈志强0668000989 <31802073+wings1029@users.noreply.github.com>
* fix(shell-env): retain active probe cache entries
Co-authored-by: 陈志强0668000989 <31802073+wings1029@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
333c4f9a61 and 244f4965dc landed six exports with no production
consumers, failing the hard-zero deadcode gate on every PR: a dead
re-export block in restart-handoff (the contract module's real importer
uses it directly), three result/mode types only referenced inside their
own modules, and two supervision symbols whose only consumers were their
tests. The types and helpers stay defined for the in-flight supervisor
work to re-export alongside real consumers; the supervision test now
proves mode resolution through the public isGatewayExternallySupervised
surface.
* fix(channel-selection): cap error dedupe Set with FIFO eviction
The module-level loggedChannelSelectionErrors Set grows without bound
for the lifetime of the gateway process. A 1024-entry FIFO eviction cap
prevents unbounded memory growth from per-plugin/account error keys.
* test(channel-selection): add LRU eviction proof with 5 focused tests
Proves the 1024-entry FIFO cap prevents unbounded growth:
- Exact limit enforcement at 1024 entries
- Oldest entry eviction on overflow
- Dedup still works after eviction
- Size never exceeds the cap
* refactor(channel-selection): use shared warning dedupe cache
Co-authored-by: 陈志强0668000989 <31802073+wings1029@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>