This PR:
- closes
[SEC-1179](https://linear.app/composio/issue/SEC-1179/composio-attacker-can-reach-internal-services-using-ssrf-to-steal)
- routes proxy binary downloads through the core `ssrfSafeFetch` guard
- requires validation-to-connection pinning and fails closed when a
configured dispatcher or proxy prevents it
- reapplies that requirement on every redirect hop while preserving
default SDK proxy compatibility
- covers direct metadata targets, configured-route bypasses, redirects,
and the bundled Bun companion
- verifies the full core and CLI suites plus the TypeScript workspace
typecheck
This PR:
- follows https://github.com/ComposioHQ/composio/pull/3901 (merged as
0abc629f5)
- fixes a help-system inconsistency: whole command families (`orgs`,
`signup`, `agent`, `connections`, `triggers`, `artifacts`, `install`)
silently fell through to the framework's raw parser rendering instead of
the curated styled help pages every other family gets
- adds curated help entries for the `agent` children (`agent signup`,
`agent login`, `agent whoami`, `agent inbox`, `agent claim`), mirroring
the `orgs list`/`orgs switch` pattern
- adds `composio help [command] [level]` — the framework has no builtin
help command, so `composio help orgs` now routes to the same curated
page as `composio orgs --help`; bare `composio help` keeps printing the
root help. The spelling resolves targets with the same longest-prefix
scan as `--help` (so `composio help dev toolkits` renders the curated
dev page), and an unknown target falls through to the framework parser
(stderr, "Did you mean?", exit 1) exactly like any other unknown command
- fixes the stale `orgs` description in the contextual-error help
registry
- adds a consistency regression test that walks every visible root
command and fails when any lacks a curated help entry, plus routing
tests for every `composio help` path (bare, family, child, level
suffixes, trailing `--help`, deep-path fallback, and unknown targets)
## Context
Auditing the CLI surfaced that `composio orgs --help` rendered a
completely different page from `composio config --help`: unstyled
headers, a different section layout, and the root-level `--log-level`
flag exposed on a subcommand page. Root cause: `root-help.ts`'s
`SUBCOMMAND_HELP` registry — which drives the curated `--help` pages —
was missing those commands, so they fell through `matchSubcommandHelp`
to v4's default parser rendering. The new consistency test walks the
visible root command graph and locks this class shut; it caught `signup`
and `agent` during development.
Review follow-ups (from code review + prior feedback):
- the `agent` family now has per-command entries, so `composio agent
signup --help` and `composio help agent signup` show signup's own flags
instead of the group page / an "Unknown command" line
- `composio help <unknown>` no longer prints "Unknown command" to stdout
with exit 0; it fails through the framework parser like every other
unknown command, so scripted probes and the stdout data channel stay
honest
- the `help` spelling resolves deep paths with the same longest-prefix
fallback `--help` uses
- the two rendering tests now actually execute (`layer(TestLive())` +
`it.effect`) — previously they returned a bare `Effect` from a plain
`it` and passed vacuously
- the changelog no longer lists `tools` as newly curated (its entry
already existed at the base of this PR)
Guidance-only surface: no parsing, execution, or exit-code behavior
changes beyond the `help` spelling itself — help pages and the new
`help` command only.
Validation:
- `pnpm --filter @composio/cli typecheck` and full suite: 1342 passed
(16 new tests, all executing under `layer(TestLive())`)
- oxlint and prettier clean
- binary smoke-tested: orgs/signup/agent (group +
children)/connections/triggers/tools/artifacts/install help pages, all
`composio help` paths (bare, family, child, level, deep-path, unknown ->
parser error on stderr), and `whoami` against the staging API
Built on top of the merged #3901.
## Summary
Live PostHog (2026-09-15): since auto-setup was restored on Sep 14, 704
installer-triggered `composio setup` runs found no host 397 times (56%);
of the 316 that found one, 282 installed the plugin (89%). The gap is
host detection, and today a real absence is indistinguishable from a
PATH miss. Manual `composio setup` over 30 days: 888 succeeded, 626
failed, and `CLI_SETUP_FAILED` only carried `error_name`. 103 of those
failures (`yes=false, target=auto, stdout_is_tty=false`) are agents
following the daily hint text into "Non-interactive setup requires
`--yes`". No event said whether the CLI was running inside Claude Code
or Codex at all.
Event contract (metrics.composio.io is being built against these names):
- Every CLI event gains `agent_host_env: 'claude' | 'codex' | 'none'`,
derived from `CLAUDECODE` / `CODEX_THREAD_ID` / `CODEX_SANDBOX`.
- `CLI_SETUP_HOST_DETECTED` gains `host_config_dir_present` and
`host_binary_in_known_paths` when `available=false`
(`$CLAUDE_CONFIG_DIR`/`~/.claude`, `$CODEX_HOME`/`~/.codex`;
`~/.claude/local`, `~/.local/bin`, `~/.npm-global/bin`,
`/usr/local/bin`, `/opt/homebrew/bin`). Both omitted when the host is
detected.
- New `CLI_PLUGIN_HINT_SHOWN` (journey stage `setup`) with `source`,
`invocation_origin`, `cli_version`, `command_path`, `agent_host`,
emitted once per printed hint and never on suppression.
- `CLI_SETUP_FAILED` gains `failure_reason_code`:
`all_requires_both_hosts | unsupported_host | target_not_installed |
no_host_detected | non_interactive_requires_yes | marketplace_conflict |
unknown`, carried on `SetupCommandError.reasonCode`.
- Hint text is now `Tip: running under <host> without the Composio
plugin — 'composio setup --yes' installs it.`
Structural notes: `agent_host_env` is stamped in `trackCliEventEffect`
(`analytics/dispatch.ts`) next to `org_id`, so every enqueued envelope
carries it with no module state or bootstrap hook. `SetupCommandError`
and `SetupFailureReasonCode` live in the leaf module
`services/setup-command-error.ts` (imports only `effect`) so
`analytics/events.ts` can use `instanceof` without a cycle;
`setup.cmd.ts` keeps its original `setupCommandError` helper with the
reason code as a third argument, and the two validate-stage failures in
`services/setup.ts` (`marketplace_conflict`, `target_not_installed`) are
raised as `SetupCommandError` directly. The raw host-env read,
`detectPluginHost`, and the known-path install probe live in
`services/agent-host-env.ts`, shared by `dispatch.ts`, `plugin-hint.ts`,
and `setup.ts`. `CLI_SETUP_HOST_DETECTED` passes the two presence
booleans straight through; `setup.ts` only probes an undetected host.
The hint tracks `CLI_PLUGIN_HINT_SHOWN` right where it prints.
## Validation
- `pnpm --filter @composio/cli` `pnpm run test` (validate:skills,
validate:boundaries, vitest): 132 files, 1374 passed, 1 skipped.
- `pnpm run typecheck` (src + test): clean.
- `oxlint` on the 14 changed TS files: clean. `prettier --check` on
changed files: clean. `git diff --check`: clean.
- `pnpm validate:agent-skills` and `pnpm validate:skill-routing`: pass
(skill reference doc changed).
- Manual, built binary with isolated
`HOME`/`COMPOSIO_CACHE_DIR`/`CLAUDE_CONFIG_DIR` and a dummy PostHog key
pointed at a dead local port: `CLAUDECODE=1 composio whoami
--telemetry-debug` printed the new hint once and enqueued
`CLI_PLUGIN_HINT_SHOWN` (`command_path: whoami`, `agent_host: claude`)
plus `CLI_COMMAND_INVOKED`/`SUCCEEDED`, all with `agent_host_env:
claude`; a second run printed no hint and no hint event.
`PATH=/usr/bin:/bin composio setup --target codex --yes
--telemetry-debug` enqueued `CLI_SETUP_HOST_DETECTED` with `available:
false, host_config_dir_present: true, host_binary_in_known_paths: false`
and `CLI_SETUP_FAILED` with `failure_reason_code: target_not_installed`.
Re-run after each simplification pass with identical output; with the
host markers unset the same events carry `agent_host_env: none`.
## Known verification limitations
- No changeset: `@composio/cli` is ignored by Changesets per
`ts/AGENTS.md`; the note went into `ts/packages/cli/CHANGELOG.md`
instead.
- Docker CLI E2E not run; no binary output contract changed except the
hint line.
- `host_binary_in_known_paths` checks two absolute directories, so the
"absent everywhere" test asserts a boolean rather than `false` to stay
machine-independent.
Not included: any change to `composio setup` help text or to the
dashboard side.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The setup-plugins suite pins the recovery command printed when native
plugin inspection fails. setup remediation messages now carry --yes so
non-interactive agents get a command that completes, so the pinned
'composio setup --target claude' fragment became
'composio setup --yes --target claude'. Matches the exact stderr CI
reported on the failing scratch run.
validateInitialState hardcodes operation 'setup' in its SetupCommandError
while its only caller, the exported inspectSetupTargets, already threads
options.operation for wording. Uninstall skips validation today
(allowMarketplaceConflict: uninstall), so nothing mislabels now, but any
future uninstall caller that validates would get operation 'setup' and the
wrong failure_reason_code in telemetry. Pass options.operation ?? 'setup'
through and use it in both constructions.
The plugin hint learned to say 'composio setup --yes', but the error
messages an agent hits next still suggested bare reruns that deterministically
fail again with non_interactive_requires_yes in a non-TTY shell - the same
103-failure loop this PR set out to fix. Add --yes to the targeted-missing and
no-host messages in setup.cmd, the validate-stage target/marketplace messages,
the verify-stage messages, and the shared recovery-hint command; the most
conflicting case also walked agents through the destructive marketplace
remove before a rerun that could not succeed.
Interactive users can drop the flag; agents get a command that actually
completes. Adds rerun-message assertions for the three reachable failure
paths.
showPluginHint claims the 24h stamp before printing, so a print that did
not succeed left the stamp standing: the hint was never delivered yet both
the hint and CLI_PLUGIN_HINT_SHOWN stayed muted for the full interval.
Capture the print with Effect.exit and remove the just-created stamp on
any non-success exit so the next invocation can retry.
Tracking stays best-effort by contract: trackCliEventEffect never fails,
so a delivered hint with a lost event is accepted and does not un-claim.
resolvePluginHintConfig inlined the same env-override-else-~/.claude and
~/.codex fallback expressions that agent-host-env.ts now owns, leaving the
host-dir default written three times across two files. Use the shared
helper for both file paths and drop the now-unused NodeOs yield.
The 'preserves nonblank path overrides' test asserted verbatim pass-through
of relative overrides, which was the cwd dependence the probe hardening
removes. It now pins absolute overrides preserved as-is plus a new case
asserting relative overrides anchor to the home directory.
CLAUDE_CONFIG_DIR / CODEX_HOME were passed verbatim to the install probe,
so a relative override resolved against the process cwd (making
host_config_dir_present depend on where composio was invoked) and a plain
file at the config path counted as 'present'. Resolve overrides against
the home directory - matching how the known-binary list already resolves -
and probe with stat so presence means an actual directory. Unreadable
paths report false instead of failing the probe.
rawHostEnvironment re-implemented by hand what src/services/config.ts already
provides: a fresh unprefixed ConfigProvider.fromEnv() per execution with
orDie error handling. Build the five host keys as one Config.all and load
them via loadHostConfig, deleting the bespoke readOptionalEnv helper and the
per-call provideServiceEffect override. The fromEnv snapshot rationale now
lives only in config.ts's documented getBaseConfigProvider.
Behavior is unchanged: same live-env semantics (vi.stubEnv stays observable),
same blank-marker handling, same orDie on config failures.
This PR:
- builds on top of https://github.com/ComposioHQ/composio/pull/4475
- stores `composio execute` responses over 40,000 UTF-8 bytes in a
session file, replacing the check for more than 10,000 o200k tokens
added in https://github.com/ComposioHQ/composio/pull/2979
- keeps `tokenCount` in the stored-output summary as an estimate
(`ceil(sizeBytes / 4)`), adds `sizeBytes`, and logs `Response stored in
<path> (N KB, ~M tokens)`
- removes `js-tiktoken` and the `execute-output-encoder-runtime`
companion from https://github.com/ComposioHQ/composio/pull/4469, with
its tsdown entry, build-guard and startup-import patterns, upgrade
fixture entry, and the encoder fallback and special-token tests
- keeps `execute-output-encoder-runtime.mjs` in the three uninstall
lists, so installs that shipped it can still remove the leftover file
- behavior change: a 10–40KB response of more than 10,000 tokens now
prints inline, and dense non-ASCII output (about 1–2 bytes per token)
can print inline at up to ~20,000 tokens
- verified: CLI typecheck, full CLI vitest (131 files, 1343 passed, 1
skipped), `test/release-workflow.test.ts`, and
`build-companion-modules.ts` with the executable graph check
## Context
Nothing reads `tokenCount` as a number: it appears only in the
stored-output summary, the two log lines, session history, and a debug
log in `run-helpers-runtime.ts`. o200k is not the tokenizer of the model
that reads the output, so the exact count only moved the cutoff. It cost
the 2.3MB rank table, ~330ms after large tool calls (measured in #4469,
not re-measured here), and a separately shipped companion whose absence
made `composio run` require a repair download after an upgrade.
Check each curated page against its own output, cover experimental commands in the registry check, and assert exit status and stderr for unknown help targets.
Drop the `help` token and redundant --help/-h flags before routing, so unknown targets get the framework's "Did you mean?" suggestion and `composio help --help` renders the curated root page. Document the `install` flags.
## Summary
`PusherService.subscribe` binds `pusher:subscription_error` after the
Pusher subscription call returns. `pusher-js` dispatches this event
asynchronously without catching listener exceptions, so authentication,
permission, server, or network subscription failures could escape as
uncaught exceptions in Node applications.
Fixes#4445
## Changes
- Log asynchronous Pusher subscription errors at the SDK error boundary
instead of throwing from the event callback.
- Add regression coverage that emits `pusher:subscription_error` after
`subscribe()` resolves and verifies that it does not throw.
- Add a patch changeset for the fixed `@composio/core`/`@composio/slim`
package group.
## Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Refactor/Chore
- [ ] Documentation
- [ ] Breaking change
## How Has This Been Tested?
- Node `v24.17.0` / pnpm `11.8.0`
- `pnpm --filter @composio/core exec vitest run
test/services/pusher.test.ts test/utils/pusher.test.ts` — 2 files, 5
tests passed
- `pnpm --filter @composio/core test` — 55 test files passed; 1,289
tests passed and 2 existing tests reported expected failures; command
exited successfully
- `pnpm --filter @composio/core typecheck`
- `pnpm lint` — passed with existing repository warnings
- `pnpm validate:changesets`
- `pusher-js` `v8.6.0` runtime probe confirmed that an exception thrown
from a `pusher:subscription_error` listener reaches Node's
`uncaughtException` handler; the regression test verifies the SDK
callback no longer throws.
## Screenshots (if applicable)
Not applicable.
## Checklist
- [x] I have read the Code of Conduct and this PR adheres to it
- [x] I ran linters/tests locally and they passed
- [x] I updated documentation as needed
- [x] I added tests or explain why not applicable
- [x] I added a changeset if this change affects published packages
## Additional context
This patch is intentionally limited to the live `PusherService` path.
XHR timeout handling is a separate concern and is not included here. The
older unreferenced `PusherUtils` helper is unchanged to keep this fix
scoped to the path used by `Triggers`.
trackCliEventEffect reads the host environment and adds agent_host_env to
every enqueued envelope, so there is no module-level state and no bootstrap
hook. SetupCommandError lives in a leaf module so analytics/events.ts can
use instanceof without a cycle, setup.cmd.ts keeps its original helper
shape with the reason code as a third argument, and plugin-hint.ts keeps
its original config resolution. Event names and properties are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keep SetupCommandError in services/setup.ts with its reasonCode, read the
code structurally in analytics/events.ts, raise the two validate-stage
failures as SetupCommandError directly instead of a second reason-code
layer, fold the host install probe into agent-host-env.ts, pass the host
presence booleans straight through on CLI_SETUP_HOST_DETECTED, and track
the plugin hint where it is printed. Event names and properties are
unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every CLI event now carries agent_host_env (claude, codex, none) read from
the host environment. CLI_SETUP_HOST_DETECTED reports whether the host's
config dir and a binary at a known install location exist when the host is
not detected, CLI_SETUP_FAILED carries a failure_reason_code read from
SetupCommandError, and the daily plugin hint emits CLI_PLUGIN_HINT_SHOWN
once per printed line and points at `composio setup --yes` so agents no
longer run into the non-interactive --yes failure.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The containment around the subscription error callback only caught
synchronous throws. TypeScript accepts an async function against a
void-returning callback type, so a rejected promise escaped as an
unhandled rejection after subscribe() resolved - on modern Node that
can terminate the process, the same failure class this PR contains.
Route the callback result through Promise.resolve(...).catch so both
sync throws and async rejections land in the same contained logger
path, and add a regression test for the async case.
Addresses greptile-apps P1 and Cursor Bugbot review comments.
The handleAssistantMessage, waitAndHandleAssistantStreamToolCalls, and
waitAndHandleAssistantToolCalls methods target the OpenAI Assistants
API, which shuts down on August 26, 2026. Add a deprecation warning
pointing new flows at OpenAIResponsesProvider.
- Use gpt-5 in the Responses API examples; gpt-4 predates the Responses
API and the repo's other Responses examples use gpt-5.
- Print response.output_text instead of indexing into content items,
which assumes non-empty message content.
- Add the OpenAIResponsesProvider type surface to the Type Definitions
section, which previously only showed the chat completions provider.
- PusherService.subscribe and Triggers.subscribe accept an optional
onSubscriptionError callback invoked with the raw pusher
pusher:subscription_error payload, giving hosts a programmatic signal
for post-resolution subscription failures (previously log-only).
- Exceptions thrown from the callback are contained and logged, never
rethrown, so a faulty handler cannot crash the host.
- The parameter is optional; existing callers are unaffected.
- Document the new parameter in the TypeScript triggers reference and
the subscribing-to-events guide; bump the changeset to minor for the
new API surface.
Applies review finding #1 from the PR #4448 review.
- Log the full pusher:subscription_error payload (type, error, status)
instead of a flattened String(data.error) so operators can tell auth
failures from permission failures.
- Log the subscription success message only when Pusher dispatches
pusher:subscription_succeeded, not when subscribe() returns.
- Extend regression coverage for both behaviors.
Applies review findings #2 and #3 from the PR #4448 review.
Replacing the pass-through with an eager projection changed the failure mode for
an auth config detail that omits a field group. Passing the group through left
`undefined` in place, which zod rejected as a handled validation error;
`transformToolkitAuthFieldGroup` instead reads `group.required`, so the same
response now throws a `TypeError` before validation runs and crashes
`toolkits.get()`.
Default a missing or null group, and a missing list inside a group, to empty
lists. This repo's docs pipeline already assumes that shape: the schemas in
`docs/lib/toolkit-api.ts` carry `.catch({ required: [], optional: [] })` on the
same fields. Normalizing also keeps the other group usable when only one is
absent, which is typically the one the caller asked for, where a validation
error would have returned nothing.
Reported by greptile-apps on #4411.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- listen: replace `Effect.catchAll`, which no longer exists in
effect@4.0.0-rc.112, with `Effect.result` and narrow the lookup failure
with `NotFoundError` from @composio/client instead of a structural
status check.
- tests: `it.scoped` is gone from @effect/vitest v4 (`it.effect` already
provides a Scope); switch the new cases over and assert the exit code on
`triggers info` too, so the shared handler's contract is covered by more
than one command.
- handle-http-error: also print the hint plainly when stderr is not a TTY.
- changelog: record the four user-visible fixes.
Claude-Session: https://claude.ai/code/session_01SvfF1wniMJsBDo5PaBjm2Y
Whether `total_items` honors the `statuses` filter is a server detail. A
count that did not would make the shared list look truncated on every call
and quietly route the account picker onto the second request forever. Take
a page as complete when it is shorter than the requested size and carries
no cursor, and cover both the short-page and full-page cases.
Claude-Session: https://claude.ai/code/session_01MTb47vexN35pLGsSZwBmrJ
The derived toolkit filter lower-cased only, while the grouping helpers in
the same module trim first and the fallback sent the raw slug to the server.
A padded `--toolkit` value could therefore match or miss depending on how
many accounts the user has. Trim once, compare with the shared normalizer,
and send the trimmed slug on the fallback. Drop the optional chaining on
fields the client types as required.
Claude-Session: https://claude.ai/code/session_01MTb47vexN35pLGsSZwBmrJ
`resolveCommandProject` gives `composio execute` the consumer project's
nano id, while the cache refresh asked `getFor` for a client keyed on the
long id. Two clients meant two memo keys, so the connected-account list
was still fetched twice per consumer execute. Use the same id on both.
Claude-Session: https://claude.ai/code/session_01MTb47vexN35pLGsSZwBmrJ
The memoized `get_latest_version` is keyed on slug, org, and project. The
command's version check passed the resolved project while the executor's
schema lookup passed nothing, so the two never shared a key and
`composio execute` still issued both requests. Hand the executor the
project scope the command resolved.
Claude-Session: https://claude.ai/code/session_01MTb47vexN35pLGsSZwBmrJ
The user context is live state that `login` fills in during the same
process. Memoizing the `null` answered before that would skip the version
check for that tool for the rest of the run. Resolve the key outside the
memo and cache only real lookups.
Claude-Session: https://claude.ai/code/session_01MTb47vexN35pLGsSZwBmrJ
A memoized tool version or connected-account list lives for the whole
process, so within one vitest file the first case that lets the lookup
succeed would hand its answer to every later case sharing the key. Expose
`clear()` on each memo plus a process-wide `clearInProcessMemos`, and call
the latter from the shared vitest setup before every test.
Claude-Session: https://claude.ai/code/session_01MTb47vexN35pLGsSZwBmrJ
`Effect.cached` makes the first caller the owner of the shared run, so
interrupting that caller stores the interrupt in the cell and replays it to
every fiber already waiting on it. On `composio execute --parallel` two specs
sharing a slug could take the second one down when the first one's
`Effect.all` failed on the tool-detail fetch.
Run the memoized effect on a detached fiber that completes a Deferred, so a
caller's interruption cancels only its own wait.
Claude-Session: https://claude.ai/code/session_01MTb47vexN35pLGsSZwBmrJ
memoizeInProcess evicted a key only on typed failures, but Effect.cached stores
the whole Exit, so a defect or an interrupted first caller was replayed for the
rest of the process. Evict on any failure cause.
ActiveConnectedAccountsListError was only ever unwrapped to its cause by both
callers, so the shared list now fails with the raw rejection.
`composio execute` listed the user's connected accounts twice on every
call: once toolkit-filtered by the account picker, once unfiltered by
session creation. The two code paths cannot see each other. The
unfiltered list is now fetched once per process and shared, and the
picker derives its toolkit subset from it: same slug match, server order
kept, first 100, exactly what its own query returned. If the shared list
is truncated (more active accounts than one page), the picker falls back
to its original request, so results are identical in every case.
Interleaved A/B against the parent commit, compiled binaries, 15 runs
each on a small response: 1735 -> 1636ms best, 1934 -> 1845ms median.
One fewer request on the critical path, ~140ms.
`memoizeInProcess` memoizes an Effect per key for the process lifetime,
shares one run between concurrent callers, and drops failures so the next
caller retries. It also covers `get_latest_version`, which the definition
refresh fetched twice with the same key on the stale path. The executor's
own version lookup stays unscoped and separate; scoping it to org and
project would change which definition it resolves, so that is left as is.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wx9gEjuiHux2weiHjdNcDs
`composio execute` stored a response in a session file once js-tiktoken
counted more than 10,000 o200k tokens. Nothing reads that count as a number,
and o200k is not the tokenizer of the model reading the output, so the exact
count only moved the cutoff while costing the 2.3MB rank table, ~330ms after
large tool calls, and a separately shipped companion file with its own repair
path.
Responses over 40,000 UTF-8 bytes are stored now. The summary keeps
`tokenCount`, now estimated at four bytes per token, and gains `sizeBytes`.
The execute-output-encoder-runtime companion, js-tiktoken, and their build,
packaging and test entries are removed. The uninstall lists keep the
companion's file name so installs that shipped it can still remove it.
Bun keeps a failed or already-loaded import in its module registry, so
importing a companion again after a repair can still see the missing
dependency or the old module. Repair now runs before the single import, and
only when the requested companion's wrapper or its import graph is missing,
so unrelated missing companions still cannot block it.
Claude-Session: https://claude.ai/code/session_01MTb47vexN35pLGsSZwBmrJ
- Import an in-process companion before checking the rest of the set, so a
missing unrelated companion cannot fail generate or run through an offline
repair. Repair runs only when the requested module fails to load.
- Check each companion's required exports, so a file left by another release
fails with a typed reinstall error instead of calling a missing export.
- When the tokenizer cannot load, store any response past the byte pre-filter.
The four-bytes-per-token estimate can undercount, so it no longer keeps a
response inline.
- Bundle the executable graph check with the release build's env inlining,
defines, NODE_ENV and syntax minification.
Claude-Session: https://claude.ai/code/session_01MTb47vexN35pLGsSZwBmrJ