@cleocode/contracts 2026.6.19 → 2026.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lease-ipc/__tests__/freeze.test.js +77 -12
- package/dist/lease-ipc/__tests__/freeze.test.js.map +1 -1
- package/dist/lease-ipc/index.d.ts +2 -2
- package/dist/lease-ipc/index.d.ts.map +1 -1
- package/dist/lease-ipc/index.js +1 -1
- package/dist/lease-ipc/index.js.map +1 -1
- package/dist/lease-ipc/messages.d.ts +135 -6
- package/dist/lease-ipc/messages.d.ts.map +1 -1
- package/dist/lease-ipc/messages.js +83 -3
- package/dist/lease-ipc/messages.js.map +1 -1
- package/dist/lease-ipc/version.d.ts +6 -3
- package/dist/lease-ipc/version.d.ts.map +1 -1
- package/dist/lease-ipc/version.js +6 -3
- package/dist/lease-ipc/version.js.map +1 -1
- package/dist/resource-governor.d.ts +95 -0
- package/dist/resource-governor.d.ts.map +1 -0
- package/dist/resource-governor.js +48 -0
- package/dist/resource-governor.js.map +1 -0
- package/package.json +2 -2
- package/src/index.ts +25 -0
- package/src/lease-ipc/__tests__/freeze.test.ts +80 -12
- package/src/lease-ipc/index.ts +10 -0
- package/src/lease-ipc/messages.ts +103 -3
- package/src/lease-ipc/version.ts +6 -3
- package/src/resource-governor.ts +123 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/contracts",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.7.2",
|
|
4
4
|
"description": "Domain types, interfaces, and contracts for the CLEO ecosystem",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
"dependencies": {
|
|
195
195
|
"zod": "^4.3.6",
|
|
196
196
|
"zod-to-json-schema": "^3.25.2",
|
|
197
|
-
"@cleocode/lafs": "2026.
|
|
197
|
+
"@cleocode/lafs": "2026.7.2"
|
|
198
198
|
},
|
|
199
199
|
"scripts": {
|
|
200
200
|
"build": "tsc -b --force && node scripts/copy-assets.mjs && node scripts/emit-schemas.mjs",
|
package/src/index.ts
CHANGED
|
@@ -777,6 +777,11 @@ export type {
|
|
|
777
777
|
QueuePriorityClass,
|
|
778
778
|
RateCheckRequest,
|
|
779
779
|
RateResultResponse,
|
|
780
|
+
ResourceAdmitDisposition,
|
|
781
|
+
ResourceAdmitRequest,
|
|
782
|
+
ResourceAdmitResultResponse,
|
|
783
|
+
ResourceReleaseRequest,
|
|
784
|
+
ResourceReleaseResultResponse,
|
|
780
785
|
ToolGrantedResponse,
|
|
781
786
|
ToolGrantRequest,
|
|
782
787
|
} from './lease-ipc/index.js';
|
|
@@ -808,6 +813,11 @@ export {
|
|
|
808
813
|
QueuePriorityClassSchema,
|
|
809
814
|
RateCheckRequestSchema,
|
|
810
815
|
RateResultResponseSchema,
|
|
816
|
+
ResourceAdmitDispositionSchema,
|
|
817
|
+
ResourceAdmitRequestSchema,
|
|
818
|
+
ResourceAdmitResultResponseSchema,
|
|
819
|
+
ResourceReleaseRequestSchema,
|
|
820
|
+
ResourceReleaseResultResponseSchema,
|
|
811
821
|
ToolGrantedResponseSchema,
|
|
812
822
|
ToolGrantRequestSchema,
|
|
813
823
|
} from './lease-ipc/index.js';
|
|
@@ -2057,6 +2067,21 @@ export type {
|
|
|
2057
2067
|
} from './release/version-bump.js';
|
|
2058
2068
|
// === Human Render Contract (Epic T10114, ADR-077) ===
|
|
2059
2069
|
export * from './render/index.js';
|
|
2070
|
+
export type {
|
|
2071
|
+
AdmissionResult,
|
|
2072
|
+
GovernorMode,
|
|
2073
|
+
ResourceClass,
|
|
2074
|
+
ResourceDeferral,
|
|
2075
|
+
ResourceGrant,
|
|
2076
|
+
} from './resource-governor.js';
|
|
2077
|
+
// === Resource Governor — Never-OOM admission layer (T11999 / Epic T11992) ===
|
|
2078
|
+
export {
|
|
2079
|
+
DEFAULT_RESOURCE_RETRY_AFTER_MS,
|
|
2080
|
+
isResourceGrant,
|
|
2081
|
+
RESOURCE_BACKPRESSURE_CODE,
|
|
2082
|
+
RESOURCE_CLASSES,
|
|
2083
|
+
RESOURCE_DEFERRED_CODE,
|
|
2084
|
+
} from './resource-governor.js';
|
|
2060
2085
|
// === Result Types (Dashboard, Stats, Log, Context, Sequence, Analysis, Deps) ===
|
|
2061
2086
|
export type {
|
|
2062
2087
|
BottleneckTask,
|
|
@@ -26,14 +26,14 @@ import {
|
|
|
26
26
|
|
|
27
27
|
describe('lease-ipc v1.1 freeze guard (T11627)', () => {
|
|
28
28
|
it('pins the parallel protocol version', () => {
|
|
29
|
-
expect(LEASE_IPC_PROTOCOL_VERSION).toBe('1.
|
|
29
|
+
expect(LEASE_IPC_PROTOCOL_VERSION).toBe('1.2.0');
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
it('the version differs from the frozen supervisor-ipc v1.0 wire version', () => {
|
|
33
33
|
// The two contracts are distinguished on the wire purely by this string;
|
|
34
34
|
// it MUST NOT collide with '1.0.0'.
|
|
35
35
|
expect(LEASE_IPC_PROTOCOL_VERSION).not.toBe('1.0.0');
|
|
36
|
-
expect(isFrozenLeaseIpcVersion('1.
|
|
36
|
+
expect(isFrozenLeaseIpcVersion('1.2.0')).toBe(true);
|
|
37
37
|
expect(isFrozenLeaseIpcVersion('1.0.0')).toBe(false);
|
|
38
38
|
});
|
|
39
39
|
|
|
@@ -46,6 +46,8 @@ describe('lease-ipc v1.1 freeze guard (T11627)', () => {
|
|
|
46
46
|
'tool_grant',
|
|
47
47
|
'queue_admit',
|
|
48
48
|
'worker_heartbeat',
|
|
49
|
+
'resource_admit',
|
|
50
|
+
'resource_release',
|
|
49
51
|
]);
|
|
50
52
|
});
|
|
51
53
|
|
|
@@ -60,6 +62,8 @@ describe('lease-ipc v1.1 freeze guard (T11627)', () => {
|
|
|
60
62
|
'child_killed_unresponsive',
|
|
61
63
|
'queue_admit_result',
|
|
62
64
|
'heartbeat_ack',
|
|
65
|
+
'resource_admit_result',
|
|
66
|
+
'resource_release_result',
|
|
63
67
|
'error',
|
|
64
68
|
]);
|
|
65
69
|
});
|
|
@@ -87,6 +91,8 @@ describe('lease-ipc v1.1 freeze guard (T11627)', () => {
|
|
|
87
91
|
'tool_grant',
|
|
88
92
|
'queue_admit',
|
|
89
93
|
'worker_heartbeat',
|
|
94
|
+
'resource_admit',
|
|
95
|
+
'resource_release',
|
|
90
96
|
'lease_granted',
|
|
91
97
|
'lease_queued',
|
|
92
98
|
'lease_denied',
|
|
@@ -96,6 +102,8 @@ describe('lease-ipc v1.1 freeze guard (T11627)', () => {
|
|
|
96
102
|
'child_killed_unresponsive',
|
|
97
103
|
'queue_admit_result',
|
|
98
104
|
'heartbeat_ack',
|
|
105
|
+
'resource_admit_result',
|
|
106
|
+
'resource_release_result',
|
|
99
107
|
'error',
|
|
100
108
|
]);
|
|
101
109
|
});
|
|
@@ -106,7 +114,7 @@ describe('lease-ipc v1.1 wire-shape parity with Rust serde (T11627)', () => {
|
|
|
106
114
|
// Literal JSON shape produced by
|
|
107
115
|
// cleo_supervisor::lease_ipc::LeaseEnvelope::request(...).to_ndjson().
|
|
108
116
|
const wire = {
|
|
109
|
-
protocol_version: '1.
|
|
117
|
+
protocol_version: '1.2.0',
|
|
110
118
|
id: 'abc',
|
|
111
119
|
direction: 'request',
|
|
112
120
|
request: {
|
|
@@ -128,7 +136,7 @@ describe('lease-ipc v1.1 wire-shape parity with Rust serde (T11627)', () => {
|
|
|
128
136
|
|
|
129
137
|
it('parses the exact JSON a Rust lease_granted response envelope serializes', () => {
|
|
130
138
|
const wire = {
|
|
131
|
-
protocol_version: '1.
|
|
139
|
+
protocol_version: '1.2.0',
|
|
132
140
|
id: 'abc',
|
|
133
141
|
direction: 'response',
|
|
134
142
|
response: {
|
|
@@ -146,7 +154,7 @@ describe('lease-ipc v1.1 wire-shape parity with Rust serde (T11627)', () => {
|
|
|
146
154
|
|
|
147
155
|
it('parses a Rust child_killed_unresponsive event envelope', () => {
|
|
148
156
|
const wire = {
|
|
149
|
-
protocol_version: '1.
|
|
157
|
+
protocol_version: '1.2.0',
|
|
150
158
|
id: 'evt-1',
|
|
151
159
|
direction: 'response',
|
|
152
160
|
response: {
|
|
@@ -162,7 +170,7 @@ describe('lease-ipc v1.1 wire-shape parity with Rust serde (T11627)', () => {
|
|
|
162
170
|
|
|
163
171
|
it('parses a reused v1.0 error response shape ({ kind, code, message })', () => {
|
|
164
172
|
const wire = {
|
|
165
|
-
protocol_version: '1.
|
|
173
|
+
protocol_version: '1.2.0',
|
|
166
174
|
id: 'e',
|
|
167
175
|
direction: 'response',
|
|
168
176
|
response: { kind: 'error', code: 'E_LEASE_UNIMPLEMENTED', message: 'deferred handler' },
|
|
@@ -172,7 +180,7 @@ describe('lease-ipc v1.1 wire-shape parity with Rust serde (T11627)', () => {
|
|
|
172
180
|
|
|
173
181
|
it('parses the exact JSON a Rust queue_admit request envelope serializes (T11630)', () => {
|
|
174
182
|
const wire = {
|
|
175
|
-
protocol_version: '1.
|
|
183
|
+
protocol_version: '1.2.0',
|
|
176
184
|
id: 'qa-1',
|
|
177
185
|
direction: 'request',
|
|
178
186
|
request: {
|
|
@@ -192,7 +200,7 @@ describe('lease-ipc v1.1 wire-shape parity with Rust serde (T11627)', () => {
|
|
|
192
200
|
|
|
193
201
|
it('parses the exact JSON a Rust queue_admit_result response envelope serializes (T11630)', () => {
|
|
194
202
|
const wire = {
|
|
195
|
-
protocol_version: '1.
|
|
203
|
+
protocol_version: '1.2.0',
|
|
196
204
|
id: 'qa-1',
|
|
197
205
|
direction: 'response',
|
|
198
206
|
response: {
|
|
@@ -208,7 +216,7 @@ describe('lease-ipc v1.1 wire-shape parity with Rust serde (T11627)', () => {
|
|
|
208
216
|
|
|
209
217
|
it('parses the exact JSON a Rust worker_heartbeat request envelope serializes (T11628)', () => {
|
|
210
218
|
const wire = {
|
|
211
|
-
protocol_version: '1.
|
|
219
|
+
protocol_version: '1.2.0',
|
|
212
220
|
id: 'hb-1',
|
|
213
221
|
direction: 'request',
|
|
214
222
|
request: {
|
|
@@ -226,7 +234,7 @@ describe('lease-ipc v1.1 wire-shape parity with Rust serde (T11627)', () => {
|
|
|
226
234
|
|
|
227
235
|
it('parses the exact JSON a Rust heartbeat_ack response envelope serializes (T11628)', () => {
|
|
228
236
|
const wire = {
|
|
229
|
-
protocol_version: '1.
|
|
237
|
+
protocol_version: '1.2.0',
|
|
230
238
|
id: 'hb-1',
|
|
231
239
|
direction: 'response',
|
|
232
240
|
response: { kind: 'heartbeat_ack' },
|
|
@@ -238,9 +246,61 @@ describe('lease-ipc v1.1 wire-shape parity with Rust serde (T11627)', () => {
|
|
|
238
246
|
}
|
|
239
247
|
});
|
|
240
248
|
|
|
249
|
+
it('parses the exact JSON a Rust resource_admit request envelope serializes (T12001)', () => {
|
|
250
|
+
const wire = {
|
|
251
|
+
protocol_version: '1.2.0',
|
|
252
|
+
id: 'ra-1',
|
|
253
|
+
direction: 'request',
|
|
254
|
+
request: {
|
|
255
|
+
kind: 'resource_admit',
|
|
256
|
+
class: 'db-heavy',
|
|
257
|
+
holder_id: 'pid-42:db-heavy',
|
|
258
|
+
budget: 1,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
const parsed = LeaseIpcEnvelopeSchema.safeParse(wire);
|
|
262
|
+
expect(parsed.success).toBe(true);
|
|
263
|
+
if (parsed.success && parsed.data.direction === 'request') {
|
|
264
|
+
expect(parsed.data.request.kind).toBe('resource_admit');
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('parses the exact JSON a Rust resource_admit_result response envelope serializes (T12001)', () => {
|
|
269
|
+
const wire = {
|
|
270
|
+
protocol_version: '1.2.0',
|
|
271
|
+
id: 'ra-1',
|
|
272
|
+
direction: 'response',
|
|
273
|
+
response: {
|
|
274
|
+
kind: 'resource_admit_result',
|
|
275
|
+
disposition: 'deferred',
|
|
276
|
+
retry_after_ms: 2000,
|
|
277
|
+
slots_remaining: 0,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
const parsed = LeaseIpcEnvelopeSchema.safeParse(wire);
|
|
281
|
+
expect(parsed.success).toBe(true);
|
|
282
|
+
if (parsed.success && parsed.data.direction === 'response') {
|
|
283
|
+
expect(parsed.data.response.kind).toBe('resource_admit_result');
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('parses the exact JSON a Rust resource_release_result response envelope serializes (T12001)', () => {
|
|
288
|
+
const wire = {
|
|
289
|
+
protocol_version: '1.2.0',
|
|
290
|
+
id: 'rr-1',
|
|
291
|
+
direction: 'response',
|
|
292
|
+
response: { kind: 'resource_release_result', released: true, slots_remaining: 1 },
|
|
293
|
+
};
|
|
294
|
+
const parsed = LeaseIpcEnvelopeSchema.safeParse(wire);
|
|
295
|
+
expect(parsed.success).toBe(true);
|
|
296
|
+
if (parsed.success && parsed.data.direction === 'response') {
|
|
297
|
+
expect(parsed.data.response.kind).toBe('resource_release_result');
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
241
301
|
it('rejects an unknown message kind (proves the union is closed)', () => {
|
|
242
302
|
const wire = {
|
|
243
|
-
protocol_version: '1.
|
|
303
|
+
protocol_version: '1.2.0',
|
|
244
304
|
id: 'abc',
|
|
245
305
|
direction: 'request',
|
|
246
306
|
request: { kind: 'lease_teleport', scope: 'project', lane: 'tasks' },
|
|
@@ -250,7 +310,7 @@ describe('lease-ipc v1.1 wire-shape parity with Rust serde (T11627)', () => {
|
|
|
250
310
|
|
|
251
311
|
it('rejects an unknown scope/lane enum value (snake_case enums are closed)', () => {
|
|
252
312
|
const wire = {
|
|
253
|
-
protocol_version: '1.
|
|
313
|
+
protocol_version: '1.2.0',
|
|
254
314
|
id: 'abc',
|
|
255
315
|
direction: 'request',
|
|
256
316
|
request: {
|
|
@@ -298,6 +358,10 @@ function sampleRequestFor(kind: (typeof LEASE_IPC_REQUEST_KINDS)[number]): unkno
|
|
|
298
358
|
};
|
|
299
359
|
case 'worker_heartbeat':
|
|
300
360
|
return { kind, child_id: 'worker-1', in_flight_llm: true };
|
|
361
|
+
case 'resource_admit':
|
|
362
|
+
return { kind, class: 'db-heavy', holder_id: 'pid-42:db-heavy', budget: 1 };
|
|
363
|
+
case 'resource_release':
|
|
364
|
+
return { kind, class: 'db-heavy', holder_id: 'pid-42:db-heavy' };
|
|
301
365
|
}
|
|
302
366
|
}
|
|
303
367
|
|
|
@@ -336,6 +400,10 @@ function sampleResponseFor(kind: (typeof LEASE_IPC_RESPONSE_KINDS)[number]): unk
|
|
|
336
400
|
};
|
|
337
401
|
case 'heartbeat_ack':
|
|
338
402
|
return { kind };
|
|
403
|
+
case 'resource_admit_result':
|
|
404
|
+
return { kind, disposition: 'deferred', retry_after_ms: 2000, slots_remaining: 0 };
|
|
405
|
+
case 'resource_release_result':
|
|
406
|
+
return { kind, released: true, slots_remaining: 1 };
|
|
339
407
|
case 'error':
|
|
340
408
|
return { kind, code: 'E_X', message: 'x' };
|
|
341
409
|
}
|
package/src/lease-ipc/index.ts
CHANGED
|
@@ -36,6 +36,11 @@ export type {
|
|
|
36
36
|
QueuePriorityClass,
|
|
37
37
|
RateCheckRequest,
|
|
38
38
|
RateResultResponse,
|
|
39
|
+
ResourceAdmitDisposition,
|
|
40
|
+
ResourceAdmitRequest,
|
|
41
|
+
ResourceAdmitResultResponse,
|
|
42
|
+
ResourceReleaseRequest,
|
|
43
|
+
ResourceReleaseResultResponse,
|
|
39
44
|
ToolGrantedResponse,
|
|
40
45
|
ToolGrantRequest,
|
|
41
46
|
WorkerHeartbeatRequest,
|
|
@@ -68,6 +73,11 @@ export {
|
|
|
68
73
|
QueuePriorityClassSchema,
|
|
69
74
|
RateCheckRequestSchema,
|
|
70
75
|
RateResultResponseSchema,
|
|
76
|
+
ResourceAdmitDispositionSchema,
|
|
77
|
+
ResourceAdmitRequestSchema,
|
|
78
|
+
ResourceAdmitResultResponseSchema,
|
|
79
|
+
ResourceReleaseRequestSchema,
|
|
80
|
+
ResourceReleaseResultResponseSchema,
|
|
71
81
|
ToolGrantedResponseSchema,
|
|
72
82
|
ToolGrantRequestSchema,
|
|
73
83
|
WorkerHeartbeatRequestSchema,
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
*
|
|
13
13
|
* ```jsonc
|
|
14
14
|
* // request
|
|
15
|
-
* { "protocol_version": "1.
|
|
15
|
+
* { "protocol_version": "1.2.0", "id": "abc", "direction": "request",
|
|
16
16
|
* "request": { "kind": "lease_acquire", "scope": "project", "lane": "tasks",
|
|
17
17
|
* "holder_id": "pid-42:tasks", "priority": 0, "ttl_ms": 30000,
|
|
18
18
|
* "reentrant": true } }
|
|
19
19
|
* // response
|
|
20
|
-
* { "protocol_version": "1.
|
|
20
|
+
* { "protocol_version": "1.2.0", "id": "abc", "direction": "response",
|
|
21
21
|
* "response": { "kind": "lease_granted", "scope": "project", "lane": "tasks",
|
|
22
22
|
* "holder_id": "pid-42:tasks", "epoch": 7, "ttl_ms": 30000,
|
|
23
23
|
* "expires_at_ms": 1000030000 } }
|
|
@@ -229,6 +229,50 @@ export const WorkerHeartbeatRequestSchema = z
|
|
|
229
229
|
/** Worker-heartbeat request payload (inferred from {@link WorkerHeartbeatRequestSchema}). */
|
|
230
230
|
export type WorkerHeartbeatRequest = z.infer<typeof WorkerHeartbeatRequestSchema>;
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Admit (or defer) a heavy resource-intensive operation through the central
|
|
234
|
+
* per-class concurrency arbiter (T12001 · Epic T11992). Mirrors
|
|
235
|
+
* `ResourceAdmitReq`. `[wired]`
|
|
236
|
+
*
|
|
237
|
+
* The client computes `budget` from its local memory-pressure sample; the
|
|
238
|
+
* supervisor enforces the per-class in-flight COUNT centrally across processes,
|
|
239
|
+
* so heavy ops (builds, tests, exodus) can never co-schedule past the budget.
|
|
240
|
+
*/
|
|
241
|
+
export const ResourceAdmitRequestSchema = z
|
|
242
|
+
.object({
|
|
243
|
+
/** Tag discriminating this request variant. */
|
|
244
|
+
kind: z.literal('resource_admit'),
|
|
245
|
+
/** The resource class (e.g. `"db-heavy"`, `"full-build"`, `"test-run"`). */
|
|
246
|
+
class: z.string().min(1),
|
|
247
|
+
/** The caller holding the slot — process/worktree-unique; the release key. */
|
|
248
|
+
holder_id: z.string().min(1),
|
|
249
|
+
/** The client-computed budget: the max concurrent holders for this class. */
|
|
250
|
+
budget: z.number().int().nonnegative(),
|
|
251
|
+
})
|
|
252
|
+
.strict();
|
|
253
|
+
|
|
254
|
+
/** Resource-admit request payload (inferred from {@link ResourceAdmitRequestSchema}). */
|
|
255
|
+
export type ResourceAdmitRequest = z.infer<typeof ResourceAdmitRequestSchema>;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Release a previously-admitted resource slot (T12001). Mirrors
|
|
259
|
+
* `ResourceReleaseReq`. Idempotent — releasing an unknown holder is a no-op.
|
|
260
|
+
* `[wired]`
|
|
261
|
+
*/
|
|
262
|
+
export const ResourceReleaseRequestSchema = z
|
|
263
|
+
.object({
|
|
264
|
+
/** Tag discriminating this request variant. */
|
|
265
|
+
kind: z.literal('resource_release'),
|
|
266
|
+
/** The resource class the slot belongs to. */
|
|
267
|
+
class: z.string().min(1),
|
|
268
|
+
/** The holder that was admitted (matches the admit `holder_id`). */
|
|
269
|
+
holder_id: z.string().min(1),
|
|
270
|
+
})
|
|
271
|
+
.strict();
|
|
272
|
+
|
|
273
|
+
/** Resource-release request payload (inferred from {@link ResourceReleaseRequestSchema}). */
|
|
274
|
+
export type ResourceReleaseRequest = z.infer<typeof ResourceReleaseRequestSchema>;
|
|
275
|
+
|
|
232
276
|
/**
|
|
233
277
|
* The discriminated union of all client → arbiter lease requests. The `kind`
|
|
234
278
|
* field selects the variant, matching the serde `#[serde(tag = "kind")]`
|
|
@@ -242,6 +286,8 @@ export const LeaseIpcRequestSchema = z.discriminatedUnion('kind', [
|
|
|
242
286
|
ToolGrantRequestSchema,
|
|
243
287
|
QueueAdmitRequestSchema,
|
|
244
288
|
WorkerHeartbeatRequestSchema,
|
|
289
|
+
ResourceAdmitRequestSchema,
|
|
290
|
+
ResourceReleaseRequestSchema,
|
|
245
291
|
]);
|
|
246
292
|
|
|
247
293
|
/** Any lease IPC request (inferred from {@link LeaseIpcRequestSchema}). */
|
|
@@ -445,6 +491,54 @@ export const HeartbeatAckResponseSchema = z
|
|
|
445
491
|
/** Heartbeat-ack response payload (inferred from {@link HeartbeatAckResponseSchema}). */
|
|
446
492
|
export type HeartbeatAckResponse = z.infer<typeof HeartbeatAckResponseSchema>;
|
|
447
493
|
|
|
494
|
+
/**
|
|
495
|
+
* The disposition of a `resource_admit` request (T12001) — `admitted` (run the
|
|
496
|
+
* heavy op now) or `deferred` (back off `retry_after_ms` and re-request; never a
|
|
497
|
+
* silent drop). Mirrors `ResourceAdmitDisposition`.
|
|
498
|
+
*/
|
|
499
|
+
export const ResourceAdmitDispositionSchema = z.enum(['admitted', 'deferred']);
|
|
500
|
+
|
|
501
|
+
/** A `resource_admit` disposition (inferred from {@link ResourceAdmitDispositionSchema}). */
|
|
502
|
+
export type ResourceAdmitDisposition = z.infer<typeof ResourceAdmitDispositionSchema>;
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Reply to a `resource_admit`: the heavy op was admitted or deferred (T12001).
|
|
506
|
+
* Mirrors `ResourceAdmitResult`.
|
|
507
|
+
*/
|
|
508
|
+
export const ResourceAdmitResultResponseSchema = z
|
|
509
|
+
.object({
|
|
510
|
+
/** Tag discriminating this response variant. */
|
|
511
|
+
kind: z.literal('resource_admit_result'),
|
|
512
|
+
/** Whether the heavy op was admitted or deferred. */
|
|
513
|
+
disposition: ResourceAdmitDispositionSchema,
|
|
514
|
+
/** Back-off in ms before re-requesting (0 when admitted). */
|
|
515
|
+
retry_after_ms: z.number().int().nonnegative(),
|
|
516
|
+
/** Remaining slots for the class after this decision. */
|
|
517
|
+
slots_remaining: z.number().int().nonnegative(),
|
|
518
|
+
})
|
|
519
|
+
.strict();
|
|
520
|
+
|
|
521
|
+
/** Resource-admit result payload (inferred from {@link ResourceAdmitResultResponseSchema}). */
|
|
522
|
+
export type ResourceAdmitResultResponse = z.infer<typeof ResourceAdmitResultResponseSchema>;
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Reply to a `resource_release`: the slot was released (T12001). Mirrors
|
|
526
|
+
* `ResourceReleaseResult`.
|
|
527
|
+
*/
|
|
528
|
+
export const ResourceReleaseResultResponseSchema = z
|
|
529
|
+
.object({
|
|
530
|
+
/** Tag discriminating this response variant. */
|
|
531
|
+
kind: z.literal('resource_release_result'),
|
|
532
|
+
/** Whether a held slot was actually released (false = no such holder). */
|
|
533
|
+
released: z.boolean(),
|
|
534
|
+
/** Remaining in-flight holders for the class after the release. */
|
|
535
|
+
slots_remaining: z.number().int().nonnegative(),
|
|
536
|
+
})
|
|
537
|
+
.strict();
|
|
538
|
+
|
|
539
|
+
/** Resource-release result payload (inferred from {@link ResourceReleaseResultResponseSchema}). */
|
|
540
|
+
export type ResourceReleaseResultResponse = z.infer<typeof ResourceReleaseResultResponseSchema>;
|
|
541
|
+
|
|
448
542
|
/**
|
|
449
543
|
* An error response correlated to a request. Mirrors the v1.0
|
|
450
544
|
* `crate::ipc::ErrorResult` shape reused by the Rust `LeaseResponse::Error`
|
|
@@ -479,6 +573,8 @@ export const LeaseIpcResponseSchema = z.discriminatedUnion('kind', [
|
|
|
479
573
|
ChildKilledUnresponsiveResponseSchema,
|
|
480
574
|
QueueAdmitResultResponseSchema,
|
|
481
575
|
HeartbeatAckResponseSchema,
|
|
576
|
+
ResourceAdmitResultResponseSchema,
|
|
577
|
+
ResourceReleaseResultResponseSchema,
|
|
482
578
|
LeaseErrorResponseSchema,
|
|
483
579
|
]);
|
|
484
580
|
|
|
@@ -559,10 +655,12 @@ export const LEASE_IPC_REQUEST_KINDS = [
|
|
|
559
655
|
'tool_grant',
|
|
560
656
|
'queue_admit',
|
|
561
657
|
'worker_heartbeat',
|
|
658
|
+
'resource_admit',
|
|
659
|
+
'resource_release',
|
|
562
660
|
] as const;
|
|
563
661
|
|
|
564
662
|
/**
|
|
565
|
-
* The v1.
|
|
663
|
+
* The v1.2 response `kind` values, in declaration order. The schema-drift guard
|
|
566
664
|
* test pins this tuple.
|
|
567
665
|
*/
|
|
568
666
|
export const LEASE_IPC_RESPONSE_KINDS = [
|
|
@@ -575,6 +673,8 @@ export const LEASE_IPC_RESPONSE_KINDS = [
|
|
|
575
673
|
'child_killed_unresponsive',
|
|
576
674
|
'queue_admit_result',
|
|
577
675
|
'heartbeat_ack',
|
|
676
|
+
'resource_admit_result',
|
|
677
|
+
'resource_release_result',
|
|
578
678
|
'error',
|
|
579
679
|
] as const;
|
|
580
680
|
|
package/src/lease-ipc/version.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* TypeScript contract MUST agree on this value. It runs **in parallel** to the
|
|
6
6
|
* byte-frozen `supervisor-ipc` v1.0 contract: the two are distinguished on the
|
|
7
7
|
* wire purely by this version string, so a single accept loop can route
|
|
8
|
-
* `'1.0.0'` → the v1.0 union and `'1.
|
|
8
|
+
* `'1.0.0'` → the v1.0 union and `'1.2.0'` → the lease union. Bump only via a
|
|
9
9
|
* coordinated dual (Rust + TS) edit, never edit this value in place.
|
|
10
10
|
*
|
|
11
11
|
* @see {@link LEASE_IPC_MESSAGE_KINDS} for the v1.1 message set.
|
|
@@ -14,13 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* The `lease-ipc` v1.
|
|
17
|
+
* The `lease-ipc` v1.2 protocol version string.
|
|
18
18
|
*
|
|
19
19
|
* Mirrors `cleo_supervisor::lease_ipc::LEASE_IPC_PROTOCOL_VERSION` on the Rust
|
|
20
20
|
* side. Both the Rust schema-drift guard (`lease_ipc::tests`) and the TS drift
|
|
21
21
|
* test (`__tests__/freeze.test.ts`) pin this exact value.
|
|
22
|
+
*
|
|
23
|
+
* v1.2 (T12001 · Epic T11992) added the `resource_admit` / `resource_release`
|
|
24
|
+
* heavy-op admission verbs; v1.1 added `queue_admit` / `worker_heartbeat`.
|
|
22
25
|
*/
|
|
23
|
-
export const LEASE_IPC_PROTOCOL_VERSION = '1.
|
|
26
|
+
export const LEASE_IPC_PROTOCOL_VERSION = '1.2.0' as const;
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Whether the given version string is the frozen `lease-ipc` v1.1 wire version.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Governor — shared types for the Never-OOM admission layer.
|
|
3
|
+
*
|
|
4
|
+
* The {@link ResourceGovernor} (in `@cleocode/core`) admits resource-intensive
|
|
5
|
+
* work through priority classes whose budgets are computed from host memory and
|
|
6
|
+
* memory-pressure (PSI). A denial returns a structured, retryable
|
|
7
|
+
* {@link ResourceDeferral} (code {@link RESOURCE_DEFERRED_CODE}) rather than a
|
|
8
|
+
* silent drop or a crash — orchestrators treat it like a lifecycle gate: wait
|
|
9
|
+
* and retry, never fail the task.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the llm-queue deferral contract (`retry_after_ms`, degrade-to-direct)
|
|
12
|
+
* so the two admission surfaces share one shape.
|
|
13
|
+
*
|
|
14
|
+
* @task T11999
|
|
15
|
+
* @epic T11992
|
|
16
|
+
* @adr resource-governor-never-oom-architecture §3.4
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Governor arbitration mode. Mirrors the writer-lease mode shape
|
|
21
|
+
* (`writer-lease.ts` {@link LeaseMode}).
|
|
22
|
+
*
|
|
23
|
+
* - `supervisor` — defer to the Rust `cleo-supervisor` `resource_admit` verb
|
|
24
|
+
* (continuous PSI, priority wakeups). Demotes to `local` when the supervisor
|
|
25
|
+
* IPC client is not wired/reachable (never deadlocks).
|
|
26
|
+
* - `local` — DEFAULT. Daemon-off arbitration through shared per-class slot
|
|
27
|
+
* directories (proper-lockfile crash-stale auto-release) + a point-sample of
|
|
28
|
+
* {@link ResourceSample} taken inside `acquire`. Genuinely cross-process
|
|
29
|
+
* without a daemon, mirroring the tool-semaphore engine.
|
|
30
|
+
* - `off` — pure pass-through; every acquire is granted immediately.
|
|
31
|
+
*/
|
|
32
|
+
export type GovernorMode = 'supervisor' | 'local' | 'off';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Priority classes (highest priority first). `interactive-cli` is NEVER gated;
|
|
36
|
+
* `full-build` is pinned to one machine-wide slot regardless of pressure.
|
|
37
|
+
*
|
|
38
|
+
* @adr resource-governor-never-oom-architecture §3.4 (classes)
|
|
39
|
+
*/
|
|
40
|
+
export type ResourceClass =
|
|
41
|
+
| 'interactive-cli'
|
|
42
|
+
| 'agent-session'
|
|
43
|
+
| 'llm-call'
|
|
44
|
+
| 'test-run'
|
|
45
|
+
| 'scoped-build'
|
|
46
|
+
| 'full-build'
|
|
47
|
+
| 'db-heavy'
|
|
48
|
+
| 'background-autonomous';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* All resource classes in descending priority order. Single source of truth for
|
|
52
|
+
* iteration + validation.
|
|
53
|
+
*/
|
|
54
|
+
export const RESOURCE_CLASSES: readonly ResourceClass[] = Object.freeze([
|
|
55
|
+
'interactive-cli',
|
|
56
|
+
'agent-session',
|
|
57
|
+
'llm-call',
|
|
58
|
+
'test-run',
|
|
59
|
+
'scoped-build',
|
|
60
|
+
'full-build',
|
|
61
|
+
'db-heavy',
|
|
62
|
+
'background-autonomous',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/** Error code emitted when an acquire is denied because no slot is available. */
|
|
66
|
+
export const RESOURCE_DEFERRED_CODE = 'E_RESOURCE_DEFERRED' as const;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Soft-signal code raised on the next admit when the host enters `backoff` —
|
|
70
|
+
* asks granted work to checkpoint. Existing grants are NEVER revoked.
|
|
71
|
+
*/
|
|
72
|
+
export const RESOURCE_BACKPRESSURE_CODE = 'E_RESOURCE_BACKPRESSURE' as const;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Structured, retryable deferral returned when admission is denied. Never a
|
|
76
|
+
* silent drop; callers back off `retryAfterMs` and re-request, or annotate the
|
|
77
|
+
* unit as deferred and let a pull-based retry pick it up.
|
|
78
|
+
*/
|
|
79
|
+
export interface ResourceDeferral {
|
|
80
|
+
/** Discriminant for narrowing against the success grant. */
|
|
81
|
+
readonly deferred: true;
|
|
82
|
+
/** The class whose budget was exhausted. */
|
|
83
|
+
readonly class: ResourceClass;
|
|
84
|
+
/** Suggested back-off before re-requesting, in milliseconds. */
|
|
85
|
+
readonly retryAfterMs: number;
|
|
86
|
+
/** Human-readable reason (pressure state, budget, held count). */
|
|
87
|
+
readonly reason: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A granted admission slot. Hold it for the lifetime of the work, then
|
|
92
|
+
* {@link ResourceGrant.release}. Releasing reaps the slot so the next acquirer
|
|
93
|
+
* can proceed; releasing twice is a no-op.
|
|
94
|
+
*/
|
|
95
|
+
export interface ResourceGrant {
|
|
96
|
+
/** Discriminant for narrowing against {@link ResourceDeferral}. */
|
|
97
|
+
readonly deferred: false;
|
|
98
|
+
/** The class this grant belongs to. */
|
|
99
|
+
readonly class: ResourceClass;
|
|
100
|
+
/**
|
|
101
|
+
* Slot index held (local mode), or `-1` for an ungated pass-through grant
|
|
102
|
+
* (`interactive-cli`, `off` mode, or an unbounded budget).
|
|
103
|
+
*/
|
|
104
|
+
readonly slot: number;
|
|
105
|
+
/** Monotonic timestamp (ms) when the grant was acquired. */
|
|
106
|
+
readonly acquiredAtMs: number;
|
|
107
|
+
/** Release the slot. Idempotent. */
|
|
108
|
+
release(): Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Discriminated union returned by a non-blocking `tryAcquire`. */
|
|
112
|
+
export type AdmissionResult = ResourceGrant | ResourceDeferral;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Default back-off hint when a class is saturated, in milliseconds. Aligned
|
|
116
|
+
* with the llm-queue `DEFAULT_ADMIT_DEADLINE_MS` so the two surfaces agree.
|
|
117
|
+
*/
|
|
118
|
+
export const DEFAULT_RESOURCE_RETRY_AFTER_MS = 2_000;
|
|
119
|
+
|
|
120
|
+
/** Type guard: did an admission attempt produce a usable grant? */
|
|
121
|
+
export function isResourceGrant(r: AdmissionResult): r is ResourceGrant {
|
|
122
|
+
return r.deferred === false;
|
|
123
|
+
}
|