Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions md/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
- [Template](./rfds/TEMPLATE/README.md)
- [Accepted](./rfds/accepted.md) <!-- put accepted rfds in this section; the file goes in the rfds directory -->
- [Registry-centric plugin distribution](./rfds/registry-centric-plugins/README.md)
- [Predicate caching](./rfds/predicate-caching/README.md)
- [Completed](./rfds/completed.md) <!-- move completed rfds to this section -->
- [Configuration parsing and normalization](./rfds/config-normalization/README.md)
- [RFD Process](./rfds/rfd-process/README.md)
85 changes: 85 additions & 0 deletions md/rfds/predicate-caching/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Predicate caching

## TL;DR

Predicates, especially custom predicates, spawn processes on every sync. This RFD lets custom predicates emit `Watch` JSONL events for files and environment variables. Symposium caches their results and skips reevaluation while the union of watched inputs is unchanged. No watch hints means cached indefinitely; `Volatile` means never cached.

## Problem

Auto-sync means predicates re-evaluate on every agent session start. A workspace with 10 plugins, each with a custom predicate, forks 10+ processes every time.

## Design

Custom predicates already emit JSONL events to stdout. We add two variants:

```rust
Comment thread
nikomatsakis marked this conversation as resolved.
#[derive(Serialize, Deserialize)]
enum CustomPredicateEvent {
// ... existing variants ...
Watch {
files: Vec<PathBuf>,
env: HashMap<String, String>,
},
Volatile {},
}
```

A predicate can emit multiple watch hints:

```jsonl
{"watch": {"files": ["CargoBrazil.toml"]}}
{"watch": {"files": ["Config"]}}
{"watch": {"env": {"LAMBDA_ENV": "prod"}}}
```

Symposium unions these into one watch set. A change to any watched input causes one reevaluation. Files are relative to the workspace root.

The process exit status determines the predicate result. `Watch` and `Volatile` events only control caching. No `Watch` events means the result is cached indefinitely and predicates must report every changing input or Symposium may reuse a stale result. `Volatile` disables caching and takes precedence over watch hints.

```jsonl
{"volatile": {}}
```

## SDK helper

The `symposium-sdk` crate provides a helper that reads an environment variable and emits its watch event:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(we should probably also have a helper to e.g. read and deserialize a JSON/TOML file or just to read a file to a string)


```rust
let val = symposium_sdk::env::var("LAMBDA_ENV")?;
// Emits {"watch": {"env": {"LAMBDA_ENV": "prod"}}}.
```

Multiple helper calls emit multiple events, which Symposium unions.

## How it works

File fingerprints use `mtime + size`; missing is a valid state. Environment fingerprints use the current value or absent state.

Cache lives at `~/.symposium/cache/predicates.json`.

1. Look up the predicate in the cache.
2. If all watched inputs match, use the cached result. An empty watch set always matches.
3. Otherwise, evaluate the predicate and obtain its result from the exit status.
4. If it emitted `Volatile`, do not cache the result. Otherwise, store the result with the union of its watch hints.

Cache is discarded on Symposium version upgrade.

## Built-in predicates

- `workspace-member()` requires no cache because it is already cheap and evaluated in memory.
- `path_exists(path)` watches the path itself.
- `env(FOO=BAR)` watches the value of `FOO`.
- `shell(cmd)` is volatile because its inputs are unknown.
- Caching `depends-on(name)` is deferred to the PM interface work.

## PM integration

Changes to `list_deps` and PM-derived caching are deferred to the PM interface work.

## Implementation steps

1. Add and parse `Watch` and `Volatile` events while keeping the exit status as the predicate result.
2. Union watch hints, cache an empty watch set indefinitely, and give `Volatile` precedence.
3. Add cache storage and fingerprint comparison.
4. Wire `path_exists`, `env`, and volatile `shell` behavior.
5. Add `symposium_sdk::env::var()` to emit environment watch events.
Loading