agents-relay 2.0.69 → 2.0.71

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,9 +1,7 @@
1
1
  import { openLocalManagedStore } from './local-managed-store.js';
2
2
  import { createJobEntity } from './workflow/entity-creation.js';
3
3
  import { normalizeThreadId } from './reviewer-thread.js';
4
- import { discoverLocalRelayStoreSnapshots } from './relay-store.js';
5
- import { homedir } from 'node:os';
6
- import { join } from 'node:path';
4
+ import { discoverLocalRelayStoreSnapshots, relayStoreDirectory } from './relay-store.js';
7
5
  /**
8
6
  * Job lifecycle bootstrap: SQLite is authoritative. No GitHub request is
9
7
  * required; an explicit PR number is optional external metadata.
@@ -16,7 +14,7 @@ export async function ensureManagedGitHubJob(_client, repositoryName, _trustedAu
16
14
  throw new Error('Invalid external PR binding number');
17
15
  const placeholder = { number: requestedNumber, title: options.title ?? options.id,
18
16
  body: options.body ?? options.objective ?? '', state: 'OPEN', head: options.head ?? '', base: options.base ?? '' };
19
- const directory = options.storeDirectory ?? join(homedir(), '.agents-relay', 'stores');
17
+ const directory = options.storeDirectory ?? relayStoreDirectory();
20
18
  const alreadyKnown = (await discoverLocalRelayStoreSnapshots(directory)).flatMap(snapshot => snapshot.jobs.filter(job => job.id === options.id).map(job => ({ job, path: snapshot.path })));
21
19
  if (alreadyKnown.length > 1)
22
20
  throw new Error('Duplicate immutable Job ID across SQLite stores: ' + options.id);
@@ -4,8 +4,7 @@ import { githubAuthContext } from './github-auth.js';
4
4
  import { noGitHubMode } from './no-github.js';
5
5
  import { basename } from 'node:path';
6
6
  import { createJobIdRegistryResolver } from './local-job-registry.js';
7
- import { homedir } from 'node:os';
8
- import { join } from 'node:path';
7
+ import { relayStoreDirectory } from './relay-store.js';
9
8
  import { NatsEventBus, eventFor } from './events.js';
10
9
  import { parseJobSchedule } from './scheduler.js';
11
10
  import { parseJobTagJson } from './task-tag.js';
@@ -73,7 +72,7 @@ export async function storeFor(args) {
73
72
  const jobId = arg(args, '--id');
74
73
  if (!repo || !jobId)
75
74
  throw new Error('Local Job requires --repo and --id (or explicit --file)');
76
- const directory = arg(args, '--store-dir') || join(homedir(), '.agents-relay', 'stores');
75
+ const directory = arg(args, '--store-dir') || relayStoreDirectory();
77
76
  const binding = await createJobIdRegistryResolver(directory, new Map())(jobId);
78
77
  if (!binding || binding.job.repository !== repo)
79
78
  throw new Error('Immutable local Job not found for repository: ' + jobId);
package/dist/cli.js CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
2
  import { configureLocalGitMerge } from './cli-local-merge-config.js';
5
3
  import { noGitHubMode } from './no-github.js';
6
- import { discoverLocalRelayStoreSnapshots } from './relay-store.js';
4
+ import { discoverLocalRelayStoreSnapshots, relayStoreDirectory } from './relay-store.js';
7
5
  import { realpathSync } from 'node:fs';
8
6
  import { once } from 'node:events';
9
7
  import { fileURLToPath } from 'node:url';
@@ -489,7 +487,7 @@ export async function createService(args) {
489
487
  let overviewProvider;
490
488
  if (repo) {
491
489
  overviewProvider = async () => {
492
- const snapshots = await discoverLocalRelayStoreSnapshots(join(homedir(), '.agents-relay', 'stores'));
490
+ const snapshots = await discoverLocalRelayStoreSnapshots(relayStoreDirectory());
493
491
  return dashboardManagedJobs(snapshots.flatMap(snapshot => snapshot.jobs
494
492
  .filter(job => job.repository === repo)
495
493
  .map(job => ({
@@ -133,6 +133,8 @@ export class LocalAuthorityDurableStore {
133
133
  const children = mergeInboundJobSnapshot(current, incoming);
134
134
  const preserveTerminal = ['COMPLETED', 'CANCELLED'].includes(current.state)
135
135
  && !['COMPLETED', 'CANCELLED'].includes(incoming.state);
136
+ // A stale snapshot must not undo an already committed Graph activation.
137
+ const preserveActivation = current.state === 'ACTIVE' && incoming.state === 'DRAFT';
136
138
  const review = current.review && incoming.review
137
139
  && current.review.updatedAt > incoming.review.updatedAt
138
140
  ? current.review : incoming.review;
@@ -142,7 +144,7 @@ export class LocalAuthorityDurableStore {
142
144
  const scalarSource = incoming.updatedAt > current.updatedAt ? incoming : current;
143
145
  return {
144
146
  ...current, ...scalarSource,
145
- state: preserveTerminal ? current.state : scalarSource.state,
147
+ state: preserveTerminal || preserveActivation ? current.state : scalarSource.state,
146
148
  updatedAt: current.updatedAt > incoming.updatedAt ? current.updatedAt : incoming.updatedAt,
147
149
  tasks: children.tasks,
148
150
  ...(children.plans ? { plans: children.plans } : {}),
@@ -1,7 +1,12 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
2
4
  /** Primary local store key is immutable Job ID, independent of a PR number. */
3
5
  export function relayJobStorePath(directory, jobId) {
4
6
  if (!jobId.trim() || jobId.length > 1024)
5
7
  throw new Error('Invalid immutable Job ID');
6
8
  return directory + '/job-' + createHash('sha256').update(jobId).digest('hex') + '.db';
7
9
  }
10
+ export function relayStoreDirectory(env = process.env) {
11
+ return env.AGENTS_RELAY_STORE_DIR || join(homedir(), '.agents-relay', 'stores');
12
+ }
@@ -1,16 +1,14 @@
1
- import { homedir } from 'node:os';
2
- import { join } from 'node:path';
3
1
  import { GhApiClient } from './github-client.js';
4
2
  import { DisabledGitHubClient, noGitHubMode } from './no-github.js';
5
3
  import { GitHubStore } from './store.js';
6
4
  import { LocalAuthorityDurableStore } from './local-authority-store.js';
7
- import { relayStorePath, relayJobStorePath, SQLiteRelayStore } from './relay-store.js';
5
+ import { relayStoreDirectory, relayStorePath, relayJobStorePath, SQLiteRelayStore } from './relay-store.js';
8
6
  /** Open the canonical V2 SQLite authority without making any GitHub request. */
9
7
  export function openLocalManagedStore(repository, prNumber, options = {}) {
10
8
  if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository) || repository.includes('..') || !Number.isInteger(prNumber) || prNumber < 0 || (prNumber === 0 && !options.jobId)) {
11
9
  throw new Error('Local managed store requires a repository and either a positive PR number or immutable jobId');
12
10
  }
13
- const directory = options.directory ?? join(homedir(), '.agents-relay', 'stores');
11
+ const directory = options.directory ?? relayStoreDirectory();
14
12
  const relay = new SQLiteRelayStore(prNumber === 0 ? relayJobStorePath(directory, options.jobId) : relayStorePath(directory, repository, prNumber));
15
13
  const client = prNumber === 0 || options.noGitHub || noGitHubMode() ? new DisabledGitHubClient() : options.client ?? new GhApiClient();
16
14
  const github = new GitHubStore(client, repository, prNumber, options.trustedAuthors ?? new Set());
@@ -367,7 +367,7 @@ export class SQLiteRelayStore {
367
367
  export function relayStorePath(directory, repository, prNumber) {
368
368
  return directory + '/' + `${repository.replace('/', '_')}-${prNumber}.db`;
369
369
  }
370
- export { relayJobStorePath } from './local-job-path.js';
370
+ export { relayJobStorePath, relayStoreDirectory } from './local-job-path.js';
371
371
  /**
372
372
  * SQLite is the only job registry. Inventory queries must be strictly read-only:
373
373
  * opening a writable RelayStore here would reset in-flight projection claims
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  export function createDashboardActions(deps) {
3
- const { cachedOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, applyOrchestratorStartAction, applyOrchestratorCompleteAction, publishOrchestratorTaskStarted, publishOrchestratorTaskCompletion, bus, dashboardTaskAction, dashboardTaskSetState } = deps;
4
- const dashboardOverview = (jobId, target) => cachedOverviews.find((item) => item.job.id === jobId
3
+ const { getOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, applyOrchestratorStartAction, applyOrchestratorCompleteAction, publishOrchestratorTaskStarted, publishOrchestratorTaskCompletion, bus, dashboardTaskAction, dashboardTaskSetState } = deps;
4
+ const currentOverviews = () => getOverviews();
5
+ const dashboardOverview = (jobId, target) => currentOverviews().find((item) => item.job.id === jobId
5
6
  && (!target?.repository || item.job.repository === target.repository)
6
7
  && (!target?.prNumber || (item.job.externalPrNumber ?? item.job.prNumber) === target.prNumber));
7
8
  const workspaceJobAction = (action) => async (jobId) => {
8
- const overview = cachedOverviews.find((item) => item.job.id === jobId);
9
+ const overview = currentOverviews().find((item) => item.job.id === jobId);
9
10
  if (!overview)
10
11
  throw new Error(`Managed job ${jobId} not found`);
11
12
  const store = (await requireLocalJobBinding(jobId)).store;
@@ -25,7 +26,7 @@ export function createDashboardActions(deps) {
25
26
  return result;
26
27
  };
27
28
  const workspaceTaskCreate = async (jobId, task) => {
28
- const overview = cachedOverviews.find((item) => item.job.id === jobId);
29
+ const overview = currentOverviews().find((item) => item.job.id === jobId);
29
30
  if (!overview)
30
31
  throw new Error(`Managed job ${jobId} not found`);
31
32
  if (task.jobId !== jobId)
@@ -59,7 +60,7 @@ export function createDashboardActions(deps) {
59
60
  return await store.load(jobId);
60
61
  };
61
62
  const workspaceTaskStart = async (jobId, taskId) => {
62
- const overview = cachedOverviews.find((item) => item.job.id === jobId);
63
+ const overview = currentOverviews().find((item) => item.job.id === jobId);
63
64
  if (!overview)
64
65
  throw new Error(`Managed job ${jobId} not found`);
65
66
  const store = (await requireLocalJobBinding(jobId)).store;
@@ -74,7 +75,7 @@ export function createDashboardActions(deps) {
74
75
  return await store.load(jobId);
75
76
  };
76
77
  const workspaceTaskComplete = async (jobId, taskId, summary) => {
77
- const overview = cachedOverviews.find((item) => item.job.id === jobId);
78
+ const overview = currentOverviews().find((item) => item.job.id === jobId);
78
79
  if (!overview)
79
80
  throw new Error(`Managed job ${jobId} not found`);
80
81
  const store = (await requireLocalJobBinding(jobId)).store;
@@ -89,7 +90,7 @@ export function createDashboardActions(deps) {
89
90
  return await store.load(jobId);
90
91
  };
91
92
  const workspaceTaskAction = (action) => async (jobId, taskId) => {
92
- const overview = cachedOverviews.find((item) => item.job.id === jobId);
93
+ const overview = currentOverviews().find((item) => item.job.id === jobId);
93
94
  if (!overview)
94
95
  throw new Error(`Managed job ${jobId} not found`);
95
96
  const store = (await requireLocalJobBinding(jobId)).store;
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, relayStorePath, SQLiteRelayStore } from './relay-store.js';
16
+ import { discoverLocalRelayStoreSnapshots, relayStoreDirectory, relayStorePath, 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,7 +116,7 @@ export async function runDaemon(argv) {
116
116
  const key = `${repository}#${prNumber}`;
117
117
  let relay = relayStores.get(key);
118
118
  if (!relay) {
119
- relay = new SQLiteRelayStore(relayStorePath(join(homedir(), '.agents-relay', 'stores'), repository, prNumber));
119
+ relay = new SQLiteRelayStore(relayStorePath(relayStoreDirectory(), repository, prNumber));
120
120
  relayStores.set(key, relay);
121
121
  }
122
122
  const github = new GitHubStore(client, repository, prNumber, trusted, seed);
@@ -127,7 +127,7 @@ export async function runDaemon(argv) {
127
127
  return projected;
128
128
  };
129
129
  // SQLite inventory, not GitHub discovery, defines which repositories exist.
130
- const existingInventory = await discoverLocalRelayStoreSnapshots(join(homedir(), '.agents-relay', 'stores'));
130
+ const existingInventory = await discoverLocalRelayStoreSnapshots(relayStoreDirectory());
131
131
  for (const snapshot of existingInventory)
132
132
  for (const job of snapshot.jobs) {
133
133
  if (!repositories.includes(job.repository))
@@ -136,7 +136,7 @@ export async function runDaemon(argv) {
136
136
  if (repositories.length === 0)
137
137
  repositories.push('local/' + basename(workspaceRoot));
138
138
  const inventoryFromLocal = async () => {
139
- const snapshots = await discoverLocalRelayStoreSnapshots(join(homedir(), '.agents-relay', 'stores'));
139
+ const snapshots = await discoverLocalRelayStoreSnapshots(relayStoreDirectory());
140
140
  const known = new Set(repositories);
141
141
  const entries = snapshots.flatMap(snapshot => snapshot.jobs.filter(job => known.has(job.repository)).map(job => ({
142
142
  job, githubState: snapshot.facts?.state ?? 'UNKNOWN',
@@ -174,7 +174,7 @@ export async function runDaemon(argv) {
174
174
  };
175
175
  };
176
176
  const refreshPullRequest = async (repository, prNumber, trigger, options) => (await refreshPullRequestSnapshot(repository, prNumber, trigger, options)).overviews;
177
- const localJobBinding = createJobIdRegistryResolver(join(homedir(), '.agents-relay', 'stores'), relayStores, { client, trustedAuthors: trusted, noGitHub });
177
+ const localJobBinding = createJobIdRegistryResolver(relayStoreDirectory(), relayStores, { client, trustedAuthors: trusted, noGitHub });
178
178
  const requireLocalJobBinding = async (jobId) => {
179
179
  const binding = await localJobBinding(jobId);
180
180
  if (!binding)
@@ -214,7 +214,7 @@ export async function runDaemon(argv) {
214
214
  return;
215
215
  if (client.rateLimitStatus().limited)
216
216
  return;
217
- const snapshots = await discoverLocalRelayStoreSnapshots(join(homedir(), '.agents-relay', 'stores'));
217
+ const snapshots = await discoverLocalRelayStoreSnapshots(relayStoreDirectory());
218
218
  const bindingCandidates = orderGitHubBindingCandidates(snapshots.flatMap(snapshot => snapshot.jobs.map(job => ({ snapshotPath: snapshot.path, job }))));
219
219
  for (const candidate of bindingCandidates) {
220
220
  const snapshot = { path: candidate.snapshotPath };
@@ -338,7 +338,7 @@ export async function runDaemon(argv) {
338
338
  const initial = cachedOverviews;
339
339
  const fallback = initial[0]?.job;
340
340
  const dashboardStore = fallback ? (await requireLocalJobBinding(fallback.id)).store : new InMemoryStore();
341
- const { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate, workspaceTaskStart, workspaceTaskComplete, workspaceTaskAction, workspaceTaskSetState } = createDashboardActions({ cachedOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, applyOrchestratorStartAction, applyOrchestratorCompleteAction, publishOrchestratorTaskStarted, publishOrchestratorTaskCompletion, bus, dashboardTaskAction, dashboardTaskSetState });
341
+ const { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate, workspaceTaskStart, workspaceTaskComplete, workspaceTaskAction, workspaceTaskSetState } = createDashboardActions({ getOverviews: () => cachedOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, applyOrchestratorStartAction, applyOrchestratorCompleteAction, publishOrchestratorTaskStarted, publishOrchestratorTaskCompletion, bus, dashboardTaskAction, dashboardTaskSetState });
342
342
  const workspaceControl = createWorkspaceControl({ client, trusted, noGitHub, repositories, refreshAll, requireLocalJobBinding, makeReconciler, concurrency, pool, bus, isEffectivelyBlocked, unblockAutonomousTasks, unblockTask, publishTaskUnblocked, dispatchTaskAction, registerBlockerDependency, discoverAgents, ensureManagedGitHubJob });
343
343
  const secretFile = value(argv, '--github-webhook-secret-file');
344
344
  const secret = noGitHub ? '' : value(argv, '--github-webhook-secret') || process.env.AGENTS_RELAY_GITHUB_WEBHOOK_SECRET || (secretFile ? (await readFile(secretFile, 'utf8')).trim() : '');
@@ -1,9 +1,7 @@
1
- import { homedir } from 'node:os';
2
- import { join } from 'node:path';
3
1
  import { eventFor } from './events.js';
4
- import { SQLiteRelayStore, relayStorePath } from './relay-store.js';
2
+ import { SQLiteRelayStore, relayStoreDirectory, relayStorePath } from './relay-store.js';
5
3
  /** Single-Job service uses the same durable publish facts as the repository daemon. */
6
- export async function handleServicePublication(job, wake, bus, directory = join(homedir(), '.agents-relay', 'stores')) {
4
+ export async function handleServicePublication(job, wake, bus, directory = relayStoreDirectory()) {
7
5
  if (!wake.publication && !wake.publishFailure)
8
6
  return;
9
7
  const sqlite = new SQLiteRelayStore(relayStorePath(directory, job.repository, job.prNumber));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.69",
3
+ "version": "2.0.71",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,9 +12,9 @@
12
12
  "test": "npm run test:full",
13
13
  "test:chatgpt-temp-quality": "AGENTS_RELAY_RUN_CHATGPT_TEMP_QUALITY=1 sh -c \"npm run build && node test/quality/run-temporary-chat.mjs\"",
14
14
  "dev": "tsx src/cli.ts",
15
- "test:publish": "npm run build && node --test --test-concurrency=1 --test-force-exit test/*.test.js",
16
- "test:fast": "npm run build && node --test --test-concurrency=2 test/workflow-lifecycle.test.js test/workflow-v2-lifecycle-parity.test.js test/workflow-effects.test.js test/relay-store.test.js test/local-authority.test.js test/local-jobid-routing.test.js test/local-sqlite-jobid.test.js test/no-github.test.js test/scheduled-jobs.test.js test/reconcile-coordinator.test.js test/service-publication.test.js test/github-webhook.test.js",
17
- "test:full": "npm run build && node --test --test-concurrency=1 test/*.test.js",
15
+ "test:publish": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 --test-force-exit test/*.test.js",
16
+ "test:fast": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=2 test/workflow-lifecycle.test.js test/workflow-v2-lifecycle-parity.test.js test/workflow-effects.test.js test/relay-store.test.js test/local-authority.test.js test/local-jobid-routing.test.js test/local-sqlite-jobid.test.js test/no-github.test.js test/scheduled-jobs.test.js test/reconcile-coordinator.test.js test/service-publication.test.js test/github-webhook.test.js",
17
+ "test:full": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 test/*.test.js",
18
18
  "e2e": "node e2e/runner/run.mjs"
19
19
  },
20
20
  "devDependencies": {