resolveSshConfig and startSshPortForward spawned a hard-coded /usr/bin/ssh,
so SSH config discovery and gateway tunneling failed wherever the system ssh
client lives elsewhere: Windows (built-in OpenSSH under System32\OpenSSH) and
NixOS (/run/current-system/sw/bin).
Route both helpers through the existing resolveSystemBin("ssh", { trust:
"strict" }) resolver, and add the Windows built-in OpenSSH directory to the
resolver's trusted Windows locations (it had System32 but not the
System32\OpenSSH subdirectory where ssh.exe actually ships). Fail closed with
a clear diagnostic when no system ssh client is present.
Closes#83289.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(memory): keep canonical rows when same-file legacy memory tables diverge
* fix(memory): keep canonical-owned sources' chunk sets coherent during legacy import
* fix(memory): import legacy chunks when canonical source has no chunks
The same-file legacy migration excluded a legacy chunk from import whenever the
canonical index already had a source row for its (path, source). That stranded a
file whose canonical source was registered but had no chunks yet (indexing
interrupted before chunks were written, or embedding pending/failed): the legacy
chunks were its only searchable content, and the matching source hash stops sync
from re-indexing, so the file went silently unsearchable.
Re-key the chunk-coherence exclusion on canonical chunk ownership instead:
snapshot the (path, source) pairs that already have canonical chunks before the
import and skip legacy chunks only for those. A source with a canonical row but
no chunks now imports its legacy chunks. The snapshot is taken pre-insert because
the exclusion predicate reads the chunks table the import writes to.
Add regressions: legacy chunks import for a chunk-less canonical source while a
chunk-owning source still drops its stale legacy chunk; and restore abort
coverage for the meta and chunks copy assertions (previously only files was
exercised).
* fix(memory): harden same-file legacy conflict recovery
* fix(memory): rebuild ambiguous partial legacy sources
* fix(memory): reconcile migrated derived indexes
* fix(memory): close migrated index ownership gaps
* test(memory): align migration expectations
* test(tooling): match routed test order
* test(memory): exercise vector reload cleanup
* test(memory): prove real vector reload cleanup
* fix(memory): make migrated indexes converge
---------
Co-authored-by: Serhii Leniv <leniv.tech@gmail.com>
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(plugin-sdk): bound Anthropic Vertex ADC credential read
canReadAnthropicVertexAdc read the entire GOOGLE_APPLICATION_CREDENTIALS
file via unbounded fs.readFileSync solely to check readability (the
contents are discarded). The provider-local copy of this logic in
extensions/anthropic-vertex/region.ts was already bounded with
tryReadSecretFileSync + a 1 MiB limit in #109260 ("reject oversized
credential files in remaining readers"); this standalone plugin-sdk
preflight helper is a duplicate that sweep missed.
Mirror the region.ts bound: replace the unbounded readFileSync with
tryReadSecretFileSync(..., { maxBytes: 1 MiB, rejectHardlinks: false }),
wrapped in try/catch (the helper throws FsSafeError on oversize) so an
oversized credential file is rejected instead of slurped into memory.
Presence-check semantics are unchanged for normal-sized files.
* refactor(plugin-sdk): remove orphaned vertex auth helper
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): record token usage when a Responses turn ends incomplete
The agent-side Responses processor only had a terminal branch for
response.completed. A stream that ends with response.incomplete — the
max_output_tokens and content_filter cases, and what Azure emits on early
truncation — matched no branch at all, so the event was dropped: usage was
never recorded and stopReason was never set. The turn reports zero tokens and
zero cost, which is the drift in #100954.
#109615 fixed the same split on the package-side processor by finalizing
completed and incomplete through one finalizeResponse. This does the same for
the agent path, which #109615 did not touch: both terminal events now record
usage, cost and service-tier pricing through one helper.
The helper moves to its own module rather than growing openai-responses-transport.ts,
which is a legacy file the max-lines ratchet will not let grow; extracting the
block drops it from 2502 to 2444 lines.
Content-filtered turns are mapped to a provider error instead of a plain length
stop, matching what the package side already does, so the two terminal surfaces
do not disagree.
* fix(agents): keep Responses output backfill on completed turns
Terminal handling now covers response.incomplete, which also routed those
events into backfillCompletedResponseOutput. That reconstruction exists to
recover a final answer when item events never arrived; an incomplete turn has
no final answer, so replaying its partial output persisted truncated text the
streaming path never emitted. Usage and stop-reason recording still apply to
both terminal events.
* refactor(ai): share one Responses terminal usage mapper
The agent transport mapped terminal usage buckets, cost, and stop reasons in
parallel with the package-side processor, so the two could drift on token
buckets, service-tier pricing, or future terminal-event semantics.
Both now call one canonical mapper. openai-responses-shared.ts cannot be
imported across the package boundary, so the mapper lives in its own module
re-exported through the existing internal/openai entry point; no new subpath
is introduced. The agent module keeps only the reasoning-token accounting the
package path does not track.
Merging the two revealed a real disagreement on totalTokens: the package took
the reported total, the agent summed the split buckets. The canonical rule is
now max(bucket sum, reported total), which keeps the reported value while
covering both payloads that omit total_tokens and payloads whose cached_tokens
exceed input_tokens, where clamping leaves the reported total short.
* fix(agents): preserve partial incomplete Responses output
Co-authored-by: Yigtwxx <yigiterdogan023@gmail.com>
* test(agents): cover incomplete terminal output backfill boundaries
The head's partial-output preservation is only safe while it stays a recovery
path. Pin both sides of that guard: text that already streamed must not be
replayed from the terminal payload, and a non-length incomplete stop must not
surface partial text as an answer.
* fix(agents): avoid shadowing Responses options
Co-authored-by: Yigtwxx <yigiterdogan023@gmail.com>
* style(agents): apply oxfmt to the incomplete backfill test
* test(agents): register incomplete Responses suite
Co-authored-by: Yiğit ERDOĞAN <yigiterdogan023@gmail.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* ci: enforce changed-file TypeScript LOC ratchet
* ci: derive release-gate LOC base from PR
* test(ci): exclude repository test helpers from LOC ratchet
* ci: validate LOC ratchet on PR merge tree
* style: format release maintainer skill
* ci: run LOC ratchet for fast-only changes
* fix(ci): harden LOC ratchet comparisons
* fix(ci): cover native TypeScript in LOC ratchet
* fix(ci): compare LOC against tested merge tree
* test(ci): cover LOC manifest routing
* test: wire Control UI suite into CI, fix its broken tests, drop dead harness surface
- fix 7 Control UI tests broken on main: module-mock factories are unreliable
under isolate:false shared workers; use RealtimeTalkSession prototype spies
and real jsdom storage instead (chat-realtime, chat-pull-requests)
- add gated checks-ui CI job (runUiTests changed-scope output): chromium
provisioning, lint:ui:no-raw-window-open, pnpm --dir ui test; ~30s on a
4vcpu runner, runs only when ui-affecting paths change
- add weekly node22-compat workflow exercising the supported lower Node bound
with the same command set as the dispatch-only ci.yml job
- delete the unreachable channels entry in EXTENSION_TEST_CONFIG_ROUTES and
the never-populated extensionRoutedChannelTestFiles override machinery
(born empty in 2ccb5cff22); resolved globs are provably identical
- guard run-vitest.mjs hardcoded path maps with existence tests so renames
cannot silently drop extended stall watchdogs
- remove unenforced coverage thresholds; test:coverage is informational, docs
updated (reference/test, help/testing, plugins/sdk-testing)
Closes#104321
* test: pin OPENCLAW_CI_RUN_UI_TESTS in the manifest env expectation
* test(ui): capture the realtime start spy instead of referencing the unbound prototype method
* test(ui): raise ui vitest timeouts for real-browser layout tests on small CI runners
The 'routes top-level test helpers to importing repo tests' test froze the
full list of tests importing test/helpers/temp-dir.ts; any new importer broke
main's checks shard (twice on 2026-07-05). Assert routing structure instead:
scan-derived direct-importer lower bound, partition invariants, and per-file
consistency with single-target routing.
694f5ac7e5 added a temp-dir import to test/scripts/package-git-fixture.test.ts
without updating the top-level helper routing expectation, breaking the
checks-node-compact-small-whole-2 shard on main and PR merge runs (same
class as 824d8af0d6).
17a1993f13 made test/scripts/e2e-shell-tempfiles.test.ts import the
temp-dir helper without updating the helper-routing expectation, so
checks-node-compact-small-whole-2 fails on every PR merge snapshot.
* ci: guard gateway protocol event coverage for iOS/Android clients (#100198)
Adds scripts/check-protocol-event-coverage.mjs, which derives the
server->client event catalog from GATEWAY_EVENTS in
src/gateway/server-methods-list.ts, extracts the events each mobile app
handles from Swift/Kotlin dispatch sites, and fails on gateway events no
client handles unless allowlisted with a reason in
scripts/protocol-event-coverage.allowlist.json. Wired as
pnpm check:protocol-coverage in the CI guards shard.
* fix(ci): scope Kotlin event extraction to handle*Event dispatch functions (#100198)
Bare event == "..." literals in predicate helpers outside the dispatch
path (gatewayEventInvalidatesNodesDevices in NodeRuntime.kt, which has no
production caller) counted as Android coverage, silently masking that
node.pair.requested/resolved have no live handler. Kotlin extraction now
only reads when(event) labels and event comparisons inside fun
handle*Event(...) bodies; node.pair.* moved to the Android allowlist with
a truthful reason. Swift extraction stays tree-wide because consumption
there always reads .event off a received EventFrame.
* fix(android): remove dead node pairing event helper
* fix(ci): preserve protocol allowlist parse errors
* test(ci): align tooling import plan