Skip to content

feat: serve Codex Skills from a lazy app-server catalog - #904

Merged
chuks-qua merged 1 commit into
mainfrom
feat/892-codex-skills-catalog
Jul 20, 2026
Merged

chuks-qua merged 1 commit into
mainfrom
feat/892-codex-skills-catalog

Conversation

@chuks-qua

@chuks-qua chuks-qua commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

What

Serve Codex Skills from one lazy provider-wide app-server catalog before thread creation.

Why

Mcode needs provider-authoritative Skill discovery without starting or coupling to a thread app-server.

Key Changes

  • Add a lazy catalog-only Codex app-server connection with 60-second idle eviction and bounded per-cwd snapshots.
  • Fetch full Skill lists for the effective working directory, preserve provider-native path identities, and reconcile skills/changed in the background.
  • Remove the Codex filesystem Skill fallback while retaining deprecated custom prompts.
  • Add controlled WebSocket and provider tests for pre-thread discovery, cwd scoping, refresh, thread independence, and failure diagnostics.

Closes #892


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features

    • Added Codex skill catalog support with workspace-specific discovery and refresh handling.
    • Catalog results now include freshness status and diagnostics when discovery is incomplete.
    • Codex skills can load from cached catalog data without waiting for a live session.
    • Catalog updates are reflected automatically when skills change.
  • Bug Fixes

    • Prevented duplicate skills with the same name but different paths from being merged.
    • Improved handling of invalid metadata and catalog refresh failures.
    • Avoided unnecessary thread creation during catalog-only operations.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Codex Skills now come from a lazy, catalog-only app-server connection with per-workspace caching, refresh notifications, diagnostics, and freshness metadata. Codex provider and WebSocket catalog flows use this service, while legacy Codex filesystem discovery is removed.

Changes

Codex catalog flow

Layer / File(s) Summary
Catalog service and scoped refresh
apps/server/src/services/codex-catalog-service.ts, apps/server/src/services/__tests__/codex-catalog-service.test.ts
Adds catalog client contracts and a service that reconciles skills, caches workspace contexts, handles skills/changed, reports diagnostics, evicts idle state, and preserves stale snapshots after failures.
Catalog-only app-server and provider integration
apps/server/src/providers/codex/codex-app-server.ts, apps/server/src/providers/codex/codex-provider.ts, apps/server/src/providers/codex/__tests__/*
Adds catalog-only handshakes and makes CodexProvider source cached and refreshed skills through CodexCatalogService without coupling catalog discovery to thread startup.
Codex skill discovery boundaries
apps/server/src/services/skill-service.ts, apps/server/src/services/skill-service.test.ts
Removes Codex filesystem and plugin fallback scans, retains prompt compatibility paths, and preserves native skills with distinct paths.
Provider catalog transport and server wiring
apps/server/src/transport/provider-catalog.ts, apps/server/src/transport/ws-router.ts, apps/server/src/transport/ws-router.validation.test.ts, apps/server/src/container.ts, apps/server/src/index.ts
Wires CodexCatalogService into dependency injection and WebSocket routing, exposing catalog diagnostics, freshness, workspace-scoped skills, and path-based identities.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WsRouter
  participant CodexCatalogService
  participant CodexCatalogClient
  participant CodexAppServer
  Client->>WsRouter: provider.catalog(workspaceId)
  WsRouter->>CodexCatalogService: refresh(cwd)
  CodexCatalogService->>CodexCatalogClient: start or reuse client
  CodexCatalogClient->>CodexAppServer: initialize catalog-only session
  CodexCatalogService->>CodexCatalogClient: listSkills(cwd)
  CodexCatalogClient-->>CodexCatalogService: skills result
  CodexCatalogService-->>WsRouter: skills, diagnostics, freshness
  WsRouter-->>Client: provider catalog snapshot
Loading

Possibly related issues

Possibly related PRs

  • Mzeey-Empire/mcode#67 — Shares the dependency-injection and WebSocket routing surfaces extended here.
  • Mzeey-Empire/mcode#247 — Provides the Codex app-server lifecycle architecture modified for catalog-only operation.
  • Mzeey-Empire/mcode#169 — Shares CodexProvider integration later refactored to use the catalog service.

Poem

A rabbit found skills in a catalog bright,
With workspaces cached through the soft moonlight.
Threads hop onward, no waiting around,
Fresh paths and warnings are neatly found.
“Changed!” cries Codex; the shelves rearrange—
And every small skill knows its new range.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: serving Codex Skills from a lazy app-server catalog.
Description check ✅ Passed The description matches the template's What/Why/Key Changes sections, but omits the optional Config Changes section.
Linked Issues check ✅ Passed The changes implement the lazy Codex catalog, pre-thread discovery, cwd scoping, refresh handling, and fallback removal described in #892.
Out of Scope Changes check ✅ Passed The diff stays focused on Codex catalog, transport, and test updates, with no obvious unrelated feature work.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@chuks-qua
chuks-qua merged commit 4fdcabf into main Jul 20, 2026
9 of 10 checks passed
@chuks-qua
chuks-qua deleted the feat/892-codex-skills-catalog branch July 20, 2026 15:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
apps/server/src/transport/provider-catalog.ts (1)

47-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add one-line docstrings.

As per coding guidelines, add one-line JSDoc/TSDoc docstrings to all exported interfaces and their properties. Consider adding brief descriptions for these new fields to explain their operational purpose in the snapshot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/transport/provider-catalog.ts` around lines 47 - 48, Add
one-line TSDoc comments to the exported interface and each property shown in
provider-catalog.ts, including diagnostics and freshness; briefly describe each
field’s operational purpose in the snapshot while preserving the existing types
and optionality.

Source: Coding guidelines

apps/server/src/transport/ws-router.ts (1)

1001-1007: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use explicit types for uninitialized variables.

As per coding guidelines, use explicit types to make the code self-documenting and to prevent uninitialized variables from implicitly evaluating to any before assignment.

Consider explicitly defining the types, for example:

let diagnostics: readonly ProviderCatalogDiagnostic[] | undefined;
let freshness: ProviderCatalogFreshness | undefined;

(You may need to import these types if they aren't already available in this file.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/transport/ws-router.ts` around lines 1001 - 1007, Declare the
uninitialized diagnostics and freshness variables in the provider catalog flow
with explicit union types matching the catalog’s diagnostic and freshness types
plus undefined. Import the types if needed, while preserving their existing
assignments from codexCatalogService.refresh(cwd).

Source: Coding guidelines

apps/server/src/services/skill-service.ts (1)

413-437: 📐 Maintainability & Code Quality | 🔵 Trivial

Docstring doesn't mention the new codex-specific bypass.

filterAndDedup's docstring only describes generic per-filtered-set dedup, but the providerId === "codex" branch (435-437) now skips that cleanup entirely and returns native items unfiltered by name collision. Worth a one-line addition explaining why codex bypasses the collision cleanup (native path identity is authoritative, not name-based dedup).

   * Deduplication is intentionally scoped to the filtered set so that a skill
   * named "deploy" can independently exist for both claude and codex.
+  * For codex, native catalog items bypass this cleanup entirely: their path
+  * identity from the app-server is authoritative, so same-name native Skills
+  * (e.g. user vs project scope) are preserved rather than deduped by name.
   */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/services/skill-service.ts` around lines 413 - 437, Update the
docstring for filterAndDedup to document the providerId === "codex" behavior:
nativeFiltered items are returned alongside the deduplicated entries without
name-based collision cleanup because native path identity is authoritative.
apps/server/src/services/codex-catalog-service.ts (1)

155-160: 🚀 Performance & Scalability | 🔵 Trivial

Optional: no in-flight request coalescing per cwd in refresh().

Concurrent refresh(cwd) calls for the same cwd (e.g. two composer mounts polling skills around the same time) each trigger an independent client.listSkills() RPC rather than sharing one in-flight request. acquireClient() already coalesces client startup via startPromise; the same pattern could dedupe concurrent refreshContext calls per key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/services/codex-catalog-service.ts` around lines 155 - 160,
Add per-cwd in-flight request coalescing to refresh() so concurrent calls share
one refreshContext(cwd, false) promise and trigger only one listSkills RPC.
Reuse the existing key normalization/context handling, store the promise while
pending, and remove it after completion so later refreshes execute normally.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/services/codex-catalog-service.ts`:
- Around line 250-277: Update acquireClient to check and return the existing
startPromise before checking client?.isAlive, ensuring concurrent callers await
startup and handshake completion. Preserve the current alive-client fast path
for cases with no in-flight start.

---

Nitpick comments:
In `@apps/server/src/services/codex-catalog-service.ts`:
- Around line 155-160: Add per-cwd in-flight request coalescing to refresh() so
concurrent calls share one refreshContext(cwd, false) promise and trigger only
one listSkills RPC. Reuse the existing key normalization/context handling, store
the promise while pending, and remove it after completion so later refreshes
execute normally.

In `@apps/server/src/services/skill-service.ts`:
- Around line 413-437: Update the docstring for filterAndDedup to document the
providerId === "codex" behavior: nativeFiltered items are returned alongside the
deduplicated entries without name-based collision cleanup because native path
identity is authoritative.

In `@apps/server/src/transport/provider-catalog.ts`:
- Around line 47-48: Add one-line TSDoc comments to the exported interface and
each property shown in provider-catalog.ts, including diagnostics and freshness;
briefly describe each field’s operational purpose in the snapshot while
preserving the existing types and optionality.

In `@apps/server/src/transport/ws-router.ts`:
- Around line 1001-1007: Declare the uninitialized diagnostics and freshness
variables in the provider catalog flow with explicit union types matching the
catalog’s diagnostic and freshness types plus undefined. Import the types if
needed, while preserving their existing assignments from
codexCatalogService.refresh(cwd).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: be1dfcc3-70cb-4b1f-898c-7ba42b7329bd

📥 Commits

Reviewing files that changed from the base of the PR and between e97e6a3 and cb10b34.

📒 Files selected for processing (15)
  • apps/server/src/container.ts
  • apps/server/src/index.ts
  • apps/server/src/providers/codex/__tests__/codex-app-server-handshake.test.ts
  • apps/server/src/providers/codex/__tests__/codex-provider-first-turn.test.ts
  • apps/server/src/providers/codex/__tests__/codex-provider-permission.test.ts
  • apps/server/src/providers/codex/__tests__/codex-provider-subagent-turn.test.ts
  • apps/server/src/providers/codex/codex-app-server.ts
  • apps/server/src/providers/codex/codex-provider.ts
  • apps/server/src/services/__tests__/codex-catalog-service.test.ts
  • apps/server/src/services/codex-catalog-service.ts
  • apps/server/src/services/skill-service.test.ts
  • apps/server/src/services/skill-service.ts
  • apps/server/src/transport/provider-catalog.ts
  • apps/server/src/transport/ws-router.ts
  • apps/server/src/transport/ws-router.validation.test.ts

Comment on lines +250 to +277
private async acquireClient(cwd?: string): Promise<CodexCatalogClient> {
if (this.client?.isAlive) return this.client;
if (this.startPromise) return this.startPromise;

const settings = this.settingsService.get();
const client = this.clientFactory.create({
cliPath: settings.provider.cli.codex || "codex",
workingDirectory: cwd ?? process.cwd(),
approvalPolicy: "never",
catalogOnly: true,
jobObject: this.jobObject,
getSpawnEnv: () => this.envService.getEnv(),
});
this.client = client;
client.on("notification", (notification: unknown) => {
this.handleNotification(notification);
});
this.startPromise = client.start().then(() => client);
try {
return await this.startPromise;
} catch (error) {
if (this.client === client) this.client = null;
await client.kill().catch(() => undefined);
throw error;
} finally {
this.startPromise = null;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Race: isAlive checked before startPromise, can return a not-yet-handshaked client.

CodexAppServer._isAlive flips to true right after spawn succeeds but before runHandshake() completes (see codex-app-server.ts start()). Because acquireClient() checks this.client?.isAlive before this.startPromise, a second concurrent refresh()/refreshContext() call landing in that window returns the client immediately and can fire listSkills() before the initialize/initialized handshake finishes.

🔒️ Reorder checks so an in-flight start is always awaited first
 private async acquireClient(cwd?: string): Promise<CodexCatalogClient> {
-    if (this.client?.isAlive) return this.client;
-    if (this.startPromise) return this.startPromise;
+    if (this.startPromise) return this.startPromise;
+    if (this.client?.isAlive) return this.client;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/services/codex-catalog-service.ts` around lines 250 - 277,
Update acquireClient to check and return the existing startPromise before
checking client?.isAlive, ensuring concurrent callers await startup and
handshake completion. Preserve the current alive-client fast path for cases with
no in-flight start.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: serve Codex Skills from a lazy app-server catalog

1 participant