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

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.98';
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,352 @@ 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
+ const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
5285
+ const vault = await LocalVault.open({
5286
+ root: resolve(dharmaHome(), 'vault'),
5287
+ masterKey: await loadOrCreateVaultMasterKey(),
5288
+ rawLocalDays: rawLocalRetentionDays(policy),
5289
+ });
5290
+ let skillPreparationsCompleted = 0;
5291
+ let skillPreparationFailures = 0;
5292
+ let skillActivationsCompleted = 0;
5293
+ let skillActivationFailures = 0;
5294
+ const skillActivationFailuresByProvider = {};
5295
+ let nextSkillActivationAt = 0;
5296
+ const skillPreparationPump = startSkillPreparationPump({
5297
+ prepare: async (assertRunning) => {
5298
+ await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
5299
+ assertRunning();
5300
+ const capability = await adapter.capability();
5301
+ assertRunning();
5302
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
5280
5303
  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,
5304
+ const provider = adapter.providerId;
5305
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5306
+ provider, assertCurrent: assertRunning }, async () => {
5307
+ const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
5308
+ provider, policyPath, fabric, automatic: true, assertRunning });
5309
+ if (!prepared) {
5310
+ assertRunning();
5311
+ return;
5312
+ }
5313
+ let retained = false;
5314
+ try {
5315
+ prepared.assertCurrent();
5316
+ const repositoryAgentId = prepared.workspace.repositoryAgentId;
5317
+ if (!repositoryAgentId)
5318
+ throw new Error('Preparation cache requires an enrolled repository agent.');
5319
+ const pendingRecord = await serializeSkillPreparationRecord({
5320
+ schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
5321
+ deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
5304
5322
  repositoryAgentId,
5305
- repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
5323
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
5306
5324
  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;
5339
- }
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.');
5325
+ bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
5326
+ ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
5327
+ preparedAt: new Date().toISOString(), activationAuthorized: false,
5328
+ });
5329
+ prepared.assertCurrent();
5330
+ await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
5331
+ prepared.assertCurrent();
5332
+ await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5333
+ provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
5334
+ expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
5335
+ repositoryAgentId,
5336
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
5337
+ policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
5338
+ bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
5339
+ onCommitted: () => { retained = true; } });
5340
+ skillPreparationsCompleted += 1;
5351
5341
  }
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')
5360
- repositorySourceWatcher.invalidate();
5361
- await saveWorkspaceRecord(canonicalWorkspace);
5362
- }
5363
- }
5364
- catch {
5365
- repositorySourceFailures += 1;
5366
- }
5367
- }
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
- }
5342
+ finally {
5343
+ if (!retained)
5344
+ await rm(prepared.sourceRoot, { recursive: true, force: true });
5345
+ }
5346
+ });
5347
+ }, () => { skillPreparationFailures += 1; });
5348
+ },
5349
+ onError: () => { skillPreparationFailures += 1; },
5350
+ });
5351
+ stopPreparation = skillPreparationPump.requestStop;
5352
+ if (stopping)
5353
+ stopPreparation();
5354
+ try {
5355
+ const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
5356
+ if (recoveredTaskTrajectories.state === 'completed') {
5357
+ taskTrajectoriesRecovered += recoveredTaskTrajectories.value.length;
5387
5358
  }
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) {
5359
+ do {
5360
+ if (Date.now() >= nextPolicyRefreshAt) {
5395
5361
  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
- });
5362
+ await syncWorkspacePolicy(fabric, canonicalWorkspace, policy.revision, true);
5363
+ policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
5364
+ evidencePolicyFresh = true;
5417
5365
  }
5418
5366
  catch {
5419
- skillActivationFailures += 1;
5420
- skillActivationFailuresByProvider[adapter.providerId] = (skillActivationFailuresByProvider[adapter.providerId] || 0) + 1;
5367
+ evidencePolicyFresh = false;
5421
5368
  }
5369
+ nextPolicyRefreshAt = Date.now() + 60_000;
5422
5370
  }
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.');
5371
+ if (canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
5372
+ try {
5373
+ const current = await pollRepositoryCandidate({ transport: fabric,
5374
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
5375
+ scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
5376
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5377
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
5378
+ if (current && canonicalWorkspace.repositoryPackage) {
5379
+ if (current.candidateId !== canonicalWorkspace.repositoryPackage.candidateId
5380
+ || current.operationId !== canonicalWorkspace.repositoryPackage.operationId) {
5381
+ throw new Error('Repository candidate status changed identity while polling.');
5382
+ }
5383
+ const baseline = advanceRepositorySourceBaseline(canonicalWorkspace.repositoryPackage, current);
5384
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
5385
+ state: current.state, candidateId: current.candidateId, operationId: current.operationId,
5386
+ snapshotHash: current.snapshotHash, releaseId: current.releaseId,
5387
+ ...baseline,
5388
+ generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
5389
+ publishedLocalSnapshotHash = baseline.publishedLocalSnapshotHash;
5390
+ if (current.state === 'blocked')
5391
+ repositorySourceWatcher.invalidate();
5392
+ await saveWorkspaceRecord(canonicalWorkspace);
5393
+ }
5429
5394
  }
5430
- if (!canonicalWorkspace.repositoryBindingId || !canonicalWorkspace.repositoryAgentId) {
5431
- throw new Error('Repository source synchronization requires a complete repository binding.');
5395
+ catch {
5396
+ repositorySourceFailures += 1;
5432
5397
  }
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;
5398
+ }
5399
+ let evidenceRequestId;
5400
+ if (evidencePolicyFresh) {
5401
+ const evidenceCycle = await deferUnavailableRelayRetention(async () => {
5402
+ const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
5403
+ const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
5404
+ return {
5405
+ synced,
5406
+ evidenceRequestId: typeof evidence.requestId === 'string' ? evidence.requestId : undefined,
5407
+ };
5408
+ });
5409
+ if (evidenceCycle.state === 'deferred') {
5410
+ evidencePolicyFresh = false;
5437
5411
  }
5438
5412
  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();
5413
+ trajectorySyncsCompleted += evidenceCycle.value.synced;
5414
+ evidenceRequestId = evidenceCycle.value.evidenceRequestId;
5415
+ if (evidenceRequestId)
5416
+ evidenceResponsesCompleted += 1;
5417
+ }
5418
+ }
5419
+ const result = await executeOneTask(fabric, leaseSeconds);
5420
+ if (result.taskId)
5421
+ tasksCompleted += 1;
5422
+ if (typeof result.failureCategory === 'string')
5423
+ lastProviderFailureCategory = result.failureCategory;
5424
+ if (!result.taskId && performance.now() >= nextSkillActivationAt) {
5425
+ for (const adapter of providerAdapters) {
5426
+ try {
5427
+ const capability = await adapter.capability();
5428
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
5429
+ continue;
5430
+ const provider = adapter.providerId;
5431
+ await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
5432
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
5433
+ provider, assertCurrent: () => { if (stopping)
5434
+ throw new Error('Relay is stopping.'); } }, async () => {
5435
+ const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
5436
+ provider, policyPath, fabric });
5437
+ const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
5438
+ provider, policyPath, fabric, automatic: true });
5439
+ if (!prepared)
5440
+ return;
5441
+ if (!cached)
5442
+ skillPreparationsCompleted += 1;
5443
+ await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
5444
+ provider, policyPath, flags: new Map() });
5445
+ skillActivationsCompleted += 1;
5446
+ });
5447
+ });
5443
5448
  }
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') {
5449
+ catch {
5450
+ skillActivationFailures += 1;
5451
+ skillActivationFailuresByProvider[adapter.providerId] = (skillActivationFailuresByProvider[adapter.providerId] || 0) + 1;
5452
+ }
5453
+ }
5454
+ nextSkillActivationAt = performance.now() + 60_000;
5455
+ }
5456
+ if (performance.now() >= nextRepositorySourceScanAt) {
5457
+ try {
5458
+ if (!repositorySourceBaselineAvailable) {
5459
+ throw new Error('Repository source baseline is unavailable; refusing an unanchored update.');
5460
+ }
5461
+ if (!canonicalWorkspace.repositoryBindingId || !canonicalWorkspace.repositoryAgentId) {
5462
+ throw new Error('Repository source synchronization requires a complete repository binding.');
5463
+ }
5464
+ const installedKnowledge = await installedRepositoryKnowledge(canonicalWorkspace);
5465
+ if (!installedKnowledge && (canonicalWorkspace.repositoryPackage?.generation ?? 0) > 0) {
5466
+ repositorySourceState = 'awaiting_signed_baseline';
5467
+ nextRepositorySourceScanAt = performance.now() + 15_000;
5468
+ }
5469
+ else {
5470
+ if (canonicalWorkspace.repositoryPackage?.state === 'blocked' && canonicalWorkspace.repositoryPackage.snapshotHash) {
5471
+ const blocked = await readRepositorySourceBaselineSnapshot(canonicalWorkspace.path, canonicalWorkspace.repositoryPackage.snapshotHash);
5472
+ if (blockedRepositorySourceRetry.consider(canonicalWorkspace.repositoryPackage.candidateId ?? '', blocked.manifest.knowledge, installedKnowledge))
5473
+ repositorySourceWatcher.invalidate();
5474
+ }
5475
+ const sourceCycle = await scanRepositorySourceChanges({
5476
+ workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
5477
+ workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5478
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
5479
+ loadRetainedKnowledge: () => installedRepositoryKnowledge(canonicalWorkspace),
5480
+ loadPublishedLocalBaseline: () => publishedLocalSnapshotHash
5481
+ ? readRepositoryPackageSnapshot(canonicalWorkspace.path, publishedLocalSnapshotHash) : Promise.resolve(null),
5482
+ submitCandidate: (snapshot, expectedLatestSourceFingerprint) => synchronizeRepositoryCandidate({ transport: fabric,
5483
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
5484
+ scope: { organizationId: canonicalWorkspace.organizationId,
5485
+ workspaceId: canonicalWorkspace.workspaceId,
5486
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
5487
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId },
5488
+ snapshot, initialRepository: false, expectedLatestSourceFingerprint }),
5489
+ });
5490
+ repositorySourceState = sourceCycle.state;
5491
+ if (sourceCycle.localMutation) {
5492
+ repositorySourceCandidates += 1;
5493
+ if (canonicalWorkspace.repositoryPackage && sourceCycle.persisted) {
5494
+ canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = sourceCycle.persisted.snapshotHash;
5495
+ if (sourceCycle.state === 'source_already_published') {
5482
5496
  canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5483
5497
  canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
5484
5498
  canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
5485
5499
  publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5486
5500
  }
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;
5494
- repositorySourceWatcher.invalidate();
5501
+ else if (sourceCycle.candidate) {
5502
+ const receipt = sourceCycle.candidate;
5503
+ if (typeof receipt.candidateId === 'string' && typeof receipt.operationId === 'string'
5504
+ && typeof receipt.snapshotHash === 'string' && sourceCycle.submittedManifestHash) {
5505
+ Object.assign(canonicalWorkspace.repositoryPackage, {
5506
+ state: receipt.state, candidateId: receipt.candidateId, operationId: receipt.operationId,
5507
+ snapshotHash: receipt.snapshotHash, sourceManifestHash: sourceCycle.submittedManifestHash,
5508
+ releaseId: typeof receipt.releaseId === 'string' ? receipt.releaseId : null,
5509
+ consolidationMode: 'repository_update',
5510
+ });
5511
+ }
5512
+ if (receipt.state === 'published') {
5513
+ canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5514
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
5515
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
5516
+ publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5517
+ }
5518
+ else if ((receipt.state === 'accepted' || receipt.state === 'processing')
5519
+ && typeof receipt.operationId === 'string') {
5520
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
5521
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = receipt.operationId;
5522
+ }
5523
+ else if (receipt.state === 'blocked') {
5524
+ canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = publishedLocalSnapshotHash;
5525
+ repositorySourceWatcher.invalidate();
5526
+ }
5495
5527
  }
5528
+ await saveWorkspaceRecord(canonicalWorkspace);
5496
5529
  }
5497
- await saveWorkspaceRecord(canonicalWorkspace);
5498
5530
  }
5531
+ nextRepositorySourceScanAt = performance.now() + (sourceCycle.state === 'debouncing' ? 15_000 : 60_000);
5499
5532
  }
5500
- nextRepositorySourceScanAt = performance.now() + (sourceCycle.state === 'debouncing' ? 15_000 : 60_000);
5533
+ }
5534
+ catch {
5535
+ repositorySourceWatcher.invalidate();
5536
+ repositorySourceFailures += 1;
5537
+ repositorySourceState = 'blocked';
5538
+ nextRepositorySourceScanAt = performance.now() + 60_000;
5501
5539
  }
5502
5540
  }
5503
- catch {
5504
- repositorySourceWatcher.invalidate();
5505
- repositorySourceFailures += 1;
5506
- repositorySourceState = 'blocked';
5507
- nextRepositorySourceScanAt = performance.now() + 60_000;
5508
- }
5509
- }
5510
- if (flags.has('once'))
5511
- break;
5512
- if (!result.taskId && !evidenceRequestId)
5513
- await new Promise((accept) => setTimeout(accept, pollMs));
5514
- } while (!stopping);
5541
+ if (flags.has('once'))
5542
+ break;
5543
+ if (!result.taskId && !evidenceRequestId)
5544
+ await new Promise((accept) => setTimeout(accept, pollMs));
5545
+ } while (!stopping);
5546
+ }
5547
+ finally {
5548
+ await skillPreparationPump.stop();
5549
+ vault.close();
5550
+ process.removeListener('SIGINT', stop);
5551
+ process.removeListener('SIGTERM', stop);
5552
+ }
5553
+ return {
5554
+ ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
5555
+ lastProviderFailureCategory,
5556
+ evidenceResponsesCompleted, trajectorySyncsCompleted,
5557
+ repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
5558
+ skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
5559
+ skillActivationFailuresByProvider,
5560
+ sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
5561
+ };
5515
5562
  }
5516
5563
  finally {
5517
- await skillPreparationPump.stop();
5518
- vault.close();
5519
- process.removeListener('SIGINT', stop);
5520
- process.removeListener('SIGTERM', stop);
5521
- await rm(pidPath, { force: true });
5564
+ await releaseRelayLease();
5522
5565
  }
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
5566
  }
5533
5567
  export async function run(argv) {
5534
5568
  const { positional, flags, repeated } = parseCliOptions(argv);