Skip to content

perf(inbound): trim cold startup import graph#52082

Merged
vincentkoc merged 7 commits intomainfrom
fix/inbound-auth-store-churn
Mar 22, 2026
Merged

perf(inbound): trim cold startup import graph#52082
vincentkoc merged 7 commits intomainfrom
fix/inbound-auth-store-churn

Conversation

@vincentkoc
Copy link
Copy Markdown
Member

Summary

This is a checkpoint PR for the current inbound cold-start reduction work.

It trims several heavy static imports off the inbound reply path by splitting hot read paths from heavier runtime/write/discovery paths.

What changed

  • trimmed session-maintenance warning routing off infra/outbound/targets.ts
  • removed Discord session-key normalization's dependency on broad plugin-sdk barrels
  • moved session forking behind session-fork.runtime.ts
  • split lightweight abort text/memory helpers from heavy abort orchestration
  • split skill command parsing/reserved-name helpers from workspace/agent skill discovery
  • split directive default-model helpers from directive persistence
  • split directive model selection/profile resolution from heavy /model info UI path
  • moved several rare execution paths behind runtime boundaries:
    • commands-core.runtime.ts
    • commands.runtime.ts
    • openclaw-tools.runtime.ts
    • abort-cutoff.runtime.ts

Measured impact

Representative cold import numbers from local node --expose-gc --import tsx probes:

  • src/infra/session-maintenance-warning.ts
    • about 9.9s / 169MB -> about 122ms / 1.9MB
  • extensions/discord/session-key-api.ts
    • about 5.5s / 169MB -> about 313ms / ~0MB
  • src/config/sessions/store.ts
    • now about 514ms / 55MB
  • src/auto-reply/reply/session.ts
    • now about 563ms / 56MB
  • src/auto-reply/reply/get-reply-inline-actions.ts
    • now about 335ms / 2.7MB
  • src/auto-reply/reply/abort-cutoff.ts
    • now about 302ms / ~0MB
  • src/auto-reply/reply/directive-handling.persist.ts
    • down from the old ~7-13s / ~160MB class to about 2.16s / 59MB

Validation

  • pnpm tsgo

Known remaining hotspot

The next measured long pole is the actual agent execution stack:

  • src/auto-reply/reply/agent-runner.ts: about 4.1s / 161.5MB
  • src/agents/pi-embedded.ts: about 4.35s / 161.6MB

That stack is pulled in by src/auto-reply/reply/get-reply-run.ts, so the next follow-up should move runReplyAgent behind a runtime boundary and continue re-measuring from there.

@vincentkoc vincentkoc self-assigned this Mar 22, 2026
@openclaw-barnacle openclaw-barnacle bot added channel: discord Channel integration: discord gateway Gateway runtime agents Agent runtime and tooling size: XL maintainer Maintainer-authored PR labels Mar 22, 2026
@vincentkoc vincentkoc force-pushed the fix/inbound-auth-store-churn branch from 8908ebe to ab5783c Compare March 22, 2026 04:45
@vincentkoc vincentkoc force-pushed the fix/inbound-auth-store-churn branch from ab5783c to b434dd5 Compare March 22, 2026 04:47
@vincentkoc vincentkoc marked this pull request as ready for review March 22, 2026 04:48
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR is a well-measured checkpoint for cold-start import graph trimming on the inbound reply path. It systematically splits monolithic modules into lightweight primitive/base files and heavier runtime files, then converts the rare hot-path consumers to lazy dynamic import() calls. The measured improvements (e.g. session-maintenance-warning.ts dropping from ~9.9 s / 169 MB to ~122 ms / 1.9 MB) are significant and the approach is architecturally sound.

Key changes:

  • New primitive modules (abort-primitives.ts, skill-commands-base.ts, context-cache.ts, directive-handling.defaults.ts, directive-handling.auth-profile.ts, directive-handling.model-selection.ts) extract lightweight helpers so hot read paths no longer drag in heavy orchestration code.
  • New .runtime.ts re-export boundaries (abort-cutoff.runtime.ts, session-fork.runtime.ts, commands-core.runtime.ts, commands.runtime.ts, openclaw-tools.runtime.ts, skill-commands.runtime.ts) gate the expensive modules behind dynamic import() at call sites.
  • session-fork.ts is correctly made async and the sole call site in session.ts is properly await-ed.
  • store-maintenance.ts switches from the config.ts barrel to io.ts directly (same exported function, lighter import graph).
  • session-maintenance-warning.ts inlines a lightweight resolveWarningDeliveryTarget to avoid pulling in the heavy outbound/targets.ts.

Notable observations:

  • The inlined normalizeDiscordChatType in session-key-normalization.ts is verified to be behaviorally identical to the original normalizeChatType in src/channels/chat-type.ts.
  • directive-handling.model.ts still retains its own independent copy of resolveModelSelectionFromDirective, while production code now uses the new copy in directive-handling.model-selection.ts. The test suite tests the old copy — consider converting the old definition to a re-export to keep tests exercising the live production path.
  • The import type { buildStatusReply, handleCommands } statement in get-reply-inline-actions.ts is redundant (those names are only consumed via dynamic import, where TypeScript infers the types automatically).

Confidence Score: 4/5

  • Safe to merge; all call-site migrations are correct and the behavioral equivalence of inlined helpers is verified. One test-coverage gap remains (tests exercise old resolveModelSelectionFromDirective in model.ts, not the new copy in model-selection.ts).
  • The import-graph splits are sound, the sync→async promotion in session-fork.ts is handled at the single call site, and the key inlined logic (normalizeDiscordChatType, resolveWarningDeliveryTarget) is verified to be equivalent. A 4 rather than 5 reflects the outstanding duplication of resolveModelSelectionFromDirective across two files where the test suite now tests a dead copy rather than the production path — a minor but real gap that's cheap to fix before or after merging.
  • src/auto-reply/reply/directive-handling.model.ts — retains a full independent copy of resolveModelSelectionFromDirective that is no longer used by production code but is still tested. Should be converted to a re-export from directive-handling.model-selection.ts.

Comments Outside Diff (1)

  1. src/auto-reply/reply/directive-handling.model.ts, line 393-407 (link)

    P2 Duplicate resolveModelSelectionFromDirective not covered by tests

    directive-handling.model.ts still contains its own full copy of resolveModelSelectionFromDirective, while production call sites in directive-handling.persist.ts and directive-handling.impl.ts now import the extracted version from directive-handling.model-selection.ts. The test file directive-handling.model.test.ts still imports from ./directive-handling.model.js, so tests exercise the old copy while the production path runs the new one.

    If the two implementations ever diverge, test failures won't surface the regression. Consider having directive-handling.model.ts re-export from directive-handling.model-selection.ts instead of keeping an independent copy, and update the test imports accordingly:

    // directive-handling.model.ts
    export { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js";
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/auto-reply/reply/directive-handling.model.ts
    Line: 393-407
    
    Comment:
    **Duplicate `resolveModelSelectionFromDirective` not covered by tests**
    
    `directive-handling.model.ts` still contains its own full copy of `resolveModelSelectionFromDirective`, while production call sites in `directive-handling.persist.ts` and `directive-handling.impl.ts` now import the extracted version from `directive-handling.model-selection.ts`. The test file `directive-handling.model.test.ts` still imports from `./directive-handling.model.js`, so tests exercise the old copy while the production path runs the new one.
    
    If the two implementations ever diverge, test failures won't surface the regression. Consider having `directive-handling.model.ts` re-export from `directive-handling.model-selection.ts` instead of keeping an independent copy, and update the test imports accordingly:
    
    ```ts
    // directive-handling.model.ts
    export { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js";
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/directive-handling.model.ts
Line: 393-407

Comment:
**Duplicate `resolveModelSelectionFromDirective` not covered by tests**

`directive-handling.model.ts` still contains its own full copy of `resolveModelSelectionFromDirective`, while production call sites in `directive-handling.persist.ts` and `directive-handling.impl.ts` now import the extracted version from `directive-handling.model-selection.ts`. The test file `directive-handling.model.test.ts` still imports from `./directive-handling.model.js`, so tests exercise the old copy while the production path runs the new one.

If the two implementations ever diverge, test failures won't surface the regression. Consider having `directive-handling.model.ts` re-export from `directive-handling.model-selection.ts` instead of keeping an independent copy, and update the test imports accordingly:

```ts
// directive-handling.model.ts
export { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js";
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/auto-reply/reply/get-reply-inline-actions.ts
Line: 24

Comment:
**Redundant `import type` for dynamically-loaded functions**

`buildStatusReply` and `handleCommands` are imported only as types here, but neither is used as a type annotation elsewhere in the file — they're consumed exclusively via dynamic `import()` calls to `commands.runtime.js`. TypeScript infers the correct types from those dynamic imports without this static declaration. This line can be safely removed:

```suggestion
```

(Delete the line entirely — no replacement needed.)

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "perf(inbound): trim ..."

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 22, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium IDOR / missing authorization: chat /model directive can select stored auth profile via guessable 8-digit @<id> suffix

1. 🟡 IDOR / missing authorization: chat /model directive can select stored auth profile via guessable 8-digit @<id> suffix

Property Value
Severity Medium
CWE CWE-639
Location src/auto-reply/reply/directive-handling.model-selection.ts:25-156

Description

The new /model directive handling infers an auth profile override when the model directive ends with an 8-digit numeric suffix (e.g. /model openai/gpt-4o@​20251001).

  • Input: params.directives.rawModelDirective comes from chat user text (/model ...).
  • Auth-profile lookup: resolveStoredNumericProfileModelDirective() loads the local auth-profiles.json store and checks store.profiles[profileId] for an 8-digit ID.
  • No authorization: if the profile exists (and provider matches), the code sets profileOverride without verifying the sender is allowed to use that auth profile (e.g., owner-only), enabling credential/profile switching by ID.
  • Impact: any chat user who is allowed to run /model directives (which may include non-owners depending on channel/group config) can switch the agent to a different stored credential profile, causing potential billing abuse or access to more privileged provider credentials.

Vulnerable code (inferred profile override by numeric ID):

const profileId = trimmed.slice(profileDelimiter + 1).trim();
if (!/^\d{8}$/.test(profileId)) return null;

const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
const profile = store.profiles[profileId];
if (!profile) return null;

return { modelRaw, profileId, profileProvider: profile.provider };

And later applying that inferred ID as profileOverride:

const rawProfile = params.directives.rawModelProfile ??
  (useStoredNumericProfile ? storedNumericProfile?.profileId : undefined);
...
profileOverride = profileResolved.profileId;

Note: This also unintentionally allows bypassing the “numeric model selection not supported in chat” guard by using a non-digit string plus @<8digits> (e.g. 2@​20251001), though the larger risk is profile selection.

Recommendation

Require explicit authorization for auth-profile overrides, and/or remove the implicit numeric-profile inference.

Recommended hardening options:

  1. Owner-only profile overrides: Only allow rawModelProfile / inferred profile usage when the sender is an owner/admin (or when a dedicated config flag is enabled).

  2. Disable numeric inference by default: Treat @​YYYYMMDD as part of the model unless the user explicitly provides a separate profile override token.

Example (conceptual) fix: pass sender auth context into model selection and block profile overrides for non-owners:

export function resolveModelSelectionFromDirective(params: {
  directives: InlineDirectives;
  ...
  senderIsOwner: boolean;
}): ... {
  ...
  const requestedProfile = params.directives.rawModelProfile ??
    (params.senderIsOwner && useStoredNumericProfile ? storedNumericProfile?.profileId : undefined);

  if (requestedProfile && !params.senderIsOwner) {
    return { errorText: "Auth profile overrides are restricted to owners." };
  }
  ...
}

Additionally, if numeric IDs must be supported, consider using non-guessable profile identifiers (e.g., provider:<uuid>), and avoid allowing selection of profiles not explicitly allowlisted for chat override.


Analyzed PR: #52082 at commit 9148c41

Last updated on: 2026-03-22T06:19:41Z

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f175b6bf70

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@vincentkoc vincentkoc merged commit 041f0b8 into main Mar 22, 2026
35 of 36 checks passed
@vincentkoc vincentkoc deleted the fix/inbound-auth-store-churn branch March 22, 2026 05:32
aquaright1 pushed a commit to aquaright1/openclaw that referenced this pull request Mar 22, 2026
* perf(inbound): trim cold startup import graph

* chore(reply): drop redundant inline action type import

* fix(inbound): restore warning and maintenance seams

* fix(reply): restore type seam and secure forked transcripts
JohnJAS pushed a commit to JohnJAS/openclaw that referenced this pull request Mar 22, 2026
* perf(inbound): trim cold startup import graph

* chore(reply): drop redundant inline action type import

* fix(inbound): restore warning and maintenance seams

* fix(reply): restore type seam and secure forked transcripts
pholpaphankorn pushed a commit to pholpaphankorn/openclaw that referenced this pull request Mar 22, 2026
* perf(inbound): trim cold startup import graph

* chore(reply): drop redundant inline action type import

* fix(inbound): restore warning and maintenance seams

* fix(reply): restore type seam and secure forked transcripts
MaheshBhushan pushed a commit to MaheshBhushan/openclaw that referenced this pull request Mar 22, 2026
* perf(inbound): trim cold startup import graph

* chore(reply): drop redundant inline action type import

* fix(inbound): restore warning and maintenance seams

* fix(reply): restore type seam and secure forked transcripts
frankekn pushed a commit to artwalker/openclaw that referenced this pull request Mar 23, 2026
* perf(inbound): trim cold startup import graph

* chore(reply): drop redundant inline action type import

* fix(inbound): restore warning and maintenance seams

* fix(reply): restore type seam and secure forked transcripts
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 24, 2026
* perf(inbound): trim cold startup import graph

* chore(reply): drop redundant inline action type import

* fix(inbound): restore warning and maintenance seams

* fix(reply): restore type seam and secure forked transcripts

(cherry picked from commit 041f0b8)
alexey-pelykh added a commit to remoteclaw/remoteclaw that referenced this pull request Mar 24, 2026
* refactor: add channel-pairing plugin-sdk module — prerequisite for upstream discord adapter batch

* refactor: tighten plugin sdk channel surfaces

(cherry picked from commit 002cc07)

* refactor: split remaining monitor runtime helpers

(cherry picked from commit 005b25e)

* perf(inbound): trim cold startup import graph (openclaw#52082)

* perf(inbound): trim cold startup import graph

* chore(reply): drop redundant inline action type import

* fix(inbound): restore warning and maintenance seams

* fix(reply): restore type seam and secure forked transcripts

(cherry picked from commit 041f0b8)

* Discord: consolidate message tool discovery

(cherry picked from commit 0a0ca80)

* Discord: move group policy behind plugin boundary

(cherry picked from commit 0bfaa36)

* Plugins: move config-backed directories behind channel plugins

(cherry picked from commit 2b5fa09)

* perf: reduce plugin runtime startup overhead

(cherry picked from commit 3382ef2)

* Channels: centralize outbound payload contracts

(cherry picked from commit 4aae0d4)

* Discord: own message tool components schema

(cherry picked from commit 05634ee)

* Plugins: remove first-party legacy message discovery shims

(cherry picked from commit 1c6676c)

* refactor: move Discord channel implementation to extensions/ (openclaw#45660)

* refactor: move Discord channel implementation to extensions/discord/src/

Move all Discord source files from src/discord/ to extensions/discord/src/,
following the extension migration pattern. Source files in src/discord/ are
replaced with re-export shims. Channel-plugin files from
src/channels/plugins/*/discord* are similarly moved and shimmed.

- Copy all .ts source files preserving subdirectory structure (monitor/, voice/)
- Move channel-plugin files (actions, normalize, onboarding, outbound, status-issues)
- Fix all relative imports to use correct paths from new location
- Create re-export shims at original locations for backward compatibility
- Delete test files from shim locations (tests live in extension now)
- Update tsconfig.plugin-sdk.dts.json rootDir from "src" to "." to accommodate
  extension files outside src/
- Update write-plugin-sdk-entry-dts.ts to match new declaration output paths

* fix: add importOriginal to thread-bindings session-meta mock for extensions test

* style: fix formatting in thread-bindings lifecycle test

(cherry picked from commit 5682ec3)

* refactor: route extension seams through public apis

(cherry picked from commit 58cf9b8)

* fix: gate setup-only plugin side effects

(cherry picked from commit 59bcac4)

* refactor: simplify tlon and discord setup accounts

(cherry picked from commit 6fa0027)

* refactor: make OutboundSendDeps dynamic with channel-ID keys (openclaw#45517)

* refactor: make OutboundSendDeps dynamic with channel-ID keys

Replace hardcoded per-channel send fields (sendTelegram, sendDiscord,
etc.) with a dynamic index-signature type keyed by channel ID. This
unblocks moving channel implementations to extensions without breaking
the outbound dispatch contract.

- OutboundSendDeps and CliDeps are now { [channelId: string]: unknown }
- Each outbound adapter resolves its send fn via bracket access with cast
- Lazy-loading preserved via createLazySender with module cache
- Delete 6 deps-send-*.runtime.ts one-liner re-export files
- Harden guardrail scan against deleted-but-tracked files

* fix: preserve outbound send-deps compatibility

* style: fix formatting issues (import order, extra bracket, trailing whitespace)

* fix: resolve type errors from dynamic OutboundSendDeps in tests and extension

* fix: remove unused OutboundSendDeps import from deliver.test-helpers

(cherry picked from commit 7764f71)

* refactor: finish plugin-owned channel runtime seams

(cherry picked from commit 7964563)

* refactor: reuse shared account config lookups

(cherry picked from commit 7ae0941)

* Channels: move onboarding adapters into extensions

(cherry picked from commit 8b001d6)

* perf: narrow discord timeout import seam

(cherry picked from commit 8e6a4c2)

* Plugin SDK: restore scoped imports for bundled channels

(cherry picked from commit 9270094)

* Channels: add message action capabilities

(cherry picked from commit 92bea97)

* refactor(discord): share plugin base config

(cherry picked from commit a0e7e3c)

* refactor: split monitor runtime helpers

(cherry picked from commit a2518a1)

* test: strengthen regression coverage and trim low-value checks

(cherry picked from commit b4656f1)

* fix: reduce plugin and discord warning noise

(cherry picked from commit c156f7c)

* refactor: move channel delivery and ACP seams into plugins

(cherry picked from commit d163278)

* Discord: lazy-load setup wizard surface

(cherry picked from commit d663df7)

* refactor(hook-tests): share subagent hook helpers

(cherry picked from commit e56e492)

* refactor(test): dedupe startup and nostr test fixtures

(cherry picked from commit f1b2c56)

* Plugins: honor native command aliases at dispatch

(cherry picked from commit f90d432)

* refactor: move Discord channel implementation to extensions/ (openclaw#45660)

* refactor: move Discord channel implementation to extensions/discord/src/

Move all Discord source files from src/discord/ to extensions/discord/src/,
following the extension migration pattern. Source files in src/discord/ are
replaced with re-export shims. Channel-plugin files from
src/channels/plugins/*/discord* are similarly moved and shimmed.

- Copy all .ts source files preserving subdirectory structure (monitor/, voice/)
- Move channel-plugin files (actions, normalize, onboarding, outbound, status-issues)
- Fix all relative imports to use correct paths from new location
- Create re-export shims at original locations for backward compatibility
- Delete test files from shim locations (tests live in extension now)
- Update tsconfig.plugin-sdk.dts.json rootDir from "src" to "." to accommodate
  extension files outside src/
- Update write-plugin-sdk-entry-dts.ts to match new declaration output paths

* fix: add importOriginal to thread-bindings session-meta mock for extensions test

* style: fix formatting in thread-bindings lifecycle test

(cherry picked from commit 5682ec3)

* fix: gate setup-only plugin side effects

(cherry picked from commit 59bcac4)

* refactor: simplify tlon and discord setup accounts

(cherry picked from commit 6fa0027)

* refactor: reuse shared account config lookups

(cherry picked from commit 7ae0941)

* refactor: remove dock shim and move session routing into plugins

(cherry picked from commit 85b7bc7)

* Channels: move onboarding adapters into extensions

(cherry picked from commit 8b001d6)

* test: strengthen regression coverage and trim low-value checks

(cherry picked from commit b4656f1)

* fix: reduce plugin and discord warning noise

(cherry picked from commit c156f7c)

* test: share runtime group policy fallback cases

(cherry picked from commit 208fb1a)

* fix: unblock discord startup on deploy rate limits

(cherry picked from commit 2659fc6)

* refactor: share discord auto thread params

(cherry picked from commit 301594b)

* test: dedupe discord forwarded media assertions

(cherry picked from commit 3eb039c)

* refactor: share discord preflight shared fields

(cherry picked from commit 58a51e2)

* test(discord): cover additional utility surfaces

(cherry picked from commit 59be2c8)

* Discord: add shared interactive renderer

(cherry picked from commit 59d355b)

* chore: remove session-fork.runtime.ts — references gutted pi-coding-agent layer

* fix: resolve remaining conflict markers in discord extension files

* fix: remove remaining diff3 base markers from discord extension files

* fix: remove duplicate extension files and broken runtime bridges — fork structure differs from upstream

* fix: remove all new files with type errors — incompatible with fork module structure

* fix: restore all remaining files with type errors to fork-sync-base versions

* fix: second pass — restore remaining files with type errors

* fix: pass 5 — restore/remove files with type errors

* fix: restore files inadvertently deleted by upstream restructuring

* fix: restore inadvertently modified files and remove orphan runtime extractions

* fix: restore dock property in PluginChannelRegistration type

* fix: restore dock property in PluginChannelRegistration and zalo exports

* fix: remove extension discord src files and restore src/discord imports — fork keeps discord in src/

* fix: round 1 type error cleanup

* fix: resolve lint error and restore test files modified by upstream refactoring

* fix: format suites.ts and restore channels-misc.test.ts

---------

Co-authored-by: Peter Steinberger <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
Co-authored-by: Gustavo Madeira Santana <[email protected]>
Co-authored-by: scoootscooob <[email protected]>
furaul pushed a commit to furaul/openclaw that referenced this pull request Mar 24, 2026
* perf(inbound): trim cold startup import graph

* chore(reply): drop redundant inline action type import

* fix(inbound): restore warning and maintenance seams

* fix(reply): restore type seam and secure forked transcripts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: discord Channel integration: discord gateway Gateway runtime maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant