@lensagent/core 0.1.0

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 (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/config/lens.example.jsonc +92 -0
  4. package/config/pair.example.json +3 -0
  5. package/dist/adapters/executor.d.ts +35 -0
  6. package/dist/adapters/executor.js +296 -0
  7. package/dist/adapters/lsp.d.ts +18 -0
  8. package/dist/adapters/lsp.js +218 -0
  9. package/dist/adapters/mcp.d.ts +32 -0
  10. package/dist/adapters/mcp.js +283 -0
  11. package/dist/adapters/revert.d.ts +12 -0
  12. package/dist/adapters/revert.js +155 -0
  13. package/dist/config/runtime.d.ts +55 -0
  14. package/dist/config/runtime.js +371 -0
  15. package/dist/config/schema.d.ts +292 -0
  16. package/dist/config/schema.js +177 -0
  17. package/dist/contracts/adapter.d.ts +28 -0
  18. package/dist/contracts/adapter.js +8 -0
  19. package/dist/contracts/capability.d.ts +41 -0
  20. package/dist/contracts/capability.js +1 -0
  21. package/dist/contracts/domain.d.ts +61 -0
  22. package/dist/contracts/domain.js +1 -0
  23. package/dist/contracts/index.d.ts +5 -0
  24. package/dist/contracts/index.js +5 -0
  25. package/dist/contracts/model.d.ts +77 -0
  26. package/dist/contracts/model.js +1 -0
  27. package/dist/contracts/ports.d.ts +14 -0
  28. package/dist/contracts/ports.js +14 -0
  29. package/dist/core/access.d.ts +24 -0
  30. package/dist/core/access.js +205 -0
  31. package/dist/core/codec.d.ts +33 -0
  32. package/dist/core/codec.js +105 -0
  33. package/dist/core/domain.d.ts +13 -0
  34. package/dist/core/domain.js +24 -0
  35. package/dist/core/host.d.ts +19 -0
  36. package/dist/core/host.js +249 -0
  37. package/dist/core/identity.d.ts +19 -0
  38. package/dist/core/identity.js +54 -0
  39. package/dist/entry/bun.d.ts +1 -0
  40. package/dist/entry/bun.js +2 -0
  41. package/dist/entry/node.d.ts +2 -0
  42. package/dist/entry/node.js +3 -0
  43. package/dist/entry/rebuild.d.ts +1 -0
  44. package/dist/entry/rebuild.js +47 -0
  45. package/dist/entry/run.d.ts +1 -0
  46. package/dist/entry/run.js +43 -0
  47. package/dist/execution/lsp-process.d.ts +14 -0
  48. package/dist/execution/lsp-process.js +234 -0
  49. package/dist/http/server.d.ts +34 -0
  50. package/dist/http/server.js +1332 -0
  51. package/dist/launch.d.ts +3 -0
  52. package/dist/launch.js +4 -0
  53. package/dist/runtime/agent.d.ts +35 -0
  54. package/dist/runtime/agent.js +853 -0
  55. package/dist/runtime/artifacts.d.ts +15 -0
  56. package/dist/runtime/artifacts.js +72 -0
  57. package/dist/runtime/cache.d.ts +20 -0
  58. package/dist/runtime/cache.js +44 -0
  59. package/dist/runtime/calls.d.ts +21 -0
  60. package/dist/runtime/calls.js +130 -0
  61. package/dist/runtime/capabilities.d.ts +58 -0
  62. package/dist/runtime/capabilities.js +196 -0
  63. package/dist/runtime/cleanup.d.ts +7 -0
  64. package/dist/runtime/cleanup.js +33 -0
  65. package/dist/runtime/context.d.ts +34 -0
  66. package/dist/runtime/context.js +169 -0
  67. package/dist/runtime/diagnostics.d.ts +8 -0
  68. package/dist/runtime/diagnostics.js +71 -0
  69. package/dist/runtime/events.d.ts +22 -0
  70. package/dist/runtime/events.js +131 -0
  71. package/dist/runtime/kernel-capabilities.d.ts +32 -0
  72. package/dist/runtime/kernel-capabilities.js +200 -0
  73. package/dist/runtime/manifests.d.ts +14 -0
  74. package/dist/runtime/manifests.js +117 -0
  75. package/dist/runtime/model.d.ts +20 -0
  76. package/dist/runtime/model.js +241 -0
  77. package/dist/runtime/observability.d.ts +22 -0
  78. package/dist/runtime/observability.js +104 -0
  79. package/dist/runtime/policy.d.ts +8 -0
  80. package/dist/runtime/policy.js +31 -0
  81. package/dist/runtime/profile.d.ts +8 -0
  82. package/dist/runtime/profile.js +52 -0
  83. package/dist/runtime/projects.d.ts +40 -0
  84. package/dist/runtime/projects.js +115 -0
  85. package/dist/runtime/results.d.ts +7 -0
  86. package/dist/runtime/results.js +55 -0
  87. package/dist/runtime/review.d.ts +9 -0
  88. package/dist/runtime/review.js +54 -0
  89. package/dist/runtime/scheduler.d.ts +15 -0
  90. package/dist/runtime/scheduler.js +85 -0
  91. package/dist/runtime/skills.d.ts +2 -0
  92. package/dist/runtime/skills.js +83 -0
  93. package/dist/runtime/tokenizer.d.ts +8 -0
  94. package/dist/runtime/tokenizer.js +30 -0
  95. package/dist/store/database.d.ts +3 -0
  96. package/dist/store/database.js +49 -0
  97. package/dist/store/event-log.d.ts +19 -0
  98. package/dist/store/event-log.js +61 -0
  99. package/dist/store/schema.d.ts +1 -0
  100. package/dist/store/schema.js +347 -0
  101. package/dist/store/store.d.ts +344 -0
  102. package/dist/store/store.js +2554 -0
  103. package/package.json +73 -0
  104. package/proto/executor.proto +314 -0
@@ -0,0 +1,115 @@
1
+ import { CapabilityRegistry } from './capabilities.js';
2
+ import { CallRuntime } from './calls.js';
3
+ import { ContextEngine } from './context.js';
4
+ import { AgentRuntime } from './agent.js';
5
+ import { KernelAdapter, kernelCapabilities } from './kernel-capabilities.js';
6
+ import { workspaceSkills } from './skills.js';
7
+ import { coreManifests, manifestSetDigest } from './manifests.js';
8
+ export class ProjectRuntimes {
9
+ store;
10
+ config;
11
+ scheduler;
12
+ artifacts;
13
+ models;
14
+ reviewer;
15
+ sharedCapabilities;
16
+ target;
17
+ events;
18
+ pinnedProjectId;
19
+ #runtimes = new Map();
20
+ #evictionTimer;
21
+ #closed = false;
22
+ constructor(store, config, scheduler, artifacts, models, reviewer, sharedCapabilities, target, events, pinnedProjectId) {
23
+ this.store = store;
24
+ this.config = config;
25
+ this.scheduler = scheduler;
26
+ this.artifacts = artifacts;
27
+ this.models = models;
28
+ this.reviewer = reviewer;
29
+ this.sharedCapabilities = sharedCapabilities;
30
+ this.target = target;
31
+ this.events = events;
32
+ this.pinnedProjectId = pinnedProjectId;
33
+ }
34
+ get(projectId) {
35
+ if (this.#closed)
36
+ throw new Error('Project runtimes are closed.');
37
+ this.store.getProject(projectId);
38
+ const existing = this.#runtimes.get(projectId);
39
+ if (existing !== undefined) {
40
+ existing.lastAccessedAt = Date.now();
41
+ return existing.runtime;
42
+ }
43
+ const runtime = this.#build(projectId);
44
+ this.#runtimes.set(projectId, { runtime, lastAccessedAt: Date.now() });
45
+ return runtime;
46
+ }
47
+ refresh(projectId) {
48
+ this.#runtimes.get(projectId)?.runtime.refreshSkills();
49
+ }
50
+ startIdleEviction(maxIdleMs = 30 * 60 * 1000) {
51
+ if (this.#evictionTimer !== undefined || this.#closed)
52
+ return;
53
+ this.#evictionTimer = setInterval(() => { void this.#evictIdle(maxIdleMs); }, Math.min(maxIdleMs, 5 * 60 * 1000));
54
+ this.#evictionTimer.unref();
55
+ }
56
+ async close() {
57
+ this.#closed = true;
58
+ if (this.#evictionTimer !== undefined)
59
+ clearInterval(this.#evictionTimer);
60
+ this.#evictionTimer = undefined;
61
+ const runtimes = [...this.#runtimes.values()].map((entry) => entry.runtime);
62
+ this.#runtimes.clear();
63
+ await Promise.allSettled(runtimes.flatMap((runtime) => [runtime.agents.close(), runtime.calls.close()]));
64
+ }
65
+ async #evictIdle(maxIdleMs) {
66
+ const timestamp = Date.now();
67
+ for (const [projectId, entry] of this.#runtimes) {
68
+ if (this.#closed || projectId === this.pinnedProjectId || timestamp - entry.lastAccessedAt < maxIdleMs)
69
+ continue;
70
+ if (this.store.hasActiveRuntimeObjects(projectId)) {
71
+ entry.lastAccessedAt = timestamp;
72
+ continue;
73
+ }
74
+ const lastAccessedAt = entry.lastAccessedAt;
75
+ await Promise.allSettled([entry.runtime.agents.close(), entry.runtime.calls.close()]);
76
+ if (entry.lastAccessedAt === lastAccessedAt && !this.store.hasActiveRuntimeObjects(projectId))
77
+ this.#runtimes.delete(projectId);
78
+ }
79
+ }
80
+ #build(projectId) {
81
+ const registry = new CapabilityRegistry(this.target);
82
+ for (const capability of this.sharedCapabilities)
83
+ registry.register(capability);
84
+ const context = new ContextEngine(this.store, registry);
85
+ const calls = new CallRuntime(this.store, registry, this.scheduler, this.artifacts, this.events);
86
+ const agents = new AgentRuntime(this.store, this.config, context, this.models, registry, calls, this.reviewer, this.events);
87
+ const kernel = new KernelAdapter(this.store, registry, this.config, (targetProjectId, agentId) => this.get(targetProjectId).agents.wake(targetProjectId, agentId), (targetProjectId, input, signal) => this.get(targetProjectId).agents.createSubagent(targetProjectId, input, signal));
88
+ for (const capability of kernelCapabilities(kernel))
89
+ registry.register(capability);
90
+ const refreshSkills = () => {
91
+ registry.removeKind('skill');
92
+ const paths = this.store.listWorkspaces(projectId).flatMap((workspace) => {
93
+ const value = workspace.paths;
94
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
95
+ });
96
+ for (const capability of workspaceSkills(paths))
97
+ registry.register(capability);
98
+ };
99
+ refreshSkills();
100
+ for (const callId of this.store.queuedDirectCallIds(projectId))
101
+ calls.dispatch(projectId, callId);
102
+ return {
103
+ projectId,
104
+ registry,
105
+ calls,
106
+ context,
107
+ agents,
108
+ refreshSkills,
109
+ contractDigest: () => manifestSetDigest([
110
+ ...coreManifests(this.config.current.config),
111
+ ...registry.contractManifests()
112
+ ], this.target)
113
+ };
114
+ }
115
+ }
@@ -0,0 +1,7 @@
1
+ import { type ArtifactOutput, type CapabilityOutput, type Json } from '../core/domain.js';
2
+ export interface NormalizedOutput {
3
+ readonly content: Json;
4
+ readonly artifacts: readonly ArtifactOutput[];
5
+ readonly truncated: boolean;
6
+ }
7
+ export declare function normalizeOutput(output: CapabilityOutput, limit: number): NormalizedOutput;
@@ -0,0 +1,55 @@
1
+ import { canonicalize } from 'json-canonicalize';
2
+ import { fail } from '../core/domain.js';
3
+ export function normalizeOutput(output, limit) {
4
+ if (!Number.isSafeInteger(limit) || limit <= 0)
5
+ fail('RESULT_NORMALIZATION_FAILED', 422, 'Capability output limit is invalid.');
6
+ let remaining = limit;
7
+ let truncated = false;
8
+ let content = null;
9
+ const artifacts = [];
10
+ const serialized = Buffer.from(typeof output.content === 'string' ? output.content : canonicalize(output.content));
11
+ if (serialized.byteLength <= Math.min(remaining, 65536)) {
12
+ content = structuredClone(output.content);
13
+ remaining -= serialized.byteLength;
14
+ }
15
+ else if (serialized.byteLength <= remaining) {
16
+ artifacts.push({
17
+ name: typeof output.content === 'string' ? 'result.txt' : 'result.json',
18
+ mediaType: typeof output.content === 'string' ? 'text/plain; charset=utf-8' : 'application/json',
19
+ bytes: serialized
20
+ });
21
+ remaining -= serialized.byteLength;
22
+ }
23
+ else if (typeof output.content === 'string') {
24
+ const bytes = utf8Prefix(serialized, remaining);
25
+ if (bytes.byteLength > 0)
26
+ artifacts.push({ name: 'result.txt', mediaType: 'text/plain; charset=utf-8', bytes });
27
+ remaining -= bytes.byteLength;
28
+ truncated = true;
29
+ }
30
+ else {
31
+ truncated = true;
32
+ }
33
+ for (const artifact of output.artifacts ?? []) {
34
+ if (artifact.bytes.byteLength <= remaining) {
35
+ artifacts.push({ ...artifact, bytes: Uint8Array.from(artifact.bytes) });
36
+ remaining -= artifact.bytes.byteLength;
37
+ }
38
+ else
39
+ truncated = true;
40
+ }
41
+ return { content, artifacts, truncated };
42
+ }
43
+ function utf8Prefix(bytes, limit) {
44
+ for (let end = Math.min(bytes.byteLength, limit); end >= Math.max(0, limit - 3); end -= 1) {
45
+ const candidate = bytes.subarray(0, end);
46
+ try {
47
+ new TextDecoder('utf-8', { fatal: true }).decode(candidate);
48
+ return Uint8Array.from(candidate);
49
+ }
50
+ catch {
51
+ // Try the previous byte boundary until the prefix is valid UTF-8.
52
+ }
53
+ }
54
+ return new Uint8Array();
55
+ }
@@ -0,0 +1,9 @@
1
+ import type { ConfigRuntime } from '../config/runtime.js';
2
+ import type { Document } from '../store/store.js';
3
+ import type { AiModelGateway } from './model.js';
4
+ export declare class AutoReviewer {
5
+ readonly gateway: AiModelGateway;
6
+ readonly config: ConfigRuntime;
7
+ constructor(gateway: AiModelGateway, config: ConfigRuntime);
8
+ review(goal: string, call: Document, signal: AbortSignal): Promise<boolean>;
9
+ }
@@ -0,0 +1,54 @@
1
+ import { fail } from '../core/domain.js';
2
+ import { resolveReasoningEffort } from './profile.js';
3
+ export class AutoReviewer {
4
+ gateway;
5
+ config;
6
+ constructor(gateway, config) {
7
+ this.gateway = gateway;
8
+ this.config = config;
9
+ }
10
+ async review(goal, call, signal) {
11
+ const snapshot = this.config.current;
12
+ const role = snapshot.config.roles['auto-review'];
13
+ if (role.model === null)
14
+ fail('CONFIG_INVALID', 503, 'The auto-review role requires a model.');
15
+ const primaryModel = role.model;
16
+ const controller = new AbortController();
17
+ const timeout = setTimeout(() => controller.abort(new Error('Auto Review timed out.')), snapshot.config.autoReview.timeoutMs);
18
+ signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true });
19
+ const messages = [
20
+ { role: 'system', content: 'Review the proposed capability call against the user goal. Return only {"decision":"approve"} or {"decision":"reject"}.' },
21
+ { role: 'user', content: JSON.stringify({ goal: bounded(goal, snapshot.config.autoReview.goalMaxBytes), call }) }
22
+ ];
23
+ const candidates = [...Array.from({ length: role.providerRouting.retryMaxAttempts }, () => primaryModel), ...role.providerRouting.fallback];
24
+ try {
25
+ for (const model of candidates) {
26
+ let response = '';
27
+ try {
28
+ const resolved = this.gateway.resolve(model, snapshot.config);
29
+ for await (const event of this.gateway.stream({ model, reasoningEffort: resolveReasoningEffort(resolved.profile, role.modelReasoningEffort), maxOutputTokens: 64, signal: controller.signal, tools: [], messages }, snapshot.config)) {
30
+ if (event.type === 'text')
31
+ response += event.text;
32
+ }
33
+ const decision = JSON.parse(response);
34
+ if (decision.decision === 'approve')
35
+ return true;
36
+ if (decision.decision === 'reject')
37
+ return false;
38
+ }
39
+ catch {
40
+ if (controller.signal.aborted)
41
+ return false;
42
+ }
43
+ }
44
+ }
45
+ finally {
46
+ clearTimeout(timeout);
47
+ }
48
+ return false;
49
+ }
50
+ }
51
+ function bounded(value, maximum) {
52
+ const bytes = Buffer.from(value);
53
+ return bytes.byteLength <= maximum ? value : bytes.subarray(0, maximum).toString();
54
+ }
@@ -0,0 +1,15 @@
1
+ import { type ResourceClaim } from './capabilities.js';
2
+ export interface SchedulerLimits {
3
+ readonly instanceMax: number;
4
+ readonly perStepMax: number;
5
+ readonly perAdapterMax: number;
6
+ }
7
+ export declare class Scheduler {
8
+ #private;
9
+ constructor(limits: SchedulerLimits);
10
+ updateLimits(limits: SchedulerLimits): void;
11
+ acquire(callId: string, adapterId: string, stepId: string | null, claims: readonly ResourceClaim[], start: () => void, signal?: AbortSignal): Promise<AbortSignal>;
12
+ release(callId: string): void;
13
+ cancel(callId: string): boolean;
14
+ close(): void;
15
+ }
@@ -0,0 +1,85 @@
1
+ import { claimsConflict } from './capabilities.js';
2
+ export class Scheduler {
3
+ #leases = new Map();
4
+ #waiters = new Set();
5
+ #limits;
6
+ #closed = false;
7
+ constructor(limits) {
8
+ this.#limits = limits;
9
+ }
10
+ updateLimits(limits) {
11
+ this.#limits = limits;
12
+ this.#wake();
13
+ }
14
+ async acquire(callId, adapterId, stepId, claims, start, signal) {
15
+ while (!this.#closed) {
16
+ const controller = this.#tryAcquire(callId, adapterId, stepId, claims);
17
+ if (controller !== null) {
18
+ try {
19
+ start();
20
+ }
21
+ catch (error) {
22
+ this.#leases.delete(callId);
23
+ this.#wake();
24
+ throw error;
25
+ }
26
+ signal?.addEventListener('abort', () => controller.abort(signal.reason), { once: true });
27
+ return controller.signal;
28
+ }
29
+ await new Promise((resolve) => {
30
+ const wake = () => {
31
+ signal?.removeEventListener('abort', wake);
32
+ this.#waiters.delete(wake);
33
+ resolve();
34
+ };
35
+ this.#waiters.add(wake);
36
+ signal?.addEventListener('abort', wake, { once: true });
37
+ });
38
+ if (signal?.aborted === true)
39
+ throw signal.reason;
40
+ }
41
+ throw new Error('Scheduler is closed.');
42
+ }
43
+ release(callId) {
44
+ if (!this.#leases.delete(callId))
45
+ return;
46
+ this.#wake();
47
+ }
48
+ cancel(callId) {
49
+ const lease = this.#leases.get(callId);
50
+ if (lease === undefined)
51
+ return false;
52
+ lease.controller.abort(new Error('Call cancelled.'));
53
+ return true;
54
+ }
55
+ close() {
56
+ this.#closed = true;
57
+ for (const lease of this.#leases.values())
58
+ lease.controller.abort(new Error('Scheduler closed.'));
59
+ this.#wake();
60
+ }
61
+ #tryAcquire(callId, adapterId, stepId, claims) {
62
+ if (this.#leases.has(callId) || this.#leases.size >= this.#limits.instanceMax)
63
+ return null;
64
+ let adapterCount = 0;
65
+ let stepCount = 0;
66
+ for (const lease of this.#leases.values()) {
67
+ if (lease.adapterId === adapterId)
68
+ adapterCount += 1;
69
+ if (stepId !== null && lease.stepId === stepId)
70
+ stepCount += 1;
71
+ if (claimsConflict(lease.claims, claims))
72
+ return null;
73
+ }
74
+ if (adapterCount >= this.#limits.perAdapterMax || stepCount >= this.#limits.perStepMax)
75
+ return null;
76
+ const controller = new AbortController();
77
+ this.#leases.set(callId, { callId, adapterId, stepId, claims, controller });
78
+ return controller;
79
+ }
80
+ #wake() {
81
+ for (const wake of this.#waiters)
82
+ wake();
83
+ this.#waiters.clear();
84
+ }
85
+ }
@@ -0,0 +1,2 @@
1
+ import type { RegisteredCapability } from './capabilities.js';
2
+ export declare function workspaceSkills(workspacePaths: readonly string[]): readonly RegisteredCapability[];
@@ -0,0 +1,83 @@
1
+ import { lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
2
+ import { relative, resolve, sep } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { capabilityManifest } from './manifests.js';
5
+ const roots = ['skills', '.agents/skills', '.codex/skills'];
6
+ export function workspaceSkills(workspacePaths) {
7
+ const selected = new Map();
8
+ for (const workspacePath of workspacePaths) {
9
+ const workspace = realpathSync(resolve(workspacePath));
10
+ for (const rootName of roots) {
11
+ const root = resolve(workspace, rootName);
12
+ if (!directory(root))
13
+ continue;
14
+ for (const entry of readdirSync(root, { withFileTypes: true }).filter((item) => item.isDirectory()).sort((a, b) => a.name.localeCompare(b.name))) {
15
+ const source = resolve(root, entry.name, 'SKILL.md');
16
+ if (!file(source))
17
+ continue;
18
+ const canonical = realpathSync(source);
19
+ if (!inside(workspace, canonical))
20
+ continue;
21
+ const name = entry.name.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^-+|-+$/g, '');
22
+ if (name.length === 0)
23
+ continue;
24
+ const id = `skill:${name}`;
25
+ if (selected.has(id))
26
+ continue;
27
+ const content = readFileSync(canonical, 'utf8').trim();
28
+ const title = /^#\s+(.+)$/m.exec(content)?.[1]?.trim() ?? name;
29
+ const promptText = [
30
+ `Skill: ${title}`,
31
+ `Source: ${relative(workspace, canonical).replaceAll(sep, '/')}`,
32
+ `Content-Digest: ${createHash('sha256').update(content).digest('hex')}`,
33
+ '',
34
+ content
35
+ ].join('\n');
36
+ const adapter = {
37
+ id: `skill.${name}`,
38
+ execute: () => Promise.resolve({ content: { instructions: promptText } })
39
+ };
40
+ const definition = {
41
+ id,
42
+ title,
43
+ description: `Load the ${title} instructions from this Workspace.`,
44
+ inputSchema: { type: 'object', additionalProperties: false },
45
+ effect: 'read_only',
46
+ adapterId: adapter.id,
47
+ loadMode: 'deferred',
48
+ outputLimitBytes: 262144,
49
+ defaultAssignment: 'all_new_agents',
50
+ kind: 'skill',
51
+ promptText
52
+ };
53
+ selected.set(id, {
54
+ definition,
55
+ adapter,
56
+ manifest: capabilityManifest(definition),
57
+ claims: () => []
58
+ });
59
+ }
60
+ }
61
+ }
62
+ return [...selected.values()].sort((left, right) => left.definition.id.localeCompare(right.definition.id));
63
+ }
64
+ function directory(path) {
65
+ try {
66
+ return lstatSync(path).isDirectory();
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ function file(path) {
73
+ try {
74
+ return lstatSync(path).isFile();
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ function inside(root, candidate) {
81
+ const relation = relative(root, candidate);
82
+ return relation === '' || relation !== '..' && !relation.startsWith(`..${sep}`);
83
+ }
@@ -0,0 +1,8 @@
1
+ export interface TokenEstimate {
2
+ readonly expected: number;
3
+ readonly safe: number;
4
+ readonly estimated: true;
5
+ readonly version: 1;
6
+ }
7
+ export declare function estimateTokens(value: string): TokenEstimate;
8
+ export declare function estimateRequest(value: unknown): TokenEstimate;
@@ -0,0 +1,30 @@
1
+ export function estimateTokens(value) {
2
+ let units = 0;
3
+ let asciiRun = 0;
4
+ const flush = () => {
5
+ units += Math.ceil(asciiRun / 4);
6
+ asciiRun = 0;
7
+ };
8
+ for (const character of value) {
9
+ const code = character.codePointAt(0) ?? 0;
10
+ if (code <= 0x7f && /[a-zA-Z0-9_]/.test(character)) {
11
+ asciiRun += 1;
12
+ continue;
13
+ }
14
+ flush();
15
+ if (/\s/u.test(character))
16
+ continue;
17
+ if (code >= 0x4e00 && code <= 0x9fff)
18
+ units += 1;
19
+ else if (code > 0xffff)
20
+ units += 2;
21
+ else
22
+ units += 1;
23
+ }
24
+ flush();
25
+ const expected = Math.max(1, units);
26
+ return { expected, safe: Math.ceil(expected * 1.25) + 8, estimated: true, version: 1 };
27
+ }
28
+ export function estimateRequest(value) {
29
+ return estimateTokens(JSON.stringify(value));
30
+ }
@@ -0,0 +1,3 @@
1
+ import Database from 'better-sqlite3';
2
+ export type Sqlite = Database.Database;
3
+ export declare function openDatabase(root: string, file: string): Sqlite;
@@ -0,0 +1,49 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import Database from 'better-sqlite3';
4
+ import { id, now } from '../core/codec.js';
5
+ import { fail } from '../core/domain.js';
6
+ import { schema } from './schema.js';
7
+ /**
8
+ * Columns added after a database already existed.
9
+ *
10
+ * `schema.ts` is one idempotent script, which works for `CREATE TABLE IF NOT EXISTS` and not for a new
11
+ * column: SQLite has no `ADD COLUMN IF NOT EXISTS`, so a second start would fail on a duplicate. The
12
+ * alternative was `storage:rebuild`, which discards every Task and Run to add a nullable text column.
13
+ *
14
+ * Each entry has to stay safe to re-run for the life of the file, so this reads the current columns
15
+ * rather than tracking a version. Keep it small: it is a way to add a nullable column, not a migration
16
+ * framework.
17
+ */
18
+ const ADDED_COLUMNS = [
19
+ // Why a Run failed, not just that it did. `error_code` alone made every Provider rejection, TLS fault
20
+ // and malformed Tool block read as one indistinguishable `RUNTIME_FAILED`, and `RunDetail.error.message`
21
+ // could only repeat the code back.
22
+ { table: 'runs', column: 'error_message', definition: 'TEXT' },
23
+ { table: 'steps', column: 'error_message', definition: 'TEXT' }
24
+ ];
25
+ function addColumns(database) {
26
+ for (const { table, column, definition } of ADDED_COLUMNS) {
27
+ const columns = database.prepare(`PRAGMA table_info(${table})`).all();
28
+ if (columns.some((entry) => entry.name === column))
29
+ continue;
30
+ database.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
31
+ }
32
+ }
33
+ export function openDatabase(root, file) {
34
+ const path = resolve(root, file);
35
+ mkdirSync(dirname(path), { recursive: true });
36
+ try {
37
+ const database = new Database(path);
38
+ database.exec(schema);
39
+ addColumns(database);
40
+ database.prepare('INSERT OR IGNORE INTO instance(id, epoch, state_revision, created_at) VALUES (1, ?, 0, ?)')
41
+ .run(id('instance'), now());
42
+ return database;
43
+ }
44
+ catch (error) {
45
+ fail('DATABASE_UNAVAILABLE', 503, 'The database could not be opened.', {
46
+ reason: error instanceof Error ? error.message : String(error)
47
+ });
48
+ }
49
+ }
@@ -0,0 +1,19 @@
1
+ import { type Json } from '../core/domain.js';
2
+ interface EventRecord {
3
+ readonly revision: number;
4
+ readonly occurred_at: string;
5
+ readonly previous_hash: string | null;
6
+ readonly state: Json;
7
+ readonly hash: string;
8
+ }
9
+ export declare class EventLog {
10
+ #private;
11
+ readonly fsync: boolean;
12
+ readonly path: string;
13
+ constructor(root: string, path: string, fsync: boolean);
14
+ load(): Promise<void>;
15
+ append(revision: number, state: Json): void;
16
+ close(): Promise<void>;
17
+ }
18
+ export declare function readEventLog(path: string): Promise<readonly EventRecord[]>;
19
+ export {};
@@ -0,0 +1,61 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { appendFile, mkdir, open, readFile } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { canonicalize } from 'json-canonicalize';
5
+ import { now } from '../core/codec.js';
6
+ import { fail } from '../core/domain.js';
7
+ export class EventLog {
8
+ fsync;
9
+ path;
10
+ #tail = Promise.resolve();
11
+ #hash = null;
12
+ constructor(root, path, fsync) {
13
+ this.fsync = fsync;
14
+ this.path = resolve(root, path);
15
+ }
16
+ async load() {
17
+ const records = await readEventLog(this.path).catch((error) => {
18
+ if (isMissing(error))
19
+ return [];
20
+ throw error;
21
+ });
22
+ this.#hash = records.at(-1)?.hash ?? null;
23
+ }
24
+ append(revision, state) {
25
+ this.#tail = this.#tail.then(async () => {
26
+ const base = { revision, occurred_at: now(), previous_hash: this.#hash, state };
27
+ const record = { ...base, hash: hash(base) };
28
+ await mkdir(dirname(this.path), { recursive: true });
29
+ await appendFile(this.path, `${JSON.stringify(record)}\n`, 'utf8');
30
+ if (this.fsync) {
31
+ const file = await open(this.path, 'r');
32
+ await file.sync();
33
+ await file.close();
34
+ }
35
+ this.#hash = record.hash;
36
+ });
37
+ }
38
+ close() {
39
+ return this.#tail;
40
+ }
41
+ }
42
+ export async function readEventLog(path) {
43
+ const source = await readFile(path, 'utf8');
44
+ const records = [];
45
+ let previous = null;
46
+ for (const line of source.split(/\r?\n/).filter(Boolean)) {
47
+ const value = JSON.parse(line);
48
+ const base = { revision: value.revision, occurred_at: value.occurred_at, previous_hash: value.previous_hash, state: value.state };
49
+ if (value.previous_hash !== previous || value.hash !== hash(base))
50
+ fail('EVENT_LOG_INVALID', 503, 'The Event Log hash chain is invalid.');
51
+ records.push(value);
52
+ previous = value.hash;
53
+ }
54
+ return records;
55
+ }
56
+ function hash(value) {
57
+ return createHash('sha256').update(canonicalize(value)).digest('hex');
58
+ }
59
+ function isMissing(error) {
60
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
61
+ }
@@ -0,0 +1 @@
1
+ export declare const schema = "\nPRAGMA foreign_keys = ON;\nPRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA busy_timeout = 5000;\n\nCREATE TABLE IF NOT EXISTS instance (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n epoch TEXT NOT NULL,\n state_revision INTEGER NOT NULL CHECK (state_revision >= 0),\n created_at TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n archived_at TEXT,\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS workspaces (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n is_default INTEGER NOT NULL CHECK (is_default IN (0, 1)),\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE(project_id, name)\n) STRICT;\nCREATE UNIQUE INDEX IF NOT EXISTS workspace_default ON workspaces(project_id) WHERE is_default = 1;\n\nCREATE TABLE IF NOT EXISTS workspace_paths (\n workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,\n ordinal INTEGER NOT NULL CHECK (ordinal >= 0),\n path TEXT NOT NULL,\n PRIMARY KEY(workspace_id, ordinal),\n UNIQUE(workspace_id, path)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS tasks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n title TEXT NOT NULL,\n archived_at TEXT,\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,\n title TEXT NOT NULL,\n archived_at TEXT,\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS agents (\n id TEXT PRIMARY KEY,\n task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,\n session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,\n workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,\n parent_agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,\n name TEXT NOT NULL,\n state TEXT NOT NULL CHECK (state IN ('IDLE', 'RUNNING', 'WAITING', 'STOPPED')),\n model TEXT NOT NULL,\n model_reasoning_effort TEXT NOT NULL,\n policy_preset TEXT NOT NULL,\n context_revision INTEGER NOT NULL CHECK (context_revision >= 0),\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n source_agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,\n role TEXT NOT NULL CHECK (role IN ('system', 'user', 'assistant', 'tool')),\n content TEXT NOT NULL,\n metadata_json TEXT,\n created_at TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS pending_messages (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,\n source_agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,\n content TEXT NOT NULL,\n ordinal INTEGER NOT NULL CHECK (ordinal >= 0),\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE(agent_id, ordinal)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS runs (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n goal_message_ids_json TEXT NOT NULL DEFAULT '[]',\n initial_step_pending INTEGER NOT NULL DEFAULT 0 CHECK (initial_step_pending IN (0, 1)),\n state TEXT NOT NULL CHECK (state IN ('READY', 'RUNNING', 'WAITING', 'TERMINAL')),\n outcome TEXT CHECK (outcome IN ('success', 'failed', 'cancelled', 'interrupted')),\n error_code TEXT,\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS steps (\n id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,\n turn_group_id TEXT NOT NULL REFERENCES context_turn_groups(id) ON DELETE RESTRICT,\n ordinal INTEGER NOT NULL CHECK (ordinal >= 0),\n state TEXT NOT NULL CHECK (state IN ('RUNNING', 'WAITING', 'TERMINAL')),\n outcome TEXT CHECK (outcome IN ('success', 'failed', 'cancelled', 'interrupted')),\n error_code TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE(run_id, ordinal)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS model_attempts (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n step_id TEXT REFERENCES steps(id) ON DELETE CASCADE,\n kind TEXT NOT NULL CHECK (kind IN ('agent', 'compaction')),\n sequence INTEGER NOT NULL CHECK (sequence >= 0),\n model TEXT NOT NULL,\n state TEXT NOT NULL CHECK (state IN ('PREPARED', 'RUNNING', 'TERMINAL')),\n outcome TEXT CHECK (outcome IN ('success', 'failed', 'cancelled', 'interrupted')),\n profile_id TEXT NOT NULL,\n profile_version INTEGER NOT NULL CHECK (profile_version >= 1),\n profile_digest TEXT NOT NULL,\n config_digest TEXT NOT NULL,\n context_revision INTEGER NOT NULL CHECK (context_revision >= 0),\n request_digest TEXT NOT NULL,\n preparation_json TEXT NOT NULL,\n usage_json TEXT,\n error_code TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE(agent_id, kind, sequence)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS attempt_capabilities (\n attempt_id TEXT NOT NULL REFERENCES model_attempts(id) ON DELETE CASCADE,\n ordinal INTEGER NOT NULL CHECK (ordinal >= 0),\n capability_id TEXT NOT NULL,\n schema_json TEXT NOT NULL,\n PRIMARY KEY(attempt_id, ordinal),\n UNIQUE(attempt_id, capability_id)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS attempt_tool_ordinals (\n attempt_id TEXT NOT NULL REFERENCES model_attempts(id) ON DELETE CASCADE,\n ordinal INTEGER NOT NULL CHECK (ordinal >= 0),\n provider_tool_id TEXT,\n complete INTEGER NOT NULL CHECK (complete IN (0, 1)),\n PRIMARY KEY(attempt_id, ordinal)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS call_partitions (\n id TEXT PRIMARY KEY,\n step_id TEXT NOT NULL REFERENCES steps(id) ON DELETE CASCADE,\n ordinal INTEGER NOT NULL CHECK (ordinal >= 0),\n mode TEXT NOT NULL CHECK (mode IN ('parallel', 'serial')),\n created_at TEXT NOT NULL,\n UNIQUE(step_id, ordinal)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS calls (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE RESTRICT,\n agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,\n step_id TEXT REFERENCES steps(id) ON DELETE CASCADE,\n attempt_id TEXT REFERENCES model_attempts(id) ON DELETE CASCADE,\n partition_id TEXT REFERENCES call_partitions(id) ON DELETE CASCADE,\n parent_call_id TEXT REFERENCES calls(id) ON DELETE CASCADE,\n source TEXT NOT NULL CHECK (source IN ('user', 'llm', 'program')),\n capability_id TEXT NOT NULL,\n provider_call_id TEXT,\n call_ordinal INTEGER CHECK (call_ordinal >= 0),\n input_json TEXT NOT NULL,\n effect TEXT NOT NULL CHECK (effect IN ('read_only', 'workspace_write', 'external', 'full')),\n policy TEXT CHECK (policy IN ('read_only', 'workspace_write', 'external', 'ask', 'auto_approve', 'full_access')),\n state TEXT NOT NULL CHECK (state IN ('WAITING', 'QUEUED', 'RUNNING', 'TERMINAL')),\n wait_reason TEXT CHECK (wait_reason IN ('user_input', 'policy_review')),\n outcome TEXT CHECK (outcome IN ('success', 'failed', 'cancelled', 'interrupted')),\n outcome_unknown INTEGER NOT NULL CHECK (outcome_unknown IN (0, 1)),\n auto_review_passed INTEGER CHECK (auto_review_passed IN (0, 1)),\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE(attempt_id, call_ordinal)\n) STRICT;\nCREATE UNIQUE INDEX IF NOT EXISTS calls_by_provider_call ON calls(attempt_id, provider_call_id) WHERE provider_call_id IS NOT NULL;\n\nCREATE TABLE IF NOT EXISTS results (\n id TEXT PRIMARY KEY,\n call_id TEXT NOT NULL UNIQUE REFERENCES calls(id) ON DELETE CASCADE,\n outcome TEXT NOT NULL CHECK (outcome IN ('success', 'failed', 'cancelled', 'interrupted')),\n content_json TEXT NOT NULL,\n truncated INTEGER NOT NULL DEFAULT 0 CHECK (truncated IN (0, 1)),\n error_code TEXT,\n error_message TEXT,\n created_at TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS artifacts (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n media_type TEXT NOT NULL,\n relative_path TEXT NOT NULL,\n sha256 TEXT NOT NULL,\n size_bytes INTEGER NOT NULL CHECK (size_bytes >= 0),\n created_at TEXT NOT NULL\n) STRICT;\nCREATE UNIQUE INDEX IF NOT EXISTS artifact_identity ON artifacts(project_id, sha256, name, media_type);\nCREATE INDEX IF NOT EXISTS artifacts_by_path ON artifacts(relative_path);\n\nCREATE TABLE IF NOT EXISTS result_artifacts (\n result_id TEXT NOT NULL REFERENCES results(id) ON DELETE CASCADE,\n artifact_id TEXT NOT NULL REFERENCES artifacts(id) ON DELETE RESTRICT,\n PRIMARY KEY(result_id, artifact_id)\n) STRICT;\n\n-- No table for write_file/edit_file file history: it belongs to the Executor, which owns the files\n-- and therefore the only authoritative view of what they contained. Core keeps Call semantics and\n-- asks the Executor to revert by call_id. See\n-- docs/architecture/harness-level3/09-file-snapshot-storage-decision.md.\n\nCREATE TABLE IF NOT EXISTS context_turn_groups (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n ordinal INTEGER NOT NULL CHECK (ordinal >= 0),\n kind TEXT NOT NULL CHECK (kind IN ('turn', 'compaction', 'included', 'ledger')),\n state TEXT NOT NULL CHECK (state IN ('OPEN', 'COMPLETE', 'ABORTED')),\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n completed_at TEXT,\n UNIQUE(agent_id, ordinal)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS context_items (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n turn_group_id TEXT NOT NULL REFERENCES context_turn_groups(id) ON DELETE CASCADE,\n turn_group_ordinal INTEGER NOT NULL CHECK (turn_group_ordinal >= 0),\n kind TEXT NOT NULL CHECK (kind IN ('message', 'tool_calls', 'result', 'compaction')),\n source_id TEXT NOT NULL,\n role TEXT CHECK (role IN ('system', 'user', 'assistant', 'tool')),\n content TEXT NOT NULL,\n tool_call_id TEXT,\n tool_name TEXT,\n ordinal INTEGER NOT NULL CHECK (ordinal >= 0),\n visible INTEGER NOT NULL CHECK (visible IN (0, 1)),\n locked INTEGER NOT NULL CHECK (locked IN (0, 1)),\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE(agent_id, ordinal),\n UNIQUE(turn_group_id, turn_group_ordinal)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS context_overrides (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n target_kind TEXT NOT NULL CHECK (target_kind IN ('agent', 'message', 'result', 'artifact')),\n target_id TEXT NOT NULL,\n instructions TEXT NOT NULL,\n revision INTEGER NOT NULL CHECK (revision >= 0),\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE(agent_id, target_kind, target_id)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS projection_decisions (\n id TEXT PRIMARY KEY,\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n attempt_id TEXT REFERENCES model_attempts(id) ON DELETE CASCADE,\n stage TEXT NOT NULL,\n source_tokens INTEGER NOT NULL CHECK (source_tokens >= 0),\n projected_tokens INTEGER NOT NULL CHECK (projected_tokens >= 0),\n details_json TEXT NOT NULL,\n created_at TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS capability_assignments (\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n capability_id TEXT NOT NULL,\n policy TEXT NOT NULL CHECK (policy IN ('read_only', 'workspace_write', 'external', 'ask', 'auto_approve', 'full_access')),\n assigned_at TEXT NOT NULL,\n PRIMARY KEY(agent_id, capability_id)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS capability_loads (\n agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,\n capability_id TEXT NOT NULL,\n loaded_at TEXT NOT NULL,\n PRIMARY KEY(agent_id, capability_id),\n FOREIGN KEY(agent_id, capability_id) REFERENCES capability_assignments(agent_id, capability_id) ON DELETE CASCADE\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS agent_plans (\n agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,\n items_json TEXT NOT NULL,\n revision INTEGER NOT NULL CHECK (revision >= 0),\n updated_at TEXT NOT NULL\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS ingress_requests (\n client_id TEXT NOT NULL,\n key TEXT NOT NULL,\n operation TEXT NOT NULL,\n request_digest TEXT NOT NULL,\n response_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY(client_id, key)\n) STRICT;\n\nCREATE TABLE IF NOT EXISTS side_effect_ledgers (\n id TEXT PRIMARY KEY,\n step_id TEXT NOT NULL REFERENCES steps(id) ON DELETE CASCADE,\n content TEXT NOT NULL,\n created_at TEXT NOT NULL\n) STRICT;\n\nCREATE INDEX IF NOT EXISTS tasks_by_project ON tasks(project_id, archived_at, updated_at DESC);\nCREATE INDEX IF NOT EXISTS sessions_by_task ON sessions(task_id, archived_at, updated_at DESC);\nCREATE INDEX IF NOT EXISTS agents_by_session ON agents(session_id, parent_agent_id, created_at);\nCREATE INDEX IF NOT EXISTS pending_by_agent ON pending_messages(agent_id, ordinal);\nCREATE INDEX IF NOT EXISTS runs_by_agent ON runs(agent_id, created_at DESC);\nCREATE UNIQUE INDEX IF NOT EXISTS one_live_run_per_agent ON runs(agent_id) WHERE state != 'TERMINAL';\nCREATE INDEX IF NOT EXISTS attempts_by_step ON model_attempts(step_id, sequence);\nCREATE INDEX IF NOT EXISTS calls_by_project ON calls(project_id, state, created_at DESC);\nCREATE INDEX IF NOT EXISTS context_by_agent ON context_items(agent_id, ordinal);\nCREATE INDEX IF NOT EXISTS artifacts_by_project ON artifacts(project_id, created_at DESC);\n";