@dharma-ai-labs/agent-fabric 0.2.97 → 0.2.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import { connectDemoDevice, verifyDemoDevice } from './demoEnrollment.js';
37
37
  import { performDemoPeerAction, withDemoDeviceLock } from './demoPeer.js';
38
38
  import { demoRepositoryPackage } from './demoPackage.js';
39
39
  import { runDemoWatch } from './demoWatch.js';
40
- const VERSION = '0.2.97';
40
+ const VERSION = '0.2.99';
41
41
  const USAGE = CLI_USAGE;
42
42
  const execFileAsync = promisify(execFile);
43
43
  const LOCAL_PROVIDER_IDS = ['codex', 'claude', 'agy', 'hermes'];
@@ -752,6 +752,30 @@ export async function relayProcessState(home = dharmaHome()) {
752
752
  return code === 'EPERM' ? 'unknown' : 'stopped';
753
753
  }
754
754
  }
755
+ export async function acquireRelayProcessLease(home = dharmaHome()) {
756
+ const pidPath = resolve(home, 'relay', 'relay.pid');
757
+ const releaseLock = await acquirePidLock(`${pidPath}.lock`, 250, 'Another relay process is already running for this device.');
758
+ try {
759
+ const state = await relayProcessState(home);
760
+ if (state !== 'stopped')
761
+ throw new Error(`Cannot start a second relay while process state is ${state}.`);
762
+ await writeFile(pidPath, `${process.pid}\n`, { mode: 0o600 });
763
+ }
764
+ catch (error) {
765
+ await releaseLock();
766
+ throw error;
767
+ }
768
+ return async () => {
769
+ try {
770
+ if ((await readFile(pidPath, 'utf8').catch(() => '')).trim() === String(process.pid)) {
771
+ await rm(pidPath, { force: true });
772
+ }
773
+ }
774
+ finally {
775
+ await releaseLock();
776
+ }
777
+ };
778
+ }
755
779
  export async function materializeWorkspacePolicy(input) {
756
780
  const allowedCommands = {};
757
781
  try {
@@ -1396,9 +1420,17 @@ export async function waitForRelayReadiness(options = {}) {
1396
1420
  lastState = await processState();
1397
1421
  if (lastState === 'running') {
1398
1422
  try {
1399
- return { state: 'running', probe: await probe() };
1423
+ const acknowledged = await probe();
1424
+ if (!acknowledged.connected)
1425
+ throw new Error('Relay session is not connected.');
1426
+ if (options.expectedVersion && acknowledged.relayVersion !== options.expectedVersion) {
1427
+ throw new Error(`relay_version_mismatch:${acknowledged.relayVersion}:${options.expectedVersion}`);
1428
+ }
1429
+ return { state: 'running', probe: acknowledged };
1400
1430
  }
1401
- catch {
1431
+ catch (error) {
1432
+ if (String(error.message).startsWith('relay_version_mismatch:'))
1433
+ throw error;
1402
1434
  acknowledgementPending = true;
1403
1435
  }
1404
1436
  }
@@ -1419,7 +1451,7 @@ async function startRelayDaemon(policyPath) {
1419
1451
  });
1420
1452
  child.unref();
1421
1453
  }
1422
- const readiness = await waitForRelayReadiness();
1454
+ const readiness = await waitForRelayReadiness({ expectedVersion: VERSION });
1423
1455
  return { started: !alreadyRunning, ...readiness };
1424
1456
  }
1425
1457
  export function providerHintFromEnvironment(env = process.env) {
@@ -5185,350 +5217,390 @@ async function relayStart(flags) {
5185
5217
  const fabric = await client();
5186
5218
  const leaseSeconds = Number(flags.get('lease-seconds') || 120);
5187
5219
  const pollMs = Math.min(Math.max(Number(flags.get('poll-seconds') || 3), 1), 60) * 1_000;
5188
- const pidPath = resolve(dharmaHome(), 'relay', 'relay.pid');
5189
- await mkdir(resolve(dharmaHome(), 'relay'), { recursive: true, mode: 0o700 });
5190
- await writeFile(pidPath, `${process.pid}\n`, { mode: 0o600 });
5191
- let stopping = false;
5192
- let stopPreparation;
5193
- const stop = () => { stopping = true; stopPreparation?.(); };
5194
- process.once('SIGINT', stop);
5195
- process.once('SIGTERM', stop);
5196
- let tasksCompleted = 0;
5197
- let lastProviderFailureCategory = null;
5198
- let taskTrajectoriesRecovered = 0;
5199
- let evidenceResponsesCompleted = 0;
5200
- let trajectorySyncsCompleted = 0;
5201
- let nextPolicyRefreshAt = 0;
5202
- let evidencePolicyFresh = false;
5203
- const repositorySourceWatcher = new RepositorySourceWatcher();
5204
- const blockedRepositorySourceRetry = new BlockedRepositorySourceRetry();
5205
- let repositorySourceBaselineAvailable = true;
5206
- let publishedLocalSnapshotHash = canonicalWorkspace.repositoryPackage?.publishedLocalSnapshotHash
5207
- || (canonicalWorkspace.repositoryPackage?.state === 'published'
5208
- && canonicalWorkspace.repositoryPackage.snapshotHash === canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash
5209
- ? canonicalWorkspace.repositoryPackage.snapshotHash : null);
5210
- if (canonicalWorkspace.repositoryPackage && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
5211
- try {
5212
- const recovered = await recoverPublishedLocalSourceBaseline({
5213
- workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
5214
- workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5215
- repositoryAgentId: canonicalWorkspace.repositoryAgentId, record: canonicalWorkspace.repositoryPackage,
5216
- });
5217
- if (recovered && recovered !== publishedLocalSnapshotHash) {
5218
- publishedLocalSnapshotHash = recovered;
5219
- canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: {
5220
- ...canonicalWorkspace.repositoryPackage, localBaselineSnapshotHash: recovered,
5221
- publishedLocalSnapshotHash: recovered,
5222
- } };
5223
- await saveWorkspaceRecord(canonicalWorkspace);
5220
+ const releaseRelayLease = await acquireRelayProcessLease();
5221
+ try {
5222
+ let stopping = false;
5223
+ let stopPreparation;
5224
+ const stop = () => { stopping = true; stopPreparation?.(); };
5225
+ process.once('SIGINT', stop);
5226
+ process.once('SIGTERM', stop);
5227
+ let tasksCompleted = 0;
5228
+ let lastProviderFailureCategory = null;
5229
+ let taskTrajectoriesRecovered = 0;
5230
+ let evidenceResponsesCompleted = 0;
5231
+ let trajectorySyncsCompleted = 0;
5232
+ let nextPolicyRefreshAt = 0;
5233
+ let evidencePolicyFresh = false;
5234
+ const repositorySourceWatcher = new RepositorySourceWatcher();
5235
+ const blockedRepositorySourceRetry = new BlockedRepositorySourceRetry();
5236
+ let repositorySourceBaselineAvailable = true;
5237
+ let publishedLocalSnapshotHash = canonicalWorkspace.repositoryPackage?.publishedLocalSnapshotHash
5238
+ || (canonicalWorkspace.repositoryPackage?.state === 'published'
5239
+ && canonicalWorkspace.repositoryPackage.snapshotHash === canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash
5240
+ ? canonicalWorkspace.repositoryPackage.snapshotHash : null);
5241
+ if (canonicalWorkspace.repositoryPackage && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
5242
+ try {
5243
+ const recovered = await recoverPublishedLocalSourceBaseline({
5244
+ workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
5245
+ workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5246
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId, record: canonicalWorkspace.repositoryPackage,
5247
+ });
5248
+ if (recovered && recovered !== publishedLocalSnapshotHash) {
5249
+ publishedLocalSnapshotHash = recovered;
5250
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: {
5251
+ ...canonicalWorkspace.repositoryPackage, localBaselineSnapshotHash: recovered,
5252
+ publishedLocalSnapshotHash: recovered,
5253
+ } };
5254
+ await saveWorkspaceRecord(canonicalWorkspace);
5255
+ }
5224
5256
  }
5225
- }
5226
- catch {
5227
- repositorySourceBaselineAvailable = false;
5228
- }
5229
- }
5230
- if (publishedLocalSnapshotHash) {
5231
- try {
5232
- await readRepositoryPackageSnapshot(canonicalWorkspace.path, publishedLocalSnapshotHash);
5233
- const localHash = canonicalWorkspace.repositoryPackage?.localBaselineSnapshotHash;
5234
- if (localHash && (localHash === publishedLocalSnapshotHash
5235
- || canonicalWorkspace.repositoryPackage?.pendingLocalSnapshotHash === localHash)) {
5236
- await seedRepositorySourceWatcher({ workspace: canonicalWorkspace.path,
5237
- organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
5238
- repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5239
- repositoryAgentId: canonicalWorkspace.repositoryAgentId,
5240
- publishedHash: publishedLocalSnapshotHash,
5241
- localHash,
5242
- watcher: repositorySourceWatcher });
5257
+ catch {
5258
+ repositorySourceBaselineAvailable = false;
5243
5259
  }
5244
5260
  }
5245
- catch {
5246
- repositorySourceBaselineAvailable = false;
5261
+ if (publishedLocalSnapshotHash) {
5262
+ try {
5263
+ await readRepositoryPackageSnapshot(canonicalWorkspace.path, publishedLocalSnapshotHash);
5264
+ const localHash = canonicalWorkspace.repositoryPackage?.localBaselineSnapshotHash;
5265
+ if (localHash && (localHash === publishedLocalSnapshotHash
5266
+ || canonicalWorkspace.repositoryPackage?.pendingLocalSnapshotHash === localHash)) {
5267
+ await seedRepositorySourceWatcher({ workspace: canonicalWorkspace.path,
5268
+ organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
5269
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5270
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId,
5271
+ publishedHash: publishedLocalSnapshotHash,
5272
+ localHash,
5273
+ watcher: repositorySourceWatcher });
5274
+ }
5275
+ }
5276
+ catch {
5277
+ repositorySourceBaselineAvailable = false;
5278
+ }
5247
5279
  }
5248
- }
5249
- let nextRepositorySourceScanAt = 0;
5250
- let repositorySourceCandidates = 0;
5251
- let repositorySourceFailures = 0;
5252
- let repositorySourceState = 'not_scanned';
5253
- const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
5254
- const vault = await LocalVault.open({
5255
- root: resolve(dharmaHome(), 'vault'),
5256
- masterKey: await loadOrCreateVaultMasterKey(),
5257
- rawLocalDays: rawLocalRetentionDays(policy),
5258
- });
5259
- let skillPreparationsCompleted = 0;
5260
- let skillPreparationFailures = 0;
5261
- let skillActivationsCompleted = 0;
5262
- let skillActivationFailures = 0;
5263
- const skillActivationFailuresByProvider = {};
5264
- let nextSkillActivationAt = 0;
5265
- const skillPreparationPump = startSkillPreparationPump({
5266
- prepare: async (assertRunning) => {
5267
- await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
5268
- assertRunning();
5269
- const capability = await adapter.capability();
5270
- assertRunning();
5271
- if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
5272
- return;
5273
- const provider = adapter.providerId;
5274
- await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5275
- provider, assertCurrent: assertRunning }, async () => {
5276
- const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
5277
- provider, policyPath, fabric, automatic: true, assertRunning });
5278
- if (!prepared) {
5279
- assertRunning();
5280
+ let nextRepositorySourceScanAt = 0;
5281
+ let repositorySourceCandidates = 0;
5282
+ let repositorySourceFailures = 0;
5283
+ let repositorySourceState = 'not_scanned';
5284
+ let sourceScanFlight = null;
5285
+ let sourceScanReady = null;
5286
+ const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
5287
+ const vault = await LocalVault.open({
5288
+ root: resolve(dharmaHome(), 'vault'),
5289
+ masterKey: await loadOrCreateVaultMasterKey(),
5290
+ rawLocalDays: rawLocalRetentionDays(policy),
5291
+ });
5292
+ let skillPreparationsCompleted = 0;
5293
+ let skillPreparationFailures = 0;
5294
+ let skillActivationsCompleted = 0;
5295
+ let skillActivationFailures = 0;
5296
+ const skillActivationFailuresByProvider = {};
5297
+ let nextSkillActivationAt = 0;
5298
+ const skillPreparationPump = startSkillPreparationPump({
5299
+ prepare: async (assertRunning) => {
5300
+ await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
5301
+ assertRunning();
5302
+ const capability = await adapter.capability();
5303
+ assertRunning();
5304
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
5280
5305
  return;
5281
- }
5282
- let retained = false;
5283
- try {
5284
- prepared.assertCurrent();
5285
- const repositoryAgentId = prepared.workspace.repositoryAgentId;
5286
- if (!repositoryAgentId)
5287
- throw new Error('Preparation cache requires an enrolled repository agent.');
5288
- const pendingRecord = await serializeSkillPreparationRecord({
5289
- schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
5290
- deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
5291
- repositoryAgentId,
5292
- repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
5293
- policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
5294
- bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
5295
- ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
5296
- preparedAt: new Date().toISOString(), activationAuthorized: false,
5297
- });
5298
- prepared.assertCurrent();
5299
- await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
5300
- prepared.assertCurrent();
5301
- await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5302
- provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
5303
- expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
5306
+ const provider = adapter.providerId;
5307
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5308
+ provider, assertCurrent: assertRunning }, async () => {
5309
+ const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
5310
+ provider, policyPath, fabric, automatic: true, assertRunning });
5311
+ if (!prepared) {
5312
+ assertRunning();
5313
+ return;
5314
+ }
5315
+ let retained = false;
5316
+ try {
5317
+ prepared.assertCurrent();
5318
+ const repositoryAgentId = prepared.workspace.repositoryAgentId;
5319
+ if (!repositoryAgentId)
5320
+ throw new Error('Preparation cache requires an enrolled repository agent.');
5321
+ const pendingRecord = await serializeSkillPreparationRecord({
5322
+ schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
5323
+ deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
5304
5324
  repositoryAgentId,
5305
- repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
5325
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
5306
5326
  policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
5307
- bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
5308
- onCommitted: () => { retained = true; } });
5309
- skillPreparationsCompleted += 1;
5310
- }
5311
- finally {
5312
- if (!retained)
5313
- await rm(prepared.sourceRoot, { recursive: true, force: true });
5314
- }
5315
- });
5316
- }, () => { skillPreparationFailures += 1; });
5317
- },
5318
- onError: () => { skillPreparationFailures += 1; },
5319
- });
5320
- stopPreparation = skillPreparationPump.requestStop;
5321
- if (stopping)
5322
- stopPreparation();
5323
- try {
5324
- const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
5325
- if (recoveredTaskTrajectories.state === 'completed') {
5326
- taskTrajectoriesRecovered += recoveredTaskTrajectories.value.length;
5327
- }
5328
- do {
5329
- if (Date.now() >= nextPolicyRefreshAt) {
5330
- try {
5331
- await syncWorkspacePolicy(fabric, canonicalWorkspace, policy.revision, true);
5332
- policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
5333
- evidencePolicyFresh = true;
5334
- }
5335
- catch {
5336
- evidencePolicyFresh = false;
5337
- }
5338
- nextPolicyRefreshAt = Date.now() + 60_000;
5327
+ bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
5328
+ ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
5329
+ preparedAt: new Date().toISOString(), activationAuthorized: false,
5330
+ });
5331
+ prepared.assertCurrent();
5332
+ await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
5333
+ prepared.assertCurrent();
5334
+ await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5335
+ provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
5336
+ expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
5337
+ repositoryAgentId,
5338
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
5339
+ policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
5340
+ bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
5341
+ onCommitted: () => { retained = true; } });
5342
+ skillPreparationsCompleted += 1;
5343
+ }
5344
+ finally {
5345
+ if (!retained)
5346
+ await rm(prepared.sourceRoot, { recursive: true, force: true });
5347
+ }
5348
+ });
5349
+ }, () => { skillPreparationFailures += 1; });
5350
+ },
5351
+ onError: () => { skillPreparationFailures += 1; },
5352
+ });
5353
+ stopPreparation = skillPreparationPump.requestStop;
5354
+ if (stopping)
5355
+ stopPreparation();
5356
+ const applyReadySourceScan = async () => {
5357
+ if (!sourceScanReady)
5358
+ return;
5359
+ const ready = sourceScanReady;
5360
+ sourceScanReady = null;
5361
+ sourceScanFlight = null;
5362
+ if ('error' in ready) {
5363
+ repositorySourceWatcher.invalidate();
5364
+ repositorySourceFailures += 1;
5365
+ repositorySourceState = 'blocked';
5366
+ nextRepositorySourceScanAt = performance.now() + 60_000;
5367
+ return;
5339
5368
  }
5340
- if (canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
5341
- try {
5342
- const current = await pollRepositoryCandidate({ transport: fabric,
5343
- outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
5344
- scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
5345
- repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5346
- repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
5347
- if (current && canonicalWorkspace.repositoryPackage) {
5348
- if (current.candidateId !== canonicalWorkspace.repositoryPackage.candidateId
5349
- || current.operationId !== canonicalWorkspace.repositoryPackage.operationId) {
5350
- throw new Error('Repository candidate status changed identity while polling.');
5369
+ if ('state' in ready) {
5370
+ repositorySourceState = ready.state;
5371
+ nextRepositorySourceScanAt = performance.now() + 15_000;
5372
+ return;
5373
+ }
5374
+ const sourceCycle = ready.cycle;
5375
+ repositorySourceState = sourceCycle.state;
5376
+ if (sourceCycle.localMutation) {
5377
+ repositorySourceCandidates += 1;
5378
+ if (canonicalWorkspace.repositoryPackage && sourceCycle.persisted) {
5379
+ canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = sourceCycle.persisted.snapshotHash;
5380
+ if (sourceCycle.state === 'source_already_published') {
5381
+ canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5382
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
5383
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
5384
+ publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5385
+ }
5386
+ else if (sourceCycle.candidate) {
5387
+ const receipt = sourceCycle.candidate;
5388
+ if (typeof receipt.candidateId === 'string' && typeof receipt.operationId === 'string'
5389
+ && typeof receipt.snapshotHash === 'string' && sourceCycle.submittedManifestHash) {
5390
+ Object.assign(canonicalWorkspace.repositoryPackage, {
5391
+ state: receipt.state, candidateId: receipt.candidateId, operationId: receipt.operationId,
5392
+ snapshotHash: receipt.snapshotHash, sourceManifestHash: sourceCycle.submittedManifestHash,
5393
+ releaseId: typeof receipt.releaseId === 'string' ? receipt.releaseId : null,
5394
+ consolidationMode: 'repository_update',
5395
+ });
5396
+ }
5397
+ if (receipt.state === 'published') {
5398
+ canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5399
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
5400
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
5401
+ publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5402
+ }
5403
+ else if ((receipt.state === 'accepted' || receipt.state === 'processing')
5404
+ && typeof receipt.operationId === 'string') {
5405
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5406
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = receipt.operationId;
5351
5407
  }
5352
- const baseline = advanceRepositorySourceBaseline(canonicalWorkspace.repositoryPackage, current);
5353
- canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
5354
- state: current.state, candidateId: current.candidateId, operationId: current.operationId,
5355
- snapshotHash: current.snapshotHash, releaseId: current.releaseId,
5356
- ...baseline,
5357
- generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
5358
- publishedLocalSnapshotHash = baseline.publishedLocalSnapshotHash;
5359
- if (current.state === 'blocked')
5408
+ else if (receipt.state === 'blocked') {
5409
+ canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = publishedLocalSnapshotHash;
5360
5410
  repositorySourceWatcher.invalidate();
5361
- await saveWorkspaceRecord(canonicalWorkspace);
5411
+ }
5362
5412
  }
5363
- }
5364
- catch {
5365
- repositorySourceFailures += 1;
5413
+ await saveWorkspaceRecord(canonicalWorkspace);
5366
5414
  }
5367
5415
  }
5368
- let evidenceRequestId;
5369
- if (evidencePolicyFresh) {
5370
- const evidenceCycle = await deferUnavailableRelayRetention(async () => {
5371
- const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
5372
- const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
5373
- return {
5374
- synced,
5375
- evidenceRequestId: typeof evidence.requestId === 'string' ? evidence.requestId : undefined,
5376
- };
5377
- });
5378
- if (evidenceCycle.state === 'deferred') {
5379
- evidencePolicyFresh = false;
5380
- }
5381
- else {
5382
- trajectorySyncsCompleted += evidenceCycle.value.synced;
5383
- evidenceRequestId = evidenceCycle.value.evidenceRequestId;
5384
- if (evidenceRequestId)
5385
- evidenceResponsesCompleted += 1;
5386
- }
5416
+ nextRepositorySourceScanAt = performance.now() + (sourceCycle.state === 'debouncing' ? 15_000 : 60_000);
5417
+ };
5418
+ const consumeSourceScan = async () => {
5419
+ try {
5420
+ await applyReadySourceScan();
5421
+ }
5422
+ catch {
5423
+ repositorySourceWatcher.invalidate();
5424
+ repositorySourceFailures += 1;
5425
+ repositorySourceState = 'blocked';
5426
+ nextRepositorySourceScanAt = performance.now() + 60_000;
5427
+ }
5428
+ };
5429
+ try {
5430
+ const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
5431
+ if (recoveredTaskTrajectories.state === 'completed') {
5432
+ taskTrajectoriesRecovered += recoveredTaskTrajectories.value.length;
5387
5433
  }
5388
- const result = await executeOneTask(fabric, leaseSeconds);
5389
- if (result.taskId)
5390
- tasksCompleted += 1;
5391
- if (typeof result.failureCategory === 'string')
5392
- lastProviderFailureCategory = result.failureCategory;
5393
- if (!result.taskId && performance.now() >= nextSkillActivationAt) {
5394
- for (const adapter of providerAdapters) {
5434
+ do {
5435
+ await consumeSourceScan();
5436
+ if (Date.now() >= nextPolicyRefreshAt) {
5395
5437
  try {
5396
- const capability = await adapter.capability();
5397
- if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
5398
- continue;
5399
- const provider = adapter.providerId;
5400
- await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
5401
- await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5402
- provider, assertCurrent: () => { if (stopping)
5403
- throw new Error('Relay is stopping.'); } }, async () => {
5404
- const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
5405
- provider, policyPath, fabric });
5406
- const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
5407
- provider, policyPath, fabric, automatic: true });
5408
- if (!prepared)
5409
- return;
5410
- if (!cached)
5411
- skillPreparationsCompleted += 1;
5412
- await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
5413
- provider, policyPath, flags: new Map() });
5414
- skillActivationsCompleted += 1;
5415
- });
5416
- });
5438
+ await syncWorkspacePolicy(fabric, canonicalWorkspace, policy.revision, true);
5439
+ policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
5440
+ evidencePolicyFresh = true;
5417
5441
  }
5418
5442
  catch {
5419
- skillActivationFailures += 1;
5420
- skillActivationFailuresByProvider[adapter.providerId] = (skillActivationFailuresByProvider[adapter.providerId] || 0) + 1;
5443
+ evidencePolicyFresh = false;
5421
5444
  }
5445
+ nextPolicyRefreshAt = Date.now() + 60_000;
5422
5446
  }
5423
- nextSkillActivationAt = performance.now() + 60_000;
5424
- }
5425
- if (performance.now() >= nextRepositorySourceScanAt) {
5426
- try {
5427
- if (!repositorySourceBaselineAvailable) {
5428
- throw new Error('Repository source baseline is unavailable; refusing an unanchored update.');
5447
+ if (!sourceScanFlight && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
5448
+ try {
5449
+ const current = await pollRepositoryCandidate({ transport: fabric,
5450
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
5451
+ scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
5452
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5453
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
5454
+ if (current && canonicalWorkspace.repositoryPackage) {
5455
+ if (current.candidateId !== canonicalWorkspace.repositoryPackage.candidateId
5456
+ || current.operationId !== canonicalWorkspace.repositoryPackage.operationId) {
5457
+ throw new Error('Repository candidate status changed identity while polling.');
5458
+ }
5459
+ const baseline = advanceRepositorySourceBaseline(canonicalWorkspace.repositoryPackage, current);
5460
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
5461
+ state: current.state, candidateId: current.candidateId, operationId: current.operationId,
5462
+ snapshotHash: current.snapshotHash, releaseId: current.releaseId,
5463
+ ...baseline,
5464
+ generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
5465
+ publishedLocalSnapshotHash = baseline.publishedLocalSnapshotHash;
5466
+ if (current.state === 'blocked')
5467
+ repositorySourceWatcher.invalidate();
5468
+ await saveWorkspaceRecord(canonicalWorkspace);
5469
+ }
5429
5470
  }
5430
- if (!canonicalWorkspace.repositoryBindingId || !canonicalWorkspace.repositoryAgentId) {
5431
- throw new Error('Repository source synchronization requires a complete repository binding.');
5471
+ catch {
5472
+ repositorySourceFailures += 1;
5432
5473
  }
5433
- const installedKnowledge = await installedRepositoryKnowledge(canonicalWorkspace);
5434
- if (!installedKnowledge && (canonicalWorkspace.repositoryPackage?.generation ?? 0) > 0) {
5435
- repositorySourceState = 'awaiting_signed_baseline';
5436
- nextRepositorySourceScanAt = performance.now() + 15_000;
5474
+ }
5475
+ let evidenceRequestId;
5476
+ if (evidencePolicyFresh) {
5477
+ const evidenceCycle = await deferUnavailableRelayRetention(async () => {
5478
+ const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
5479
+ const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
5480
+ return {
5481
+ synced,
5482
+ evidenceRequestId: typeof evidence.requestId === 'string' ? evidence.requestId : undefined,
5483
+ };
5484
+ });
5485
+ if (evidenceCycle.state === 'deferred') {
5486
+ evidencePolicyFresh = false;
5437
5487
  }
5438
5488
  else {
5439
- if (canonicalWorkspace.repositoryPackage?.state === 'blocked' && canonicalWorkspace.repositoryPackage.snapshotHash) {
5440
- const blocked = await readRepositorySourceBaselineSnapshot(canonicalWorkspace.path, canonicalWorkspace.repositoryPackage.snapshotHash);
5441
- if (blockedRepositorySourceRetry.consider(canonicalWorkspace.repositoryPackage.candidateId ?? '', blocked.manifest.knowledge, installedKnowledge))
5442
- repositorySourceWatcher.invalidate();
5489
+ trajectorySyncsCompleted += evidenceCycle.value.synced;
5490
+ evidenceRequestId = evidenceCycle.value.evidenceRequestId;
5491
+ if (evidenceRequestId)
5492
+ evidenceResponsesCompleted += 1;
5493
+ }
5494
+ }
5495
+ const result = await executeOneTask(fabric, leaseSeconds);
5496
+ if (result.taskId)
5497
+ tasksCompleted += 1;
5498
+ if (typeof result.failureCategory === 'string')
5499
+ lastProviderFailureCategory = result.failureCategory;
5500
+ if (!result.taskId && performance.now() >= nextSkillActivationAt) {
5501
+ for (const adapter of providerAdapters) {
5502
+ try {
5503
+ const capability = await adapter.capability();
5504
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
5505
+ continue;
5506
+ const provider = adapter.providerId;
5507
+ await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
5508
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5509
+ provider, assertCurrent: () => { if (stopping)
5510
+ throw new Error('Relay is stopping.'); } }, async () => {
5511
+ const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
5512
+ provider, policyPath, fabric });
5513
+ const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
5514
+ provider, policyPath, fabric, automatic: true });
5515
+ if (!prepared)
5516
+ return;
5517
+ if (!cached)
5518
+ skillPreparationsCompleted += 1;
5519
+ await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
5520
+ provider, policyPath, flags: new Map() });
5521
+ skillActivationsCompleted += 1;
5522
+ });
5523
+ });
5443
5524
  }
5444
- const sourceCycle = await scanRepositorySourceChanges({
5445
- workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
5446
- workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5447
- repositoryAgentId: canonicalWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
5448
- loadRetainedKnowledge: () => installedRepositoryKnowledge(canonicalWorkspace),
5449
- loadPublishedLocalBaseline: () => publishedLocalSnapshotHash
5450
- ? readRepositoryPackageSnapshot(canonicalWorkspace.path, publishedLocalSnapshotHash) : Promise.resolve(null),
5451
- submitCandidate: (snapshot, expectedLatestSourceFingerprint) => synchronizeRepositoryCandidate({ transport: fabric,
5452
- outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
5453
- scope: { organizationId: canonicalWorkspace.organizationId,
5454
- workspaceId: canonicalWorkspace.workspaceId,
5455
- repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5456
- repositoryAgentId: canonicalWorkspace.repositoryAgentId },
5457
- snapshot, initialRepository: false, expectedLatestSourceFingerprint }),
5458
- });
5459
- repositorySourceState = sourceCycle.state;
5460
- if (sourceCycle.localMutation) {
5461
- repositorySourceCandidates += 1;
5462
- if (canonicalWorkspace.repositoryPackage && sourceCycle.persisted) {
5463
- canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = sourceCycle.persisted.snapshotHash;
5464
- if (sourceCycle.state === 'source_already_published') {
5465
- canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5466
- canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
5467
- canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
5468
- publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5469
- }
5470
- else if (sourceCycle.candidate) {
5471
- const receipt = sourceCycle.candidate;
5472
- if (typeof receipt.candidateId === 'string' && typeof receipt.operationId === 'string'
5473
- && typeof receipt.snapshotHash === 'string' && sourceCycle.submittedManifestHash) {
5474
- Object.assign(canonicalWorkspace.repositoryPackage, {
5475
- state: receipt.state, candidateId: receipt.candidateId, operationId: receipt.operationId,
5476
- snapshotHash: receipt.snapshotHash, sourceManifestHash: sourceCycle.submittedManifestHash,
5477
- releaseId: typeof receipt.releaseId === 'string' ? receipt.releaseId : null,
5478
- consolidationMode: 'repository_update',
5479
- });
5480
- }
5481
- if (receipt.state === 'published') {
5482
- canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5483
- canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
5484
- canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
5485
- publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5486
- }
5487
- else if ((receipt.state === 'accepted' || receipt.state === 'processing')
5488
- && typeof receipt.operationId === 'string') {
5489
- canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5490
- canonicalWorkspace.repositoryPackage.pendingLocalOperationId = receipt.operationId;
5491
- }
5492
- else if (receipt.state === 'blocked') {
5493
- canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = publishedLocalSnapshotHash;
5525
+ catch {
5526
+ skillActivationFailures += 1;
5527
+ skillActivationFailuresByProvider[adapter.providerId] = (skillActivationFailuresByProvider[adapter.providerId] || 0) + 1;
5528
+ }
5529
+ }
5530
+ nextSkillActivationAt = performance.now() + 60_000;
5531
+ }
5532
+ if (!sourceScanFlight && performance.now() >= nextRepositorySourceScanAt) {
5533
+ const sourceWorkspace = canonicalWorkspace;
5534
+ const sourceBaselineHash = publishedLocalSnapshotHash;
5535
+ sourceScanFlight = (async () => {
5536
+ try {
5537
+ if (!repositorySourceBaselineAvailable) {
5538
+ throw new Error('Repository source baseline is unavailable; refusing an unanchored update.');
5539
+ }
5540
+ if (!sourceWorkspace.repositoryBindingId || !sourceWorkspace.repositoryAgentId) {
5541
+ throw new Error('Repository source synchronization requires a complete repository binding.');
5542
+ }
5543
+ const installedKnowledge = await installedRepositoryKnowledge(sourceWorkspace);
5544
+ if (!installedKnowledge && (sourceWorkspace.repositoryPackage?.generation ?? 0) > 0) {
5545
+ sourceScanReady = { state: 'awaiting_signed_baseline' };
5546
+ }
5547
+ else {
5548
+ if (sourceWorkspace.repositoryPackage?.state === 'blocked' && sourceWorkspace.repositoryPackage.snapshotHash) {
5549
+ const blocked = await readRepositorySourceBaselineSnapshot(sourceWorkspace.path, sourceWorkspace.repositoryPackage.snapshotHash);
5550
+ if (blockedRepositorySourceRetry.consider(sourceWorkspace.repositoryPackage.candidateId ?? '', blocked.manifest.knowledge, installedKnowledge))
5494
5551
  repositorySourceWatcher.invalidate();
5495
- }
5496
5552
  }
5497
- await saveWorkspaceRecord(canonicalWorkspace);
5553
+ const sourceCycle = await scanRepositorySourceChanges({
5554
+ workspace: sourceWorkspace.path, organizationId: sourceWorkspace.organizationId,
5555
+ workspaceId: sourceWorkspace.workspaceId, repositoryBindingId: sourceWorkspace.repositoryBindingId,
5556
+ repositoryAgentId: sourceWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
5557
+ loadRetainedKnowledge: () => installedRepositoryKnowledge(sourceWorkspace),
5558
+ loadPublishedLocalBaseline: () => sourceBaselineHash
5559
+ ? readRepositoryPackageSnapshot(sourceWorkspace.path, sourceBaselineHash) : Promise.resolve(null),
5560
+ submitCandidate: (snapshot, expectedLatestSourceFingerprint) => synchronizeRepositoryCandidate({ transport: fabric,
5561
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
5562
+ scope: { organizationId: sourceWorkspace.organizationId,
5563
+ workspaceId: sourceWorkspace.workspaceId,
5564
+ repositoryBindingId: sourceWorkspace.repositoryBindingId,
5565
+ repositoryAgentId: sourceWorkspace.repositoryAgentId },
5566
+ snapshot, initialRepository: false, expectedLatestSourceFingerprint }),
5567
+ });
5568
+ sourceScanReady = { cycle: sourceCycle };
5498
5569
  }
5499
5570
  }
5500
- nextRepositorySourceScanAt = performance.now() + (sourceCycle.state === 'debouncing' ? 15_000 : 60_000);
5501
- }
5502
- }
5503
- catch {
5504
- repositorySourceWatcher.invalidate();
5505
- repositorySourceFailures += 1;
5506
- repositorySourceState = 'blocked';
5507
- nextRepositorySourceScanAt = performance.now() + 60_000;
5571
+ catch (error) {
5572
+ sourceScanReady = { error };
5573
+ }
5574
+ })();
5508
5575
  }
5509
- }
5510
- if (flags.has('once'))
5511
- break;
5512
- if (!result.taskId && !evidenceRequestId)
5513
- await new Promise((accept) => setTimeout(accept, pollMs));
5514
- } while (!stopping);
5576
+ if (flags.has('once'))
5577
+ break;
5578
+ if (!result.taskId && !evidenceRequestId)
5579
+ await new Promise((accept) => setTimeout(accept, pollMs));
5580
+ } while (!stopping);
5581
+ }
5582
+ finally {
5583
+ if (sourceScanFlight)
5584
+ await sourceScanFlight;
5585
+ await consumeSourceScan();
5586
+ await skillPreparationPump.stop();
5587
+ vault.close();
5588
+ process.removeListener('SIGINT', stop);
5589
+ process.removeListener('SIGTERM', stop);
5590
+ }
5591
+ return {
5592
+ ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
5593
+ lastProviderFailureCategory,
5594
+ evidenceResponsesCompleted, trajectorySyncsCompleted,
5595
+ repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
5596
+ skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
5597
+ skillActivationFailuresByProvider,
5598
+ sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
5599
+ };
5515
5600
  }
5516
5601
  finally {
5517
- await skillPreparationPump.stop();
5518
- vault.close();
5519
- process.removeListener('SIGINT', stop);
5520
- process.removeListener('SIGTERM', stop);
5521
- await rm(pidPath, { force: true });
5602
+ await releaseRelayLease();
5522
5603
  }
5523
- return {
5524
- ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
5525
- lastProviderFailureCategory,
5526
- evidenceResponsesCompleted, trajectorySyncsCompleted,
5527
- repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
5528
- skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
5529
- skillActivationFailuresByProvider,
5530
- sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
5531
- };
5532
5604
  }
5533
5605
  export async function run(argv) {
5534
5606
  const { positional, flags, repeated } = parseCliOptions(argv);