@lensagent/core 0.1.0 → 0.1.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.
Files changed (99) hide show
  1. package/README.md +63 -4
  2. package/config/lens.example.jsonc +36 -13
  3. package/dist/adapters/executor-router.d.ts +20 -0
  4. package/dist/adapters/executor-router.js +86 -0
  5. package/dist/adapters/executor.d.ts +9 -3
  6. package/dist/adapters/executor.js +196 -61
  7. package/dist/adapters/lsp.d.ts +2 -1
  8. package/dist/adapters/lsp.js +98 -41
  9. package/dist/adapters/mcp.js +52 -17
  10. package/dist/adapters/revert.d.ts +5 -4
  11. package/dist/adapters/revert.js +6 -64
  12. package/dist/adapters/web.d.ts +9 -0
  13. package/dist/adapters/web.js +244 -0
  14. package/dist/config/files.d.ts +14 -0
  15. package/dist/config/files.js +57 -0
  16. package/dist/config/runtime.d.ts +3 -4
  17. package/dist/config/runtime.js +116 -97
  18. package/dist/config/schema.d.ts +56 -10
  19. package/dist/config/schema.js +48 -8
  20. package/dist/contracts/capability.d.ts +27 -1
  21. package/dist/contracts/domain.d.ts +1 -0
  22. package/dist/contracts/model.d.ts +33 -6
  23. package/dist/contracts/review.d.ts +17 -0
  24. package/dist/contracts/review.js +1 -0
  25. package/dist/core/access-policy.d.ts +40 -0
  26. package/dist/core/access-policy.js +68 -0
  27. package/dist/core/access.d.ts +30 -2
  28. package/dist/core/access.js +159 -26
  29. package/dist/core/context-boundaries.d.ts +25 -0
  30. package/dist/core/context-boundaries.js +106 -0
  31. package/dist/core/context.d.ts +3 -0
  32. package/dist/core/context.js +11 -0
  33. package/dist/core/host.js +261 -173
  34. package/dist/entry/pair.d.ts +1 -0
  35. package/dist/entry/pair.js +14 -0
  36. package/dist/entry/run.js +1 -1
  37. package/dist/execution/lsp-process.d.ts +3 -2
  38. package/dist/execution/lsp-process.js +46 -28
  39. package/dist/http/server.d.ts +5 -0
  40. package/dist/http/server.js +262 -80
  41. package/dist/runtime/agent.d.ts +9 -4
  42. package/dist/runtime/agent.js +361 -286
  43. package/dist/runtime/artifacts.d.ts +13 -1
  44. package/dist/runtime/artifacts.js +137 -10
  45. package/dist/runtime/cache.d.ts +59 -16
  46. package/dist/runtime/cache.js +124 -43
  47. package/dist/runtime/calls.d.ts +1 -1
  48. package/dist/runtime/calls.js +164 -48
  49. package/dist/runtime/capabilities.d.ts +2 -2
  50. package/dist/runtime/capabilities.js +4 -5
  51. package/dist/runtime/cleanup.d.ts +5 -2
  52. package/dist/runtime/cleanup.js +48 -17
  53. package/dist/runtime/compaction.d.ts +9 -0
  54. package/dist/runtime/compaction.js +52 -0
  55. package/dist/runtime/context.d.ts +8 -4
  56. package/dist/runtime/context.js +115 -128
  57. package/dist/runtime/events.d.ts +7 -0
  58. package/dist/runtime/events.js +83 -0
  59. package/dist/runtime/images.d.ts +14 -0
  60. package/dist/runtime/images.js +82 -0
  61. package/dist/runtime/kernel-capabilities.d.ts +7 -1
  62. package/dist/runtime/kernel-capabilities.js +146 -68
  63. package/dist/runtime/model-audit.d.ts +9 -0
  64. package/dist/runtime/model-audit.js +39 -0
  65. package/dist/runtime/model.d.ts +109 -4
  66. package/dist/runtime/model.js +368 -143
  67. package/dist/runtime/observability.d.ts +1 -0
  68. package/dist/runtime/observability.js +60 -15
  69. package/dist/runtime/policy.d.ts +1 -1
  70. package/dist/runtime/policy.js +2 -2
  71. package/dist/runtime/profile.d.ts +2 -1
  72. package/dist/runtime/profile.js +12 -15
  73. package/dist/runtime/projects.d.ts +3 -1
  74. package/dist/runtime/projects.js +38 -17
  75. package/dist/runtime/ptc-worker.d.ts +1 -0
  76. package/dist/runtime/ptc-worker.js +97 -0
  77. package/dist/runtime/ptc.d.ts +16 -0
  78. package/dist/runtime/ptc.js +128 -0
  79. package/dist/runtime/results.d.ts +5 -4
  80. package/dist/runtime/results.js +29 -51
  81. package/dist/runtime/review.d.ts +4 -1
  82. package/dist/runtime/review.js +101 -22
  83. package/dist/runtime/scheduler.js +1 -0
  84. package/dist/runtime/skills.js +34 -5
  85. package/dist/runtime/task-audit.d.ts +21 -0
  86. package/dist/runtime/task-audit.js +122 -0
  87. package/dist/runtime/time.d.ts +9 -0
  88. package/dist/runtime/time.js +18 -0
  89. package/dist/runtime/tokenizer.d.ts +2 -0
  90. package/dist/runtime/tokenizer.js +26 -1
  91. package/dist/store/database.js +26 -1
  92. package/dist/store/event-log.d.ts +1 -0
  93. package/dist/store/event-log.js +22 -5
  94. package/dist/store/schema.d.ts +1 -1
  95. package/dist/store/schema.js +50 -1
  96. package/dist/store/store.d.ts +65 -19
  97. package/dist/store/store.js +986 -188
  98. package/package.json +18 -8
  99. package/proto/executor.proto +69 -55
@@ -1,12 +1,18 @@
1
- import { applyPurge } from '../runtime/cleanup.js';
1
+ import { dirname, resolve } from 'node:path';
2
+ import { once } from 'node:events';
3
+ import { ConfigFiles } from '../config/files.js';
4
+ import { applyPurge, commitPurge } from '../runtime/cleanup.js';
2
5
  import Fastify from 'fastify';
3
6
  import { digest, expectedRevision, integer, jsonValue, optionalText, record, text } from '../core/codec.js';
4
7
  import { LensError, fail } from '../core/domain.js';
8
+ import { loopbackAddress } from '../core/access-policy.js';
5
9
  import { ExecutorCallError } from '../adapters/executor.js';
6
- import { resolveReasoningEffort } from '../runtime/profile.js';
10
+ import { modelReasoningEfforts, resolveReasoningEffort } from '../runtime/profile.js';
7
11
  import { componentIdentity } from '../core/identity.js';
8
12
  export function createServer(dependencies) {
9
13
  const app = Fastify({ logger: false, bodyLimit: 4 * 1024 * 1024, requestIdHeader: 'x-correlation-id', ajv: { customOptions: { removeAdditional: false } }, ...(dependencies.https === undefined ? {} : { https: dependencies.https }) });
14
+ let managedFiles;
15
+ const files = () => managedFiles ??= new ConfigFiles(dirname(dependencies.config.path), [dependencies.config.path, dependencies.pair.path]);
10
16
  const contexts = new WeakMap();
11
17
  const starts = new WeakMap();
12
18
  const specs = [];
@@ -20,19 +26,34 @@ export function createServer(dependencies) {
20
26
  error_code: typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code : null
21
27
  });
22
28
  }
29
+ dependencies.observability.record('http', 'request_error', 'failed', 0, { status: failure.status, error_code: failure.code });
23
30
  void reply.status(failure.status).send({
24
31
  error: { code: failure.code, message: failure.message, retryable: retryableFailure(failure.status), details: failure.details },
25
32
  correlation_id: request.id
26
33
  });
27
34
  });
28
- app.addHook('onRequest', async (request) => {
35
+ app.addHook('onRequest', async (request, reply) => {
29
36
  starts.set(request, performance.now());
30
37
  if (!request.url.startsWith('/api/v1/'))
31
38
  return;
39
+ if (!dependencies.config.current.config.tls.enabled && !loopbackAddress(request.socket.remoteAddress)) {
40
+ fail('HTTPS_REQUIRED', 403, 'Remote clients must use HTTPS.');
41
+ }
32
42
  const readiness = dependencies.ready();
33
43
  if (readiness.mode !== 'full')
34
44
  fail('RUNTIME_NOT_READY', 503, 'Lens Core is not ready.', { blockers: readiness.blockers });
35
- const ingress = await dependencies.pair.authorize(request.headers, dependencies.config.current.config.tls.enabled ? 'https' : 'http', request.id);
45
+ const ingress = await dependencies.pair.authorize(request.headers, dependencies.config.current.config.tls.enabled ? 'https' : 'http', request.id, true,
46
+ // The raw socket address, never `request.ip`: the latter honours forwarded headers, and a caller-supplied
47
+ // header must not be able to make a remote caller look local enough to admit itself.
48
+ request.socket.remoteAddress);
49
+ const path = request.url.split('?')[0] ?? '';
50
+ const sensitive = path === '/api/v1/config' || path === '/api/v1/config/validate' || path.startsWith('/api/v1/config/files') || /^\/api\/v1\/tasks\/[^/]+\/audit$/.test(path);
51
+ if (sensitive || !['GET', 'HEAD', 'OPTIONS'].includes(request.method)) {
52
+ if (!ingress.strongTrust)
53
+ fail('STRONG_TRUST_REQUIRED', 403, 'This operation requires a strongly trusted client.');
54
+ }
55
+ if (sensitive)
56
+ reply.header('Cache-Control', 'no-store');
36
57
  const projectId = resolveProjectId(request, dependencies.defaultProjectId);
37
58
  // Name the Project that was not found. An anonymous 404 here is indistinguishable from a
38
59
  // missing Route, and the id is supplied by the caller in a header, so echoing it back is
@@ -86,6 +107,7 @@ export function createServer(dependencies) {
86
107
  const bodySchema = requestBody(normalizedMethod, path);
87
108
  app.route({ method, url: path, handler, ...(bodySchema === null ? {} : { schema: { body: bodySchema } }) });
88
109
  };
110
+ const page = (request) => pageOptions(request, dependencies.config.current.config.snapshot);
89
111
  const project = (request) => {
90
112
  const context = contexts.get(request);
91
113
  if (context === undefined)
@@ -113,7 +135,7 @@ export function createServer(dependencies) {
113
135
  // in the 503 the two look alike, and offering a hard kill on that basis can terminate an
114
136
  // unrelated process. `pid` is what the endpoint-owner lookup verifies against.
115
137
  if (ready.mode !== 'full')
116
- return reply.status(503).send({ error: { code: 'RUNTIME_NOT_READY', message: 'Lens Core is not ready.', details: { blockers: ready.blockers, ...componentSummary() } } });
138
+ return reply.status(503).send({ error: { code: 'RUNTIME_NOT_READY', message: 'Lens Core is not ready.', retryable: true, details: { blockers: ready.blockers, ...componentSummary() } } });
117
139
  return {
118
140
  data: {
119
141
  status: 'ready',
@@ -156,9 +178,35 @@ export function createServer(dependencies) {
156
178
  });
157
179
  route('PUT', '/api/v1/config', async (request) => {
158
180
  const input = body(request);
159
- await dependencies.config.replace(text(input.content, 'content', 4 * 1024 * 1024), text(input.expected_file_digest, 'expected_file_digest'));
181
+ await dependencies.config.replace(text(input.content, 'content', 4 * 1024 * 1024), text(input.expected_file_digest, 'expected_file_digest'), () => dependencies.pair.requireStrong(request.headers));
160
182
  return { data: dependencies.config.document(), meta: configMeta(dependencies) };
161
183
  });
184
+ route('GET', '/api/v1/access', (request) => ({ data: { client_id: contexts.get(request)?.ingress.clientId ?? null, strong_trust: contexts.get(request)?.ingress.strongTrust === true } }));
185
+ route('GET', '/api/v1/config/files', async () => ({ data: [{ id: 'lens', format: 'jsonc' }, { id: 'pair', format: 'json' }, ...(await files().list()).map(id => ({ id, format: id.endsWith('jsonc') ? 'jsonc' : 'json' }))] }));
186
+ route('GET', '/api/v1/config/files/:file_id', async (request) => {
187
+ const fileId = param(request, 'file_id');
188
+ if (fileId === 'lens')
189
+ return { data: dependencies.config.document() };
190
+ if (fileId === 'pair')
191
+ return { data: await dependencies.pair.document() };
192
+ return { data: await files().document(fileId) };
193
+ });
194
+ route('PUT', '/api/v1/config/files/:file_id', async (request) => {
195
+ const input = body(request);
196
+ const source = text(input.content, 'content', 4 * 1024 * 1024);
197
+ const expected = text(input.expected_file_digest, 'expected_file_digest');
198
+ const fileId = param(request, 'file_id');
199
+ if (fileId === 'lens') {
200
+ await dependencies.config.replace(source, expected, () => dependencies.pair.requireStrong(request.headers));
201
+ return { data: dependencies.config.document() };
202
+ }
203
+ if (fileId === 'pair') {
204
+ await dependencies.pair.replace(source, expected, request.headers);
205
+ return { data: { saved: true } };
206
+ }
207
+ await files().replace(fileId, source, expected, () => dependencies.pair.requireStrong(request.headers));
208
+ return { data: await files().document(fileId) };
209
+ });
162
210
  route('GET', '/api/v1/config/options', (request) => ({ data: configOptions(dependencies, project(request)) }));
163
211
  route('GET', '/api/v1/capabilities', (request) => {
164
212
  // 02A: Page<CapabilityCatalogEntry>. A bare array shares no shape with that, so a client
@@ -167,15 +215,58 @@ export function createServer(dependencies) {
167
215
  return { data: wholePage(dependencies.runtime(project(request)).registry.catalog(), dependencies) };
168
216
  });
169
217
  route('GET', '/api/v1/capabilities/:capability_id', (request) => ({ data: dependencies.runtime(project(request)).registry.get(param(request, 'capability_id')) }));
218
+ async function projectExecutorOperation(operation) {
219
+ try {
220
+ return await operation();
221
+ }
222
+ catch (error) {
223
+ if (error instanceof ExecutorCallError) {
224
+ if ([7, 16].includes(error.grpcCode))
225
+ fail('EXECUTOR_ACCESS_DENIED', 403, 'Executor denied access. Check Core approval, directory authorization and whether this operation is allowed before retrying.');
226
+ if ([3, 5].includes(error.grpcCode))
227
+ fail('EXECUTOR_PATH_INVALID', 400, 'Executor could not browse the requested server directory.');
228
+ fail('EXECUTOR_UNAVAILABLE', 503, 'Executor is unavailable.');
229
+ }
230
+ throw error;
231
+ }
232
+ }
170
233
  route('GET', '/api/v1/projects', (request) => ({ data: dependencies.store.listProjects(page(request), archiveMode(request)) }));
171
- route('GET', '/api/v1/projects/browse', async (request, reply) => ({ data: await dependencies.executor.browse(optionalText(query(request).path, 'path') ?? process.cwd(), clientSignal(request, reply)) }));
172
- route('POST', '/api/v1/projects/browse/open', async (request, reply) => ({ data: await dependencies.executor.open(paths(body(request).workspace_paths), text(body(request).path, 'path'), clientSignal(request, reply)) }));
173
- route('POST', '/api/v1/projects/choose-folder', async (request, reply) => ({ data: await dependencies.executor.chooseFolder(optionalText(body(request).initial_path, 'initial_path'), clientSignal(request, reply)) }));
234
+ const endpoint = (value) => {
235
+ const selected = executorId(value);
236
+ if (dependencies.executorRouter)
237
+ return dependencies.executorRouter.endpoint(selected);
238
+ if (selected === 'remote')
239
+ return dependencies.executor;
240
+ if (selected !== 'local')
241
+ fail('EXECUTOR_NOT_CONFIGURED', 409, 'Executor is not configured.');
242
+ if (!dependencies.localExecutor)
243
+ fail('LOCAL_EXECUTOR_NOT_CONFIGURED', 409, 'Local Executor is not configured.');
244
+ return dependencies.localExecutor;
245
+ };
246
+ route('GET', '/api/v1/executors', () => {
247
+ const config = dependencies.config.current.config;
248
+ const entries = [
249
+ { executor_id: 'remote', name: config.executor.sharedFilesystem === true && !dependencies.localExecutor ? 'This computer' : 'Primary Executor', is_local: config.executor.sharedFilesystem === true && !dependencies.localExecutor, default_workspace_path: config.executor.sharedFilesystem === true ? resolve(dirname(dependencies.config.path), '../workspace') : null },
250
+ ...(dependencies.localExecutor ? [{ executor_id: 'local', name: 'This computer', is_local: true, default_workspace_path: resolve(dirname(dependencies.config.path), '../workspace') }] : []),
251
+ ...Object.entries(config.executors ?? {}).map(([id, value]) => ({ executor_id: id, name: value.name, is_local: false, default_workspace_path: null }))
252
+ ];
253
+ return { data: { items: entries } };
254
+ });
255
+ route('GET', '/api/v1/projects/browse', async (request, reply) => ({ data: await projectExecutorOperation(() => endpoint(query(request).executor_id).browse(optionalText(query(request).path, 'path') ?? '.', clientSignal(request, reply))) }));
256
+ route('POST', '/api/v1/projects/browse/open', async (request, reply) => ({ data: await endpoint(body(request).executor_id).open(paths(body(request).workspace_paths), text(body(request).path, 'path'), clientSignal(request, reply)) }));
257
+ route('POST', '/api/v1/projects/choose-folder', async (request, reply) => {
258
+ const selected = executorId(body(request).executor_id);
259
+ const isLocal = selected === 'local' ? dependencies.localExecutor !== undefined : selected === 'remote' && dependencies.config.current.config.executor.sharedFilesystem === true && !dependencies.localExecutor;
260
+ if (!isLocal)
261
+ fail('NATIVE_FOLDER_PICKER_LOCAL_ONLY', 400, 'Native folder selection is only available on this computer. Enter a remote folder path manually.');
262
+ return { data: await projectExecutorOperation(() => endpoint(selected).chooseFolder(optionalText(body(request).initial_path, 'initial_path'), clientSignal(request, reply))) };
263
+ });
174
264
  route('POST', '/api/v1/projects', async (request, reply) => {
175
265
  const input = body(request);
176
266
  const workspacePaths = paths(input.workspace_paths ?? input.paths);
177
- const validation = await validateWorkspacePaths(dependencies, workspacePaths, input.create_if_missing === true);
178
- return reply.status(201).send({ data: dependencies.store.createProject(text(input.name, 'name', 256), validatedWorkspacePaths(validation)) });
267
+ const selected = executorId(input.executor_id);
268
+ const validation = await validateWorkspacePaths({ executor: endpoint(selected) }, workspacePaths, input.create_if_missing === true);
269
+ return reply.status(201).send({ data: dependencies.store.createProject(text(input.name, 'name', 256), validatedWorkspacePaths(validation), selected) });
179
270
  });
180
271
  route('GET', '/api/v1/projects/:project_id', (request) => ({ data: dependencies.store.projectDetail(param(request, 'project_id')) }));
181
272
  route('PATCH', '/api/v1/projects/:project_id', (request) => ({ data: dependencies.store.updateProject(param(request, 'project_id'), expectedRevision(body(request).expected_revision), text(body(request).name, 'name', 256)) }));
@@ -186,8 +277,9 @@ export function createServer(dependencies) {
186
277
  const projectId = param(request, 'project_id');
187
278
  const input = body(request);
188
279
  const workspacePaths = paths(input.paths);
189
- const validation = await validateWorkspacePaths(dependencies, workspacePaths, input.create_if_missing === true);
190
- const workspace = dependencies.store.createWorkspace(projectId, text(input.name, 'name', 256), validatedWorkspacePaths(validation), input.is_default === true);
280
+ const selected = executorId(input.executor_id);
281
+ const validation = await validateWorkspacePaths({ executor: endpoint(selected) }, workspacePaths, input.create_if_missing === true);
282
+ const workspace = dependencies.store.createWorkspace(projectId, text(input.name, 'name', 256), validatedWorkspacePaths(validation), input.is_default === true, selected);
191
283
  dependencies.refreshProject(projectId);
192
284
  return reply.status(201).send({ data: workspace });
193
285
  });
@@ -196,7 +288,10 @@ export function createServer(dependencies) {
196
288
  const projectId = param(request, 'project_id');
197
289
  const input = body(request);
198
290
  const workspacePaths = paths(input.paths);
199
- const validation = await validateWorkspacePaths(dependencies, workspacePaths, input.create_if_missing === true);
291
+ const existing = dependencies.store.getWorkspace(projectId, param(request, 'workspace_id'));
292
+ if (input.executor_id !== undefined && input.executor_id !== existing.executor_id)
293
+ fail('WORKSPACE_EXECUTOR_IMMUTABLE', 409, 'Create another Workspace to change execution endpoint.');
294
+ const validation = await validateWorkspacePaths({ executor: endpoint(existing.executor_id) }, workspacePaths, input.create_if_missing === true);
200
295
  const workspace = dependencies.store.updateWorkspace(projectId, param(request, 'workspace_id'), expectedRevision(input.expected_revision), text(input.name, 'name', 256), validatedWorkspacePaths(validation), input.is_default === true);
201
296
  dependencies.refreshProject(projectId);
202
297
  return { data: workspace };
@@ -212,12 +307,50 @@ export function createServer(dependencies) {
212
307
  route('GET', '/api/v1/tasks', (request) => ({ data: dependencies.store.listAllTasks(project(request), page(request), query(request).archived === 'all') }));
213
308
  route('POST', '/api/v1/tasks', (request, reply) => reply.status(201).send({ data: dependencies.store.createTask(project(request), text(body(request).title, 'title', 256)) }));
214
309
  route('GET', '/api/v1/tasks/:task_id', (request) => ({ data: dependencies.store.taskDetail(project(request), param(request, 'task_id')) }));
310
+ route('GET', '/api/v1/tasks/:task_id/audit', async (request) => {
311
+ const projectId = project(request);
312
+ const taskId = param(request, 'task_id');
313
+ dependencies.store.getTask(projectId, taskId);
314
+ const options = page(request);
315
+ const source = optionalText(query(request).source, 'source') ?? 'core';
316
+ if (source !== 'core' && source !== 'executor')
317
+ fail('AUDIT_SOURCE_INVALID', 400, 'Audit source must be core or executor.');
318
+ if (source === 'executor') {
319
+ try {
320
+ return { data: await endpoint(query(request).executor_id).taskAudit(projectId, taskId, options.limit, options.cursor) };
321
+ }
322
+ catch (error) {
323
+ if (error instanceof ExecutorCallError) {
324
+ if (error.grpcCode === 3)
325
+ fail('AUDIT_CURSOR_INVALID', 400, 'Executor rejected the audit query or cursor.');
326
+ if (error.grpcCode === 9)
327
+ fail('AUDIT_CURSOR_STALE', 409, 'Executor audit cursor is stale.');
328
+ if ([7, 16].includes(error.grpcCode))
329
+ fail('EXECUTOR_AUDIT_FORBIDDEN', 403, 'Executor refused the audit query.');
330
+ if (error.grpcCode === 15)
331
+ fail('AUDIT_INTEGRITY_ERROR', 503, 'Executor audit integrity check failed.');
332
+ if (error.grpcCode === 8)
333
+ fail('AUDIT_QUERY_LIMIT', 503, 'Executor audit query exceeded its scan or response limits.');
334
+ if (error.grpcCode === 12)
335
+ fail('EXECUTOR_AUDIT_UNSUPPORTED', 503, 'Executor does not support audit queries; deploy a matching protocol version.');
336
+ }
337
+ if (error instanceof LensError)
338
+ throw error;
339
+ fail('EXECUTOR_AUDIT_UNAVAILABLE', 503, 'Executor audit storage or transport is unavailable.');
340
+ }
341
+ }
342
+ if (dependencies.taskAudit === undefined)
343
+ fail('AUDIT_UNAVAILABLE', 503, 'Task audit storage is not configured.');
344
+ return { data: await dependencies.taskAudit.query(projectId, taskId, options.limit, options.cursor) };
345
+ });
215
346
  route('PATCH', '/api/v1/tasks/:task_id', (request) => ({ data: dependencies.store.updateTask(project(request), param(request, 'task_id'), expectedRevision(body(request).expected_revision), text(body(request).title, 'title', 256)) }));
216
347
  route('POST', '/api/v1/tasks/:task_id/archive', (request) => ({ data: dependencies.store.archiveTask(project(request), param(request, 'task_id'), expectedRevision(body(request).expected_revision), true) }));
217
348
  route('POST', '/api/v1/tasks/:task_id/restore', (request) => ({ data: dependencies.store.archiveTask(project(request), param(request, 'task_id'), expectedRevision(body(request).expected_revision), false) }));
218
349
  route('DELETE', '/api/v1/tasks/:task_id', async (request) => {
219
- const purged = dependencies.store.deleteTask(project(request), param(request, 'task_id'), queryRevision(request));
220
- await applyPurge(purged, dependencies.artifacts, dependencies.executor);
350
+ const purged = await commitPurge(dependencies.executorRouter ?? dependencies.executor, stateId => dependencies.store.deleteTask(project(request), param(request, 'task_id'), queryRevision(request), stateId));
351
+ if ([purged.purged_calls, purged.orphaned_artifact_paths].some(value => Array.isArray(value) && value.length > 0)) {
352
+ await applyPurge(project(request), dependencies.store, dependencies.artifacts, dependencies.executorRouter ?? dependencies.executor);
353
+ }
221
354
  return { data: { ok: true } };
222
355
  });
223
356
  route('GET', '/api/v1/tasks/:task_id/sessions', (request) => ({ data: dependencies.store.listSessions(project(request), param(request, 'task_id'), page(request), query(request).archived === 'all') }));
@@ -238,7 +371,8 @@ export function createServer(dependencies) {
238
371
  model_reasoning_effort: defaults.modelReasoningEffort,
239
372
  policy_preset: defaults.policyPreset,
240
373
  initial_prompt: initialPrompt ?? null,
241
- initial_message: initialMessage ?? null
374
+ initial_message: initialMessage ?? null,
375
+ agent_name: optionalText(input.agent_name, 'agent_name', 256) ?? title
242
376
  };
243
377
  const command = ingressCommand(request, 'session.create', payload, 'SESSION_REQUEST_KEY_REQUIRED');
244
378
  const created = dependencies.store.createSessionWithAgent(projectId, {
@@ -273,10 +407,14 @@ export function createServer(dependencies) {
273
407
  route('PATCH', '/api/v1/agents/:agent_id', (request) => {
274
408
  const input = body(request);
275
409
  const current = dependencies.store.getAgent(project(request), param(request, 'agent_id'));
410
+ const model = optionalText(input.model_reference, 'model_reference') ?? stringValue(current.model);
411
+ const effort = optionalText(input.model_reasoning_effort, 'model_reasoning_effort') ?? stringValue(current.model_reasoning_effort);
412
+ dependencies.models.resolve(model);
413
+ resolveReasoningEffort(dependencies.config.current.config, model, effort);
276
414
  return { data: dependencies.store.updateAgent(project(request), param(request, 'agent_id'), expectedRevision(input.expected_revision), {
277
415
  name: optionalText(input.name, 'name', 256) ?? stringValue(current.name),
278
- model: optionalText(input.model_reference, 'model_reference') ?? stringValue(current.model),
279
- modelReasoningEffort: optionalText(input.model_reasoning_effort, 'model_reasoning_effort') ?? stringValue(current.model_reasoning_effort),
416
+ model,
417
+ modelReasoningEffort: effort,
280
418
  policyPreset: optionalText(input.policy_preset, 'policy_preset') ?? stringValue(current.policy_preset)
281
419
  }) };
282
420
  });
@@ -322,6 +460,7 @@ export function createServer(dependencies) {
322
460
  const runId = stringValue(dependencies.store.getStep(project(request), param(request, 'step_id')).run_id);
323
461
  return { data: dependencies.store.getRunGoal(project(request), runId) };
324
462
  });
463
+ route('GET', '/api/v1/agents/:agent_id/cache-usage', (request) => ({ data: dependencies.store.cacheUsage(project(request), param(request, 'agent_id')) }));
325
464
  route('GET', '/api/v1/agents/:agent_id/model-attempts', (request) => ({ data: dependencies.store.listAttempts(project(request), 'agent_id', param(request, 'agent_id'), page(request)) }));
326
465
  route('GET', '/api/v1/steps/:step_id/model-attempts', (request) => ({ data: dependencies.store.listAttempts(project(request), 'step_id', param(request, 'step_id'), page(request)) }));
327
466
  route('GET', '/api/v1/model-attempts/:attempt_id', (request) => ({ data: dependencies.store.getAttempt(project(request), param(request, 'attempt_id')) }));
@@ -345,19 +484,21 @@ export function createServer(dependencies) {
345
484
  route('POST', '/api/v1/agents/:agent_id/context/preview', (request) => {
346
485
  const agent = dependencies.store.getAgent(project(request), param(request, 'agent_id'));
347
486
  const model = dependencies.models.resolve(stringValue(agent.model));
348
- const prepared = dependencies.runtime(project(request)).context.prepare(project(request), param(request, 'agent_id'), dependencies.config.current.config, model.contextWindowTokens, model.profile);
349
- if (prepared.sourceRevision !== expectedRevision(body(request).expected_context_revision))
487
+ if (agent.context_revision !== expectedRevision(body(request).expected_context_revision))
350
488
  fail('REVISION_CONFLICT', 409, 'The Context changed before preview.');
489
+ const source = dependencies.store.contextSource(project(request), param(request, 'agent_id'));
490
+ const prepared = dependencies.runtime(project(request)).context.prepareSource(source, dependencies.config.current.config, model.contextWindowTokens, model.profile);
351
491
  return { data: prepared };
352
492
  });
353
- route('POST', '/api/v1/agents/:agent_id/context/compact', async (request, reply) => ({ data: await dependencies.runtime(project(request)).agents.compact(project(request), param(request, 'agent_id'), expectedRevision(body(request).expected_context_revision), optionalText(body(request).pivot_turn_group_id, 'pivot_turn_group_id'), clientSignal(request, reply)) }));
493
+ route('POST', '/api/v1/agents/:agent_id/context/compact', async (request, reply) => ({ data: await dependencies.runtime(project(request)).agents.compact(project(request), param(request, 'agent_id'), expectedRevision(body(request).expected_context_revision), optionalText(body(request).focus, 'focus'), clientSignal(request, reply)) }));
354
494
  route('POST', '/api/v1/agents/:agent_id/context/fork', (request, reply) => {
355
495
  const projectId = project(request);
356
496
  const source = dependencies.store.getAgent(projectId, param(request, 'agent_id'));
357
497
  const input = body(request);
358
498
  const model = optionalText(input.model_reference, 'model_reference') ?? stringValue(source.model);
359
499
  const effort = optionalText(input.model_reasoning_effort, 'model_reasoning_effort') ?? stringValue(source.model_reasoning_effort);
360
- resolveReasoningEffort(dependencies.models.resolve(model).profile, effort);
500
+ dependencies.models.resolve(model);
501
+ resolveReasoningEffort(dependencies.config.current.config, model, effort);
361
502
  return reply.status(201).send({ data: dependencies.store.createContextFork(projectId, param(request, 'agent_id'), {
362
503
  throughTurnGroupId: text(input.through_turn_group_id, 'through_turn_group_id'),
363
504
  expectedContextRevision: expectedRevision(input.expected_context_revision),
@@ -368,6 +509,8 @@ export function createServer(dependencies) {
368
509
  });
369
510
  route('GET', '/api/v1/agents/:agent_id/context/turn-groups', (request) => ({ data: dependencies.store.listTurnGroups(project(request), param(request, 'agent_id'), page(request)) }));
370
511
  route('GET', '/api/v1/agents/:agent_id/context/projection/decisions', (request) => ({ data: dependencies.store.listProjectionDecisions(project(request), param(request, 'agent_id'), page(request)) }));
512
+ route('GET', '/api/v1/agents/:agent_id/context/working', (request) => ({ data: dependencies.store.workingContext(project(request), param(request, 'agent_id')) }));
513
+ route('GET', '/api/v1/context/:context_item_id/sources', (request) => ({ data: dependencies.store.compactionSources(project(request), param(request, 'context_item_id')) }));
371
514
  route('GET', '/api/v1/calls', (request) => ({ data: dependencies.store.listCalls(project(request), page(request), { ...(typeof query(request).state === 'string' ? { state: query(request).state } : {}) }) }));
372
515
  route('POST', '/api/v1/calls', (request, reply) => {
373
516
  const input = body(request);
@@ -382,7 +525,7 @@ export function createServer(dependencies) {
382
525
  }, 'DIRECT_REQUEST_KEY_REQUIRED');
383
526
  const replay = dependencies.store.ingressReplay(command);
384
527
  if (replay !== null)
385
- return reply.status(202).send({ data: replay });
528
+ return reply.status(202).send({ data: dependencies.store.callSummary(project(request), stringValue(replay.id)) });
386
529
  const effect = dependencies.runtime(project(request)).registry.effect(capabilityId, argumentsValue);
387
530
  const call = dependencies.store.createCall(project(request), {
388
531
  workspaceId,
@@ -468,7 +611,7 @@ export function createServer(dependencies) {
468
611
  fail('INVALID_INPUT', 400, 'call_ids must all belong to one Workspace.');
469
612
  const workspaceId = [...workspaceIds][0] ?? '';
470
613
  const paths = dependencies.store.projectWorkspacePaths(projectId, workspaceId);
471
- return { data: await dependencies.executor.describeCallHistory(workspaceId, callIds, paths, false) };
614
+ return { data: await (dependencies.executorRouter?.workspace(project(request), workspaceId) ?? dependencies.executor).describeCallHistory(workspaceId, callIds, paths, false) };
472
615
  });
473
616
  route('POST', '/api/v1/agents/:agent_id/subagents', async (request, reply) => {
474
617
  const projectId = project(request);
@@ -482,6 +625,9 @@ export function createServer(dependencies) {
482
625
  model: defaults.model,
483
626
  model_reasoning_effort: defaults.modelReasoningEffort,
484
627
  policy_preset: defaults.policyPreset,
628
+ focus: typeof input.focus === 'string' ? input.focus : null,
629
+ fork_context: jsonValue(input.fork_context),
630
+ initial_prompt: optionalText(input.initial_prompt, 'initial_prompt') ?? null,
485
631
  message: message ?? null
486
632
  }, 'IDEMPOTENCY_KEY_INVALID');
487
633
  const forkContext = input.fork_context;
@@ -494,6 +640,7 @@ export function createServer(dependencies) {
494
640
  model: defaults.model,
495
641
  modelReasoningEffort: defaults.modelReasoningEffort,
496
642
  ...(typeof input.initial_prompt === 'string' ? { initialPrompt: input.initial_prompt } : {}),
643
+ ...(typeof input.focus === 'string' ? { compactFocus: input.focus } : {}),
497
644
  ...(message === undefined ? {} : { initialMessage: message }),
498
645
  ingress: command
499
646
  }, clientSignal(request, reply));
@@ -537,8 +684,8 @@ export function createServer(dependencies) {
537
684
  meta: runtimeMeta(dependencies, projectId)
538
685
  };
539
686
  });
540
- route('GET', '/api/v1/events', async (request, reply) => streamEvents(dependencies, contexts.get(request), reply, request));
541
- route('GET', '/api/v1/runtime/diagnostics', () => ({ data: { readiness: dependencies.ready(), config: configMeta(dependencies), state_revision: dependencies.store.stateRevision(), storage: dependencies.store.storageStats() } }));
687
+ route('GET', '/api/v1/events', async (request, reply) => streamEvents(dependencies, contexts.get(request), reply));
688
+ route('GET', '/api/v1/runtime/diagnostics', () => ({ data: { readiness: dependencies.ready(), config: configMeta(dependencies), state_revision: dependencies.store.stateRevision(), storage: dependencies.store.storageStats(), observability: dependencies.observability.status() } }));
542
689
  // The three diagnostic windows are documented as RuntimeRecordWindow<T>, not bare arrays.
543
690
  // They are bounded in-process buffers with no replay, so `next_sequence` is the highest
544
691
  // sequence returned and `has_more` reports whether the window filled the requested limit.
@@ -548,13 +695,14 @@ export function createServer(dependencies) {
548
695
  route('GET', '/api/v1/runtime/metrics', (_request, reply) => reply.type('text/plain; version=0.0.4').send(dependencies.observability.metrics()));
549
696
  route('GET', '/api/v1/runtime/operations', (request) => ({ data: { calls: dependencies.store.listCalls(project(request), { limit: dependencies.config.current.config.snapshot.pageSizeMax }, { state: 'RUNNING' }) } }));
550
697
  route('POST', '/api/v1/runtime/maintenance/run', async (request) => {
551
- const purged = dependencies.store.purgeArchived(project(request), optionalText(body(request).before, 'before') ?? new Date().toISOString());
552
- const files = await applyPurge(purged, dependencies.artifacts, dependencies.executor);
698
+ const before = optionalText(body(request).before, 'before') ?? new Date().toISOString();
699
+ const purged = await commitPurge(dependencies.executorRouter ?? dependencies.executor, stateId => dependencies.store.purgeArchived(project(request), before, stateId));
700
+ const files = await applyPurge(project(request), dependencies.store, dependencies.artifacts, dependencies.executorRouter ?? dependencies.executor);
553
701
  // purged_calls is an instruction for applyPurge, not part of the Route response.
554
702
  return { data: { purged_tasks: purged.purged_tasks, orphaned_artifact_paths: purged.orphaned_artifact_paths, files } };
555
703
  });
556
704
  route('GET', '/api/v1/runtime/storage', () => ({ data: dependencies.store.storageStats() }));
557
- app.setNotFoundHandler((_request, reply) => reply.status(404).send({ error: { code: 'ROUTE_NOT_FOUND', message: 'The requested Route does not exist.', details: {} } }));
705
+ app.setNotFoundHandler((_request, reply) => reply.status(404).send({ error: { code: 'ROUTE_NOT_FOUND', message: 'The requested Route does not exist.', retryable: false, details: {} } }));
558
706
  return app;
559
707
  }
560
708
  function createAgent(dependencies, projectId, taskId, input) {
@@ -583,8 +731,8 @@ function agentDefaults(dependencies, projectId, roleName, input) {
583
731
  const preset = dependencies.config.current.config.policyPresets[policyPreset];
584
732
  if (preset === undefined)
585
733
  fail('POLICY_PRESET_MISSING', 409, 'The selected Policy preset does not exist.');
586
- const resolved = dependencies.models.resolve(model);
587
- resolveReasoningEffort(resolved.profile, modelReasoningEffort);
734
+ dependencies.models.resolve(model);
735
+ resolveReasoningEffort(dependencies.config.current.config, model, modelReasoningEffort);
588
736
  const registry = dependencies.runtime(projectId).registry;
589
737
  const capabilityIds = [...new Set([
590
738
  ...registry.list().filter((definition) => definition.defaultAssignment === 'all_new_agents').map((definition) => definition.id),
@@ -631,14 +779,23 @@ function refreshCapabilities(dependencies, projectId, agentId) {
631
779
  });
632
780
  return dependencies.store.replaceCapabilities(projectId, agentId, capabilities, loaded);
633
781
  }
634
- async function streamEvents(dependencies, context, reply, request) {
782
+ async function streamEvents(dependencies, context, reply) {
635
783
  if (context === undefined)
636
784
  fail('TRUSTED_INGRESS_REQUIRED', 500, 'Trusted ingress is missing.');
637
785
  const subscription = dependencies.events.subscribe(context.ingress.clientId, context.projectId);
638
786
  reply.hijack();
639
787
  reply.raw.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache, no-transform', connection: 'keep-alive' });
640
788
  const controller = new AbortController();
641
- request.raw.once('close', () => controller.abort());
789
+ const abort = () => controller.abort();
790
+ reply.raw.once('close', abort);
791
+ reply.raw.once('error', abort);
792
+ const drain = async () => {
793
+ if (reply.raw.writableNeedDrain)
794
+ await once(reply.raw, 'drain', {
795
+ signal: AbortSignal.any([controller.signal, AbortSignal.timeout(dependencies.config.current.config.sse.heartbeatIntervalMs)])
796
+ });
797
+ controller.signal.throwIfAborted();
798
+ };
642
799
  let sequence = 0;
643
800
  /**
644
801
  * Writes one SSE frame. `kind` and `occurred_at` go in the JSON body, not only in the
@@ -666,12 +823,17 @@ async function streamEvents(dependencies, context, reply, request) {
666
823
  state_revision: String(dependencies.store.stateRevision()),
667
824
  ...runtimeMeta(dependencies, context.projectId)
668
825
  });
669
- const heartbeat = setInterval(() => send('runtime.notice', { code: 'heartbeat', state_revision: String(dependencies.store.stateRevision()), ...runtimeMeta(dependencies, context.projectId) }), dependencies.config.current.config.sse.heartbeatIntervalMs);
826
+ const heartbeat = setInterval(() => {
827
+ if (!reply.raw.writableNeedDrain && !controller.signal.aborted)
828
+ send('runtime.notice', { code: 'heartbeat', state_revision: String(dependencies.store.stateRevision()), ...runtimeMeta(dependencies, context.projectId) });
829
+ }, dependencies.config.current.config.sse.heartbeatIntervalMs);
670
830
  try {
671
831
  for (;;) {
832
+ await drain();
672
833
  const event = await subscription.next(controller.signal);
673
834
  if (event === null)
674
835
  break;
836
+ await drain();
675
837
  send(event.type, {
676
838
  event_id: event.id,
677
839
  project_id: event.projectId,
@@ -682,10 +844,19 @@ async function streamEvents(dependencies, context, reply, request) {
682
844
  });
683
845
  }
684
846
  }
847
+ catch (error) {
848
+ if (!controller.signal.aborted)
849
+ dependencies.observability.record('http', 'sse', 'failed', 0, { error_code: error instanceof Error && error.name === 'TimeoutError' ? 'SSE_BACKPRESSURE_TIMEOUT' : 'SSE_STREAM_FAILED' });
850
+ }
685
851
  finally {
686
852
  clearInterval(heartbeat);
687
853
  subscription.close();
688
- reply.raw.end();
854
+ reply.raw.off('close', abort);
855
+ reply.raw.off('error', abort);
856
+ if (reply.raw.writableNeedDrain)
857
+ reply.raw.destroy();
858
+ else
859
+ reply.raw.end();
689
860
  }
690
861
  }
691
862
  function runtimeMeta(dependencies, projectId) {
@@ -694,6 +865,7 @@ function runtimeMeta(dependencies, projectId) {
694
865
  config_digest: dependencies.config.current.digest,
695
866
  pair_digest: dependencies.pair.pairDigest,
696
867
  runtime_contract_digest: dependencies.runtime(projectId).contractDigest(),
868
+ project_capabilities_digest: dependencies.runtime(projectId).capabilitiesDigest(),
697
869
  access_contract: accessContract(dependencies)
698
870
  };
699
871
  }
@@ -736,14 +908,6 @@ function requireLoopback(request) {
736
908
  fail('SHUTDOWN_FORBIDDEN', 403, 'Shutdown may only be requested from the local machine.');
737
909
  }
738
910
  }
739
- function loopbackAddress(address) {
740
- // Absent for a non-IP socket. Refusing is the safe answer: the caller cannot be shown to be local.
741
- if (address === undefined)
742
- return false;
743
- // A dual-stack listener reports an IPv4 peer as `::ffff:127.0.0.1`.
744
- const normalized = address.startsWith('::ffff:') ? address.slice('::ffff:'.length) : address;
745
- return normalized === '::1' || /^127\./.test(normalized);
746
- }
747
911
  /**
748
912
  * CORE-15. 02A defines this as the catalog a client needs to offer model, reasoning-effort,
749
913
  * Runtime Role and Policy choices: `models`, `model_profiles`, `roles`, `policy_presets` and
@@ -758,7 +922,6 @@ function loopbackAddress(address) {
758
922
  * truth. Reasoning efforts follow the same rule the config validator applies: a Model Harness
759
923
  * Profile constrains them when one is attached, otherwise the full ladder is allowed.
760
924
  */
761
- const EFFORT_LADDER = ['none', 'minimal', 'low', 'medium', 'high'];
762
925
  const EFFECT_SCOPE_DESCRIPTIONS = {
763
926
  read_only: 'Reads Workspace state without changing it.',
764
927
  workspace_write: 'Creates or modifies files inside the Workspace.',
@@ -776,22 +939,18 @@ const POLICY_VALUE_DESCRIPTIONS = {
776
939
  function configOptions(dependencies, projectId) {
777
940
  const config = dependencies.config.current.config;
778
941
  const profiles = config.modelHarnessProfiles ?? {};
779
- const efforts = (harnessProfile) => harnessProfile === undefined
780
- ? EFFORT_LADDER
781
- : profiles[harnessProfile]?.effort.allowed ?? [];
782
942
  const models = Object.entries(config.providers).flatMap(([providerId, provider]) => Object.entries(provider.models).map(([modelId, model]) => {
783
- const allowed = efforts(model.harnessProfile);
943
+ const allowed = modelReasoningEfforts(config, `${providerId}/${modelId}`);
784
944
  return {
785
945
  model_reference: `${providerId}/${modelId}`,
786
946
  allowed_reasoning_efforts: allowed,
787
- default_reasoning_effort: model.harnessProfile === undefined
788
- ? 'medium'
789
- : profiles[model.harnessProfile]?.effort.default ?? allowed[0] ?? 'medium',
947
+ default_reasoning_effort: allowed[0] ?? '',
790
948
  harness_profile_id: model.harnessProfile ?? '',
791
949
  harness_profile_version: model.harnessProfile === undefined ? 0 : profiles[model.harnessProfile]?.version ?? 0,
792
950
  context_window_tokens: model.contextWindowTokens,
793
951
  usage_reliable: model.usageReliable ?? null,
794
- cache_supported: model.cacheSupported ?? null
952
+ cache_supported: (model.promptCache ?? provider.promptCache)?.mode !== undefined ? (model.promptCache ?? provider.promptCache)?.mode !== 'off' : model.cacheSupported ?? null,
953
+ prompt_cache: model.promptCache ?? provider.promptCache ?? null
795
954
  };
796
955
  }));
797
956
  return {
@@ -801,8 +960,6 @@ function configOptions(dependencies, projectId) {
801
960
  model_profiles: Object.entries(profiles).map(([id, profile]) => ({
802
961
  id,
803
962
  version: profile.version,
804
- allowed_reasoning_efforts: profile.effort.allowed,
805
- default_reasoning_effort: profile.effort.default,
806
963
  reasoning_state_round_trip: profile.semantics.reasoningStateRoundTrip,
807
964
  thinking_continuation: profile.semantics.thinkingContinuation,
808
965
  structured_output: profile.semantics.structuredOutput,
@@ -856,11 +1013,20 @@ function schemaDetails(error) {
856
1013
  * failure to answer is the Executor's (`EXECUTOR_UNAVAILABLE`, 503, retryable). Collapsing both
857
1014
  * into the latter told the user to retry a path that would always be rejected, and hid the reason.
858
1015
  */
1016
+ function executorId(value) {
1017
+ if (value === undefined || value === 'remote')
1018
+ return 'remote';
1019
+ if (typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/u.test(value))
1020
+ return value;
1021
+ fail('EXECUTOR_INVALID', 400, 'executor_id must be a configured Executor identifier.');
1022
+ }
859
1023
  async function validateWorkspacePaths(dependencies, workspacePaths, createIfMissing) {
860
1024
  try {
861
1025
  return await dependencies.executor.validatePaths(workspacePaths, undefined, createIfMissing);
862
1026
  }
863
1027
  catch (error) {
1028
+ if (error instanceof LensError && error.code === 'SHARED_WORKSPACE_REQUIRED')
1029
+ throw error;
864
1030
  if (error instanceof ExecutorCallError && error.refused) {
865
1031
  fail('WORKSPACE_PATH_INVALID', 422, error.message, { workspace_paths: [...workspacePaths] });
866
1032
  }
@@ -957,6 +1123,7 @@ function openapi(specs) {
957
1123
  [spec.method]: {
958
1124
  operationId: operationId(spec.method, spec.path),
959
1125
  parameters: parameters(spec.method, spec.path),
1126
+ ...(spec.path.startsWith('/api/v1/') ? { security: [{ PairBearer: [] }] } : {}),
960
1127
  ...(body === null ? {} : { requestBody: { required: true, content: { 'application/json': { schema: body } } } }),
961
1128
  responses: responses(spec.method, spec.path)
962
1129
  }
@@ -967,24 +1134,31 @@ function openapi(specs) {
967
1134
  info: { title: 'Lens Core API', version: '1.0.0' },
968
1135
  paths,
969
1136
  components: {
1137
+ securitySchemes: {
1138
+ PairBearer: {
1139
+ type: 'http',
1140
+ scheme: 'bearer',
1141
+ description: 'Pair credential issued by the local administrator. Send the client identity headers alongside Authorization: Bearer <credential>.'
1142
+ }
1143
+ },
970
1144
  schemas: {
971
1145
  Success: {
972
1146
  type: 'object',
973
- required: ['data'],
974
- properties: { data: {}, meta: { type: 'object', additionalProperties: true } },
1147
+ required: ['data', 'correlation_id'],
1148
+ properties: { data: {}, correlation_id: { type: 'string' }, meta: { type: 'object', additionalProperties: true } },
975
1149
  additionalProperties: false
976
1150
  },
977
1151
  Error: {
978
1152
  type: 'object',
979
- required: ['error'],
1153
+ required: ['error', 'correlation_id'],
980
1154
  properties: {
981
1155
  error: {
982
1156
  type: 'object',
983
- required: ['code', 'message', 'details'],
984
- properties: { code: { type: 'string' }, message: { type: 'string' }, details: {} },
1157
+ required: ['code', 'message', 'details', 'retryable'],
1158
+ properties: { code: { type: 'string' }, message: { type: 'string' }, retryable: { type: 'boolean' }, details: {} },
985
1159
  additionalProperties: false
986
1160
  },
987
- request_id: { type: 'string' }
1161
+ correlation_id: { type: 'string' }
988
1162
  },
989
1163
  additionalProperties: false
990
1164
  }
@@ -1005,20 +1179,20 @@ function requestBody(method, path) {
1005
1179
  return null;
1006
1180
  if (path === '/api/v1/config/validate')
1007
1181
  return objectSchema({ content: { type: 'string', minLength: 1 } }, ['content']);
1008
- if (path === '/api/v1/config')
1182
+ if (path === '/api/v1/config' || path === '/api/v1/config/files/:file_id')
1009
1183
  return objectSchema({ content: { type: 'string', minLength: 1 }, expected_file_digest: stringSchema }, ['content', 'expected_file_digest']);
1010
1184
  if (path === '/api/v1/projects')
1011
- return objectSchema({ name: stringSchema, workspace_paths: stringArraySchema, create_if_missing: booleanSchema }, ['name', 'workspace_paths']);
1185
+ return objectSchema({ name: stringSchema, workspace_paths: stringArraySchema, executor_id: stringSchema, create_if_missing: booleanSchema }, ['name', 'workspace_paths']);
1012
1186
  if (path === '/api/v1/projects/:project_id')
1013
1187
  return objectSchema({ name: stringSchema, expected_revision: revisionSchema }, ['name', 'expected_revision']);
1014
1188
  if (path === '/api/v1/projects/browse/open')
1015
- return objectSchema({ workspace_paths: stringArraySchema, path: stringSchema }, ['workspace_paths', 'path']);
1189
+ return objectSchema({ workspace_paths: stringArraySchema, path: stringSchema, executor_id: stringSchema }, ['workspace_paths', 'path']);
1016
1190
  if (path === '/api/v1/projects/choose-folder')
1017
- return objectSchema({ initial_path: stringSchema });
1191
+ return objectSchema({ initial_path: stringSchema, executor_id: stringSchema });
1018
1192
  if (path === '/api/v1/calls/revert-preview')
1019
1193
  return objectSchema({ call_ids: stringArraySchema }, ['call_ids']);
1020
1194
  if (/\/projects\/:project_id\/workspaces(?:\/:workspace_id)?$/.test(path))
1021
- return objectSchema({ name: stringSchema, paths: stringArraySchema, is_default: booleanSchema, create_if_missing: booleanSchema, expected_revision: revisionSchema }, method === 'patch' ? ['name', 'paths', 'expected_revision'] : ['name', 'paths']);
1195
+ return objectSchema({ name: stringSchema, paths: stringArraySchema, executor_id: stringSchema, is_default: booleanSchema, create_if_missing: booleanSchema, expected_revision: revisionSchema }, method === 'patch' ? ['name', 'paths', 'expected_revision'] : ['name', 'paths']);
1022
1196
  if (path.endsWith('/archive') || path.endsWith('/restore'))
1023
1197
  return objectSchema({ expected_revision: revisionSchema }, ['expected_revision']);
1024
1198
  if (path === '/api/v1/tasks' || path === '/api/v1/projects/:project_id/tasks')
@@ -1065,12 +1239,12 @@ function requestBody(method, path) {
1065
1239
  if (path.endsWith('/context/projection/reset'))
1066
1240
  return objectSchema({
1067
1241
  expected_context_revision: revisionSchema,
1068
- stages: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'string', enum: ['tool_result_budget', 'snip', 'microcompact', 'context_collapse'] } }
1242
+ stages: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'string', enum: ['snip'] } }
1069
1243
  }, ['expected_context_revision']);
1070
1244
  if (path.endsWith('/context/preview'))
1071
1245
  return objectSchema({ expected_context_revision: revisionSchema }, ['expected_context_revision']);
1072
1246
  if (path.endsWith('/context/compact'))
1073
- return objectSchema({ expected_context_revision: revisionSchema, pivot_turn_group_id: stringSchema }, ['expected_context_revision']);
1247
+ return objectSchema({ expected_context_revision: revisionSchema, focus: stringSchema }, ['expected_context_revision']);
1074
1248
  if (path.endsWith('/context/fork'))
1075
1249
  return objectSchema({
1076
1250
  through_turn_group_id: stringSchema,
@@ -1095,6 +1269,7 @@ function requestBody(method, path) {
1095
1269
  return objectSchema({
1096
1270
  name: stringSchema,
1097
1271
  fork_context: { type: 'string', enum: ['full', 'compact', 'none'] },
1272
+ focus: stringSchema,
1098
1273
  model_reference: stringSchema,
1099
1274
  model_reasoning_effort: stringSchema,
1100
1275
  initial_prompt: { type: 'string' },
@@ -1124,13 +1299,19 @@ function objectSchema(properties, required = []) {
1124
1299
  }
1125
1300
  function parameters(method, path) {
1126
1301
  const result = [];
1302
+ if (method === 'get' && path === '/api/v1/tasks/:task_id/audit') {
1303
+ result.push({ name: 'source', in: 'query', required: false, schema: { type: 'string', enum: ['core', 'executor'], default: 'core' } });
1304
+ }
1305
+ if (method === 'get' && (path === '/api/v1/projects/browse' || path === '/api/v1/tasks/:task_id/audit')) {
1306
+ result.push({ name: 'executor_id', in: 'query', required: false, schema: stringSchema });
1307
+ }
1127
1308
  for (const match of path.matchAll(/:([a-zA-Z0-9_]+)/g)) {
1128
1309
  result.push({ name: match[1] ?? '', in: 'path', required: true, schema: stringSchema });
1129
1310
  }
1130
1311
  if (path.startsWith('/api/v1/')) {
1131
1312
  result.push({ name: 'X-Lens-Client-Kind', in: 'header', required: true, schema: { type: 'string', enum: ['native', 'browser'] } }, { name: 'X-Lens-Client-Mac', in: 'header', required: false, schema: stringSchema }, { name: 'X-Lens-Hardware-Id', in: 'header', required: false, schema: stringSchema }, { name: 'X-Lens-Browser-Fingerprint', in: 'header', required: false, schema: stringSchema }, { name: 'X-Lens-Project-Id', in: 'header', required: false, schema: stringSchema });
1132
1313
  }
1133
- if (method === 'get' && (/\/tasks$|\/sessions$|\/runs$|\/steps$|\/calls$|\/artifacts$|\/context$|\/timeline$|\/model-attempts$|\/partitions$|\/turn-groups$|\/decisions$|\/logs$|\/traces$|\/agent-ops$/.test(path))) {
1314
+ if (method === 'get' && (/\/audit$|\/tasks$|\/sessions$|\/runs$|\/steps$|\/calls$|\/artifacts$|\/context$|\/timeline$|\/model-attempts$|\/partitions$|\/turn-groups$|\/decisions$|\/logs$|\/traces$|\/agent-ops$/.test(path))) {
1134
1315
  result.push({ name: 'cursor', in: 'query', required: false, schema: stringSchema }, { name: 'limit', in: 'query', required: false, schema: { type: 'integer', minimum: 1, maximum: 500 } });
1135
1316
  }
1136
1317
  if (method === 'delete')
@@ -1148,26 +1329,27 @@ function idempotentRoute(method, path) {
1148
1329
  || path.endsWith('/subagent-broadcasts'));
1149
1330
  }
1150
1331
  function responses(method, path) {
1332
+ const errors = errorResponses(path.startsWith('/api/v1/'));
1151
1333
  if (path === '/api/v1/events')
1152
- return { '200': { description: 'Server-sent RuntimeEvent stream', content: { 'text/event-stream': { schema: { type: 'string' } } } }, ...errorResponses() };
1334
+ return { '200': { description: 'Server-sent RuntimeEvent stream', content: { 'text/event-stream': { schema: { type: 'string' } } } }, ...errors };
1153
1335
  if (path.endsWith('/runtime/metrics'))
1154
- return { '200': { description: 'Prometheus metrics', content: { 'text/plain': { schema: { type: 'string' } } } }, ...errorResponses() };
1336
+ return { '200': { description: 'Prometheus metrics', content: { 'text/plain': { schema: { type: 'string' } } } }, ...errors };
1155
1337
  if (path === '/api/v1/artifacts/:artifact_id') {
1156
1338
  const content = { 'application/octet-stream': { schema: { type: 'string', contentEncoding: 'binary' } } };
1157
1339
  return {
1158
1340
  '200': { description: 'Artifact bytes', content },
1159
1341
  '206': { description: 'Artifact byte range', content },
1160
- ...errorResponses()
1342
+ ...errors
1161
1343
  };
1162
1344
  }
1163
1345
  const status = method === 'post' && (/\/projects$|\/workspaces$|\/tasks$|\/sessions$|\/agents$|\/subagents$|\/context-overrides$|\/context\/fork$|\/results\/:result_id\/include$/.test(path)) ? '201'
1164
1346
  : method === 'post' && (path === '/health/shutdown' || path.endsWith('/messages') || path === '/api/v1/calls' || path.endsWith('/subagent-messages') || path.endsWith('/subagent-broadcasts')) ? '202'
1165
1347
  : '200';
1166
- return { [status]: { description: 'Success', content: { 'application/json': { schema: { '$ref': '#/components/schemas/Success' } } } }, ...errorResponses() };
1348
+ return { [status]: { description: 'Success', content: { 'application/json': { schema: { '$ref': '#/components/schemas/Success' } } } }, ...errors };
1167
1349
  }
1168
- function errorResponses() {
1350
+ function errorResponses(protectedRoute) {
1169
1351
  const response = { description: 'Error', content: { 'application/json': { schema: { '$ref': '#/components/schemas/Error' } } } };
1170
- return Object.fromEntries(['400', '403', '404', '409', '413', '416', '422', '428', '500', '502', '503'].map((status) => [status, response]));
1352
+ return Object.fromEntries(['400', ...(protectedRoute ? ['401'] : []), '403', '404', '409', '413', '416', '422', '428', '500', '502', '503'].map((status) => [status, response]));
1171
1353
  }
1172
1354
  function operationId(method, path) {
1173
1355
  return `${method}_${path.replace(/^\/api\/v1\//, '').replace(/^\//, '').replace(/[:/.-]+/g, '_') || 'root'}`;
@@ -1241,7 +1423,7 @@ function resolveProjectId(request, fallback) {
1241
1423
  const OPAQUE_KEYS = new Set([
1242
1424
  'inputSchema', 'outputSchema', 'input_schema', 'output_schema',
1243
1425
  'input', 'content', 'details', 'payload', 'preparation', 'usage', 'metadata',
1244
- 'answer', 'prompt', 'arguments', 'structured', 'document', 'config'
1426
+ 'answer', 'prompt', 'arguments', 'structured', 'document', 'config', 'request', 'observation'
1245
1427
  ]);
1246
1428
  // Core mints identifiers as `<entity>_<uuid>`, so the documented field name is derivable from
1247
1429
  // the value itself rather than from a per-Route table.
@@ -1270,7 +1452,7 @@ function retryableFailure(status) {
1270
1452
  return status === 408 || status === 429 || status >= 500;
1271
1453
  }
1272
1454
  function routePath(request) {
1273
- return request.routeOptions.url ?? request.url.split('?')[0] ?? 'unknown';
1455
+ return request.routeOptions.url ?? 'unmatched';
1274
1456
  }
1275
1457
  function idempotencyKey(request, missingCode) {
1276
1458
  const value = request.headers['idempotency-key'];
@@ -1294,11 +1476,11 @@ function queryRevision(request) {
1294
1476
  function param(request, name) {
1295
1477
  return text(request.params[name], name, 256);
1296
1478
  }
1297
- function page(request) {
1479
+ function pageOptions(request, limits) {
1298
1480
  const values = query(request);
1299
1481
  const cursor = optionalText(values.cursor, 'cursor', 4096);
1300
- const rawLimit = values.limit === undefined ? 100 : typeof values.limit === 'string' ? Number(values.limit) : values.limit;
1301
- const limit = Math.min(integer(rawLimit, 'limit', 1), 500);
1482
+ const rawLimit = values.limit === undefined ? limits.pageSizeDefault : typeof values.limit === 'string' ? Number(values.limit) : values.limit;
1483
+ const limit = Math.min(integer(rawLimit, 'limit', 1), limits.pageSizeMax);
1302
1484
  return { ...(cursor === undefined ? {} : { cursor }), limit };
1303
1485
  }
1304
1486
  function paths(value) {