@dharma-ai-labs/agent-fabric 0.2.119 → 0.2.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/AGENT_FABRIC_ONBOARDING.md +2 -0
  2. package/README.md +23 -0
  3. package/dist/demoEnrollment.d.ts.map +1 -1
  4. package/dist/demoEnrollment.js +81 -9
  5. package/dist/demoEnrollment.js.map +1 -1
  6. package/dist/demoEnrollment.test.js +185 -0
  7. package/dist/demoEnrollment.test.js.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +541 -372
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.test.js +2 -2
  12. package/dist/index.test.js.map +1 -1
  13. package/dist/legacyInstallerRecovery.d.ts +30 -0
  14. package/dist/legacyInstallerRecovery.d.ts.map +1 -0
  15. package/dist/legacyInstallerRecovery.js +107 -0
  16. package/dist/legacyInstallerRecovery.js.map +1 -0
  17. package/dist/legacyInstallerRecovery.test.d.ts +2 -0
  18. package/dist/legacyInstallerRecovery.test.d.ts.map +1 -0
  19. package/dist/legacyInstallerRecovery.test.js +126 -0
  20. package/dist/legacyInstallerRecovery.test.js.map +1 -0
  21. package/dist/relayAutostart.d.ts +1 -0
  22. package/dist/relayAutostart.d.ts.map +1 -1
  23. package/dist/relayAutostart.js +12 -3
  24. package/dist/relayAutostart.js.map +1 -1
  25. package/dist/relayAutostart.test.js +37 -0
  26. package/dist/relayAutostart.test.js.map +1 -1
  27. package/dist/relayRuntimeUpgrade.test.js +13 -2
  28. package/dist/relayRuntimeUpgrade.test.js.map +1 -1
  29. package/dist/repositoryInstallerFiles.d.ts +2 -1
  30. package/dist/repositoryInstallerFiles.d.ts.map +1 -1
  31. package/dist/repositoryInstallerFiles.js +6 -3
  32. package/dist/repositoryInstallerFiles.js.map +1 -1
  33. package/dist/repositoryOnboardingCaller.test.js +24 -0
  34. package/dist/repositoryOnboardingCaller.test.js.map +1 -1
  35. package/dist/repositoryOnboardingFlow.test.js +4 -3
  36. package/dist/repositoryOnboardingFlow.test.js.map +1 -1
  37. package/dist/repositoryRelaySupervisor.d.ts +39 -0
  38. package/dist/repositoryRelaySupervisor.d.ts.map +1 -0
  39. package/dist/repositoryRelaySupervisor.js +145 -0
  40. package/dist/repositoryRelaySupervisor.js.map +1 -0
  41. package/dist/repositoryRelaySupervisor.test.d.ts +2 -0
  42. package/dist/repositoryRelaySupervisor.test.d.ts.map +1 -0
  43. package/dist/repositoryRelaySupervisor.test.js +189 -0
  44. package/dist/repositoryRelaySupervisor.test.js.map +1 -0
  45. package/dist/skillPreparationCache.test.js +8 -4
  46. package/dist/skillPreparationCache.test.js.map +1 -1
  47. package/dist/skillPreparationCaller.test.js +4 -2
  48. package/dist/skillPreparationCaller.test.js.map +1 -1
  49. package/dist/usage.d.ts +1 -1
  50. package/dist/usage.d.ts.map +1 -1
  51. package/dist/usage.js +1 -1
  52. package/dist/usage.js.map +1 -1
  53. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import { contentHash, getActiveSkillBundleAuthorization, getExpiredSkillBundleAu
17
17
  import { executeTask, FileActionExecutionJournal, FileTaskReceiptStore, providerInstructionsForTask, } from '@dharma-ai-labs/agent-fabric-task-runner';
18
18
  import { CLI_USAGE } from './usage.js';
19
19
  import { superviseRelay } from './relaySupervisor.js';
20
+ import { repositoryRelayObservationReady, runRegisteredRepositoryRelays, selectRepositoryRelayRegistrations, serializeRelayWork, waitForRelayRefresh } from './repositoryRelaySupervisor.js';
20
21
  import { disableRelayAutostart, enableRelayAutostart, inspectOwnedRelayAutostart, relayAutostartStatus, startRelayAutostart, stopRelayAutostart } from './relayAutostart.js';
21
22
  import { relayRuntimeObservationReady, upgradeRelayRuntime } from './relayRuntimeUpgrade.js';
22
23
  import { assertStudyExactPoll, studyTaskIdFromFlags } from './studyExactTaskSelector.js';
@@ -25,6 +26,7 @@ import { inventoryRepositoryPackage, readRepositoryPackageSnapshot, readReposito
25
26
  import { validateRepositorySourceAuthorization } from './repositorySourceAuthorization.js';
26
27
  import { advanceRepositorySourceBaseline, BlockedRepositorySourceRetry, fetchRepositorySourceAuthorization, recoverPublishedLocalSourceBaseline, RepositorySourceWatcher, scanRepositorySourceChanges, seedRepositorySourceWatcher } from './repositorySourceSync.js';
27
28
  import { assertRepositoryInstallerOwnership, writeRepositoryInstallerFile } from './repositoryInstallerFiles.js';
29
+ import { recoverLegacyRepositoryInstaller, selectLegacyInstallerRecoveryWorkspace } from './legacyInstallerRecovery.js';
28
30
  import { onboardingResumeCommand, selectDeviceWorkspace, workspaceIdForDevice } from './onboardingWorkspace.js';
29
31
  import { receiveRepositoryPackageDelivery } from './repositoryPackageDelivery.js';
30
32
  import { selectInstalledRepositoryKnowledge } from './repositoryInstalledKnowledge.js';
@@ -56,7 +58,7 @@ import { openCodexAppServerTransport } from '@dharma-ai-labs/agent-fabric-provid
56
58
  import { createNamedSessionTrust, isNamedSessionOwnerReceipt, renewNamedSessionLifetime } from './namedSessionTrust.js';
57
59
  import { startNamedCodexThread } from './namedCodexThread.js';
58
60
  export { openCooperativeInboxSession } from './cooperativeInboxSession.js';
59
- const VERSION = '0.2.119';
61
+ const VERSION = '0.2.120';
60
62
  const USAGE = CLI_USAGE;
61
63
  const execFileAsync = promisify(execFile);
62
64
  const LOCAL_PROVIDER_IDS = ['codex', 'claude', 'agy', 'hermes'];
@@ -1303,12 +1305,24 @@ async function registry() {
1303
1305
  }
1304
1306
  async function saveRegistry(items) {
1305
1307
  await mkdir(resolve(dharmaHome(), 'registry'), { recursive: true, mode: 0o700 });
1306
- await writeFile(workspaceRegistryPath(), `${JSON.stringify(items, null, 2)}\n`, { mode: 0o600 });
1308
+ await writeJsonAtomic(workspaceRegistryPath(), items);
1307
1309
  }
1308
1310
  async function saveWorkspaceRecord(entry) {
1309
- const items = (await registry()).filter((item) => item.workspaceId !== entry.workspaceId);
1310
- items.push(entry);
1311
- await saveRegistry(items);
1311
+ const release = await acquirePidLock(`${workspaceRegistryPath()}.mutation.lock`, 10_000, 'Workspace registry is busy; preserve it for recovery.');
1312
+ try {
1313
+ const value = await readFile(workspaceRegistryPath(), 'utf8').catch(error => {
1314
+ if (error.code === 'ENOENT')
1315
+ return '[]';
1316
+ throw error;
1317
+ });
1318
+ const items = JSON.parse(value);
1319
+ if (!Array.isArray(items))
1320
+ throw new Error('Workspace registry is invalid; preserve it for recovery.');
1321
+ await saveRegistry([...items.filter(item => item.workspaceId !== entry.workspaceId), entry]);
1322
+ }
1323
+ finally {
1324
+ await release();
1325
+ }
1312
1326
  }
1313
1327
  async function gitValue(workspace, argv) {
1314
1328
  try {
@@ -1656,6 +1670,15 @@ export async function waitForRelayReadiness(options = {}) {
1656
1670
  throw new Error(`The outbound relay did not become ready after bootstrap (${reason}).`);
1657
1671
  }
1658
1672
  async function startRelayDaemon(policyPath) {
1673
+ const config = await readDeviceConfig();
1674
+ if (!config)
1675
+ throw new Error('Relay startup requires current device enrollment.');
1676
+ const policy = await loadOrganizationPolicy(policyPath);
1677
+ const workspaceId = policy.serverAuthorization?.workspaceId;
1678
+ const selected = (await registry()).find(row => row.workspaceId === workspaceId
1679
+ && row.organizationId === config.organizationId && resolve(row.path, '.dharma', 'approved-policy.json') === policyPath);
1680
+ if (!selected)
1681
+ throw new Error('Relay policy must belong to this enrolled repository.');
1659
1682
  const alreadyRunning = await relayProcessState() === 'running';
1660
1683
  const supervisorState = await relaySupervisorProcessState();
1661
1684
  if (supervisorState === 'unknown')
@@ -1677,8 +1700,11 @@ async function startRelayDaemon(policyPath) {
1677
1700
  const binding = await readFile(resolve(dharmaHome(), 'relay', 'supervisor-workspace.json'), 'utf8')
1678
1701
  .then(value => JSON.parse(value)).catch(() => null);
1679
1702
  if (binding?.pid === pid) {
1680
- if (binding.policyPath !== policyPath) {
1681
- throw new Error('relay_workspace_conflict: the active relay supervisor belongs to another workspace.');
1703
+ if (binding.version !== VERSION || !binding.standardRepositories) {
1704
+ throw new Error('relay_runtime_upgrade_required: safely stop and upgrade the existing startup-anchor relay; do not retarget a working receiver.');
1705
+ }
1706
+ if (binding.organizationId !== config.organizationId || binding.deviceId !== config.deviceId) {
1707
+ throw new Error('relay_workspace_conflict: the active supervisor belongs to another enrollment.');
1682
1708
  }
1683
1709
  supervisorReady = true;
1684
1710
  break;
@@ -1689,7 +1715,14 @@ async function startRelayDaemon(policyPath) {
1689
1715
  }
1690
1716
  if (!supervisorReady)
1691
1717
  throw new Error('The relay supervisor did not become ready after bootstrap.');
1692
- const readiness = await waitForRelayReadiness({ expectedVersion: VERSION });
1718
+ const readiness = await waitForRelayReadiness({ expectedVersion: VERSION, attempts: 120, delayMs: 1000, processState: async () => {
1719
+ if (await relayProcessState() !== 'running')
1720
+ return 'stopped';
1721
+ const pid = Number((await readFile(resolve(dharmaHome(), 'relay', 'relay.pid'), 'utf8')).trim());
1722
+ const poll = await readFile(resolve(dharmaHome(), 'relay', 'repositories', selected.workspaceId, 'last-successful-poll.json'), 'utf8').then(value => JSON.parse(value)).catch(() => null);
1723
+ return repositoryRelayObservationReady({ observation: poll, workspaceId: selected.workspaceId,
1724
+ version: VERSION, pid }) ? 'running' : 'stopped';
1725
+ } });
1693
1726
  return { started: !alreadyRunning, supervisor: 'running', ...readiness };
1694
1727
  }
1695
1728
  async function supervisedDemoCycle(registration, signal) {
@@ -1752,7 +1785,8 @@ async function relaySupervise(flags) {
1752
1785
  const health = createDemoWatchHealthRecorder({ home: dharmaHome(), pid: process.pid, version: VERSION });
1753
1786
  try {
1754
1787
  await writeJsonAtomic(bindingPath, { pid: process.pid, workspaceId: selectedWorkspace?.workspaceId ?? null,
1755
- policyPath, demoWatches: true, version: VERSION });
1788
+ policyPath, demoWatches: true, version: VERSION, standardRepositories: Boolean(policyPath),
1789
+ organizationId: config?.organizationId ?? null, deviceId: config?.deviceId ?? null });
1756
1790
  const demo = runDemoSupervisor({ signal: controller.signal,
1757
1791
  list: () => listDemoWatchRegistrations(dharmaHome()), cycle: (registration, signal) => {
1758
1792
  if (!health.available(demoWatchRegistrationKey(registration))) {
@@ -2123,11 +2157,36 @@ async function bootstrap(flags) {
2123
2157
  const launcher = await installStableRepositoryLauncher(workspace);
2124
2158
  const autostart = flags.has('no-relay-daemon')
2125
2159
  ? { state: 'disabled', backend: null }
2126
- : await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => withRelayStartupMutation(() => enableRelayAutostart({
2127
- home: dharmaHome(), workspace, policy: resolve(workspace, '.dharma', 'approved-policy.json'),
2128
- launcher: resolve(workspace, process.platform === 'win32' ? launcher.windows : launcher.shell),
2129
- version: VERSION,
2130
- })));
2160
+ : await withOnboardingStage('autostart', String(onboarded.workspaceId || ''), `dharma bootstrap --resume --complete --portal-url ${hqUrl} --organization-id ${organizationId} --workspace . --policy-revision ${policyRevision}`, () => withRelayStartupMutation(async () => {
2161
+ const home = dharmaHome();
2162
+ const existing = await relayAutostartStatus({ home });
2163
+ if (existing.backend !== null || existing.state !== 'disabled') {
2164
+ const anchor = await inspectOwnedRelayAutostart({ home });
2165
+ if (anchor.policy !== null) {
2166
+ const current = await readDeviceConfig();
2167
+ if (!current || current.organizationId !== organizationId || current.deviceId !== config.deviceId) {
2168
+ throw new Error('relay_workspace_conflict: startup enrollment changed.');
2169
+ }
2170
+ const selected = selectDeviceWorkspace(await registry(), { organizationId,
2171
+ deviceId: config.deviceId, path: anchor.workspace });
2172
+ if (!selected)
2173
+ throw new Error('relay_workspace_conflict: startup anchor is not registered to this device.');
2174
+ if (anchor.policy !== resolve(anchor.workspace, '.dharma', 'approved-policy.json')) {
2175
+ throw new Error('relay_workspace_conflict: startup policy is not canonical.');
2176
+ }
2177
+ const authorized = await loadOrganizationPolicy(anchor.policy);
2178
+ const canonical = (await registry()).filter(row => row.workspaceId === authorized.serverAuthorization?.workspaceId
2179
+ && row.organizationId === organizationId && row.path === anchor.workspace
2180
+ && row.routeHash === selected.routeHash && row.repositoryRemoteHash === selected.repositoryRemoteHash);
2181
+ if (canonical.length !== 1)
2182
+ throw new Error('relay_workspace_conflict: startup policy route changed.');
2183
+ await loadVerifiedWorkspacePolicy(anchor.policy, authorized.serverAuthorization?.workspaceId ?? '');
2184
+ }
2185
+ }
2186
+ return enableRelayAutostart({ home, workspace, policy: resolve(workspace, '.dharma', 'approved-policy.json'),
2187
+ launcher: resolve(workspace, process.platform === 'win32' ? launcher.windows : launcher.shell),
2188
+ version: VERSION, preserveStandardAnchor: true });
2189
+ }));
2131
2190
  let sharedRepositoryReady = onboarded.sharedRepositoryReady === true;
2132
2191
  const completionRequested = flags.has('complete');
2133
2192
  if (!completionRequested) {
@@ -3885,6 +3944,38 @@ The CLI enrolls this device through browser-confirmed Clerk organization consent
3885
3944
  knowledge,
3886
3945
  };
3887
3946
  }
3947
+ async function repositoryInstallerRecoveryCommand(flags) {
3948
+ if (flags.has('apply') === flags.has('dry-run'))
3949
+ throw new Error('Choose exactly one of --apply or --dry-run.');
3950
+ const fromWorkspaceId = required(flags, 'from-workspace-id');
3951
+ const workspaceId = required(flags, 'workspace-id');
3952
+ const workspace = await realpath(String(flags.get('workspace') || '.'));
3953
+ const organizationId = required(flags, 'organization-id');
3954
+ const config = await readDeviceConfig();
3955
+ if (!config || config.organizationId !== organizationId)
3956
+ throw new Error('Recovery requires current organization enrollment.');
3957
+ await loadDeviceEnrollmentAnchor({ config });
3958
+ const identity = await preflightBootstrapWorkspaceIdentity(workspace);
3959
+ const registered = selectLegacyInstallerRecoveryWorkspace(await registry(), { organizationId, deviceId: config.deviceId,
3960
+ path: workspace, repositoryRemoteHash: identity.fingerprint, workspaceId });
3961
+ const repositoryAgentKey = `repo:${identity.fingerprint.slice('sha256:'.length, 'sha256:'.length + 24)}`;
3962
+ if (!registered?.repositoryBindingId || !registered.repositoryAgentId
3963
+ || registered.repositoryAgentKey !== repositoryAgentKey) {
3964
+ throw new Error('Recovery requires the enrolled device canonical repository binding.');
3965
+ }
3966
+ const policy = await loadVerifiedWorkspacePolicy(resolve(workspace, '.dharma', 'approved-policy.json'), registered.workspaceId);
3967
+ if (policy.organizationId !== organizationId || policy.serverAuthorization?.workspaceId !== registered.workspaceId) {
3968
+ throw new Error('Recovery requires a current signed workspace policy.');
3969
+ }
3970
+ await assertWorkspaceAuthorizationCurrent(registered.workspaceId, policy.serverAuthorization);
3971
+ const scope = { organizationId, workspaceId: registered.workspaceId,
3972
+ repositoryBindingId: registered.repositoryBindingId, repositoryAgentId: registered.repositoryAgentId };
3973
+ validateRepositorySourceAuthorization(await fetchRepositorySourceAuthorization(await client(), scope), scope, new Date());
3974
+ const recovery = await recoverLegacyRepositoryInstaller({ workspace, fromWorkspaceId,
3975
+ workspaceId: registered.workspaceId, repositoryAgentKey, apply: flags.has('apply') });
3976
+ return { ok: true, dryRun: !recovery.applied, serverMutation: false, recovery,
3977
+ nextAction: 'Resume pinned onboarding; recovery is not signed skill installation or activation.' };
3978
+ }
3888
3979
  async function repositorySnapshotCommand(flags, outputs) {
3889
3980
  if (flags.has('apply') && flags.has('dry-run'))
3890
3981
  throw new Error('Choose --apply or --dry-run, not both.');
@@ -5972,6 +6063,52 @@ async function takeCachedSkillUpdate(input) {
5972
6063
  }
5973
6064
  }
5974
6065
  async function relayStart(flags) {
6066
+ const anchor = resolve(required(flags, 'policy'));
6067
+ const config = await readDeviceConfig();
6068
+ if (!config)
6069
+ throw new Error('Relay requires current enrollment.');
6070
+ const declared = await loadOrganizationPolicy(anchor);
6071
+ const rows = await registry();
6072
+ const selected = rows.find(row => row.workspaceId === declared.serverAuthorization?.workspaceId
6073
+ && row.organizationId === config.organizationId && resolve(row.path, '.dharma', 'approved-policy.json') === anchor);
6074
+ if (!selected)
6075
+ throw new Error('Relay policy must be the canonical policy of one registered workspace.');
6076
+ const authorized = await selectRepositoryRelayRegistrations(rows, config, loadOrganizationPolicy);
6077
+ if (!authorized.some(row => row.workspaceId === selected.workspaceId && row.policyPath === anchor)) {
6078
+ throw new Error('Relay policy is not registered to this enrolled device.');
6079
+ }
6080
+ await loadVerifiedWorkspacePolicy(anchor, selected.workspaceId);
6081
+ const release = await acquireRelayProcessLease();
6082
+ const controller = new AbortController();
6083
+ const stop = () => controller.abort();
6084
+ process.once('SIGINT', stop);
6085
+ process.once('SIGTERM', stop);
6086
+ const serialized = serializeRelayWork();
6087
+ try {
6088
+ if (flags.has('once'))
6089
+ return await relayWorkspaceLoop(flags, controller.signal, serialized);
6090
+ await runRegisteredRepositoryRelays({ signal: controller.signal, list: async () => {
6091
+ const currentConfig = await readDeviceConfig();
6092
+ if (currentConfig?.organizationId !== config.organizationId || currentConfig.deviceId !== config.deviceId) {
6093
+ throw new Error('Relay enrollment changed.');
6094
+ }
6095
+ const records = JSON.parse(await readFile(workspaceRegistryPath(), 'utf8'));
6096
+ if (!Array.isArray(records))
6097
+ throw new Error('Invalid workspace registry.');
6098
+ return selectRepositoryRelayRegistrations(records, config, loadOrganizationPolicy);
6099
+ }, run: (row, signal) => relayWorkspaceLoop(new Map([...flags, ['policy', row.policyPath]]), signal, serialized),
6100
+ observe: event => process.stderr.write(`${JSON.stringify({ event: 'repository_relay', ...event })}\n`) });
6101
+ return { ok: true, stopped: true };
6102
+ }
6103
+ finally {
6104
+ controller.abort();
6105
+ process.removeListener('SIGINT', stop);
6106
+ process.removeListener('SIGTERM', stop);
6107
+ await release();
6108
+ }
6109
+ }
6110
+ async function relayWorkspaceLoop(flags, signal, serialized) {
6111
+ signal.throwIfAborted();
5975
6112
  const policyPath = resolve(required(flags, 'policy'));
5976
6113
  const declaredPolicy = await loadOrganizationPolicy(policyPath);
5977
6114
  const declaredWorkspaceId = declaredPolicy.serverAuthorization?.workspaceId;
@@ -5986,400 +6123,411 @@ async function relayStart(flags) {
5986
6123
  }
5987
6124
  let canonicalWorkspace = selectedWorkspace;
5988
6125
  let policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
6126
+ signal.throwIfAborted();
5989
6127
  const fabric = await client();
5990
6128
  const leaseSeconds = Number(flags.get('lease-seconds') || 120);
5991
6129
  const pollMs = Math.min(Math.max(Number(flags.get('poll-seconds') || 3), 1), 60) * 1_000;
5992
- const releaseRelayLease = await acquireRelayProcessLease();
5993
- try {
5994
- let stopping = false;
5995
- let stopPreparation;
5996
- const stop = () => { stopping = true; stopPreparation?.(); };
5997
- process.once('SIGINT', stop);
5998
- process.once('SIGTERM', stop);
5999
- let tasksCompleted = 0;
6000
- let lastProviderFailureCategory = null;
6001
- let taskTrajectoriesRecovered = 0;
6002
- let evidenceResponsesCompleted = 0;
6003
- let trajectorySyncsCompleted = 0;
6004
- let nextPolicyRefreshAt = 0;
6005
- let lastPollReceiptAt = 0;
6006
- let evidencePolicyFresh = false;
6007
- const repositorySourceWatcher = new RepositorySourceWatcher();
6008
- const blockedRepositorySourceRetry = new BlockedRepositorySourceRetry();
6009
- let repositorySourceBaselineAvailable = true;
6010
- let publishedLocalSnapshotHash = canonicalWorkspace.repositoryPackage?.publishedLocalSnapshotHash
6011
- || (canonicalWorkspace.repositoryPackage?.state === 'published'
6012
- && canonicalWorkspace.repositoryPackage.snapshotHash === canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash
6013
- ? canonicalWorkspace.repositoryPackage.snapshotHash : null);
6014
- if (canonicalWorkspace.repositoryPackage && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6015
- try {
6016
- const recovered = await recoverPublishedLocalSourceBaseline({
6017
- workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
6018
- workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6019
- repositoryAgentId: canonicalWorkspace.repositoryAgentId, record: canonicalWorkspace.repositoryPackage,
6020
- });
6021
- if (recovered && recovered !== publishedLocalSnapshotHash) {
6022
- publishedLocalSnapshotHash = recovered;
6023
- canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: {
6024
- ...canonicalWorkspace.repositoryPackage, localBaselineSnapshotHash: recovered,
6025
- publishedLocalSnapshotHash: recovered,
6026
- } };
6027
- await saveWorkspaceRecord(canonicalWorkspace);
6028
- }
6029
- }
6030
- catch {
6031
- repositorySourceBaselineAvailable = false;
6130
+ let stopping = signal.aborted;
6131
+ let stopPreparation;
6132
+ const stop = () => { stopping = true; stopPreparation?.(); };
6133
+ let tasksCompleted = 0;
6134
+ let lastProviderFailureCategory = null;
6135
+ let taskTrajectoriesRecovered = 0;
6136
+ let evidenceResponsesCompleted = 0;
6137
+ let trajectorySyncsCompleted = 0;
6138
+ let nextPolicyRefreshAt = 0;
6139
+ let lastPollReceiptAt = 0;
6140
+ let evidencePolicyFresh = false;
6141
+ const repositorySourceWatcher = new RepositorySourceWatcher();
6142
+ const blockedRepositorySourceRetry = new BlockedRepositorySourceRetry();
6143
+ let repositorySourceBaselineAvailable = true;
6144
+ let publishedLocalSnapshotHash = canonicalWorkspace.repositoryPackage?.publishedLocalSnapshotHash
6145
+ || (canonicalWorkspace.repositoryPackage?.state === 'published'
6146
+ && canonicalWorkspace.repositoryPackage.snapshotHash === canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash
6147
+ ? canonicalWorkspace.repositoryPackage.snapshotHash : null);
6148
+ if (canonicalWorkspace.repositoryPackage && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6149
+ try {
6150
+ const recovered = await recoverPublishedLocalSourceBaseline({
6151
+ workspace: canonicalWorkspace.path, organizationId: canonicalWorkspace.organizationId,
6152
+ workspaceId: canonicalWorkspace.workspaceId, repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6153
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId, record: canonicalWorkspace.repositoryPackage,
6154
+ });
6155
+ if (recovered && recovered !== publishedLocalSnapshotHash) {
6156
+ publishedLocalSnapshotHash = recovered;
6157
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: {
6158
+ ...canonicalWorkspace.repositoryPackage, localBaselineSnapshotHash: recovered,
6159
+ publishedLocalSnapshotHash: recovered,
6160
+ } };
6161
+ await saveWorkspaceRecord(canonicalWorkspace);
6032
6162
  }
6033
6163
  }
6034
- if (publishedLocalSnapshotHash) {
6035
- try {
6036
- await readRepositoryPackageSnapshot(canonicalWorkspace.path, publishedLocalSnapshotHash);
6037
- const localHash = canonicalWorkspace.repositoryPackage?.localBaselineSnapshotHash;
6038
- if (localHash && (localHash === publishedLocalSnapshotHash
6039
- || canonicalWorkspace.repositoryPackage?.pendingLocalSnapshotHash === localHash)) {
6040
- await seedRepositorySourceWatcher({ workspace: canonicalWorkspace.path,
6041
- organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6042
- repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6043
- repositoryAgentId: canonicalWorkspace.repositoryAgentId,
6044
- publishedHash: publishedLocalSnapshotHash,
6045
- localHash,
6046
- watcher: repositorySourceWatcher });
6047
- }
6048
- }
6049
- catch {
6050
- repositorySourceBaselineAvailable = false;
6164
+ catch {
6165
+ repositorySourceBaselineAvailable = false;
6166
+ }
6167
+ }
6168
+ if (publishedLocalSnapshotHash) {
6169
+ try {
6170
+ await readRepositoryPackageSnapshot(canonicalWorkspace.path, publishedLocalSnapshotHash);
6171
+ const localHash = canonicalWorkspace.repositoryPackage?.localBaselineSnapshotHash;
6172
+ if (localHash && (localHash === publishedLocalSnapshotHash
6173
+ || canonicalWorkspace.repositoryPackage?.pendingLocalSnapshotHash === localHash)) {
6174
+ await seedRepositorySourceWatcher({ workspace: canonicalWorkspace.path,
6175
+ organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6176
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6177
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId,
6178
+ publishedHash: publishedLocalSnapshotHash,
6179
+ localHash,
6180
+ watcher: repositorySourceWatcher });
6051
6181
  }
6052
6182
  }
6053
- let nextRepositorySourceScanAt = 0;
6054
- let repositorySourceCandidates = 0;
6055
- let repositorySourceFailures = 0;
6056
- let repositorySourceState = 'not_scanned';
6057
- let sourceScanFlight = null;
6058
- let sourceScanReady = null;
6059
- const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
6060
- const vault = await LocalVault.open({
6061
- root: resolve(dharmaHome(), 'vault'),
6062
- masterKey: await loadOrCreateVaultMasterKey(),
6063
- rawLocalDays: rawLocalRetentionDays(policy),
6064
- });
6065
- let skillPreparationsCompleted = 0;
6066
- let skillPreparationFailures = 0;
6067
- let skillActivationsCompleted = 0;
6068
- let skillActivationFailures = 0;
6069
- const skillActivationFailuresByProvider = {};
6070
- let nextSkillActivationAt = 0;
6071
- const skillPreparationPump = startSkillPreparationPump({
6072
- prepare: async (assertRunning) => {
6073
- await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
6074
- assertRunning();
6075
- const capability = await adapter.capability();
6076
- assertRunning();
6077
- if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6183
+ catch {
6184
+ repositorySourceBaselineAvailable = false;
6185
+ }
6186
+ }
6187
+ let nextRepositorySourceScanAt = 0;
6188
+ let repositorySourceCandidates = 0;
6189
+ let repositorySourceFailures = 0;
6190
+ let repositorySourceState = 'not_scanned';
6191
+ let sourceScanFlight = null;
6192
+ let sourceScanReady = null;
6193
+ const { LocalVault, loadOrCreateVaultMasterKey } = await loadVaultModule();
6194
+ const vault = await LocalVault.open({
6195
+ root: resolve(dharmaHome(), 'vault'),
6196
+ masterKey: await loadOrCreateVaultMasterKey(),
6197
+ rawLocalDays: rawLocalRetentionDays(policy),
6198
+ });
6199
+ if (signal.aborted)
6200
+ vault.close();
6201
+ signal.throwIfAborted();
6202
+ signal.addEventListener('abort', stop, { once: true });
6203
+ if (signal.aborted)
6204
+ stop();
6205
+ let skillPreparationsCompleted = 0;
6206
+ let skillPreparationFailures = 0;
6207
+ let skillActivationsCompleted = 0;
6208
+ let skillActivationFailures = 0;
6209
+ const skillActivationFailuresByProvider = {};
6210
+ let nextSkillActivationAt = 0;
6211
+ const skillPreparationPump = startSkillPreparationPump({
6212
+ prepare: async (assertRunning) => {
6213
+ await prepareProvidersIndependently(providerAdapters, assertRunning, async (adapter) => {
6214
+ assertRunning();
6215
+ const capability = await adapter.capability();
6216
+ assertRunning();
6217
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6218
+ return;
6219
+ const provider = adapter.providerId;
6220
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6221
+ provider, assertCurrent: assertRunning }, async () => {
6222
+ const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6223
+ provider, policyPath, fabric, automatic: true, assertRunning });
6224
+ if (!prepared) {
6225
+ assertRunning();
6078
6226
  return;
6079
- const provider = adapter.providerId;
6080
- await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6081
- provider, assertCurrent: assertRunning }, async () => {
6082
- const prepared = await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6083
- provider, policyPath, fabric, automatic: true, assertRunning });
6084
- if (!prepared) {
6085
- assertRunning();
6086
- return;
6087
- }
6088
- let retained = false;
6089
- try {
6090
- prepared.assertCurrent();
6091
- const repositoryAgentId = prepared.workspace.repositoryAgentId;
6092
- if (!repositoryAgentId)
6093
- throw new Error('Preparation cache requires an enrolled repository agent.');
6094
- const pendingRecord = await serializeSkillPreparationRecord({
6095
- schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
6096
- deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
6227
+ }
6228
+ let retained = false;
6229
+ try {
6230
+ prepared.assertCurrent();
6231
+ const repositoryAgentId = prepared.workspace.repositoryAgentId;
6232
+ if (!repositoryAgentId)
6233
+ throw new Error('Preparation cache requires an enrolled repository agent.');
6234
+ const pendingRecord = await serializeSkillPreparationRecord({
6235
+ schema: 'dharma.skill-preparation/v1', organizationId: prepared.config.organizationId,
6236
+ deviceId: prepared.config.deviceId, workspaceId: prepared.workspace.workspaceId,
6237
+ repositoryAgentId,
6238
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
6239
+ policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
6240
+ bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
6241
+ ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
6242
+ preparedAt: new Date().toISOString(), activationAuthorized: false,
6243
+ });
6244
+ prepared.assertCurrent();
6245
+ await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
6246
+ prepared.assertCurrent();
6247
+ await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6248
+ provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
6249
+ expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
6097
6250
  repositoryAgentId,
6098
- repositoryBindingId: prepared.workspace.repositoryBindingId ?? null, provider,
6251
+ repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
6099
6252
  policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
6100
- bundle: prepared.bundle, repositoryPackage: prepared.repositoryDelivery
6101
- ? { envelope: prepared.repositoryDelivery.envelope, index: prepared.repositoryDelivery.index } : null,
6102
- preparedAt: new Date().toISOString(), activationAuthorized: false,
6103
- });
6104
- prepared.assertCurrent();
6105
- await writeFile(resolve(prepared.sourceRoot, 'PREPARED.json'), pendingRecord, { mode: 0o600, flag: 'wx' });
6106
- prepared.assertCurrent();
6107
- await publishSkillPreparationCache({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6108
- provider, sourceRoot: prepared.sourceRoot, recordBytes: pendingRecord, assertCurrent: prepared.assertCurrent,
6109
- expected: { organizationId: prepared.config.organizationId, deviceId: prepared.config.deviceId,
6110
- repositoryAgentId,
6111
- repositoryBindingId: prepared.workspace.repositoryBindingId ?? null,
6112
- policyHash: sha256(canonicalize(prepared.policy)), rolloutId: prepared.rollout.id,
6113
- bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
6114
- onCommitted: () => { retained = true; } });
6115
- skillPreparationsCompleted += 1;
6116
- }
6117
- finally {
6118
- if (!retained)
6119
- await rm(prepared.sourceRoot, { recursive: true, force: true });
6120
- }
6121
- });
6122
- }, () => { skillPreparationFailures += 1; });
6123
- },
6124
- onError: () => { skillPreparationFailures += 1; },
6125
- });
6126
- stopPreparation = skillPreparationPump.requestStop;
6127
- if (stopping)
6128
- stopPreparation();
6129
- const applyReadySourceScan = async () => {
6130
- if (!sourceScanReady)
6131
- return;
6132
- const ready = sourceScanReady;
6133
- sourceScanReady = null;
6134
- sourceScanFlight = null;
6135
- if ('error' in ready) {
6136
- repositorySourceWatcher.invalidate();
6137
- repositorySourceFailures += 1;
6138
- repositorySourceState = 'blocked';
6139
- nextRepositorySourceScanAt = performance.now() + 60_000;
6140
- return;
6141
- }
6142
- if ('state' in ready) {
6143
- repositorySourceState = ready.state;
6144
- nextRepositorySourceScanAt = performance.now() + 15_000;
6145
- return;
6146
- }
6147
- const sourceCycle = ready.cycle;
6148
- repositorySourceState = sourceCycle.state;
6149
- if (sourceCycle.localMutation) {
6150
- repositorySourceCandidates += 1;
6151
- if (canonicalWorkspace.repositoryPackage && sourceCycle.persisted) {
6152
- canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = sourceCycle.persisted.snapshotHash;
6153
- if (sourceCycle.state === 'source_already_published') {
6253
+ bundleId: prepared.bundle.bundleId, bundleHash: prepared.bundle.bundleHash },
6254
+ onCommitted: () => { retained = true; } });
6255
+ skillPreparationsCompleted += 1;
6256
+ }
6257
+ finally {
6258
+ if (!retained)
6259
+ await rm(prepared.sourceRoot, { recursive: true, force: true });
6260
+ }
6261
+ });
6262
+ }, () => { skillPreparationFailures += 1; });
6263
+ },
6264
+ onError: () => { skillPreparationFailures += 1; },
6265
+ });
6266
+ stopPreparation = skillPreparationPump.requestStop;
6267
+ if (stopping)
6268
+ stopPreparation();
6269
+ const applyReadySourceScan = async () => {
6270
+ if (!sourceScanReady)
6271
+ return;
6272
+ const ready = sourceScanReady;
6273
+ sourceScanReady = null;
6274
+ sourceScanFlight = null;
6275
+ if ('error' in ready) {
6276
+ repositorySourceWatcher.invalidate();
6277
+ repositorySourceFailures += 1;
6278
+ repositorySourceState = 'blocked';
6279
+ nextRepositorySourceScanAt = performance.now() + 60_000;
6280
+ return;
6281
+ }
6282
+ if ('state' in ready) {
6283
+ repositorySourceState = ready.state;
6284
+ nextRepositorySourceScanAt = performance.now() + 15_000;
6285
+ return;
6286
+ }
6287
+ const sourceCycle = ready.cycle;
6288
+ repositorySourceState = sourceCycle.state;
6289
+ if (sourceCycle.localMutation) {
6290
+ repositorySourceCandidates += 1;
6291
+ if (canonicalWorkspace.repositoryPackage && sourceCycle.persisted) {
6292
+ canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = sourceCycle.persisted.snapshotHash;
6293
+ if (sourceCycle.state === 'source_already_published') {
6294
+ canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6295
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
6296
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
6297
+ publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6298
+ }
6299
+ else if (sourceCycle.candidate) {
6300
+ const receipt = sourceCycle.candidate;
6301
+ if (typeof receipt.candidateId === 'string' && typeof receipt.operationId === 'string'
6302
+ && typeof receipt.snapshotHash === 'string' && sourceCycle.submittedManifestHash) {
6303
+ Object.assign(canonicalWorkspace.repositoryPackage, {
6304
+ state: receipt.state, candidateId: receipt.candidateId, operationId: receipt.operationId,
6305
+ snapshotHash: receipt.snapshotHash, sourceManifestHash: sourceCycle.submittedManifestHash,
6306
+ releaseId: typeof receipt.releaseId === 'string' ? receipt.releaseId : null,
6307
+ consolidationMode: 'repository_update',
6308
+ });
6309
+ }
6310
+ if (receipt.state === 'published') {
6154
6311
  canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6155
6312
  canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
6156
6313
  canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
6157
6314
  publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6158
6315
  }
6159
- else if (sourceCycle.candidate) {
6160
- const receipt = sourceCycle.candidate;
6161
- if (typeof receipt.candidateId === 'string' && typeof receipt.operationId === 'string'
6162
- && typeof receipt.snapshotHash === 'string' && sourceCycle.submittedManifestHash) {
6163
- Object.assign(canonicalWorkspace.repositoryPackage, {
6164
- state: receipt.state, candidateId: receipt.candidateId, operationId: receipt.operationId,
6165
- snapshotHash: receipt.snapshotHash, sourceManifestHash: sourceCycle.submittedManifestHash,
6166
- releaseId: typeof receipt.releaseId === 'string' ? receipt.releaseId : null,
6167
- consolidationMode: 'repository_update',
6168
- });
6169
- }
6170
- if (receipt.state === 'published') {
6171
- canonicalWorkspace.repositoryPackage.publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6172
- canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = null;
6173
- canonicalWorkspace.repositoryPackage.pendingLocalOperationId = null;
6174
- publishedLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6175
- }
6176
- else if ((receipt.state === 'accepted' || receipt.state === 'processing')
6177
- && typeof receipt.operationId === 'string') {
6178
- canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6179
- canonicalWorkspace.repositoryPackage.pendingLocalOperationId = receipt.operationId;
6316
+ else if ((receipt.state === 'accepted' || receipt.state === 'processing')
6317
+ && typeof receipt.operationId === 'string') {
6318
+ canonicalWorkspace.repositoryPackage.pendingLocalSnapshotHash = sourceCycle.persisted.snapshotHash;
6319
+ canonicalWorkspace.repositoryPackage.pendingLocalOperationId = receipt.operationId;
6320
+ }
6321
+ else if (receipt.state === 'blocked') {
6322
+ canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = publishedLocalSnapshotHash;
6323
+ repositorySourceWatcher.invalidate();
6324
+ }
6325
+ }
6326
+ await saveWorkspaceRecord(canonicalWorkspace);
6327
+ }
6328
+ }
6329
+ nextRepositorySourceScanAt = performance.now() + (sourceCycle.state === 'debouncing' ? 15_000 : 60_000);
6330
+ };
6331
+ const consumeSourceScan = async () => {
6332
+ try {
6333
+ await applyReadySourceScan();
6334
+ }
6335
+ catch {
6336
+ repositorySourceWatcher.invalidate();
6337
+ repositorySourceFailures += 1;
6338
+ repositorySourceState = 'blocked';
6339
+ nextRepositorySourceScanAt = performance.now() + 60_000;
6340
+ }
6341
+ };
6342
+ try {
6343
+ signal.throwIfAborted();
6344
+ const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
6345
+ if (recoveredTaskTrajectories.state === 'completed') {
6346
+ taskTrajectoriesRecovered += recoveredTaskTrajectories.value.length;
6347
+ }
6348
+ do {
6349
+ if (stopping)
6350
+ break;
6351
+ await consumeSourceScan();
6352
+ if (Date.now() >= nextPolicyRefreshAt) {
6353
+ try {
6354
+ await syncWorkspacePolicy(fabric, canonicalWorkspace, policy.revision, true);
6355
+ policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
6356
+ evidencePolicyFresh = true;
6357
+ }
6358
+ catch {
6359
+ evidencePolicyFresh = false;
6360
+ }
6361
+ nextPolicyRefreshAt = Date.now() + 60_000;
6362
+ }
6363
+ if (!sourceScanFlight && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6364
+ try {
6365
+ const current = await pollRepositoryCandidate({ transport: fabric,
6366
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6367
+ scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6368
+ repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6369
+ repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
6370
+ if (current && canonicalWorkspace.repositoryPackage) {
6371
+ if (current.candidateId !== canonicalWorkspace.repositoryPackage.candidateId
6372
+ || current.operationId !== canonicalWorkspace.repositoryPackage.operationId) {
6373
+ throw new Error('Repository candidate status changed identity while polling.');
6180
6374
  }
6181
- else if (receipt.state === 'blocked') {
6182
- canonicalWorkspace.repositoryPackage.localBaselineSnapshotHash = publishedLocalSnapshotHash;
6375
+ const baseline = advanceRepositorySourceBaseline(canonicalWorkspace.repositoryPackage, current);
6376
+ canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
6377
+ state: current.state, candidateId: current.candidateId, operationId: current.operationId,
6378
+ snapshotHash: current.snapshotHash, releaseId: current.releaseId,
6379
+ ...baseline,
6380
+ generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
6381
+ publishedLocalSnapshotHash = baseline.publishedLocalSnapshotHash;
6382
+ if (current.state === 'blocked')
6183
6383
  repositorySourceWatcher.invalidate();
6184
- }
6384
+ await saveWorkspaceRecord(canonicalWorkspace);
6185
6385
  }
6186
- await saveWorkspaceRecord(canonicalWorkspace);
6386
+ }
6387
+ catch {
6388
+ repositorySourceFailures += 1;
6187
6389
  }
6188
6390
  }
6189
- nextRepositorySourceScanAt = performance.now() + (sourceCycle.state === 'debouncing' ? 15_000 : 60_000);
6190
- };
6191
- const consumeSourceScan = async () => {
6192
- try {
6193
- await applyReadySourceScan();
6194
- }
6195
- catch {
6196
- repositorySourceWatcher.invalidate();
6197
- repositorySourceFailures += 1;
6198
- repositorySourceState = 'blocked';
6199
- nextRepositorySourceScanAt = performance.now() + 60_000;
6391
+ let evidenceRequestId;
6392
+ if (evidencePolicyFresh && !stopping) {
6393
+ const evidenceCycle = await deferUnavailableRelayRetention(async () => {
6394
+ const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
6395
+ const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
6396
+ return {
6397
+ synced,
6398
+ evidenceRequestId: typeof evidence.requestId === 'string' ? evidence.requestId : undefined,
6399
+ };
6400
+ });
6401
+ if (evidenceCycle.state === 'deferred') {
6402
+ evidencePolicyFresh = false;
6403
+ }
6404
+ else {
6405
+ trajectorySyncsCompleted += evidenceCycle.value.synced;
6406
+ evidenceRequestId = evidenceCycle.value.evidenceRequestId;
6407
+ if (evidenceRequestId)
6408
+ evidenceResponsesCompleted += 1;
6409
+ }
6200
6410
  }
6201
- };
6202
- try {
6203
- const recoveredTaskTrajectories = await deferUnavailableRelayRetention(async () => finalizeRecoveredSignedTaskTrajectories(fabric, policy));
6204
- if (recoveredTaskTrajectories.state === 'completed') {
6205
- taskTrajectoriesRecovered += recoveredTaskTrajectories.value.length;
6411
+ const result = await serialized(async () => {
6412
+ if (stopping)
6413
+ return { taskId: null };
6414
+ return executeOneTask(fabric, leaseSeconds);
6415
+ });
6416
+ if (!stopping && Date.now() - lastPollReceiptAt >= 30_000) {
6417
+ const observation = {
6418
+ at: new Date().toISOString(), workspaceId: canonicalWorkspace.workspaceId, version: VERSION, pid: process.pid,
6419
+ };
6420
+ await writeJsonAtomic(resolve(dharmaHome(), 'relay', 'repositories', canonicalWorkspace.workspaceId, 'last-successful-poll.json'), observation);
6421
+ const binding = await readFile(resolve(dharmaHome(), 'relay', 'supervisor-workspace.json'), 'utf8')
6422
+ .then(value => JSON.parse(value)).catch(() => null);
6423
+ if (!binding || binding.workspaceId === canonicalWorkspace.workspaceId) {
6424
+ await writeJsonAtomic(resolve(dharmaHome(), 'relay', 'last-successful-poll.json'), observation);
6425
+ }
6426
+ lastPollReceiptAt = Date.now();
6206
6427
  }
6207
- do {
6208
- await consumeSourceScan();
6209
- if (Date.now() >= nextPolicyRefreshAt) {
6428
+ if (result.taskId)
6429
+ tasksCompleted += 1;
6430
+ if (typeof result.failureCategory === 'string')
6431
+ lastProviderFailureCategory = result.failureCategory;
6432
+ if (!stopping && !result.taskId && performance.now() >= nextSkillActivationAt) {
6433
+ for (const adapter of providerAdapters) {
6210
6434
  try {
6211
- await syncWorkspacePolicy(fabric, canonicalWorkspace, policy.revision, true);
6212
- policy = await loadVerifiedWorkspacePolicy(policyPath, canonicalWorkspace.workspaceId);
6213
- evidencePolicyFresh = true;
6435
+ const capability = await adapter.capability();
6436
+ if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6437
+ continue;
6438
+ const provider = adapter.providerId;
6439
+ await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
6440
+ await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6441
+ provider, assertCurrent: () => { if (stopping)
6442
+ throw new Error('Relay is stopping.'); } }, async () => {
6443
+ const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6444
+ provider, policyPath, fabric });
6445
+ const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6446
+ provider, policyPath, fabric, automatic: true });
6447
+ if (!prepared)
6448
+ return;
6449
+ if (!cached)
6450
+ skillPreparationsCompleted += 1;
6451
+ await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
6452
+ provider, policyPath, flags: new Map() });
6453
+ skillActivationsCompleted += 1;
6454
+ });
6455
+ });
6214
6456
  }
6215
6457
  catch {
6216
- evidencePolicyFresh = false;
6458
+ skillActivationFailures += 1;
6459
+ skillActivationFailuresByProvider[adapter.providerId] = (skillActivationFailuresByProvider[adapter.providerId] || 0) + 1;
6217
6460
  }
6218
- nextPolicyRefreshAt = Date.now() + 60_000;
6219
6461
  }
6220
- if (!sourceScanFlight && canonicalWorkspace.repositoryBindingId && canonicalWorkspace.repositoryAgentId) {
6462
+ nextSkillActivationAt = performance.now() + 60_000;
6463
+ }
6464
+ if (!stopping && !sourceScanFlight && performance.now() >= nextRepositorySourceScanAt) {
6465
+ const sourceWorkspace = canonicalWorkspace;
6466
+ const sourceBaselineHash = publishedLocalSnapshotHash;
6467
+ sourceScanFlight = (async () => {
6221
6468
  try {
6222
- const current = await pollRepositoryCandidate({ transport: fabric,
6223
- outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6224
- scope: { organizationId: canonicalWorkspace.organizationId, workspaceId: canonicalWorkspace.workspaceId,
6225
- repositoryBindingId: canonicalWorkspace.repositoryBindingId,
6226
- repositoryAgentId: canonicalWorkspace.repositoryAgentId } });
6227
- if (current && canonicalWorkspace.repositoryPackage) {
6228
- if (current.candidateId !== canonicalWorkspace.repositoryPackage.candidateId
6229
- || current.operationId !== canonicalWorkspace.repositoryPackage.operationId) {
6230
- throw new Error('Repository candidate status changed identity while polling.');
6231
- }
6232
- const baseline = advanceRepositorySourceBaseline(canonicalWorkspace.repositoryPackage, current);
6233
- canonicalWorkspace = { ...canonicalWorkspace, repositoryPackage: { ...canonicalWorkspace.repositoryPackage,
6234
- state: current.state, candidateId: current.candidateId, operationId: current.operationId,
6235
- snapshotHash: current.snapshotHash, releaseId: current.releaseId,
6236
- ...baseline,
6237
- generation: current.state === 'published' ? Math.max(1, canonicalWorkspace.repositoryPackage.generation) : canonicalWorkspace.repositoryPackage.generation } };
6238
- publishedLocalSnapshotHash = baseline.publishedLocalSnapshotHash;
6239
- if (current.state === 'blocked')
6240
- repositorySourceWatcher.invalidate();
6241
- await saveWorkspaceRecord(canonicalWorkspace);
6469
+ if (!repositorySourceBaselineAvailable) {
6470
+ throw new Error('Repository source baseline is unavailable; refusing an unanchored update.');
6242
6471
  }
6243
- }
6244
- catch {
6245
- repositorySourceFailures += 1;
6246
- }
6247
- }
6248
- let evidenceRequestId;
6249
- if (evidencePolicyFresh) {
6250
- const evidenceCycle = await deferUnavailableRelayRetention(async () => {
6251
- const synced = await syncPendingRetentionCapsules(vault, fabric, policy, canonicalWorkspace.workspaceId);
6252
- const evidence = await processEvidenceRequest(fabric, policy, canonicalWorkspace.workspaceId);
6253
- return {
6254
- synced,
6255
- evidenceRequestId: typeof evidence.requestId === 'string' ? evidence.requestId : undefined,
6256
- };
6257
- });
6258
- if (evidenceCycle.state === 'deferred') {
6259
- evidencePolicyFresh = false;
6260
- }
6261
- else {
6262
- trajectorySyncsCompleted += evidenceCycle.value.synced;
6263
- evidenceRequestId = evidenceCycle.value.evidenceRequestId;
6264
- if (evidenceRequestId)
6265
- evidenceResponsesCompleted += 1;
6266
- }
6267
- }
6268
- const result = await executeOneTask(fabric, leaseSeconds);
6269
- if (Date.now() - lastPollReceiptAt >= 30_000) {
6270
- await writeJsonAtomic(resolve(dharmaHome(), 'relay', 'last-successful-poll.json'), {
6271
- at: new Date().toISOString(), workspaceId: canonicalWorkspace.workspaceId, version: VERSION, pid: process.pid,
6272
- });
6273
- lastPollReceiptAt = Date.now();
6274
- }
6275
- if (result.taskId)
6276
- tasksCompleted += 1;
6277
- if (typeof result.failureCategory === 'string')
6278
- lastProviderFailureCategory = result.failureCategory;
6279
- if (!result.taskId && performance.now() >= nextSkillActivationAt) {
6280
- for (const adapter of providerAdapters) {
6281
- try {
6282
- const capability = await adapter.capability();
6283
- if (capability.skillInstall === 'unavailable' || !isLocalProviderId(adapter.providerId))
6284
- continue;
6285
- const provider = adapter.providerId;
6286
- await withWorkspaceSkillActivationLock(canonicalWorkspace.workspaceId, provider, async () => {
6287
- await withSkillPreparationTransaction({ home: dharmaHome(), workspaceId: canonicalWorkspace.workspaceId,
6288
- provider, assertCurrent: () => { if (stopping)
6289
- throw new Error('Relay is stopping.'); } }, async () => {
6290
- const cached = await takeCachedSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6291
- provider, policyPath, fabric });
6292
- const prepared = cached || await prepareSkillUpdate({ workspaceId: canonicalWorkspace.workspaceId,
6293
- provider, policyPath, fabric, automatic: true });
6294
- if (!prepared)
6295
- return;
6296
- if (!cached)
6297
- skillPreparationsCompleted += 1;
6298
- await activatePreparedSkillUpdate({ prepared, workspaceId: canonicalWorkspace.workspaceId,
6299
- provider, policyPath, flags: new Map() });
6300
- skillActivationsCompleted += 1;
6301
- });
6302
- });
6472
+ if (!sourceWorkspace.repositoryBindingId || !sourceWorkspace.repositoryAgentId) {
6473
+ throw new Error('Repository source synchronization requires a complete repository binding.');
6303
6474
  }
6304
- catch {
6305
- skillActivationFailures += 1;
6306
- skillActivationFailuresByProvider[adapter.providerId] = (skillActivationFailuresByProvider[adapter.providerId] || 0) + 1;
6475
+ const installedKnowledge = await installedRepositoryKnowledge(sourceWorkspace);
6476
+ if (!installedKnowledge && (sourceWorkspace.repositoryPackage?.generation ?? 0) > 0) {
6477
+ sourceScanReady = { state: 'awaiting_signed_baseline' };
6307
6478
  }
6308
- }
6309
- nextSkillActivationAt = performance.now() + 60_000;
6310
- }
6311
- if (!sourceScanFlight && performance.now() >= nextRepositorySourceScanAt) {
6312
- const sourceWorkspace = canonicalWorkspace;
6313
- const sourceBaselineHash = publishedLocalSnapshotHash;
6314
- sourceScanFlight = (async () => {
6315
- try {
6316
- if (!repositorySourceBaselineAvailable) {
6317
- throw new Error('Repository source baseline is unavailable; refusing an unanchored update.');
6318
- }
6319
- if (!sourceWorkspace.repositoryBindingId || !sourceWorkspace.repositoryAgentId) {
6320
- throw new Error('Repository source synchronization requires a complete repository binding.');
6479
+ else {
6480
+ if (sourceWorkspace.repositoryPackage?.state === 'blocked' && sourceWorkspace.repositoryPackage.snapshotHash) {
6481
+ const blocked = await readRepositorySourceBaselineSnapshot(sourceWorkspace.path, sourceWorkspace.repositoryPackage.snapshotHash);
6482
+ if (blockedRepositorySourceRetry.consider(sourceWorkspace.repositoryPackage.candidateId ?? '', blocked.manifest.knowledge, installedKnowledge))
6483
+ repositorySourceWatcher.invalidate();
6321
6484
  }
6322
- const installedKnowledge = await installedRepositoryKnowledge(sourceWorkspace);
6323
- if (!installedKnowledge && (sourceWorkspace.repositoryPackage?.generation ?? 0) > 0) {
6324
- sourceScanReady = { state: 'awaiting_signed_baseline' };
6325
- }
6326
- else {
6327
- if (sourceWorkspace.repositoryPackage?.state === 'blocked' && sourceWorkspace.repositoryPackage.snapshotHash) {
6328
- const blocked = await readRepositorySourceBaselineSnapshot(sourceWorkspace.path, sourceWorkspace.repositoryPackage.snapshotHash);
6329
- if (blockedRepositorySourceRetry.consider(sourceWorkspace.repositoryPackage.candidateId ?? '', blocked.manifest.knowledge, installedKnowledge))
6330
- repositorySourceWatcher.invalidate();
6331
- }
6332
- const sourceCycle = await scanRepositorySourceChanges({
6333
- workspace: sourceWorkspace.path, organizationId: sourceWorkspace.organizationId,
6334
- workspaceId: sourceWorkspace.workspaceId, repositoryBindingId: sourceWorkspace.repositoryBindingId,
6335
- repositoryAgentId: sourceWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
6336
- loadRetainedKnowledge: () => installedRepositoryKnowledge(sourceWorkspace),
6337
- loadPublishedLocalBaseline: () => sourceBaselineHash
6338
- ? readRepositoryPackageSnapshot(sourceWorkspace.path, sourceBaselineHash) : Promise.resolve(null),
6339
- submitCandidate: (snapshot, expectedLatestSourceFingerprint) => synchronizeRepositoryCandidate({ transport: fabric,
6340
- outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6341
- scope: { organizationId: sourceWorkspace.organizationId,
6342
- workspaceId: sourceWorkspace.workspaceId,
6343
- repositoryBindingId: sourceWorkspace.repositoryBindingId,
6344
- repositoryAgentId: sourceWorkspace.repositoryAgentId },
6345
- snapshot, initialRepository: false, expectedLatestSourceFingerprint }),
6346
- });
6347
- sourceScanReady = { cycle: sourceCycle };
6348
- }
6349
- }
6350
- catch (error) {
6351
- sourceScanReady = { error };
6485
+ const sourceCycle = await scanRepositorySourceChanges({
6486
+ workspace: sourceWorkspace.path, organizationId: sourceWorkspace.organizationId,
6487
+ workspaceId: sourceWorkspace.workspaceId, repositoryBindingId: sourceWorkspace.repositoryBindingId,
6488
+ repositoryAgentId: sourceWorkspace.repositoryAgentId, transport: fabric, watcher: repositorySourceWatcher,
6489
+ loadRetainedKnowledge: () => installedRepositoryKnowledge(sourceWorkspace),
6490
+ loadPublishedLocalBaseline: () => sourceBaselineHash
6491
+ ? readRepositoryPackageSnapshot(sourceWorkspace.path, sourceBaselineHash) : Promise.resolve(null),
6492
+ submitCandidate: (snapshot, expectedLatestSourceFingerprint) => synchronizeRepositoryCandidate({ transport: fabric,
6493
+ outboxRoot: resolve(dharmaHome(), 'relay', 'repository-candidates'),
6494
+ scope: { organizationId: sourceWorkspace.organizationId,
6495
+ workspaceId: sourceWorkspace.workspaceId,
6496
+ repositoryBindingId: sourceWorkspace.repositoryBindingId,
6497
+ repositoryAgentId: sourceWorkspace.repositoryAgentId },
6498
+ snapshot, initialRepository: false, expectedLatestSourceFingerprint }),
6499
+ });
6500
+ sourceScanReady = { cycle: sourceCycle };
6352
6501
  }
6353
- })();
6354
- }
6355
- if (flags.has('once'))
6356
- break;
6357
- if (!result.taskId && !evidenceRequestId)
6358
- await new Promise((accept) => setTimeout(accept, pollMs));
6359
- } while (!stopping);
6360
- }
6361
- finally {
6362
- if (sourceScanFlight)
6363
- await sourceScanFlight;
6364
- await consumeSourceScan();
6365
- await skillPreparationPump.stop();
6366
- vault.close();
6367
- process.removeListener('SIGINT', stop);
6368
- process.removeListener('SIGTERM', stop);
6369
- }
6370
- return {
6371
- ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
6372
- lastProviderFailureCategory,
6373
- evidenceResponsesCompleted, trajectorySyncsCompleted,
6374
- repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
6375
- skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
6376
- skillActivationFailuresByProvider,
6377
- sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
6378
- };
6502
+ }
6503
+ catch (error) {
6504
+ sourceScanReady = { error };
6505
+ }
6506
+ })();
6507
+ }
6508
+ if (flags.has('once'))
6509
+ break;
6510
+ if (!result.taskId && !evidenceRequestId)
6511
+ await waitForRelayRefresh(pollMs, signal);
6512
+ } while (!stopping);
6379
6513
  }
6380
6514
  finally {
6381
- await releaseRelayLease();
6515
+ if (sourceScanFlight)
6516
+ await sourceScanFlight;
6517
+ await consumeSourceScan();
6518
+ await skillPreparationPump.stop();
6519
+ vault.close();
6520
+ signal.removeEventListener('abort', stop);
6382
6521
  }
6522
+ return {
6523
+ ok: true, stopped: true, tasksCompleted, taskTrajectoriesRecovered,
6524
+ lastProviderFailureCategory,
6525
+ evidenceResponsesCompleted, trajectorySyncsCompleted,
6526
+ repositorySourceCandidates, repositorySourceFailures, repositorySourceState,
6527
+ skillPreparationsCompleted, skillPreparationFailures, skillActivationsCompleted, skillActivationFailures,
6528
+ skillActivationFailuresByProvider,
6529
+ sharedRepositoryReady: await repositorySharedReady(canonicalWorkspace),
6530
+ };
6383
6531
  }
6384
6532
  export async function run(argv) {
6385
6533
  const { positional, flags, repeated } = parseCliOptions(argv);
@@ -6571,6 +6719,8 @@ export async function run(argv) {
6571
6719
  }
6572
6720
  if (command === 'repositories' && subcommand === 'list')
6573
6721
  return repositoriesList(flags);
6722
+ if (command === 'repositories' && subcommand === 'recover-installer')
6723
+ return repositoryInstallerRecoveryCommand(flags);
6574
6724
  if (command === 'repositories' && subcommand === 'snapshot') {
6575
6725
  return repositorySnapshotCommand(flags, repeated.get('approved-output') || []);
6576
6726
  }
@@ -6624,6 +6774,25 @@ export async function run(argv) {
6624
6774
  try {
6625
6775
  const config = JSON.parse(await readFile(configPath(), 'utf8'));
6626
6776
  const status = { version: VERSION, enrolled: true, relay, supervisor, autostart, reconnect };
6777
+ if (flags.has('verbose') || flags.has('diagnostic')) {
6778
+ const relayPid = await readFile(resolve(dharmaHome(), 'relay', 'relay.pid'), 'utf8')
6779
+ .then(value => Number(value.trim())).catch(() => 0);
6780
+ try {
6781
+ const registered = await selectRepositoryRelayRegistrations(await registry(), config, loadOrganizationPolicy);
6782
+ const observations = [];
6783
+ for (const row of registered) {
6784
+ const observation = await readFile(resolve(dharmaHome(), 'relay', 'repositories', row.workspaceId, 'last-successful-poll.json'), 'utf8').then(value => JSON.parse(value)).catch(() => null);
6785
+ observations.push({ workspaceId: row.workspaceId,
6786
+ state: relay === 'running' && repositoryRelayObservationReady({ observation,
6787
+ workspaceId: row.workspaceId, version: VERSION, pid: relayPid }) ? 'acknowledged_recently' : 'pending',
6788
+ lastSuccessfulPollAt: typeof observation?.at === 'string' ? observation.at : null });
6789
+ }
6790
+ status.repositoryRelays = { state: 'observed', repositories: observations };
6791
+ }
6792
+ catch {
6793
+ status.repositoryRelays = { state: 'unavailable', repositories: [] };
6794
+ }
6795
+ }
6627
6796
  if (flags.has('verbose') || flags.has('diagnostic')) {
6628
6797
  Object.assign(status, { home: dharmaHome(), organizationId: config.organizationId, deviceId: config.deviceId });
6629
6798
  }