agents-relay 2.0.109 → 2.0.110

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.
@@ -1,6 +1,6 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import { mkdirSync } from 'node:fs';
3
- import { join, dirname } from 'node:path';
3
+ import { relayUnifiedStorePath } from './relay-store-discovery.js';
4
4
  function validateRepository(repository) {
5
5
  const [owner, repo, extra] = repository.split('/');
6
6
  if (!owner || !repo || extra || !/^[\w.-]+$/.test(owner) || !/^[\w.-]+$/.test(repo) || owner === '..' || repo === '..')
@@ -19,7 +19,7 @@ export function allocateEntityId(directory, repository, kind, requestKey, occupi
19
19
  if (requestKey !== undefined && (!requestKey || requestKey.length > 512))
20
20
  throw new Error('Invalid idempotency key');
21
21
  mkdirSync(directory, { recursive: true });
22
- const db = new DatabaseSync(process.env.AGENTS_RELAY_SQLITE_MODE === 'unified' ? join(dirname(directory), 'relay.sqlite') : join(directory, 'entity-id-sequences.sqlite'));
22
+ const db = new DatabaseSync(relayUnifiedStorePath(directory));
23
23
  try {
24
24
  db.exec('PRAGMA journal_mode=WAL; PRAGMA busy_timeout=10000;');
25
25
  db.exec('CREATE TABLE IF NOT EXISTS sequences (repository TEXT NOT NULL, kind TEXT NOT NULL, value INTEGER NOT NULL, PRIMARY KEY(repository,kind)); CREATE TABLE IF NOT EXISTS idempotency (repository TEXT NOT NULL, kind TEXT NOT NULL, request_key TEXT NOT NULL, entity_id TEXT NOT NULL, PRIMARY KEY(repository,kind,request_key));');
@@ -1,12 +1,5 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { homedir } from 'node:os';
3
2
  import { join } from 'node:path';
4
- /** Primary local store key is immutable Job ID, independent of a PR number. */
5
- export function relayJobStorePath(directory, jobId) {
6
- if (!jobId.trim() || jobId.length > 1024)
7
- throw new Error('Invalid immutable Job ID');
8
- return directory + '/job-' + createHash('sha256').update(jobId).digest('hex') + '.db';
9
- }
10
3
  export function relayStoreDirectory(env = process.env) {
11
4
  return env.AGENTS_RELAY_STORE_DIR || join(homedir(), '.agents-relay', 'stores');
12
5
  }
@@ -1,6 +1,6 @@
1
1
  import { GitHubStore, InMemoryStore } from './store.js';
2
2
  import { LocalAuthorityDurableStore } from './local-authority-store.js';
3
- import { discoverLocalRelayStoreSnapshots, unifiedRelayStoreEnabled, SQLiteRelayStore } from './relay-store.js';
3
+ import { discoverLocalRelayStoreSnapshots, SQLiteRelayStore } from './relay-store.js';
4
4
  /** Resolve exactly one SQLite-backed Job from its immutable ID, independent of PR binding. */
5
5
  export function createJobIdRegistryResolver(directory, openStores, options = {}) {
6
6
  return async (jobId) => {
@@ -12,10 +12,10 @@ export function createJobIdRegistryResolver(directory, openStores, options = {})
12
12
  if (!selected)
13
13
  return null;
14
14
  const { snapshot, job } = selected;
15
- const scopedKey = unifiedRelayStoreEnabled() ? `${snapshot.path}#${job.id}` : snapshot.path;
15
+ const scopedKey = `${snapshot.path}#${job.id}`;
16
16
  let relay = openStores.get(scopedKey);
17
17
  if (!relay) {
18
- relay = new SQLiteRelayStore(snapshot.path, unifiedRelayStoreEnabled() ? job.id : undefined);
18
+ relay = new SQLiteRelayStore(snapshot.path, job.id);
19
19
  openStores.set(scopedKey, relay);
20
20
  }
21
21
  // SQLite remains lifecycle authority. A positive PR binding only enables
@@ -2,16 +2,16 @@ import { GhApiClient } from './github-client.js';
2
2
  import { DisabledGitHubClient, noGitHubMode } from './no-github.js';
3
3
  import { GitHubStore } from './store.js';
4
4
  import { LocalAuthorityDurableStore } from './local-authority-store.js';
5
- import { relayStoreDirectory, relayStorePath, relayJobStorePath, relayUnifiedStorePath, unifiedRelayStoreEnabled, resolveUnifiedJobScope, SQLiteRelayStore } from './relay-store.js';
5
+ import { relayStoreDirectory, relayUnifiedStorePath, resolveUnifiedJobScope, SQLiteRelayStore } from './relay-store.js';
6
6
  /** Open the canonical V2 SQLite authority without making any GitHub request. */
7
7
  export function openLocalManagedStore(repository, prNumber, options = {}) {
8
8
  if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) || repository.includes('..') || !Number.isInteger(prNumber) || prNumber < 0 || (prNumber === 0 && !options.jobId)) {
9
9
  throw new Error('Local managed store requires a repository and either a positive PR number or immutable jobId');
10
10
  }
11
11
  const directory = options.directory ?? relayStoreDirectory();
12
- const unified = unifiedRelayStoreEnabled();
13
- const scope = unified ? resolveUnifiedJobScope(directory, repository, prNumber, options.jobId) : undefined;
14
- const relay = new SQLiteRelayStore(unified ? relayUnifiedStorePath(directory) : (prNumber === 0 ? relayJobStorePath(directory, options.jobId) : relayStorePath(directory, repository, prNumber)), scope);
12
+ const path = relayUnifiedStorePath(directory);
13
+ const scope = resolveUnifiedJobScope(directory, repository, prNumber, options.jobId);
14
+ const relay = new SQLiteRelayStore(path, scope);
15
15
  const client = prNumber === 0 || options.noGitHub || noGitHubMode() ? new DisabledGitHubClient() : options.client ?? new GhApiClient();
16
16
  const github = new GitHubStore(client, repository, prNumber, options.trustedAuthors ?? new Set());
17
17
  return {
@@ -1,16 +1,22 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
- import { dirname, join } from 'node:path';
3
2
  import { readdir } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { existsSync } from 'node:fs';
4
5
  import { normalizeLegacyJobPrNumber } from './job-pr-number-migration.js';
5
- /** Unified mode must be opted into only after the complete offline migration passes. */
6
- export function unifiedRelayStoreEnabled(env = process.env) {
7
- return env.AGENTS_RELAY_SQLITE_MODE === 'unified';
8
- }
9
6
  export function relayUnifiedStorePath(directory) { return join(dirname(directory), 'relay.sqlite'); }
10
7
  /** Resolve one Job scope by repository and PR, refusing ambiguous matches. */
11
8
  export function resolveUnifiedJobScope(directory, repository, prNumber, hintId) {
12
- const db = new DatabaseSync(relayUnifiedStorePath(directory), { readOnly: true });
9
+ const path = relayUnifiedStorePath(directory);
10
+ if (hintId && !existsSync(path))
11
+ return hintId;
12
+ const db = new DatabaseSync(path, { readOnly: true });
13
13
  try {
14
+ const hasRegistry = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='source_registry'").get();
15
+ if (!hasRegistry) {
16
+ if (hintId)
17
+ return hintId;
18
+ throw new Error(`Unified Job scope is missing for ${repository} PR ${prNumber}; supply immutable Job ID`);
19
+ }
14
20
  if (hintId) {
15
21
  const binding = db.prepare('SELECT repository,pr_number FROM source_registry WHERE job_id=?').get(hintId);
16
22
  if (binding && (binding.repository !== repository || (prNumber > 0 && binding.pr_number !== prNumber)))
@@ -26,68 +32,39 @@ export function resolveUnifiedJobScope(directory, repository, prNumber, hintId)
26
32
  db.close();
27
33
  }
28
34
  }
29
- /** Canonical on-disk Relay Store location for one managed GitHub pull request. */
30
- export function relayStorePath(directory, repository, prNumber) {
31
- return directory + '/' + `${repository.replace('/', '_')}-${prNumber}.db`;
32
- }
33
35
  /**
34
36
  * SQLite is the only job registry. Inventory queries must be strictly read-only:
35
37
  * opening a writable RelayStore here would reset in-flight projection claims
36
38
  * on every dashboard refresh and could replay external merge side effects.
37
39
  */
38
40
  export async function discoverLocalRelayStoreSnapshots(directory) {
39
- if (unifiedRelayStoreEnabled()) {
40
- const path = relayUnifiedStorePath(directory);
41
- const db = new DatabaseSync(path, { readOnly: true });
42
- try {
43
- const snapshots = [];
44
- const rows = db.prepare('SELECT json FROM job_snapshot ORDER BY CAST(job_id AS INTEGER)').all();
45
- const allPublications = Object.fromEntries(db.prepare('SELECT job_id,json FROM publication_state').all().map(row => [row.job_id, JSON.parse(row.json)]));
46
- for (const row of rows) {
47
- const job = normalizeLegacyJobPrNumber(JSON.parse(row.json));
48
- const fact = db.prepare('SELECT state,head_ref,head_sha,observed_at FROM pull_request_facts WHERE scope_job_id=? AND id=1').get(job.id);
49
- const facts = fact ? { state: fact.state, head: fact.head_ref && fact.head_sha ? { ref: fact.head_ref, sha: fact.head_sha } : null, observedAt: fact.observed_at } : null;
50
- const publications = {};
51
- if (allPublications[job.id])
52
- publications[job.id] = allPublications[job.id];
53
- snapshots.push({ path, jobs: [job], facts, publications });
54
- }
55
- return snapshots;
56
- }
57
- finally {
58
- db.close();
41
+ const path = relayUnifiedStorePath(directory);
42
+ if (!existsSync(path)) {
43
+ // Never silently treat legacy per-Job files as an empty authoritative database.
44
+ const files = await readdir(directory).catch(error => {
45
+ if (error.code === 'ENOENT')
46
+ return [];
47
+ throw error;
48
+ });
49
+ if (files.some(name => name.endsWith('.db'))) {
50
+ throw new Error(`Unified Relay SQLite database missing at ${path}; legacy Job databases exist in ${directory}`);
59
51
  }
52
+ return [];
60
53
  }
61
- let names;
54
+ const db = new DatabaseSync(path, { readOnly: true });
62
55
  try {
63
- names = await readdir(directory);
56
+ const rows = db.prepare('SELECT json FROM job_snapshot ORDER BY CAST(job_id AS INTEGER), job_id').all();
57
+ const allPublications = Object.fromEntries(db.prepare('SELECT job_id,json FROM publication_state').all().map(row => [row.job_id, JSON.parse(row.json)]));
58
+ return rows.map(row => {
59
+ const job = normalizeLegacyJobPrNumber(JSON.parse(row.json));
60
+ const fact = db.prepare('SELECT state,head_ref,head_sha,observed_at FROM pull_request_facts WHERE scope_job_id=? AND id=1').get(job.id);
61
+ return { path, jobs: [job], facts: fact ? { state: fact.state, head: fact.head_ref && fact.head_sha ? { ref: fact.head_ref, sha: fact.head_sha } : null, observedAt: fact.observed_at } : null, publications: allPublications[job.id] ? { [job.id]: allPublications[job.id] } : {} };
62
+ });
64
63
  }
65
- catch {
66
- return [];
64
+ catch (error) {
65
+ throw new Error(`Unified Relay SQLite inventory unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
67
66
  }
68
- const snapshots = [];
69
- for (const name of names.filter(item => item.endsWith('.db')).sort()) {
70
- const path = directory + '/' + name;
71
- let db;
72
- try {
73
- db = new DatabaseSync(path, { readOnly: true });
74
- const rows = db.prepare('SELECT json FROM job_snapshot ORDER BY job_id').all();
75
- const jobs = rows.map(row => normalizeLegacyJobPrNumber(JSON.parse(row.json)));
76
- const row = db.prepare('SELECT state,head_ref,head_sha,observed_at FROM pull_request_facts WHERE id=1').get();
77
- const facts = row ? {
78
- state: row.state,
79
- head: row.head_ref && row.head_sha ? { ref: row.head_ref, sha: row.head_sha } : null,
80
- observedAt: row.observed_at,
81
- } : null;
82
- const publications = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='publication_state'").get() ? Object.fromEntries(db.prepare('SELECT job_id,json FROM publication_state').all().map(item => [item.job_id, JSON.parse(item.json)])) : {};
83
- snapshots.push({ path, jobs, facts, publications });
84
- }
85
- catch {
86
- // Ignore unrelated/corrupt files without allowing one damaged store to block recovery.
87
- }
88
- finally {
89
- db?.close();
90
- }
67
+ finally {
68
+ db.close();
91
69
  }
92
- return snapshots;
93
70
  }
@@ -7,6 +7,7 @@ import { recordPublicationInDatabase, loadPublicationFromDatabase, listPublicati
7
7
  import { observability } from './observability.js';
8
8
  import { createJobIfAbsent } from './relay-create.js';
9
9
  import { ensureUnifiedJobRegistry, createUnifiedJobIfAbsent } from './unified-job-registry.js';
10
+ import { migrateUnifiedRelaySchema } from './unified-relay-schema.js';
10
11
  import { saveUnifiedPullRequestFacts } from './unified-pr-facts.js';
11
12
  import { migrateLegacyJobPrNumbers, normalizeLegacyJobPrNumber } from './job-pr-number-migration.js';
12
13
  import { hostInvocationTablesSql, recordHostInvocationEvent, hasHostInvocationDecision, recordHostInvocationDecision } from './host-invocation-store.js';
@@ -57,9 +58,14 @@ export class SQLiteRelayStore {
57
58
  this.db = new DatabaseSync(path);
58
59
  this.db.exec('PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON;');
59
60
  if (scopeJobId) {
60
- const schema = this.db.prepare("SELECT value FROM migration_metadata WHERE key='schema'").get();
61
- if (schema?.value !== 'single-sqlite-staging-v1')
62
- throw new Error('Unified Relay schema is missing or unsupported');
61
+ const hasUnifiedSchema = this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='migration_metadata'").get();
62
+ if (!hasUnifiedSchema)
63
+ migrateUnifiedRelaySchema(this.db);
64
+ else {
65
+ const schema = this.db.prepare("SELECT value FROM migration_metadata WHERE key='schema'").get();
66
+ if (schema?.value !== 'single-sqlite-staging-v1')
67
+ throw new Error('Unified Relay schema is missing or unsupported');
68
+ }
63
69
  }
64
70
  else
65
71
  this.migrate();
@@ -460,5 +466,5 @@ export class SQLiteRelayStore {
460
466
  async recordProjectionReceipt(operationKey, status, attempts, detail) { this.outbox.recordReceipt(operationKey, status, attempts, detail, new Date().toISOString()); }
461
467
  close() { this.db.close(); }
462
468
  }
463
- export { relayJobStorePath, relayStoreDirectory } from './local-job-path.js';
464
- export { unifiedRelayStoreEnabled, relayUnifiedStorePath, resolveUnifiedJobScope, relayStorePath, discoverLocalRelayStoreSnapshots } from './relay-store-discovery.js';
469
+ export { relayStoreDirectory } from './local-job-path.js';
470
+ export { relayUnifiedStorePath, resolveUnifiedJobScope, discoverLocalRelayStoreSnapshots } from './relay-store-discovery.js';
package/dist/relayd.js CHANGED
@@ -13,7 +13,7 @@ import { Reconciler } from './reconciler.js';
13
13
  import { dashboardManagedJobs, GitHubStore, InMemoryStore, resolvePullRequest } from './store.js';
14
14
  import { GitHubProjectionWriter } from './projected-store.js';
15
15
  import { LocalAuthorityDurableStore } from './local-authority-store.js';
16
- import { discoverLocalRelayStoreSnapshots, relayStoreDirectory, relayStorePath, relayUnifiedStorePath, unifiedRelayStoreEnabled, resolveUnifiedJobScope, SQLiteRelayStore } from './relay-store.js';
16
+ import { discoverLocalRelayStoreSnapshots, relayStoreDirectory, relayUnifiedStorePath, resolveUnifiedJobScope, SQLiteRelayStore } from './relay-store.js';
17
17
  import { createJobIdRegistryResolver } from './local-job-registry.js';
18
18
  import { projectAuthoritativeLocalMerge } from './local-merge-projection.js';
19
19
  import { archiveDisplayPullRequest } from './github-pr-archive.js';
@@ -116,8 +116,8 @@ export async function runDaemon(argv) {
116
116
  const key = `${repository}#${prNumber}`;
117
117
  let relay = relayStores.get(key);
118
118
  if (!relay) {
119
- const scope = unifiedRelayStoreEnabled() ? resolveUnifiedJobScope(relayStoreDirectory(), repository, prNumber) : undefined;
120
- relay = new SQLiteRelayStore(scope ? relayUnifiedStorePath(relayStoreDirectory()) : relayStorePath(relayStoreDirectory(), repository, prNumber), scope);
119
+ const scope = resolveUnifiedJobScope(relayStoreDirectory(), repository, prNumber);
120
+ relay = new SQLiteRelayStore(relayUnifiedStorePath(relayStoreDirectory()), scope);
121
121
  relayStores.set(key, relay);
122
122
  }
123
123
  const github = new GitHubStore(client, repository, prNumber, trusted, seed);
@@ -229,10 +229,10 @@ export async function runDaemon(argv) {
229
229
  if (targetRef !== 'refs/heads/main')
230
230
  throw new Error('GitHub sync only supports local main as merge target');
231
231
  const branch = sourceRef.slice('refs/heads/'.length), base = targetRef.slice('refs/heads/'.length);
232
- const scopedKey = unifiedRelayStoreEnabled() ? `${snapshot.path}#${discovered.id}` : snapshot.path;
232
+ const scopedKey = `${snapshot.path}#${discovered.id}`;
233
233
  let relay = relayStores.get(scopedKey);
234
234
  if (!relay) {
235
- relay = new SQLiteRelayStore(snapshot.path, unifiedRelayStoreEnabled() ? discovered.id : undefined);
235
+ relay = new SQLiteRelayStore(snapshot.path, discovered.id);
236
236
  relayStores.set(scopedKey, relay);
237
237
  }
238
238
  const canonical = repositoryPaths.get(discovered.repository) ?? await resolveManagedProjectPath(discovered.repository, explicitProjectPath, workspaceRoot);
@@ -1,10 +1,10 @@
1
1
  import { eventFor } from './events.js';
2
- import { SQLiteRelayStore, relayStoreDirectory, relayStorePath, relayUnifiedStorePath, unifiedRelayStoreEnabled } from './relay-store.js';
2
+ import { SQLiteRelayStore, relayStoreDirectory, relayUnifiedStorePath } from './relay-store.js';
3
3
  /** Single-Job service uses the same durable publish facts as the repository daemon. */
4
4
  export async function handleServicePublication(job, wake, bus, directory = relayStoreDirectory()) {
5
5
  if (!wake.publication && !wake.publishFailure)
6
6
  return;
7
- const sqlite = new SQLiteRelayStore(unifiedRelayStoreEnabled() ? relayUnifiedStorePath(directory) : relayStorePath(directory, job.repository, job.prNumber), unifiedRelayStoreEnabled() ? job.id : undefined);
7
+ const sqlite = new SQLiteRelayStore(relayUnifiedStorePath(directory), job.id);
8
8
  try {
9
9
  await sqlite.recordPublication(job.id, wake.deliveryId, wake.publication, wake.publishFailure ?? null, wake.publicationObservedAt);
10
10
  }
@@ -0,0 +1,26 @@
1
+ import { hostInvocationTablesSql } from './host-invocation-store.js';
2
+ /** Bootstrap the single authoritative Relay SQLite schema. */
3
+ export function migrateUnifiedRelaySchema(db) {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS source_registry (job_id TEXT PRIMARY KEY, source_file TEXT NOT NULL UNIQUE, repository TEXT NOT NULL, pr_number INTEGER NOT NULL);
6
+ CREATE INDEX IF NOT EXISTS source_registry_repo_pr ON source_registry(repository,pr_number);
7
+ CREATE TABLE IF NOT EXISTS migration_metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
8
+ CREATE TABLE IF NOT EXISTS relay_meta (scope_job_id TEXT NOT NULL REFERENCES source_registry(job_id), key TEXT NOT NULL, value TEXT NOT NULL, PRIMARY KEY(scope_job_id,key));
9
+ CREATE TABLE IF NOT EXISTS job_snapshot (job_id TEXT PRIMARY KEY, json TEXT NOT NULL, updated_at TEXT NOT NULL);
10
+ CREATE INDEX IF NOT EXISTS job_snapshot_updated ON job_snapshot(updated_at);
11
+ CREATE TABLE IF NOT EXISTS pull_request_facts (scope_job_id TEXT NOT NULL REFERENCES source_registry(job_id), id INTEGER NOT NULL CHECK(id=1), state TEXT NOT NULL CHECK(state IN ('OPEN','MERGED','CLOSED')), head_ref TEXT, head_sha TEXT, observed_at TEXT NOT NULL, PRIMARY KEY(scope_job_id,id));
12
+ CREATE TABLE IF NOT EXISTS publication_state (job_id TEXT PRIMARY KEY, json TEXT NOT NULL);
13
+ CREATE TABLE IF NOT EXISTS publication_delivery (job_id TEXT NOT NULL, delivery_id TEXT NOT NULL, type TEXT NOT NULL, json TEXT NOT NULL, observed_at TEXT NOT NULL, PRIMARY KEY(job_id,delivery_id));
14
+ CREATE TABLE IF NOT EXISTS agent_snapshot (scope_job_id TEXT NOT NULL REFERENCES source_registry(job_id), agent_id TEXT NOT NULL, json TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(scope_job_id,agent_id));
15
+ CREATE TABLE IF NOT EXISTS event_receipt (scope_job_id TEXT NOT NULL REFERENCES source_registry(job_id), event_id TEXT NOT NULL, stream TEXT, sequence INTEGER, received_at TEXT NOT NULL, PRIMARY KEY(scope_job_id,event_id));
16
+ CREATE TABLE IF NOT EXISTS execution_process (job_id TEXT NOT NULL, task_id TEXT NOT NULL, execution_id TEXT NOT NULL, lease_owner TEXT NOT NULL, host TEXT NOT NULL, pid INTEGER NOT NULL, pgid INTEGER NOT NULL, start_token TEXT NOT NULL, observed_at TEXT NOT NULL, ended_at TEXT, PRIMARY KEY(job_id,task_id,execution_id));
17
+ ${hostInvocationTablesSql}
18
+ CREATE TABLE IF NOT EXISTS projection_outbox (scope_job_id TEXT NOT NULL REFERENCES source_registry(job_id), id INTEGER NOT NULL, idempotency_key TEXT NOT NULL, operation_key TEXT, coalesce_key TEXT, operation TEXT NOT NULL, payload_json TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','processing','conflict','dead')), attempts INTEGER NOT NULL DEFAULT 0, next_attempt_at TEXT, last_error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, conflict_definition_json TEXT, conflict_evidence_json TEXT, conflict_detected_at TEXT, conflict_disposition TEXT, conflict_resolution_json TEXT, PRIMARY KEY(scope_job_id,id), UNIQUE(scope_job_id,idempotency_key));
19
+ CREATE INDEX IF NOT EXISTS projection_unified_due ON projection_outbox(scope_job_id,status,next_attempt_at,priority DESC,id);
20
+ CREATE INDEX IF NOT EXISTS projection_unified_coalesce ON projection_outbox(scope_job_id,coalesce_key,status);
21
+ CREATE TABLE IF NOT EXISTS projection_receipt (scope_job_id TEXT NOT NULL REFERENCES source_registry(job_id), operation_key TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('succeeded','failed')), attempts INTEGER NOT NULL, detail_json TEXT, recorded_at TEXT NOT NULL, PRIMARY KEY(scope_job_id,operation_key));
22
+ CREATE TABLE IF NOT EXISTS sequences (repository TEXT NOT NULL, kind TEXT NOT NULL, value INTEGER NOT NULL, PRIMARY KEY(repository,kind));
23
+ CREATE TABLE IF NOT EXISTS idempotency (repository TEXT NOT NULL, kind TEXT NOT NULL, request_key TEXT NOT NULL, entity_id TEXT NOT NULL, PRIMARY KEY(repository,kind,request_key));
24
+ INSERT INTO migration_metadata(key,value) VALUES('schema','single-sqlite-staging-v1'),('job_count','0');
25
+ `);
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.109",
3
+ "version": "2.0.110",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "repository": {
6
6
  "type": "git",