Caching System¶
Overview¶
ARC-1 includes a built-in caching layer that sits between the intent handler and the ADT client. Its purpose is to reduce redundant HTTP calls to the SAP system, speed up responses for repeated operations, and enable features like reverse dependency lookup that would otherwise require expensive full-system scans on every request.
The cache stores five types of data:
- Source code -- raw ABAP source keyed by
(objectType, objectName), with a SHA-256 content hash. - Dependency graphs -- compressed dependency contracts keyed by source hash.
- Dependency edges -- directional relationships between objects (CALLS, USES, IMPLEMENTS, INCLUDES).
- Node metadata -- object type, name, package, and source hash for each cached object.
- Function group mappings -- which function module belongs to which function group.
The system operates in three tiers of increasing capability:
| Tier | Transport | Backend | Lifetime | Features |
|---|---|---|---|---|
| 1 | stdio (Claude Desktop) | Memory | Single session | Dedup fetches within session |
| 2 | http-streamable (server) | SQLite | Persists across sessions | Shared warm cache for all sessions |
| 3 | Docker + warmup | SQLite + pre-warmer | Persists + pre-indexed | Reverse dependency lookup, sub-second dep resolution |
Configuration¶
All cache settings follow the standard ARC-1 configuration priority: CLI flags > environment variables > .env file > defaults.
| Env Variable | CLI Flag | Values | Default | Description |
|---|---|---|---|---|
ARC1_CACHE |
--cache |
auto, memory, sqlite, none |
auto |
Cache backend selection. auto picks memory for stdio, SQLite for http-streamable. |
ARC1_CACHE_FILE |
--cache-file |
File path | .arc1-cache.db |
Path to the SQLite database file. Relative paths resolve from the working directory. |
ARC1_CACHE_WARMUP |
--cache-warmup |
true, false |
false |
Run the pre-warmer on startup (enumerates TADIR, fetches all custom objects). |
ARC1_CACHE_WARMUP_PACKAGES |
--cache-warmup-packages |
Comma-separated patterns | (empty = all custom) | Package filter for warmup. Supports wildcards. |
Auto mode behavior¶
When ARC1_CACHE=auto (the default):
- stdio transport -- uses in-memory cache. No files created, no persistence. The cache dies with the process.
- http-streamable transport -- uses SQLite. The database file is created at the path specified by
ARC1_CACHE_FILE.
To disable caching entirely, set ARC1_CACHE=none.
How It Works¶
Source code caching¶
Every time ARC-1 fetches source code from SAP (classes, interfaces, programs, functions, etc.), the response is stored in the cache keyed by OBJECTTYPE:OBJECTNAME (uppercased). A SHA-256 hash of the source content is computed and stored alongside it.
On subsequent requests for the same object, the cached source is returned immediately without an ADT call. When a cached source is returned via SAPRead, the response is prefixed with [cached] so the caller knows the result came from cache. This matches the behavior of SAPContext dependency results.
Hash-on-fetch mechanism¶
The SHA-256 hash serves a dual purpose:
-
Dependency graph cache key -- dependency graphs (the contracts extracted by the AST parser) are keyed by the source hash, not by the object name. If the source code hasn't changed, the hash is the same, and the entire dependency resolution is skipped -- no AST parsing, no downstream fetches.
-
Delta detection during warmup -- when the pre-warmer re-runs, it compares the hash of freshly fetched source against the cached hash. If they match, the object is skipped entirely (no dep extraction, no edge updates).
Dependency graph caching¶
When SAPContext resolves dependencies for an object, the result is a list of contracts (compressed representations of each dependency). This list is stored keyed by the source hash. On the next request:
- Fetch source (cache hit or miss).
- Compute hash.
- Look up dep graph by hash.
- If found, return cached contracts -- zero additional ADT calls.
- If not found, resolve deps normally, then store the result.
Function group resolution caching¶
Function modules in SAP belong to function groups, but the mapping is not encoded in the module name. ARC-1 must search ADT to resolve which group a function belongs to. These mappings are cached permanently (they rarely change) to avoid repeated search calls.
Write invalidation¶
When SAPWrite modifies an object, the cache entry for that object's source is invalidated. The next read will fetch fresh source from SAP, compute a new hash, and trigger dependency re-resolution if needed. This ensures the cache never serves stale source after a write.
Cache Strategies by Deployment¶
| Aspect | stdio (Claude Desktop) | http-streamable (server) | Docker + warmup |
|---|---|---|---|
| Backend | Memory | SQLite | SQLite |
| Persistence | None (session-scoped) | Across restarts | Across restarts |
| Config needed | None (zero config) | None (auto-detects) | ARC1_CACHE_WARMUP=true |
| First request | Always cold | Warm after first session | Pre-warmed on startup |
| Reverse deps | Not available | Not available | Available (SAPContext(action="usages")) |
| Multi-user | N/A (single user) | Shared cache | Shared cache |
| Typical setup | npx arc-1 |
arc-1 --transport http-streamable |
See Docker section below |
stdio (Claude Desktop)¶
No configuration required. The memory cache eliminates duplicate fetches within a single conversation. When the process exits, the cache is gone.
{
"mcpServers": {
"arc1": {
"command": "npx",
"args": ["-y", "arc-1"],
"env": {
"SAP_URL": "http://sap-host:50000",
"SAP_USER": "developer",
"SAP_PASSWORD": "secret"
}
}
}
}
http-streamable (server)¶
SQLite cache is selected automatically. The database persists across server restarts, so the second session benefits from the first session's fetches.
arc-1 --transport http-streamable \
--url http://sap-host:50000 \
--user developer \
--password secret
Docker with warmup¶
Full-strength caching with pre-indexed dependency graph and reverse lookup support.
docker run -d \
-e SAP_URL=http://sap-host:50000 \
-e SAP_USER=developer \
-e SAP_PASSWORD=secret \
-e SAP_TRANSPORT=http-streamable \
-e ARC1_CACHE_WARMUP=true \
-e ARC1_CACHE_WARMUP_PACKAGES="Z*,Y*" \
-v arc1-cache:/app/cache \
-e ARC1_CACHE_FILE=/app/cache/arc1.db \
-p 8080:8080 \
ghcr.io/marianfoo/arc-1
Pre-Warmer¶
The pre-warmer runs at startup when ARC1_CACHE_WARMUP=true. It populates the cache with all custom objects so that the first user request is fast and reverse dependency lookups are available.
Pipeline¶
- Enumerate -- queries TADIR for all objects of type CLAS, INTF, and FUGR where the object name starts with
Z*,Y*, or/*(namespaced). - Fetch -- retrieves source code for each object in parallel batches of 5 concurrent requests.
- Delta check -- compares the SHA-256 hash of fetched source against the cached hash. If unchanged, the object is skipped (no re-parsing).
- Extract -- runs the local AST parser (
@abaplint/core) on each changed source to extract dependencies. No additional ADT calls are needed for this step. - Index -- stores source, node metadata, and dependency edges in the cache. For function groups, individual function modules are enumerated and indexed separately.
- Enable reverse lookup -- sets the
warmupDoneflag, which enablesSAPContext(action="usages").
Package filter syntax¶
The ARC1_CACHE_WARMUP_PACKAGES value is a comma-separated list of patterns. Each pattern maps to a SQL LIKE clause on the TADIR DEVCLASS column. The * wildcard maps to %.
| Filter | Effect |
|---|---|
| (empty) | All custom objects (Z, Y, /*) |
ZPROJECT |
Only package ZPROJECT (exact match) |
Z* |
All packages starting with Z |
Z*,Y* |
All Z and Y packages |
/COMPANY/* |
All packages in the /COMPANY/ namespace |
ZMOD1,ZMOD2,/NS/* |
Specific packages plus a namespace |
Timing estimates¶
Estimates assume 5 concurrent requests (the default WARMUP_CONCURRENT value) and typical on-premise network latency:
| System size | Objects | Estimated time |
|---|---|---|
| Small | ~500 | 2-3 minutes |
| Medium | ~2,000 | 8-12 minutes |
| Large | ~5,000 | 20-30 minutes |
Delta re-runs are significantly faster because unchanged objects are skipped after hash comparison. Only objects with modified source are re-fetched and re-parsed.
The maximum number of objects per warmup run is capped at 10,000 (WARMUP_MAX_OBJECTS).
Docker cron example¶
To keep the cache fresh on a running Docker container, schedule periodic re-warmup via cron or an external scheduler:
# Re-run warmup every 4 hours via docker exec
# (the server handles this as a SAPManage action, or restart the container)
0 */4 * * * docker restart arc1-container
Alternatively, mount the SQLite database on a persistent volume so that restarts with ARC1_CACHE_WARMUP=true perform a delta update rather than a full re-index:
docker run -d --name arc1 \
-v arc1-cache:/app/cache \
-e ARC1_CACHE_FILE=/app/cache/arc1.db \
-e ARC1_CACHE_WARMUP=true \
-e ARC1_CACHE_WARMUP_PACKAGES="Z*" \
# ... other env vars ...
ghcr.io/marianfoo/arc-1
Reverse Dependency Lookup¶
What it does¶
SAPContext(action="usages", name="ZCL_MY_CLASS") returns all objects that depend on the given object -- i.e., "who calls/uses this class?"
This is a reverse lookup on the edge index: find all edges where toId matches the target object.
Requirements¶
Reverse dependency lookup is only available after the pre-warmer has run. The warmupDone flag must be set to true. Without warmup, the edge index is empty and there is nothing to reverse-look-up.
How it works¶
- The pre-warmer extracts dependencies from every indexed object and stores them as directed edges (
fromId -> toId). - When
getUsages(objectName)is called, the cache queries all edges wheretoId = objectName.toUpperCase(). - Results include the calling object (
fromId) and the relationship type (CALLS,USES,IMPLEMENTS,INCLUDES).
Fallback when warmup is not available¶
If warmup has not run, SAPContext(action="usages", ...) returns an isError: true response with setup instructions — telling the caller to start ARC-1 with --cache-warmup (or ARC1_CACHE_WARMUP=true), wait for indexing to complete, then retry.
Performance Impact¶
| Scenario | Description | Estimated ADT call savings |
|---|---|---|
| A | Single session, no warmup (memory cache) | 50-60% -- eliminates duplicate fetches within the session |
| B | Same session with warmup (SQLite, pre-indexed) | 85-95% -- most source and all deps served from cache |
| C | Productive system, multiple users (shared SQLite) | Sub-linear scaling -- each user benefits from objects fetched by others |
The biggest savings come from dependency graph caching. A single SAPContext call for a class with 15 dependencies would normally require 16+ ADT calls (1 for the class + 1 per dependency). With a warm cache and unchanged source, this drops to 0 ADT calls.
Disk Space¶
What is stored¶
| Data type | Storage per object | Notes |
|---|---|---|
| Source code | Varies (typically 2-50 KB) | Full ABAP source text |
| Dependency graphs | ~1-5 KB per object | JSON-serialized contract list |
| Edges | ~100 bytes each | One row per dependency relationship |
| Node metadata | ~200 bytes each | Object type, name, package, hash |
| Function group mappings | ~100 bytes each | Function name to group name |
Typical database sizes¶
| System size | Custom objects | Approximate SQLite size |
|---|---|---|
| Small | ~500 | 35-50 MB |
| Medium | ~2,000 | 60-100 MB |
| Large | ~5,000 | 100-150 MB |
CPU overhead¶
- SHA-256 hashing: negligible (~0 ms per object for typical source sizes).
- AST parsing (
@abaplint/core): approximately 10 ms per object. This only runs on cache misses or during warmup for changed objects. - SQLite I/O: single-digit milliseconds for reads; writes are batched during warmup.
Limitations and Caveats¶
Understanding these limitations helps you avoid surprises in production.
External writes are not detected¶
The cache is only invalidated when ARC-1 itself performs a write via SAPWrite. If someone modifies ABAP objects through other tools (ABAP Development Tools in Eclipse, SE38, transaction ABAP Workbench, or another ARC-1 instance), the cache will serve stale source until:
- The server restarts (memory cache) or the SQLite file is deleted
- ARC-1 performs its own write on the same object (triggers automatic invalidation)
Mitigation: For collaborative development environments, use stdio mode (memory cache dies on process exit) or restart the http-streamable server after external changes.
No TTL — entries never expire automatically¶
Cache entries have no expiry time. A source entry stored today will still be returned a week later unless explicitly invalidated by an ARC-1 write. This is intentional (SAP objects rarely change without a write) but means stale data is possible if the same object is modified externally.
Warmup covers CLAS, INTF, and FUGR only¶
The pre-warmer enumerates TADIR and only indexes objects of type CLAS (classes), INTF (interfaces), and FUGR (function groups). Programs (PROG), includes (INCL), CDS views (DDLS), behavior definitions (BDEF), and other types are not pre-indexed.
This means:
- SAPContext(action="usages") only finds callers among indexed object types (classes, interfaces, function groups).
- Programs that call a class won't appear in usages results.
- On-demand caching (reading PROG/DDLS/etc.) still works — those types are cached the first time they're read, they just aren't in the edge index.
SQLite requires a native addon¶
The SQLite backend uses better-sqlite3, a native Node.js addon compiled for the host platform. If the addon is missing or compiled for a different platform, ARC-1 automatically falls back to an in-memory cache and logs a warning:
This happens automatically — the server still starts and caches in memory. To verify which backend is active, use SAPManage(action="cache_stats").
Warmup does not block server startup¶
The pre-warmer runs concurrently in the background. The server starts accepting MCP requests immediately, even if warmup is still running. During warmup:
- Source reads are served normally (cache misses go to SAP, hits return immediately).
SAPContext(action="usages")returns a "warmup not complete" error until warmup finishes.SAPManage(action="cache_stats")showswarmupAvailable: falsewhile in progress.
The [cached] marker in SAPContext output¶
When SAPContext(action="deps") resolves dependencies from the cache (zero ADT calls), the output header includes [cached]:
Without the marker, some or all dependencies were freshly fetched from SAP. First call after server start will not show [cached]. Subsequent calls for unchanged objects will.
Monitoring¶
Use SAPManage(action="cache_stats") to inspect the current state of the cache:
{
"enabled": true,
"warmupAvailable": true,
"nodeCount": 1523,
"edgeCount": 8742,
"apiCount": 0,
"sourceCount": 1523,
"contractCount": 1401
}
| Field | Description |
|---|---|
enabled |
Whether caching is active (false when ARC1_CACHE=none) |
warmupAvailable |
Whether the pre-warmer has completed (enables reverse dep lookup) |
sourceCount |
Number of cached source code entries |
contractCount |
Number of cached dependency graphs |
edgeCount |
Number of dependency edges (used for reverse lookup) |
nodeCount |
Number of cached object metadata entries |
apiCount |
Number of cached released API entries (for clean core checks) |
When enabled is false, caching is disabled and all fields except enabled and message are absent.