File Reservation
Pessimistic file locking.
The file reservation protocol prevents concurrent sessions from writing to the same file simultaneously. Workers reserve files before modifying them and release them when done, giving other agents a chance to coordinate.
Overview
When multiple agents are active in the same repository (e.g. a feature agent and a QA agent running concurrently), file reservation ensures they do not destructively overwrite each other's changes. The mechanism is pessimistic - a second reservation attempt on an already-reserved file returns a conflict, and the requesting worker must wait or choose a different approach.
File reservations are scoped to a session and automatically released when the session reaches a terminal state (completed, failed, stopped). Manual release is only required for mid-session cleanup.
Endpoints
Reserve a file
POST /api/sessions/{sessionId}/files/reserve
Authorization: Bearer <runtimeJwt>
Content-Type: application/jsonRequest body
{
"filePath": "src/services/user-service.ts",
"ttlSeconds": 120
}| Field | Type | Required | Description |
|---|---|---|---|
filePath | string | Yes | Repository-relative path to the file |
ttlSeconds | number | No | Reservation TTL in seconds (default: 60, max: 300) |
Success response (200)
{
"reservationId": "res_01abc...",
"filePath": "src/services/user-service.ts",
"sessionId": "sess_01abc...",
"expiresAt": "2026-06-02T12:02:00Z"
}Conflict response (409)
{
"error": "File already reserved",
"reservedBy": "sess_02def...",
"expiresAt": "2026-06-02T12:01:30Z"
}When you receive a 409, the expiresAt tells you when the existing reservation expires. You can either wait and retry, or check whether the blocking session is still active.
Release a file
POST /api/sessions/{sessionId}/files/release
Authorization: Bearer <runtimeJwt>
Content-Type: application/json{
"filePath": "src/services/user-service.ts"
}Success response (200)
{ "ok": true }Release all files
Release all file reservations held by a session at once (use at session end):
POST /api/sessions/{sessionId}/files/release-all
Authorization: Bearer <runtimeJwt>Success response (200)
{
"releasedCount": 3,
"releasedPaths": [
"src/services/user-service.ts",
"src/models/user.ts",
"tests/user-service.test.ts"
]
}Check for conflicts
Check whether any of a list of files are currently reserved by another session:
GET /api/sessions/{sessionId}/files/check-conflicts
Authorization: Bearer <runtimeJwt>Query parameters
| Parameter | Description |
|---|---|
paths | Comma-separated list of file paths to check |
Example
curl "https://app.rensei.ai/api/sessions/sess_01abc.../files/check-conflicts?paths=src/services/user-service.ts,src/models/user.ts" \
-H "Authorization: Bearer <runtimeJwt>"Response
{
"conflicts": [
{
"filePath": "src/services/user-service.ts",
"reservedBy": "sess_02def...",
"expiresAt": "2026-06-02T12:01:30Z"
}
],
"clear": ["src/models/user.ts"]
}List reserved files
List all files currently reserved by a specific session:
GET /api/sessions/{sessionId}/reserved-files
Authorization: Bearer <runtimeJwt>Response
{
"reservations": [
{
"reservationId": "res_01abc...",
"filePath": "src/services/user-service.ts",
"reservedAt": "2026-06-02T12:00:00Z",
"expiresAt": "2026-06-02T12:02:00Z"
}
]
}Usage pattern
A well-behaved worker follows this pattern for each file it modifies:
Check for conflicts before starting work on a set of files using the batch conflict check endpoint. If conflicts are found, wait until the blocking reservation expires.
Reserve each file before writing. Use a TTL that covers your expected edit window plus a buffer.
Write the files and commit your changes.
Release the files explicitly after committing. This allows other waiting agents to proceed without waiting for TTL expiry.
TypeScript example
async function withFileReservation(
sessionId: string,
jwt: string,
filePaths: string[],
fn: () => Promise<void>,
): Promise<void> {
const base = 'https://app.rensei.ai';
const headers = {
Authorization: `Bearer ${jwt}`,
'Content-Type': 'application/json',
};
const reservationIds: string[] = [];
try {
// Reserve all files
for (const filePath of filePaths) {
const res = await fetch(`${base}/api/sessions/${sessionId}/files/reserve`, {
method: 'POST',
headers,
body: JSON.stringify({ filePath, ttlSeconds: 120 }),
});
if (res.status === 409) {
const conflict = await res.json();
throw new Error(`File locked by ${conflict.reservedBy}: ${filePath}`);
}
const { reservationId } = await res.json();
reservationIds.push(reservationId);
}
// Execute the write operation
await fn();
} finally {
// Release all reservations
await fetch(`${base}/api/sessions/${sessionId}/files/release-all`, {
method: 'POST',
headers,
});
}
}Related pages
- Session Lifecycle - session state transitions
- Poll & Heartbeat - receiving work items